@c9up/atlas 0.1.9 → 0.1.10
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.win32-x64-msvc.node +0 -0
- package/dist/AtlasProvider.d.ts.map +1 -1
- package/dist/AtlasProvider.js +9 -1
- package/dist/AtlasProvider.js.map +1 -1
- package/dist/BaseEntity.d.ts +3 -12
- package/dist/BaseEntity.d.ts.map +1 -1
- package/dist/BaseEntity.js +20 -4
- package/dist/BaseEntity.js.map +1 -1
- package/dist/BaseRepository.d.ts.map +1 -1
- package/dist/BaseRepository.js +37 -89
- package/dist/BaseRepository.js.map +1 -1
- package/dist/ModelQuery.d.ts +2 -2
- package/dist/ModelQuery.d.ts.map +1 -1
- package/dist/ModelQuery.js +62 -17
- package/dist/ModelQuery.js.map +1 -1
- package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
- package/dist/adapters/NapiDbAdapter.js +7 -5
- package/dist/adapters/NapiDbAdapter.js.map +1 -1
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +5 -6
- package/dist/configure.js.map +1 -1
- package/dist/decorators/entity.js +1 -1
- package/dist/decorators/entity.js.map +1 -1
- package/dist/metadata-keys.d.ts +20 -0
- package/dist/metadata-keys.d.ts.map +1 -0
- package/dist/metadata-keys.js +13 -0
- package/dist/metadata-keys.js.map +1 -0
- package/dist/query/native.d.ts +7 -0
- package/dist/query/native.d.ts.map +1 -1
- package/dist/query/native.js +10 -1
- package/dist/query/native.js.map +1 -1
- package/dist/schema/MigrationRunner.d.ts.map +1 -1
- package/dist/schema/MigrationRunner.js +5 -2
- package/dist/schema/MigrationRunner.js.map +1 -1
- package/dist/schema/Seeder.d.ts +13 -8
- package/dist/schema/Seeder.d.ts.map +1 -1
- package/dist/schema/Seeder.js +13 -8
- package/dist/schema/Seeder.js.map +1 -1
- package/dist/services/db.d.ts +7 -0
- package/dist/services/db.d.ts.map +1 -1
- package/dist/services/db.js +10 -0
- package/dist/services/db.js.map +1 -1
- package/dist/testing/Factory.d.ts +6 -7
- package/dist/testing/Factory.d.ts.map +1 -1
- package/dist/testing/Factory.js +3 -5
- package/dist/testing/Factory.js.map +1 -1
- package/dist/utils/safePath.d.ts +7 -1
- package/dist/utils/safePath.d.ts.map +1 -1
- package/dist/utils/safePath.js +24 -1
- package/dist/utils/safePath.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 +1 -1
- package/src/AtlasProvider.ts +9 -1
- package/src/BaseEntity.ts +27 -12
- package/src/BaseRepository.ts +38 -98
- package/src/ModelQuery.ts +66 -17
- package/src/adapters/NapiDbAdapter.ts +11 -8
- package/src/configure.ts +5 -6
- package/src/decorators/entity.ts +1 -1
- package/src/metadata-keys.ts +22 -0
- package/src/query/native.ts +12 -1
- package/src/schema/MigrationRunner.ts +5 -2
- package/src/schema/Seeder.ts +14 -12
- package/src/services/db.ts +10 -0
- package/src/testing/Factory.ts +9 -12
- package/src/utils/safePath.ts +27 -2
package/dist/schema/Seeder.d.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Seeder — populate the database with default / reference / test data.
|
|
3
3
|
*
|
|
4
|
-
* The canonical pattern:
|
|
4
|
+
* The canonical pattern (idempotent via `upsert`, keyed on a unique column):
|
|
5
5
|
*
|
|
6
6
|
* export default class CountrySeeder extends BaseSeeder {
|
|
7
7
|
* async run() {
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* const countries = new BaseRepository(Country, this.db)
|
|
9
|
+
* // Conflict on isoCode → update name. Safe to re-run.
|
|
10
|
+
* await countries.upsert(
|
|
11
|
+
* [
|
|
12
|
+
* { isoCode: 'FR', name: 'France' },
|
|
13
|
+
* { isoCode: 'IN', name: 'India' },
|
|
14
|
+
* ],
|
|
15
|
+
* ['isoCode'],
|
|
16
|
+
* ['name'],
|
|
17
|
+
* )
|
|
13
18
|
* }
|
|
14
19
|
* }
|
|
15
20
|
*
|
|
@@ -25,7 +30,7 @@ import type { DatabaseConnection } from "../BaseRepository.js";
|
|
|
25
30
|
export declare abstract class BaseSeeder {
|
|
26
31
|
protected db: DatabaseConnection;
|
|
27
32
|
constructor(db: DatabaseConnection);
|
|
28
|
-
/** The seeder body. Should be idempotent — `
|
|
33
|
+
/** The seeder body. Should be idempotent — `repo.upsert(...)` is the recommended pattern. */
|
|
29
34
|
abstract run(): Promise<void> | void;
|
|
30
35
|
}
|
|
31
36
|
/** Legacy alias kept for code using the previous API. */
|
|
@@ -35,7 +40,7 @@ export declare const Seeder: typeof BaseSeeder;
|
|
|
35
40
|
* sequentially so ordering is deterministic and side effects are visible to
|
|
36
41
|
* subsequent seeders.
|
|
37
42
|
*/
|
|
38
|
-
export declare function runSeeders(seeders: BaseSeeder[]
|
|
43
|
+
export declare function runSeeders(seeders: BaseSeeder[]): Promise<void>;
|
|
39
44
|
/**
|
|
40
45
|
* Discover seeder files under `dir`, import them in alphabetical order, build
|
|
41
46
|
* instances with the given DB connection, and run them sequentially.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Seeder.d.ts","sourceRoot":"","sources":["../../src/schema/Seeder.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Seeder.d.ts","sourceRoot":"","sources":["../../src/schema/Seeder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAQ/D;;;;;GAKG;AACH,8BAAsB,UAAU;IAC/B,SAAS,CAAC,EAAE,EAAE,kBAAkB,CAAC;gBAErB,EAAE,EAAE,kBAAkB;IAIlC,6FAA6F;IAC7F,QAAQ,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;CACpC;AAED,yDAAyD;AACzD,eAAO,MAAM,MAAM,mBAAa,CAAC;AAEjC;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIrE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACvC,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,kBAAkB,EACtB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACrC,OAAO,CAAC,MAAM,EAAE,CAAC,CAqDnB"}
|
package/dist/schema/Seeder.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Seeder — populate the database with default / reference / test data.
|
|
3
3
|
*
|
|
4
|
-
* The canonical pattern:
|
|
4
|
+
* The canonical pattern (idempotent via `upsert`, keyed on a unique column):
|
|
5
5
|
*
|
|
6
6
|
* export default class CountrySeeder extends BaseSeeder {
|
|
7
7
|
* async run() {
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* const countries = new BaseRepository(Country, this.db)
|
|
9
|
+
* // Conflict on isoCode → update name. Safe to re-run.
|
|
10
|
+
* await countries.upsert(
|
|
11
|
+
* [
|
|
12
|
+
* { isoCode: 'FR', name: 'France' },
|
|
13
|
+
* { isoCode: 'IN', name: 'India' },
|
|
14
|
+
* ],
|
|
15
|
+
* ['isoCode'],
|
|
16
|
+
* ['name'],
|
|
17
|
+
* )
|
|
13
18
|
* }
|
|
14
19
|
* }
|
|
15
20
|
*
|
|
@@ -38,7 +43,7 @@ export const Seeder = BaseSeeder;
|
|
|
38
43
|
* sequentially so ordering is deterministic and side effects are visible to
|
|
39
44
|
* subsequent seeders.
|
|
40
45
|
*/
|
|
41
|
-
export async function runSeeders(seeders
|
|
46
|
+
export async function runSeeders(seeders) {
|
|
42
47
|
for (const seeder of seeders) {
|
|
43
48
|
await seeder.run();
|
|
44
49
|
}
|
|
@@ -70,7 +75,7 @@ export async function runSeederDirectory(dir, db, options) {
|
|
|
70
75
|
const executed = [];
|
|
71
76
|
for (const file of selected) {
|
|
72
77
|
assertSafeName(file, "E_SEEDER_INVALID", "seeder");
|
|
73
|
-
const resolved = assertPathInsideBase(dir, file, "E_SEEDER_INVALID_PATH", "Seeder");
|
|
78
|
+
const resolved = await assertPathInsideBase(dir, file, "E_SEEDER_INVALID_PATH", "Seeder");
|
|
74
79
|
// `pathToFileURL` is required on Windows where bare `C:\…` paths
|
|
75
80
|
// trip ESM's ERR_UNSUPPORTED_ESM_URL_SCHEME on dynamic import.
|
|
76
81
|
const mod = await import(pathToFileURL(resolved).href);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Seeder.js","sourceRoot":"","sources":["../../src/schema/Seeder.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"Seeder.js","sourceRoot":"","sources":["../../src/schema/Seeder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,GAAG,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACN,oBAAoB,EACpB,cAAc,EACd,UAAU,GACV,MAAM,sBAAsB,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,OAAgB,UAAU;IACrB,EAAE,CAAqB;IAEjC,YAAY,EAAsB;QACjC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACd,CAAC;CAID;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,CAAC;AAEjC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAqB;IACrD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC9B,MAAM,MAAM,CAAC,GAAG,EAAE,CAAC;IACpB,CAAC;AACF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,GAAW,EACX,EAAsB,EACtB,OAAuC;IAEvC,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,UAAU,CACnB,wBAAwB,EACxB,+BAA+B,GAAG,EAAE,EACpC;YACC,IAAI,EAAE,uEAAuE;SAC7E,CACD,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;SACvC,MAAM,CACN,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CACvE;SACA,IAAI,EAAE,CAAC;IAET,MAAM,QAAQ,GAAG,OAAO,EAAE,KAAK;QAC9B,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACtB,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,CACpD;QACF,CAAC,CAAC,QAAQ,CAAC;IAEZ,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC7B,cAAc,CAAC,IAAI,EAAE,kBAAkB,EAAE,QAAQ,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAC1C,GAAG,EACH,IAAI,EACJ,uBAAuB,EACvB,QAAQ,CACR,CAAC;QAEF,iEAAiE;QACjE,+DAA+D;QAC/D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,WAAW,IAAI,OAAO,WAAW,KAAK,UAAU,EAAE,CAAC;YACvD,MAAM,IAAI,UAAU,CACnB,kBAAkB,EAClB,UAAU,IAAI,mDAAmD,EACjE;gBACC,IAAI,EAAE,uFAAuF;aAC7F,CACD,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAe,IAAI,WAAW,CAAC,EAAE,CAAC,CAAC;QACjD,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAC;QACrB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC"}
|
package/dist/services/db.d.ts
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
import type { AsyncDatabaseConnection } from "../adapters/NapiDbAdapter.js";
|
|
16
16
|
/** @internal Bind the singleton (called by AtlasProvider). */
|
|
17
17
|
export declare function setDb(connection: AsyncDatabaseConnection): void;
|
|
18
|
+
/**
|
|
19
|
+
* @internal Unbind the singleton IF it still points at `connection` (called by
|
|
20
|
+
* `AtlasProvider.shutdown()`). Ownership-guarded: when a second provider rebound
|
|
21
|
+
* the singleton, the older provider's shutdown must not clear the newer binding.
|
|
22
|
+
* Without this, `db.*` after shutdown would dereference a closed connection.
|
|
23
|
+
*/
|
|
24
|
+
export declare function clearDb(connection: AsyncDatabaseConnection): void;
|
|
18
25
|
/** @internal Read the singleton (or `undefined` pre-boot). */
|
|
19
26
|
export declare function getDb(): AsyncDatabaseConnection | undefined;
|
|
20
27
|
declare const db: AsyncDatabaseConnection;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAI5E,8DAA8D;AAC9D,wBAAgB,KAAK,CAAC,UAAU,EAAE,uBAAuB,GAAG,IAAI,CAE/D;AAED,8DAA8D;AAC9D,wBAAgB,KAAK,IAAI,uBAAuB,GAAG,SAAS,CAE3D;AAED,QAAA,MAAM,EAAE,EAAE,uBAaR,CAAC;AAEH,eAAe,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAI5E,8DAA8D;AAC9D,wBAAgB,KAAK,CAAC,UAAU,EAAE,uBAAuB,GAAG,IAAI,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,uBAAuB,GAAG,IAAI,CAEjE;AAED,8DAA8D;AAC9D,wBAAgB,KAAK,IAAI,uBAAuB,GAAG,SAAS,CAE3D;AAED,QAAA,MAAM,EAAE,EAAE,uBAaR,CAAC;AAEH,eAAe,EAAE,CAAC"}
|
package/dist/services/db.js
CHANGED
|
@@ -17,6 +17,16 @@ let instance;
|
|
|
17
17
|
export function setDb(connection) {
|
|
18
18
|
instance = connection;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* @internal Unbind the singleton IF it still points at `connection` (called by
|
|
22
|
+
* `AtlasProvider.shutdown()`). Ownership-guarded: when a second provider rebound
|
|
23
|
+
* the singleton, the older provider's shutdown must not clear the newer binding.
|
|
24
|
+
* Without this, `db.*` after shutdown would dereference a closed connection.
|
|
25
|
+
*/
|
|
26
|
+
export function clearDb(connection) {
|
|
27
|
+
if (instance === connection)
|
|
28
|
+
instance = undefined;
|
|
29
|
+
}
|
|
20
30
|
/** @internal Read the singleton (or `undefined` pre-boot). */
|
|
21
31
|
export function getDb() {
|
|
22
32
|
return instance;
|
package/dist/services/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,IAAI,QAA6C,CAAC;AAElD,8DAA8D;AAC9D,MAAM,UAAU,KAAK,CAAC,UAAmC;IACxD,QAAQ,GAAG,UAAU,CAAC;AACvB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,KAAK;IACpB,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED,MAAM,EAAE,GAA4B,IAAI,KAAK,CAAC,EAA6B,EAAE;IAC5E,GAAG,CAAC,OAAO,EAAE,IAAI;QAChB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACd,iEAAiE;gBAChE,gEAAgE;gBAChE,+DAA+D;gBAC/D,aAAa,CACd,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QACpD,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACnE,CAAC;CACD,CAAC,CAAC;AAEH,eAAe,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,IAAI,QAA6C,CAAC;AAElD,8DAA8D;AAC9D,MAAM,UAAU,KAAK,CAAC,UAAmC;IACxD,QAAQ,GAAG,UAAU,CAAC;AACvB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,OAAO,CAAC,UAAmC;IAC1D,IAAI,QAAQ,KAAK,UAAU;QAAE,QAAQ,GAAG,SAAS,CAAC;AACnD,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,KAAK;IACpB,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED,MAAM,EAAE,GAA4B,IAAI,KAAK,CAAC,EAA6B,EAAE;IAC5E,GAAG,CAAC,OAAO,EAAE,IAAI;QAChB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACd,iEAAiE;gBAChE,gEAAgE;gBAChE,+DAA+D;gBAC/D,aAAa,CACd,CAAC;QACH,CAAC;QACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QACpD,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACnE,CAAC;CACD,CAAC,CAAC;AAEH,eAAe,EAAE,CAAC"}
|
|
@@ -21,15 +21,14 @@ type StateFn<D> = (data: D) => void;
|
|
|
21
21
|
export interface FactoryBuilder<T extends BaseEntity> {
|
|
22
22
|
/** Override specific fields for the next call (reset after consumption). */
|
|
23
23
|
merge(overrides: Partial<Record<string, unknown>>): FactoryBuilder<T>;
|
|
24
|
-
/**
|
|
25
|
-
* Declare a named variation of this factory. States are stored on the
|
|
26
|
-
* factory itself and don't mutate the caller — `apply()` returns a child
|
|
27
|
-
* builder with the state active.
|
|
28
|
-
*/
|
|
24
|
+
/** Declare a named variation of this factory, stored on the factory's state map. */
|
|
29
25
|
state(name: string, fn: StateFn<Record<string, unknown>>): FactoryBuilder<T>;
|
|
30
26
|
/**
|
|
31
|
-
* Activate one or more declared states for the
|
|
32
|
-
* compose (all
|
|
27
|
+
* Activate one or more declared states for the NEXT build. Multiple applies
|
|
28
|
+
* compose (all fire, in order). NOTE: this mutates the builder's shared
|
|
29
|
+
* pending state and returns the SAME builder for chaining — there is no
|
|
30
|
+
* isolated child builder. The pending set is reset after each make/create
|
|
31
|
+
* (audit 2026-06-13). `merge()` behaves the same way.
|
|
33
32
|
*/
|
|
34
33
|
apply(...stateNames: string[]): FactoryBuilder<T>;
|
|
35
34
|
/** Create and persist a single entity (fires lifecycle hooks via `repo.create`). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Factory.d.ts","sourceRoot":"","sources":["../../src/testing/Factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/E,KAAK,iBAAiB,CAAC,CAAC,SAAS,UAAU,IAAI,UAAU,CAAC,CAAC;AAE3D,mEAAmE;AACnE,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAEpC,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,UAAU;IACnD,4EAA4E;IAC5E,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;IAEtE
|
|
1
|
+
{"version":3,"file":"Factory.d.ts","sourceRoot":"","sources":["../../src/testing/Factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/E,KAAK,iBAAiB,CAAC,CAAC,SAAS,UAAU,IAAI,UAAU,CAAC,CAAC;AAE3D,mEAAmE;AACnE,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAEpC,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,UAAU;IACnD,4EAA4E;IAC5E,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;IAEtE,oFAAoF;IACpF,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;IAE7E;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,UAAU,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;IAElD,oFAAoF;IACpF,MAAM,CAAC,EAAE,EAAE,kBAAkB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAE3C,kEAAkE;IAClE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,kBAAkB,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAEhE,oFAAoF;IACpF,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEhC,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAEnD;;;OAGG;IACH,WAAW,IAAI,CAAC,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,UAAU,EAC3C,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC,EACjC,QAAQ,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrC,cAAc,CAAC,CAAC,CAAC,CA0FnB"}
|
package/dist/testing/Factory.js
CHANGED
|
@@ -64,13 +64,11 @@ export function factory(entityClass, defaults) {
|
|
|
64
64
|
return data;
|
|
65
65
|
},
|
|
66
66
|
makeMany(count) {
|
|
67
|
-
// Re-evaluate defaults for each row so `Date.now()` / faker generate
|
|
67
|
+
// Re-evaluate defaults for each row so `Date.now()` / faker generate
|
|
68
|
+
// distinct values. `buildData()` reads (never mutates) the pending
|
|
69
|
+
// overrides/states, so they stay stable across iterations on their own.
|
|
68
70
|
const rows = [];
|
|
69
|
-
const capturedOverrides = pendingOverrides;
|
|
70
|
-
const capturedStates = pendingStates;
|
|
71
71
|
for (let i = 0; i < count; i++) {
|
|
72
|
-
pendingOverrides = capturedOverrides;
|
|
73
|
-
pendingStates = capturedStates;
|
|
74
72
|
rows.push(buildData());
|
|
75
73
|
}
|
|
76
74
|
resetPending();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Factory.js","sourceRoot":"","sources":["../../src/testing/Factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAE,cAAc,EAA2B,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"Factory.js","sourceRoot":"","sources":["../../src/testing/Factory.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/safePath.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ export declare function assertSafeName(name: string, errorCode: string, kind: st
|
|
|
17
17
|
* Resolve `fileName` inside `baseDir` and throw if the resulting path escapes
|
|
18
18
|
* the base — guards against symlink / `../` traversal attacks when loading
|
|
19
19
|
* migration or seeder files dynamically.
|
|
20
|
+
*
|
|
21
|
+
* Two layers: (1) a logical `path.resolve` check that catches `../` walks, and
|
|
22
|
+
* (2) a `realpath` check that follows symlinks — a symlink FILE sitting inside
|
|
23
|
+
* the base but pointing at `/etc/passwd` passes the logical check yet is caught
|
|
24
|
+
* here. When the file doesn't exist yet (ENOENT), only the logical check
|
|
25
|
+
* applies and the caller's own existence check produces the not-found error.
|
|
20
26
|
*/
|
|
21
|
-
export declare function assertPathInsideBase(baseDir: string, fileName: string, errorCode: string, kind: string): string
|
|
27
|
+
export declare function assertPathInsideBase(baseDir: string, fileName: string, errorCode: string, kind: string): Promise<string>;
|
|
22
28
|
//# sourceMappingURL=safePath.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safePath.d.ts","sourceRoot":"","sources":["../../src/utils/safePath.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,uGAAuG;AACvG,wBAAsB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ5D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC7B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACV,IAAI,CAIN;AAED
|
|
1
|
+
{"version":3,"file":"safePath.d.ts","sourceRoot":"","sources":["../../src/utils/safePath.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,uGAAuG;AACvG,wBAAsB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ5D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC7B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACV,IAAI,CAIN;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CA6BjB"}
|
package/dist/utils/safePath.js
CHANGED
|
@@ -34,13 +34,36 @@ export function assertSafeName(name, errorCode, kind) {
|
|
|
34
34
|
* Resolve `fileName` inside `baseDir` and throw if the resulting path escapes
|
|
35
35
|
* the base — guards against symlink / `../` traversal attacks when loading
|
|
36
36
|
* migration or seeder files dynamically.
|
|
37
|
+
*
|
|
38
|
+
* Two layers: (1) a logical `path.resolve` check that catches `../` walks, and
|
|
39
|
+
* (2) a `realpath` check that follows symlinks — a symlink FILE sitting inside
|
|
40
|
+
* the base but pointing at `/etc/passwd` passes the logical check yet is caught
|
|
41
|
+
* here. When the file doesn't exist yet (ENOENT), only the logical check
|
|
42
|
+
* applies and the caller's own existence check produces the not-found error.
|
|
37
43
|
*/
|
|
38
|
-
export function assertPathInsideBase(baseDir, fileName, errorCode, kind) {
|
|
44
|
+
export async function assertPathInsideBase(baseDir, fileName, errorCode, kind) {
|
|
39
45
|
const resolved = path.resolve(baseDir, fileName);
|
|
40
46
|
const base = path.resolve(baseDir);
|
|
41
47
|
if (!resolved.startsWith(base + path.sep) && resolved !== base) {
|
|
42
48
|
throw new AtlasError(errorCode, `${kind} path escapes directory: ${fileName}`);
|
|
43
49
|
}
|
|
50
|
+
// Symlink-aware check: resolve real targets and re-verify containment.
|
|
51
|
+
try {
|
|
52
|
+
const realBase = await fsp.realpath(base);
|
|
53
|
+
const realResolved = await fsp.realpath(resolved);
|
|
54
|
+
if (!realResolved.startsWith(realBase + path.sep) &&
|
|
55
|
+
realResolved !== realBase) {
|
|
56
|
+
throw new AtlasError(errorCode, `${kind} path escapes directory via symlink: ${fileName}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// File not yet created — the logical check above stands; let the caller's
|
|
61
|
+
// existence check report the missing file. Re-throw real traversal errors.
|
|
62
|
+
if (err instanceof AtlasError)
|
|
63
|
+
throw err;
|
|
64
|
+
if (err.code !== "ENOENT")
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
44
67
|
return resolved;
|
|
45
68
|
}
|
|
46
69
|
//# sourceMappingURL=safePath.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safePath.js","sourceRoot":"","sources":["../../src/utils/safePath.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,GAAG,MAAM,kBAAkB,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,CAAS;IACzC,IAAI,CAAC;QACJ,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACnE,MAAM,GAAG,CAAC;IACX,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC7B,IAAY,EACZ,SAAiB,EACjB,IAAY;IAEZ,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,UAAU,CAAC,SAAS,EAAE,WAAW,IAAI,UAAU,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;AACF,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"safePath.js","sourceRoot":"","sources":["../../src/utils/safePath.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,GAAG,MAAM,kBAAkB,CAAC;AACxC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,CAAS;IACzC,IAAI,CAAC;QACJ,MAAM,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACnE,MAAM,GAAG,CAAC;IACX,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC7B,IAAY,EACZ,SAAiB,EACjB,IAAY;IAEZ,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,UAAU,CAAC,SAAS,EAAE,WAAW,IAAI,UAAU,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;AACF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,OAAe,EACf,QAAgB,EAChB,SAAiB,EACjB,IAAY;IAEZ,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QAChE,MAAM,IAAI,UAAU,CACnB,SAAS,EACT,GAAG,IAAI,4BAA4B,QAAQ,EAAE,CAC7C,CAAC;IACH,CAAC;IACD,uEAAuE;IACvE,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC1C,MAAM,YAAY,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAClD,IACC,CAAC,YAAY,CAAC,UAAU,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC;YAC7C,YAAY,KAAK,QAAQ,EACxB,CAAC;YACF,MAAM,IAAI,UAAU,CACnB,SAAS,EACT,GAAG,IAAI,wCAAwC,QAAQ,EAAE,CACzD,CAAC;QACH,CAAC;IACF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,0EAA0E;QAC1E,2EAA2E;QAC3E,IAAI,GAAG,YAAY,UAAU;YAAE,MAAM,GAAG,CAAC;QACzC,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,GAAG,CAAC;IACjE,CAAC;IACD,OAAO,QAAQ,CAAC;AACjB,CAAC"}
|
package/index.darwin-arm64.node
CHANGED
|
Binary file
|
package/index.darwin-x64.node
CHANGED
|
Binary file
|
|
Binary file
|
package/index.linux-x64-gnu.node
CHANGED
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/AtlasProvider.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type AsyncDatabaseConnection,
|
|
26
26
|
createNapiConnection,
|
|
27
27
|
} from "./adapters/NapiDbAdapter.js";
|
|
28
|
-
import { setAtlasDialect } from "./query/native.js";
|
|
28
|
+
import { clearCastRegistry, setAtlasDialect } from "./query/native.js";
|
|
29
29
|
import {
|
|
30
30
|
type DatabaseAdapter,
|
|
31
31
|
MigrationRunner,
|
|
@@ -221,6 +221,14 @@ export default class AtlasProvider {
|
|
|
221
221
|
const named = [...this.#connections.entries()];
|
|
222
222
|
const results = await Promise.allSettled(named.map(([, c]) => c.close()));
|
|
223
223
|
this.#connections.clear();
|
|
224
|
+
|
|
225
|
+
// Release the module-level singletons this provider populated at boot so a
|
|
226
|
+
// re-boot starts clean and `db.*` can't dereference a now-closed handle.
|
|
227
|
+
// `clearDb` is ownership-guarded, so clearing every connection only unbinds
|
|
228
|
+
// the one still owning the singleton.
|
|
229
|
+
const { clearDb } = await import("./services/db.js");
|
|
230
|
+
for (const [, conn] of named) clearDb(conn);
|
|
231
|
+
clearCastRegistry();
|
|
224
232
|
const errors = results
|
|
225
233
|
.map((r, i) =>
|
|
226
234
|
r.status === "rejected" ? { name: named[i][0], error: r.reason } : null,
|
package/src/BaseEntity.ts
CHANGED
|
@@ -11,18 +11,23 @@
|
|
|
11
11
|
* @implements FR29, FR35, stories 32.1 through 32.5
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { getRelationMetadata } from "./decorators/entity.js";
|
|
14
15
|
import { MassAssignmentError } from "./errors.js";
|
|
16
|
+
import {
|
|
17
|
+
COLUMN_SERIALIZE_KEY,
|
|
18
|
+
COMPUTED_KEY,
|
|
19
|
+
type ColumnSerializeConfig,
|
|
20
|
+
} from "./metadata-keys.js";
|
|
15
21
|
|
|
16
22
|
export interface DomainEvent {
|
|
17
23
|
name: string;
|
|
18
24
|
data: Record<string, unknown>;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export const COLUMN_SERIALIZE_KEY = Symbol.for("atlas:columnSerialize");
|
|
27
|
+
// COMPUTED_KEY / COLUMN_SERIALIZE_KEY / ColumnSerializeConfig moved to
|
|
28
|
+
// ./metadata-keys.js to break the BaseEntity ↔ entity-decorator runtime cycle
|
|
29
|
+
// (fallow 2026-06-14). Re-exported here for backward compatibility.
|
|
30
|
+
export { COLUMN_SERIALIZE_KEY, COMPUTED_KEY };
|
|
26
31
|
|
|
27
32
|
/** Symbol property key used by entities to back-reference their hydrating repo. */
|
|
28
33
|
export const REPO_REF = Symbol.for("atlas:repoRef");
|
|
@@ -115,13 +120,7 @@ export type RelationProxy =
|
|
|
115
120
|
| BelongsToRelationProxy
|
|
116
121
|
| ManyToManyRelationProxy;
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
export interface ColumnSerializeConfig {
|
|
120
|
-
/** Rename this column at toJSON time (e.g. `password` → `passwordHash`). Null = hidden. */
|
|
121
|
-
serializeAs?: string | null;
|
|
122
|
-
/** Transform function applied to the value at toJSON time. */
|
|
123
|
-
serialize?: (value: unknown) => unknown;
|
|
124
|
-
}
|
|
123
|
+
export type { ColumnSerializeConfig };
|
|
125
124
|
|
|
126
125
|
/**
|
|
127
126
|
* Internal reserved keys on BaseEntity that must never be treated as database
|
|
@@ -495,6 +494,13 @@ export class BaseEntity {
|
|
|
495
494
|
ctor.visible && ctor.visible.length > 0 ? new Set(ctor.visible) : null;
|
|
496
495
|
|
|
497
496
|
const serializeConfig = getColumnSerializeConfig(ctor);
|
|
497
|
+
// Relation `serializeAs` overrides — a preloaded relation is a plain
|
|
498
|
+
// property on the instance, so it shows up in `Object.keys` below. Without
|
|
499
|
+
// this map the `serializeAs` declared on @HasOne/@HasMany/etc. was silently
|
|
500
|
+
// ignored (the relation always serialized under its property name).
|
|
501
|
+
const relByKey = new Map(
|
|
502
|
+
getRelationMetadata(ctor).map((r) => [r.propertyKey, r]),
|
|
503
|
+
);
|
|
498
504
|
const result: Record<string, unknown> = {};
|
|
499
505
|
|
|
500
506
|
// Regular columns (respecting hidden/visible + serialize overrides)
|
|
@@ -503,6 +509,15 @@ export class BaseEntity {
|
|
|
503
509
|
if (visible && !visible.has(key)) continue;
|
|
504
510
|
if (hidden.has(key)) continue;
|
|
505
511
|
|
|
512
|
+
// Preloaded relation: honour its `serializeAs` (rename, or `null` hides
|
|
513
|
+
// it from the JSON). The nested entity serializes via its own toJSON.
|
|
514
|
+
const rel = relByKey.get(key);
|
|
515
|
+
if (rel) {
|
|
516
|
+
if (rel.serializeAs === null) continue;
|
|
517
|
+
result[rel.serializeAs ?? key] = this[key];
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
506
521
|
const cfg = serializeConfig[key];
|
|
507
522
|
if (cfg?.serializeAs === null) continue; // explicit hide
|
|
508
523
|
|
package/src/BaseRepository.ts
CHANGED
|
@@ -351,15 +351,10 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
351
351
|
// ─── Finders ──────────────────────────────────────────────
|
|
352
352
|
|
|
353
353
|
async find(id: string | number | bigint): Promise<T | null> {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
this.#
|
|
358
|
-
const { sql, params } = this.#compileSelect({ wheres, limit: 1 });
|
|
359
|
-
const rows = await this.#db.query<Row>(sql, params);
|
|
360
|
-
const row = rows[0];
|
|
361
|
-
if (!row) return null;
|
|
362
|
-
return this.#hydrate(row);
|
|
354
|
+
// Route through the query builder so the read hooks (beforeFind/afterFind)
|
|
355
|
+
// fire and a `beforeFind` hook can mutate the query — the previous direct
|
|
356
|
+
// `#compileSelect` fast path bypassed every read hook silently.
|
|
357
|
+
return this.query().where(this.#primaryKey, id).first();
|
|
363
358
|
}
|
|
364
359
|
|
|
365
360
|
async findOrFail(id: string | number): Promise<T> {
|
|
@@ -373,60 +368,34 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
373
368
|
}
|
|
374
369
|
|
|
375
370
|
async findBy(column: string, value: unknown): Promise<T | null> {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
{ column: col, operator: "=", value, type: "and" },
|
|
379
|
-
];
|
|
380
|
-
this.#appendSoftScope(wheres);
|
|
381
|
-
const { sql, params } = this.#compileSelect({ wheres, limit: 1 });
|
|
382
|
-
const rows = await this.#db.query<Row>(sql, params);
|
|
383
|
-
const row = rows[0];
|
|
384
|
-
if (!row) return null;
|
|
385
|
-
return this.#hydrate(row);
|
|
371
|
+
// Through the builder for read-hook parity (see `find`).
|
|
372
|
+
return this.query().where(column, value).first();
|
|
386
373
|
}
|
|
387
374
|
|
|
388
375
|
async all(): Promise<T[]> {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return this
|
|
376
|
+
// Through the builder so beforeFetch/afterFetch fire. The builder applies
|
|
377
|
+
// the soft-delete scope by default, exactly like the old fast path.
|
|
378
|
+
return this.query().exec();
|
|
392
379
|
}
|
|
393
380
|
|
|
394
381
|
async allWithTrashed(): Promise<T[]> {
|
|
395
|
-
return this
|
|
382
|
+
return this.query().withTrashed().exec();
|
|
396
383
|
}
|
|
397
384
|
|
|
398
385
|
async onlyTrashed(): Promise<T[]> {
|
|
399
386
|
if (!this.#softDeletes) return [];
|
|
400
|
-
return this
|
|
401
|
-
wheres: [
|
|
402
|
-
{
|
|
403
|
-
column: "deleted_at",
|
|
404
|
-
operator: "IS NOT NULL",
|
|
405
|
-
value: null,
|
|
406
|
-
type: "and",
|
|
407
|
-
},
|
|
408
|
-
],
|
|
409
|
-
});
|
|
387
|
+
return this.query().onlyTrashed().exec();
|
|
410
388
|
}
|
|
411
389
|
|
|
412
390
|
async where(column: string, value: unknown): Promise<T[]> {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
this
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// blew up on Postgres/MySQL the moment the app ran against a real driver.
|
|
422
|
-
// Using the PK works on every dialect and matches the user's actual
|
|
423
|
-
// schema — the ordering contract is "most recent first by PK" for
|
|
424
|
-
// `repo.where(col, val)` as a convenience finder.
|
|
425
|
-
const pkCol = camelToSnake(this.#primaryKey);
|
|
426
|
-
return this.#runSelect({
|
|
427
|
-
wheres,
|
|
428
|
-
orderBy: [{ column: pkCol, direction: "desc" }],
|
|
429
|
-
});
|
|
391
|
+
// Order by the primary key (DESC = most recent insert first when the PK is
|
|
392
|
+
// an auto-increment integer or a monotonic UUID). The ordering contract is
|
|
393
|
+
// "most recent first by PK" for `repo.where(col, val)` as a convenience
|
|
394
|
+
// finder. Through the builder for read-hook parity (see `find`).
|
|
395
|
+
return this.query()
|
|
396
|
+
.where(column, value)
|
|
397
|
+
.orderBy(this.#primaryKey, "desc")
|
|
398
|
+
.exec();
|
|
430
399
|
}
|
|
431
400
|
|
|
432
401
|
// ─── Create / Save / Delete ───────────────────────────────
|
|
@@ -450,6 +419,7 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
450
419
|
await this.#insert(entity);
|
|
451
420
|
await fireHooks(this.#entityClass, "afterCreate", entity);
|
|
452
421
|
await fireHooks(this.#entityClass, "afterSave", entity);
|
|
422
|
+
await this.#dispatchDomainEvents(entity);
|
|
453
423
|
return entity;
|
|
454
424
|
}
|
|
455
425
|
|
|
@@ -494,6 +464,17 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
494
464
|
}
|
|
495
465
|
await fireHooks(this.#entityClass, "afterSave", entity);
|
|
496
466
|
|
|
467
|
+
await this.#dispatchDomainEvents(entity);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Flush the entity's accumulated domain events through `onDomainEvents`.
|
|
472
|
+
* On dispatch failure the events are re-queued on the entity and the error
|
|
473
|
+
* propagates, so a caller can retry without losing them. Shared by `save`,
|
|
474
|
+
* `create` and `createMany` — every persistence path that produces a live
|
|
475
|
+
* entity must dispatch, otherwise events silently vanish on batch inserts.
|
|
476
|
+
*/
|
|
477
|
+
async #dispatchDomainEvents(entity: BaseEntity): Promise<void> {
|
|
497
478
|
const events = entity.flushDomainEvents();
|
|
498
479
|
if (events.length > 0 && this.onDomainEvents) {
|
|
499
480
|
try {
|
|
@@ -576,6 +557,9 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
576
557
|
await fireHooks(this.#entityClass, "afterCreate", e);
|
|
577
558
|
await fireHooks(this.#entityClass, "afterSave", e);
|
|
578
559
|
}
|
|
560
|
+
for (const e of entities) {
|
|
561
|
+
await this.#dispatchDomainEvents(e);
|
|
562
|
+
}
|
|
579
563
|
return entities;
|
|
580
564
|
}
|
|
581
565
|
|
|
@@ -908,43 +892,6 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
908
892
|
|
|
909
893
|
// ─── Private helpers ──────────────────────────────────────
|
|
910
894
|
|
|
911
|
-
#compileSelect(opts: {
|
|
912
|
-
wheres?: Array<Record<string, unknown>>;
|
|
913
|
-
orderBy?: Array<Record<string, unknown>>;
|
|
914
|
-
limit?: number;
|
|
915
|
-
}): { sql: string; params: unknown[] } {
|
|
916
|
-
const spec = {
|
|
917
|
-
kind: "select",
|
|
918
|
-
table: this.#tableName,
|
|
919
|
-
select: ["*"],
|
|
920
|
-
wheres: opts.wheres ?? [],
|
|
921
|
-
orderBy: opts.orderBy ?? [],
|
|
922
|
-
groupBy: [],
|
|
923
|
-
having: [],
|
|
924
|
-
limit: opts.limit ?? null,
|
|
925
|
-
offset: null,
|
|
926
|
-
distinct: false,
|
|
927
|
-
ctes: [],
|
|
928
|
-
unions: [],
|
|
929
|
-
// Cast WHERE params on native-typed columns (uuid/timestamp/…) so a
|
|
930
|
-
// `WHERE id = $1` on a uuid PK emits `$1::uuid` — otherwise Postgres
|
|
931
|
-
// rejects it with `operator does not exist: uuid = text`.
|
|
932
|
-
casts: this.#castTypes,
|
|
933
|
-
};
|
|
934
|
-
const compiled = compileStatementNative(spec, this.#dialect);
|
|
935
|
-
return { sql: compiled.statements[0], params: compiled.params };
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
async #runSelect(opts: {
|
|
939
|
-
wheres?: Array<Record<string, unknown>>;
|
|
940
|
-
orderBy?: Array<Record<string, unknown>>;
|
|
941
|
-
limit?: number;
|
|
942
|
-
}): Promise<T[]> {
|
|
943
|
-
const { sql, params } = this.#compileSelect(opts);
|
|
944
|
-
const rows = await this.#db.query<Row>(sql, params);
|
|
945
|
-
return rows.map((r) => this.#hydrate(r));
|
|
946
|
-
}
|
|
947
|
-
|
|
948
895
|
async #runDelete(wheres: Array<Record<string, unknown>>): Promise<void> {
|
|
949
896
|
const compiled = compileStatementNative(
|
|
950
897
|
{ kind: "delete", table: this.#tableName, wheres },
|
|
@@ -1023,17 +970,6 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
1023
970
|
return {};
|
|
1024
971
|
}
|
|
1025
972
|
|
|
1026
|
-
#appendSoftScope(wheres: Array<Record<string, unknown>>): void {
|
|
1027
|
-
if (this.#softDeletes) {
|
|
1028
|
-
wheres.push({
|
|
1029
|
-
column: "deleted_at",
|
|
1030
|
-
operator: "IS NULL",
|
|
1031
|
-
value: null,
|
|
1032
|
-
type: "and",
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
973
|
async #insert(entity: T): Promise<void> {
|
|
1038
974
|
// Auto-generate the PK when declared via `@PrimaryKey({ generated })`.
|
|
1039
975
|
this.#applyPrimaryKeyGenerator(entity);
|
|
@@ -1314,6 +1250,10 @@ export class BaseRepository<T extends BaseEntity> {
|
|
|
1314
1250
|
const relatedRepo = new BaseRepository<BaseEntity>(relatedClass, this.#db, {
|
|
1315
1251
|
dialect: this.#dialect,
|
|
1316
1252
|
});
|
|
1253
|
+
// Propagate the domain-event sink so entities persisted through a relation
|
|
1254
|
+
// proxy (user.related('posts').create(...)) dispatch their events too —
|
|
1255
|
+
// otherwise the related entity's events silently vanish.
|
|
1256
|
+
relatedRepo.onDomainEvents = this.onDomainEvents;
|
|
1317
1257
|
const db = this.#db;
|
|
1318
1258
|
|
|
1319
1259
|
// FK column naming: belongsTo stores the FK on THIS side; has* / m2m on the OTHER side.
|