@c9up/atlas 0.3.11 → 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.
- package/db.darwin-arm64.node +0 -0
- package/db.darwin-x64.node +0 -0
- package/db.linux-arm64-gnu.node +0 -0
- package/db.linux-x64-gnu.node +0 -0
- package/db.win32-x64-msvc.node +0 -0
- package/dist/AtlasProvider.d.ts +1 -0
- package/dist/AtlasProvider.d.ts.map +1 -1
- package/dist/AtlasProvider.js +52 -13
- package/dist/AtlasProvider.js.map +1 -1
- package/dist/BaseRepository.d.ts.map +1 -1
- package/dist/BaseRepository.js +30 -12
- package/dist/BaseRepository.js.map +1 -1
- package/dist/ModelQuery.d.ts.map +1 -1
- package/dist/ModelQuery.js +34 -13
- package/dist/ModelQuery.js.map +1 -1
- package/dist/augmentations.d.ts +44 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +19 -0
- package/dist/augmentations.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -1
- package/dist/query/DatabaseQueryBuilder.js +24 -8
- package/dist/query/DatabaseQueryBuilder.js.map +1 -1
- package/dist/query/QueryBuilder.js +15 -1
- package/dist/query/QueryBuilder.js.map +1 -1
- package/dist/schema/MigrationRunner.d.ts.map +1 -1
- package/dist/schema/MigrationRunner.js +112 -7
- package/dist/schema/MigrationRunner.js.map +1 -1
- package/dist/schema/SchemaCheck.d.ts.map +1 -1
- package/dist/schema/SchemaCheck.js +19 -7
- package/dist/schema/SchemaCheck.js.map +1 -1
- package/dist/testing/DatabaseCleanup.js +15 -1
- package/dist/testing/DatabaseCleanup.js.map +1 -1
- package/dist/testing/Factory.js +2 -2
- package/dist/testing/Factory.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +10 -5
- package/scripts/build-napi-types.mjs +4 -3
- package/scripts/generate-napi-types.mjs +9 -6
- package/src/AtlasProvider.ts +65 -18
- package/src/BaseRepository.ts +30 -12
- package/src/ModelQuery.ts +37 -11
- package/src/augmentations.ts +49 -0
- package/src/index.ts +1 -0
- package/src/query/DatabaseQueryBuilder.ts +25 -8
- package/src/query/QueryBuilder.ts +16 -1
- package/src/schema/MigrationRunner.ts +124 -11
- package/src/schema/SchemaCheck.ts +27 -10
- package/src/testing/DatabaseCleanup.ts +16 -1
- 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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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))
|
package/src/AtlasProvider.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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) =>
|
|
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
|
|
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
|
|
349
|
+
forward(emitter, "db:connection:connect", node);
|
|
317
350
|
const onDisconnect = (node: unknown) =>
|
|
318
|
-
emitter
|
|
351
|
+
forward(emitter, "db:connection:disconnect", node);
|
|
319
352
|
const onError = (node: unknown, err?: unknown) =>
|
|
320
|
-
emitter
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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"
|
|
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) {
|
package/src/BaseRepository.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1074
|
+
entity.setProp(prop, this.#applyConsume(prop, v, entity));
|
|
1072
1075
|
}
|
|
1073
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
4742
|
+
for (const [i, col] of cols.entries()) {
|
|
4734
4743
|
q.orWhere((inner) => {
|
|
4735
|
-
for (
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|
2239
|
-
return
|
|
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
|
|
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]
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|