@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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/lib/authoring.d.ts +114 -0
  4. package/lib/authoring.js +242 -0
  5. package/lib/authoring.js.map +1 -0
  6. package/lib/chunk-26D7WX7Q.js +31 -0
  7. package/lib/chunk-26D7WX7Q.js.map +1 -0
  8. package/lib/chunk-IUPOUD4L.js +330 -0
  9. package/lib/chunk-IUPOUD4L.js.map +1 -0
  10. package/lib/chunk-LC3VHUM2.js +56 -0
  11. package/lib/chunk-LC3VHUM2.js.map +1 -0
  12. package/lib/chunk-RSGP7GVO.js +252 -0
  13. package/lib/chunk-RSGP7GVO.js.map +1 -0
  14. package/lib/client-HZF4ZWGO.js +13 -0
  15. package/lib/client-HZF4ZWGO.js.map +1 -0
  16. package/lib/config-BYh7WA4P.d.ts +259 -0
  17. package/lib/config.d.ts +2 -0
  18. package/lib/config.js +27 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/driver-LVldBEhS.d.ts +818 -0
  21. package/lib/driver.d.ts +151 -0
  22. package/lib/driver.js +47 -0
  23. package/lib/driver.js.map +1 -0
  24. package/lib/index.d.ts +154 -0
  25. package/lib/index.js +758 -0
  26. package/lib/index.js.map +1 -0
  27. package/lib/query.d.ts +81 -0
  28. package/lib/query.js +30 -0
  29. package/lib/query.js.map +1 -0
  30. package/lib/secrets-BETi5p8g.d.ts +26 -0
  31. package/lib/testing.d.ts +99 -0
  32. package/lib/testing.js +212 -0
  33. package/lib/testing.js.map +1 -0
  34. package/package.json +102 -0
  35. package/src/authoring.ts +360 -0
  36. package/src/cli-kit/config.ts +226 -0
  37. package/src/cli-kit/diff.ts +273 -0
  38. package/src/cli-kit/filter.ts +159 -0
  39. package/src/cli-kit/merge.ts +380 -0
  40. package/src/cli-kit/meta.ts +123 -0
  41. package/src/cli-kit/pager.ts +42 -0
  42. package/src/cli-kit/schema.ts +214 -0
  43. package/src/cli-kit/style.ts +24 -0
  44. package/src/client.ts +244 -0
  45. package/src/config.ts +199 -0
  46. package/src/connection.ts +120 -0
  47. package/src/driver/driver.ts +413 -0
  48. package/src/driver/index.ts +31 -0
  49. package/src/driver/portable-ir.ts +51 -0
  50. package/src/driver/portable.ts +124 -0
  51. package/src/driver/sdk.ts +73 -0
  52. package/src/index.ts +185 -0
  53. package/src/kind/index.ts +28 -0
  54. package/src/kind/plan.ts +412 -0
  55. package/src/kind/registry.ts +270 -0
  56. package/src/query/call.ts +21 -0
  57. package/src/query/codec.ts +33 -0
  58. package/src/query/index.ts +22 -0
  59. package/src/query/project.ts +25 -0
  60. package/src/query/ref.ts +32 -0
  61. package/src/query.ts +5 -0
  62. package/src/secrets.ts +61 -0
  63. package/src/seed.ts +14 -0
  64. package/src/testing.ts +402 -0
@@ -0,0 +1,252 @@
1
+ // src/cli-kit/config.ts
2
+ import { existsSync, statSync } from "fs";
3
+ import { basename, dirname, resolve } from "path";
4
+ import { createJiti } from "jiti";
5
+ var CONFIG_NAMES = [
6
+ "better-schemic.config.ts",
7
+ "better-schemic.config.mjs",
8
+ "better-schemic.config.js",
9
+ "better-schemic.ts",
10
+ // Legacy aliases (pre-rename) — still discovered, after the canonical names.
11
+ "schemic.config.ts",
12
+ "schemic.config.mjs",
13
+ "schemic.config.js",
14
+ "schemic.ts"
15
+ ];
16
+ function schemaIsFilePath(path) {
17
+ if (existsSync(path)) return statSync(path).isFile();
18
+ return /\.[mc]?[jt]s$/.test(path);
19
+ }
20
+ function loadDotEnv(dir) {
21
+ const proc = process;
22
+ if (typeof proc.loadEnvFile !== "function") return;
23
+ for (const name of [".env.local", ".env"]) {
24
+ const file = resolve(dir, name);
25
+ if (!existsSync(file)) continue;
26
+ try {
27
+ proc.loadEnvFile(file);
28
+ } catch {
29
+ }
30
+ }
31
+ }
32
+ function makeJiti() {
33
+ return createJiti(import.meta.url, {
34
+ interopDefault: true,
35
+ fsCache: false,
36
+ moduleCache: false
37
+ });
38
+ }
39
+ async function loadProject(opts) {
40
+ const cwd = opts?.cwd ?? process.cwd();
41
+ const candidates = opts?.config ? [resolve(cwd, opts.config)] : CONFIG_NAMES.map((n) => resolve(cwd, n)).filter((p) => existsSync(p));
42
+ if (!candidates.length || !existsSync(candidates[0])) {
43
+ throw new Error(
44
+ "No better-schemic.ts / better-schemic.config.ts found \u2014 run `better-schemic init` first."
45
+ );
46
+ }
47
+ const jiti = makeJiti();
48
+ for (const path of candidates) {
49
+ const root = dirname(path);
50
+ loadDotEnv(root);
51
+ const loaded = await jiti.import(path);
52
+ const config = [
53
+ loaded.default,
54
+ loaded.betterSchemic,
55
+ loaded.schemic,
56
+ loaded
57
+ ].find(
58
+ (c) => !!c && typeof c === "object" && "connections" in c
59
+ );
60
+ if (config?.connections && Object.keys(config.connections).length > 0) {
61
+ return { config, root };
62
+ }
63
+ if (!opts?.config && (basename(path) === "better-schemic.ts" || basename(path) === "schemic.ts"))
64
+ continue;
65
+ throw new Error(`Invalid config at ${path}: expected a "connections" map.`);
66
+ }
67
+ throw new Error(
68
+ 'No Better-schemic config found \u2014 ./better-schemic.ts exists but doesn\'t export a config with a "connections" map. Run `better-schemic init`, or export one via defineConfig.'
69
+ );
70
+ }
71
+ function resolveConnectionConfig(config, connection, conn, driver, root) {
72
+ const { schema, migrations, key, ...params } = conn;
73
+ const schemaPath = resolve(root, schema);
74
+ const migrationsDir = migrations ? resolve(root, migrations) : resolve(schemaPath, "..", "migrations");
75
+ return {
76
+ connection: key ? `${connection}:${key}` : connection,
77
+ driver,
78
+ root,
79
+ schemaPath,
80
+ schemaIsFile: schemaIsFilePath(schemaPath),
81
+ migrationsDir,
82
+ metaDir: resolve(migrationsDir, "meta"),
83
+ migrationsTable: config.migrationsTable ?? "_migrations",
84
+ params,
85
+ seed: config.seed
86
+ };
87
+ }
88
+ async function loadConfig(opts) {
89
+ const { config, root } = await loadProject(opts);
90
+ const names = Object.keys(config.connections);
91
+ const name = config.defaultConnection ?? (names.length === 1 ? names[0] : "default");
92
+ const entry = config.connections[name];
93
+ if (!entry) {
94
+ throw new Error(
95
+ `No connection named "${name}". Set "defaultConnection" or pass --connection. Known: ${names.join(", ")}.`
96
+ );
97
+ }
98
+ const ctx = { connections: {}, env: process.env };
99
+ const resolved = await entry.resolve(ctx);
100
+ if (resolved.length !== 1) {
101
+ throw new Error(
102
+ `Connection "${name}" resolved to ${resolved.length} connections (a collection); pass --connection ${name}:<key>.`
103
+ );
104
+ }
105
+ return resolveConnectionConfig(config, name, resolved[0], entry.driver, root);
106
+ }
107
+
108
+ // src/client.ts
109
+ function asyncDisposable(proto) {
110
+ proto[Symbol.asyncDispose] = function() {
111
+ return this.close();
112
+ };
113
+ }
114
+ function makeRuntimeContext(config, root) {
115
+ const opened = /* @__PURE__ */ new Map();
116
+ const resolving = /* @__PURE__ */ new Set();
117
+ const open = async (name) => {
118
+ if (resolving.has(name)) {
119
+ throw new Error(
120
+ `cyclic connection resolution: "${name}" is already resolving (a resolver reached back into itself via ctx.connections)`
121
+ );
122
+ }
123
+ const entry = config.connections[name];
124
+ if (!entry) throw new Error(`ctx.connections.${name}: no such connection`);
125
+ if (!entry.client) {
126
+ throw new Error(
127
+ `ctx.connections.${name}: the "${entry.driver}" connection factory predates runtime cross-connection access \u2014 update @better-schemic/${entry.driver}`
128
+ );
129
+ }
130
+ resolving.add(name);
131
+ try {
132
+ const bases = await entry.resolve(context, void 0);
133
+ if (bases.length !== 1) {
134
+ throw new Error(
135
+ `ctx.connections.${name}: resolved to ${bases.length} configs \u2014 a sibling reached via ctx.connections must resolve to exactly one`
136
+ );
137
+ }
138
+ const rc = resolveConnectionConfig(
139
+ config,
140
+ name,
141
+ bases[0],
142
+ entry.driver,
143
+ root
144
+ );
145
+ return await entry.client(rc);
146
+ } finally {
147
+ resolving.delete(name);
148
+ }
149
+ };
150
+ const context = {
151
+ env: process.env,
152
+ connections: new Proxy(
153
+ {},
154
+ {
155
+ get(_t, name) {
156
+ const openOnce = () => {
157
+ let client = opened.get(name);
158
+ if (!client) {
159
+ client = open(name);
160
+ opened.set(name, client);
161
+ }
162
+ return client;
163
+ };
164
+ return {
165
+ query: async (sql, vars) => {
166
+ const db = await openOnce();
167
+ return db.query(sql, vars);
168
+ },
169
+ then: (onOk, onErr) => openOnce().then(onOk, onErr)
170
+ };
171
+ }
172
+ }
173
+ )
174
+ };
175
+ const closeOpened = async () => {
176
+ for (const c of opened.values()) {
177
+ try {
178
+ await (await c).close?.();
179
+ } catch {
180
+ }
181
+ }
182
+ opened.clear();
183
+ };
184
+ return { context, closeOpened, resolving };
185
+ }
186
+ async function resolveConnection(opts = {}) {
187
+ const { config, root } = await loadProject({
188
+ config: opts.config,
189
+ cwd: opts.cwd
190
+ });
191
+ return exactlyOne(
192
+ await resolveFromConfig(config, root, { name: opts.name, args: opts.args })
193
+ );
194
+ }
195
+ async function resolveFromConfig(config, root, opts = {}) {
196
+ const names = Object.keys(config.connections);
197
+ const name = opts.name ?? config.defaultConnection ?? (names.length === 1 ? names[0] : "default");
198
+ const entry = config.connections[name];
199
+ if (!entry) {
200
+ throw new Error(
201
+ `connection "${name}" is not defined in the config (have: ${names.join(", ") || "none"})`
202
+ );
203
+ }
204
+ const rt = makeRuntimeContext(config, root);
205
+ rt.resolving.add(name);
206
+ try {
207
+ const bases = await entry.resolve(rt.context, opts.args);
208
+ if (!bases.length) {
209
+ throw new Error(`connection "${name}" resolved to no config`);
210
+ }
211
+ const resolved = bases.map(
212
+ (b) => resolveConnectionConfig(config, name, b, entry.driver, root)
213
+ );
214
+ const labels = resolved.map(
215
+ (rc, i) => bases[i].key ?? entry.label?.(rc) ?? `${name}[${i}]`
216
+ );
217
+ return { entry, name, resolved, labels };
218
+ } finally {
219
+ rt.resolving.delete(name);
220
+ await rt.closeOpened();
221
+ }
222
+ }
223
+ function exactlyOne(r) {
224
+ if (r.resolved.length !== 1) {
225
+ throw new Error(
226
+ `connection "${r.name}" resolved to ${r.resolved.length} configs (${r.labels.join(", ")}) \u2014 a parameterized/bulk connection. Pass args selecting exactly one, e.g. betterSchemic.connect("${r.name}", { ... }).`
227
+ );
228
+ }
229
+ return r.resolved[0];
230
+ }
231
+ async function connectFromConfig(config, name, args) {
232
+ const r = await resolveFromConfig(config, process.cwd(), { name, args });
233
+ const resolved = exactlyOne(r);
234
+ if (!r.entry.client) {
235
+ throw new Error(
236
+ `the "${r.entry.driver}" connection factory predates config.connect() \u2014 update @better-schemic/${r.entry.driver} to a version whose ${r.entry.driver}Connection embeds a client opener`
237
+ );
238
+ }
239
+ return r.entry.client(resolved);
240
+ }
241
+
242
+ export {
243
+ makeJiti,
244
+ loadProject,
245
+ resolveConnectionConfig,
246
+ loadConfig,
247
+ asyncDisposable,
248
+ resolveConnection,
249
+ resolveFromConfig,
250
+ connectFromConfig
251
+ };
252
+ //# sourceMappingURL=chunk-RSGP7GVO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli-kit/config.ts","../src/client.ts"],"sourcesContent":["import { existsSync, statSync } from \"node:fs\";\nimport { basename, dirname, resolve } from \"node:path\";\nimport type { BetterSchemicConfig } from \"@better-schemic/core/config\";\nimport { createJiti } from \"jiti\";\nimport type { ConnectionConfigBase, ResolveContext } from \"../connection\";\n\n// `better-schemic.ts` is the scaffolded name (the config IS the app's DB module — `betterSchemic.connect()`);\n// the legacy `schemic.config.*` / `schemic.ts` spellings keep working. Checked LAST + shape-guarded, so an unrelated\n// `./better-schemic.ts` helper module in a project never shadows a real `better-schemic.config.ts`.\nconst CONFIG_NAMES = [\n \"better-schemic.config.ts\",\n \"better-schemic.config.mjs\",\n \"better-schemic.config.js\",\n \"better-schemic.ts\",\n // Legacy aliases (pre-rename) — still discovered, after the canonical names.\n \"schemic.config.ts\",\n \"schemic.config.mjs\",\n \"schemic.config.js\",\n \"schemic.ts\",\n];\n\n/**\n * Is the schema path a single file (vs a directory of schema modules)? Determined by `stat` when\n * it exists, else inferred from a `.ts`/`.js`-ish extension.\n */\nfunction schemaIsFilePath(path: string): boolean {\n if (existsSync(path)) return statSync(path).isFile();\n return /\\.[mc]?[jt]s$/.test(path);\n}\n\n/**\n * Load `.env(.local)` from the project root into `process.env` so the config's own explicit\n * `process.env.X` reads resolve when run under node (bun loads `.env` itself). Does not override\n * already-set variables, so shell env still wins; load `.env.local` first so it beats `.env`.\n */\nfunction loadDotEnv(dir: string): void {\n const proc = process as typeof process & {\n loadEnvFile?: (path: string) => void;\n };\n if (typeof proc.loadEnvFile !== \"function\") return;\n for (const name of [\".env.local\", \".env\"]) {\n const file = resolve(dir, name);\n if (!existsSync(file)) continue;\n try {\n proc.loadEnvFile(file);\n } catch {\n // ignore a malformed .env file\n }\n }\n}\n\n/**\n * A resolved, per-CONNECTION config — the dialect-NEUTRAL shape every command operates on (one\n * connection at a time). `params` are the driver-specific connection params (opaque to core; the\n * driver's `connect` reads them). Built by resolving one entry of `config.connections`.\n */\nexport interface ResolvedConfig {\n /** Resolved connection name (e.g. `default`, or `tenants:abc` within a collection). */\n connection: string;\n /** The driver this connection uses (the package the CLI dynamically loads). */\n driver: string;\n /** Project root (the directory containing the config file). */\n root: string;\n /** Absolute schema path — a single `.ts` module, or a directory of them. */\n schemaPath: string;\n /** Whether `schemaPath` is a single file (vs a directory of schema modules). */\n schemaIsFile: boolean;\n /** Absolute migrations directory (per connection's schema). */\n migrationsDir: string;\n /** Absolute migration meta directory (the snapshot). */\n metaDir: string;\n /** Name of the table that records applied migrations. */\n migrationsTable: string;\n /** Driver-specific connection params (url/namespace/… or whatever the driver defines). Opaque to core. */\n params: Record<string, unknown>;\n /** Optional seed script (project-level). */\n seed?: string;\n}\n\n/**\n * A jiti instance for loading the project's TS/ESM modules. Caches are off so `--watch` re-reads\n * edited schema files. (Bare deps like `@better-schemic/core` are native-imported, so registries stay shared.)\n */\nexport function makeJiti() {\n return createJiti(import.meta.url, {\n interopDefault: true,\n fsCache: false,\n moduleCache: false,\n });\n}\n\n/** Find + load `better-schemic.ts` / `better-schemic.config.ts` (legacy `schemic.*` aliases included) into the dialect-neutral {@link BetterSchemicConfig}. */\nexport async function loadProject(opts?: {\n config?: string;\n cwd?: string;\n}): Promise<{ config: BetterSchemicConfig; root: string }> {\n const cwd = opts?.cwd ?? process.cwd();\n const candidates = opts?.config\n ? [resolve(cwd, opts.config)]\n : CONFIG_NAMES.map((n) => resolve(cwd, n)).filter((p) => existsSync(p));\n if (!candidates.length || !existsSync(candidates[0])) {\n throw new Error(\n \"No better-schemic.ts / better-schemic.config.ts found — run `better-schemic init` first.\",\n );\n }\n const jiti = makeJiti();\n for (const path of candidates) {\n const root = dirname(path);\n loadDotEnv(root); // populate process.env before the config module's explicit reads\n const loaded = (await jiti.import(path)) as {\n default?: BetterSchemicConfig;\n betterSchemic?: BetterSchemicConfig;\n schemic?: BetterSchemicConfig;\n } & BetterSchemicConfig;\n // Accept a default export OR the named `betterSchemic` export (legacy: `schemic`) — the scaffolded\n // form is the NAMED one (`export const betterSchemic = defineConfig(...)`), so app code auto-imports\n // a deterministic identifier (`import { betterSchemic } from \"./better-schemic.config\"` ->\n // `betterSchemic.connect()`). Selected by SHAPE, not presence: jiti's interopDefault makes\n // `loaded.default` a truthy proxy even when the module has no real default export, so a presence\n // chain would shadow the named export.\n const config = [\n loaded.default,\n loaded.betterSchemic,\n loaded.schemic,\n loaded,\n ].find(\n (c): c is BetterSchemicConfig =>\n !!c && typeof c === \"object\" && \"connections\" in c,\n );\n if (config?.connections && Object.keys(config.connections).length > 0) {\n return { config, root };\n }\n // An AUTO-discovered bare `better-schemic.ts` (or legacy `schemic.ts`) without a connections map is\n // an unrelated helper module, not a config — skip it (an explicitly-passed or `*.config.*` file\n // still errors loudly).\n if (\n !opts?.config &&\n (basename(path) === \"better-schemic.ts\" ||\n basename(path) === \"schemic.ts\")\n )\n continue;\n throw new Error(`Invalid config at ${path}: expected a \"connections\" map.`);\n }\n throw new Error(\n 'No Better-schemic config found — ./better-schemic.ts exists but doesn\\'t export a config with a \"connections\" map. Run `better-schemic init`, or export one via defineConfig.',\n );\n}\n\n/**\n * Build the {@link ResolvedConfig} for one connection of the project. `ctx` carries the lazy\n * cross-connection proxy + CLI `--arg`s (the CLI provides the real one; a static connection ignores\n * it). A resolver returning a COLLECTION yields one ResolvedConfig per keyed entry.\n *\n * NOTE (WIP — multi-connection): the full resolution engine (lazy proxy DAG, `--connection`/`--all`\n * addressing, collection fan-out) lives in `@better-schemic/cli`; this builder handles a single resolved\n * connection config. See docs/MULTI-CONNECTION.md.\n */\nexport function resolveConnectionConfig(\n config: BetterSchemicConfig,\n connection: string,\n conn: ConnectionConfigBase,\n driver: string,\n root: string,\n): ResolvedConfig {\n const { schema, migrations, key, ...params } = conn;\n const schemaPath = resolve(root, schema);\n // Default migrations dir is RELATIVE TO THE SCHEMA (the documented contract): the sibling\n // `migrations` dir next to the schema dir (or next to a single-file schema). For the standard\n // scaffold (`schema: \"./database/schema\"`) that is `./database/migrations`, unchanged; a nested\n // schema (`./src/database/schema`) correctly gets `./src/database/migrations` instead of a\n // root-fixed default that split state across two locations.\n const migrationsDir = migrations\n ? resolve(root, migrations)\n : resolve(schemaPath, \"..\", \"migrations\");\n return {\n connection: key ? `${connection}:${key}` : connection,\n driver,\n root,\n schemaPath,\n schemaIsFile: schemaIsFilePath(schemaPath),\n migrationsDir,\n metaDir: resolve(migrationsDir, \"meta\"),\n migrationsTable: config.migrationsTable ?? \"_migrations\",\n params: params as Record<string, unknown>,\n seed: config.seed,\n };\n}\n\n/**\n * Load the project and resolve the DEFAULT connection to a {@link ResolvedConfig} — the single-\n * connection convenience path. (Multi-connection addressing + resolver context are added by the CLI;\n * here a static default connection is resolved with an empty context.)\n */\nexport async function loadConfig(opts?: {\n config?: string;\n cwd?: string;\n}): Promise<ResolvedConfig> {\n const { config, root } = await loadProject(opts);\n const names = Object.keys(config.connections);\n const name =\n config.defaultConnection ?? (names.length === 1 ? names[0] : \"default\");\n const entry = config.connections[name];\n if (!entry) {\n throw new Error(\n `No connection named \"${name}\". Set \"defaultConnection\" or pass --connection. Known: ${names.join(\", \")}.`,\n );\n }\n const ctx: ResolveContext = { connections: {}, env: process.env };\n const resolved = await entry.resolve(ctx);\n if (resolved.length !== 1) {\n throw new Error(\n `Connection \"${name}\" resolved to ${resolved.length} connections (a collection); pass --connection ${name}:<key>.`,\n );\n }\n return resolveConnectionConfig(config, name, resolved[0], entry.driver, root);\n}\n\n/** Per-command connection flag overrides (CLI args, applied by the driver over `params`). */\nexport interface ConnectionOverrides {\n url?: string;\n namespace?: string;\n database?: string;\n username?: string;\n password?: string;\n authLevel?: string;\n}\n","// The neutral foundation for the bound ORM CLIENT (P1) — the runtime read/write handle each driver\n// builds on. Core owns only the dialect-agnostic parts: the disposable lifecycle contract and the\n// managed-connection resolution. A driver's client (`@better-schemic/<driver>/client`) extends `OrmClientBase`,\n// binds its native connection, and adds its TYPED `select`/`call` (+ writes in P2). See\n// docs/proposals/managed-connections-and-orm-client.md.\n\nimport {\n loadProject,\n type ResolvedConfig,\n resolveConnectionConfig,\n} from \"./cli-kit/config\";\nimport type { BetterSchemicConfig } from \"./config\";\nimport type { AnyConnectionEntry, ResolveContext } from \"./connection\";\n\n/**\n * The neutral bound-client contract. A driver's client extends this and adds its typed query surface.\n * It is an `AsyncDisposable`, so `await using db = await connect()` closes it at block exit.\n *\n * DISPOSE RULE (hard): a MANAGED client (opened by {@link resolveConnection} + the driver's `connect`)\n * closes the connection it opened; a BYO client (wrapping the user's own pool) MUST make `close` a\n * NO-OP — we never close a connection the user owns. The driver enforces this at construction.\n */\nexport interface OrmClientBase extends AsyncDisposable {\n /** Close the underlying connection (managed only; a BYO client is a no-op — see the dispose rule). */\n close(): Promise<void>;\n}\n\n/** Mixin the default `[Symbol.asyncDispose]` (= `close`) onto a client class's prototype. */\nexport function asyncDisposable<T extends { close(): Promise<void> }>(\n proto: T,\n): void {\n (proto as { [Symbol.asyncDispose]?: () => Promise<void> })[\n Symbol.asyncDispose\n ] = function (this: T) {\n return this.close();\n };\n}\n\n/** Options for {@link resolveConnection}: which connection, and where the config lives. */\nexport interface ResolveConnectionOptions {\n /** Connection name; defaults to `defaultConnection`, else the sole connection, else `\"default\"`. */\n name?: string;\n /** Path to `better-schemic.config.ts` (else auto-discovered from `cwd`). */\n config?: string;\n /** Working directory to discover the config + resolve relative paths from. */\n cwd?: string;\n /** The resolver's typed args (its declared 2nd param) for a PARAMETERIZED connection. */\n args?: unknown;\n}\n\n/**\n * A live resolution context for RUNTIME connects: `ctx.connections.<name>` lazily opens the sibling\n * via ITS entry's embedded client opener (so a resolver can query another connection to enumerate a\n * fleet), with CYCLE detection; everything opened during resolution is closed when it settles.\n */\nfunction makeRuntimeContext(config: BetterSchemicConfig, root: string) {\n const opened = new Map<string, Promise<unknown>>();\n const resolving = new Set<string>();\n\n const open = async (name: string): Promise<unknown> => {\n if (resolving.has(name)) {\n throw new Error(\n `cyclic connection resolution: \"${name}\" is already resolving (a resolver reached back into itself via ctx.connections)`,\n );\n }\n const entry = config.connections[name];\n if (!entry) throw new Error(`ctx.connections.${name}: no such connection`);\n if (!entry.client) {\n throw new Error(\n `ctx.connections.${name}: the \"${entry.driver}\" connection factory predates runtime cross-connection access — update @better-schemic/${entry.driver}`,\n );\n }\n resolving.add(name);\n try {\n const bases = await entry.resolve(context, undefined);\n if (bases.length !== 1) {\n throw new Error(\n `ctx.connections.${name}: resolved to ${bases.length} configs — a sibling reached via ctx.connections must resolve to exactly one`,\n );\n }\n const rc = resolveConnectionConfig(\n config,\n name,\n bases[0],\n entry.driver,\n root,\n );\n return await entry.client(rc);\n } finally {\n resolving.delete(name);\n }\n };\n\n const context: ResolveContext = {\n env: process.env,\n connections: new Proxy(\n {},\n {\n get(_t, name: string) {\n const openOnce = () => {\n let client = opened.get(name);\n if (!client) {\n client = open(name);\n opened.set(name, client);\n }\n return client;\n };\n // The handle is THENABLE to the sibling's FULL ORM client (typed in the chained form:\n // `const main = await ctx.connections.main; main.select(...)`) and keeps a direct\n // `.query` for the neutral/literal form. Do NOT stash the client past resolution — it is\n // closed when resolution settles.\n return {\n query: async (sql: string, vars?: Record<string, unknown>) => {\n const db = (await openOnce()) as {\n query(sql: string, vars?: unknown): Promise<unknown>;\n };\n return db.query(sql, vars);\n },\n then: (\n onOk?: (v: unknown) => unknown,\n onErr?: (e: unknown) => unknown,\n ) => openOnce().then(onOk, onErr),\n };\n },\n },\n ) as ResolveContext[\"connections\"],\n };\n\n const closeOpened = async () => {\n for (const c of opened.values()) {\n try {\n await ((await c) as { close?: () => Promise<void> }).close?.();\n } catch {\n // best-effort cleanup of resolution-time siblings\n }\n }\n opened.clear();\n };\n\n return { context, closeOpened, resolving };\n}\n\n/**\n * Resolve ONE named connection from the disk-discovered project config — the MANAGED path a driver's\n * standalone `connect(name?)` uses before `driver.connect(config)`. Single-config or a teaching\n * error (a bulk resolution must be arg-selected).\n */\nexport async function resolveConnection(\n opts: ResolveConnectionOptions = {},\n): Promise<ResolvedConfig> {\n const { config, root } = await loadProject({\n config: opts.config,\n cwd: opts.cwd,\n });\n return exactlyOne(\n await resolveFromConfig(config, root, { name: opts.name, args: opts.args }),\n );\n}\n\n/**\n * The shared single-name resolution over an IN-MEMORY config: pick the entry (named /\n * defaultConnection / sole / `\"default\"`), run the resolver with its typed `args`, and build one\n * {@link ResolvedConfig} per returned config, each with a display label (config `key` > the entry's\n * `label` hook > positional `name[i]`). Resolvers may query siblings via `ctx.connections` (lazy,\n * cycle-checked; opened siblings are closed when resolution settles). Used by both\n * {@link resolveConnection} (disk-discovered config) and {@link connectFromConfig} (`config.connect`).\n */\nexport async function resolveFromConfig(\n config: BetterSchemicConfig,\n root: string,\n opts: { name?: string; args?: unknown } = {},\n): Promise<{\n entry: AnyConnectionEntry;\n name: string;\n resolved: ResolvedConfig[];\n labels: string[];\n}> {\n const names = Object.keys(config.connections);\n const name =\n opts.name ??\n config.defaultConnection ??\n (names.length === 1 ? names[0] : \"default\");\n const entry = config.connections[name];\n if (!entry) {\n throw new Error(\n `connection \"${name}\" is not defined in the config (have: ${names.join(\", \") || \"none\"})`,\n );\n }\n\n const rt = makeRuntimeContext(config, root);\n rt.resolving.add(name); // the target itself is mid-resolution — a self-reference is a cycle\n try {\n const bases = await entry.resolve(rt.context, opts.args);\n if (!bases.length) {\n throw new Error(`connection \"${name}\" resolved to no config`);\n }\n const resolved = bases.map((b) =>\n resolveConnectionConfig(config, name, b, entry.driver, root),\n );\n const labels = resolved.map(\n (rc, i) => bases[i].key ?? entry.label?.(rc) ?? `${name}[${i}]`,\n );\n return { entry, name, resolved, labels };\n } finally {\n rt.resolving.delete(name);\n await rt.closeOpened();\n }\n}\n\n/** Single-config resolution or a TEACHING error — bulk (array) resolutions must be arg-selected. */\nfunction exactlyOne(r: {\n name: string;\n resolved: ResolvedConfig[];\n labels: string[];\n}): ResolvedConfig {\n if (r.resolved.length !== 1) {\n throw new Error(\n `connection \"${r.name}\" resolved to ${r.resolved.length} configs (${r.labels.join(\", \")}) — ` +\n `a parameterized/bulk connection. Pass args selecting exactly one, e.g. betterSchemic.connect(\"${r.name}\", { ... }).`,\n );\n }\n return r.resolved[0];\n}\n\n/**\n * The runtime behind `config.connect(name, args?)` (see `defineConfig`): resolve the entry from the\n * in-memory config and open its bound ORM client via the factory-embedded\n * {@link AnyConnectionEntry.client} opener. The static return type is the entry's own client type\n * (inferred per entry in `BetterSchemicProject`).\n */\nexport async function connectFromConfig(\n config: BetterSchemicConfig,\n name?: string,\n args?: unknown,\n): Promise<unknown> {\n const r = await resolveFromConfig(config, process.cwd(), { name, args });\n const resolved = exactlyOne(r);\n if (!r.entry.client) {\n throw new Error(\n `the \"${r.entry.driver}\" connection factory predates config.connect() — update @better-schemic/${r.entry.driver} to a version whose ${r.entry.driver}Connection embeds a client opener`,\n );\n }\n return r.entry.client(resolved);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,13 @@
1
+ import {
2
+ asyncDisposable,
3
+ connectFromConfig,
4
+ resolveConnection,
5
+ resolveFromConfig
6
+ } from "./chunk-RSGP7GVO.js";
7
+ export {
8
+ asyncDisposable,
9
+ connectFromConfig,
10
+ resolveConnection,
11
+ resolveFromConfig
12
+ };
13
+ //# sourceMappingURL=client-HZF4ZWGO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,259 @@
1
+ import * as jiti from 'jiti';
2
+
3
+ /**
4
+ * A resolved, per-CONNECTION config — the dialect-NEUTRAL shape every command operates on (one
5
+ * connection at a time). `params` are the driver-specific connection params (opaque to core; the
6
+ * driver's `connect` reads them). Built by resolving one entry of `config.connections`.
7
+ */
8
+ interface ResolvedConfig {
9
+ /** Resolved connection name (e.g. `default`, or `tenants:abc` within a collection). */
10
+ connection: string;
11
+ /** The driver this connection uses (the package the CLI dynamically loads). */
12
+ driver: string;
13
+ /** Project root (the directory containing the config file). */
14
+ root: string;
15
+ /** Absolute schema path — a single `.ts` module, or a directory of them. */
16
+ schemaPath: string;
17
+ /** Whether `schemaPath` is a single file (vs a directory of schema modules). */
18
+ schemaIsFile: boolean;
19
+ /** Absolute migrations directory (per connection's schema). */
20
+ migrationsDir: string;
21
+ /** Absolute migration meta directory (the snapshot). */
22
+ metaDir: string;
23
+ /** Name of the table that records applied migrations. */
24
+ migrationsTable: string;
25
+ /** Driver-specific connection params (url/namespace/… or whatever the driver defines). Opaque to core. */
26
+ params: Record<string, unknown>;
27
+ /** Optional seed script (project-level). */
28
+ seed?: string;
29
+ }
30
+ /**
31
+ * A jiti instance for loading the project's TS/ESM modules. Caches are off so `--watch` re-reads
32
+ * edited schema files. (Bare deps like `@better-schemic/core` are native-imported, so registries stay shared.)
33
+ */
34
+ declare function makeJiti(): jiti.Jiti;
35
+ /** Find + load `better-schemic.ts` / `better-schemic.config.ts` (legacy `schemic.*` aliases included) into the dialect-neutral {@link BetterSchemicConfig}. */
36
+ declare function loadProject(opts?: {
37
+ config?: string;
38
+ cwd?: string;
39
+ }): Promise<{
40
+ config: BetterSchemicConfig;
41
+ root: string;
42
+ }>;
43
+ /**
44
+ * Build the {@link ResolvedConfig} for one connection of the project. `ctx` carries the lazy
45
+ * cross-connection proxy + CLI `--arg`s (the CLI provides the real one; a static connection ignores
46
+ * it). A resolver returning a COLLECTION yields one ResolvedConfig per keyed entry.
47
+ *
48
+ * NOTE (WIP — multi-connection): the full resolution engine (lazy proxy DAG, `--connection`/`--all`
49
+ * addressing, collection fan-out) lives in `@better-schemic/cli`; this builder handles a single resolved
50
+ * connection config. See docs/MULTI-CONNECTION.md.
51
+ */
52
+ declare function resolveConnectionConfig(config: BetterSchemicConfig, connection: string, conn: ConnectionConfigBase, driver: string, root: string): ResolvedConfig;
53
+ /**
54
+ * Load the project and resolve the DEFAULT connection to a {@link ResolvedConfig} — the single-
55
+ * connection convenience path. (Multi-connection addressing + resolver context are added by the CLI;
56
+ * here a static default connection is resolved with an empty context.)
57
+ */
58
+ declare function loadConfig(opts?: {
59
+ config?: string;
60
+ cwd?: string;
61
+ }): Promise<ResolvedConfig>;
62
+
63
+ type MaybePromise<T> = T | Promise<T>;
64
+ /** Minimal Standard Schema v1 surface — what a connection's `args` schema must expose. */
65
+ interface StandardSchemaLike {
66
+ "~standard": {
67
+ validate(value: unknown): MaybePromise<{
68
+ value: unknown;
69
+ } | {
70
+ issues: readonly {
71
+ message: string;
72
+ }[];
73
+ }>;
74
+ };
75
+ }
76
+ /** The dialect-neutral fields the orchestration reads off every connection config. */
77
+ interface ConnectionConfigBase {
78
+ /** Schema dir (the desired state + its migration files/snapshot). Shared dir = shared schema. */
79
+ schema: string;
80
+ /** Optional DISPLAY label for this config within a bulk (array) resolution — reporting/logs only. */
81
+ key?: string;
82
+ /** Migrations dir override; defaults relative to `schema`. */
83
+ migrations?: string;
84
+ }
85
+ /** A live, queryable handle to ANOTHER (already-resolved) connection, for use inside a resolver. */
86
+ interface ResolvedConnectionHandle {
87
+ query<T = unknown>(sql: string, vars?: Record<string, unknown>): Promise<T[]>;
88
+ }
89
+ /**
90
+ * What a connection RESOLVER receives. `connections` is a LAZY proxy of the other connections —
91
+ * touching one resolves + connects it on demand (so the dependency graph falls out of access; cycles
92
+ * error). `args` are CLI `--arg k=v` values (so a resolver can yield a SUBSET without resolving all).
93
+ */
94
+ interface ResolveContext {
95
+ connections: Record<string, ResolvedConnectionHandle>;
96
+ env: NodeJS.ProcessEnv;
97
+ }
98
+ /**
99
+ * The opaque, branded output of a `<driver>Connection(...)` factory — the only thing `defineConfig`'s
100
+ * `connections` map accepts. Never hand-authored. `driver` is the package the CLI dynamically loads;
101
+ * `resolve` always normalizes to an ARRAY (a single connection -> one element, a collection -> many).
102
+ */
103
+ interface ConnectionEntry<Client = unknown, Args = undefined> {
104
+ readonly __betterSchemic: "connection";
105
+ readonly driver: string;
106
+ resolve(ctx: ResolveContext, args?: Args): Promise<ConnectionConfigBase[]>;
107
+ /**
108
+ * Lazily open this connection's bound ORM CLIENT for a resolved config — embedded by the driver
109
+ * factory (with a lazy `import()` of its own client module, so authoring a config never pulls the
110
+ * engine). This is what powers the typed `config.connect(name)` on `defineConfig`'s return.
111
+ */
112
+ client?(config: ResolvedConfig): Promise<Client>;
113
+ /**
114
+ * Dialect-specific DISPLAY identity for a resolved config (bulk reporting / errors / logs) —
115
+ * e.g. surreal `ns/db`. Precedence: config `key` > this hook > positional `name[i]`.
116
+ */
117
+ label?(config: ResolvedConfig): string;
118
+ /** PHANTOM (never assigned) — anchors `Client`/`Args` so `config.connect` can infer them per entry. */
119
+ readonly __types?: {
120
+ client: Client;
121
+ args: Args;
122
+ };
123
+ }
124
+ /** Cross-driver erasure of the entry generics (like `AnyField`) — the shape neutral maps hold. */
125
+ type AnyConnectionEntry = ConnectionEntry<any, any>;
126
+ /** A connection factory's input: a static config, or a resolver yielding one config or a keyed collection. */
127
+ type ConnectionInput<C extends ConnectionConfigBase, Args = undefined> = C | ((ctx: ResolveContext, args: Args) => MaybePromise<C | C[]>);
128
+ /**
129
+ * Build a {@link ConnectionEntry} from a driver tag + a static config or resolver — the primitive each
130
+ * driver package wraps in its typed `<driver>Connection(...)` factory (which fixes `C` to the driver's
131
+ * own connection shape and overloads the array form to require `key`). Returns a branded entry whose
132
+ * `resolve` always yields an array. `extras` carries the factory-embedded client opener (for
133
+ * `config.connect`) and the optional args schema.
134
+ */
135
+ declare function connectionEntry<C extends ConnectionConfigBase, Client = unknown, Args = undefined>(driver: string, input: ConnectionInput<C, Args>, extras?: {
136
+ client?: (config: ResolvedConfig) => Promise<Client>;
137
+ label?: (config: ResolvedConfig) => string;
138
+ }): ConnectionEntry<Client, Args>;
139
+ /** Type guard: is a `connections` map value a real factory output (vs a stray object)? */
140
+ declare function isConnectionEntry(v: unknown): v is ConnectionEntry;
141
+
142
+ /**
143
+ * Configuration for the `better-schemic` CLI — author it in `better-schemic.config.ts`
144
+ * (`schemic.config.ts` still loads as a legacy alias).
145
+ *
146
+ * A project declares one or more named CONNECTIONS, each built by a per-driver factory
147
+ * (`<driver>Connection(...)` exported from `@better-schemic/<driver>/connection`). Connection values are EXPLICIT —
148
+ * there is no env-var magic; read env yourself where you want it (`url: process.env.MY_URL`).
149
+ * See `@better-schemic/core` docs/MULTI-CONNECTION.md.
150
+ *
151
+ * ```ts
152
+ * import { defineConfig } from "@better-schemic/core/config";
153
+ * import { surrealConnection } from "@better-schemic/surrealdb/connection";
154
+ *
155
+ * export default defineConfig({
156
+ * connections: {
157
+ * default: surrealConnection({
158
+ * schema: "./database/schema",
159
+ * url: "ws://localhost:8000",
160
+ * namespace: "app",
161
+ * database: "app",
162
+ * }),
163
+ * },
164
+ * });
165
+ * ```
166
+ *
167
+ * For MULTIPLE databases (multi-tenant / heterogeneous / DB-per-user), add more named connections;
168
+ * a connection may be a resolver (incl. an array → a collection). See docs/MULTI-CONNECTION.md.
169
+ *
170
+ * NOTE: this file is dialect-NEUTRAL. Driver-specific connection shapes (SurrealDB's
171
+ * url/namespace/authLevel, its check-engine options, …) live in the driver package's
172
+ * `<driver>Connection` factory, not here.
173
+ */
174
+
175
+ interface BetterSchemicConfig {
176
+ /** Named database connections — each produced by a per-driver `<driver>Connection(...)` factory. */
177
+ connections: Record<string, AnyConnectionEntry>;
178
+ /**
179
+ * With more than one connection, the connection a bare command targets (must name a single static
180
+ * connection). Absent + ambiguous → a live command errors asking for `--connection`.
181
+ */
182
+ defaultConnection?: string;
183
+ /** Table that records applied migrations (per connection). Default `_migrations`. */
184
+ migrationsTable?: string;
185
+ /** Optional seed script run by `better-schemic seed`. */
186
+ seed?: string;
187
+ }
188
+ /**
189
+ * Legacy alias — prefer {@link BetterSchemicConfig}.
190
+ */
191
+ type SchemicConfig = BetterSchemicConfig;
192
+ /** The bound ORM client type a {@link ConnectionEntry} opens (inferred from the driver factory). */
193
+ type EntryClient<E> = E extends ConnectionEntry<infer Client, any> ? Client : never;
194
+ /** The typed resolver `args` a {@link ConnectionEntry} accepts (from its `args` schema). */
195
+ type EntryArgs<E> = E extends ConnectionEntry<any, infer Args> ? Args : never;
196
+ /**
197
+ * What {@link defineConfig} ADDS to your config: the config IS the app's typed entry point to its
198
+ * databases. `connect(name, args?)` autocompletes your connection names, types `args` per connection
199
+ * (the resolver's declared 2nd param — absent for a static/argless connection), and returns that
200
+ * entry's own client type (a heterogeneous-driver project types per-connection). A PARAMETERIZED
201
+ * connection whose resolver returns an ARRAY is bulk-only: `connect` throws a teaching error — pass
202
+ * `args` selecting ONE config. The client is disposable: `await using db = await betterSchemic.connect()`.
203
+ */
204
+ interface BetterSchemicProject<Conns extends Record<string, AnyConnectionEntry>> {
205
+ connect<N extends keyof Conns & string>(name?: N, args?: EntryArgs<Conns[N]>): Promise<EntryClient<Conns[N]>>;
206
+ }
207
+ /**
208
+ * Type + enrich a Better-schemic config: returns the config with a typed `connect()` attached — the config
209
+ * itself is the factory. The loader accepts a `default` export OR the NAMED `betterSchemic` export
210
+ * (`schemic` still loads as a legacy alias); the scaffolded form is the named one
211
+ * (deterministic auto-import, no file rename needed):
212
+ *
213
+ * ```ts
214
+ * // better-schemic.config.ts (better-schemic.ts, schemic.config.ts, and schemic.ts also load)
215
+ * export const betterSchemic = defineConfig({ connections: { ... } });
216
+ * // app code: import { betterSchemic } from "./better-schemic.config"; → await using db = await betterSchemic.connect();
217
+ * ```
218
+ */
219
+ /**
220
+ * A resolver's `ctx` in the CHAINED form: `connections` is typed with the ACCUMULATED prior
221
+ * connections — each handle is thenable to that entry's FULL ORM client (`const main = await
222
+ * ctx.connections.main; main.select(...)`) and keeps a direct `.query`. Order = visibility: a
223
+ * resolver only sees connections declared BEFORE it (structural cycle prevention). Do not stash a
224
+ * sibling client — it is closed when resolution settles.
225
+ */
226
+ type ChainCtx<Conns extends Record<string, AnyConnectionEntry>> = Omit<ResolveContext, "connections"> & {
227
+ connections: {
228
+ [K in keyof Conns]: PromiseLike<EntryClient<Conns[K]>> & ResolvedConnectionHandle;
229
+ };
230
+ };
231
+ /** The shape a driver connection factory must have to be used as the `.connection()` driver marker. */
232
+ interface ChainableDriverFactory<C extends ConnectionConfigBase, Client> {
233
+ (input: ConnectionInput<C, any>): ConnectionEntry<Client, any>;
234
+ }
235
+ /**
236
+ * The CHAINED config builder (`defineConfig().connection(...)`): each `.connection(name, factory,
237
+ * input)` uses the driver FACTORY ITSELF as the driver marker and contextually types the resolver's
238
+ * `ctx.connections` with everything declared so far. The literal `defineConfig({ connections })`
239
+ * form remains for static maps.
240
+ */
241
+ interface ChainedConfig<Conns extends Record<string, AnyConnectionEntry>> extends BetterSchemicProject<Conns> {
242
+ connections: Conns;
243
+ defaultConnection?: string;
244
+ migrationsTable?: string;
245
+ seed?: string;
246
+ connection<N extends string, C extends ConnectionConfigBase, Client, Args = undefined>(name: N, factory: ChainableDriverFactory<C, Client>, input: C | ((ctx: ChainCtx<Conns>, args: Args) => C | C[] | Promise<C | C[]>)): ChainedConfig<Conns & {
247
+ [K in N]: ConnectionEntry<Client, Args>;
248
+ }>;
249
+ }
250
+ /** Start a CHAINED config: `defineConfig().connection("main", surrealConnection, {...})`. */
251
+ declare function defineConfig(base?: Omit<BetterSchemicConfig, "connections">): ChainedConfig<Record<never, never>>;
252
+ /** Type + enrich a literal config — returns it with the typed `connect()` attached. */
253
+ declare function defineConfig<const C extends BetterSchemicConfig>(config: C): C & BetterSchemicProject<C["connections"]>;
254
+ /**
255
+ * Legacy alias — prefer {@link BetterSchemicProject}.
256
+ */
257
+ type SchemicProject<Conns extends Record<string, AnyConnectionEntry>> = BetterSchemicProject<Conns>;
258
+
259
+ export { type AnyConnectionEntry as A, type BetterSchemicConfig as B, type ChainCtx as C, type EntryArgs as E, type ResolvedConfig as R, type SchemicConfig as S, type BetterSchemicProject as a, type ChainableDriverFactory as b, type ChainedConfig as c, type ConnectionConfigBase as d, type ConnectionEntry as e, type ConnectionInput as f, type EntryClient as g, type ResolveContext as h, type ResolvedConnectionHandle as i, type SchemicProject as j, type StandardSchemaLike as k, connectionEntry as l, isConnectionEntry as m, loadConfig as n, loadProject as o, makeJiti as p, defineConfig as q, resolveConnectionConfig as r };
@@ -0,0 +1,2 @@
1
+ export { B as BetterSchemicConfig, a as BetterSchemicProject, C as ChainCtx, b as ChainableDriverFactory, c as ChainedConfig, E as EntryArgs, g as EntryClient, S as SchemicConfig, j as SchemicProject, q as defineConfig } from './config-BYh7WA4P.js';
2
+ import 'jiti';
package/lib/config.js ADDED
@@ -0,0 +1,27 @@
1
+ // src/config.ts
2
+ function defineConfig(config) {
3
+ const isLiteral = !!config && "connections" in config;
4
+ const base = isLiteral ? config : {
5
+ ...config,
6
+ connections: {}
7
+ };
8
+ const withApi = (cfg) => ({
9
+ ...cfg,
10
+ async connect(name, args) {
11
+ const { connectFromConfig } = await import("./client-HZF4ZWGO.js");
12
+ return connectFromConfig(cfg, name, args);
13
+ },
14
+ connection(name, factory, input) {
15
+ const entry = factory(input);
16
+ return withApi({
17
+ ...cfg,
18
+ connections: { ...cfg.connections, [name]: entry }
19
+ });
20
+ }
21
+ });
22
+ return withApi(base);
23
+ }
24
+ export {
25
+ defineConfig
26
+ };
27
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["/**\n * Configuration for the `better-schemic` CLI — author it in `better-schemic.config.ts`\n * (`schemic.config.ts` still loads as a legacy alias).\n *\n * A project declares one or more named CONNECTIONS, each built by a per-driver factory\n * (`<driver>Connection(...)` exported from `@better-schemic/<driver>/connection`). Connection values are EXPLICIT —\n * there is no env-var magic; read env yourself where you want it (`url: process.env.MY_URL`).\n * See `@better-schemic/core` docs/MULTI-CONNECTION.md.\n *\n * ```ts\n * import { defineConfig } from \"@better-schemic/core/config\";\n * import { surrealConnection } from \"@better-schemic/surrealdb/connection\";\n *\n * export default defineConfig({\n * connections: {\n * default: surrealConnection({\n * schema: \"./database/schema\",\n * url: \"ws://localhost:8000\",\n * namespace: \"app\",\n * database: \"app\",\n * }),\n * },\n * });\n * ```\n *\n * For MULTIPLE databases (multi-tenant / heterogeneous / DB-per-user), add more named connections;\n * a connection may be a resolver (incl. an array → a collection). See docs/MULTI-CONNECTION.md.\n *\n * NOTE: this file is dialect-NEUTRAL. Driver-specific connection shapes (SurrealDB's\n * url/namespace/authLevel, its check-engine options, …) live in the driver package's\n * `<driver>Connection` factory, not here.\n */\nimport type {\n AnyConnectionEntry,\n ConnectionConfigBase,\n ConnectionEntry,\n ConnectionInput,\n ResolveContext,\n ResolvedConnectionHandle,\n} from \"./connection\";\n\nexport interface BetterSchemicConfig {\n /** Named database connections — each produced by a per-driver `<driver>Connection(...)` factory. */\n connections: Record<string, AnyConnectionEntry>;\n /**\n * With more than one connection, the connection a bare command targets (must name a single static\n * connection). Absent + ambiguous → a live command errors asking for `--connection`.\n */\n defaultConnection?: string;\n /** Table that records applied migrations (per connection). Default `_migrations`. */\n migrationsTable?: string;\n /** Optional seed script run by `better-schemic seed`. */\n seed?: string;\n}\n\n/**\n * Legacy alias — prefer {@link BetterSchemicConfig}.\n */\nexport type SchemicConfig = BetterSchemicConfig;\n\n/** The bound ORM client type a {@link ConnectionEntry} opens (inferred from the driver factory). */\n// biome-ignore lint/suspicious/noExplicitAny: matching the erased Args slot.\nexport type EntryClient<E> =\n E extends ConnectionEntry<infer Client, any> ? Client : never;\n/** The typed resolver `args` a {@link ConnectionEntry} accepts (from its `args` schema). */\n// biome-ignore lint/suspicious/noExplicitAny: matching the erased Client slot.\nexport type EntryArgs<E> =\n E extends ConnectionEntry<any, infer Args> ? Args : never;\n\n/**\n * What {@link defineConfig} ADDS to your config: the config IS the app's typed entry point to its\n * databases. `connect(name, args?)` autocompletes your connection names, types `args` per connection\n * (the resolver's declared 2nd param — absent for a static/argless connection), and returns that\n * entry's own client type (a heterogeneous-driver project types per-connection). A PARAMETERIZED\n * connection whose resolver returns an ARRAY is bulk-only: `connect` throws a teaching error — pass\n * `args` selecting ONE config. The client is disposable: `await using db = await betterSchemic.connect()`.\n */\nexport interface BetterSchemicProject<\n Conns extends Record<string, AnyConnectionEntry>,\n> {\n connect<N extends keyof Conns & string>(\n name?: N,\n args?: EntryArgs<Conns[N]>,\n ): Promise<EntryClient<Conns[N]>>;\n}\n\n/**\n * Type + enrich a Better-schemic config: returns the config with a typed `connect()` attached — the config\n * itself is the factory. The loader accepts a `default` export OR the NAMED `betterSchemic` export\n * (`schemic` still loads as a legacy alias); the scaffolded form is the named one\n * (deterministic auto-import, no file rename needed):\n *\n * ```ts\n * // better-schemic.config.ts (better-schemic.ts, schemic.config.ts, and schemic.ts also load)\n * export const betterSchemic = defineConfig({ connections: { ... } });\n * // app code: import { betterSchemic } from \"./better-schemic.config\"; → await using db = await betterSchemic.connect();\n * ```\n */\n/**\n * A resolver's `ctx` in the CHAINED form: `connections` is typed with the ACCUMULATED prior\n * connections — each handle is thenable to that entry's FULL ORM client (`const main = await\n * ctx.connections.main; main.select(...)`) and keeps a direct `.query`. Order = visibility: a\n * resolver only sees connections declared BEFORE it (structural cycle prevention). Do not stash a\n * sibling client — it is closed when resolution settles.\n */\nexport type ChainCtx<Conns extends Record<string, AnyConnectionEntry>> = Omit<\n ResolveContext,\n \"connections\"\n> & {\n connections: {\n [K in keyof Conns]: PromiseLike<EntryClient<Conns[K]>> &\n ResolvedConnectionHandle;\n };\n};\n\n/** The shape a driver connection factory must have to be used as the `.connection()` driver marker. */\nexport interface ChainableDriverFactory<\n C extends ConnectionConfigBase,\n Client,\n> {\n // biome-ignore lint/suspicious/noExplicitAny: the chain re-types input/args itself (variance cast).\n (input: ConnectionInput<C, any>): ConnectionEntry<Client, any>;\n}\n\n/**\n * The CHAINED config builder (`defineConfig().connection(...)`): each `.connection(name, factory,\n * input)` uses the driver FACTORY ITSELF as the driver marker and contextually types the resolver's\n * `ctx.connections` with everything declared so far. The literal `defineConfig({ connections })`\n * form remains for static maps.\n */\nexport interface ChainedConfig<Conns extends Record<string, AnyConnectionEntry>>\n extends BetterSchemicProject<Conns> {\n connections: Conns;\n defaultConnection?: string;\n migrationsTable?: string;\n seed?: string;\n connection<\n N extends string,\n C extends ConnectionConfigBase,\n Client,\n Args = undefined,\n >(\n name: N,\n factory: ChainableDriverFactory<C, Client>,\n input:\n | C\n | ((ctx: ChainCtx<Conns>, args: Args) => C | C[] | Promise<C | C[]>),\n ): ChainedConfig<Conns & { [K in N]: ConnectionEntry<Client, Args> }>;\n}\n\n/** Start a CHAINED config: `defineConfig().connection(\"main\", surrealConnection, {...})`. */\nexport function defineConfig(\n base?: Omit<BetterSchemicConfig, \"connections\">,\n): ChainedConfig<Record<never, never>>;\n/** Type + enrich a literal config — returns it with the typed `connect()` attached. */\nexport function defineConfig<const C extends BetterSchemicConfig>(\n config: C,\n): C & BetterSchemicProject<C[\"connections\"]>;\nexport function defineConfig(\n config?: BetterSchemicConfig | Omit<BetterSchemicConfig, \"connections\">,\n): unknown {\n const isLiteral = !!config && \"connections\" in config;\n const base: BetterSchemicConfig = isLiteral\n ? (config as BetterSchemicConfig)\n : {\n ...(config as Omit<BetterSchemicConfig, \"connections\"> | undefined),\n connections: {},\n };\n\n const withApi = (cfg: BetterSchemicConfig): unknown => ({\n ...cfg,\n async connect(name?: string, args?: unknown) {\n // Lazy: authoring/loading a config stays light; the client machinery loads only when used.\n const { connectFromConfig } = await import(\"./client\");\n return connectFromConfig(cfg, name, args);\n },\n connection(\n name: string,\n factory: (input: unknown) => AnyConnectionEntry,\n input: unknown,\n ) {\n // The factory IS the driver marker: it stamps the driver tag, config type, and client opener.\n // The chain re-types ctx/args itself, so the factory is called through a variance cast.\n const entry = factory(input as never);\n return withApi({\n ...cfg,\n connections: { ...cfg.connections, [name]: entry },\n });\n },\n });\n\n return withApi(base);\n}\n\n/**\n * Legacy alias — prefer {@link BetterSchemicProject}.\n */\nexport type SchemicProject<Conns extends Record<string, AnyConnectionEntry>> =\n BetterSchemicProject<Conns>;\n"],"mappings":";AA8JO,SAAS,aACd,QACS;AACT,QAAM,YAAY,CAAC,CAAC,UAAU,iBAAiB;AAC/C,QAAM,OAA4B,YAC7B,SACD;AAAA,IACE,GAAI;AAAA,IACJ,aAAa,CAAC;AAAA,EAChB;AAEJ,QAAM,UAAU,CAAC,SAAuC;AAAA,IACtD,GAAG;AAAA,IACH,MAAM,QAAQ,MAAe,MAAgB;AAE3C,YAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,sBAAU;AACrD,aAAO,kBAAkB,KAAK,MAAM,IAAI;AAAA,IAC1C;AAAA,IACA,WACE,MACA,SACA,OACA;AAGA,YAAM,QAAQ,QAAQ,KAAc;AACpC,aAAO,QAAQ;AAAA,QACb,GAAG;AAAA,QACH,aAAa,EAAE,GAAG,IAAI,aAAa,CAAC,IAAI,GAAG,MAAM;AAAA,MACnD,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,QAAQ,IAAI;AACrB;","names":[]}