@c9up/atlas 0.1.3 → 0.1.5

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 (150) 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 +106 -0
  7. package/dist/AtlasProvider.d.ts.map +1 -0
  8. package/dist/AtlasProvider.js +190 -0
  9. package/dist/AtlasProvider.js.map +1 -0
  10. package/dist/BaseEntity.d.ts +255 -0
  11. package/dist/BaseEntity.d.ts.map +1 -0
  12. package/dist/BaseEntity.js +420 -0
  13. package/dist/BaseEntity.js.map +1 -0
  14. package/dist/BaseRepository.d.ts +175 -0
  15. package/dist/BaseRepository.d.ts.map +1 -0
  16. package/dist/BaseRepository.js +1451 -0
  17. package/dist/BaseRepository.js.map +1 -0
  18. package/dist/ModelQuery.d.ts +338 -0
  19. package/dist/ModelQuery.d.ts.map +1 -0
  20. package/dist/ModelQuery.js +1598 -0
  21. package/dist/ModelQuery.js.map +1 -0
  22. package/dist/Transaction.d.ts +15 -0
  23. package/dist/Transaction.d.ts.map +1 -0
  24. package/dist/Transaction.js +67 -0
  25. package/dist/Transaction.js.map +1 -0
  26. package/dist/adapters/NapiDbAdapter.d.ts +34 -0
  27. package/dist/adapters/NapiDbAdapter.d.ts.map +1 -0
  28. package/dist/adapters/NapiDbAdapter.js +96 -0
  29. package/dist/adapters/NapiDbAdapter.js.map +1 -0
  30. package/dist/config.d.ts +4 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +4 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/configure.d.ts +10 -0
  35. package/dist/configure.d.ts.map +1 -0
  36. package/dist/configure.js +25 -0
  37. package/dist/configure.js.map +1 -0
  38. package/dist/decorators/entity.d.ts +235 -0
  39. package/dist/decorators/entity.d.ts.map +1 -0
  40. package/dist/decorators/entity.js +264 -0
  41. package/dist/decorators/entity.js.map +1 -0
  42. package/dist/decorators/hooks.d.ts +70 -0
  43. package/dist/decorators/hooks.d.ts.map +1 -0
  44. package/dist/decorators/hooks.js +107 -0
  45. package/dist/decorators/hooks.js.map +1 -0
  46. package/dist/decorators/scope.d.ts +35 -0
  47. package/dist/decorators/scope.d.ts.map +1 -0
  48. package/dist/decorators/scope.js +29 -0
  49. package/dist/decorators/scope.js.map +1 -0
  50. package/dist/errors.d.ts +54 -0
  51. package/dist/errors.d.ts.map +1 -0
  52. package/dist/errors.js +82 -0
  53. package/dist/errors.js.map +1 -0
  54. package/dist/index.d.ts +39 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +28 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/naming/NamingStrategy.d.ts +52 -0
  59. package/dist/naming/NamingStrategy.d.ts.map +1 -0
  60. package/dist/naming/NamingStrategy.js +65 -0
  61. package/dist/naming/NamingStrategy.js.map +1 -0
  62. package/dist/query/QueryBuilder.d.ts +95 -0
  63. package/dist/query/QueryBuilder.d.ts.map +1 -0
  64. package/dist/query/QueryBuilder.js +310 -0
  65. package/dist/query/QueryBuilder.js.map +1 -0
  66. package/dist/query/native.d.ts +18 -0
  67. package/dist/query/native.d.ts.map +1 -0
  68. package/dist/query/native.js +49 -0
  69. package/dist/query/native.js.map +1 -0
  70. package/dist/schema/Migration.d.ts +55 -0
  71. package/dist/schema/Migration.d.ts.map +1 -0
  72. package/dist/schema/Migration.js +68 -0
  73. package/dist/schema/Migration.js.map +1 -0
  74. package/dist/schema/MigrationRunner.d.ts +115 -0
  75. package/dist/schema/MigrationRunner.d.ts.map +1 -0
  76. package/dist/schema/MigrationRunner.js +406 -0
  77. package/dist/schema/MigrationRunner.js.map +1 -0
  78. package/dist/schema/Schema.d.ts +22 -0
  79. package/dist/schema/Schema.d.ts.map +1 -0
  80. package/dist/schema/Schema.js +51 -0
  81. package/dist/schema/Schema.js.map +1 -0
  82. package/dist/schema/SchemaBuilder.d.ts +14 -0
  83. package/dist/schema/SchemaBuilder.d.ts.map +1 -0
  84. package/dist/schema/SchemaBuilder.js +13 -0
  85. package/dist/schema/SchemaBuilder.js.map +1 -0
  86. package/dist/schema/Seeder.d.ts +54 -0
  87. package/dist/schema/Seeder.d.ts.map +1 -0
  88. package/dist/schema/Seeder.js +89 -0
  89. package/dist/schema/Seeder.js.map +1 -0
  90. package/dist/schema/TableBuilder.d.ts +95 -0
  91. package/dist/schema/TableBuilder.d.ts.map +1 -0
  92. package/dist/schema/TableBuilder.js +234 -0
  93. package/dist/schema/TableBuilder.js.map +1 -0
  94. package/dist/schema/types.d.ts +30 -0
  95. package/dist/schema/types.d.ts.map +1 -0
  96. package/dist/schema/types.js +19 -0
  97. package/dist/schema/types.js.map +1 -0
  98. package/dist/services/db.d.ts +22 -0
  99. package/dist/services/db.d.ts.map +1 -0
  100. package/dist/services/db.js +37 -0
  101. package/dist/services/db.js.map +1 -0
  102. package/dist/testing/DatabaseCleanup.d.ts +19 -0
  103. package/dist/testing/DatabaseCleanup.d.ts.map +1 -0
  104. package/dist/testing/DatabaseCleanup.js +39 -0
  105. package/dist/testing/DatabaseCleanup.js.map +1 -0
  106. package/dist/testing/Factory.d.ts +59 -0
  107. package/dist/testing/Factory.d.ts.map +1 -0
  108. package/dist/testing/Factory.js +105 -0
  109. package/dist/testing/Factory.js.map +1 -0
  110. package/dist/testing/TestDatabase.d.ts +25 -0
  111. package/dist/testing/TestDatabase.d.ts.map +1 -0
  112. package/dist/testing/TestDatabase.js +52 -0
  113. package/dist/testing/TestDatabase.js.map +1 -0
  114. package/dist/testing/index.d.ts +4 -0
  115. package/dist/testing/index.d.ts.map +1 -0
  116. package/dist/testing/index.js +4 -0
  117. package/dist/testing/index.js.map +1 -0
  118. package/dist/utils/casing.d.ts +5 -0
  119. package/dist/utils/casing.d.ts.map +1 -0
  120. package/dist/utils/casing.js +9 -0
  121. package/dist/utils/casing.js.map +1 -0
  122. package/dist/utils/dialectFromUrl.d.ts +10 -0
  123. package/dist/utils/dialectFromUrl.d.ts.map +1 -0
  124. package/dist/utils/dialectFromUrl.js +8 -0
  125. package/dist/utils/dialectFromUrl.js.map +1 -0
  126. package/dist/utils/identifier.d.ts +19 -0
  127. package/dist/utils/identifier.d.ts.map +1 -0
  128. package/dist/utils/identifier.js +27 -0
  129. package/dist/utils/identifier.js.map +1 -0
  130. package/dist/utils/safePath.d.ts +22 -0
  131. package/dist/utils/safePath.d.ts.map +1 -0
  132. package/dist/utils/safePath.js +46 -0
  133. package/dist/utils/safePath.js.map +1 -0
  134. package/dist/utils/transactionBrand.d.ts +5 -0
  135. package/dist/utils/transactionBrand.d.ts.map +1 -0
  136. package/dist/utils/transactionBrand.js +5 -0
  137. package/dist/utils/transactionBrand.js.map +1 -0
  138. package/index.darwin-arm64.node +0 -0
  139. package/index.darwin-x64.node +0 -0
  140. package/index.linux-arm64-gnu.node +0 -0
  141. package/index.linux-x64-gnu.node +0 -0
  142. package/index.win32-x64-msvc.node +0 -0
  143. package/package.json +1 -1
  144. package/src/AtlasProvider.ts +2 -2
  145. package/src/BaseRepository.ts +73 -11
  146. package/src/adapters/NapiDbAdapter.ts +1 -1
  147. package/src/schema/MigrationRunner.ts +16 -12
  148. package/src/schema/TableBuilder.ts +26 -0
  149. package/src/schema/types.ts +2 -0
  150. package/src/testing/DatabaseCleanup.ts +6 -7
@@ -0,0 +1,1451 @@
1
+ /**
2
+ * BaseRepository — Data Mapper ORM with typed CRUD, soft deletes, and domain events.
3
+ *
4
+ * @implements FR29, FR31, FR35
5
+ */
6
+ import { randomUUID } from "node:crypto";
7
+ import { REPO_REF } from "./BaseEntity.js";
8
+ import { getColumnMetadata, getDateColumnConfig, getEntityMetadata, getPrimaryKey, getPrimaryKeyGenerator, getRelationMetadata, hasSoftDeletes, } from "./decorators/entity.js";
9
+ import { fireHooks } from "./decorators/hooks.js";
10
+ import { AtlasError, EntityNotFoundError } from "./errors.js";
11
+ import { ModelQuery, runWithAtlasInternalBypass } from "./ModelQuery.js";
12
+ import { compileStatementNative, getAtlasDialect, } from "./query/native.js";
13
+ import { camelToSnake, snakeToCamel } from "./utils/casing.js";
14
+ /**
15
+ * Coerce a `lastInsertRowid` to a JS number when it fits, leaving large
16
+ * mysql/sqlite values as bigint so callers don't silently lose precision.
17
+ */
18
+ function normalizeRowid(rowid) {
19
+ if (typeof rowid === "number")
20
+ return rowid;
21
+ return rowid <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(rowid) : rowid;
22
+ }
23
+ /**
24
+ * Whether a primary-key value should be treated as supplied. Distinguishes
25
+ * "explicit zero / empty-string id" from "unset" — only `null`/`undefined`
26
+ * route through the INSERT path; every other value is a candidate UPDATE.
27
+ */
28
+ function isProvidedPk(pk) {
29
+ return pk !== undefined && pk !== null;
30
+ }
31
+ /**
32
+ * Detect a unique-key / primary-key violation from the underlying driver
33
+ * error. Used by `save()` to recover from a TOCTOU race between the
34
+ * `find(pk)` check and the `INSERT`: a concurrent insert that wins the PK
35
+ * race surfaces as one of these codes, and we fall back to UPDATE rather
36
+ * than propagate a DB constraint error.
37
+ *
38
+ * - PostgreSQL: SQLSTATE `23505` (`unique_violation`)
39
+ * - SQLite: `SQLITE_CONSTRAINT_PRIMARYKEY` / `SQLITE_CONSTRAINT_UNIQUE`
40
+ * - MySQL: `ER_DUP_ENTRY` (named) / errno `1062` (numeric)
41
+ */
42
+ function isUniqueKeyViolation(err) {
43
+ if (err === null || typeof err !== "object")
44
+ return false;
45
+ const e = err;
46
+ const code = e.code;
47
+ const errno = e.errno;
48
+ return (code === "23505" ||
49
+ code === "SQLITE_CONSTRAINT_PRIMARYKEY" ||
50
+ code === "SQLITE_CONSTRAINT_UNIQUE" ||
51
+ code === "ER_DUP_ENTRY" ||
52
+ errno === 1062);
53
+ }
54
+ // ─── Repository ─────────────────────────────────────────────
55
+ /** Logical column types Postgres won't coerce from a text-bound param. */
56
+ const POSTGRES_CAST_TYPES = new Set([
57
+ "timestamp",
58
+ "datetime",
59
+ "date",
60
+ "time",
61
+ "uuid",
62
+ "json",
63
+ "jsonb",
64
+ ]);
65
+ /**
66
+ * Snake column → logical type for params needing a Postgres `$N::<type>` cast.
67
+ * sqlx binds JS strings as `text`; Postgres won't implicitly coerce that to
68
+ * timestamp/uuid/date. The Rust compiler applies these on Postgres only.
69
+ */
70
+ function computeCastTypes(entityClass) {
71
+ const out = {};
72
+ for (const col of getColumnMetadata(entityClass)) {
73
+ const t = col.type?.toLowerCase();
74
+ if (t && POSTGRES_CAST_TYPES.has(t)) {
75
+ out[camelToSnake(col.propertyKey)] = t;
76
+ }
77
+ }
78
+ // A uuid-strategy primary key is generated app-side as a string.
79
+ if (getPrimaryKeyGenerator(entityClass) === "uuid") {
80
+ out[camelToSnake(getPrimaryKey(entityClass) ?? "id")] ??= "uuid";
81
+ }
82
+ return out;
83
+ }
84
+ export class BaseRepository {
85
+ #entityClass;
86
+ #tableName;
87
+ #primaryKey;
88
+ #columns;
89
+ #db;
90
+ #softDeletes;
91
+ #validColumns;
92
+ #columnMap; // camelCase → snake_case (cached)
93
+ #dateColumns;
94
+ /** Snake column → logical type for params needing a Postgres `::cast`. */
95
+ #castTypes;
96
+ /**
97
+ * Per-property `prepare` (model → DB) callbacks lifted directly from
98
+ * `@Column({ prepare })` metadata. Keyed by camelCase `propertyKey`.
99
+ * Mirror of Adonis Lucid's `@column.prepare`. Story 35.10.
100
+ */
101
+ #columnPrepares;
102
+ /**
103
+ * Per-property `consume` (DB → model) callbacks lifted directly from
104
+ * `@Column({ consume })` metadata. Keyed by camelCase `propertyKey`.
105
+ * Mirror of Adonis Lucid's `@column.consume`. Story 35.10.
106
+ */
107
+ #columnConsumes;
108
+ /**
109
+ * SQL dialect used by this repository. Resolved at construction time from
110
+ * the connection (if it exposes a `dialect` property) or from the explicit
111
+ * `options.dialect` override, falling back to the process-wide default as
112
+ * the last resort. Passed to every `compileStatementNative` call so that
113
+ * multi-connection apps with heterogeneous dialects (postgres + mysql, …)
114
+ * compile each query with the correct target.
115
+ */
116
+ #dialect;
117
+ /** Callback to dispatch domain events (set by framework integration). */
118
+ onDomainEvents;
119
+ constructor(entityClass, db, options) {
120
+ this.#entityClass = entityClass;
121
+ this.#db = db;
122
+ // Dialect resolution order: explicit option > connection.dialect > process default.
123
+ const connDialect = db.dialect;
124
+ this.#dialect = options?.dialect ?? connDialect ?? getAtlasDialect();
125
+ const meta = getEntityMetadata(entityClass);
126
+ if (!meta) {
127
+ throw new AtlasError("NOT_ENTITY", `Class '${entityClass.name}' is not decorated with @Entity()`, {
128
+ hint: "Add @Entity('table_name') decorator to the class.",
129
+ });
130
+ }
131
+ this.#tableName = meta.tableName;
132
+ this.#primaryKey = getPrimaryKey(entityClass) ?? "id";
133
+ const columnsMeta = getColumnMetadata(entityClass);
134
+ this.#columns = columnsMeta.map((c) => c.propertyKey);
135
+ this.#softDeletes = hasSoftDeletes(entityClass);
136
+ this.#dateColumns = getDateColumnConfig(entityClass);
137
+ // Lift per-column `prepare` / `consume` callbacks directly from metadata.
138
+ // No global registry, no late-registration concern: callbacks are baked
139
+ // into the entity definition. Mirrors Adonis Lucid's `@column.prepare` /
140
+ // `@column.consume` pattern.
141
+ this.#columnPrepares = new Map();
142
+ this.#columnConsumes = new Map();
143
+ for (const col of columnsMeta) {
144
+ if (col.prepare)
145
+ this.#columnPrepares.set(col.propertyKey, col.prepare);
146
+ if (col.consume)
147
+ this.#columnConsumes.set(col.propertyKey, col.consume);
148
+ }
149
+ // Pre-compute column mappings for validation + hydration.
150
+ // Snapshot is frozen at construction — `@Column` decorators that run
151
+ // AFTER the repository instance is created (e.g. lazy/dynamic
152
+ // definitions) are invisible to the validator and will be rejected
153
+ // by `#resolveColumn`. Decorators must run at class-body evaluation
154
+ // time, before any repository for that entity is instantiated.
155
+ this.#validColumns = new Set();
156
+ this.#columnMap = new Map();
157
+ for (const col of this.#columns) {
158
+ const snake = camelToSnake(col);
159
+ this.#validColumns.add(col);
160
+ this.#validColumns.add(snake);
161
+ this.#columnMap.set(col, snake);
162
+ this.#columnMap.set(snake, snake);
163
+ }
164
+ this.#validColumns.add(this.#primaryKey);
165
+ this.#validColumns.add(camelToSnake(this.#primaryKey));
166
+ this.#columnMap.set(this.#primaryKey, camelToSnake(this.#primaryKey));
167
+ // Postgres cast hints: sqlx binds JS strings as `text`, which Postgres
168
+ // won't coerce to timestamp/uuid/date. See `computeCastTypes`.
169
+ this.#castTypes = computeCastTypes(entityClass);
170
+ }
171
+ // ─── Column validation ────────────────────────────────────
172
+ /** Resolve a column name to snake_case. Throws on invalid column. */
173
+ #resolveColumn(column) {
174
+ const mapped = this.#columnMap.get(column);
175
+ if (mapped)
176
+ return mapped;
177
+ const snake = camelToSnake(column);
178
+ if (this.#validColumns.has(snake))
179
+ return snake;
180
+ throw new AtlasError("E_INVALID_COLUMN", `Column '${column}' does not exist on ${this.#entityClass.name}`, {
181
+ hint: `Valid columns: ${this.#columns.join(", ")}`,
182
+ });
183
+ }
184
+ // ─── Query builder ────────────────────────────────────────
185
+ query() {
186
+ return new ModelQuery(this.#tableName, this.#db, (row) => this.#hydrate(row), this.#entityClass, (col) => this.#resolveColumn(col), this.#softDeletes, this.#dialect);
187
+ }
188
+ // ─── Transaction ──────────────────────────────────────────
189
+ useTransaction(trx) {
190
+ // Propagate the owning repo's dialect so the transactional copy stays on
191
+ // the correct SQL flavour — critical for multi-connection apps where the
192
+ // primary is postgres but a tenant runs on sqlite (or vice versa).
193
+ // Without this the transactional repo silently fell back to the global
194
+ // default and compiled mis-quoted SQL.
195
+ const repo = new BaseRepository(this.#entityClass, trx, {
196
+ dialect: this.#dialect,
197
+ });
198
+ repo.onDomainEvents = this.onDomainEvents;
199
+ return repo;
200
+ }
201
+ // ─── Finders ──────────────────────────────────────────────
202
+ async find(id) {
203
+ const wheres = [
204
+ { column: this.#primaryKey, operator: "=", value: id, type: "and" },
205
+ ];
206
+ this.#appendSoftScope(wheres);
207
+ const { sql, params } = this.#compileSelect({ wheres, limit: 1 });
208
+ const rows = await this.#db.query(sql, params);
209
+ const row = rows[0];
210
+ if (!row)
211
+ return null;
212
+ return this.#hydrate(row);
213
+ }
214
+ async findOrFail(id) {
215
+ const entity = await this.find(id);
216
+ if (!entity) {
217
+ throw new EntityNotFoundError(this.#entityClass.name, {
218
+ [this.#primaryKey]: id,
219
+ });
220
+ }
221
+ return entity;
222
+ }
223
+ async findBy(column, value) {
224
+ const col = this.#resolveColumn(column);
225
+ const wheres = [
226
+ { column: col, operator: "=", value, type: "and" },
227
+ ];
228
+ this.#appendSoftScope(wheres);
229
+ const { sql, params } = this.#compileSelect({ wheres, limit: 1 });
230
+ const rows = await this.#db.query(sql, params);
231
+ const row = rows[0];
232
+ if (!row)
233
+ return null;
234
+ return this.#hydrate(row);
235
+ }
236
+ async all() {
237
+ const wheres = [];
238
+ this.#appendSoftScope(wheres);
239
+ return this.#runSelect({ wheres });
240
+ }
241
+ async allWithTrashed() {
242
+ return this.#runSelect({});
243
+ }
244
+ async onlyTrashed() {
245
+ if (!this.#softDeletes)
246
+ return [];
247
+ return this.#runSelect({
248
+ wheres: [
249
+ {
250
+ column: "deleted_at",
251
+ operator: "IS NOT NULL",
252
+ value: null,
253
+ type: "and",
254
+ },
255
+ ],
256
+ });
257
+ }
258
+ async where(column, value) {
259
+ const col = this.#resolveColumn(column);
260
+ const wheres = [
261
+ { column: col, operator: "=", value, type: "and" },
262
+ ];
263
+ this.#appendSoftScope(wheres);
264
+ // Order by the resolved primary key (DESC = most recent insert first when
265
+ // the PK is an auto-increment integer or a monotonic UUID). Previously
266
+ // this hard-coded `rowid DESC`, which is a SQLite-only pseudo-column and
267
+ // blew up on Postgres/MySQL the moment the app ran against a real driver.
268
+ // Using the PK works on every dialect and matches the user's actual
269
+ // schema — the ordering contract is "most recent first by PK" for
270
+ // `repo.where(col, val)` as a convenience finder.
271
+ const pkCol = camelToSnake(this.#primaryKey);
272
+ return this.#runSelect({
273
+ wheres,
274
+ orderBy: [{ column: pkCol, direction: "desc" }],
275
+ });
276
+ }
277
+ // ─── Create / Save / Delete ───────────────────────────────
278
+ /**
279
+ * Build an entity from a plain object and persist it. Fires `beforeSave` →
280
+ * `beforeCreate` → INSERT → `afterCreate` → `afterSave`.
281
+ */
282
+ async create(data) {
283
+ const entity = new this.#entityClass();
284
+ for (const [key, value] of Object.entries(data)) {
285
+ if (this.#validColumns.has(key) ||
286
+ this.#validColumns.has(camelToSnake(key))) {
287
+ entity.setProp(key, value);
288
+ }
289
+ }
290
+ await fireHooks(this.#entityClass, "beforeSave", entity);
291
+ await fireHooks(this.#entityClass, "beforeCreate", entity);
292
+ await this.#insert(entity);
293
+ await fireHooks(this.#entityClass, "afterCreate", entity);
294
+ await fireHooks(this.#entityClass, "afterSave", entity);
295
+ return entity;
296
+ }
297
+ /**
298
+ * Persist an entity. Insert if PK is missing or row doesn't exist, update
299
+ * otherwise. Fires `beforeSave` → (`beforeCreate` | `beforeUpdate`) → DB →
300
+ * (`afterCreate` | `afterUpdate`) → `afterSave`, then dispatches
301
+ * accumulated domain events through `onDomainEvents`.
302
+ *
303
+ * Race-safety: the `find(pk)` → branch decision has a TOCTOU window. If a
304
+ * concurrent save inserts the same PK between our `find` and our `#insert`,
305
+ * the INSERT hits a unique-key violation; we catch it and fall back to the
306
+ * UPDATE path. The race-loser still fires `beforeCreate` before the
307
+ * recovery (its hook ran once before the conflict surfaced) — design
308
+ * `beforeCreate` hooks to be idempotent or move side-effects into
309
+ * `afterCreate` / `afterSave` where they only fire on commit.
310
+ */
311
+ async save(entity) {
312
+ const pk = entity[this.#primaryKey];
313
+ // Treat a present PK (including `0` and `''`) as a candidate update —
314
+ // `pk && ...` would route legitimate zero / empty-string keys through
315
+ // INSERT and double-write the row.
316
+ const isUpdate = isProvidedPk(pk) && (await this.find(pk)) !== null;
317
+ await fireHooks(this.#entityClass, "beforeSave", entity);
318
+ if (isUpdate) {
319
+ await this.#runUpdateBranch(entity);
320
+ }
321
+ else {
322
+ try {
323
+ await this.#runInsertBranch(entity);
324
+ }
325
+ catch (err) {
326
+ // Race recovery: the row didn't exist when we checked, but a
327
+ // concurrent insert beat us to it. Only fall back when the PK
328
+ // was explicitly provided (auto-generated PK can't collide on
329
+ // a fresh insert — DB generates a unique one per call).
330
+ if (isProvidedPk(pk) && isUniqueKeyViolation(err)) {
331
+ await this.#runUpdateBranch(entity);
332
+ }
333
+ else {
334
+ throw err;
335
+ }
336
+ }
337
+ }
338
+ await fireHooks(this.#entityClass, "afterSave", entity);
339
+ const events = entity.flushDomainEvents();
340
+ if (events.length > 0 && this.onDomainEvents) {
341
+ try {
342
+ await this.onDomainEvents([...events]);
343
+ }
344
+ catch (err) {
345
+ for (const e of events)
346
+ entity.addDomainEvent(e.name, e.data);
347
+ throw err;
348
+ }
349
+ }
350
+ }
351
+ async #runInsertBranch(entity) {
352
+ await fireHooks(this.#entityClass, "beforeCreate", entity);
353
+ await this.#insert(entity);
354
+ await fireHooks(this.#entityClass, "afterCreate", entity);
355
+ }
356
+ async #runUpdateBranch(entity) {
357
+ await fireHooks(this.#entityClass, "beforeUpdate", entity);
358
+ await this.#update(entity);
359
+ await fireHooks(this.#entityClass, "afterUpdate", entity);
360
+ }
361
+ /**
362
+ * Insert many rows in a single multi-row INSERT. Fires beforeSave/beforeCreate
363
+ * on each hydrated entity, then hydrates from the RETURNING clause (postgres +
364
+ * sqlite) before firing afterCreate/afterSave. On mysql, falls back to N single
365
+ * INSERTs (documented limitation).
366
+ *
367
+ * @implements Story 30.1 + 30.5
368
+ */
369
+ async createMany(rows) {
370
+ if (rows.length === 0)
371
+ return [];
372
+ const entities = rows.map((r) => {
373
+ const e = new this.#entityClass();
374
+ for (const [k, v] of Object.entries(r)) {
375
+ if (this.#validColumns.has(k) ||
376
+ this.#validColumns.has(camelToSnake(k)))
377
+ e.setProp(k, v);
378
+ }
379
+ return e;
380
+ });
381
+ for (const e of entities) {
382
+ await fireHooks(this.#entityClass, "beforeSave", e);
383
+ await fireHooks(this.#entityClass, "beforeCreate", e);
384
+ }
385
+ if (this.#dialect === "mysql") {
386
+ // mysql: loop single inserts (no RETURNING).
387
+ for (const e of entities)
388
+ await this.#insert(e);
389
+ }
390
+ else {
391
+ const specRows = entities.map((e) => this.#entityToRowPairs(e));
392
+ const spec = {
393
+ kind: "insert",
394
+ table: this.#tableName,
395
+ rows: specRows,
396
+ casts: this.#castTypes,
397
+ returning: [
398
+ camelToSnake(this.#primaryKey),
399
+ ...this.#columns.map((c) => camelToSnake(c)),
400
+ ],
401
+ };
402
+ const compiled = compileStatementNative(spec, this.#dialect);
403
+ const returned = await this.#db.query(compiled.statements[0], compiled.params);
404
+ returned.forEach((row, i) => {
405
+ for (const [k, v] of Object.entries(row))
406
+ entities[i].setProp(snakeToCamel(k), v);
407
+ entities[i].markAsPersisted();
408
+ });
409
+ }
410
+ for (const e of entities) {
411
+ await fireHooks(this.#entityClass, "afterCreate", e);
412
+ await fireHooks(this.#entityClass, "afterSave", e);
413
+ }
414
+ return entities;
415
+ }
416
+ /**
417
+ * Persist many already-constructed entity instances. Same hooks + batching
418
+ * as `createMany`, but accepts prebuilt entities so dirty tracking works.
419
+ *
420
+ * @implements Story 30.5
421
+ */
422
+ async saveMany(entities) {
423
+ if (entities.length === 0)
424
+ return [];
425
+ // Split new vs already-persisted; for simplicity, persist new ones as a
426
+ // batch and fall back to per-entity save for dirty ones.
427
+ const fresh = [];
428
+ const dirty = [];
429
+ for (const e of entities) {
430
+ if (Object.keys(e.$original ?? {}).length === 0)
431
+ fresh.push(e);
432
+ else
433
+ dirty.push(e);
434
+ }
435
+ if (fresh.length > 0) {
436
+ const rows = fresh.map((e) => {
437
+ const r = {};
438
+ for (const c of this.#columns) {
439
+ const v = e[c];
440
+ if (v !== undefined)
441
+ r[c] = v;
442
+ }
443
+ return r;
444
+ });
445
+ const created = await this.createMany(rows);
446
+ // Copy generated PKs back to the original instances.
447
+ created.forEach((c, i) => {
448
+ fresh[i].setProp(this.#primaryKey, c[this.#primaryKey]);
449
+ fresh[i].markAsPersisted();
450
+ });
451
+ }
452
+ for (const d of dirty)
453
+ await this.save(d);
454
+ return entities;
455
+ }
456
+ /**
457
+ * Dialect-aware upsert. postgres + sqlite emit `ON CONFLICT DO UPDATE`; mysql
458
+ * emits `ON DUPLICATE KEY UPDATE`. Empty `updateColumns` = DO NOTHING.
459
+ *
460
+ * @implements Story 30.4
461
+ */
462
+ async upsert(data, conflictColumns, updateColumns = []) {
463
+ const rowsArr = Array.isArray(data) ? data : [data];
464
+ const rows = rowsArr.map((r) => this.#plainToRowPairs(r));
465
+ const spec = {
466
+ kind: "upsert",
467
+ table: this.#tableName,
468
+ rows,
469
+ conflictColumns: conflictColumns.map((c) => this.#resolveColumn(c)),
470
+ updateColumns: updateColumns.map((c) => this.#resolveColumn(c)),
471
+ casts: this.#castTypes,
472
+ };
473
+ const compiled = compileStatementNative(spec, this.#dialect);
474
+ const result = await this.#db.execute(compiled.statements[0], compiled.params);
475
+ return result.rowsAffected;
476
+ }
477
+ /**
478
+ * Find a row matching `search` or create one merged with `defaults`.
479
+ *
480
+ * @implements Story 30.6
481
+ */
482
+ async firstOrCreate(search, defaults = {}) {
483
+ const existing = await this.#findBySearch(search);
484
+ if (existing)
485
+ return existing;
486
+ return this.create({ ...search, ...defaults });
487
+ }
488
+ /** Find a row or build an in-memory instance without persisting. */
489
+ async firstOrNew(search, defaults = {}) {
490
+ const existing = await this.#findBySearch(search);
491
+ if (existing)
492
+ return existing;
493
+ const e = new this.#entityClass();
494
+ for (const [k, v] of Object.entries({ ...search, ...defaults })) {
495
+ if (this.#validColumns.has(k) || this.#validColumns.has(camelToSnake(k)))
496
+ e.setProp(k, v);
497
+ }
498
+ return e;
499
+ }
500
+ /** Atomic find-or-update-or-insert. */
501
+ async updateOrCreate(search, values) {
502
+ const existing = await this.#findBySearch(search);
503
+ if (existing) {
504
+ for (const [k, v] of Object.entries(values))
505
+ existing.setProp(k, v);
506
+ await this.save(existing);
507
+ return existing;
508
+ }
509
+ return this.create({ ...search, ...values });
510
+ }
511
+ async #findBySearch(search) {
512
+ let q = this.query();
513
+ for (const [k, v] of Object.entries(search))
514
+ q = q.where(k, v);
515
+ return q.first();
516
+ }
517
+ /**
518
+ * Apply `@Column({ prepare })` (model → DB) when declared. Adonis Lucid's
519
+ * contract — callback receives the raw value (including null/undefined) and
520
+ * decides what to do with it.
521
+ */
522
+ #applyPrepare(propertyKey, value) {
523
+ const prepare = this.#columnPrepares.get(propertyKey);
524
+ if (!prepare)
525
+ return value;
526
+ let result;
527
+ try {
528
+ result = prepare(value);
529
+ }
530
+ catch (err) {
531
+ throw wrapAdapterError("prepare", propertyKey, err);
532
+ }
533
+ assertNotPromise("prepare", propertyKey, result);
534
+ return result;
535
+ }
536
+ #applyConsume(propertyKey, value) {
537
+ const consume = this.#columnConsumes.get(propertyKey);
538
+ if (!consume)
539
+ return value;
540
+ let result;
541
+ try {
542
+ result = consume(value);
543
+ }
544
+ catch (err) {
545
+ throw wrapAdapterError("consume", propertyKey, err);
546
+ }
547
+ assertNotPromise("consume", propertyKey, result);
548
+ return result;
549
+ }
550
+ #plainToRowPairs(obj) {
551
+ const pairs = [];
552
+ for (const [k, v] of Object.entries(obj)) {
553
+ // Skip explicit `undefined` so we don't emit `undefined` as a SQL bind —
554
+ // the Rust DML compiler / NAPI layer rejects it. `null` is allowed
555
+ // through because that's a meaningful SQL value.
556
+ if (v === undefined)
557
+ continue;
558
+ // Prepare map is keyed by camelCase property name. The input bag may use
559
+ // either camel or snake — try the raw key first, else convert.
560
+ const propKey = this.#columnPrepares.has(k) ? k : snakeToCamel(k);
561
+ pairs.push([this.#resolveColumn(k), this.#applyPrepare(propKey, v)]);
562
+ }
563
+ return pairs;
564
+ }
565
+ #entityToRowPairs(entity) {
566
+ const pairs = [];
567
+ for (const col of this.#columns) {
568
+ const v = entity[col];
569
+ if (v !== undefined)
570
+ pairs.push([camelToSnake(col), this.#applyPrepare(col, v)]);
571
+ }
572
+ return pairs;
573
+ }
574
+ /** Delete the entity. Fires `beforeDelete` → DB → `afterDelete`. Soft-delete aware. */
575
+ async delete(entity) {
576
+ await fireHooks(this.#entityClass, "beforeDelete", entity);
577
+ const pk = entity[this.#primaryKey];
578
+ if (this.#softDeletes) {
579
+ const now = new Date().toISOString();
580
+ await this.#runUpdate([["deleted_at", now]], [{ column: this.#primaryKey, operator: "=", value: pk, type: "and" }]);
581
+ entity.setProp("deletedAt", now);
582
+ }
583
+ else {
584
+ await this.#runDelete([
585
+ { column: this.#primaryKey, operator: "=", value: pk, type: "and" },
586
+ ]);
587
+ }
588
+ await fireHooks(this.#entityClass, "afterDelete", entity);
589
+ }
590
+ /** Permanently delete (bypasses soft delete). Fires `beforeDelete` / `afterDelete` hooks. */
591
+ async forceDelete(entity) {
592
+ await fireHooks(this.#entityClass, "beforeDelete", entity);
593
+ await this.#runDelete([
594
+ {
595
+ column: this.#primaryKey,
596
+ operator: "=",
597
+ value: entity[this.#primaryKey],
598
+ type: "and",
599
+ },
600
+ ]);
601
+ await fireHooks(this.#entityClass, "afterDelete", entity);
602
+ }
603
+ async restore(entity) {
604
+ if (!this.#softDeletes)
605
+ return;
606
+ await this.#runUpdate([["deleted_at", null]], [
607
+ {
608
+ column: this.#primaryKey,
609
+ operator: "=",
610
+ value: entity[this.#primaryKey],
611
+ type: "and",
612
+ },
613
+ ]);
614
+ entity.setProp("deletedAt", null);
615
+ }
616
+ // ─── Bulk updates ─────────────────────────────────────────
617
+ async updateById(id, data) {
618
+ const set = this.#buildSetPairs(data);
619
+ await this.#runUpdate(set, [
620
+ { column: this.#primaryKey, operator: "=", value: id, type: "and" },
621
+ ]);
622
+ }
623
+ async updateWhere(column, columnValue, data) {
624
+ const whereCol = this.#resolveColumn(column);
625
+ const set = this.#buildSetPairs(data);
626
+ await this.#runUpdate(set, [
627
+ { column: whereCol, operator: "=", value: columnValue, type: "and" },
628
+ ]);
629
+ }
630
+ async increment(id, columnOrMap, amount = 1) {
631
+ const set = this.#buildIncrementPairs(columnOrMap, amount, "increment");
632
+ await this.#runUpdate(set, [
633
+ { column: this.#primaryKey, operator: "=", value: id, type: "and" },
634
+ ]);
635
+ }
636
+ async decrement(id, columnOrMap, amount = 1) {
637
+ const set = this.#buildIncrementPairs(columnOrMap, amount, "decrement");
638
+ await this.#runUpdate(set, [
639
+ { column: this.#primaryKey, operator: "=", value: id, type: "and" },
640
+ ]);
641
+ }
642
+ // ─── Raw ──────────────────────────────────────────────────
643
+ async raw(sql, ...params) {
644
+ const rows = await this.#db.query(sql, params);
645
+ return rows.map((r) => this.#hydrate(r));
646
+ }
647
+ // ─── Accessors ────────────────────────────────────────────
648
+ getTableName() {
649
+ return this.#tableName;
650
+ }
651
+ getPrimaryKeyColumn() {
652
+ return this.#primaryKey;
653
+ }
654
+ // ─── Private helpers ──────────────────────────────────────
655
+ #compileSelect(opts) {
656
+ const spec = {
657
+ kind: "select",
658
+ table: this.#tableName,
659
+ select: ["*"],
660
+ wheres: opts.wheres ?? [],
661
+ orderBy: opts.orderBy ?? [],
662
+ groupBy: [],
663
+ having: [],
664
+ limit: opts.limit ?? null,
665
+ offset: null,
666
+ distinct: false,
667
+ ctes: [],
668
+ unions: [],
669
+ };
670
+ const compiled = compileStatementNative(spec, this.#dialect);
671
+ return { sql: compiled.statements[0], params: compiled.params };
672
+ }
673
+ async #runSelect(opts) {
674
+ const { sql, params } = this.#compileSelect(opts);
675
+ const rows = await this.#db.query(sql, params);
676
+ return rows.map((r) => this.#hydrate(r));
677
+ }
678
+ async #runDelete(wheres) {
679
+ const compiled = compileStatementNative({ kind: "delete", table: this.#tableName, wheres }, this.#dialect);
680
+ await this.#db.execute(compiled.statements[0], compiled.params);
681
+ }
682
+ /**
683
+ * Emit an UPDATE. Each entry in `set` is either `[col, rawValue]` (plain
684
+ * binding — `SET col = ?`) or `[col, { op: 'increment' | 'decrement', value }]`
685
+ * (atomic expression — `SET col = col ± ?`). The Rust compiler picks the
686
+ * right SQL via `SetValue::Value` / `SetValue::Expression`.
687
+ */
688
+ async #runUpdate(set, wheres) {
689
+ if (set.length === 0)
690
+ return;
691
+ const compiled = compileStatementNative({
692
+ kind: "update",
693
+ table: this.#tableName,
694
+ set,
695
+ wheres,
696
+ casts: this.#castTypes,
697
+ }, this.#dialect);
698
+ await this.#db.execute(compiled.statements[0], compiled.params);
699
+ }
700
+ /**
701
+ * Execute the INSERT and return whatever the engine surfaces about the
702
+ * fresh row: the RETURNING projection (postgres / sqlite) when available,
703
+ * otherwise just the rowsAffected count (mysql doesn't support RETURNING).
704
+ * Caller decides how much to rehydrate.
705
+ */
706
+ async #runInsert(values) {
707
+ if (values.length === 0)
708
+ return {};
709
+ const supportsReturning = this.#dialect !== "mysql";
710
+ const spec = supportsReturning
711
+ ? {
712
+ kind: "insert",
713
+ table: this.#tableName,
714
+ values,
715
+ casts: this.#castTypes,
716
+ returning: [
717
+ camelToSnake(this.#primaryKey),
718
+ ...this.#columns.map((c) => camelToSnake(c)),
719
+ ],
720
+ }
721
+ : {
722
+ kind: "insert",
723
+ table: this.#tableName,
724
+ values,
725
+ casts: this.#castTypes,
726
+ };
727
+ const compiled = compileStatementNative(spec, this.#dialect);
728
+ if (supportsReturning) {
729
+ const rows = await this.#db.query(compiled.statements[0], compiled.params);
730
+ const first = rows[0];
731
+ return first ? { row: first } : {};
732
+ }
733
+ await this.#db.execute(compiled.statements[0], compiled.params);
734
+ // MySQL path: napi adapter doesn't surface lastInsertRowid through
735
+ // `execute()`. Callers that need it must use an explicit dialect-
736
+ // specific query (e.g. `SELECT LAST_INSERT_ID()`). For Atlas's
737
+ // public surface, the entity carries the PK already (either set
738
+ // by the caller or generated client-side as a UUID).
739
+ return {};
740
+ }
741
+ #appendSoftScope(wheres) {
742
+ if (this.#softDeletes) {
743
+ wheres.push({
744
+ column: "deleted_at",
745
+ operator: "IS NULL",
746
+ value: null,
747
+ type: "and",
748
+ });
749
+ }
750
+ }
751
+ async #insert(entity) {
752
+ // Auto-generate the PK when declared via `@PrimaryKey({ generated })`.
753
+ this.#applyPrimaryKeyGenerator(entity);
754
+ // Auto-populate @column.dateTime({ autoCreate: true }) fields before building the row.
755
+ this.#applyAutoTimestamps(entity, "insert");
756
+ const data = this.#entityToRow(entity);
757
+ const result = await this.#runInsert(Object.entries(data));
758
+ // Hydrate DB-generated values (auto-increment ids, default columns) so
759
+ // callers see them on the entity without an extra `find()`. Mirrors
760
+ // `createMany`, where the multi-row path already does this.
761
+ if (result.row) {
762
+ for (const [k, v] of Object.entries(result.row))
763
+ entity.setProp(snakeToCamel(k), v);
764
+ }
765
+ else if (result.lastInsertRowid !== undefined &&
766
+ !isProvidedPk(entity[this.#primaryKey])) {
767
+ entity.setProp(this.#primaryKey, normalizeRowid(result.lastInsertRowid));
768
+ }
769
+ // After a successful INSERT, the entity is now persisted — snapshot
770
+ // its columns so subsequent dirty checks compare against the DB state.
771
+ entity.markAsPersisted();
772
+ }
773
+ /**
774
+ * UPDATE the entity — emits only the dirty columns (story 32.2).
775
+ *
776
+ * If no column is dirty, skips the query entirely (common case when a
777
+ * `save()` is called defensively without any real mutation).
778
+ */
779
+ async #update(entity) {
780
+ // Auto-bump @column.dateTime({ autoUpdate: true }) BEFORE computing $dirty
781
+ // so the bumped column lands in the SET if anything else is dirty.
782
+ this.#applyAutoTimestamps(entity, "update");
783
+ const dirty = entity.$dirty;
784
+ const pk = entity[this.#primaryKey];
785
+ // Primary key is never part of the SET — it's the WHERE.
786
+ delete dirty[this.#primaryKey];
787
+ if (Object.keys(dirty).length === 0)
788
+ return; // nothing changed
789
+ // Map dirty camelCase keys to snake_case DB columns. `$dirty` keys are
790
+ // already camelCase (they come from `entity.setProp` / direct assignment),
791
+ // so the prepare lookup uses `k` as-is. Skip explicit `undefined`
792
+ // assignments to mirror `#buildSetPairs` / `#plainToRowPairs` — the
793
+ // Rust DML compiler / NAPI layer rejects `undefined` binds.
794
+ const setPairs = [];
795
+ for (const [k, v] of Object.entries(dirty)) {
796
+ if (v === undefined)
797
+ continue;
798
+ setPairs.push([camelToSnake(k), this.#applyPrepare(k, v)]);
799
+ }
800
+ if (setPairs.length === 0) {
801
+ // All dirty entries were `undefined` (skipped above). Re-snapshot
802
+ // anyway: without this, `$dirty` keeps reporting the same
803
+ // undefined keys forever and a caller checking `entity.isDirty()`
804
+ // loops on a no-op save.
805
+ entity.markAsPersisted();
806
+ return;
807
+ }
808
+ await this.#runUpdate(setPairs, [
809
+ { column: this.#primaryKey, operator: "=", value: pk, type: "and" },
810
+ ]);
811
+ // Re-snapshot after a successful UPDATE.
812
+ entity.markAsPersisted();
813
+ }
814
+ /**
815
+ * Generate the primary key on INSERT when the entity declares
816
+ * `@PrimaryKey({ generated: 'uuid' })` and no value is set. Caller-supplied
817
+ * PKs win — we only fill in when the field is `undefined`.
818
+ */
819
+ #applyPrimaryKeyGenerator(entity) {
820
+ const strategy = getPrimaryKeyGenerator(this.#entityClass);
821
+ if (!strategy)
822
+ return;
823
+ if (entity[this.#primaryKey] !== undefined)
824
+ return;
825
+ if (strategy === "uuid") {
826
+ entity.setProp(this.#primaryKey, randomUUID());
827
+ }
828
+ }
829
+ /**
830
+ * Apply auto-timestamp columns (`@column.dateTime({ autoCreate, autoUpdate })`)
831
+ * on the entity before persistence. Called from `#insert` and `#update`.
832
+ */
833
+ #applyAutoTimestamps(entity, phase) {
834
+ const now = new Date();
835
+ for (const [prop, cfg] of Object.entries(this.#dateColumns)) {
836
+ if (phase === "insert") {
837
+ if (cfg.autoCreate && entity[prop] === undefined) {
838
+ entity.setProp(prop, now);
839
+ }
840
+ if (cfg.autoUpdate && entity[prop] === undefined) {
841
+ entity.setProp(prop, now);
842
+ }
843
+ }
844
+ else if (phase === "update" && cfg.autoUpdate) {
845
+ entity.setProp(prop, now);
846
+ }
847
+ }
848
+ }
849
+ #hydrate(row) {
850
+ const entity = new this.#entityClass();
851
+ for (const [key, value] of Object.entries(row)) {
852
+ const camelKey = snakeToCamel(key);
853
+ // Resolve against declared column metadata, not `in entity` — fields
854
+ // using Adonis' `declare field: T` pattern are not own-properties of
855
+ // a freshly constructed instance.
856
+ const targetKey = this.#validColumns.has(camelKey)
857
+ ? camelKey
858
+ : this.#validColumns.has(key)
859
+ ? key
860
+ : null;
861
+ if (!targetKey)
862
+ continue;
863
+ // Apply `@Column({ consume })` if declared on this property. Unlike the
864
+ // previous registry-based design, the callback receives every value
865
+ // including `null` / `undefined` — the user's `consume` is responsible
866
+ // for its own null-handling, matching Adonis Lucid's contract.
867
+ entity.setProp(targetKey, this.#applyConsume(targetKey, value));
868
+ }
869
+ // Freeze the original snapshot — from now on, only columns changed AFTER
870
+ // hydration are considered dirty by `entity.$dirty`.
871
+ entity.markAsPersisted();
872
+ // Back-pointer so `entity.refresh()` / `entity.fresh()` can re-query.
873
+ Object.defineProperty(entity, REPO_REF, {
874
+ value: this,
875
+ enumerable: false,
876
+ configurable: true,
877
+ });
878
+ return entity;
879
+ }
880
+ /**
881
+ * Re-read the entity's row from the database and mutate the instance in place.
882
+ * Used by `entity.refresh()` — not normally called directly.
883
+ *
884
+ * @implements Story 32.6
885
+ */
886
+ async refresh(entity) {
887
+ const pk = entity[this.#primaryKey];
888
+ if (pk === undefined || pk === null) {
889
+ throw new EntityNotFoundError(this.#entityClass.name, {
890
+ [this.#primaryKey]: pk,
891
+ });
892
+ }
893
+ const fresh = await this.find(pk);
894
+ if (!fresh) {
895
+ throw new EntityNotFoundError(this.#entityClass.name, {
896
+ [this.#primaryKey]: pk,
897
+ });
898
+ }
899
+ // Copy all column values from the fresh row onto the existing instance.
900
+ for (const col of this.#columns) {
901
+ entity.setProp(col, fresh[col]);
902
+ }
903
+ entity.setProp(this.#primaryKey, fresh[this.#primaryKey]);
904
+ entity.markAsPersisted();
905
+ }
906
+ /**
907
+ * Re-read the entity's row and return a NEW instance (the input is untouched).
908
+ *
909
+ * @implements Story 32.6
910
+ */
911
+ /**
912
+ * Lazy-load a relation count into `entity.$extras[alias ?? `${relationName}_count`]`.
913
+ * Uses `ModelQuery.withCount` with a restrictive `WHERE pk = ?` so it reads
914
+ * one entity's row back with the aggregate column attached.
915
+ *
916
+ * @implements Story 29.2
917
+ */
918
+ async loadCount(entity, relationName, alias) {
919
+ const pk = entity[this.#primaryKey];
920
+ if (pk === undefined || pk === null) {
921
+ throw new EntityNotFoundError(this.#entityClass.name, {
922
+ [this.#primaryKey]: pk,
923
+ });
924
+ }
925
+ const finalAlias = alias ?? `${relationName}_count`;
926
+ const q = this.query()
927
+ .where(this.#primaryKey, pk)
928
+ .withCount(relationName, (sub) => {
929
+ sub.as(finalAlias);
930
+ });
931
+ const [refreshed] = await q.exec();
932
+ if (refreshed)
933
+ entity.setExtra(finalAlias, refreshed.getExtra(finalAlias));
934
+ }
935
+ /**
936
+ * Lazy-load a relation aggregate. The builder callback sets the aggregate via
937
+ * `.sum/.avg/.min/.max/.count` and the alias via `.as('name')`.
938
+ *
939
+ * @implements Story 29.2
940
+ */
941
+ async loadAggregate(entity, relationName, build) {
942
+ const pk = entity[this.#primaryKey];
943
+ if (pk === undefined || pk === null) {
944
+ throw new EntityNotFoundError(this.#entityClass.name, {
945
+ [this.#primaryKey]: pk,
946
+ });
947
+ }
948
+ let capturedAlias;
949
+ const q = this.query()
950
+ .where(this.#primaryKey, pk)
951
+ .withAggregate(relationName, (sub) => {
952
+ build(sub);
953
+ capturedAlias = sub.subqueryAlias;
954
+ });
955
+ const [refreshed] = await q.exec();
956
+ const alias = capturedAlias ?? relationName;
957
+ if (refreshed)
958
+ entity.setExtra(alias, refreshed.getExtra(alias));
959
+ }
960
+ /**
961
+ * Lazy-load a relation onto an already-fetched entity. Re-uses the preload
962
+ * resolver by running a fresh query with `.where(pk = entity.pk).preload(...)`.
963
+ *
964
+ * @implements Story 31.10
965
+ */
966
+ async loadRelation(entity, relationName, callback) {
967
+ const pk = entity[this.#primaryKey];
968
+ if (pk === undefined || pk === null) {
969
+ throw new EntityNotFoundError(this.#entityClass.name, {
970
+ [this.#primaryKey]: pk,
971
+ });
972
+ }
973
+ const q = this.query().where(this.#primaryKey, pk);
974
+ if (callback)
975
+ q.preload(relationName, callback);
976
+ else
977
+ q.preload(relationName);
978
+ const [hydrated] = await q.exec();
979
+ if (hydrated) {
980
+ // Copy the loaded relation onto the caller's instance.
981
+ const value = hydrated[relationName];
982
+ entity.setProp(relationName, value);
983
+ }
984
+ }
985
+ /**
986
+ * Return a thin relation proxy bound to the given parent instance. Only
987
+ * `hasOne` / `hasMany` (and trivially `manyToMany` insert paths) are wired
988
+ * here; richer operations (attach/detach/sync) live in Story 31.7's proxy.
989
+ *
990
+ * @implements Story 31.5
991
+ */
992
+ relatedProxy(entity, relationName) {
993
+ const relations = getRelationMetadata(this.#entityClass);
994
+ const relation = relations.find((r) => r.propertyKey === relationName);
995
+ if (!relation)
996
+ throw new Error(`Relation '${relationName}' not found on ${this.#entityClass.name}`);
997
+ const relatedClass = relation.target();
998
+ const relatedMeta = getEntityMetadata(relatedClass);
999
+ if (!relatedMeta)
1000
+ throw new Error(`Entity metadata missing on related class ${relatedClass.name}`);
1001
+ const relatedTable = relatedMeta.tableName;
1002
+ const parentPk = relation.localKey ?? getPrimaryKey(this.#entityClass) ?? "id";
1003
+ const parentIdValue = entity[parentPk];
1004
+ const relatedRepo = new BaseRepository(relatedClass, this.#db, {
1005
+ dialect: this.#dialect,
1006
+ });
1007
+ const db = this.#db;
1008
+ // FK column naming: belongsTo stores the FK on THIS side; has* / m2m on the OTHER side.
1009
+ const fkCol = relation.foreignKey ??
1010
+ (relation.type === "belongsTo"
1011
+ ? `${camelToSnake(relatedClass.name)}_id`
1012
+ : `${camelToSnake(this.#entityClass.name)}_id`);
1013
+ const fkProp = snakeToCamel(fkCol);
1014
+ const injectFk = (data) => ({
1015
+ ...data,
1016
+ [fkCol]: parentIdValue,
1017
+ [fkProp]: parentIdValue,
1018
+ });
1019
+ // Shared "has" proxy methods (create/createMany/save/saveMany).
1020
+ const hasOps = {
1021
+ async create(data) {
1022
+ return relatedRepo.create(injectFk(data));
1023
+ },
1024
+ async createMany(rows) {
1025
+ return relatedRepo.createMany(rows.map(injectFk));
1026
+ },
1027
+ async save(related) {
1028
+ related.setProp(fkCol, parentIdValue);
1029
+ related.setProp(fkProp, parentIdValue);
1030
+ await relatedRepo.save(related);
1031
+ },
1032
+ async saveMany(related) {
1033
+ for (const r of related) {
1034
+ r.setProp(fkCol, parentIdValue);
1035
+ r.setProp(fkProp, parentIdValue);
1036
+ }
1037
+ return relatedRepo.saveMany(related);
1038
+ },
1039
+ };
1040
+ // Scoped query builder (Story 31.9) — pre-applies the FK predicate
1041
+ // (or pivot JOIN for m2m) so downstream filters/updates/deletes stay
1042
+ // inside the relation boundary.
1043
+ const scopedQuery = () => {
1044
+ const q = relatedRepo.query();
1045
+ if (relation.type === "manyToMany") {
1046
+ if (!relation.pivot)
1047
+ throw new Error(`@ManyToMany ${relationName} requires pivot options`);
1048
+ const pivot = relation.pivot;
1049
+ const pivotFk = pivot.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
1050
+ const pivotOther = pivot.otherKey ?? `${camelToSnake(relatedClass.name)}_id`;
1051
+ const relatedPk = getPrimaryKey(relatedClass) ?? "id";
1052
+ // Inline validated quote (same policy as the m2m branch below).
1053
+ const dialect = this.#dialect;
1054
+ const quote = (name) => {
1055
+ if (!/^[A-Za-z0-9_]+$/.test(name)) {
1056
+ throw new Error(`Unsafe identifier in pivot metadata: '${name}'`);
1057
+ }
1058
+ return dialect === "mysql" ? `\`${name}\`` : `"${name}"`;
1059
+ };
1060
+ // EXISTS (SELECT 1 FROM pivot WHERE pivot.pivotFk = ? AND pivot.pivotOther = related.pk)
1061
+ // Framework-internal raw fragment (identifiers already validated by
1062
+ // the `quote` helper above) — bypass strict mode so this path still
1063
+ // works when the user enables `setAtlasStrictMode(true)` on their app.
1064
+ runWithAtlasInternalBypass(() => {
1065
+ q.whereRaw(`EXISTS (SELECT 1 FROM ${quote(pivot.pivotTable)} ` +
1066
+ `WHERE ${quote(pivot.pivotTable)}.${quote(pivotFk)} = ? ` +
1067
+ `AND ${quote(pivot.pivotTable)}.${quote(pivotOther)} = ${quote(relatedTable)}.${quote(relatedPk)})`, [parentIdValue]);
1068
+ });
1069
+ }
1070
+ else if (relation.type === "belongsTo") {
1071
+ const ownerKey = relation.ownerKey ?? getPrimaryKey(relatedClass) ?? "id";
1072
+ q.where(ownerKey, entity[fkProp] ?? entity[fkCol]);
1073
+ }
1074
+ else {
1075
+ // hasOne / hasMany
1076
+ q.where(fkCol, parentIdValue);
1077
+ }
1078
+ return q;
1079
+ };
1080
+ if (relation.type === "belongsTo") {
1081
+ // Story 31.6 — associate / dissociate set the FK on THIS entity and save
1082
+ // it through the outer repository. Both methods close over `parentRepo`,
1083
+ // which is the repo that owns `entity` (i.e. `this`). The double cast is
1084
+ // the standard TS idiom for widening a generic `this` — safe because
1085
+ // `T extends BaseEntity`.
1086
+ const parentRepo = this;
1087
+ const proxy = {
1088
+ type: "belongsTo",
1089
+ ...hasOps,
1090
+ query: scopedQuery,
1091
+ async associate(model) {
1092
+ if (model === null || model === undefined) {
1093
+ throw new Error(`related('${relationName}').associate() rejects null/undefined — use dissociate() instead`);
1094
+ }
1095
+ const ownerKey = relation.ownerKey ?? getPrimaryKey(relatedClass) ?? "id";
1096
+ const fkValue = model[ownerKey];
1097
+ entity.setProp(fkCol, fkValue);
1098
+ entity.setProp(fkProp, fkValue);
1099
+ await parentRepo.save(entity);
1100
+ },
1101
+ async dissociate() {
1102
+ entity.setProp(fkCol, null);
1103
+ entity.setProp(fkProp, null);
1104
+ await parentRepo.save(entity);
1105
+ },
1106
+ };
1107
+ return proxy;
1108
+ }
1109
+ if (relation.type === "manyToMany") {
1110
+ if (!relation.pivot)
1111
+ throw new Error(`@ManyToMany ${relationName} requires pivot options`);
1112
+ const pivot = relation.pivot;
1113
+ const pivotTable = pivot.pivotTable;
1114
+ const pivotFk = pivot.foreignKey ?? `${camelToSnake(this.#entityClass.name)}_id`;
1115
+ const pivotOther = pivot.otherKey ?? `${camelToSnake(relatedClass.name)}_id`;
1116
+ const tsConfig = pivot.pivotTimestamps;
1117
+ const pivotAdapters = pivot.pivotColumnAdapters;
1118
+ const dialect = this.#dialect;
1119
+ /**
1120
+ * Resolve pivot timestamp column names from the decorator config.
1121
+ *
1122
+ * Three forms supported:
1123
+ * - `pivotTimestamps: true` → { created_at, updated_at } default names
1124
+ * - `pivotTimestamps: { createdAt: false, updatedAt: 'updated_on' }` → opt-out / rename
1125
+ * - `pivotTimestamps: undefined` → no timestamps written
1126
+ *
1127
+ * `false` opts a timestamp out; a string overrides the column name;
1128
+ * `undefined` falls back to the default name.
1129
+ */
1130
+ const resolveTimestamps = () => {
1131
+ if (!tsConfig)
1132
+ return {};
1133
+ const now = new Date().toISOString();
1134
+ let createdCol;
1135
+ let updatedCol;
1136
+ if (tsConfig === true) {
1137
+ createdCol = "created_at";
1138
+ updatedCol = "updated_at";
1139
+ }
1140
+ else {
1141
+ createdCol =
1142
+ tsConfig.createdAt === false
1143
+ ? null
1144
+ : (tsConfig.createdAt ?? "created_at");
1145
+ updatedCol =
1146
+ tsConfig.updatedAt === false
1147
+ ? null
1148
+ : (tsConfig.updatedAt ?? "updated_at");
1149
+ }
1150
+ const out = {};
1151
+ if (createdCol)
1152
+ out[createdCol] = now;
1153
+ if (updatedCol)
1154
+ out[updatedCol] = now;
1155
+ return out;
1156
+ };
1157
+ const normalizeAttach = (arg) => {
1158
+ if (Array.isArray(arg))
1159
+ return arg.map((id) => ({ id, extras: {} }));
1160
+ return Object.entries(arg).map(([id, extras]) => ({ id, extras }));
1161
+ };
1162
+ // Current pivot rows — compiled through the Rust SELECT path so the
1163
+ // pivot identifiers go through `quote_identifier` (rejects anything
1164
+ // outside `[A-Za-z0-9_]`), rather than through the ad-hoc `quote`
1165
+ // helper that would blindly wrap a malicious metadata string.
1166
+ //
1167
+ // Now async — every site in `sync()` is in an async closure.
1168
+ const currentIds = async () => {
1169
+ const selectSpec = {
1170
+ kind: "select",
1171
+ table: pivotTable,
1172
+ select: [pivotOther],
1173
+ wheres: [
1174
+ {
1175
+ column: pivotFk,
1176
+ operator: "=",
1177
+ value: parentIdValue,
1178
+ type: "and",
1179
+ },
1180
+ ],
1181
+ selectSubqueries: [],
1182
+ orderBy: [],
1183
+ groupBy: [],
1184
+ having: [],
1185
+ limit: null,
1186
+ offset: null,
1187
+ distinct: false,
1188
+ ctes: [],
1189
+ unions: [],
1190
+ joins: [],
1191
+ lockMode: null,
1192
+ };
1193
+ const compiled = compileStatementNative(selectSpec, dialect);
1194
+ const rows = await db.query(compiled.statements[0], compiled.params);
1195
+ return rows.map((r) => r[pivotOther]);
1196
+ };
1197
+ // Delete via the Rust DELETE compiler so the pivot table + columns get
1198
+ // `quote_identifier` validation (rejects `"`, `;`, etc.) — safer than
1199
+ // the previous hand-built SQL with a dumb `"` wrapper.
1200
+ const detach = async (ids) => {
1201
+ const wheres = [
1202
+ { column: pivotFk, operator: "=", value: parentIdValue, type: "and" },
1203
+ ];
1204
+ if (ids && ids.length > 0) {
1205
+ wheres.push({
1206
+ column: pivotOther,
1207
+ operator: "IN",
1208
+ value: ids,
1209
+ type: "and",
1210
+ });
1211
+ }
1212
+ const spec = {
1213
+ kind: "delete",
1214
+ table: pivotTable,
1215
+ wheres,
1216
+ returning: [],
1217
+ };
1218
+ const compiled = compileStatementNative(spec, dialect);
1219
+ await db.execute(compiled.statements[0], compiled.params);
1220
+ };
1221
+ const attach = async (ids) => {
1222
+ const entries = normalizeAttach(ids);
1223
+ if (entries.length === 0)
1224
+ return;
1225
+ const ts = resolveTimestamps();
1226
+ // Normalize heterogeneous extras: compute the union of extra keys
1227
+ // across all entries and back-fill missing keys with `null`, so every
1228
+ // row in the multi-insert shares the same column set (required by the
1229
+ // Rust compiler's homogeneity check).
1230
+ const extraKeys = new Set();
1231
+ for (const e of entries) {
1232
+ for (const k of Object.keys(e.extras))
1233
+ extraKeys.add(k);
1234
+ }
1235
+ // Reject extras keys that collide with reserved pivot columns. Without
1236
+ // this guard, an extras entry named after the FK or a timestamp column
1237
+ // would emit a duplicate column in the INSERT row pair: the FK case
1238
+ // silently overrides `parentIdValue` (corrupting the join); the
1239
+ // timestamp case duplicates the column entirely (driver-dependent
1240
+ // failure or last-wins overwrite).
1241
+ for (const k of extraKeys) {
1242
+ if (k === pivotFk || k === pivotOther) {
1243
+ throw new Error(`Pivot extras key '${k}' collides with the ${k === pivotFk ? "foreignKey" : "otherKey"} column on '${pivotTable}'. Reserved keys MUST NOT appear in attach()/sync() extras.`);
1244
+ }
1245
+ if (Object.hasOwn(ts, k)) {
1246
+ throw new Error(`Pivot extras key '${k}' collides with a pivotTimestamps column on '${pivotTable}'. Disable the timestamp in the relation options or rename your extra.`);
1247
+ }
1248
+ }
1249
+ const rowPairs = entries.map((e) => {
1250
+ const pairs = [
1251
+ [pivotFk, parentIdValue],
1252
+ [pivotOther, e.id],
1253
+ ];
1254
+ for (const k of extraKeys) {
1255
+ const raw = e.extras[k] ?? null;
1256
+ const prepare = pivotAdapters?.[k]?.prepare;
1257
+ if (!prepare) {
1258
+ pairs.push([k, raw]);
1259
+ continue;
1260
+ }
1261
+ let encoded;
1262
+ try {
1263
+ encoded = prepare(raw);
1264
+ }
1265
+ catch (err) {
1266
+ throw wrapAdapterError("prepare", k, err);
1267
+ }
1268
+ assertNotPromise("prepare", k, encoded);
1269
+ pairs.push([k, encoded]);
1270
+ }
1271
+ for (const [k, v] of Object.entries(ts))
1272
+ pairs.push([k, v]);
1273
+ return pairs;
1274
+ });
1275
+ // Postgres casts for the pivot row: the two FK columns reference the
1276
+ // parent / related PK types (often uuid), and pivot timestamps are
1277
+ // bound strings — all need `$N::<type>` on Postgres.
1278
+ const pivotCasts = {};
1279
+ const parentPkCast = this.#castTypes[camelToSnake(this.#primaryKey)];
1280
+ if (parentPkCast)
1281
+ pivotCasts[pivotFk] = parentPkCast;
1282
+ const relatedPkCast = computeCastTypes(relatedClass)[camelToSnake(getPrimaryKey(relatedClass) ?? "id")];
1283
+ if (relatedPkCast)
1284
+ pivotCasts[pivotOther] = relatedPkCast;
1285
+ for (const k of Object.keys(ts))
1286
+ pivotCasts[k] = "timestamp";
1287
+ const spec = {
1288
+ kind: "insert",
1289
+ table: pivotTable,
1290
+ rows: rowPairs,
1291
+ casts: pivotCasts,
1292
+ };
1293
+ const compiled = compileStatementNative(spec, dialect);
1294
+ await db.execute(compiled.statements[0], compiled.params);
1295
+ };
1296
+ /**
1297
+ * Diff the current pivot state against a target set and apply the
1298
+ * minimum attach/detach to converge.
1299
+ *
1300
+ * **NOT ATOMIC.** `sync` reads the pivot, computes the diff, then
1301
+ * writes — another process mutating the pivot between the read and
1302
+ * the writes will cause divergence. Wrap the call in a transaction
1303
+ * if you need strong consistency under concurrent writers.
1304
+ *
1305
+ * On SQLite this is typically fine because better-sqlite3 serializes
1306
+ * writes per connection; on Postgres/MySQL use `useTransaction` first.
1307
+ */
1308
+ const sync = async (target, additive = false) => {
1309
+ const current = new Set(await currentIds());
1310
+ const entries = normalizeAttach(target);
1311
+ const desired = new Set(entries.map((e) => e.id));
1312
+ const toAttach = entries.filter((e) => !current.has(e.id));
1313
+ const toDetach = additive
1314
+ ? []
1315
+ : [...current].filter((id) => !desired.has(id));
1316
+ if (toDetach.length > 0)
1317
+ await detach(toDetach);
1318
+ if (toAttach.length > 0) {
1319
+ const attachArg = {};
1320
+ for (const e of toAttach)
1321
+ attachArg[String(e.id)] = e.extras;
1322
+ await attach(attachArg);
1323
+ }
1324
+ };
1325
+ const proxy = {
1326
+ type: "manyToMany",
1327
+ ...hasOps,
1328
+ query: scopedQuery,
1329
+ attach,
1330
+ detach,
1331
+ sync,
1332
+ };
1333
+ return proxy;
1334
+ }
1335
+ // Default: hasOne / hasMany
1336
+ if (relation.type === "hasOne") {
1337
+ // @HasOne is a one-to-one relation — createMany/saveMany would violate
1338
+ // the invariant at the ORM level (and silently shadow a missing UNIQUE
1339
+ // constraint at the DB level). The typed proxy declares them as
1340
+ // `Promise<never>` so callers get a compile-time signal; at runtime
1341
+ // both throw a clear error.
1342
+ const reject = async (op) => {
1343
+ throw new Error(`related('${relationName}').${op}() is not supported on @HasOne — ` +
1344
+ `use .create() / .save() for a single related row.`);
1345
+ };
1346
+ const proxy = {
1347
+ type: "hasOne",
1348
+ create: hasOps.create,
1349
+ save: hasOps.save,
1350
+ createMany: () => reject("createMany"),
1351
+ saveMany: () => reject("saveMany"),
1352
+ query: scopedQuery,
1353
+ };
1354
+ return proxy;
1355
+ }
1356
+ const proxy = {
1357
+ type: "hasMany",
1358
+ ...hasOps,
1359
+ query: scopedQuery,
1360
+ };
1361
+ return proxy;
1362
+ }
1363
+ async fresh(entity) {
1364
+ const pk = entity[this.#primaryKey];
1365
+ if (pk === undefined || pk === null) {
1366
+ throw new EntityNotFoundError(this.#entityClass.name, {
1367
+ [this.#primaryKey]: pk,
1368
+ });
1369
+ }
1370
+ const found = await this.find(pk);
1371
+ if (!found) {
1372
+ throw new EntityNotFoundError(this.#entityClass.name, {
1373
+ [this.#primaryKey]: pk,
1374
+ });
1375
+ }
1376
+ return found;
1377
+ }
1378
+ #entityToRow(entity) {
1379
+ // `#columns` already includes the primary-key property: `@PrimaryKey()`
1380
+ // internally calls `@Column()` to register the PK as a regular column
1381
+ // (see decorators/entity.ts). The earlier trailing block re-emitted
1382
+ // the PK as a raw camelCase key, producing a double-write for non-`id`
1383
+ // PK names (`{ user_id: ..., userId: ... }` would land in the row dict).
1384
+ const row = {};
1385
+ for (const col of this.#columns) {
1386
+ const value = entity[col];
1387
+ if (value !== undefined) {
1388
+ row[camelToSnake(col)] = this.#applyPrepare(col, value);
1389
+ }
1390
+ }
1391
+ return row;
1392
+ }
1393
+ #buildSetPairs(data) {
1394
+ const pairs = [];
1395
+ for (const [key, value] of Object.entries(data)) {
1396
+ // Mirror `#plainToRowPairs` — skip undefined so updates can't bind it.
1397
+ if (value === undefined)
1398
+ continue;
1399
+ const propKey = this.#columnPrepares.has(key) ? key : snakeToCamel(key);
1400
+ pairs.push([
1401
+ this.#resolveColumn(key),
1402
+ this.#applyPrepare(propKey, value),
1403
+ ]);
1404
+ }
1405
+ return pairs;
1406
+ }
1407
+ /**
1408
+ * Build SET pairs for `increment`/`decrement` — each entry carries a
1409
+ * `{ op, value }` payload that the Rust compiler turns into
1410
+ * `SET col = col ± ?` instead of the standard `SET col = ?`.
1411
+ */
1412
+ #buildIncrementPairs(columnOrMap, amount, op) {
1413
+ if (typeof columnOrMap === "string") {
1414
+ return [[this.#resolveColumn(columnOrMap), { op, value: amount }]];
1415
+ }
1416
+ return Object.entries(columnOrMap).map(([col, delta]) => [this.#resolveColumn(col), { op, value: delta }]);
1417
+ }
1418
+ }
1419
+ /**
1420
+ * Annotate an adapter callback failure with the property key that triggered
1421
+ * it. Without this, a `prepare`/`consume` throwing on row N silently surfaces
1422
+ * as "Invalid bind value" or similar, with no hint at WHICH column the
1423
+ * adapter rejected — the dev has to bisect across every adapter-tagged
1424
+ * property to find the culprit.
1425
+ */
1426
+ function wrapAdapterError(phase, propertyKey, err) {
1427
+ const message = err instanceof Error ? err.message : String(err);
1428
+ // `cause: err` preserves the original error (and its stack) per ES2022
1429
+ // Error Cause. The wrapped Error keeps its own `stack` pointing at the
1430
+ // wrap site so `console.error(wrapped)` shows the column-annotated
1431
+ // header; Node ≥16.9 walks the cause chain to print the underlying
1432
+ // throw's stack underneath.
1433
+ return new Error(`@Column.${phase} threw on '${propertyKey}': ${message}`, {
1434
+ cause: err,
1435
+ });
1436
+ }
1437
+ /**
1438
+ * Adapter callbacks must be synchronous — the bind layer cannot await before
1439
+ * handing values to the Rust DML compiler. Catching an `async` adapter here
1440
+ * gives the user a column-annotated error instead of an opaque "Invalid bind
1441
+ * value" downstream when the unawaited Promise hits the NAPI boundary.
1442
+ */
1443
+ function assertNotPromise(phase, propertyKey, value) {
1444
+ if (value !== null &&
1445
+ typeof value === "object" &&
1446
+ "then" in value &&
1447
+ typeof Reflect.get(value, "then") === "function") {
1448
+ throw new Error(`@Column.${phase} on '${propertyKey}' returned a Promise — adapters must be synchronous (the bind layer cannot await).`);
1449
+ }
1450
+ }
1451
+ //# sourceMappingURL=BaseRepository.js.map