@c9up/atlas 0.3.10 → 0.3.12

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 (57) hide show
  1. package/db.darwin-arm64.node +0 -0
  2. package/db.darwin-x64.node +0 -0
  3. package/db.linux-arm64-gnu.node +0 -0
  4. package/db.linux-x64-gnu.node +0 -0
  5. package/db.win32-x64-msvc.node +0 -0
  6. package/dist/AtlasProvider.d.ts +1 -0
  7. package/dist/AtlasProvider.d.ts.map +1 -1
  8. package/dist/AtlasProvider.js +52 -13
  9. package/dist/AtlasProvider.js.map +1 -1
  10. package/dist/BaseRepository.d.ts.map +1 -1
  11. package/dist/BaseRepository.js +30 -12
  12. package/dist/BaseRepository.js.map +1 -1
  13. package/dist/ModelQuery.d.ts.map +1 -1
  14. package/dist/ModelQuery.js +34 -13
  15. package/dist/ModelQuery.js.map +1 -1
  16. package/dist/augmentations.d.ts +44 -0
  17. package/dist/augmentations.d.ts.map +1 -0
  18. package/dist/augmentations.js +19 -0
  19. package/dist/augmentations.js.map +1 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -1
  25. package/dist/query/DatabaseQueryBuilder.js +24 -8
  26. package/dist/query/DatabaseQueryBuilder.js.map +1 -1
  27. package/dist/query/QueryBuilder.js +15 -1
  28. package/dist/query/QueryBuilder.js.map +1 -1
  29. package/dist/schema/MigrationRunner.d.ts.map +1 -1
  30. package/dist/schema/MigrationRunner.js +112 -7
  31. package/dist/schema/MigrationRunner.js.map +1 -1
  32. package/dist/schema/SchemaCheck.d.ts.map +1 -1
  33. package/dist/schema/SchemaCheck.js +19 -7
  34. package/dist/schema/SchemaCheck.js.map +1 -1
  35. package/dist/testing/DatabaseCleanup.js +15 -1
  36. package/dist/testing/DatabaseCleanup.js.map +1 -1
  37. package/dist/testing/Factory.js +2 -2
  38. package/dist/testing/Factory.js.map +1 -1
  39. package/index.darwin-arm64.node +0 -0
  40. package/index.darwin-x64.node +0 -0
  41. package/index.linux-arm64-gnu.node +0 -0
  42. package/index.linux-x64-gnu.node +0 -0
  43. package/index.win32-x64-msvc.node +0 -0
  44. package/package.json +10 -5
  45. package/scripts/build-napi-types.mjs +4 -3
  46. package/scripts/generate-napi-types.mjs +9 -6
  47. package/src/AtlasProvider.ts +65 -18
  48. package/src/BaseRepository.ts +30 -12
  49. package/src/ModelQuery.ts +37 -11
  50. package/src/augmentations.ts +49 -0
  51. package/src/index.ts +1 -0
  52. package/src/query/DatabaseQueryBuilder.ts +25 -8
  53. package/src/query/QueryBuilder.ts +16 -1
  54. package/src/schema/MigrationRunner.ts +124 -11
  55. package/src/schema/SchemaCheck.ts +27 -10
  56. package/src/testing/DatabaseCleanup.ts +16 -1
  57. package/src/testing/Factory.ts +2 -2
@@ -7,7 +7,8 @@
7
7
  * cargo a `/tmp/...` path the native proc-macro cannot write to, so the
8
8
  * type-def file comes back empty and the build fails for no visible reason.
9
9
  *
10
- * One crate at a time on purpose: napi-derive APPENDS to `TYPE_DEF_TMP_PATH`
10
+ * One crate at a time on purpose: with napi-derive 2 the writes all went to a
11
+ * single `TYPE_DEF_TMP_PATH` and a parallel build interleaved them
11
12
  * while cargo compiles, and a parallel build interleaves the writes —
12
13
  * definitions go missing, silently, and the generated file comes out short.
13
14
  */
@@ -28,11 +29,11 @@ const scratch = mkdtempSync(join(tmpdir(), 'napi-types-'))
28
29
  try {
29
30
  const lines = []
30
31
  for (const crate of CRATES) {
31
- const perCrate = join(scratch, `${crate}.jsonl`)
32
+ const perCrate = join(scratch, crate)
32
33
  writeFileSync(perCrate, '')
33
34
  execFileSync('cargo', ['build', '-p', crate], {
34
35
  cwd: packageRoot,
35
- env: { ...process.env, TYPE_DEF_TMP_PATH: perCrate },
36
+ env: { ...process.env, NAPI_TYPE_DEF_TMP_FOLDER: scratch },
36
37
  stdio: ['ignore', 'ignore', 'inherit'],
37
38
  })
38
39
  const emitted = readFileSync(perCrate, 'utf8').split('\n').filter(Boolean)
@@ -130,12 +130,15 @@ for (const fn of fns) {
130
130
  // napi-derive emits the whole declaration for a function, unlike a struct
131
131
  // where `def` holds only the members.
132
132
  const declaration = fn.def.trim()
133
- out.push(
134
- declaration.startsWith('export declare function')
135
- ? `${declaration};`
136
- : `export declare function ${fn.name}${declaration};`,
137
- '',
138
- )
133
+ // napi-derive 3 emits a bare `function name(...)` where 2 emitted the
134
+ // signature after the name; concatenating onto the former produced
135
+ // `function xfunction x(...)`.
136
+ const rendered = declaration.startsWith('export declare function')
137
+ ? declaration
138
+ : declaration.startsWith('function ')
139
+ ? `export declare ${declaration}`
140
+ : `export declare function ${fn.name}${declaration}`
141
+ out.push(`${rendered};`, '')
139
142
  }
140
143
 
141
144
  const stale = Object.keys(CALLBACK_REFINEMENTS).filter((k) => !used.has(k))
@@ -21,6 +21,7 @@
21
21
  * const tenant1 = await app.container.resolve('db:tenant1') // named
22
22
  */
23
23
 
24
+ import "./augmentations.js";
24
25
  import {
25
26
  type AsyncDatabaseConnection,
26
27
  createNapiConnection,
@@ -63,10 +64,42 @@ export interface AtlasAppContext {
63
64
  config: { get<T = unknown>(key: string): T | undefined };
64
65
  }
65
66
 
66
- /** True when `x` looks like an event emitter with `emit(event, data)`. */
67
+ /**
68
+ * Forward one event to the application's emitter without letting it come back.
69
+ *
70
+ * `@adonisjs/events` declares `emit(): Promise<void>` and rethrows when a
71
+ * listener fails and the application registered no error handler. These bridges
72
+ * are driven by driver callbacks that nobody awaits, so that rejection had
73
+ * nowhere to go and ended the process — a query-log listener taking down the
74
+ * request it was observing.
75
+ *
76
+ * Called INSIDE the async function: `Promise.resolve(emit(...))` runs `emit`
77
+ * first, so a synchronous throw would still escape.
78
+ */
79
+ function forward(
80
+ emitter: { emit: (event: string, data: unknown) => unknown },
81
+ event: string,
82
+ data: unknown,
83
+ ): void {
84
+ void (async () => emitter.emit(event, data))().catch((error: unknown) => {
85
+ process.stderr.write(
86
+ `[atlas] '${event}' listener failed: ${
87
+ error instanceof Error ? error.message : String(error)
88
+ }\n`,
89
+ );
90
+ });
91
+ }
92
+
93
+ /**
94
+ * True when `x` looks like an event emitter with `emit(event, data)`.
95
+ *
96
+ * The return is `unknown`, not `void`: `void` ACCEPTS a promise-returning
97
+ * function and then reads as if there were nothing to handle, which is exactly
98
+ * how an Adonis emitter's rejection went unnoticed here.
99
+ */
67
100
  function hasEmit(
68
101
  x: unknown,
69
- ): x is { emit: (event: string, data: unknown) => void } {
102
+ ): x is { emit: (event: string, data: unknown) => unknown } {
70
103
  return (
71
104
  typeof x === "object" &&
72
105
  x !== null &&
@@ -287,7 +320,7 @@ export default class AtlasProvider {
287
320
  const { onDbQuery } = await import("./events.js");
288
321
  this.#dbQueryBridge?.();
289
322
  this.#dbQueryBridge = onDbQuery((event) => {
290
- emitter.emit("db:query", event);
323
+ forward(emitter, "db:query", event);
291
324
  });
292
325
  }
293
326
 
@@ -313,11 +346,11 @@ export default class AtlasProvider {
313
346
  const mgr = connectionManager();
314
347
  this.#connectionBridge?.();
315
348
  const onConnect = (node: unknown) =>
316
- emitter.emit("db:connection:connect", node);
349
+ forward(emitter, "db:connection:connect", node);
317
350
  const onDisconnect = (node: unknown) =>
318
- emitter.emit("db:connection:disconnect", node);
351
+ forward(emitter, "db:connection:disconnect", node);
319
352
  const onError = (node: unknown, err?: unknown) =>
320
- emitter.emit("db:connection:error", [err, node]);
353
+ forward(emitter, "db:connection:error", [err, node]);
321
354
  mgr.on("connect", onConnect);
322
355
  mgr.on("disconnect", onDisconnect);
323
356
  mgr.on("error", onError);
@@ -382,7 +415,10 @@ export default class AtlasProvider {
382
415
  const successes: Array<{ name: string; conn: AsyncDatabaseConnection }> =
383
416
  [];
384
417
  results.forEach((r, i) => {
385
- const [name] = entries[i];
418
+ // `results` came from `entries`, one for one, so the pair is there.
419
+ const entry = entries[i];
420
+ if (entry === undefined) return;
421
+ const [name] = entry;
386
422
  if (r.status === "fulfilled") successes.push({ name, conn: r.value });
387
423
  else failures.push({ name, error: r.reason });
388
424
  });
@@ -396,7 +432,8 @@ export default class AtlasProvider {
396
432
  // has already opened. Closures run in parallel with allSettled so a
397
433
  // stuck close doesn't block the rollback path.
398
434
  await Promise.allSettled(successes.map((s) => s.conn.close()));
399
- const first = failures[0];
435
+ const [first = { name: "(unknown)", error: new Error("unknown") }] =
436
+ failures;
400
437
  const others = failures
401
438
  .slice(1)
402
439
  .map((f) => `${f.name}: ${String(f.error)}`)
@@ -432,18 +469,26 @@ export default class AtlasProvider {
432
469
  for (const { name, conn } of successes) {
433
470
  this.#connections.set(name, conn);
434
471
  dbServices.registerConnection(name, conn, connections[name]);
472
+ this.app.container.singleton(`atlas.db:${name}`, () =>
473
+ this.#requireConnection(name),
474
+ );
435
475
  this.app.container.singleton(`db:${name}`, () =>
436
476
  this.#requireConnection(name),
437
477
  );
438
478
  }
439
479
 
440
480
  // Expose the default under the short aliases `db` and `db.connection`.
441
- this.app.container.singleton("db", () =>
442
- this.#requireConnection(this.#defaultName),
443
- );
444
- this.app.container.singleton("db.connection", () =>
445
- this.#requireConnection(this.#defaultName),
446
- );
481
+ // Namespaced by the package that owns it, the way upstream namespaces
482
+ // `lucid.db`, `auth.manager` and `drive.manager` by theirs. The bare
483
+ // token stays bound beside it: it is what every existing
484
+ // `container.make(...)` asks for, and a token is not worth breaking an
485
+ // application over.
486
+ const defaultConnection = (): AsyncDatabaseConnection =>
487
+ this.#requireConnection(this.#defaultName);
488
+ this.app.container.singleton("atlas.db", defaultConnection);
489
+ this.app.container.singleton("db", defaultConnection);
490
+ this.app.container.singleton("atlas.db.connection", defaultConnection);
491
+ this.app.container.singleton("db.connection", defaultConnection);
447
492
 
448
493
  // Populate the `@c9up/atlas/services/db` proxy so apps can
449
494
  // `import db from '@c9up/atlas/services/db'` from anywhere.
@@ -456,7 +501,7 @@ export default class AtlasProvider {
456
501
  // The dialect set module-wide is the DEFAULT connection's dialect.
457
502
  // Per-connection dialect (when a user hits a non-default) is read from
458
503
  // the connection URL at query time by each call site that cares.
459
- setAtlasDialect(dialectFromUrl(connections[defaultName]?.url));
504
+ setAtlasDialect(dialectFromUrl(connections[defaultName]?.url ?? ""));
460
505
 
461
506
  // Auto-run migrations on boot — but NOT in production unless explicitly
462
507
  // opted in: starting the app should not silently mutate the schema in
@@ -485,7 +530,7 @@ export default class AtlasProvider {
485
530
  if (migrationsPath) {
486
531
  await this.#registerMigrationSource(
487
532
  migrationsPath,
488
- connections[defaultName]?.url,
533
+ connections[defaultName]?.url ?? "",
489
534
  defaultConn,
490
535
  config.migrations?.tableName ?? config.migrations?.table,
491
536
  {
@@ -497,7 +542,7 @@ export default class AtlasProvider {
497
542
  if (migrationsPath && !cliDrivesMigrations && autoMigrateAllowed) {
498
543
  await this.#runMigrations(
499
544
  migrationsPath,
500
- connections[defaultName]?.url,
545
+ connections[defaultName]?.url ?? "",
501
546
  defaultConn,
502
547
  config.migrations?.tableName ?? config.migrations?.table,
503
548
  {
@@ -557,7 +602,9 @@ export default class AtlasProvider {
557
602
  clearCastRegistry();
558
603
  const errors = results
559
604
  .map((r, i) =>
560
- r.status === "rejected" ? { name: named[i][0], error: r.reason } : null,
605
+ r.status === "rejected"
606
+ ? { name: named[i]?.[0] ?? "(unknown)", error: r.reason }
607
+ : null,
561
608
  )
562
609
  .filter((x): x is { name: string; error: unknown } => x !== null);
563
610
  if (errors.length > 0) {
@@ -1060,17 +1060,20 @@ export class BaseRepository<T extends BaseEntity> {
1060
1060
  };
1061
1061
  const compiled = compileStatementNative(spec, this.#dialect);
1062
1062
  const returned = await this.#db.query<Record<string, unknown>>(
1063
- compiled.statements[0],
1063
+ onlyStatement(compiled),
1064
1064
  compiled.params,
1065
1065
  );
1066
1066
  returned.forEach((row, i) => {
1067
+ // One returned row per entity written, in the same order.
1068
+ const entity = entities[i];
1069
+ if (entity === undefined) return;
1067
1070
  for (const [k, v] of Object.entries(row)) {
1068
1071
  const prop = this.#columnByDbName.get(k) ?? snakeToCamel(k);
1069
1072
  // Run the DB value through consume so date columns come back as
1070
1073
  // Chronos DateTime (not the raw ISO string) — mirrors #hydrate.
1071
- entities[i].setProp(prop, this.#applyConsume(prop, v, entities[i]));
1074
+ entity.setProp(prop, this.#applyConsume(prop, v, entity));
1072
1075
  }
1073
- entities[i].markAsPersisted();
1076
+ entity.markAsPersisted();
1074
1077
  });
1075
1078
  }
1076
1079
 
@@ -1187,7 +1190,7 @@ export class BaseRepository<T extends BaseEntity> {
1187
1190
  };
1188
1191
  const compiled = compileStatementNative(spec, this.#dialect);
1189
1192
  const result = await this.#db.execute(
1190
- compiled.statements[0],
1193
+ onlyStatement(compiled),
1191
1194
  compiled.params,
1192
1195
  );
1193
1196
  return result.rowsAffected;
@@ -1852,7 +1855,7 @@ export class BaseRepository<T extends BaseEntity> {
1852
1855
  { kind: "delete", table: this.#tableName, wheres },
1853
1856
  this.#dialect,
1854
1857
  );
1855
- await this.#db.execute(compiled.statements[0], compiled.params);
1858
+ await this.#db.execute(onlyStatement(compiled), compiled.params);
1856
1859
  }
1857
1860
 
1858
1861
  /**
@@ -1878,7 +1881,7 @@ export class BaseRepository<T extends BaseEntity> {
1878
1881
  },
1879
1882
  this.#dialect,
1880
1883
  );
1881
- await this.#db.execute(compiled.statements[0], compiled.params);
1884
+ await this.#db.execute(onlyStatement(compiled), compiled.params);
1882
1885
  }
1883
1886
 
1884
1887
  /**
@@ -1910,13 +1913,13 @@ export class BaseRepository<T extends BaseEntity> {
1910
1913
  const compiled = compileStatementNative(spec, this.#dialect);
1911
1914
  if (supportsReturning) {
1912
1915
  const rows = await this.#db.query<Row>(
1913
- compiled.statements[0],
1916
+ onlyStatement(compiled),
1914
1917
  compiled.params,
1915
1918
  );
1916
1919
  const first = rows[0];
1917
1920
  return first ? { row: first } : {};
1918
1921
  }
1919
- await this.#db.execute(compiled.statements[0], compiled.params);
1922
+ await this.#db.execute(onlyStatement(compiled), compiled.params);
1920
1923
  // MySQL path: napi adapter doesn't surface lastInsertRowid through
1921
1924
  // `execute()`. Callers that need it must use an explicit dialect-
1922
1925
  // specific query (e.g. `SELECT LAST_INSERT_ID()`). For Atlas's
@@ -2900,7 +2903,7 @@ export class BaseRepository<T extends BaseEntity> {
2900
2903
  };
2901
2904
  const compiled = compileStatementNative(selectSpec, dialect);
2902
2905
  const rows = await conn.query<Record<string, unknown>>(
2903
- compiled.statements[0],
2906
+ onlyStatement(compiled),
2904
2907
  compiled.params,
2905
2908
  );
2906
2909
  return rows.map((r) => ({ id: asId(r[pivotOther]), row: r }));
@@ -2937,7 +2940,7 @@ export class BaseRepository<T extends BaseEntity> {
2937
2940
  casts: pivotKeyCasts,
2938
2941
  };
2939
2942
  const compiled = compileStatementNative(spec, dialect);
2940
- await conn.execute(compiled.statements[0], compiled.params);
2943
+ await conn.execute(onlyStatement(compiled), compiled.params);
2941
2944
  };
2942
2945
 
2943
2946
  const attach = async (
@@ -2978,7 +2981,7 @@ export class BaseRepository<T extends BaseEntity> {
2978
2981
  casts: pivotCasts,
2979
2982
  };
2980
2983
  const compiled = compileStatementNative(spec, dialect);
2981
- await conn.execute(compiled.statements[0], compiled.params);
2984
+ await conn.execute(onlyStatement(compiled), compiled.params);
2982
2985
  };
2983
2986
 
2984
2987
  // Refresh one already-attached pivot row's attributes (sync's update arm,
@@ -3016,7 +3019,7 @@ export class BaseRepository<T extends BaseEntity> {
3016
3019
  casts,
3017
3020
  };
3018
3021
  const compiled = compileStatementNative(spec, dialect);
3019
- await conn.execute(compiled.statements[0], compiled.params);
3022
+ await conn.execute(onlyStatement(compiled), compiled.params);
3020
3023
  };
3021
3024
 
3022
3025
  /**
@@ -3381,3 +3384,18 @@ export function assertNotPromise(
3381
3384
  );
3382
3385
  }
3383
3386
  }
3387
+
3388
+ /**
3389
+ * The single statement a compile produced.
3390
+ *
3391
+ * `compileStatementNative` answers a list because a few specs expand to more
3392
+ * than one; the callers here compile specs that do not, and this is where that
3393
+ * is stated instead of reading index zero as a value that might not be there.
3394
+ */
3395
+ function onlyStatement(compiled: { statements: string[] }): string {
3396
+ const [statement] = compiled.statements;
3397
+ if (statement === undefined) {
3398
+ throw new Error("atlas: the query compiler produced no statement");
3399
+ }
3400
+ return statement;
3401
+ }
package/src/ModelQuery.ts CHANGED
@@ -1040,7 +1040,10 @@ export class ModelQuery<T extends BaseEntity> {
1040
1040
  const aliased = col.match(
1041
1041
  /^([A-Za-z_][A-Za-z0-9_]*)\s+as\s+([A-Za-z_][A-Za-z0-9_]*)$/i,
1042
1042
  );
1043
- if (aliased) return `${this.#resolveColumn(aliased[1])} AS ${aliased[2]}`;
1043
+ const [, aliasSource, aliasName] = aliased ?? [];
1044
+ if (aliasSource !== undefined && aliasName !== undefined) {
1045
+ return `${this.#resolveColumn(aliasSource)} AS ${aliasName}`;
1046
+ }
1044
1047
  return col;
1045
1048
  }
1046
1049
 
@@ -3001,7 +3004,13 @@ export class ModelQuery<T extends BaseEntity> {
3001
3004
  `Expected exactly one ${this.#tableName} but the query matched multiple rows (sole()).`,
3002
3005
  );
3003
3006
  }
3004
- return rows[0];
3007
+ const [only] = rows;
3008
+ if (only === undefined) {
3009
+ throw new Error(
3010
+ `Expected exactly one ${this.#tableName} but the query matched none (sole()).`,
3011
+ );
3012
+ }
3013
+ return only;
3005
3014
  }
3006
3015
 
3007
3016
  /**
@@ -3182,7 +3191,7 @@ export class ModelQuery<T extends BaseEntity> {
3182
3191
  */
3183
3192
  #compiledNative(): { sql: string; params: unknown[] } {
3184
3193
  const compiled = compileStatementNative(this.#buildSpec(), this.#dialect);
3185
- const sql = this.#commentPrefix() + compiled.statements[0];
3194
+ const sql = this.#commentPrefix() + onlyStatement(compiled);
3186
3195
  return { sql, params: compiled.params };
3187
3196
  }
3188
3197
 
@@ -4003,7 +4012,7 @@ export class ModelQuery<T extends BaseEntity> {
4003
4012
  };
4004
4013
  const compiled = compileStatementNative(spec, this.#dialect);
4005
4014
  return this.#db.query<Record<string, unknown>>(
4006
- compiled.statements[0],
4015
+ onlyStatement(compiled),
4007
4016
  compiled.params,
4008
4017
  );
4009
4018
  }
@@ -4730,12 +4739,14 @@ export class ModelQuery<T extends BaseEntity> {
4730
4739
  }
4731
4740
  // Build the disjunctive tuple comparison as a nested group of WHEREs.
4732
4741
  clone.where((q) => {
4733
- for (let i = 0; i < cols.length; i++) {
4742
+ for (const [i, col] of cols.entries()) {
4734
4743
  q.orWhere((inner) => {
4735
- for (let j = 0; j < i; j++) inner.where(cols[j], decoded.v[j]);
4744
+ for (const [j, earlier] of cols.slice(0, i).entries()) {
4745
+ inner.where(earlier, decoded.v[j]);
4746
+ }
4736
4747
  // The comparison has to follow the walk: `>` reads forward
4737
4748
  // through an ascending order, `<` through a descending one.
4738
- inner.where(cols[i], descending ? "<" : ">", decoded.v[i]);
4749
+ inner.where(col, descending ? "<" : ">", decoded.v[i]);
4739
4750
  });
4740
4751
  }
4741
4752
  });
@@ -5340,7 +5351,7 @@ export class ModelQuery<T extends BaseEntity> {
5340
5351
  const compiled = compileStatementNative(spec, this.#dialect);
5341
5352
  const r = await this.#raceTimeout(
5342
5353
  this.#db.execute(
5343
- this.#commentPrefix() + compiled.statements[0],
5354
+ this.#commentPrefix() + onlyStatement(compiled),
5344
5355
  compiled.params,
5345
5356
  this.#meta("dml"),
5346
5357
  ),
@@ -5394,7 +5405,7 @@ export class ModelQuery<T extends BaseEntity> {
5394
5405
  params: unknown[];
5395
5406
  } {
5396
5407
  const compiled = compileStatementNative(spec, this.#dialect);
5397
- const sql = this.#commentPrefix() + compiled.statements[0];
5408
+ const sql = this.#commentPrefix() + onlyStatement(compiled);
5398
5409
  return { sql, bindings: compiled.params, params: compiled.params };
5399
5410
  }
5400
5411
 
@@ -5453,7 +5464,7 @@ export class ModelQuery<T extends BaseEntity> {
5453
5464
  if (returning && returning.length > 0) {
5454
5465
  return this.#raceTimeout(
5455
5466
  this.#db.query<Record<string, unknown>>(
5456
- this.#commentPrefix() + compiled.statements[0],
5467
+ this.#commentPrefix() + onlyStatement(compiled),
5457
5468
  compiled.params,
5458
5469
  this.#meta("dml"),
5459
5470
  ),
@@ -5461,7 +5472,7 @@ export class ModelQuery<T extends BaseEntity> {
5461
5472
  }
5462
5473
  const r = await this.#raceTimeout(
5463
5474
  this.#db.execute(
5464
- this.#commentPrefix() + compiled.statements[0],
5475
+ this.#commentPrefix() + onlyStatement(compiled),
5465
5476
  compiled.params,
5466
5477
  this.#meta("dml"),
5467
5478
  ),
@@ -5597,3 +5608,18 @@ export class ModelQuery<T extends BaseEntity> {
5597
5608
 
5598
5609
  /** `col as alias` — the Lucid aggregate spelling. */
5599
5610
  const ALIASED = /^(.*?)\s+as\s+(\S+)$/i;
5611
+
5612
+ /**
5613
+ * The single statement a compile produced.
5614
+ *
5615
+ * `compileStatementNative` answers a list because a few specs expand to more
5616
+ * than one; the callers here compile specs that do not, and this is where that
5617
+ * is stated instead of reading index zero as a value that might not be there.
5618
+ */
5619
+ function onlyStatement(compiled: { statements: string[] }): string {
5620
+ const [statement] = compiled.statements;
5621
+ if (statement === undefined) {
5622
+ throw new Error("atlas: the query compiler produced no statement");
5623
+ }
5624
+ return statement;
5625
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Teach ream's `ContainerBindings` what `container.make('db')` returns.
3
+ *
4
+ * ream declares that interface open on purpose: it registers its own entries
5
+ * and expects each package to contribute the one it owns — the comment on the
6
+ * interface names `db` (atlas) as exactly this. Nothing filled it in, so
7
+ * resolving by the string token answered `unknown` and every call site had to
8
+ * assert a type it could not prove.
9
+ *
10
+ * Loaded from the package barrel and from the provider, so registering atlas
11
+ * is enough — an application writes no `declare module` of its own.
12
+ *
13
+ * Type-only, and ream is an OPTIONAL peer: nothing here reaches a runtime
14
+ * import, the provider still duck-types its host, and a `declare module` for a
15
+ * specifier that does not resolve is simply inert. Atlas stays usable with no
16
+ * framework at all.
17
+ */
18
+
19
+ // Referenced so the augmentation below resolves the module it augments.
20
+ import type {} from "@c9up/ream/types";
21
+ import type { AsyncDatabaseConnection } from "./adapters/NapiDbAdapter.js";
22
+
23
+ declare module "@c9up/ream/types" {
24
+ interface ContainerBindings {
25
+ /** The default connection, bound by `AtlasProvider`. */
26
+ "atlas.db": AsyncDatabaseConnection;
27
+ /**
28
+ * The same binding under the name it had before the token carried its
29
+ * package. Kept bound so an existing `container.make(...)` resolves.
30
+ */
31
+ db: AsyncDatabaseConnection;
32
+ /** The default connection, under the longer name. */
33
+ "atlas.db.connection": AsyncDatabaseConnection;
34
+ /**
35
+ * The same binding under the name it had before the token carried its
36
+ * package. Kept bound so an existing `container.make(...)` resolves.
37
+ */
38
+ "db.connection": AsyncDatabaseConnection;
39
+ /**
40
+ * One named connection per entry in the config — `db:primary`,
41
+ * `db:replica`. A template-literal key rather than an enumeration,
42
+ * because the names come from an application's config file and this
43
+ * package cannot know them.
44
+ */
45
+ [
46
+ connection: `atlas.db:${string}` | `db:${string}`
47
+ ]: AsyncDatabaseConnection;
48
+ }
49
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import "reflect-metadata";
8
+ import "./augmentations.js";
8
9
 
9
10
  export { SQLITE_PROD_PRAGMAS } from "./AtlasProvider.js";
10
11
  export type {
@@ -2125,7 +2125,7 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
2125
2125
  */
2126
2126
  #compiledNative(): { sql: string; params: unknown[] } {
2127
2127
  const compiled = compileStatementNative(this.#selectSpec(), this.#dialect);
2128
- const sql = this.#commentPrefix() + compiled.statements[0];
2128
+ const sql = this.#commentPrefix() + onlyStatement(compiled);
2129
2129
  if (this.#debug) {
2130
2130
  console.debug("[atlas:sql]", sql, compiled.params);
2131
2131
  }
@@ -2222,7 +2222,7 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
2222
2222
  );
2223
2223
  const rows = await this.#raceTimeout(
2224
2224
  this.#exec.query<{ aggregate: number | string | null }>(
2225
- compiled.statements[0],
2225
+ onlyStatement(compiled),
2226
2226
  compiled.params,
2227
2227
  this.#queryMeta("aggregate"),
2228
2228
  ),
@@ -2235,8 +2235,10 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
2235
2235
  * `'* as total'` → `COUNT(*) AS total`; `'amount'` → `SUM(amount)`.
2236
2236
  */
2237
2237
  #aggProjection(fn: string, expr: string): string {
2238
- const m = expr.match(/^(.*?)\s+as\s+(.+)$/i);
2239
- return m ? `${fn}(${m[1].trim()}) AS ${m[2].trim()}` : `${fn}(${expr})`;
2238
+ const [, source, alias] = expr.match(/^(.*?)\s+as\s+(.+)$/i) ?? [];
2239
+ return source !== undefined && alias !== undefined
2240
+ ? `${fn}(${source.trim()}) AS ${alias.trim()}`
2241
+ : `${fn}(${expr})`;
2240
2242
  }
2241
2243
 
2242
2244
  /**
@@ -2401,7 +2403,7 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
2401
2403
  ? { ...spec, returning: this.#returningCols }
2402
2404
  : spec;
2403
2405
  const compiled = compileStatementNative(withReturning, this.#dialect);
2404
- const sql = this.#commentPrefix() + compiled.statements[0];
2406
+ const sql = this.#commentPrefix() + onlyStatement(compiled);
2405
2407
  return { sql, bindings: compiled.params, params: compiled.params };
2406
2408
  }
2407
2409
 
@@ -2593,7 +2595,7 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
2593
2595
  const rows = [Object.entries(data)];
2594
2596
  return new DmlBuilder(
2595
2597
  () =>
2596
- rows[0].length === 0
2598
+ (rows[0]?.length ?? 0) === 0
2597
2599
  ? Promise.resolve<Array<Record<string, unknown> | number>>([])
2598
2600
  : this.#runDml(
2599
2601
  this.#buildInsertOrUpsertSpec(rows),
@@ -2728,7 +2730,7 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
2728
2730
  );
2729
2731
  const result = await this.#raceTimeout(
2730
2732
  this.#exec.execute(
2731
- this.#commentPrefix() + compiled.statements[0],
2733
+ this.#commentPrefix() + onlyStatement(compiled),
2732
2734
  compiled.params,
2733
2735
  this.#queryMeta(op),
2734
2736
  ),
@@ -2773,7 +2775,7 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
2773
2775
  const countSql =
2774
2776
  this.#groupBys.length > 0
2775
2777
  ? `SELECT COUNT(*) AS aggregate FROM (${this.#compiledNative().sql}) AS __paginate_count`
2776
- : countCompiled.statements[0];
2778
+ : onlyStatement(countCompiled);
2777
2779
  const countParams =
2778
2780
  this.#groupBys.length > 0
2779
2781
  ? this.#compiledNative().params
@@ -2833,3 +2835,18 @@ function lastInsertIdOf(result: unknown): number | undefined {
2833
2835
  }
2834
2836
  return undefined;
2835
2837
  }
2838
+
2839
+ /**
2840
+ * The single statement a compile produced.
2841
+ *
2842
+ * `compileStatementNative` answers a list because a few specs expand to more
2843
+ * than one; the callers here compile specs that do not, and this is where that
2844
+ * is stated instead of reading index zero as a value that might not be there.
2845
+ */
2846
+ function onlyStatement(compiled: { statements: string[] }): string {
2847
+ const [statement] = compiled.statements;
2848
+ if (statement === undefined) {
2849
+ throw new Error("atlas: the query compiler produced no statement");
2850
+ }
2851
+ return statement;
2852
+ }
@@ -415,7 +415,7 @@ export class QueryBuilder<_T = Record<string, unknown>> {
415
415
  { kind: "select", ...desc },
416
416
  this.#dialect,
417
417
  );
418
- return { sql: compiled.statements[0], params: compiled.params };
418
+ return { sql: onlyStatement(compiled), params: compiled.params };
419
419
  }
420
420
 
421
421
  /** Get the table name. */
@@ -428,3 +428,18 @@ export class QueryBuilder<_T = Record<string, unknown>> {
428
428
  return [...this.#preload];
429
429
  }
430
430
  }
431
+
432
+ /**
433
+ * The single statement a compile produced.
434
+ *
435
+ * `compileStatementNative` answers a list because a few specs expand to more
436
+ * than one; the callers here compile specs that do not, and this is where that
437
+ * is stated instead of reading index zero as a value that might not be there.
438
+ */
439
+ function onlyStatement(compiled: { statements: string[] }): string {
440
+ const [statement] = compiled.statements;
441
+ if (statement === undefined) {
442
+ throw new Error("atlas: the query compiler produced no statement");
443
+ }
444
+ return statement;
445
+ }