@better-schemic/core 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/lib/authoring.d.ts +114 -0
- package/lib/authoring.js +242 -0
- package/lib/authoring.js.map +1 -0
- package/lib/chunk-26D7WX7Q.js +31 -0
- package/lib/chunk-26D7WX7Q.js.map +1 -0
- package/lib/chunk-IUPOUD4L.js +330 -0
- package/lib/chunk-IUPOUD4L.js.map +1 -0
- package/lib/chunk-LC3VHUM2.js +56 -0
- package/lib/chunk-LC3VHUM2.js.map +1 -0
- package/lib/chunk-RSGP7GVO.js +252 -0
- package/lib/chunk-RSGP7GVO.js.map +1 -0
- package/lib/client-HZF4ZWGO.js +13 -0
- package/lib/client-HZF4ZWGO.js.map +1 -0
- package/lib/config-BYh7WA4P.d.ts +259 -0
- package/lib/config.d.ts +2 -0
- package/lib/config.js +27 -0
- package/lib/config.js.map +1 -0
- package/lib/driver-LVldBEhS.d.ts +818 -0
- package/lib/driver.d.ts +151 -0
- package/lib/driver.js +47 -0
- package/lib/driver.js.map +1 -0
- package/lib/index.d.ts +154 -0
- package/lib/index.js +758 -0
- package/lib/index.js.map +1 -0
- package/lib/query.d.ts +81 -0
- package/lib/query.js +30 -0
- package/lib/query.js.map +1 -0
- package/lib/secrets-BETi5p8g.d.ts +26 -0
- package/lib/testing.d.ts +99 -0
- package/lib/testing.js +212 -0
- package/lib/testing.js.map +1 -0
- package/package.json +102 -0
- package/src/authoring.ts +360 -0
- package/src/cli-kit/config.ts +226 -0
- package/src/cli-kit/diff.ts +273 -0
- package/src/cli-kit/filter.ts +159 -0
- package/src/cli-kit/merge.ts +380 -0
- package/src/cli-kit/meta.ts +123 -0
- package/src/cli-kit/pager.ts +42 -0
- package/src/cli-kit/schema.ts +214 -0
- package/src/cli-kit/style.ts +24 -0
- package/src/client.ts +244 -0
- package/src/config.ts +199 -0
- package/src/connection.ts +120 -0
- package/src/driver/driver.ts +413 -0
- package/src/driver/index.ts +31 -0
- package/src/driver/portable-ir.ts +51 -0
- package/src/driver/portable.ts +124 -0
- package/src/driver/sdk.ts +73 -0
- package/src/index.ts +185 -0
- package/src/kind/index.ts +28 -0
- package/src/kind/plan.ts +412 -0
- package/src/kind/registry.ts +270 -0
- package/src/query/call.ts +21 -0
- package/src/query/codec.ts +33 -0
- package/src/query/index.ts +22 -0
- package/src/query/project.ts +25 -0
- package/src/query/ref.ts +32 -0
- package/src/query.ts +5 -0
- package/src/secrets.ts +61 -0
- package/src/seed.ts +14 -0
- package/src/testing.ts +402 -0
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli-kit/style.ts","../src/cli-kit/diff.ts","../src/cli-kit/filter.ts","../src/cli-kit/merge.ts","../src/cli-kit/meta.ts","../src/cli-kit/pager.ts","../src/cli-kit/schema.ts"],"sourcesContent":["// Tiny ANSI styling, gated on a TTY and honoring NO_COLOR. No dependency.\n/** Whether colored output is enabled (a TTY and `NO_COLOR` unset). */\nexport const colorEnabled = () =>\n Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;\nconst paint = (code: number, s: string) =>\n colorEnabled() ? `\\x1b[${code}m${s}\\x1b[0m` : s;\n\nexport const style = {\n green: (s: string) => paint(32, s),\n red: (s: string) => paint(31, s),\n yellow: (s: string) => paint(33, s),\n cyan: (s: string) => paint(36, s),\n dim: (s: string) => paint(90, s),\n bold: (s: string) => paint(1, s),\n};\n\n/** A green ✓ success line. */\nexport const ok = (s: string) => `${style.green(\"✓\")} ${s}`;\n/** A red ✗ failure line. */\nexport const fail = (s: string) => `${style.red(\"✗\")} ${s}`;\n\n/** Pluralize `n thing` / `n things`. */\nexport const plural = (n: number, word: string) =>\n `${n} ${word}${n === 1 ? \"\" : \"s\"}`;\n","// The DIALECT-FREE diff DISPLAY + shared types. The SurrealDB statement-diff engine (buildSnapshot/\n// diffSnapshots/renderMigration) lives in `./surreal-diff` and is invoked through the driver; this\n// module is what the CLI shell uses to RENDER any driver's Diff (git-style file groups, word-diff,\n// unified patch, kind summaries). Dialect-free: `kind` is an opaque string, not a Surreal kind union.\nimport type { KindRegistry } from \"../kind\";\nimport type { SecretRef } from \"../secrets\";\nimport { colorEnabled, style } from \"./style\";\n\n/**\n * One object's change, for display. `kind` is the object kind, `table` its owner (a table name,\n * or the object's own name for db-level objects). `add` carries the new DDL; `remove` carries the\n * `REMOVE` statement (`ddl`) plus the dropped object's prior DDL (`old`, for the unified patch);\n * `change` pairs old↔new.\n */\nexport type DiffItem = {\n key: string;\n table: string;\n /** Dialect-defined object kind (e.g. \"table\"/\"field\"/\"index\") — opaque to the display. */\n kind: string;\n /** The source file this object lives in (or lived in, for a removal). Absent if unknown. */\n file?: string;\n} & (\n | { op: \"add\"; ddl: string }\n | { op: \"remove\"; ddl: string; old: string }\n | { op: \"change\"; before: string; after: string }\n);\n\nexport interface Diff {\n up: string[];\n down: string[];\n /**\n * Apply-time secret bindings: `$param` name -> a write-only {@link SecretRef} (e.g. `env(\"X\")`).\n * Populated by a driver whose DDL emits secret placeholders (SurrealDB `DEFINE ACCESS … KEY $param`).\n * MODEL 1: stored in the migration so it replays without the live schema; the **value never appears\n * here** — the apply layer resolves each ref through a `SecretProvider` and binds it (`db.query(ddl,\n * resolved)`), so secrets stay out of the schema, snapshot, and migration files. Diff-excluded +\n * snapshot-omitted, so a redacted secret never reads as drift.\n */\n bindings?: Record<string, SecretRef>;\n /** Structured per-object changes for the human display (word-level diff). */\n items?: DiffItem[];\n /** Every desired statement (the `next` schema), for the `--full` context view. */\n full?: { key: string; table: string; ddl: string }[];\n}\n\n/** `true` if the two snapshots define the same objects with identical DDL. */\nexport function isEmptyDiff(diff: Diff): boolean {\n return diff.up.length === 0;\n}\n\n/**\n * Inline word-level diff of two statements: shared tokens dim, removed tokens red, added tokens\n * green (LCS over space-separated tokens). So a changed field shows the whole statement with only\n * the changed words highlighted.\n */\nexport function tokenDiff(before: string, after: string): string {\n const a = before.split(\" \");\n const b = after.split(\" \");\n const m = a.length;\n const n = b.length;\n const dp: number[][] = Array.from({ length: m + 1 }, () =>\n new Array(n + 1).fill(0),\n );\n for (let i = m - 1; i >= 0; i--) {\n for (let j = n - 1; j >= 0; j--) {\n dp[i][j] =\n a[i] === b[j]\n ? dp[i + 1][j + 1] + 1\n : Math.max(dp[i + 1][j], dp[i][j + 1]);\n }\n }\n // With color: red/green/dim. Without (pipe / CI / NO_COLOR): git `--word-diff=plain` markers\n // `[-removed-]`/`{+added+}` so removed-vs-added is unambiguous and assertable.\n const colored = colorEnabled();\n const del = (t: string) => (colored ? style.red(t) : `[-${t}-]`);\n const ins = (t: string) => (colored ? style.green(t) : `{+${t}+}`);\n const eq = (t: string) => (colored ? style.dim(t) : t);\n const out: string[] = [];\n let i = 0;\n let j = 0;\n while (i < m && j < n) {\n if (a[i] === b[j]) {\n out.push(eq(a[i]));\n i++;\n j++;\n } else if (dp[i + 1][j] >= dp[i][j + 1]) {\n out.push(del(a[i++]));\n } else {\n out.push(ins(b[j++]));\n }\n }\n while (i < m) out.push(del(a[i++]));\n while (j < n) out.push(ins(b[j++]));\n return out.join(\" \");\n}\n\n/**\n * Prefix EVERY line of `text` with the diff indicator — statements are multi-line now that drivers\n * pretty-print display DDL, and a bare continuation line would read as context, not change.\n */\nfunction mark(text: string, sign: string, color: (s: string) => string): string {\n return text\n .split(\"\\n\")\n .map((l) => color(` ${sign} ${l}`))\n .join(\"\\n\");\n}\n\n/**\n * Render one display item: `+`/`-` line for add/remove. A change renders as separate red `-` /\n * green `+` lines by default, or as a single inline word-diff when `inline` is set (the A/B toggle).\n */\nfunction renderItem(it: DiffItem, inline = false): string {\n if (it.op === \"add\") return mark(it.ddl, \"+\", style.green);\n if (it.op === \"remove\") return mark(it.ddl, \"-\", style.red);\n if (inline)\n return ` ${tokenDiff(collapseWs(it.before), collapseWs(it.after))}`;\n return `${mark(it.before, \"-\", style.red)}\\n${mark(it.after, \"+\", style.green)}`;\n}\n\n/** The inline word-diff is a one-line token view — collapse pretty-printed statements onto it. */\nfunction collapseWs(text: string): string {\n return text.replace(/\\s+/g, \" \").trim();\n}\n\n/**\n * Group diff items by their source file (git-style), so the display reads like a file diff. Items\n * with no file (an older snapshot, or the live DB) fall back to a per-object group headed by the\n * object's bare name. Returns groups in first-seen order, each with its header.\n */\nfunction groupByFile(\n items: DiffItem[],\n): { header: string; items: DiffItem[] }[] {\n const order: string[] = [];\n const byKey = new Map<string, DiffItem[]>();\n for (const it of items) {\n const key = it.file ?? `\\0${it.table}`; // \\0 can't collide with a real path\n let group = byKey.get(key);\n if (!group) {\n group = [];\n byKey.set(key, group);\n order.push(key);\n }\n group.push(it);\n }\n return order.map((key) => {\n const group = byKey.get(key) ?? [];\n return {\n header: group.find((i) => i.file)?.file ?? group[0].table,\n items: group,\n };\n });\n}\n\n/** Render display items as a git-style file diff: each group headed by its source file path. */\nexport function formatItems(items: DiffItem[], inline = false): string {\n return groupByFile(items)\n .map((g) =>\n [\n style.bold(g.header),\n ...g.items.map((it) => renderItem(it, inline)),\n ].join(\"\\n\"),\n )\n .join(\"\\n\\n\");\n}\n\n/**\n * A standard **unified diff** of the change, grouped one section per source file (git-style) — for\n * piping through a diff viewer (git's pager / delta). Objects with no file fall back to a section\n * headed by the object's bare name. A statement may be pretty-printed multi-line, so every LINE gets\n * its own `+`/`-` and the hunk counts count lines, not statements.\n */\nexport function formatPatch(diff: Diff): string {\n const items = diff.items ?? [];\n if (!items.length) return \"\";\n const out: string[] = [];\n for (const { header, items: group } of groupByFile(items)) {\n const lines: string[] = [];\n let dels = 0;\n let adds = 0;\n const push = (text: string, sign: \"+\" | \"-\") => {\n for (const l of text.split(\"\\n\")) {\n lines.push(`${sign}${l}`);\n if (sign === \"+\") adds++;\n else dels++;\n }\n };\n for (const it of group) {\n if (it.op === \"add\") push(it.ddl, \"+\");\n else if (it.op === \"remove\") push(it.old, \"-\");\n else {\n push(it.before, \"-\");\n push(it.after, \"+\");\n }\n }\n out.push(\n `diff --git a/${header} b/${header}`,\n `--- a/${header}`,\n `+++ b/${header}`,\n `@@ -${dels ? 1 : 0},${dels} +${adds ? 1 : 0},${adds} @@`,\n ...lines,\n );\n }\n return `${out.join(\"\\n\")}\\n`;\n}\n\n/** `--full`: the whole desired schema — unchanged dim, additions green, changes word-diffed. */\nfunction formatFull(diff: Diff, inline = false): string {\n const byKey = new Map((diff.items ?? []).map((it) => [it.key, it]));\n const out: string[] = [];\n let prev: string | undefined;\n for (const f of diff.full ?? []) {\n if (prev !== undefined && f.table !== prev) out.push(\"\");\n const it = byKey.get(f.key);\n if (it?.op === \"change\") out.push(renderItem(it, inline));\n else if (it?.op === \"add\") out.push(mark(f.ddl, \"+\", style.green));\n else\n out.push(\n f.ddl\n .split(\"\\n\")\n .map((l) => style.dim(` ${l}`))\n .join(\"\\n\"),\n );\n prev = f.table;\n }\n const removed = (diff.items ?? []).filter((it) => it.op === \"remove\");\n if (removed.length) {\n out.push(\"\");\n for (const it of removed) out.push(renderItem(it, inline));\n }\n return out.join(\"\\n\");\n}\n\n/** A human-readable view of a diff's forward (and optionally reverse) changes. */\nexport function formatDiff(\n diff: Diff,\n opts: { down?: boolean; full?: boolean; inline?: boolean } = {},\n): string {\n if (!diff.up.length) return \"No changes.\";\n let out = opts.full\n ? formatFull(diff, opts.inline)\n : formatItems(diff.items ?? [], opts.inline);\n if (opts.down) {\n const down = diff.down\n .map((s) =>\n s\n .split(\"\\n\")\n .map((l) => style.dim(` ${l}`))\n .join(\"\\n\"),\n )\n .join(\"\\n\");\n out += `\\n\\n${style.dim(\" rollback (down):\")}\\n${down}`;\n }\n return out;\n}\n\n/**\n * A per-kind breakdown of a set of changes, e.g. `1 Table, 2 Fields`. Counts each item by its\n * structured `kind` and labels it from the registry's per-kind {@link KindRegistry.display} (singular\n * when the count is one), so the summary is correct for every dialect — no DDL parsing.\n */\nexport function summarizeKinds(\n registry: KindRegistry,\n items: readonly { kind: string }[],\n): string {\n const counts = new Map<string, number>();\n for (const it of items) counts.set(it.kind, (counts.get(it.kind) ?? 0) + 1);\n const parts: string[] = [];\n for (const [kind, n] of counts) {\n const d = registry.display(kind);\n parts.push(`${n} ${n === 1 ? d.label : d.plural}`);\n }\n return parts.join(\", \");\n}\n","import type { Command } from \"commander\";\nimport type { KindRegistry, PortableObject } from \"../kind\";\nimport { snapshotKinds, snapshotObjects } from \"../kind\";\nimport type { StoredSnapshot } from \"./meta\";\n\n/**\n * Per-kind object filter for `pull`/`diff`/`sync`/`generate`. Each kind is independently\n * included (optionally name-restricted). `DEFINE ACCESS` is OPT-IN everywhere — excluded\n * unless `--access` is given — so an introspection (which redacts access signing keys) can't\n * silently rotate them. Table-scoped objects (fields/indexes/events) follow their table.\n */\ninterface Cat {\n on: boolean;\n /** When set, only these names of the kind are included. */\n names?: Set<string>;\n}\n\nexport interface Filter {\n tables: Cat;\n functions: Cat;\n events: Cat;\n access: Cat;\n}\n\n/** Commander's parse of one `--kind [names]` / `--no-kind` flag: `undefined`/`true`/string/`false`. */\ntype FlagValue = string | boolean | undefined;\n\nexport interface FilterOpts {\n tables?: FlagValue;\n functions?: FlagValue;\n events?: FlagValue;\n access?: FlagValue;\n}\n\nfunction cat(v: FlagValue, defaultOn: boolean): Cat {\n if (v === undefined) return { on: defaultOn };\n if (v === false) return { on: false };\n if (v === true) return { on: true };\n const names = new Set(\n v\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean),\n );\n return names.size ? { on: true, names } : { on: true };\n}\n\n/** Build a {@link Filter} from CLI flags. Access is opt-in (`--access`); the rest default to on. */\nexport function parseFilter(o: FilterOpts): Filter {\n return {\n tables: cat(o.tables, true),\n functions: cat(o.functions, true),\n events: cat(o.events, true),\n access: cat(o.access, false), // DEFINE ACCESS is explicit everywhere — see module note.\n };\n}\n\n/** Attach the per-kind `--tables/--functions/--events/--access [names]` (+ `--no-*`) options. */\nexport function kindFlags(cmd: Command): Command {\n return cmd\n .option(\"--tables [names]\", \"only these tables (comma-separated)\")\n .option(\"--no-tables\", \"exclude all tables\")\n .option(\"--functions [names]\", \"only these functions\")\n .option(\"--no-functions\", \"exclude all functions\")\n .option(\"--events [names]\", \"only these events\")\n .option(\"--no-events\", \"exclude all events\")\n .option(\n \"--access [names]\",\n \"include access (off by default; key not pulled)\",\n )\n .option(\"--no-access\", \"exclude all access (the default)\");\n}\n\n/** Whether a name passes a category gate (kind on + name allowed). Shared with the surreal filters. */\nexport const inCat = (c: Cat, name: string): boolean =>\n c.on && (!c.names || c.names.has(name));\n\n// The SurrealDB statement/struct filters (`included`/`filterSnapshot`/`mergeSnapshot`/\n// `filterStructured`) live in `./surreal-filter`. Below are the dialect-free kind-registry filters.\n\n// --- kind-registry filters (the stored-snapshot path) -------------------------------------------\n\nconst objKey = (o: PortableObject) => `${o.kind}:${o.name}`;\n\n/** Which Filter category gates a TOP-LEVEL kind (and whose name-set its name is matched against). */\nfunction category(kind: string): keyof Filter {\n if (kind === \"function\") return \"functions\";\n if (kind === \"access\") return \"access\";\n return \"tables\"; // a table (and any future top-level structural kind)\n}\n\n/**\n * Whether a portable object passes the filter. A TOP-LEVEL object (no owner) is gated by its kind's\n * category + name. An OWNED object (owner set — an index/event/constraint) FOLLOWS its owner table's\n * inclusion; an `event` is ADDITIONALLY gated by the `events` category. (Fields are substrate nested\n * in their table object, never standalone here.)\n */\nexport function passesFilter(\n registry: KindRegistry,\n o: PortableObject,\n f: Filter,\n): boolean {\n const owner = registry.engine(o.kind)?.owner?.(o);\n if (owner) {\n if (!inCat(f.tables, owner.name)) return false;\n return o.kind === \"event\" ? inCat(f.events, o.name) : true;\n }\n return inCat(f[category(o.kind)], o.name);\n}\n\n/** Keep only the portable objects that pass the filter (the {@link filterStructured} analog). */\nexport function filterKinds(\n registry: KindRegistry,\n objects: PortableObject[],\n f: Filter,\n): PortableObject[] {\n return objects.filter((o) => passesFilter(registry, o, f));\n}\n\n/**\n * The stored snapshot to persist after a filtered `generate`: INCLUDED objects take their new state\n * from `next`, EXCLUDED objects keep `prev`'s. Dedup by `kind:name` (an included `next` object wins).\n * `files` overlays next on prev.\n */\nexport function mergeStored(\n registry: KindRegistry,\n prev: StoredSnapshot,\n next: StoredSnapshot,\n f: Filter,\n): StoredSnapshot {\n const merged = new Map<string, PortableObject>();\n for (const o of snapshotObjects(prev.schema))\n if (!passesFilter(registry, o, f)) merged.set(objKey(o), o);\n for (const o of snapshotObjects(next.schema))\n if (passesFilter(registry, o, f)) merged.set(objKey(o), o);\n return {\n version: 3,\n driver: next.driver,\n schema: snapshotKinds([...merged.values()], registry),\n files: { ...(prev.files ?? {}), ...(next.files ?? {}) },\n };\n}\n\n/**\n * Restrict the `disk` objects to those that ALSO exist `live` (intersect by `kind:name`) AND pass the\n * filter — for `baseline`. Hand-written schema not yet in the DB stays pending; what's really there is\n * captured. Each field/index/event/constraint is its own object, so intersect-by-key handles them.\n */\nexport function intersectKinds(\n registry: KindRegistry,\n disk: PortableObject[],\n live: PortableObject[],\n f: Filter,\n): PortableObject[] {\n const liveKeys = new Set(live.map(objKey));\n return disk.filter(\n (o) => liveKeys.has(objKey(o)) && passesFilter(registry, o, f),\n );\n}\n","// Surgically merge freshly-pulled `s.*` definitions into existing schema files, instead of\n// overwriting them. Uses magicast (recast under the hood), so untouched code — user comments,\n// extra imports, unrelated consts, hand-formatted fields — survives. The live DB wins on any\n// object/field it defines; the only thing at risk is LOCAL-ONLY content (a field or whole const\n// that exists in your files but not in the DB), which the caller resolves via keep/drop.\n\nimport { mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { generateCode, parseModule } from \"magicast\";\nimport { colorEnabled, style } from \"./style\";\n\n/** One rendered object (table / function / access): its const statement + the imports it needs. */\nexport interface RenderedUnit {\n kind: \"table\" | \"function\" | \"access\";\n /** DB object name (drives the file path). */\n name: string;\n /** The exported const identifier (`User`, `math_add`, …). */\n exportName: string;\n /** The `export const … = define…(…);` statement source (no import lines). */\n code: string;\n /** The `import …` lines this unit needs. */\n imports: string[];\n}\n\n/** Local-only content a merge would drop when mirroring the DB. */\nexport interface LocalOnly {\n /** Per existing const: field keys present locally but absent from the DB object. */\n fields: { exportName: string; fields: string[] }[];\n /** Whole consts present locally whose object the DB no longer has. */\n objects: string[];\n}\n\nexport interface MergeResult {\n content: string;\n localOnly: LocalOnly;\n}\n\n/** Options governing what happens to local-only content. */\nexport interface MergeOptions {\n /** Keep local-only fields (graft them back into the merged object). */\n keepLocalFields: boolean;\n /** Keep local-only consts (objects the DB no longer has). */\n keepLocalObjects: boolean;\n}\n\n/** What pulling would do to one schema file. */\nexport interface PullFilePlan {\n /** Path relative to the project root (for display). */\n rel: string;\n /** Absolute path on disk. */\n abs: string;\n /**\n * `create` (new file), `update` (merged edits), `unchanged` (already matches the DB), or `delete`\n * (a file that is purely local-only entities the DB doesn't have — removed when mirroring).\n */\n action: \"create\" | \"update\" | \"unchanged\" | \"delete\";\n /** Current file contents (`\"\"` for a new file). */\n before: string;\n /** Contents after the pull (the merged result). */\n after: string;\n /** Local-only content this file would drop when mirroring the DB. */\n localOnly: LocalOnly;\n}\n\n/** A driver's introspection rendered into a per-file write plan (see a driver's `planPull`). */\nexport interface PullPlan {\n files: PullFilePlan[];\n}\n\n/** Apply a plan: write created/updated files, delete local-only files. Returns the paths touched. */\nexport function applyPull(plan: PullPlan): string[] {\n const touched: string[] = [];\n for (const f of plan.files) {\n if (f.action === \"unchanged\") continue;\n if (f.action === \"delete\") {\n rmSync(f.abs, { force: true });\n } else {\n mkdirSync(dirname(f.abs), { recursive: true });\n writeFileSync(f.abs, f.after);\n }\n touched.push(f.rel);\n }\n return touched;\n}\n\n// --- AST helpers ------------------------------------------------------------------------------\n// A permissive view over the recast/babel nodes we touch — every property we read, all optional,\n// so navigation needs no per-access casts.\ninterface AstNode {\n type: string;\n name?: string;\n callee?: AstNode;\n object?: AstNode;\n body?: AstNode;\n arguments?: AstNode[];\n properties?: AstNode[];\n declaration?: AstNode;\n declarations?: Array<{ id?: { name?: string }; init?: AstNode }>;\n key?: { name?: string; value?: string };\n comments?: Array<{ leading?: boolean }>;\n}\n\ninterface MagicastModule {\n $ast: { body: AstNode[] };\n imports: {\n $add: (s: { from: string; imported: string; local: string }) => void;\n };\n}\n\nconst asMod = (mod: unknown): MagicastModule =>\n mod as unknown as MagicastModule;\n\n/** The exported const's name, or null if this statement isn't an `export const X = …`. */\nfunction exportConstName(node: AstNode): string | null {\n if (node.type !== \"ExportNamedDeclaration\") return null;\n if (node.declaration?.type !== \"VariableDeclaration\") return null;\n return node.declaration.declarations?.[0]?.id?.name ?? null;\n}\n\n/** The init expression of an `export const X = <init>`. */\nfunction exportConstInit(node: AstNode): AstNode | null {\n return node.declaration?.declarations?.[0]?.init ?? null;\n}\n\n/** Walk a `define…(…).a().b()` chain down to the `define…(…)` CallExpression. */\nfunction defineCall(init: AstNode | null): AstNode | null {\n let n: AstNode | null = init;\n while (n) {\n if (n.type === \"CallExpression\") {\n if (\n n.callee?.type === \"Identifier\" &&\n /^define/.test(n.callee.name ?? \"\")\n )\n return n;\n n =\n n.callee?.type === \"MemberExpression\"\n ? (n.callee.object ?? null)\n : (n.callee ?? null);\n } else if (n.type === \"MemberExpression\") {\n n = n.object ?? null;\n } else break;\n }\n return null;\n}\n\n/** The fields ObjectExpression of a `defineTable`/`defineRelation` call (handles the `self` arrow form). */\nfunction fieldsObject(init: AstNode | null): AstNode | null {\n const call = defineCall(init);\n if (!call) return null;\n let obj = call.arguments?.[1] ?? null;\n if (obj?.type === \"ArrowFunctionExpression\") obj = obj.body ?? null;\n return obj?.type === \"ObjectExpression\" ? obj : null;\n}\n\n/** Property key as a string (`name` / `\"weird-key\"`). */\nfunction propKey(p: AstNode): string | undefined {\n return p.key?.name ?? p.key?.value;\n}\n\n/** Whether a node carries a leading comment (so we don't clobber/duplicate it). */\nfunction hasLeadingComment(node: AstNode): boolean {\n return Boolean(node.comments?.some((c) => c.leading));\n}\n\n/** Parse `import { a, b } from \"x\"` lines into `{from, names}` (our generated imports use no aliases). */\nfunction parseImportLine(\n line: string,\n): { from: string; names: string[] } | null {\n const m = /import\\s*\\{([^}]*)\\}\\s*from\\s*[\"']([^\"']+)[\"']/.exec(line);\n if (!m) return null;\n return {\n from: m[2],\n names: m[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean),\n };\n}\n\n/**\n * Merge `units` into `existingSrc`. Each unit replaces the matching `export const` in place (DB wins),\n * preserving everything else in the file. New units are appended; needed imports are unioned in.\n * Local-only fields/objects are reported, and kept or dropped per `opts`.\n */\nexport function mergeUnits(\n existingSrc: string,\n units: RenderedUnit[],\n opts: MergeOptions,\n): MergeResult {\n const mod = parseModule(existingSrc);\n const body = asMod(mod).$ast.body;\n\n // Index existing exported consts by name.\n const existing = new Map<string, AstNode>();\n for (const node of body) {\n const name = exportConstName(node);\n if (name) existing.set(name, node);\n }\n\n const localOnly: LocalOnly = { fields: [], objects: [] };\n const desiredNames = new Set(units.map((u) => u.exportName));\n\n for (const unit of units) {\n // Parse the freshly-rendered unit to lift its statement node.\n const unitBody = asMod(parseModule(unit.code)).$ast.body;\n const desiredNode = unitBody.find(\n (n) => exportConstName(n) === unit.exportName,\n );\n if (!desiredNode) continue; // shouldn't happen — the renderer always emits the const\n\n const prior = existing.get(unit.exportName);\n if (prior) {\n // Table/relation: reconcile fields. Functions/access are atomic (whole-const replace).\n if (unit.kind === \"table\") {\n const priorObj = fieldsObject(exportConstInit(prior));\n const desiredObj = fieldsObject(exportConstInit(desiredNode));\n if (priorObj?.properties && desiredObj?.properties) {\n const desiredKeys = new Set(desiredObj.properties.map(propKey));\n const localFields = priorObj.properties.filter(\n (p) => !desiredKeys.has(propKey(p)),\n );\n if (localFields.length) {\n localOnly.fields.push({\n exportName: unit.exportName,\n fields: localFields.map((p) => propKey(p) ?? \"?\"),\n });\n // Graft the local-only field nodes (with their comments) onto the merged object.\n if (opts.keepLocalFields)\n for (const p of localFields) desiredObj.properties.push(p);\n }\n }\n }\n // Preserve a user's leading comment above the const (the renderer emits none for tables;\n // for access the renderer's own NOTE already lives on desiredNode, so don't double it).\n if (!hasLeadingComment(desiredNode))\n desiredNode.comments = prior.comments;\n body[body.indexOf(prior)] = desiredNode;\n } else {\n body.push(desiredNode);\n }\n addImports(mod, unit.imports);\n }\n\n // Existing consts the DB no longer has → local-only objects.\n for (const [name, node] of existing) {\n if (desiredNames.has(name)) continue;\n localOnly.objects.push(name);\n if (!opts.keepLocalObjects) body.splice(body.indexOf(node), 1);\n }\n\n return { content: ensureTrailingNewline(generateCode(mod).code), localOnly };\n}\n\n/** Union the named imports from `lines` into the module (magicast dedupes against existing). */\nfunction addImports(mod: unknown, lines: string[]): void {\n const imports = asMod(mod).imports;\n for (const line of lines) {\n const parsed = parseImportLine(line);\n if (!parsed) continue;\n for (const name of parsed.names)\n imports.$add({ from: parsed.from, imported: name, local: name });\n }\n}\n\nfunction ensureTrailingNewline(s: string): string {\n return s.endsWith(\"\\n\") ? s : `${s}\\n`;\n}\n\n// --- Line diff (colored preview + git-style patch) -------------------------------------------\n\ntype LineOp = { tag: \" \" | \"-\" | \"+\"; line: string };\n\nconst splitLines = (s: string): string[] =>\n s === \"\" ? [] : s.replace(/\\n$/, \"\").split(\"\\n\");\n\n/** LCS line-level ops between two texts (a trailing newline is ignored). */\nfunction lineOps(before: string, after: string): LineOp[] {\n const a = splitLines(before);\n const b = splitLines(after);\n const m = a.length;\n const n = b.length;\n const dp: number[][] = Array.from({ length: m + 1 }, () =>\n new Array(n + 1).fill(0),\n );\n for (let i = m - 1; i >= 0; i--)\n for (let j = n - 1; j >= 0; j--)\n dp[i][j] =\n a[i] === b[j]\n ? dp[i + 1][j + 1] + 1\n : Math.max(dp[i + 1][j], dp[i][j + 1]);\n const ops: LineOp[] = [];\n let i = 0;\n let j = 0;\n while (i < m && j < n) {\n if (a[i] === b[j]) {\n ops.push({ tag: \" \", line: a[i] });\n i++;\n j++;\n } else if (dp[i + 1][j] >= dp[i][j + 1]) {\n ops.push({ tag: \"-\", line: a[i++] });\n } else {\n ops.push({ tag: \"+\", line: b[j++] });\n }\n }\n while (i < m) ops.push({ tag: \"-\", line: a[i++] });\n while (j < n) ops.push({ tag: \"+\", line: b[j++] });\n return ops;\n}\n\n/**\n * A compact colored line diff for previews. A new file renders as all-green additions; an edit\n * renders removed (red `-`) / added (green `+`) lines with a little surrounding context (long\n * unchanged runs collapse to `…`).\n */\nexport function lineDiff(before: string, after: string): string {\n const ops = lineOps(before, after);\n // Keep changed lines plus up to 2 lines of context around each; collapse long unchanged runs.\n const CONTEXT = 2;\n const keep = new Array(ops.length).fill(false);\n ops.forEach((op, idx) => {\n if (op.tag === \" \") return;\n for (\n let k = Math.max(0, idx - CONTEXT);\n k <= Math.min(ops.length - 1, idx + CONTEXT);\n k++\n )\n keep[k] = true;\n });\n const out: string[] = [];\n let gap = false;\n ops.forEach((op, idx) => {\n if (!keep[idx]) {\n if (!gap) out.push(style.dim(\" …\"));\n gap = true;\n return;\n }\n gap = false;\n if (op.tag === \" \") out.push(style.dim(` ${op.line}`));\n else if (op.tag === \"-\") out.push(style.red(`- ${op.line}`));\n else out.push(style.green(`+ ${op.line}`));\n });\n return out.join(\"\\n\");\n}\n\n/**\n * A git-style unified diff between two texts, headed by `label` as the file path — for piping to a\n * diff viewer (delta / git's pager). Returns \"\" when there's no change.\n */\nexport function unifiedDiff(\n before: string,\n after: string,\n label: string,\n): string {\n const ops = lineOps(before, after);\n if (!ops.some((o) => o.tag !== \" \")) return \"\";\n const oldLen = ops.filter((o) => o.tag !== \"+\").length;\n const newLen = ops.filter((o) => o.tag !== \"-\").length;\n const body = ops.map((o) =>\n o.tag === \" \" ? ` ${o.line}` : `${o.tag}${o.line}`,\n );\n return `${[\n `diff --git a/${label} b/${label}`,\n `--- a/${label}`,\n `+++ b/${label}`,\n `@@ -1,${oldLen} +1,${newLen} @@`,\n ...body,\n ].join(\"\\n\")}\\n`;\n}\n\n/** Colored verb for a pull action (`new` / `update` / `delete` / `unchanged`). */\nexport function actionLabel(\n action: \"create\" | \"update\" | \"unchanged\" | \"delete\",\n): string {\n if (action === \"create\") return colorEnabled() ? style.green(\"new\") : \"new\";\n if (action === \"update\")\n return colorEnabled() ? style.yellow(\"update\") : \"update\";\n if (action === \"delete\")\n return colorEnabled() ? style.red(\"delete\") : \"delete\";\n return style.dim(\"unchanged\");\n}\n","import { createHash } from \"node:crypto\";\nimport {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { KindSnapshot } from \"../kind\";\n\n// The legacy STATEMENT snapshot types (Snapshot/SnapshotStatement/EMPTY_SNAPSHOT) now live in\n// cli/structure.ts (the SurrealDB module that produces them) — this file is the NEUTRAL stored-\n// snapshot + migration-file engine.\n\n/**\n * The STORED snapshot (`_snapshot.json`): the canonical schema is portable objects grouped by kind\n * (a {@link KindSnapshot}); DDL is derived generically via the kind registry (`buildKindDiff`/\n * `emitKinds`). Diffed against the next `generate`. `files` maps each object's name to its\n * project-root-relative source file (display-only; attached to diff items by the CLI). Pre-launch:\n * the format is free to change, so there is no on-disk version migration — an unrecognized snapshot\n * is treated as empty (regenerate via `better-schemic gen --baseline`).\n */\nexport interface StoredSnapshot {\n version: 3;\n /** The driver that authored this snapshot (\"surrealdb\"). */\n driver: string;\n /** Portable objects grouped by kind. */\n schema: KindSnapshot;\n files?: Record<string, string>;\n}\n\n/** A migration file on disk. The filename is the source of truth — there's no journal. */\nexport interface Migration {\n /** Filename without the `.surql` extension, e.g. `20260607153045_add_users`. */\n tag: string;\n /** Filename, e.g. `20260607153045_add_users.surql`. */\n file: string;\n}\n\nconst SNAPSHOT_FILE = \"_snapshot.json\";\n/** Fallback migration extension when a caller has no driver to hand (the driver provides the real one). */\nconst DEFAULT_MIGRATION_EXT = \".surql\";\n\n/** A fresh empty STORED snapshot. Fresh each call so callers can't alias shared empty state. */\nfunction emptyStored(): StoredSnapshot {\n return {\n version: 3,\n driver: \"surrealdb\",\n schema: { kinds: {} },\n files: {},\n };\n}\n\n/** The empty STORED snapshot — used as `prev` for a `--baseline` generate (diff against nothing). */\nexport const EMPTY_STORED: StoredSnapshot = emptyStored();\n\n/**\n * Read the stored snapshot. Pre-launch: any snapshot that isn't the current `version: 3` shape (a\n * pre-portable v1/v2, or absent) is treated as EMPTY — regenerate with `better-schemic gen --baseline`.\n */\nexport function readSnapshot(metaDir: string): StoredSnapshot {\n const path = join(metaDir, SNAPSHOT_FILE);\n if (!existsSync(path)) return emptyStored();\n const raw = JSON.parse(readFileSync(path, \"utf8\")) as Partial<StoredSnapshot>;\n if (raw.version === 3 && raw.driver && raw.schema)\n return { files: {}, ...(raw as StoredSnapshot) };\n return emptyStored();\n}\n\nexport function writeSnapshot(metaDir: string, snapshot: StoredSnapshot): void {\n mkdirSync(metaDir, { recursive: true });\n writeFileSync(\n join(metaDir, SNAPSHOT_FILE),\n `${JSON.stringify(snapshot, null, 2)}\\n`,\n );\n}\n\n/**\n * All migration files in `migrationsDir`, in apply order. Filenames are timestamp-prefixed, so\n * a plain ascending sort is chronological (and legacy `0001_` names sort before timestamped\n * ones). The `meta/` directory and any file not ending in `ext` (the driver's migration extension)\n * are ignored.\n */\nexport function listMigrations(\n migrationsDir: string,\n ext: string = DEFAULT_MIGRATION_EXT,\n): Migration[] {\n if (!existsSync(migrationsDir)) return [];\n return readdirSync(migrationsDir)\n .filter((f) => f.endsWith(ext))\n .sort()\n .map((file) => ({ tag: file.slice(0, -ext.length), file }));\n}\n\n/** A sortable UTC timestamp prefix for a new migration, e.g. `20260607153045`. */\nexport function timestamp(date: Date): string {\n const p = (n: number) => String(n).padStart(2, \"0\");\n return (\n `${date.getUTCFullYear()}` +\n p(date.getUTCMonth() + 1) +\n p(date.getUTCDate()) +\n p(date.getUTCHours()) +\n p(date.getUTCMinutes()) +\n p(date.getUTCSeconds())\n );\n}\n\n/** sha256 of a migration file's contents (drift detection / apply-time bookkeeping). */\nexport function checksum(content: string): string {\n return createHash(\"sha256\").update(content).digest(\"hex\").slice(0, 16);\n}\n\n/** Turn a free-form migration name into a filename-safe slug. */\nexport function slug(name: string): string {\n return (\n name\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\") || \"migration\"\n );\n}\n","import { execFileSync, spawn } from \"node:child_process\";\n\n/** Read a single git config value (global/system included), or undefined. */\nfunction gitConfig(key: string): string | undefined {\n try {\n const v = execFileSync(\"git\", [\"config\", \"--get\", key], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n return v || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The diff pager, resolved the way git does: `pager.diff` → `core.pager` → `$GIT_PAGER` →\n * `$PAGER`. So a user with `core.pager = delta` gets delta for free, with their own config.\n */\nexport function resolvePager(): string | undefined {\n return (\n gitConfig(\"pager.diff\") ||\n gitConfig(\"core.pager\") ||\n process.env.GIT_PAGER ||\n process.env.PAGER ||\n undefined\n );\n}\n\n/** Pipe `text` through `pager` (a shell command, possibly with args); resolve when it exits. */\nexport function pipeThroughPager(pager: string, text: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = spawn(\"sh\", [\"-c\", pager], {\n stdio: [\"pipe\", \"inherit\", \"inherit\"],\n });\n child.once(\"error\", reject);\n child.once(\"close\", () => resolve());\n // The pager may quit before reading all input (e.g. `less` on a short diff) — ignore EPIPE.\n child.stdin.on(\"error\", () => {});\n child.stdin.end(text);\n });\n}\n","import { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Authored, AuthoredDef } from \"@better-schemic/core\";\nimport { makeJiti } from \"./config\";\n\n/**\n * The NEUTRAL view of a loaded table the engine reads — just `name` plus the `config.relation` flag\n * used for ordering. A driver casts this to its own concrete table builder in `lower`. (The runtime\n * object is the driver's real `TableDef`; the engine never names that type.)\n */\n\n/** Import one schema module, wrapping a crash with the FAILING FILE path (original as `cause`). */\nasync function importSchemaModule(\n jiti: ReturnType<typeof makeJiti>,\n file: string,\n): Promise<unknown> {\n try {\n return await jiti.import(file);\n } catch (err) {\n throw new Error(\n `failed to load schema module ${file}: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err },\n );\n }\n}\n\nexport interface AnyTable extends Authored {\n readonly config: { readonly relation?: unknown };\n}\n\n/**\n * Duck-typed `TableDef` check. We avoid `instanceof` on purpose: the user's schema and the\n * CLI may end up with different module instances of `@better-schemic/core`, so we recognize a table\n * by shape instead. (Structural access into `emitStatements` works regardless.)\n */\nfunction isTableDef(v: unknown): v is AnyTable {\n if (!v || typeof v !== \"object\") return false;\n const t = v as Record<string, unknown>;\n return (\n typeof t.name === \"string\" &&\n typeof t.fields === \"object\" &&\n t.fields !== null &&\n typeof t.config === \"object\" &&\n t.config !== null &&\n typeof t.record === \"function\"\n );\n}\n\n/**\n * Duck-typed standalone-def check — DRIVER-AGNOSTIC: any non-table object carrying a string `kind`\n * and `name` is a standalone definable (event/function/access/enum/view/sequence/…). Core must not\n * hardcode a dialect's kinds — the driver's `registry`/`explode` owns which kinds are valid. Tables\n * are matched by `isTableDef` first, so they never reach here. See `isTableDef` on why not `instanceof`.\n */\nfunction isStandaloneDef(v: unknown): v is AuthoredDef {\n if (!v || typeof v !== \"object\") return false;\n const d = v as Record<string, unknown>;\n return (\n typeof d.kind === \"string\" &&\n d.kind.length > 0 &&\n typeof d.name === \"string\"\n );\n}\n\nfunction tsFiles(dir: string): string[] {\n const out: string[] = [];\n for (const entry of readdirSync(dir).sort()) {\n const p = join(dir, entry);\n if (statSync(p).isDirectory()) out.push(...tsFiles(p));\n else if (/\\.(ts|mts|js|mjs)$/.test(entry) && !entry.endsWith(\".d.ts\"))\n out.push(p);\n }\n return out;\n}\n\n/** The schema module file(s) for a path: the file itself, or every module under the directory. */\nfunction schemaFiles(path: string): string[] {\n return statSync(path).isFile() ? [path] : tsFiles(path);\n}\n\n/** Import a schema module file and yield its exported tables/relations, paired with the file. */\nasync function* tablesIn(\n jiti: ReturnType<typeof makeJiti>,\n file: string,\n): AsyncGenerator<AnyTable> {\n const mod = (await importSchemaModule(jiti, file)) as Record<\n string,\n unknown\n >;\n for (const value of Object.values(mod)) if (isTableDef(value)) yield value;\n}\n\n/**\n * Load every schema object from `schemaPath` (a single `.ts` module, or a directory of them): the\n * tables/relations (ordered normal-before-relation, then by name, for stable DDL) and the standalone\n * defs (any non-table `{ kind, name }` definable — the driver's registry owns the kinds). One pass.\n */\nexport async function loadDefs(schemaPath: string): Promise<{\n tables: AnyTable[];\n defs: AuthoredDef[];\n /** Absolute source file each table/def was loaded from (for `diff`'s file annotations). */\n fileOf: Map<AnyTable | AuthoredDef, string>;\n}> {\n if (!existsSync(schemaPath)) {\n throw new Error(`Schema path not found: ${schemaPath}`);\n }\n const jiti = makeJiti();\n const tables = new Map<string, AnyTable>();\n const defs: AuthoredDef[] = [];\n const fileOf = new Map<AnyTable | AuthoredDef, string>();\n for (const file of schemaFiles(schemaPath)) {\n const mod = (await importSchemaModule(jiti, file)) as Record<\n string,\n unknown\n >;\n for (const value of Object.values(mod)) {\n if (isTableDef(value)) {\n tables.set(value.name, value); // last def of a name wins\n fileOf.set(value, file);\n } else if (isStandaloneDef(value)) {\n defs.push(value);\n fileOf.set(value, file);\n }\n }\n }\n const rank = (t: AnyTable) => (t.config.relation ? 1 : 0);\n const sorted = [...tables.values()].sort(\n (a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name),\n );\n return { tables: sorted, defs, fileOf };\n}\n\n/** The tables/relations from `schemaPath` (standalone events excluded — see {@link loadDefs}). */\nexport async function loadSchemas(schemaPath: string): Promise<AnyTable[]> {\n return (await loadDefs(schemaPath)).tables;\n}\n\n/** A schema file's exported entities (tables/functions/accesses) and whether it holds ONLY those. */\nexport interface LocalFileEntities {\n /** Each schema entity by its export-const identifier + its DB name (table/function/access name). */\n entities: { exportName: string; name: string; kind: \"table\" | \"def\" }[];\n /** True when EVERY runtime export of the file is a schema entity (no helpers / other exports). */\n pureSchema: boolean;\n}\n\n/**\n * Scan each schema file for the tables/functions/accesses it exports (by export-const name), and\n * whether the file is purely schema. `pull` uses this to find whole-entity local-only schema\n * (entities the live DB doesn't have) and to decide whether a file is safe to delete when mirroring\n * the DB. Standalone events are not whole entities (they attach to a table), so they don't count as\n * entities — a file exporting one is therefore not `pureSchema` and won't be auto-deleted.\n */\nexport async function scanLocalEntities(\n schemaPath: string,\n): Promise<Map<string, LocalFileEntities>> {\n if (!existsSync(schemaPath)) return new Map();\n const jiti = makeJiti();\n const out = new Map<string, LocalFileEntities>();\n for (const file of schemaFiles(schemaPath)) {\n const exports = Object.entries(\n (await importSchemaModule(jiti, file)) as Record<string, unknown>,\n );\n const entities: LocalFileEntities[\"entities\"] = [];\n for (const [exportName, value] of exports) {\n if (isTableDef(value))\n entities.push({ exportName, name: value.name, kind: \"table\" });\n else if (\n isStandaloneDef(value) &&\n (value.kind === \"function\" || value.kind === \"access\")\n )\n entities.push({ exportName, name: value.name, kind: \"def\" });\n }\n if (entities.length)\n out.set(file, {\n entities,\n pureSchema: entities.length === exports.length,\n });\n }\n return out;\n}\n\n/** Map of table name → the file that defines it (for `pull`'s duplicate-definition check). */\nexport async function existingTables(\n schemaPath: string,\n): Promise<Map<string, string>> {\n if (!existsSync(schemaPath)) return new Map();\n const jiti = makeJiti();\n const out = new Map<string, string>();\n for (const file of schemaFiles(schemaPath)) {\n for await (const t of tablesIn(jiti, file)) out.set(t.name, file);\n }\n return out;\n}\n\n/**\n * Names defined in more than one place, mapped to the files that define them (a file repeats if it\n * defines the same name twice). `loadSchemas` silently lets the last definition win, so this is how\n * `doctor` surfaces the otherwise-invisible conflict.\n */\nexport async function duplicateTables(\n schemaPath: string,\n): Promise<Map<string, string[]>> {\n if (!existsSync(schemaPath)) return new Map();\n const jiti = makeJiti();\n const seen = new Map<string, string[]>();\n for (const file of schemaFiles(schemaPath)) {\n for await (const t of tablesIn(jiti, file)) {\n const files = seen.get(t.name);\n if (files) files.push(file);\n else seen.set(t.name, [file]);\n }\n }\n return new Map([...seen].filter(([, files]) => files.length > 1));\n}\n"],"mappings":"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","names":["i","j","i","j","mkdirSync","writeFileSync","existsSync","readdirSync","join","readdirSync","join","existsSync"]}
|
package/lib/query.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { C as CallableFunctions } from './driver-LVldBEhS.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import './config-BYh7WA4P.js';
|
|
4
|
+
import 'jiti';
|
|
5
|
+
import './secrets-BETi5p8g.js';
|
|
6
|
+
import 'commander';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Invoke a defined DB function via the driver's `callable` capability and **decode its result through the
|
|
10
|
+
* function's `.returns(R)` schema** — the neutral half of the query layer's (B) `.call()`. A driver's
|
|
11
|
+
* `defineFunction(args).returns(R).call(db, appArgs)` composes this: it encodes `appArgs` to wire (via
|
|
12
|
+
* the arg schemas) and passes `R` here. Decode-by-default is the differentiator — results come back as
|
|
13
|
+
* real `App` types (`Date`, `RecordId`, …), not wire. A `.raw()` path skips this and returns
|
|
14
|
+
* `callable.invoke(...)` directly.
|
|
15
|
+
*/
|
|
16
|
+
declare function callFunction<S extends z.ZodType>(callable: CallableFunctions, conn: unknown, name: string, args: Record<string, unknown>, returns: S): Promise<z.output<S>>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One selected projection column: the output key (`as`) plus the source Zod schema to decode it through.
|
|
20
|
+
* The schema may itself be a `z.object(...)` for a nested projection — the driver's builder assembles the
|
|
21
|
+
* tree; core just decodes it.
|
|
22
|
+
*/
|
|
23
|
+
interface ProjectionField {
|
|
24
|
+
readonly as: string;
|
|
25
|
+
readonly schema: z.ZodType;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build an ad-hoc Zod object codec for a projection (a subset / rename of a table's columns). A full-row
|
|
29
|
+
* read decodes through the driver's `TableDef`; a *projection* isn't a full row, so this assembles a
|
|
30
|
+
* codec from exactly the selected columns' schemas.
|
|
31
|
+
*/
|
|
32
|
+
declare function projectionSchema(fields: readonly ProjectionField[]): z.ZodObject<Record<string, z.ZodType>>;
|
|
33
|
+
/** Decode raw projected rows (DB wire → app values) through the ad-hoc projection codec. */
|
|
34
|
+
declare function decodeProjection<T = Record<string, unknown>>(fields: readonly ProjectionField[], rows: readonly unknown[]): T[];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The neutral carrier a driver's field reference extends so the core projection inference can read its
|
|
38
|
+
* app-value type — the cross-driver contract for `@better-schemic/core/query`. Builders are driver-owned (each
|
|
39
|
+
* driver ships its own `FieldRef` with its own operators at `@better-schemic/<driver>/query`); the ONE thing
|
|
40
|
+
* core needs from any such ref is the *decoded app value* it stands for, carried here as a phantom.
|
|
41
|
+
*
|
|
42
|
+
* A driver's ref does: `interface SurrealRef<T> extends FieldRefBase<T> { eq(v: T): Expr; … }`.
|
|
43
|
+
* `Project` (./project) then reads `T` back out of any ref in a returned projection shape.
|
|
44
|
+
*/
|
|
45
|
+
declare const REF_VALUE: unique symbol;
|
|
46
|
+
interface FieldRefBase<T> {
|
|
47
|
+
/** Phantom — the decoded app-value type this ref projects to. Never present at runtime. */
|
|
48
|
+
readonly [REF_VALUE]: T;
|
|
49
|
+
}
|
|
50
|
+
/** The app-value type carried by a field ref (`never` if it isn't one). */
|
|
51
|
+
type RefValue<R> = R extends FieldRefBase<infer T> ? T : never;
|
|
52
|
+
/**
|
|
53
|
+
* Brand a driver's field-ref implementation with the neutral {@link FieldRefBase} carrier, so `Project`
|
|
54
|
+
* can read its value back out. The phantom symbol is module-private on purpose (drivers can't forge it),
|
|
55
|
+
* so this is the one sanctioned bridge — a runtime identity that saves every driver an `as unknown as`
|
|
56
|
+
* cast. `impl`'s type is inferred; `T` (the carried value type) defaults to `unknown` because refs are
|
|
57
|
+
* untyped at runtime — the per-field type is supplied by the driver's `Row` mapping. Usage:
|
|
58
|
+
* `return brandRef({ eq, neq, gte, … });`.
|
|
59
|
+
*/
|
|
60
|
+
declare function brandRef<I extends object, T = unknown>(impl: I): I & FieldRefBase<T>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Infer the result element type of a `.return(row => P)` projection: replace every field ref in the
|
|
64
|
+
* returned shape `P` with the decoded app value it carries, recursing into nested objects and arrays.
|
|
65
|
+
* This is the type-level half of result typing (surqlize's `InheritableIntoType` analog); the runtime
|
|
66
|
+
* half is the projection codec (./codec). Generic over ANY driver ref — it only reads the
|
|
67
|
+
* `FieldRefBase` carrier, never a concrete driver type.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* type R = Project<{ name: Ref<string>; meta: { at: Ref<Date> } }>;
|
|
71
|
+
* // ^? { name: string; meta: { at: Date } }
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* Refs are matched BEFORE the generic object branch, so a ref (which is itself an object carrying
|
|
75
|
+
* operator methods) is unwrapped to its value rather than mapped field-by-field.
|
|
76
|
+
*/
|
|
77
|
+
type Project<P> = P extends FieldRefBase<infer T> ? T : P extends readonly (infer E)[] ? Project<E>[] : P extends object ? {
|
|
78
|
+
[K in keyof P]: Project<P[K]>;
|
|
79
|
+
} : P;
|
|
80
|
+
|
|
81
|
+
export { CallableFunctions, type FieldRefBase, type Project, type ProjectionField, type RefValue, brandRef, callFunction, decodeProjection, projectionSchema };
|
package/lib/query.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/query/call.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
async function callFunction(callable, conn, name, args, returns) {
|
|
4
|
+
const raw = await callable.invoke(conn, name, args);
|
|
5
|
+
return z.decode(returns, raw);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/query/codec.ts
|
|
9
|
+
import { z as z2 } from "zod";
|
|
10
|
+
function projectionSchema(fields) {
|
|
11
|
+
const shape = {};
|
|
12
|
+
for (const f of fields) shape[f.as] = f.schema;
|
|
13
|
+
return z2.object(shape);
|
|
14
|
+
}
|
|
15
|
+
function decodeProjection(fields, rows) {
|
|
16
|
+
const schema = projectionSchema(fields);
|
|
17
|
+
return rows.map((r) => z2.decode(schema, r));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/query/ref.ts
|
|
21
|
+
function brandRef(impl) {
|
|
22
|
+
return impl;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
brandRef,
|
|
26
|
+
callFunction,
|
|
27
|
+
decodeProjection,
|
|
28
|
+
projectionSchema
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=query.js.map
|
package/lib/query.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/query/call.ts","../src/query/codec.ts","../src/query/ref.ts"],"sourcesContent":["import { z } from \"zod\";\nimport type { CallableFunctions } from \"../driver/driver\";\n\n/**\n * Invoke a defined DB function via the driver's `callable` capability and **decode its result through the\n * function's `.returns(R)` schema** — the neutral half of the query layer's (B) `.call()`. A driver's\n * `defineFunction(args).returns(R).call(db, appArgs)` composes this: it encodes `appArgs` to wire (via\n * the arg schemas) and passes `R` here. Decode-by-default is the differentiator — results come back as\n * real `App` types (`Date`, `RecordId`, …), not wire. A `.raw()` path skips this and returns\n * `callable.invoke(...)` directly.\n */\nexport async function callFunction<S extends z.ZodType>(\n callable: CallableFunctions,\n conn: unknown,\n name: string,\n args: Record<string, unknown>,\n returns: S,\n): Promise<z.output<S>> {\n const raw = await callable.invoke(conn, name, args);\n return z.decode(returns, raw as never);\n}\n","import { z } from \"zod\";\n\n/**\n * One selected projection column: the output key (`as`) plus the source Zod schema to decode it through.\n * The schema may itself be a `z.object(...)` for a nested projection — the driver's builder assembles the\n * tree; core just decodes it.\n */\nexport interface ProjectionField {\n readonly as: string;\n readonly schema: z.ZodType;\n}\n\n/**\n * Build an ad-hoc Zod object codec for a projection (a subset / rename of a table's columns). A full-row\n * read decodes through the driver's `TableDef`; a *projection* isn't a full row, so this assembles a\n * codec from exactly the selected columns' schemas.\n */\nexport function projectionSchema(\n fields: readonly ProjectionField[],\n): z.ZodObject<Record<string, z.ZodType>> {\n const shape: Record<string, z.ZodType> = {};\n for (const f of fields) shape[f.as] = f.schema;\n return z.object(shape);\n}\n\n/** Decode raw projected rows (DB wire → app values) through the ad-hoc projection codec. */\nexport function decodeProjection<T = Record<string, unknown>>(\n fields: readonly ProjectionField[],\n rows: readonly unknown[],\n): T[] {\n const schema = projectionSchema(fields);\n return rows.map((r) => z.decode(schema, r as never) as T);\n}\n","/**\n * The neutral carrier a driver's field reference extends so the core projection inference can read its\n * app-value type — the cross-driver contract for `@better-schemic/core/query`. Builders are driver-owned (each\n * driver ships its own `FieldRef` with its own operators at `@better-schemic/<driver>/query`); the ONE thing\n * core needs from any such ref is the *decoded app value* it stands for, carried here as a phantom.\n *\n * A driver's ref does: `interface SurrealRef<T> extends FieldRefBase<T> { eq(v: T): Expr; … }`.\n * `Project` (./project) then reads `T` back out of any ref in a returned projection shape.\n */\ndeclare const REF_VALUE: unique symbol;\n\nexport interface FieldRefBase<T> {\n /** Phantom — the decoded app-value type this ref projects to. Never present at runtime. */\n readonly [REF_VALUE]: T;\n}\n\n/** The app-value type carried by a field ref (`never` if it isn't one). */\nexport type RefValue<R> = R extends FieldRefBase<infer T> ? T : never;\n\n/**\n * Brand a driver's field-ref implementation with the neutral {@link FieldRefBase} carrier, so `Project`\n * can read its value back out. The phantom symbol is module-private on purpose (drivers can't forge it),\n * so this is the one sanctioned bridge — a runtime identity that saves every driver an `as unknown as`\n * cast. `impl`'s type is inferred; `T` (the carried value type) defaults to `unknown` because refs are\n * untyped at runtime — the per-field type is supplied by the driver's `Row` mapping. Usage:\n * `return brandRef({ eq, neq, gte, … });`.\n */\nexport function brandRef<I extends object, T = unknown>(\n impl: I,\n): I & FieldRefBase<T> {\n return impl as I & FieldRefBase<T>;\n}\n"],"mappings":";AAAA,SAAS,SAAS;AAWlB,eAAsB,aACpB,UACA,MACA,MACA,MACA,SACsB;AACtB,QAAM,MAAM,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI;AAClD,SAAO,EAAE,OAAO,SAAS,GAAY;AACvC;;;ACpBA,SAAS,KAAAA,UAAS;AAiBX,SAAS,iBACd,QACwC;AACxC,QAAM,QAAmC,CAAC;AAC1C,aAAW,KAAK,OAAQ,OAAM,EAAE,EAAE,IAAI,EAAE;AACxC,SAAOA,GAAE,OAAO,KAAK;AACvB;AAGO,SAAS,iBACd,QACA,MACK;AACL,QAAM,SAAS,iBAAiB,MAAM;AACtC,SAAO,KAAK,IAAI,CAAC,MAAMA,GAAE,OAAO,QAAQ,CAAU,CAAM;AAC1D;;;ACLO,SAAS,SACd,MACqB;AACrB,SAAO;AACT;","names":["z"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** An author-time reference to a secret, resolved to its value at apply time — never the value itself. */
|
|
2
|
+
interface SecretRef {
|
|
3
|
+
/** `env` → resolved from `process.env`; `secret` → resolved from the configured {@link SecretProvider}. */
|
|
4
|
+
readonly kind: "env" | "secret";
|
|
5
|
+
/** The environment-variable / secret name to resolve at apply. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
}
|
|
8
|
+
/** Bind a value to an environment variable, resolved at apply from `process.env[name]`. */
|
|
9
|
+
declare function env(name: string): SecretRef;
|
|
10
|
+
/** Bind a value to a named secret, resolved at apply from the configured {@link SecretProvider}. */
|
|
11
|
+
declare function secret(name: string): SecretRef;
|
|
12
|
+
/** Runtime guard: is `v` a {@link SecretRef} (vs a raw `string` literal key)? */
|
|
13
|
+
declare function isSecretRef(v: unknown): v is SecretRef;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves {@link SecretRef}s to their values at apply time. Pluggable: the default
|
|
16
|
+
* {@link envSecretProvider} reads every ref from `process.env`; swap it for a vault / file source by
|
|
17
|
+
* passing a custom provider to the apply layer. The resolved value is handed to the DB as a BOUND
|
|
18
|
+
* PARAMETER — never string-spliced into the DDL.
|
|
19
|
+
*/
|
|
20
|
+
interface SecretProvider {
|
|
21
|
+
resolve(ref: SecretRef): string | Promise<string>;
|
|
22
|
+
}
|
|
23
|
+
/** Default provider: resolves every {@link SecretRef} from `process.env[ref.name]`; throws if unset. */
|
|
24
|
+
declare const envSecretProvider: SecretProvider;
|
|
25
|
+
|
|
26
|
+
export { type SecretProvider as S, type SecretRef as a, envSecretProvider as b, env as e, isSecretRef as i, secret as s };
|
package/lib/testing.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { K as KindRegistry, D as Driver } from './driver-LVldBEhS.js';
|
|
2
|
+
import './config-BYh7WA4P.js';
|
|
3
|
+
import 'jiti';
|
|
4
|
+
import './secrets-BETi5p8g.js';
|
|
5
|
+
import 'commander';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The driver's authoring namespace (`s`) — a bag of field builders, some NESTED (e.g.
|
|
9
|
+
* `s.iso.{date,time,datetime,duration}`). Intentionally loose: the suite duck-types fields and
|
|
10
|
+
* enforces the real contract at RUNTIME, not via this type (see the header note), so a precise
|
|
11
|
+
* "function-or-nested-namespace" shape would only fight the internal `s.<key>()` calls for no gain.
|
|
12
|
+
*/
|
|
13
|
+
type Authoring = Record<string, any>;
|
|
14
|
+
interface DriverConformanceOptions {
|
|
15
|
+
/** The driver's registry name (e.g. `"surrealdb"`). */
|
|
16
|
+
name: string;
|
|
17
|
+
/** The driver's authoring namespace — the `s` each package exports. */
|
|
18
|
+
s: Authoring;
|
|
19
|
+
/** The driver under test (already registered by importing its package). */
|
|
20
|
+
driver: Driver<unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Authors the driver's primary fielded definable — a table, collection, node-type, … — from a name
|
|
23
|
+
* and a field shape. Used to lower a probe object through the pipeline. Drivers pass their own
|
|
24
|
+
* `define*` for this (e.g. `defineEntity: defineTable`); the suite stays shape-agnostic.
|
|
25
|
+
*/
|
|
26
|
+
defineEntity: (name: string, shape: Record<string, any>) => any;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Assert a `@better-schemic/<driver>` conforms to the Better-schemic driver contract: the Driver is registered with
|
|
30
|
+
* the IR pipeline + execution ops, and its `s` is a Zod-drop-in SUPERSET (the canonical drop-in set is
|
|
31
|
+
* present, carries the right schemas, composes through wrappers, and lowers to the portable IR).
|
|
32
|
+
*/
|
|
33
|
+
declare function describeDriverConformance(opts: DriverConformanceOptions): void;
|
|
34
|
+
/** Coverage status, mirroring the `docs/COVERAGE.md` checkbox: full round-trip / partial / not done. */
|
|
35
|
+
type CoverageStatus = "x" | "~" | " ";
|
|
36
|
+
/** A registered KIND and the round-trip status it claims. */
|
|
37
|
+
interface KindCoverage {
|
|
38
|
+
name: string;
|
|
39
|
+
status: CoverageStatus;
|
|
40
|
+
note?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A finer-grained FEATURE within a kind, and (when done) the test that proves it. */
|
|
43
|
+
interface FeatureCoverage {
|
|
44
|
+
key: string;
|
|
45
|
+
kind: string;
|
|
46
|
+
status: CoverageStatus;
|
|
47
|
+
/** A substring of the test title that exercises this feature — REQUIRED when status is `x`. */
|
|
48
|
+
coveredBy?: string;
|
|
49
|
+
note?: string;
|
|
50
|
+
}
|
|
51
|
+
/** The live inputs a reconcile runs against (the pure form — no `bun:test`, no filesystem). */
|
|
52
|
+
interface CoverageReconcileInput {
|
|
53
|
+
registry: KindRegistry;
|
|
54
|
+
kinds: KindCoverage[];
|
|
55
|
+
features: FeatureCoverage[];
|
|
56
|
+
/** Concatenated source of the driver's `*.test.ts` — real test titles are extracted from it. */
|
|
57
|
+
testSrc: string;
|
|
58
|
+
}
|
|
59
|
+
/** One named check and the assertions it failed (empty = passed). */
|
|
60
|
+
interface CoverageCheck {
|
|
61
|
+
name: string;
|
|
62
|
+
failures: string[];
|
|
63
|
+
}
|
|
64
|
+
/** The reconcile outcome: per-check breakdown + a flattened failure list for a single-assert test. */
|
|
65
|
+
interface CoverageReconcileResult {
|
|
66
|
+
checks: CoverageCheck[];
|
|
67
|
+
failures: string[];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reconcile a driver's declared coverage against the live registry + tests. PURE — returns a per-check
|
|
71
|
+
* breakdown; {@link describeCoverageReconcile} is the `bun:test` shell around it. See the section header
|
|
72
|
+
* for what is (and deliberately isn't) checked.
|
|
73
|
+
*/
|
|
74
|
+
declare function reconcileCoverage(input: CoverageReconcileInput): CoverageReconcileResult;
|
|
75
|
+
/** Options for the `bun:test` reconcile shell. Supply `testDir` (read for you) OR a pre-read `testSrc`. */
|
|
76
|
+
interface CoverageReconcileOptions {
|
|
77
|
+
/** Label for the describe block (typically the driver name). */
|
|
78
|
+
name?: string;
|
|
79
|
+
registry: KindRegistry;
|
|
80
|
+
kinds: KindCoverage[];
|
|
81
|
+
features: FeatureCoverage[];
|
|
82
|
+
/** The dir holding this driver's `*.test.ts`; the helper concatenates them to prove test coverage. */
|
|
83
|
+
testDir?: string;
|
|
84
|
+
/** Pre-read test source, if you'd rather gather it yourself (alternative to `testDir`). */
|
|
85
|
+
testSrc?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Register the coverage reconcile as a `bun:test` block — one named `test(...)` per check, so CI reads
|
|
89
|
+
* granularly. A driver calls this from a single `*.test.ts` with its manifest + `testDir`:
|
|
90
|
+
*
|
|
91
|
+
* import { describeCoverageReconcile } from "@better-schemic/core/testing";
|
|
92
|
+
* import { registry } from "../src/kinds";
|
|
93
|
+
* import { KIND_MANIFEST, FEATURE_MANIFEST } from "./coverage-manifest";
|
|
94
|
+
* describeCoverageReconcile({ name: "sqlite", registry, kinds: KIND_MANIFEST,
|
|
95
|
+
* features: FEATURE_MANIFEST, testDir: import.meta.dir });
|
|
96
|
+
*/
|
|
97
|
+
declare function describeCoverageReconcile(opts: CoverageReconcileOptions): void;
|
|
98
|
+
|
|
99
|
+
export { type CoverageCheck, type CoverageReconcileInput, type CoverageReconcileOptions, type CoverageReconcileResult, type CoverageStatus, type DriverConformanceOptions, type FeatureCoverage, type KindCoverage, describeCoverageReconcile, describeDriverConformance, reconcileCoverage };
|
package/lib/testing.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
driverNames,
|
|
3
|
+
emitKinds,
|
|
4
|
+
getDriver,
|
|
5
|
+
lowerSchema
|
|
6
|
+
} from "./chunk-IUPOUD4L.js";
|
|
7
|
+
|
|
8
|
+
// src/testing.ts
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { readdirSync, readFileSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var DROP_INS = [
|
|
13
|
+
{ key: "string", build: (s) => s.string() },
|
|
14
|
+
{ key: "number", build: (s) => s.number() },
|
|
15
|
+
{ key: "boolean", build: (s) => s.boolean() },
|
|
16
|
+
{ key: "date", build: (s) => s.date() },
|
|
17
|
+
{ key: "literal", build: (s) => s.literal("a") },
|
|
18
|
+
{ key: "enum", build: (s) => s.enum(["a", "b"]) },
|
|
19
|
+
{ key: "object", build: (s) => s.object({ inner: s.literal("a") }) },
|
|
20
|
+
{ key: "array", build: (s) => s.array(s.literal("a")) }
|
|
21
|
+
];
|
|
22
|
+
var SCALAR_CHECKS = [
|
|
23
|
+
{ key: "string", valid: "hello", invalid: 123 },
|
|
24
|
+
{ key: "number", valid: 123, invalid: "hello" },
|
|
25
|
+
{ key: "boolean", valid: true, invalid: "hello" }
|
|
26
|
+
];
|
|
27
|
+
function toSchema(v) {
|
|
28
|
+
const field = v;
|
|
29
|
+
if (field && isZod(field.schema)) return field.schema;
|
|
30
|
+
if (isZod(v)) return v;
|
|
31
|
+
throw new Error("expected a field (with a `.schema` Zod type) or a Zod type");
|
|
32
|
+
}
|
|
33
|
+
function isZod(v) {
|
|
34
|
+
return !!v && typeof v.safeParse === "function";
|
|
35
|
+
}
|
|
36
|
+
function isField(v) {
|
|
37
|
+
return isZod(v?.schema);
|
|
38
|
+
}
|
|
39
|
+
function describeDriverConformance(opts) {
|
|
40
|
+
const { name, s, driver, defineEntity } = opts;
|
|
41
|
+
describe(`driver conformance: ${name}`, () => {
|
|
42
|
+
describe("Driver contract", () => {
|
|
43
|
+
test("is registered under its name", () => {
|
|
44
|
+
expect(driverNames()).toContain(name);
|
|
45
|
+
expect(getDriver(name)).toBe(driver);
|
|
46
|
+
expect(driver.name).toBe(name);
|
|
47
|
+
});
|
|
48
|
+
test("exposes a kind registry + the schema/execution ops", () => {
|
|
49
|
+
expect(driver.registry).toBeDefined();
|
|
50
|
+
expect(typeof driver.registry.entries).toBe("function");
|
|
51
|
+
expect(driver.registry.names().length).toBeGreaterThan(0);
|
|
52
|
+
for (const op of [
|
|
53
|
+
"explode",
|
|
54
|
+
"introspectAll",
|
|
55
|
+
"connect",
|
|
56
|
+
"apply",
|
|
57
|
+
"close"
|
|
58
|
+
]) {
|
|
59
|
+
expect(typeof driver[op]).toBe("function");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("zod drop-in surface (s.* is a Zod superset)", () => {
|
|
64
|
+
for (const { key, build } of DROP_INS) {
|
|
65
|
+
test(`s.${key}() exists and returns a field`, () => {
|
|
66
|
+
expect(typeof s[key]).toBe("function");
|
|
67
|
+
const field = build(s);
|
|
68
|
+
expect(isField(field)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
for (const { key, valid, invalid } of SCALAR_CHECKS) {
|
|
72
|
+
test(`s.${key}() carries a "${key}" Zod schema`, () => {
|
|
73
|
+
const schema = toSchema(s[key]());
|
|
74
|
+
expect(schema.safeParse(valid).success).toBe(true);
|
|
75
|
+
expect(schema.safeParse(invalid).success).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
describe("Zod-clean codecs + wrappers", () => {
|
|
80
|
+
test("decode/encode delegate to the inner Zod schema", () => {
|
|
81
|
+
const field = s.string();
|
|
82
|
+
expect(field.decode("hi")).toBe("hi");
|
|
83
|
+
expect(field.encode("hi")).toBe("hi");
|
|
84
|
+
});
|
|
85
|
+
test("wrappers preserve field-ness (optional/array compose)", () => {
|
|
86
|
+
const field = s.string();
|
|
87
|
+
expect(isField(field.optional())).toBe(true);
|
|
88
|
+
expect(isField(field.array())).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe("lowering (drop-in fields \u2192 kind registry)", () => {
|
|
92
|
+
test("an entity of drop-in fields explodes + lowers + emits, carrying every field", () => {
|
|
93
|
+
const shape = {};
|
|
94
|
+
for (const { key, build } of DROP_INS) shape[`f_${key}`] = build(s);
|
|
95
|
+
const entity = defineEntity("better_schemic_conformance_probe", shape);
|
|
96
|
+
const portable = lowerSchema(
|
|
97
|
+
driver.registry,
|
|
98
|
+
driver.explode([entity], [])
|
|
99
|
+
);
|
|
100
|
+
expect(portable.length).toBeGreaterThan(0);
|
|
101
|
+
const ddl = emitKinds(driver.registry, portable).join("\n");
|
|
102
|
+
expect(ddl.length).toBeGreaterThan(0);
|
|
103
|
+
for (const { key } of DROP_INS) {
|
|
104
|
+
expect(ddl).toContain(`f_${key}`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function extractTestTitles(src) {
|
|
111
|
+
const re = /\b(?:test|it)(\.[\w.]+)?\s*\(\s*(["'`])((?:\\.|(?!\2)[\s\S])*?)\2/g;
|
|
112
|
+
const titles = [];
|
|
113
|
+
for (const m of src.matchAll(re)) {
|
|
114
|
+
const modifier = m[1] ?? "";
|
|
115
|
+
if (/\.(?:skip|todo)\b/.test(modifier)) continue;
|
|
116
|
+
titles.push(m[3]);
|
|
117
|
+
}
|
|
118
|
+
return titles;
|
|
119
|
+
}
|
|
120
|
+
function reconcileCoverage(input) {
|
|
121
|
+
const { registry, kinds, features, testSrc } = input;
|
|
122
|
+
const registered = new Set(registry.names());
|
|
123
|
+
const checks = [];
|
|
124
|
+
const declared = new Set(kinds.map((k) => k.name));
|
|
125
|
+
const kindsFailures = [];
|
|
126
|
+
for (const name of registered)
|
|
127
|
+
if (!declared.has(name))
|
|
128
|
+
kindsFailures.push(
|
|
129
|
+
`kind "${name}" is registered but missing from the manifest`
|
|
130
|
+
);
|
|
131
|
+
for (const name of declared)
|
|
132
|
+
if (!registered.has(name))
|
|
133
|
+
kindsFailures.push(
|
|
134
|
+
`kind "${name}" is in the manifest but not registered`
|
|
135
|
+
);
|
|
136
|
+
checks.push({
|
|
137
|
+
name: "registered kinds match the manifest",
|
|
138
|
+
failures: kindsFailures
|
|
139
|
+
});
|
|
140
|
+
checks.push({
|
|
141
|
+
name: "every feature maps to a registered kind",
|
|
142
|
+
failures: features.filter((f) => !registered.has(f.kind)).map((f) => `feature "${f.key}" -> unknown kind "${f.kind}"`)
|
|
143
|
+
});
|
|
144
|
+
const titles = extractTestTitles(testSrc);
|
|
145
|
+
const coverFailures = [];
|
|
146
|
+
for (const f of features) {
|
|
147
|
+
if (f.status !== "x") continue;
|
|
148
|
+
if (!f.coveredBy) {
|
|
149
|
+
coverFailures.push(
|
|
150
|
+
`feature "${f.key}" is [x] but declares no coveredBy test`
|
|
151
|
+
);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!titles.some((t) => t.includes(f.coveredBy)))
|
|
155
|
+
coverFailures.push(
|
|
156
|
+
`feature "${f.key}" coveredBy "${f.coveredBy}" \u2014 no matching (non-skipped) test title`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
checks.push({
|
|
160
|
+
name: "every [x] feature names a covering test",
|
|
161
|
+
failures: coverFailures
|
|
162
|
+
});
|
|
163
|
+
checks.push({
|
|
164
|
+
name: "no duplicate feature keys",
|
|
165
|
+
failures: duplicates(
|
|
166
|
+
features.map((f) => f.key),
|
|
167
|
+
"feature key"
|
|
168
|
+
)
|
|
169
|
+
});
|
|
170
|
+
checks.push({
|
|
171
|
+
name: "no duplicate kind entries",
|
|
172
|
+
failures: duplicates(
|
|
173
|
+
kinds.map((k) => k.name),
|
|
174
|
+
"kind entry"
|
|
175
|
+
)
|
|
176
|
+
});
|
|
177
|
+
return { checks, failures: checks.flatMap((c) => c.failures) };
|
|
178
|
+
}
|
|
179
|
+
function duplicates(names, label) {
|
|
180
|
+
const seen = /* @__PURE__ */ new Set();
|
|
181
|
+
const dup = [];
|
|
182
|
+
for (const n of names) {
|
|
183
|
+
if (seen.has(n)) dup.push(`duplicate ${label} "${n}"`);
|
|
184
|
+
seen.add(n);
|
|
185
|
+
}
|
|
186
|
+
return dup;
|
|
187
|
+
}
|
|
188
|
+
function describeCoverageReconcile(opts) {
|
|
189
|
+
const { name, registry, kinds, features } = opts;
|
|
190
|
+
const testSrc = opts.testSrc ?? (opts.testDir ? readTestSrc(opts.testDir) : "");
|
|
191
|
+
describe(name ? `coverage reconcile: ${name}` : "coverage reconcile", () => {
|
|
192
|
+
for (const check of reconcileCoverage({
|
|
193
|
+
registry,
|
|
194
|
+
kinds,
|
|
195
|
+
features,
|
|
196
|
+
testSrc
|
|
197
|
+
}).checks) {
|
|
198
|
+
test(check.name, () => {
|
|
199
|
+
expect(check.failures).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function readTestSrc(dir) {
|
|
205
|
+
return readdirSync(dir).filter((f) => f.endsWith(".test.ts")).map((f) => readFileSync(join(dir, f), "utf8")).join("\n");
|
|
206
|
+
}
|
|
207
|
+
export {
|
|
208
|
+
describeCoverageReconcile,
|
|
209
|
+
describeDriverConformance,
|
|
210
|
+
reconcileCoverage
|
|
211
|
+
};
|
|
212
|
+
//# sourceMappingURL=testing.js.map
|