@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The neutral MULTI-CONNECTION contract (design: docs/MULTI-CONNECTION.md). A project's config maps
|
|
2
|
+
// names to CONNECTIONS; each is produced by a per-driver `<driver>Connection(...)` factory that wraps
|
|
3
|
+
// {@link connectionEntry} with its own typed connection shape. Everything here is dialect-free — the
|
|
4
|
+
// CLI reads only these neutral fields; driver-specific connection params ride on the driver's own
|
|
5
|
+
// config type. The resolution engine (lazy DAG, fan-out, addressing) lives in the CLI layer.
|
|
6
|
+
|
|
7
|
+
// Type-only (erased): the resolved per-connection config a factory-embedded `client` opener receives.
|
|
8
|
+
import type { ResolvedConfig } from "./cli-kit/config";
|
|
9
|
+
|
|
10
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
11
|
+
|
|
12
|
+
/** Minimal Standard Schema v1 surface — what a connection's `args` schema must expose. */
|
|
13
|
+
export interface StandardSchemaLike {
|
|
14
|
+
"~standard": {
|
|
15
|
+
validate(
|
|
16
|
+
value: unknown,
|
|
17
|
+
): MaybePromise<
|
|
18
|
+
{ value: unknown } | { issues: readonly { message: string }[] }
|
|
19
|
+
>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The dialect-neutral fields the orchestration reads off every connection config. */
|
|
24
|
+
export interface ConnectionConfigBase {
|
|
25
|
+
/** Schema dir (the desired state + its migration files/snapshot). Shared dir = shared schema. */
|
|
26
|
+
schema: string;
|
|
27
|
+
/** Optional DISPLAY label for this config within a bulk (array) resolution — reporting/logs only. */
|
|
28
|
+
key?: string;
|
|
29
|
+
/** Migrations dir override; defaults relative to `schema`. */
|
|
30
|
+
migrations?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A live, queryable handle to ANOTHER (already-resolved) connection, for use inside a resolver. */
|
|
34
|
+
export interface ResolvedConnectionHandle {
|
|
35
|
+
query<T = unknown>(sql: string, vars?: Record<string, unknown>): Promise<T[]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What a connection RESOLVER receives. `connections` is a LAZY proxy of the other connections —
|
|
40
|
+
* touching one resolves + connects it on demand (so the dependency graph falls out of access; cycles
|
|
41
|
+
* error). `args` are CLI `--arg k=v` values (so a resolver can yield a SUBSET without resolving all).
|
|
42
|
+
*/
|
|
43
|
+
export interface ResolveContext {
|
|
44
|
+
connections: Record<string, ResolvedConnectionHandle>;
|
|
45
|
+
env: NodeJS.ProcessEnv;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The opaque, branded output of a `<driver>Connection(...)` factory — the only thing `defineConfig`'s
|
|
50
|
+
* `connections` map accepts. Never hand-authored. `driver` is the package the CLI dynamically loads;
|
|
51
|
+
* `resolve` always normalizes to an ARRAY (a single connection -> one element, a collection -> many).
|
|
52
|
+
*/
|
|
53
|
+
export interface ConnectionEntry<Client = unknown, Args = undefined> {
|
|
54
|
+
readonly __betterSchemic: "connection";
|
|
55
|
+
readonly driver: string;
|
|
56
|
+
resolve(ctx: ResolveContext, args?: Args): Promise<ConnectionConfigBase[]>;
|
|
57
|
+
/**
|
|
58
|
+
* Lazily open this connection's bound ORM CLIENT for a resolved config — embedded by the driver
|
|
59
|
+
* factory (with a lazy `import()` of its own client module, so authoring a config never pulls the
|
|
60
|
+
* engine). This is what powers the typed `config.connect(name)` on `defineConfig`'s return.
|
|
61
|
+
*/
|
|
62
|
+
client?(config: ResolvedConfig): Promise<Client>;
|
|
63
|
+
/**
|
|
64
|
+
* Dialect-specific DISPLAY identity for a resolved config (bulk reporting / errors / logs) —
|
|
65
|
+
* e.g. surreal `ns/db`. Precedence: config `key` > this hook > positional `name[i]`.
|
|
66
|
+
*/
|
|
67
|
+
label?(config: ResolvedConfig): string;
|
|
68
|
+
/** PHANTOM (never assigned) — anchors `Client`/`Args` so `config.connect` can infer them per entry. */
|
|
69
|
+
readonly __types?: { client: Client; args: Args };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Cross-driver erasure of the entry generics (like `AnyField`) — the shape neutral maps hold. */
|
|
73
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-driver erasure of the per-entry client/args types.
|
|
74
|
+
export type AnyConnectionEntry = ConnectionEntry<any, any>;
|
|
75
|
+
|
|
76
|
+
/** A connection factory's input: a static config, or a resolver yielding one config or a keyed collection. */
|
|
77
|
+
export type ConnectionInput<C extends ConnectionConfigBase, Args = undefined> =
|
|
78
|
+
| C
|
|
79
|
+
| ((ctx: ResolveContext, args: Args) => MaybePromise<C | C[]>);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a {@link ConnectionEntry} from a driver tag + a static config or resolver — the primitive each
|
|
83
|
+
* driver package wraps in its typed `<driver>Connection(...)` factory (which fixes `C` to the driver's
|
|
84
|
+
* own connection shape and overloads the array form to require `key`). Returns a branded entry whose
|
|
85
|
+
* `resolve` always yields an array. `extras` carries the factory-embedded client opener (for
|
|
86
|
+
* `config.connect`) and the optional args schema.
|
|
87
|
+
*/
|
|
88
|
+
export function connectionEntry<
|
|
89
|
+
C extends ConnectionConfigBase,
|
|
90
|
+
Client = unknown,
|
|
91
|
+
Args = undefined,
|
|
92
|
+
>(
|
|
93
|
+
driver: string,
|
|
94
|
+
input: ConnectionInput<C, Args>,
|
|
95
|
+
extras?: {
|
|
96
|
+
client?: (config: ResolvedConfig) => Promise<Client>;
|
|
97
|
+
label?: (config: ResolvedConfig) => string;
|
|
98
|
+
},
|
|
99
|
+
): ConnectionEntry<Client, Args> {
|
|
100
|
+
return {
|
|
101
|
+
__betterSchemic: "connection",
|
|
102
|
+
driver,
|
|
103
|
+
...(extras?.client ? { client: extras.client } : {}),
|
|
104
|
+
...(extras?.label ? { label: extras.label } : {}),
|
|
105
|
+
async resolve(ctx, args) {
|
|
106
|
+
const out =
|
|
107
|
+
typeof input === "function" ? await input(ctx, args as Args) : input;
|
|
108
|
+
return Array.isArray(out) ? out : [out];
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Type guard: is a `connections` map value a real factory output (vs a stray object)? */
|
|
114
|
+
export function isConnectionEntry(v: unknown): v is ConnectionEntry {
|
|
115
|
+
return (
|
|
116
|
+
typeof v === "object" &&
|
|
117
|
+
v !== null &&
|
|
118
|
+
(v as { __betterSchemic?: unknown }).__betterSchemic === "connection"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// The DRIVER interface — the dialect seam (see docs/MULTI-DB-SPIKE.md).
|
|
2
|
+
//
|
|
3
|
+
// Everything dialect-specific lives behind a `Driver`: lowering authoring to the Struct-IR, emitting
|
|
4
|
+
// DDL, introspecting a live DB, normalizing to a canonical form, and executing. Everything ABOVE the
|
|
5
|
+
// driver (the diff algorithm, the magicast TS-merge, the migration model, the CLI shell) stays
|
|
6
|
+
// dialect-free and calls these ops.
|
|
7
|
+
//
|
|
8
|
+
// The connection type is a driver-private parameter `Conn`: the orchestration treats it opaquely and
|
|
9
|
+
// only ever hands it back to the SAME driver. So the Surreal driver is `Driver<Surreal>`, and core
|
|
10
|
+
// never sees the concrete type. The AUTHORING
|
|
11
|
+
// types (`Tbl`/`Def`) are driver-private the same way — opaque to core beyond the neutral
|
|
12
|
+
// `Authored`/`AuthoredDef` bounds — so the neutral engine never names a dialect's concrete builder
|
|
13
|
+
// (`TableDef`/`StandaloneDef`).
|
|
14
|
+
|
|
15
|
+
import type { ResolvedConfig } from "../cli-kit/config";
|
|
16
|
+
import type { Diff } from "../cli-kit/diff";
|
|
17
|
+
import type { Filter } from "../cli-kit/filter";
|
|
18
|
+
import type { PullPlan } from "../cli-kit/merge";
|
|
19
|
+
import type { Definable, KindRegistry, PortableObject } from "../kind";
|
|
20
|
+
import type { SecretProvider, SecretRef } from "../secrets";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The dialect-NEUTRAL authoring contract — the only structure the orchestration reads off an
|
|
24
|
+
* authored object (everything else is opaque and handed straight to {@link Driver.explode}). A table
|
|
25
|
+
* contributes just its `name`; this is the upper bound for a driver's table-authoring type. The
|
|
26
|
+
* Surreal `TableDef` is a structural subtype, as is any future dialect's table builder.
|
|
27
|
+
*/
|
|
28
|
+
export interface Authored {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The neutral contract for a standalone (non-table) authored object — an event/function/access. It
|
|
34
|
+
* adds a `kind` discriminant and, for objects owned by a table (e.g. an event), the owner `table`
|
|
35
|
+
* name (so the snapshot can file-link a child object under its parent). The Surreal `StandaloneDef`
|
|
36
|
+
* union is a structural subtype.
|
|
37
|
+
*/
|
|
38
|
+
export interface AuthoredDef extends Authored {
|
|
39
|
+
readonly kind: string;
|
|
40
|
+
readonly table?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A single emitted DDL statement, structured: object identity (`kind`/`name`/`table`) + the dialect
|
|
45
|
+
* `ddl` string, plus an optional clause map (each value an `ALTER … <set>` form) for dialects that
|
|
46
|
+
* diff clause-level. `kind` is a dialect-defined string the orchestration treats opaquely — the
|
|
47
|
+
* SurrealDB `DefineStatement` (with its fixed kind union) is a structural subtype of this.
|
|
48
|
+
*/
|
|
49
|
+
export interface Statement {
|
|
50
|
+
kind: string;
|
|
51
|
+
name: string;
|
|
52
|
+
table?: string;
|
|
53
|
+
ddl: string;
|
|
54
|
+
clauses?: Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* Apply-time secret bindings for this statement's `$param` placeholders — `param` name ->
|
|
57
|
+
* a write-only {@link SecretRef}. Collected into {@link Diff.bindings}; the value never lives here
|
|
58
|
+
* (resolved at apply through a `SecretProvider`). See {@link Diff.bindings}.
|
|
59
|
+
*/
|
|
60
|
+
bindings?: Record<string, SecretRef>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Options for {@link Driver.emit} — mirrors the existing `DefineOptions` (e.g. IF NOT EXISTS). */
|
|
64
|
+
export interface EmitOptions {
|
|
65
|
+
ifNotExists?: boolean;
|
|
66
|
+
overwrite?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Options for {@link Driver.apply}. */
|
|
70
|
+
export interface ApplyOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Run the whole batch atomically. `migrate` wraps up/down + `_migrations` bookkeeping in one
|
|
73
|
+
* transaction; a driver that can't MUST surface that (the migration model degrades to best-effort).
|
|
74
|
+
*/
|
|
75
|
+
transactional?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Per-connection overrides (url/namespace/credentials) — superset across dialects. */
|
|
79
|
+
export interface ConnectionOverrides {
|
|
80
|
+
url?: string;
|
|
81
|
+
namespace?: string;
|
|
82
|
+
database?: string;
|
|
83
|
+
username?: string;
|
|
84
|
+
password?: string;
|
|
85
|
+
authLevel?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The direction a migration is applied in. */
|
|
89
|
+
export type MigrationDirection = "up" | "down";
|
|
90
|
+
|
|
91
|
+
/** A migration's bookkeeping identity, recorded in the migrations-tracking table. */
|
|
92
|
+
export interface MigrationRecord {
|
|
93
|
+
tag: string;
|
|
94
|
+
file: string;
|
|
95
|
+
/** sha of the migration file at apply time (drift detection). */
|
|
96
|
+
checksum: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The apply-time, dialect-specific half of the migration runner. The orchestration (which
|
|
101
|
+
* migrations are pending, ordering, the lock-then-loop) stays driver-neutral in cli/migrate.ts;
|
|
102
|
+
* this capability owns the SQL: the tracking table, the applied-records, the advisory lock, and the
|
|
103
|
+
* atomic apply+record. A driver WITHOUT it can't run migrations (diff/gen still work). `Conn` is the
|
|
104
|
+
* driver's own connection type.
|
|
105
|
+
*/
|
|
106
|
+
export interface MigrationStore<Conn = unknown> {
|
|
107
|
+
/** This dialect's migration-file extension, e.g. `".surql"` (SurrealDB). */
|
|
108
|
+
readonly extension: string;
|
|
109
|
+
/** Render a diff as this dialect's migration-file body (e.g. SurrealQL `IF $direction` up/down). */
|
|
110
|
+
render(tag: string, diff: Diff): string;
|
|
111
|
+
/** Ensure the migrations-tracking table exists. */
|
|
112
|
+
ensure(conn: Conn, table: string): Promise<void>;
|
|
113
|
+
/** Applied migrations: tag -> checksum recorded at apply time. */
|
|
114
|
+
applied(conn: Conn, table: string): Promise<Map<string, string>>;
|
|
115
|
+
/**
|
|
116
|
+
* Apply one migration's `up`/`down` PROGRAM plus its bookkeeping write atomically: on `up` record
|
|
117
|
+
* the migration, on `down` erase it — so the record is written iff the DDL actually applied.
|
|
118
|
+
*/
|
|
119
|
+
apply(
|
|
120
|
+
conn: Conn,
|
|
121
|
+
table: string,
|
|
122
|
+
m: {
|
|
123
|
+
content: string;
|
|
124
|
+
direction: MigrationDirection;
|
|
125
|
+
record: MigrationRecord;
|
|
126
|
+
},
|
|
127
|
+
): Promise<void>;
|
|
128
|
+
/** Record a migration as applied WITHOUT running its DDL (baseline of an existing DB). */
|
|
129
|
+
record(conn: Conn, table: string, record: MigrationRecord): Promise<void>;
|
|
130
|
+
/** Drop all applied records (baseline-squash reconcile). */
|
|
131
|
+
clear(conn: Conn, table: string): Promise<void>;
|
|
132
|
+
/** Take an advisory lock so two runs can't race — throws if already held. */
|
|
133
|
+
lock(conn: Conn, table: string): Promise<void>;
|
|
134
|
+
/** Release the advisory lock (idempotent). */
|
|
135
|
+
unlock(conn: Conn, table: string): Promise<void>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An OPTIONAL throwaway-instance capability for round-trip canonicalization and `sz check`'s
|
|
140
|
+
* migration replay. Absent -> `check`/replay-verification is degraded/unavailable (diff/apply still
|
|
141
|
+
* work, since a kind's `lower`/`introspectAll` already canonicalize).
|
|
142
|
+
*/
|
|
143
|
+
export interface ShadowCapability<Conn> {
|
|
144
|
+
/** Apply `ddl` to a fresh scratch DB, introspect it back to portable objects, then drop it. */
|
|
145
|
+
roundTrip(
|
|
146
|
+
conn: Conn,
|
|
147
|
+
config: ResolvedConfig,
|
|
148
|
+
ddl: string,
|
|
149
|
+
): Promise<PortableObject[]>;
|
|
150
|
+
/** Spin up a fully-isolated ephemeral instance (for migration replay). Caller must `stop()`. */
|
|
151
|
+
ephemeral?(): Promise<{ conn: Conn; stop: () => Promise<void> }>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* User-defined DB functions — the `.call` side of the query layer's (B) surface (DB functions as code).
|
|
156
|
+
* `invoke` calls a defined function by name with already-encoded args and returns the function's RAW
|
|
157
|
+
* result (the driver extracts it from its own response shape — surreal `RETURN fn::name($a)` yields the
|
|
158
|
+
* value; a row-returning call yields a row set). The caller decodes that raw value through the
|
|
159
|
+
* function's `.returns(R)` schema via `callFunction` in `@better-schemic/core/query`. A defined function still
|
|
160
|
+
* emits/migrates via the schema engine regardless; this capability only adds INVOCATION.
|
|
161
|
+
*/
|
|
162
|
+
export interface CallableFunctions<Conn = unknown> {
|
|
163
|
+
invoke(
|
|
164
|
+
conn: Conn,
|
|
165
|
+
name: string,
|
|
166
|
+
args: Record<string, unknown>,
|
|
167
|
+
): Promise<unknown>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- driver-contributed CLI commands -----------------------------------------------------------
|
|
171
|
+
// A driver may contribute dialect-specific commands invoked as `sc <kind> <verb> [args]` (e.g. surreal
|
|
172
|
+
// `sc access rotate <name>`). CORE provides only the general
|
|
173
|
+
// mechanism: it discovers `driver.commands`, registers each, parses argv against `args`, resolves the
|
|
174
|
+
// connection, and dispatches to `run` with a {@link CommandContext}. The DRIVER owns the dialect logic
|
|
175
|
+
// and the meaning of each kind/verb/arg — core never names one. Depth is fixed at kind/verb.
|
|
176
|
+
|
|
177
|
+
/** A declared argument for a {@link DriverCommand} — used for parsing + `--help`. */
|
|
178
|
+
export interface CommandArgs {
|
|
179
|
+
/**
|
|
180
|
+
* Positional args, in order. Core collects ALL positional tokens into a list (incl. raw `key=value`)
|
|
181
|
+
* and the driver interprets + validates arity; mark the last `variadic` for "one or more".
|
|
182
|
+
*/
|
|
183
|
+
positionals?: readonly {
|
|
184
|
+
name: string;
|
|
185
|
+
required?: boolean;
|
|
186
|
+
variadic?: boolean;
|
|
187
|
+
help?: string;
|
|
188
|
+
}[];
|
|
189
|
+
/** Named flags. `value: true` takes a value (`--user U`); otherwise it's a boolean (`--dry-run`). */
|
|
190
|
+
flags?: readonly {
|
|
191
|
+
name: string;
|
|
192
|
+
value?: boolean;
|
|
193
|
+
required?: boolean;
|
|
194
|
+
help?: string;
|
|
195
|
+
}[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** argv parsed against a command's {@link CommandArgs}: positional tokens (driver-interpreted) + flags. */
|
|
199
|
+
export interface ParsedCommandArgs {
|
|
200
|
+
/** Every positional token in order (incl. raw `key=value`); the driver interprets them. */
|
|
201
|
+
positionals: string[];
|
|
202
|
+
/** Flags: a value-flag -> its string, a boolean flag -> `true`, an absent flag -> `undefined`. */
|
|
203
|
+
flags: Record<string, string | boolean | undefined>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Output + input helpers core hands a command (so a driver never touches stdio directly). */
|
|
207
|
+
export interface CommandIo {
|
|
208
|
+
ok(message: string): void;
|
|
209
|
+
fail(message: string): void;
|
|
210
|
+
info(message: string): void;
|
|
211
|
+
/** Prompt for a line of input (`hidden` masks it) — for a sensitive value not passed inline (no shell-history leak). */
|
|
212
|
+
prompt(question: string, opts?: { hidden?: boolean }): Promise<string>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The context core hands a {@link DriverCommand.run}: a connected db + the resolved config + io + secrets. */
|
|
216
|
+
export interface CommandContext<Conn = unknown> {
|
|
217
|
+
conn: Conn;
|
|
218
|
+
config: ResolvedConfig;
|
|
219
|
+
io: CommandIo;
|
|
220
|
+
/** The configured secret provider (default reads `process.env`) for resolving {@link SecretRef}s. */
|
|
221
|
+
secrets: SecretProvider;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A driver-contributed CLI command — `sc <kind> <verb> [args]`. Core dispatches; the driver owns the
|
|
226
|
+
* dialect logic in {@link run}. The driver validates positional arity itself (core only collects them).
|
|
227
|
+
*/
|
|
228
|
+
export interface DriverCommand<Conn = unknown> {
|
|
229
|
+
/** Kind namespace — `sc <kind> …` (e.g. `"access"`, `"table"`). */
|
|
230
|
+
kind: string;
|
|
231
|
+
/** Verb under the kind — `sc <kind> <verb>` (e.g. `"rotate"`, `"check"`, `"find"`). */
|
|
232
|
+
verb: string;
|
|
233
|
+
/** One-line help shown in the command listing. */
|
|
234
|
+
summary: string;
|
|
235
|
+
/** Declared args for parsing + `--help`. */
|
|
236
|
+
args?: CommandArgs;
|
|
237
|
+
run(ctx: CommandContext<Conn>, args: ParsedCommandArgs): Promise<void>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A database dialect, expressed as a SET OF KINDS (core-v2). The driver registers its object kinds on
|
|
242
|
+
* `registry`; core orchestrates schema ops GENERICALLY over it (`lowerSchema`/`buildKindDiff`/
|
|
243
|
+
* `emitKinds`/`orderObjects`) — it never names a kind. The driver owns only what isn't generic: the
|
|
244
|
+
* authoring -> kinded `explode`, a single-read `introspectAll`, the connection lifecycle, and the
|
|
245
|
+
* dialect-specific command capabilities. The field/type substrate (`PortableType`/`s.*`) stays core.
|
|
246
|
+
* See docs/kind-registry-flip-plan.md.
|
|
247
|
+
*/
|
|
248
|
+
export interface Driver<
|
|
249
|
+
Conn = unknown,
|
|
250
|
+
Tbl extends Authored = Authored,
|
|
251
|
+
Def extends AuthoredDef = AuthoredDef,
|
|
252
|
+
> {
|
|
253
|
+
readonly name: string;
|
|
254
|
+
|
|
255
|
+
// --- kind registry (the schema engine) -----------------------------------------------------
|
|
256
|
+
/** This driver's registered KINDS. Core runs lower/diff/emit/order generically over it. */
|
|
257
|
+
readonly registry: KindRegistry;
|
|
258
|
+
/**
|
|
259
|
+
* Authoring (loaded `defineTable`/standalone defs) -> kinded {@link Definable}s. The driver-side
|
|
260
|
+
* fan-out: one inline-authored table explodes into `[table, ...index, ...event/constraint]`, each
|
|
261
|
+
* tagged with its `kind`. Core then lowers via `lowerSchema(registry, explode(...))` — so
|
|
262
|
+
* `KindEngine.lower` stays 1:1 and the contract needs no explode hook.
|
|
263
|
+
*/
|
|
264
|
+
explode(tables: Tbl[], defs: Def[]): Definable[];
|
|
265
|
+
/**
|
|
266
|
+
* Live connection -> ALL portable objects, fanned across kinds from ONE read (INFO STRUCTURE /
|
|
267
|
+
* the system catalog). Must canonicalize IDENTICALLY to lowering (a clean apply round-trips to a zero diff)
|
|
268
|
+
* and be COMPLETE (return every diffable kind, else presence-phantom-diffs). `exclude` skips tables
|
|
269
|
+
* by name.
|
|
270
|
+
*/
|
|
271
|
+
introspectAll(conn: Conn, exclude?: Set<string>): Promise<PortableObject[]>;
|
|
272
|
+
|
|
273
|
+
// --- execution -----------------------------------------------------------------------------
|
|
274
|
+
connect(config: ResolvedConfig, over?: ConnectionOverrides): Promise<Conn>;
|
|
275
|
+
apply(conn: Conn, statements: string[], opts?: ApplyOptions): Promise<void>;
|
|
276
|
+
/** Tear down a connection opened by {@link connect} (the orchestration owns the lifecycle). */
|
|
277
|
+
close(conn: Conn): Promise<void>;
|
|
278
|
+
|
|
279
|
+
// --- optional capabilities -----------------------------------------------------------------
|
|
280
|
+
readonly shadow?: ShadowCapability<Conn>;
|
|
281
|
+
/** Apply-time migration bookkeeping. Absent -> this driver can't run migrations (diff/gen still do). */
|
|
282
|
+
readonly migrations?: MigrationStore<Conn>;
|
|
283
|
+
|
|
284
|
+
// --- optional COMMAND capabilities ---------------------------------------------------------
|
|
285
|
+
// The dialect-agnostic CLI routes each schema-syncing command through one of these. A driver that
|
|
286
|
+
// omits a capability makes that command unavailable on it — the CLI never hardcodes `if surreal`.
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Diff the LIVE database against the loaded schema into executable up/down DDL. Owns every
|
|
290
|
+
* dialect-specific normalization and apply-time fixup (Surreal: a shadow-DB round-trip to cancel
|
|
291
|
+
* formatting noise, the redacted-access-key swap, and the implicit-wildcard OVERWRITE re-mark), so
|
|
292
|
+
* the result is safe to apply as-is. Backs `diff --live`, `push`, and the baseline reconcile.
|
|
293
|
+
*/
|
|
294
|
+
diffLive?(conn: Conn, config: ResolvedConfig, filter: Filter): Promise<Diff>;
|
|
295
|
+
/** Reduce a live diff (from {@link diffLive}) to the statements `push` applies; `prune: false` keeps removals. */
|
|
296
|
+
syncPlan?(diff: Diff, prune?: boolean): string[];
|
|
297
|
+
/**
|
|
298
|
+
* Dialect-specific CLI commands invoked as `sc <kind> <verb> [args]` — e.g. surreal `access rotate`.
|
|
299
|
+
* Core discovers + dispatches them generically (see {@link DriverCommand});
|
|
300
|
+
* it never names a kind/verb. Absent -> this driver contributes no extra commands.
|
|
301
|
+
*/
|
|
302
|
+
readonly commands?: readonly DriverCommand<Conn>[];
|
|
303
|
+
/**
|
|
304
|
+
* Render portable objects to per-file source in THIS dialect's `s.*` syntax, filtered — the codegen
|
|
305
|
+
* behind the offline `diff --ts` and `pull`. Takes `PortableObject[]` (this driver's own portable
|
|
306
|
+
* shape): `diff --ts` renders the SNAPSHOT side (stored portable) and the DESIRED side
|
|
307
|
+
* (`lowerSchema(explode(...))`) at MATCHING fidelity so an in-sync schema yields identical files;
|
|
308
|
+
* `pull` renders the introspected DB. The driver re-derives its structured form from the portable
|
|
309
|
+
* objects (parsing its own DDL where needed — docs/kind-registry-flip-plan.md §6b). `single` (a file
|
|
310
|
+
* key) folds everything into one module; otherwise `fileFor` maps each object to its own file.
|
|
311
|
+
*/
|
|
312
|
+
renderSchema?(
|
|
313
|
+
objects: PortableObject[],
|
|
314
|
+
filter: Filter,
|
|
315
|
+
fileFor: (kind: string, name: string) => string,
|
|
316
|
+
single?: string,
|
|
317
|
+
): Map<string, string>;
|
|
318
|
+
/**
|
|
319
|
+
* The two sides of `diff --ts --live` rendered to per-file source: the live DB (`current`) and the
|
|
320
|
+
* declared schema (`desired`), both normalized through the dialect so an unchanged schema yields
|
|
321
|
+
* identical files.
|
|
322
|
+
*/
|
|
323
|
+
diffTsLive?(
|
|
324
|
+
conn: Conn,
|
|
325
|
+
config: ResolvedConfig,
|
|
326
|
+
filter: Filter,
|
|
327
|
+
fileFor: (kind: string, name: string) => string,
|
|
328
|
+
single?: string,
|
|
329
|
+
): Promise<{ current: Map<string, string>; desired: Map<string, string> }>;
|
|
330
|
+
/**
|
|
331
|
+
* Replay every migration into a throwaway engine and diff the result against the schema (`check`).
|
|
332
|
+
* Owns ephemeral-engine selection + setup; `log` receives progress lines. Needs a {@link shadow}-
|
|
333
|
+
* class capability. An empty diff means the migrations reproduce the schema.
|
|
334
|
+
*/
|
|
335
|
+
checkReplay?(
|
|
336
|
+
config: ResolvedConfig,
|
|
337
|
+
over: ConnectionOverrides,
|
|
338
|
+
filter: Filter,
|
|
339
|
+
log: (msg: string) => void,
|
|
340
|
+
): Promise<Diff>;
|
|
341
|
+
/** Introspect the live DB and plan schema-file codegen (`pull`); writing is the neutral `applyPull`. */
|
|
342
|
+
planPull?(
|
|
343
|
+
conn: Conn,
|
|
344
|
+
config: ResolvedConfig,
|
|
345
|
+
opts: { filter: Filter; keepLocal?: boolean },
|
|
346
|
+
): Promise<PullPlan>;
|
|
347
|
+
/** A human-readable server identity for `doctor` (e.g. "SurrealDB 3.1.3"); throws if unreachable. */
|
|
348
|
+
serverInfo?(conn: Conn): Promise<string>;
|
|
349
|
+
/**
|
|
350
|
+
* Run a raw READ query and return rows — for connection RESOLVERS (a multi-connection resolver's
|
|
351
|
+
* `ctx.connections.<name>.query(...)`) and `seed`. The `sql` is this dialect's query language; the
|
|
352
|
+
* orchestration treats the rows opaquely. Absent -> a resolver can't read from this connection.
|
|
353
|
+
*/
|
|
354
|
+
query?<T = unknown>(
|
|
355
|
+
conn: Conn,
|
|
356
|
+
sql: string,
|
|
357
|
+
vars?: Record<string, unknown>,
|
|
358
|
+
): Promise<T[]>;
|
|
359
|
+
/**
|
|
360
|
+
* User-defined DB functions ((B) of the query layer) — invoke a defined function + decode the result.
|
|
361
|
+
* Absent -> no `.call()` surface on this driver (the function still emits/migrates). See
|
|
362
|
+
* {@link CallableFunctions}.
|
|
363
|
+
*/
|
|
364
|
+
readonly callable?: CallableFunctions<Conn>;
|
|
365
|
+
/**
|
|
366
|
+
* The dialect-specific files `better-schemic init` scaffolds, keyed by project-relative path: a
|
|
367
|
+
* connections-only `better-schemic.config.ts` (using this driver's `<driver>Connection` factory), a sample
|
|
368
|
+
* schema module in this dialect's `s.*`, a seed stub, a `.env.example`, … The CLI writes them
|
|
369
|
+
* verbatim (never overwriting) alongside the dialect-neutral migration snapshot it records itself.
|
|
370
|
+
* Absent -> `better-schemic init` can't scaffold a project for this driver.
|
|
371
|
+
*/
|
|
372
|
+
initScaffold?(): Record<string, string>;
|
|
373
|
+
/**
|
|
374
|
+
* Scaffold a NEW entity file's contents — the starter `s.*` / `define*` module for an object of
|
|
375
|
+
* `kind` named `name` (e.g. `("table", "user")` -> a `defineTable("user", { … })` module in this
|
|
376
|
+
* dialect's authoring). Returns the file text; the CLI writes it under the kind's
|
|
377
|
+
* {@link KindRegistry.display} folder. THROW for a kind this driver can't author (the CLI surfaces
|
|
378
|
+
* the message). Absent -> `better-schemic new` is unavailable for this driver.
|
|
379
|
+
*/
|
|
380
|
+
scaffoldEntity?(kind: string, name: string): string;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --- Registry -----------------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
// Shared across every loaded copy of `@better-schemic/core` so the registry is process-global — the CLI run
|
|
386
|
+
// via `bunx` resolves its own core, while a driver loaded from the user's project resolves the
|
|
387
|
+
// project's core; without sharing, the driver self-registers in one Map and the CLI reads an empty
|
|
388
|
+
// one. Keyed by a REGISTERED symbol (`Symbol.for`) — same key in every instance, but no string-keyed
|
|
389
|
+
// `globalThis` pollution and namespaced so it can't collide.
|
|
390
|
+
const REGISTRY_KEY = Symbol.for("@better-schemic/core.driverRegistry");
|
|
391
|
+
const REGISTRY: Map<string, Driver<unknown>> = ((
|
|
392
|
+
globalThis as Record<symbol, Map<string, Driver<unknown>> | undefined>
|
|
393
|
+
)[REGISTRY_KEY] ??= new Map<string, Driver<unknown>>());
|
|
394
|
+
|
|
395
|
+
/** Register a driver under its `name` (idempotent; last write wins). */
|
|
396
|
+
export function registerDriver(driver: Driver<unknown>): void {
|
|
397
|
+
REGISTRY.set(driver.name, driver);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Look up a registered driver, or throw with the list of known names. */
|
|
401
|
+
export function getDriver(name: string): Driver<unknown> {
|
|
402
|
+
const d = REGISTRY.get(name);
|
|
403
|
+
if (!d) {
|
|
404
|
+
const known = [...REGISTRY.keys()].join(", ") || "(none registered)";
|
|
405
|
+
throw new Error(`Unknown database driver "${name}". Registered: ${known}.`);
|
|
406
|
+
}
|
|
407
|
+
return d;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** All registered driver names (for help text / config validation). */
|
|
411
|
+
export function driverNames(): string[] {
|
|
412
|
+
return [...REGISTRY.keys()];
|
|
413
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The driver layer — the multi-DB seam (see docs/MULTI-DB-SPIKE.md).
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
ApplyOptions,
|
|
5
|
+
Authored,
|
|
6
|
+
AuthoredDef,
|
|
7
|
+
ConnectionOverrides,
|
|
8
|
+
Driver,
|
|
9
|
+
EmitOptions,
|
|
10
|
+
MigrationDirection,
|
|
11
|
+
MigrationRecord,
|
|
12
|
+
MigrationStore,
|
|
13
|
+
ShadowCapability,
|
|
14
|
+
Statement,
|
|
15
|
+
} from "./driver";
|
|
16
|
+
export { driverNames, getDriver, registerDriver } from "./driver";
|
|
17
|
+
export type {
|
|
18
|
+
GeometryKind,
|
|
19
|
+
PortableType,
|
|
20
|
+
ScalarName,
|
|
21
|
+
} from "./portable";
|
|
22
|
+
export {
|
|
23
|
+
array,
|
|
24
|
+
literal,
|
|
25
|
+
nullable,
|
|
26
|
+
option,
|
|
27
|
+
record,
|
|
28
|
+
scalar,
|
|
29
|
+
union,
|
|
30
|
+
} from "./portable";
|
|
31
|
+
export type { PortableField, PortablePermissions } from "./portable-ir";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The portable FIELD SUBSTRATE — the dialect-independent field model the kind registry builds on.
|
|
2
|
+
//
|
|
3
|
+
// core-v2 (the kind-registry flip) retired the fixed-slot `PortableDb` (tables/functions/accesses/
|
|
4
|
+
// natives) + the per-slot object types (`PortableTable`/`PortableEvent`/…): a schema is now a flat
|
|
5
|
+
// `PortableObject[]` of OPEN kinds (see ../kind), each driver owning its own portable shape. What
|
|
6
|
+
// stays in core is the SUBSTRATE every kind composes — a field's structured {@link PortableType} +
|
|
7
|
+
// its clauses — so a table kind nests `PortableField`s and the
|
|
8
|
+
// cross-driver field model + Zod drop-in keep working.
|
|
9
|
+
//
|
|
10
|
+
// Field/permission CLAUSES (default/value/assert/index spec/…) are carried verbatim as dialect
|
|
11
|
+
// expression strings: they don't port across dialects, so a foreign driver honors the ones it can and
|
|
12
|
+
// surfaces the rest as a documented capability gap. Only the keystone (the TYPE) is fully portable.
|
|
13
|
+
|
|
14
|
+
import type { PortableType } from "./portable";
|
|
15
|
+
|
|
16
|
+
/** CRUD permissions — each a boolean (FULL/NONE) or a dialect WHERE-expression string (carried verbatim). */
|
|
17
|
+
export interface PortablePermissions {
|
|
18
|
+
select?: boolean | string;
|
|
19
|
+
create?: boolean | string;
|
|
20
|
+
update?: boolean | string;
|
|
21
|
+
delete?: boolean | string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A field in the portable substrate: a structured {@link PortableType} + its dialect clauses (verbatim). */
|
|
25
|
+
export interface PortableField {
|
|
26
|
+
name: string;
|
|
27
|
+
type: PortableType;
|
|
28
|
+
flexible?: boolean;
|
|
29
|
+
readonly?: boolean;
|
|
30
|
+
default?: string;
|
|
31
|
+
default_always?: boolean;
|
|
32
|
+
value?: string;
|
|
33
|
+
computed?: string;
|
|
34
|
+
assert?: string;
|
|
35
|
+
/**
|
|
36
|
+
* A field-level CHECK constraint (dialect boolean expression, carried verbatim). DISTINCT from
|
|
37
|
+
* `assert`: that is Surreal's `ASSERT`; this is the SQL `CHECK` a SQL driver emits. A
|
|
38
|
+
* driver maps whichever of the two it supports and surfaces the other as a capability gap.
|
|
39
|
+
*/
|
|
40
|
+
check?: string;
|
|
41
|
+
comment?: string;
|
|
42
|
+
/** Referential action(s) on a foreign reference. A driver honors the actions it supports. */
|
|
43
|
+
reference?: { on_delete?: string; on_update?: string };
|
|
44
|
+
/**
|
|
45
|
+
* Auto-generated identity column — `GENERATED ALWAYS`/`BY DEFAULT AS IDENTITY`; a `serial`/
|
|
46
|
+
* auto-increment column maps here too. Absent → an ordinary column. Drivers without identity ignore it.
|
|
47
|
+
*/
|
|
48
|
+
identity?: "always" | "by-default";
|
|
49
|
+
permissions?: PortablePermissions;
|
|
50
|
+
table: string;
|
|
51
|
+
}
|