first commit
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# WrNexus database system — complete design & plan
|
||||
|
||||
Goal: a batteries-included data layer — **schema, migrations, a sqlc-like typed
|
||||
query generator, pluggable adapters (Postgres / MySQL / SQLite / MongoDB / …),
|
||||
and tools to run queries and map results back into (validated) schemas.**
|
||||
|
||||
Reuses what already exists: the `@wrnexus/validation` `v` builder (for field
|
||||
types + coercion + validation), the config/CLI/asset plumbing, and the
|
||||
per-request `Context` (for `ctx.db`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Packages
|
||||
|
||||
- `@wrnexus/db` — core: model/schema layer, the `Driver` interface, connection
|
||||
management, the query runtime, result→schema mapping, migration runner.
|
||||
- `@wrnexus/db-postgres`, `@wrnexus/db-mysql`, `@wrnexus/db-sqlite`,
|
||||
`@wrnexus/db-mongo` — thin adapter packages (each wraps a proven driver:
|
||||
`postgres`/`pg`, `mysql2`, `bun:sqlite`, `mongodb`). Installed on demand; the
|
||||
core stays dependency-free.
|
||||
- CLI verbs in `@wrnexus/cli`: `wrnexus db migrate|rollback|status|new|generate|studio`.
|
||||
|
||||
## 2. Schema / models (`app/db/schema.ts`)
|
||||
|
||||
Models are defined once, in TS, layered on the validation builder so the SAME
|
||||
definition drives DDL, query result typing, and input validation:
|
||||
|
||||
```ts
|
||||
import { table, v } from "@wrnexus/db";
|
||||
export const users = table("users", {
|
||||
id: v.id(), // pk, auto (serial / uuid per adapter)
|
||||
email: v.string().email().unique(),
|
||||
name: v.string().min(1),
|
||||
age: v.number().int().optional(),
|
||||
createdAt: v.timestamp().default("now"),
|
||||
});
|
||||
```
|
||||
|
||||
`table()` returns a Model with `.columns`, `.parse(row)` (coerce+validate a DB
|
||||
row into a typed object), `.describe()` (for migrations), and adapter-agnostic
|
||||
metadata (indexes, uniques, relations via `v.ref(users.id)`).
|
||||
|
||||
## 3. Adapters (`Driver` interface)
|
||||
|
||||
The core talks to every database through one small interface, so queries and
|
||||
migrations are portable where possible:
|
||||
|
||||
```ts
|
||||
interface Driver {
|
||||
connect(url: string): Promise<Connection>;
|
||||
dialect: "postgres" | "mysql" | "sqlite" | "mongo";
|
||||
query(sql: string, params: unknown[]): Promise<Row[]>; // SQL adapters
|
||||
exec(sql: string, params: unknown[]): Promise<{ rowCount: number }>;
|
||||
transaction<T>(fn: (tx: Connection) => Promise<T>): Promise<T>;
|
||||
// Mongo adapter implements a parallel collection API (find/insert/update/...)
|
||||
// behind the same Model layer instead of SQL.
|
||||
}
|
||||
```
|
||||
|
||||
SQL adapters share one SQL builder/renderer with dialect quirks (`$1` vs `?`,
|
||||
`RETURNING`, upserts). Mongo maps Model operations to collection calls. Connection
|
||||
pooling + lifecycle handled by the core; `ctx.db` is a per-request handle,
|
||||
`getDb()` an app-wide singleton.
|
||||
|
||||
## 4. Migrations (`app/db/migrations/*`)
|
||||
|
||||
Versioned, timestamped files with `up`/`down`. Two authoring modes:
|
||||
|
||||
- **SQL** — `0001_init.sql` (a `-- +up` / `-- +down` split), run as-is.
|
||||
- **From models** — `wrnexus db new --from-models` diffs `schema.ts` against the
|
||||
last applied state and emits the SQL for you to review.
|
||||
|
||||
`wrnexus db migrate` applies pending migrations inside a transaction and records
|
||||
them in a `_wire_migrations` table; `rollback` runs `down`; `status` lists state.
|
||||
Dialect-aware SQL generation for create/alter/index.
|
||||
|
||||
## 5. sqlc-like typed queries (`app/db/queries/*.sql` → generated TS)
|
||||
|
||||
Write annotated SQL; `wrnexus db generate` produces typed functions whose
|
||||
parameters and results are checked, and whose rows are mapped through the model
|
||||
schemas:
|
||||
|
||||
```sql
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT * FROM users WHERE email = :email;
|
||||
|
||||
-- name: ListActiveUsers :many
|
||||
SELECT id, name FROM users WHERE age >= :minAge ORDER BY name;
|
||||
|
||||
-- name: CreateUser :exec
|
||||
INSERT INTO users (email, name) VALUES (:email, :name);
|
||||
```
|
||||
|
||||
→ `app/db/queries.gen.ts`:
|
||||
|
||||
```ts
|
||||
export const GetUserByEmail = (db, args: { email: string }): Promise<User | null> => …
|
||||
export const ListActiveUsers = (db, args: { minAge: number }): Promise<Pick<User,"id"|"name">[]> => …
|
||||
```
|
||||
|
||||
The generator parses the SQL (params from `:name`, result columns), infers types
|
||||
from the referenced models, renders dialect-specific SQL, and wraps execution +
|
||||
`model.parse` result mapping. `:one|:many|:exec` set the return shape. For Mongo,
|
||||
the equivalent is a typed collection API (`db.users.find(...)`), since raw SQL
|
||||
doesn't apply.
|
||||
|
||||
## 6. Running queries + result → schema
|
||||
|
||||
Every result is coerced/validated through the model's `.parse` (same engine as
|
||||
form/API validation), so the data you get back matches your schema or throws a
|
||||
clear error. Helpers: `db.one/all/exec`, transactions via `db.tx(async t => …)`,
|
||||
and the generated functions above.
|
||||
|
||||
## 7. Config & security
|
||||
|
||||
- `wrnexus.config.ts` `db: { driver: "postgres", url: process.env.DATABASE_URL }`
|
||||
(multiple named connections supported). Never hard-code secrets.
|
||||
- All queries are **parameterized** (no string interpolation of input); the
|
||||
generator forbids raw interpolation. Migrations run only from disk files.
|
||||
- `ctx.db` in pages/API/middleware; pooled; closed on shutdown.
|
||||
|
||||
## 8. `wrnexus db studio` (optional, later)
|
||||
|
||||
A local Artifact/web UI to browse tables + run read-only queries in dev.
|
||||
|
||||
---
|
||||
|
||||
## Phased implementation plan (each phase: built, tested, documented)
|
||||
|
||||
1. **Core + SQLite adapter** — `@wrnexus/db`, `table()`/`v` model layer, `Driver`
|
||||
interface, `bun:sqlite` adapter, `db.one/all/exec/tx`, result→schema mapping.
|
||||
(SQLite first = zero external service to test against.)
|
||||
2. **Migrations** ✅ done — runner + `_wire_migrations`, `wrnexus db migrate/rollback/status/new`,
|
||||
`.sql` (`-- +up`/`-- +down`) + `--from-models` DDL generation. (Model-diffing beyond the
|
||||
initial CREATE TABLE is a later enhancement; needs topological ordering by FK refs.)
|
||||
3. **sqlc-like generator** ✅ done — `.sql` parsing (`-- name: X :one/:many/:exec`),
|
||||
param/result type inference from models (comparisons, INSERT column lists, SELECT
|
||||
list, aggregates → number), `wrnexus db generate` + auto-regen in `wrnexus build`.
|
||||
(Postgres `$N` placeholders + `::casts` handled when the pg adapter lands.)
|
||||
4. **Postgres + MySQL adapters** ✅ done — built on Bun's native `Bun.SQL` (zero
|
||||
external driver), `@wrnexus/db/postgres` + `/mysql`. Driver logic validated via
|
||||
Bun.sql's SQLite backend; dialect DDL (`SERIAL`/`BOOLEAN`/`JSONB`) + generator
|
||||
`$N` placeholders verified. Live connect/query needs a running server to test.
|
||||
5. **MongoDB adapter** ✅ built (unverified) — `@wrnexus/db/mongo`: a document
|
||||
collection API (`find/findOne/insert/update/delete/count`) keyed by models, reads
|
||||
coerced through `model.parse`, `_id`→`id`. Lazily imports `mongodb` (optional dep).
|
||||
Not wired into the SQL singleton/migrations/generator (Mongo isn't SQL). Needs
|
||||
`mongodb` + a server to test.
|
||||
6. **Polish** — ✅ FK-topological ordering in `--from-models` (referenced tables
|
||||
first). Deferred: `db studio` (web UI), richer relations/joins helpers.
|
||||
|
||||
### Transaction model (refactor)
|
||||
|
||||
`Driver` now exposes `transaction(fn)` (single reserved connection) instead of
|
||||
`begin/commit/rollback`, so pooled drivers (Bun.sql) keep a transaction on one
|
||||
connection. `Db.tx` opens it; nested `tx` reuses the current one.
|
||||
|
||||
### Adapter resolution
|
||||
|
||||
`@wrnexus/db/connect` `connectFromConfig({driver,url}, appRoot?)` maps a driver name
|
||||
to its adapter (sqlite/postgres/mysql) and resolves `file:` URLs against the app
|
||||
root. Used by `wrnexus db`, the dev server, and prod.
|
||||
|
||||
## Open decisions (need your call before phase 1)
|
||||
|
||||
- **Query authoring:** sqlc-style annotated `.sql` + generator (recommended, matches
|
||||
your ask) vs a fluent TS query builder vs support both.
|
||||
- **Schema source of truth:** TS models (`schema.ts`, recommended — one definition
|
||||
for DDL + validation + result typing) vs raw SQL DDL (`schema.sql`, closer to
|
||||
sqlc) vs both.
|
||||
- **First adapter to build:** SQLite (recommended — testable with no server) vs
|
||||
Postgres vs MySQL vs Mongo.
|
||||
@@ -0,0 +1,102 @@
|
||||
# WrNexus batteries-included: design & roadmap
|
||||
|
||||
Goal: let users start faster with a built-in UI library, theming, i18n, and
|
||||
validation. Built **foundation-first** so each subsystem lands solid and tested.
|
||||
|
||||
Order (one subsystem per turn):
|
||||
|
||||
1. **Theme system** ✅ done — design tokens, SSR + client, no-flash, overridable.
|
||||
2. **Wire UI** (`@wrnexus/ui`) ✅ done — 17 components + layout primitives as `.wrn`,
|
||||
consuming theme tokens, overridable 4 ways; auto-discovered + `wrnexus eject`.
|
||||
3. **Layout system** ✅ done (folded into turn 2) — layout primitives (Container,
|
||||
Stack, HStack, Grid, Divider, Spacer) + page layouts (`app/layout.wrn` + `<slot>`).
|
||||
4. **Validation** ✅ done — one schema for both form (client) and API (server).
|
||||
5. **i18n** ✅ done — translations for pages and API responses.
|
||||
|
||||
## 4. i18n (done)
|
||||
|
||||
- `@wrnexus/i18n`: locales in `app/locales/<lang>.json` (nested/flat keys, `{param}`
|
||||
interpolation). Per request the lang resolves from the `wire-lang` cookie →
|
||||
Accept-Language → config default (`wrnexus.config.ts` `i18n.default`).
|
||||
- **Server:** `ctx.lang` + `ctx.t(key, params)` on every request (pages + API).
|
||||
- **Views:** `{t:key}` text sugar compiles to a `<span data-t="key">` marker;
|
||||
`t:<attr>="key"` translates an attribute. `translateHtml` resolves both on the
|
||||
final HTML (after components + layout), and `<html lang>` is set (no flash).
|
||||
- **Switch:** `[data-wire-lang-set="es"]` (or `<select data-wire-lang>`) → the tiny
|
||||
`/__wrnexus/i18n.js` sets the cookie + reloads; injected only when present.
|
||||
- Fallback chain: current lang → default lang → the key itself.
|
||||
|
||||
All four subsystems + the layout system are complete.
|
||||
|
||||
## 3. Validation (done)
|
||||
|
||||
- `@wrnexus/validation`: fluent `v` builder (`v.object({ email: v.string().email(),
|
||||
password: v.string().min(8) })`). `.parse(data)` runs server-side (coerce + errors);
|
||||
`.describe()` emits a JSON descriptor. Shared `checkField`/`applyRule` back both.
|
||||
- Define once in `app/schemas/<name>.ts`. **API:** `import login from "../schemas/login";
|
||||
parseBody(login, ctx.req)` → 400 `{errors}` or `{value}`. **Form:** `<form
|
||||
data-schema="login">` with `[data-error="field"]` spans.
|
||||
- The framework bakes all descriptors into `/__wrnexus/schemas.js`
|
||||
(`window.__wireSchemas`) and ships an eval-free `/__wrnexus/validate.js` that validates
|
||||
`form[data-schema]` on submit/blur — injected only when a page has `data-schema`.
|
||||
|
||||
## 2 + 5. Wire UI + layout (done)
|
||||
|
||||
- **Distribution:** `@wrnexus/ui` ships `.wrn` components under `components/`; the
|
||||
framework auto-discovers them (router `componentDirs`), app/components shadow by
|
||||
name, and `wrnexus eject <name>` copies one into the app to own it.
|
||||
- **Codegen:** components now bake prop-driven text/attributes server-side
|
||||
(`__wireHtml`/`__wireAttr`) so static components ship zero JS; state-referencing
|
||||
text stays a reactive mustache. Reserved-word props (`class`) get `__p_` refs;
|
||||
attribute `{expr}` enables `variant`/`size`/`class` composition.
|
||||
- **Slots:** `renderComponents` is nesting-aware and fills `<slot>` with mount
|
||||
children — this also powers page layouts (`app/layout.wrn` wraps every page).
|
||||
- **Styles:** one themed stylesheet `/__wrnexus/ui.css` (all `.wire-*` classes use
|
||||
`var(--wire-*)`), linked theme → ui → app so app CSS overrides win.
|
||||
|
||||
---
|
||||
|
||||
## 1. Theme system (this turn)
|
||||
|
||||
**Tokens.** Kebab-case token keys become `--wire-<key>` CSS custom properties.
|
||||
Framework ships default `light` + `dark` token sets (colors, radius, etc.); user
|
||||
config deep-merges over them and may add new themes.
|
||||
|
||||
**Config** (`wrnexus.config.ts`):
|
||||
|
||||
```ts
|
||||
theme: {
|
||||
default: "dark",
|
||||
themes: {
|
||||
light: { "color-primary": "#2563eb", "radius": "8px", ... },
|
||||
dark: { "color-primary": "#6c8cff", ... },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Generated CSS** (`/__wrnexus/theme.css`), linked first in `<head>` so global.css
|
||||
and component styles can read/override it:
|
||||
|
||||
```css
|
||||
:root{ /* default theme tokens */ }
|
||||
[data-theme="light"]{ --wire-color-primary:#2563eb; color-scheme:light; ... }
|
||||
[data-theme="dark"]{ --wire-color-primary:#6c8cff; color-scheme:dark; ... }
|
||||
```
|
||||
|
||||
**SSR (no flash).** Server reads the `wire-theme` cookie (falls back to config
|
||||
default) and renders `<html data-theme="…">`, so the correct theme paints on the
|
||||
first byte. The theme name is validated against configured names.
|
||||
|
||||
**Client** (`/__wrnexus/theme.js`, injected only when a page has a toggle):
|
||||
`window.wireTheme.{get,set,toggle}` — sets `data-theme` on `<html>`, persists the
|
||||
`wire-theme` cookie, and binds `[data-wire-theme-toggle]` /
|
||||
`[data-wire-theme-set="name"]` elements so a `.wrn` component can switch themes
|
||||
without writing JS.
|
||||
|
||||
**Override.** Users change tokens in config, or redefine any `--wire-*` variable
|
||||
in their own CSS (loaded after theme.css → wins). Wire UI components consume
|
||||
`var(--wire-*)`, so overriding a token restyles every component at once.
|
||||
|
||||
**Integration points:** `@wrnexus/styles` (theme module), `@wrnexus/ssr`
|
||||
(`renderDocument` html attrs), dev runtime/assets, prod server, `wrnexus build`
|
||||
(emits `dist/theme.css` + `dist/theme.js`, hashed into the asset version).
|
||||
Reference in New Issue
Block a user