@schemic/core 0.1.0-alpha.0

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/lib/authoring.d.ts +89 -0
  4. package/lib/authoring.js +187 -0
  5. package/lib/authoring.js.map +1 -0
  6. package/lib/chunk-C4D6JWSE.js +54 -0
  7. package/lib/chunk-C4D6JWSE.js.map +1 -0
  8. package/lib/chunk-T23RNU7G.js +304 -0
  9. package/lib/chunk-T23RNU7G.js.map +1 -0
  10. package/lib/config-TIiKDd9t.d.ts +97 -0
  11. package/lib/config.d.ts +1 -0
  12. package/lib/config.js +8 -0
  13. package/lib/config.js.map +1 -0
  14. package/lib/driver-Dh5hLKHm.d.ts +736 -0
  15. package/lib/driver.d.ts +150 -0
  16. package/lib/driver.js +47 -0
  17. package/lib/driver.js.map +1 -0
  18. package/lib/index.d.ts +84 -0
  19. package/lib/index.js +794 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/testing.d.ts +29 -0
  22. package/lib/testing.js +111 -0
  23. package/lib/testing.js.map +1 -0
  24. package/package.json +93 -0
  25. package/src/authoring.ts +304 -0
  26. package/src/cli-kit/config.ts +179 -0
  27. package/src/cli-kit/diff.ts +230 -0
  28. package/src/cli-kit/filter.ts +159 -0
  29. package/src/cli-kit/merge.ts +380 -0
  30. package/src/cli-kit/meta.ts +123 -0
  31. package/src/cli-kit/pager.ts +42 -0
  32. package/src/cli-kit/schema.ts +186 -0
  33. package/src/cli-kit/style.ts +24 -0
  34. package/src/config.ts +51 -0
  35. package/src/connection.ts +78 -0
  36. package/src/driver/driver.ts +300 -0
  37. package/src/driver/index.ts +31 -0
  38. package/src/driver/portable-ir.ts +51 -0
  39. package/src/driver/portable.ts +124 -0
  40. package/src/driver/sdk.ts +66 -0
  41. package/src/index.ts +145 -0
  42. package/src/kind/index.ts +28 -0
  43. package/src/kind/plan.ts +390 -0
  44. package/src/kind/registry.ts +225 -0
  45. package/src/testing.ts +181 -0
@@ -0,0 +1,304 @@
1
+ // The NEUTRAL, dialect-agnostic AUTHORING BASE (docs/AUTHORING-SPLIT.md — "base builder in core").
2
+ // Each driver package builds its `s.*` on this: `class <D>Field extends SFieldBase<S, Flags, <D>Meta>`
3
+ // adds the dialect's native authoring (`$`-methods) and its `$<driver>(type, codec)` escape hatch for
4
+ // types not representable on the wire; the base provides the Zod codec, the Zod wrappers, the full
5
+ // `z.*` passthrough, and the `rebuild`/`blank` seam that carries native metadata through a chain.
6
+ //
7
+ // It references NOTHING dialect-specific — it's generic over the per-dialect native-metadata slot `N`.
8
+ // It is also Zod-CLEAN: app-side behaviour delegates to the inner Zod schema (`z.decode`/`z.encode`/
9
+ // the wrappers) via Zod's public API, with side-channel metadata kept on WeakMaps — never patching
10
+ // Zod internals.
11
+
12
+ import * as z from "zod";
13
+
14
+ // `SFieldBase` is INVARIANT in its native-metadata slot `N` (the protected `rebuild(native: N)` makes
15
+ // N contravariant while `native`/`blank` make it covariant). So a dialect field — `SField` with
16
+ // `N = SurrealMeta` — is NOT assignable to a fixed `N = unknown`, which would make `AnyField` reject
17
+ // real dialect fields (e.g. `.or(s.int())`). At THIS cross-dialect boundary `N` is honestly "any
18
+ // dialect's metadata": erase it to `any` (bivariant) so every driver's field is an `AnyField`. The
19
+ // concrete `N` is preserved everywhere it matters — each driver's own field type keeps `N = <D>Meta`.
20
+
21
+ /** Any field of ANY dialect — the base type the helpers + wrappers accept. */
22
+ // biome-ignore lint/suspicious/noExplicitAny: cross-dialect erasure of the invariant native slot N.
23
+ export type AnyField = SFieldBase<z.ZodType, string, any>;
24
+
25
+ /** The Zod schema a field (or a raw Zod schema) carries. */
26
+ export type SchemaOf<F> =
27
+ // biome-ignore lint/suspicious/noExplicitAny: match a field of any dialect (N is invariant).
28
+ F extends SFieldBase<infer S, string, any>
29
+ ? S
30
+ : F extends z.ZodType
31
+ ? F
32
+ : never;
33
+
34
+ /** The `Flags` channel a field carries (driver `$`-methods brand it; widens to `string` for `Shape`). */
35
+ export type FlagsOf<F> =
36
+ // biome-ignore lint/suspicious/noExplicitAny: match a field of any dialect (N is invariant).
37
+ F extends SFieldBase<z.ZodType, infer Fl, any> ? Fl : never;
38
+
39
+ /** The schema one wrapper down — what `unwrap()` returns. */
40
+ export type InnerOf<S extends z.ZodType> =
41
+ S extends z.ZodOptional<infer I extends z.ZodType>
42
+ ? I
43
+ : S extends z.ZodNullable<infer I extends z.ZodType>
44
+ ? I
45
+ : S extends z.ZodDefault<infer I extends z.ZodType>
46
+ ? I
47
+ : S extends z.ZodPrefault<infer I extends z.ZodType>
48
+ ? I
49
+ : S extends z.ZodCatch<infer I extends z.ZodType>
50
+ ? I
51
+ : S extends z.ZodReadonly<infer I extends z.ZodType>
52
+ ? I
53
+ : S extends z.ZodArray<infer I extends z.ZodType>
54
+ ? I
55
+ : S;
56
+
57
+ /**
58
+ * Maps an object schema (built via a driver's `s.object`) to its original field shape, so nested
59
+ * fields keep their authoring metadata through generation. Kept on the schema, not the field, so it
60
+ * composes through `array()`/`optional()`/nesting.
61
+ */
62
+ export const objectFieldsRegistry = new WeakMap<
63
+ z.ZodType,
64
+ Record<string, AnyField>
65
+ >();
66
+
67
+ /**
68
+ * The PORTABLE, dialect-agnostic field base. Holds the Zod schema, an opaque per-dialect `native`
69
+ * metadata slot, the field-level codecs, and the app-land Zod wrappers (which carry `native` forward
70
+ * via the `rebuild` hook so a chain keeps its concrete dialect type). Each dialect subclasses it to
71
+ * add native authoring (`$`-methods) and re-type the wrappers so a chain stays its own field type.
72
+ */
73
+ export abstract class SFieldBase<
74
+ S extends z.ZodType = z.ZodType,
75
+ Flags extends string = never,
76
+ N = unknown,
77
+ > {
78
+ constructor(
79
+ readonly schema: S,
80
+ readonly native: N,
81
+ ) {}
82
+
83
+ /** Rebuild a sibling field of the SAME dialect with a new schema/flags. Each dialect overrides it. */
84
+ protected abstract rebuild<S2 extends z.ZodType, F2 extends string>(
85
+ schema: S2,
86
+ native: N,
87
+ ): SFieldBase<S2, F2, N>;
88
+ /** A fresh, empty native-metadata bag (for wrappers like `or`/`and` that reset it). */
89
+ protected abstract blank(): N;
90
+
91
+ // --- Field-level codec (raw, on `this.schema`): `decode` reads (wire -> app), `encode` writes
92
+ // (app -> wire). Create-shaping is a table concept, so these are NOT create-shaped. ---
93
+ /** Decode a DB value to its app type (wire -> app). */
94
+ decode(value: unknown): z.output<S> {
95
+ return z.decode(this.schema, value as never);
96
+ }
97
+ /** Encode an app value to its DB wire type (app -> wire). */
98
+ encode(value: z.output<S>): z.input<S> {
99
+ return z.encode(this.schema, value);
100
+ }
101
+ decodeAsync(value: unknown): Promise<z.output<S>> {
102
+ return z.decodeAsync(this.schema, value as never);
103
+ }
104
+ encodeAsync(value: z.output<S>): Promise<z.input<S>> {
105
+ return z.encodeAsync(this.schema, value);
106
+ }
107
+ safeDecode(value: unknown) {
108
+ return z.safeDecode(this.schema, value as never);
109
+ }
110
+ safeEncode(value: z.output<S>) {
111
+ return z.safeEncode(this.schema, value);
112
+ }
113
+ safeDecodeAsync(value: unknown) {
114
+ return z.safeDecodeAsync(this.schema, value as never);
115
+ }
116
+ safeEncodeAsync(value: z.output<S>) {
117
+ return z.safeEncodeAsync(this.schema, value);
118
+ }
119
+ // Deprecated Zod-style aliases — `parse` runs the DECODE direction (wire -> app).
120
+ /** @deprecated `parse` decodes a value (wire -> app). Use {@link decode}. */
121
+ parse(value: unknown): z.output<S> {
122
+ return this.decode(value);
123
+ }
124
+ /** @deprecated Use {@link safeDecode}. */
125
+ safeParse(value: unknown) {
126
+ return this.safeDecode(value);
127
+ }
128
+ /** @deprecated Use {@link decodeAsync}. */
129
+ parseAsync(value: unknown): Promise<z.output<S>> {
130
+ return this.decodeAsync(value);
131
+ }
132
+ /** @deprecated Use {@link safeDecodeAsync}. */
133
+ safeParseAsync(value: unknown) {
134
+ return this.safeDecodeAsync(value);
135
+ }
136
+
137
+ // Zod wrappers — delegate to the inner schema, carry native metadata + flags forward.
138
+ optional(): SFieldBase<z.ZodOptional<S>, Flags, N> {
139
+ return this.rebuild(this.schema.optional(), this.native);
140
+ }
141
+ nullable(): SFieldBase<z.ZodNullable<S>, Flags, N> {
142
+ return this.rebuild(this.schema.nullable(), this.native);
143
+ }
144
+ default(value: z.input<S>): SFieldBase<z.ZodDefault<S>, Flags, N> {
145
+ return this.rebuild(this.schema.default(value as never), this.native);
146
+ }
147
+ /** Zod prefault: fill an absent value with `value`, then validate it (unlike `.default`). */
148
+ prefault(value: z.input<S>): SFieldBase<z.ZodPrefault<S>, Flags, N> {
149
+ return this.rebuild(z.prefault(this.schema, value as never), this.native);
150
+ }
151
+ /** Zod catch: fall back to `value` when parsing fails. */
152
+ catch(value: z.output<S>): SFieldBase<z.ZodCatch<S>, Flags, N> {
153
+ return this.rebuild(this.schema.catch(value as never), this.native);
154
+ }
155
+ array(): SFieldBase<z.ZodArray<S>, Flags, N> {
156
+ return this.rebuild(z.array(this.schema), this.native);
157
+ }
158
+ nullish(): SFieldBase<z.ZodOptional<z.ZodNullable<S>>, Flags, N> {
159
+ return this.rebuild(this.schema.nullish(), this.native);
160
+ }
161
+ /** Zod union — `a.or(b)` accepts either. Mirrors Zod's `.or()`. */
162
+ or<F extends AnyField | z.ZodType>(
163
+ other: F,
164
+ ): SFieldBase<z.ZodUnion<[S, SchemaOf<F>]>, never, N> {
165
+ return this.rebuild<z.ZodUnion<[S, SchemaOf<F>]>, never>(
166
+ z.union([this.schema, toZod(other)]) as z.ZodUnion<[S, SchemaOf<F>]>,
167
+ this.blank(),
168
+ );
169
+ }
170
+ /** Zod intersection — `a.and(b)`. Mirrors Zod's `.and()`. */
171
+ and<F extends AnyField | z.ZodType>(
172
+ other: F,
173
+ ): SFieldBase<z.ZodIntersection<S, SchemaOf<F>>, never, N> {
174
+ return this.rebuild<z.ZodIntersection<S, SchemaOf<F>>, never>(
175
+ z.intersection(this.schema, toZod(other) as SchemaOf<F>),
176
+ this.blank(),
177
+ );
178
+ }
179
+
180
+ // --- Native Zod passthrough (drop-in for `z.*`): app-side validation / transform / metadata,
181
+ // delegated to the inner schema. The dialect-DDL side stays under the driver's `$`-methods. ---
182
+ refine(
183
+ check: (arg: z.output<S>) => unknown,
184
+ params?: string | z.core.$ZodCustomParams,
185
+ ): this {
186
+ return this.rebuild(
187
+ this.schema.refine(check, params) as S,
188
+ this.native,
189
+ ) as unknown as this;
190
+ }
191
+ superRefine(
192
+ refinement: (
193
+ arg: z.output<S>,
194
+ ctx: z.core.$RefinementCtx<z.output<S>>,
195
+ ) => void,
196
+ ): this {
197
+ return this.rebuild(
198
+ this.schema.superRefine(refinement) as S,
199
+ this.native,
200
+ ) as unknown as this;
201
+ }
202
+ check(
203
+ ...checks: (z.core.CheckFn<z.output<S>> | z.core.$ZodCheck<z.output<S>>)[]
204
+ ): this {
205
+ return this.rebuild(
206
+ this.schema.check(...checks) as S,
207
+ this.native,
208
+ ) as unknown as this;
209
+ }
210
+ overwrite(fn: (x: z.output<S>) => z.output<S>): this {
211
+ return this.rebuild(
212
+ this.schema.overwrite(fn) as S,
213
+ this.native,
214
+ ) as unknown as this;
215
+ }
216
+ brand<B extends PropertyKey = PropertyKey>(value?: B): this {
217
+ return this.rebuild(
218
+ this.schema.brand(value) as unknown as S,
219
+ this.native,
220
+ ) as unknown as this;
221
+ }
222
+ /** Zod's app-side metadata (JSON-schema/docs) — distinct from a driver's `$comment()`. */
223
+ describe(description: string): this {
224
+ return this.rebuild(
225
+ this.schema.describe(description) as S,
226
+ this.native,
227
+ ) as unknown as this;
228
+ }
229
+ meta(data: z.core.GlobalMeta): this {
230
+ return this.rebuild(
231
+ this.schema.meta(data) as S,
232
+ this.native,
233
+ ) as unknown as this;
234
+ }
235
+ /** Zod's app-side readonly (TS-immutable output) — distinct from a driver's `$readonly()`. */
236
+ readonly(): SFieldBase<z.ZodReadonly<S>, Flags, N> {
237
+ return this.rebuild(this.schema.readonly(), this.native);
238
+ }
239
+ /** Zod transform — changes the decoded `App<>` value; the stored (wire) type is unchanged. */
240
+ transform<NewOut>(
241
+ fn: (arg: z.output<S>, ctx: z.core.$RefinementCtx<z.output<S>>) => NewOut,
242
+ ): SFieldBase<
243
+ z.ZodPipe<S, z.ZodTransform<Awaited<NewOut>, z.output<S>>>,
244
+ Flags,
245
+ N
246
+ > {
247
+ return this.rebuild(this.schema.transform(fn), this.native);
248
+ }
249
+ /** Zod pipe — feed this field's output into `target`; the stored (wire) type stays `this`. */
250
+ pipe<T extends z.core.$ZodType<unknown, z.output<S>>>(
251
+ target: T,
252
+ ): SFieldBase<z.ZodPipe<S, T>, Flags, N> {
253
+ return this.rebuild(
254
+ this.schema.pipe(target) as z.ZodPipe<S, T>,
255
+ this.native,
256
+ );
257
+ }
258
+ /** Peel one wrapper (optional/nullable/default/prefault/catch/readonly/array) off the field. */
259
+ unwrap(): SFieldBase<InnerOf<S>, Flags, N> {
260
+ const def = this.schema._zod.def as {
261
+ innerType?: z.ZodType;
262
+ element?: z.ZodType;
263
+ };
264
+ const inner = def.innerType ?? def.element ?? this.schema;
265
+ return this.rebuild(inner, this.native) as unknown as SFieldBase<
266
+ InnerOf<S>,
267
+ Flags,
268
+ N
269
+ >;
270
+ }
271
+
272
+ /** Object-only: allow arbitrary extra keys — `FLEXIBLE` in DDL. Mirrors Zod's `.loose()`. */
273
+ loose(): this {
274
+ return this.objectMode("loose");
275
+ }
276
+ /** Object-only: reject unknown keys — the default. Mirrors Zod's `.strict()`. */
277
+ strict(): this {
278
+ return this.objectMode("strict");
279
+ }
280
+ /** Alias for {@link loose} — a `FLEXIBLE` object accepting arbitrary keys. */
281
+ flexible(): this {
282
+ return this.loose();
283
+ }
284
+ private objectMode(mode: "loose" | "strict"): this {
285
+ const obj = this.schema as unknown as {
286
+ loose?: () => z.ZodType;
287
+ strict?: () => z.ZodType;
288
+ };
289
+ if (typeof obj.loose !== "function" || typeof obj.strict !== "function") {
290
+ return this; // not an object schema — no-op
291
+ }
292
+ const next = (mode === "loose"
293
+ ? obj.loose()
294
+ : obj.strict()) as unknown as S;
295
+ // Carry the nested-field registry forward so DDL/create-shaping still see the subfields.
296
+ const fields = objectFieldsRegistry.get(this.schema);
297
+ if (fields) objectFieldsRegistry.set(next, fields);
298
+ return this.rebuild(next, this.native) as unknown as this;
299
+ }
300
+ }
301
+
302
+ /** Unwrap a field to its Zod schema (raw Zod schemas pass through). */
303
+ export const toZod = (v: AnyField | z.ZodType): z.ZodType =>
304
+ v instanceof SFieldBase ? v.schema : v;
@@ -0,0 +1,179 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import type { SchemicConfig } from "@schemic/core/config";
4
+ import { createJiti } from "jiti";
5
+ import type { ConnectionConfigBase, ResolveContext } from "../connection";
6
+
7
+ const CONFIG_NAMES = [
8
+ "schemic.config.ts",
9
+ "schemic.config.mjs",
10
+ "schemic.config.js",
11
+ ];
12
+
13
+ const DEFAULT_MIGRATIONS = "./database/migrations";
14
+
15
+ /**
16
+ * Is the schema path a single file (vs a directory of schema modules)? Determined by `stat` when
17
+ * it exists, else inferred from a `.ts`/`.js`-ish extension.
18
+ */
19
+ function schemaIsFilePath(path: string): boolean {
20
+ if (existsSync(path)) return statSync(path).isFile();
21
+ return /\.[mc]?[jt]s$/.test(path);
22
+ }
23
+
24
+ /**
25
+ * Load `.env(.local)` from the project root into `process.env` so the config's own explicit
26
+ * `process.env.X` reads resolve when run under node (bun loads `.env` itself). Does not override
27
+ * already-set variables, so shell env still wins; load `.env.local` first so it beats `.env`.
28
+ */
29
+ function loadDotEnv(dir: string): void {
30
+ const proc = process as typeof process & {
31
+ loadEnvFile?: (path: string) => void;
32
+ };
33
+ if (typeof proc.loadEnvFile !== "function") return;
34
+ for (const name of [".env.local", ".env"]) {
35
+ const file = resolve(dir, name);
36
+ if (!existsSync(file)) continue;
37
+ try {
38
+ proc.loadEnvFile(file);
39
+ } catch {
40
+ // ignore a malformed .env file
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * A resolved, per-CONNECTION config — the dialect-NEUTRAL shape every command operates on (one
47
+ * connection at a time). `params` are the driver-specific connection params (opaque to core; the
48
+ * driver's `connect` reads them). Built by resolving one entry of `config.connections`.
49
+ */
50
+ export interface ResolvedConfig {
51
+ /** Resolved connection name (e.g. `default`, or `tenants:abc` within a collection). */
52
+ connection: string;
53
+ /** The driver this connection uses (the package the CLI dynamically loads). */
54
+ driver: string;
55
+ /** Project root (the directory containing the config file). */
56
+ root: string;
57
+ /** Absolute schema path — a single `.ts` module, or a directory of them. */
58
+ schemaPath: string;
59
+ /** Whether `schemaPath` is a single file (vs a directory of schema modules). */
60
+ schemaIsFile: boolean;
61
+ /** Absolute migrations directory (per connection's schema). */
62
+ migrationsDir: string;
63
+ /** Absolute migration meta directory (the snapshot). */
64
+ metaDir: string;
65
+ /** Name of the table that records applied migrations. */
66
+ migrationsTable: string;
67
+ /** Driver-specific connection params (url/namespace/… or whatever the driver defines). Opaque to core. */
68
+ params: Record<string, unknown>;
69
+ /** Optional seed script (project-level). */
70
+ seed?: string;
71
+ }
72
+
73
+ /**
74
+ * A jiti instance for loading the project's TS/ESM modules. Caches are off so `--watch` re-reads
75
+ * edited schema files. (Bare deps like `@schemic/core` are native-imported, so registries stay shared.)
76
+ */
77
+ export function makeJiti() {
78
+ return createJiti(import.meta.url, {
79
+ interopDefault: true,
80
+ fsCache: false,
81
+ moduleCache: false,
82
+ });
83
+ }
84
+
85
+ /** Find + load `schemic.config.ts` into the dialect-neutral {@link SchemicConfig}. */
86
+ export async function loadProject(opts?: {
87
+ config?: string;
88
+ cwd?: string;
89
+ }): Promise<{ config: SchemicConfig; root: string }> {
90
+ const cwd = opts?.cwd ?? process.cwd();
91
+ const path = opts?.config
92
+ ? resolve(cwd, opts.config)
93
+ : CONFIG_NAMES.map((n) => resolve(cwd, n)).find((p) => existsSync(p));
94
+ if (!path || !existsSync(path)) {
95
+ throw new Error("No schemic.config.ts found — run `schemic init` first.");
96
+ }
97
+ const root = dirname(path);
98
+ loadDotEnv(root); // populate process.env before the config module's explicit reads
99
+ const loaded = (await makeJiti().import(path)) as {
100
+ default?: SchemicConfig;
101
+ } & SchemicConfig;
102
+ const config = loaded.default ?? loaded;
103
+ if (!config?.connections || Object.keys(config.connections).length === 0) {
104
+ throw new Error(`Invalid config at ${path}: expected a "connections" map.`);
105
+ }
106
+ return { config, root };
107
+ }
108
+
109
+ /**
110
+ * Build the {@link ResolvedConfig} for one connection of the project. `ctx` carries the lazy
111
+ * cross-connection proxy + CLI `--arg`s (the CLI provides the real one; a static connection ignores
112
+ * it). A resolver returning a COLLECTION yields one ResolvedConfig per keyed entry.
113
+ *
114
+ * NOTE (WIP — multi-connection): the full resolution engine (lazy proxy DAG, `--connection`/`--all`
115
+ * addressing, collection fan-out) lives in `@schemic/cli`; this builder handles a single resolved
116
+ * connection config. See docs/MULTI-CONNECTION.md.
117
+ */
118
+ export function resolveConnectionConfig(
119
+ config: SchemicConfig,
120
+ connection: string,
121
+ conn: ConnectionConfigBase,
122
+ driver: string,
123
+ root: string,
124
+ ): ResolvedConfig {
125
+ const { schema, migrations, key, ...params } = conn;
126
+ const schemaPath = resolve(root, schema);
127
+ const migrationsDir = resolve(root, migrations ?? DEFAULT_MIGRATIONS);
128
+ return {
129
+ connection: key ? `${connection}:${key}` : connection,
130
+ driver,
131
+ root,
132
+ schemaPath,
133
+ schemaIsFile: schemaIsFilePath(schemaPath),
134
+ migrationsDir,
135
+ metaDir: resolve(migrationsDir, "meta"),
136
+ migrationsTable: config.migrationsTable ?? "_migrations",
137
+ params: params as Record<string, unknown>,
138
+ seed: config.seed,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Load the project and resolve the DEFAULT connection to a {@link ResolvedConfig} — the single-
144
+ * connection convenience path. (Multi-connection addressing + resolver context are added by the CLI;
145
+ * here a static default connection is resolved with an empty context.)
146
+ */
147
+ export async function loadConfig(opts?: {
148
+ config?: string;
149
+ cwd?: string;
150
+ }): Promise<ResolvedConfig> {
151
+ const { config, root } = await loadProject(opts);
152
+ const names = Object.keys(config.connections);
153
+ const name =
154
+ config.defaultConnection ?? (names.length === 1 ? names[0] : "default");
155
+ const entry = config.connections[name];
156
+ if (!entry) {
157
+ throw new Error(
158
+ `No connection named "${name}". Set "defaultConnection" or pass --connection. Known: ${names.join(", ")}.`,
159
+ );
160
+ }
161
+ const ctx: ResolveContext = { connections: {}, args: {}, env: process.env };
162
+ const resolved = await entry.resolve(ctx);
163
+ if (resolved.length !== 1) {
164
+ throw new Error(
165
+ `Connection "${name}" resolved to ${resolved.length} connections (a collection); pass --connection ${name}:<key>.`,
166
+ );
167
+ }
168
+ return resolveConnectionConfig(config, name, resolved[0], entry.driver, root);
169
+ }
170
+
171
+ /** Per-command connection flag overrides (CLI args, applied by the driver over `params`). */
172
+ export interface ConnectionOverrides {
173
+ url?: string;
174
+ namespace?: string;
175
+ database?: string;
176
+ username?: string;
177
+ password?: string;
178
+ authLevel?: string;
179
+ }