@danceroutine/tango-orm 1.6.0 → 1.8.0

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 (84) hide show
  1. package/dist/InternalDialect-ClSaUNso.js +10 -0
  2. package/dist/InternalDialect-ClSaUNso.js.map +1 -0
  3. package/dist/PostgresAdapter-CXKdKBG-.js +4 -0
  4. package/dist/PostgresAdapter-DySFW6vy.js +128 -0
  5. package/dist/PostgresAdapter-DySFW6vy.js.map +1 -0
  6. package/dist/{SqliteClient-CjOK9-ki.js → SqliteAdapter-CDdOjRmW.js} +57 -3
  7. package/dist/SqliteAdapter-CDdOjRmW.js.map +1 -0
  8. package/dist/SqliteAdapter-mjtXuVTg.js +4 -0
  9. package/dist/connection/adapters/Adapter.d.ts +32 -1
  10. package/dist/connection/adapters/dialects/PostgresAdapter.d.ts +5 -6
  11. package/dist/connection/adapters/dialects/SqliteAdapter.d.ts +4 -6
  12. package/dist/connection/adapters/index.d.ts +1 -1
  13. package/dist/connection/index.d.ts +1 -1
  14. package/dist/connection/index.js +4 -5
  15. package/dist/{connection-B_K2ZAf7.js → connection-Dmhgx31M.js} +5 -7
  16. package/dist/{connection-B_K2ZAf7.js.map → connection-Dmhgx31M.js.map} +1 -1
  17. package/dist/{defaultRuntime-BPK9kWEW.js → defaultRuntime-DzqBQ9Hb.js} +63 -16
  18. package/dist/defaultRuntime-DzqBQ9Hb.js.map +1 -0
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +11 -12
  21. package/dist/manager/ManagerLike.d.ts +19 -0
  22. package/dist/manager/ModelManager.d.ts +45 -2
  23. package/dist/manager/index.d.ts +6 -0
  24. package/dist/manager/index.js +8 -7
  25. package/dist/manager/internal/MutationCompiler.d.ts +14 -6
  26. package/dist/manager/relations/ManyToManyRelatedManager.d.ts +147 -0
  27. package/dist/manager/relations/ManyToManyRelatedQuerySet.d.ts +62 -0
  28. package/dist/manager/relations/MaterializedModelRecord.d.ts +28 -0
  29. package/dist/manager/relations/index.d.ts +9 -0
  30. package/dist/manager/relations/internal/ThroughTableManager.d.ts +79 -0
  31. package/dist/manager-DrDTiCAz.js +24 -0
  32. package/dist/manager-DrDTiCAz.js.map +1 -0
  33. package/dist/query/ModelQuerySet.d.ts +20 -0
  34. package/dist/query/QBuilder.d.ts +3 -3
  35. package/dist/query/QuerySet.d.ts +58 -18
  36. package/dist/query/compiler/QueryCompiler.d.ts +13 -4
  37. package/dist/query/domain/CompiledQuery.d.ts +169 -2
  38. package/dist/query/domain/FilterInput.d.ts +1 -1
  39. package/dist/query/domain/FilterKey.d.ts +4 -2
  40. package/dist/query/domain/QNode.d.ts +4 -4
  41. package/dist/query/domain/QuerySetState.d.ts +3 -3
  42. package/dist/query/domain/RelationMeta.d.ts +9 -0
  43. package/dist/query/domain/RelationTyping.d.ts +47 -0
  44. package/dist/query/domain/TableMetaFactory.d.ts +1 -14
  45. package/dist/query/domain/index.d.ts +1 -1
  46. package/dist/query/domain/internal/InternalPrefetchQueryKind.d.ts +20 -0
  47. package/dist/query/index.d.ts +1 -0
  48. package/dist/query/index.js +3 -2
  49. package/dist/query/internal/isQNodeLike.d.ts +3 -0
  50. package/dist/query/planning/QueryPlanner.d.ts +1 -1
  51. package/dist/{query-C6So1r6H.js → query-DUZnBFhf.js} +474 -156
  52. package/dist/query-DUZnBFhf.js.map +1 -0
  53. package/dist/registerModelObjects-DxlBfuUN.js +797 -0
  54. package/dist/registerModelObjects-DxlBfuUN.js.map +1 -0
  55. package/dist/runtime/TangoRuntime.d.ts +9 -0
  56. package/dist/runtime/index.d.ts +3 -2
  57. package/dist/runtime/index.js +7 -6
  58. package/dist/runtime/internal/SqliteDBClientProvider.d.ts +3 -0
  59. package/dist/{runtime-ByXbpVBS.js → runtime-1H88J3nN.js} +3 -3
  60. package/dist/runtime-1H88J3nN.js.map +1 -0
  61. package/dist/transaction/index.js +5 -4
  62. package/dist/{transaction-Cs0Z9tbW.js → transaction-ZhfDf-f8.js} +2 -2
  63. package/dist/{transaction-Cs0Z9tbW.js.map → transaction-ZhfDf-f8.js.map} +1 -1
  64. package/dist/validation/SQLValidationEngine.d.ts +22 -5
  65. package/dist/validation/SqlValidationPlan.d.ts +5 -4
  66. package/dist/validation/internal/InternalSqlValidationPlanKind.d.ts +25 -0
  67. package/dist/validation/internal/InternalValidatedFilterDescriptorKind.d.ts +4 -0
  68. package/package.json +6 -6
  69. package/dist/PostgresAdapter-BFdo_nIt.js +0 -4
  70. package/dist/PostgresAdapter-CMiEpHya.js +0 -49
  71. package/dist/PostgresAdapter-CMiEpHya.js.map +0 -1
  72. package/dist/PostgresClient-BQJZfEOT.js +0 -68
  73. package/dist/PostgresClient-BQJZfEOT.js.map +0 -1
  74. package/dist/SqliteAdapter-A-P9zUhP.js +0 -4
  75. package/dist/SqliteAdapter-CeqhyrPC.js +0 -44
  76. package/dist/SqliteAdapter-CeqhyrPC.js.map +0 -1
  77. package/dist/SqliteClient-CjOK9-ki.js.map +0 -1
  78. package/dist/defaultRuntime-BPK9kWEW.js.map +0 -1
  79. package/dist/manager-C6oJ2tAF.js +0 -13
  80. package/dist/manager-C6oJ2tAF.js.map +0 -1
  81. package/dist/query-C6So1r6H.js.map +0 -1
  82. package/dist/registerModelObjects-BKMpfc4Z.js +0 -263
  83. package/dist/registerModelObjects-BKMpfc4Z.js.map +0 -1
  84. package/dist/runtime-ByXbpVBS.js.map +0 -1
@@ -0,0 +1,797 @@
1
+ import { InternalQNodeType, InternalRelationKind, InternalSqlValidationPlanKind, ModelQuerySet, OrmSqlSafetyAdapter, QBuilder, QueryResult, QuerySet, TableMetaFactory, isQNodeLike } from "./query-DUZnBFhf.js";
2
+ import { RuntimeBoundClient, TransactionEngine, getTangoRuntime } from "./defaultRuntime-DzqBQ9Hb.js";
3
+ import { NotFoundError } from "@danceroutine/tango-core";
4
+ import { ModelRegistry, registerModelAugmentor } from "@danceroutine/tango-schema";
5
+
6
+ //#region src/manager/relations/ManyToManyRelatedQuerySet.ts
7
+ function applyShape(rows, shape) {
8
+ return typeof shape === "function" ? rows.map(shape) : rows.map((row) => shape.parse(row));
9
+ }
10
+ var ManyToManyRelatedQuerySet = class ManyToManyRelatedQuerySet extends QuerySet {
11
+ constructor(executor, bridge, state = {}) {
12
+ super(executor, state);
13
+ this.bridge = bridge;
14
+ }
15
+ async fetch(shape) {
16
+ if (this.isStateTrivial()) {
17
+ const cache = this.bridge.getCache();
18
+ if (cache !== null) {
19
+ const results = shape ? applyShape(cache, shape) : [...cache];
20
+ return new QueryResult(results);
21
+ }
22
+ }
23
+ const ids = await this.bridge.fetchTargetIds();
24
+ if (ids.length === 0) return new QueryResult([]);
25
+ const scopedQs = new ModelQuerySet(this.executor, this.scopedState(ids));
26
+ return shape ? scopedQs.fetch(shape) : scopedQs.fetch();
27
+ }
28
+ async fetchOne(shape) {
29
+ const result = shape ? await this.fetch(shape) : await this.fetch();
30
+ return result.items[0] ?? null;
31
+ }
32
+ async count() {
33
+ if (this.isStateTrivial()) {
34
+ const cache = this.bridge.getCache();
35
+ if (cache !== null) return cache.length;
36
+ }
37
+ const ids = await this.bridge.fetchTargetIds();
38
+ if (ids.length === 0) return 0;
39
+ if (this.isStateTrivial()) return ids.length;
40
+ const scopedQs = new ModelQuerySet(this.executor, this.scopedState(ids));
41
+ return scopedQs.count();
42
+ }
43
+ spawn(state) {
44
+ return new ManyToManyRelatedQuerySet(this.executor, this.bridge, state);
45
+ }
46
+ isStateTrivial() {
47
+ return Object.keys(this.state).length === 0;
48
+ }
49
+ scopedState(ids) {
50
+ const inFilter = { [`${this.bridge.targetPrimaryKeyField}__in`]: [...ids] };
51
+ const inAtom = {
52
+ kind: InternalQNodeType.ATOM,
53
+ where: inFilter
54
+ };
55
+ const merged = this.state.q ? QBuilder.and(inAtom, this.state.q) : inAtom;
56
+ return {
57
+ ...this.state,
58
+ q: merged
59
+ };
60
+ }
61
+ };
62
+
63
+ //#endregion
64
+ //#region src/manager/internal/MutationCompiler.ts
65
+ const InternalDuplicateInsertPolicy = {
66
+ ERROR: "error",
67
+ IGNORE: "ignore"
68
+ };
69
+ var MutationCompiler = class {
70
+ adapter;
71
+ placeholders;
72
+ constructor(adapter) {
73
+ this.adapter = adapter;
74
+ this.placeholders = adapter.placeholders;
75
+ }
76
+ compileInsert(plan, values) {
77
+ return {
78
+ sql: `INSERT INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES (${this.placeholders.list(plan.writeKeys.length)}) RETURNING *`,
79
+ params: values
80
+ };
81
+ }
82
+ compileUpdate(plan, values, id) {
83
+ const sets = plan.writeKeys.map((key, index) => `${key} = ${this.placeholders.at(index + 1)}`).join(", ");
84
+ const whereParam = this.placeholders.at(plan.writeKeys.length + 1);
85
+ return {
86
+ sql: `UPDATE ${plan.meta.table} SET ${sets} WHERE ${plan.meta.pk} = ${whereParam} RETURNING *`,
87
+ params: [...values, id]
88
+ };
89
+ }
90
+ compileDelete(plan, id) {
91
+ return {
92
+ sql: `DELETE FROM ${plan.meta.table} WHERE ${plan.meta.pk} = ${this.placeholders.at(1)}`,
93
+ params: [id]
94
+ };
95
+ }
96
+ compileDeleteByJoinKeys(plan, leftFilterKey, rightFilterKey, leftValue, rightValue) {
97
+ const leftDescriptor = plan.filterKeys[leftFilterKey];
98
+ const rightDescriptor = plan.filterKeys[rightFilterKey];
99
+ if (!leftDescriptor || !rightDescriptor) throw new Error(`MutationCompiler.compileDeleteByJoinKeys: filter keys '${leftFilterKey}' and '${rightFilterKey}' must be present on the validated plan.`);
100
+ return {
101
+ sql: `DELETE FROM ${plan.meta.table} WHERE ${leftDescriptor.field} = ${this.placeholders.at(1)} AND ${rightDescriptor.field} = ${this.placeholders.at(2)}`,
102
+ params: [leftValue, rightValue]
103
+ };
104
+ }
105
+ compileBulkInsert(plan, valueRows) {
106
+ const columnCount = plan.writeKeys.length;
107
+ const placeholders = valueRows.map((_row, rowIndex) => `(${this.placeholders.listFromOffset(columnCount, rowIndex * columnCount)})`).join(", ");
108
+ return {
109
+ sql: `INSERT INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES ${placeholders} RETURNING *`,
110
+ params: valueRows.flat()
111
+ };
112
+ }
113
+ compileInsertJoinLinks(plan, sourceKey, targetKey, ownerValue, targetValues, duplicatePolicy) {
114
+ const valueRows = targetValues.map((targetValue) => [ownerValue, targetValue]);
115
+ const placeholders = valueRows.map((_row, rowIndex) => `(${this.placeholders.listFromOffset(2, rowIndex * 2)})`).join(", ");
116
+ const params = valueRows.flat();
117
+ if (duplicatePolicy === InternalDuplicateInsertPolicy.IGNORE) switch (this.adapter.dialect) {
118
+ case "postgres": return {
119
+ sql: `INSERT INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES ${placeholders} ON CONFLICT (${sourceKey}, ${targetKey}) DO NOTHING`,
120
+ params
121
+ };
122
+ case "sqlite": return {
123
+ sql: `INSERT OR IGNORE INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES ${placeholders}`,
124
+ params
125
+ };
126
+ }
127
+ return {
128
+ sql: `INSERT INTO ${plan.meta.table} (${plan.writeKeys.join(", ")}) VALUES ${placeholders}`,
129
+ params
130
+ };
131
+ }
132
+ compileDeleteJoinLinks(plan, leftFilterKey, rightFilterKey, leftValue, rightValues) {
133
+ const leftDescriptor = plan.filterKeys[leftFilterKey];
134
+ const rightDescriptor = plan.filterKeys[rightFilterKey];
135
+ if (!leftDescriptor || !rightDescriptor) throw new Error(`MutationCompiler.compileDeleteJoinLinks: filter keys '${leftFilterKey}' and '${rightFilterKey}' must be present on the validated plan.`);
136
+ if (rightValues.length === 0) throw new Error("MutationCompiler.compileDeleteJoinLinks requires at least one target value.");
137
+ if (rightValues.length === 1) return {
138
+ sql: `DELETE FROM ${plan.meta.table} WHERE ${leftDescriptor.field} = ${this.placeholders.at(1)} AND ${rightDescriptor.field} = ${this.placeholders.at(2)}`,
139
+ params: [leftValue, rightValues[0]]
140
+ };
141
+ return {
142
+ sql: `DELETE FROM ${plan.meta.table} WHERE ${leftDescriptor.field} = ${this.placeholders.at(1)} AND ${rightDescriptor.field} IN (${this.placeholders.listFromOffset(rightValues.length, 1)})`,
143
+ params: [leftValue, ...rightValues]
144
+ };
145
+ }
146
+ };
147
+
148
+ //#endregion
149
+ //#region src/manager/relations/internal/ThroughTableManager.ts
150
+ var ThroughTableManager = class ThroughTableManager {
151
+ constructor(client, mutationCompiler, descriptor, adapter, sqlSafetyAdapter$1 = new OrmSqlSafetyAdapter()) {
152
+ this.client = client;
153
+ this.mutationCompiler = mutationCompiler;
154
+ this.descriptor = descriptor;
155
+ this.adapter = adapter;
156
+ this.sqlSafetyAdapter = sqlSafetyAdapter$1;
157
+ }
158
+ /**
159
+ * Derive a {@link ThroughTableLinkDescriptor} from the through-model
160
+ * metadata exposed by a many-to-many relation edge.
161
+ */
162
+ static buildLinkDescriptor(relation, throughModelFields) {
163
+ if (!relation.throughTable || !relation.throughSourceKey || !relation.throughTargetKey) throw new Error("Cannot derive a through-table descriptor from a relation that is not a persisted many-to-many edge.");
164
+ const primaryKeyField = throughModelFields.find((field) => field.primaryKey);
165
+ if (!primaryKeyField) throw new Error("Through-model metadata is missing a primary-key field.");
166
+ return {
167
+ table: relation.throughTable,
168
+ primaryKey: primaryKeyField.name,
169
+ columns: Object.fromEntries(throughModelFields.map((field) => [field.name, field.type])),
170
+ sourceColumn: relation.throughSourceKey,
171
+ targetColumn: relation.throughTargetKey
172
+ };
173
+ }
174
+ /**
175
+ * Convenience factory that derives the join-table descriptor from the
176
+ * relation edge and instantiates a {@link ThroughTableManager} in one
177
+ * step.
178
+ */
179
+ static fromRelation(inputs) {
180
+ return new ThroughTableManager(inputs.client, inputs.mutationCompiler, ThroughTableManager.buildLinkDescriptor(inputs.relation, inputs.throughModelFields), inputs.adapter, inputs.sqlSafetyAdapter);
181
+ }
182
+ /**
183
+ * Read every target primary-key value linked to the supplied owner via
184
+ * the join table. Used by ManyToManyRelatedManager.all to scope
185
+ * follow-up target queries to the current owner.
186
+ */
187
+ async selectTargetIdsForOwner(ownerPrimaryKey) {
188
+ const validated = this.sqlSafetyAdapter.validate({
189
+ kind: InternalSqlValidationPlanKind.SELECT,
190
+ meta: {
191
+ table: this.descriptor.table,
192
+ pk: this.descriptor.primaryKey,
193
+ columns: this.descriptor.columns
194
+ },
195
+ filterKeys: [this.descriptor.sourceColumn, this.descriptor.targetColumn]
196
+ });
197
+ const sourceColumn = validated.filterKeys[this.descriptor.sourceColumn].field;
198
+ const targetColumn = validated.filterKeys[this.descriptor.targetColumn].field;
199
+ const placeholder = this.adapter.placeholders.at(1);
200
+ const sql = `SELECT ${targetColumn} AS target_id FROM ${validated.meta.table} WHERE ${sourceColumn} = ${placeholder}`;
201
+ const result = await this.client.query(sql, [ownerPrimaryKey]);
202
+ return result.rows.map((row) => row.target_id).filter((value) => typeof value === "string" || typeof value === "number");
203
+ }
204
+ async insertLink(ownerPrimaryKey, targetPrimaryKey, options = {}) {
205
+ await this.insertLinks(ownerPrimaryKey, [targetPrimaryKey], options);
206
+ }
207
+ async insertLinks(ownerPrimaryKey, targetPrimaryKeys, options = {}) {
208
+ if (targetPrimaryKeys.length === 0) return;
209
+ const validatedPlan = this.sqlSafetyAdapter.validate({
210
+ kind: InternalSqlValidationPlanKind.INSERT,
211
+ meta: {
212
+ table: this.descriptor.table,
213
+ pk: this.descriptor.primaryKey,
214
+ columns: this.descriptor.columns
215
+ },
216
+ writeKeys: [this.descriptor.sourceColumn, this.descriptor.targetColumn]
217
+ });
218
+ const duplicatePolicy = options.onDuplicate ?? InternalDuplicateInsertPolicy.ERROR;
219
+ if (duplicatePolicy === InternalDuplicateInsertPolicy.IGNORE && !this.adapter.features.ignoreDuplicateInsert) throw new Error(`Adapter '${this.adapter.name}' does not support duplicate-safe link insertion for many-to-many writes.`);
220
+ const compiled = this.mutationCompiler.compileInsertJoinLinks(validatedPlan, this.descriptor.sourceColumn, this.descriptor.targetColumn, ownerPrimaryKey, targetPrimaryKeys, duplicatePolicy);
221
+ await this.client.query(compiled.sql, compiled.params);
222
+ }
223
+ async deleteLink(ownerPrimaryKey, targetPrimaryKey) {
224
+ await this.deleteLinks(ownerPrimaryKey, [targetPrimaryKey]);
225
+ }
226
+ async deleteLinks(ownerPrimaryKey, targetPrimaryKeys) {
227
+ if (targetPrimaryKeys.length === 0) return;
228
+ const validated = this.sqlSafetyAdapter.validate({
229
+ kind: InternalSqlValidationPlanKind.SELECT,
230
+ meta: {
231
+ table: this.descriptor.table,
232
+ pk: this.descriptor.primaryKey,
233
+ columns: this.descriptor.columns
234
+ },
235
+ filterKeys: [this.descriptor.sourceColumn, this.descriptor.targetColumn]
236
+ });
237
+ const compiled = this.mutationCompiler.compileDeleteJoinLinks(validated, this.descriptor.sourceColumn, this.descriptor.targetColumn, ownerPrimaryKey, targetPrimaryKeys);
238
+ await this.client.query(compiled.sql, compiled.params);
239
+ }
240
+ };
241
+
242
+ //#endregion
243
+ //#region src/manager/relations/ManyToManyRelatedManager.ts
244
+ var ManyToManyRelatedManager = class ManyToManyRelatedManager {
245
+ static BRAND = "tango.orm.m2m_related_manager";
246
+ __tangoBrand = ManyToManyRelatedManager.BRAND;
247
+ prefetchCache = null;
248
+ /**
249
+ * Constructor is internal. Application and ORM code must build instances
250
+ * through {@link ManyToManyRelatedManager.create}, which owns the wiring
251
+ * between relation metadata and the backing through-table mutator. The
252
+ * testing package exposes a fixture for unit tests that need a custom
253
+ * mutator.
254
+ */
255
+ constructor(inputs) {
256
+ this.inputs = inputs;
257
+ }
258
+ /**
259
+ * Narrow an unknown value to {@link ManyToManyRelatedManager}.
260
+ */
261
+ static isManyToManyRelatedManager(value) {
262
+ return typeof value === "object" && value !== null && value.__tangoBrand === ManyToManyRelatedManager.BRAND;
263
+ }
264
+ /**
265
+ * Build a {@link ManyToManyRelatedManager} bound to a single owner record.
266
+ * The factory derives the join-table descriptor from the relation edge and
267
+ * through-model fields, wires a {@link ThroughTableManager} against the
268
+ * supplied runtime-bound client, and returns a manager whose `add`,
269
+ * `remove`, and `all` methods enroll in any active
270
+ * `transaction.atomic(...)` boundary.
271
+ */
272
+ static create(inputs) {
273
+ const throughTableManager = ThroughTableManager.fromRelation({
274
+ relation: inputs.relation,
275
+ throughModelFields: inputs.throughModelFields,
276
+ client: inputs.client,
277
+ mutationCompiler: inputs.mutationCompiler,
278
+ adapter: inputs.adapter,
279
+ sqlSafetyAdapter: inputs.sqlSafetyAdapter
280
+ });
281
+ return new ManyToManyRelatedManager({
282
+ ownerPrimaryKey: inputs.ownerPrimaryKey,
283
+ relationName: inputs.relationName,
284
+ ownerModelLabel: inputs.ownerModelLabel,
285
+ targetPrimaryKeyField: inputs.relation.targetPrimaryKey,
286
+ throughTableManager,
287
+ targetExecutorProvider: inputs.targetExecutorProvider,
288
+ runAtomic: inputs.runAtomic
289
+ });
290
+ }
291
+ /**
292
+ * Insert join-table rows linking the owning record to the supplied
293
+ * targets. Duplicate links are ignored so repeated `add(...)` calls are
294
+ * idempotent. When multiple targets are supplied, Tango performs the
295
+ * membership write inside one `transaction.atomic(...)` boundary.
296
+ */
297
+ async add(...targets) {
298
+ const targetPrimaryKeys = this.resolveTargetPrimaryKeys(targets);
299
+ if (targetPrimaryKeys.length === 0) return;
300
+ if (targetPrimaryKeys.length === 1) await this.inputs.throughTableManager.insertLink(this.inputs.ownerPrimaryKey, targetPrimaryKeys[0], { onDuplicate: InternalDuplicateInsertPolicy.IGNORE });
301
+ else await this.inputs.runAtomic(() => this.inputs.throughTableManager.insertLinks(this.inputs.ownerPrimaryKey, targetPrimaryKeys, { onDuplicate: InternalDuplicateInsertPolicy.IGNORE }));
302
+ this.invalidateCache();
303
+ }
304
+ /**
305
+ * Delete join-table rows linking the owning record to the supplied
306
+ * targets. When multiple targets are supplied, Tango performs the
307
+ * membership write inside one `transaction.atomic(...)` boundary.
308
+ */
309
+ async remove(...targets) {
310
+ const targetPrimaryKeys = this.resolveTargetPrimaryKeys(targets);
311
+ if (targetPrimaryKeys.length === 0) return;
312
+ if (targetPrimaryKeys.length === 1) await this.inputs.throughTableManager.deleteLink(this.inputs.ownerPrimaryKey, targetPrimaryKeys[0]);
313
+ else await this.inputs.runAtomic(() => this.inputs.throughTableManager.deleteLinks(this.inputs.ownerPrimaryKey, targetPrimaryKeys));
314
+ this.invalidateCache();
315
+ }
316
+ /**
317
+ * Return a {@link QuerySet} for the related target rows of this many-to-many
318
+ * relation. When the relation was already loaded by `prefetchRelated(...)`,
319
+ * the first `fetch()` resolves with the cached materialization without
320
+ * re-querying. Mutating the membership through `add`/`remove` invalidates
321
+ * that cache.
322
+ */
323
+ all() {
324
+ const executor = this.inputs.targetExecutorProvider();
325
+ if (!executor) throw new Error(`Cannot resolve a target query executor for relation '${this.inputs.relationName}' on '${this.inputs.ownerModelLabel}'.`);
326
+ return new ManyToManyRelatedQuerySet(executor, {
327
+ getCache: () => this.prefetchCache,
328
+ fetchTargetIds: () => this.inputs.throughTableManager.selectTargetIdsForOwner(this.inputs.ownerPrimaryKey),
329
+ targetPrimaryKeyField: this.inputs.targetPrimaryKeyField
330
+ });
331
+ }
332
+ /**
333
+ * Replace the prefetch cache with the supplied target rows. Called by the
334
+ * many-to-many prefetch path so a follow-up `all()` resolves without an
335
+ * extra database round-trip.
336
+ */
337
+ primePrefetchCache(targets) {
338
+ this.prefetchCache = [...targets];
339
+ }
340
+ /**
341
+ * Drop any cached prefetch results. Mutating helpers call this so reads
342
+ * after an `add`/`remove` go back to the database.
343
+ */
344
+ invalidateCache() {
345
+ this.prefetchCache = null;
346
+ }
347
+ /**
348
+ * Snapshot of the current prefetch cache, exposed for diagnostics and
349
+ * focused unit testing. Returns a fresh array copy so callers cannot
350
+ * mutate the manager's internal state.
351
+ */
352
+ snapshotCache() {
353
+ return this.prefetchCache ? [...this.prefetchCache] : null;
354
+ }
355
+ resolveTargetPrimaryKeys(targets) {
356
+ const resolved = [];
357
+ const seen = new Set();
358
+ for (const target of targets) {
359
+ const primaryKey = this.resolveTargetPrimaryKey(target);
360
+ const canonical = this.canonicalizePrimaryKey(primaryKey);
361
+ if (seen.has(canonical)) continue;
362
+ seen.add(canonical);
363
+ resolved.push(primaryKey);
364
+ }
365
+ return resolved;
366
+ }
367
+ resolveTargetPrimaryKey(target) {
368
+ if (typeof target === "string" || typeof target === "number") return target;
369
+ if (typeof target === "object" && target !== null) {
370
+ const targetPrimaryKey = target[this.inputs.targetPrimaryKeyField];
371
+ if (targetPrimaryKey === undefined || targetPrimaryKey === null) throw new Error(`Cannot resolve target primary key '${this.inputs.targetPrimaryKeyField}' for relation '${this.inputs.relationName}' on '${this.inputs.ownerModelLabel}'.`);
372
+ return targetPrimaryKey;
373
+ }
374
+ throw new Error(`Unsupported target reference for relation '${this.inputs.relationName}' on '${this.inputs.ownerModelLabel}'. Expected a record, a primary-key carrier, or a primary-key value.`);
375
+ }
376
+ canonicalizePrimaryKey(primaryKey) {
377
+ switch (typeof primaryKey) {
378
+ case "string": return `string:${primaryKey}`;
379
+ case "number": return `number:${primaryKey}`;
380
+ case "bigint": return `bigint:${String(primaryKey)}`;
381
+ case "boolean": return `boolean:${primaryKey}`;
382
+ default: return `json:${JSON.stringify(primaryKey)}`;
383
+ }
384
+ }
385
+ };
386
+
387
+ //#endregion
388
+ //#region src/manager/ModelManager.ts
389
+ const sqlSafetyAdapter = new OrmSqlSafetyAdapter();
390
+ var ModelManager = class ModelManager {
391
+ static BRAND = "tango.orm.model_manager";
392
+ __tangoBrand = ModelManager.BRAND;
393
+ queryExecutor;
394
+ mutationCompiler;
395
+ model;
396
+ client;
397
+ adapter;
398
+ runtime;
399
+ constructor(model, runtime) {
400
+ this.model = model;
401
+ this.runtime = runtime;
402
+ this.client = new RuntimeBoundClient(runtime);
403
+ this.adapter = runtime.getAdapter();
404
+ this.mutationCompiler = new MutationCompiler(this.adapter);
405
+ this.queryExecutor = {
406
+ get meta() {
407
+ return ModelManager.createTableMeta(model);
408
+ },
409
+ client: this.client,
410
+ adapter: this.adapter,
411
+ run: async (compiled) => {
412
+ const result = await this.client.query(compiled.sql, compiled.params);
413
+ return result.rows;
414
+ },
415
+ attachPersistedRecordAccessors: (record, modelKey) => {
416
+ this.attachManyToManyRelatedManagers(record, modelKey ?? this.model.metadata.key);
417
+ }
418
+ };
419
+ }
420
+ get meta() {
421
+ return ModelManager.createTableMeta(this.model);
422
+ }
423
+ /**
424
+ * Narrow an unknown value to `ModelManager`.
425
+ */
426
+ static isModelManager(value) {
427
+ return typeof value === "object" && value !== null && value.__tangoBrand === ModelManager.BRAND;
428
+ }
429
+ static createTableMeta(model) {
430
+ const rawMeta = TableMetaFactory.create(model);
431
+ const validatedMeta = sqlSafetyAdapter.validate({
432
+ kind: InternalSqlValidationPlanKind.INSERT,
433
+ meta: rawMeta,
434
+ writeKeys: Object.keys(rawMeta.columns)
435
+ }).meta;
436
+ if (rawMeta.relations) validatedMeta.relations = rawMeta.relations;
437
+ return validatedMeta;
438
+ }
439
+ static mergeCreatePayloadFromWhere(modelName, pkColumn, where, defaults) {
440
+ const hasDefaultsArg = defaults !== undefined;
441
+ const providedDefaults = defaults !== undefined ? { ...defaults } : undefined;
442
+ if (isQNodeLike(where)) {
443
+ if (!hasDefaultsArg || !providedDefaults || Object.keys(providedDefaults).length === 0) throw new Error(`Cannot create ${modelName} from Q filters without defaults.`);
444
+ const fromQ = ModelManager.collectPlainFieldsFromQNode(modelName, where);
445
+ const merged$1 = {
446
+ ...fromQ,
447
+ ...providedDefaults
448
+ };
449
+ const keys$1 = Object.keys(merged$1).filter((key) => merged$1[key] !== undefined);
450
+ const nonPkKeys$1 = keys$1.filter((key) => key !== pkColumn);
451
+ if (nonPkKeys$1.length === 0) throw new Error(`Cannot create ${modelName} without any values.`);
452
+ return merged$1;
453
+ }
454
+ const atom = where;
455
+ const entries = Object.entries(atom);
456
+ const plainEntries = entries.filter(([key]) => !String(key).includes("__"));
457
+ const lookupOnly = entries.length > 0 && plainEntries.length === 0;
458
+ const fromAtom = plainEntries.length > 0 ? Object.fromEntries(plainEntries) : undefined;
459
+ const mergeBase = {
460
+ ...fromAtom,
461
+ ...providedDefaults
462
+ };
463
+ if (lookupOnly && ModelManager.countDefinedValues(mergeBase, pkColumn) === 0) throw new Error(`Cannot create ${modelName} from lookup-only filters without defaults.`);
464
+ const cleanedAtom = plainEntries.length > 0 ? Object.fromEntries(plainEntries) : {};
465
+ const merged = {
466
+ ...cleanedAtom,
467
+ ...mergeBase
468
+ };
469
+ const keys = Object.keys(merged).filter((key) => merged[key] !== undefined);
470
+ const nonPkKeys = keys.filter((key) => key !== pkColumn);
471
+ if (nonPkKeys.length === 0) throw new Error(`Cannot create ${modelName} without any values.`);
472
+ return merged;
473
+ }
474
+ static countDefinedValues(payload, pkColumn) {
475
+ return Object.entries(payload).filter(([key, value]) => key !== pkColumn && value !== undefined).length;
476
+ }
477
+ static collectPlainFieldsFromQNode(modelName, node) {
478
+ switch (node.kind) {
479
+ case InternalQNodeType.ATOM: return ModelManager.omitLookupKeysFromAtom(node.where);
480
+ case InternalQNodeType.AND: {
481
+ const partials = (node.nodes ?? []).map((child) => ModelManager.collectPlainFieldsFromQNode(modelName, child));
482
+ return ModelManager.mergeCompatiblePartials(partials);
483
+ }
484
+ case InternalQNodeType.OR: {
485
+ const partials = (node.nodes ?? []).map((child) => ModelManager.collectPlainFieldsFromQNode(modelName, child));
486
+ const nonEmpty = partials.filter((partial) => Object.keys(partial).length > 0);
487
+ if (nonEmpty.length > 1) throw new Error(`Cannot derive a create payload from ${modelName} OR filters with multiple predicates. Supply defaults that fully describe the insert.`);
488
+ return nonEmpty.length === 1 ? nonEmpty[0] : {};
489
+ }
490
+ case InternalQNodeType.NOT: return {};
491
+ }
492
+ }
493
+ static omitLookupKeysFromAtom(atom) {
494
+ const entries = Object.entries(atom).filter(([key]) => !String(key).includes("__"));
495
+ return Object.fromEntries(entries);
496
+ }
497
+ static mergeCompatiblePartials(partials) {
498
+ const merged = {};
499
+ for (const partial of partials) for (const [key, value] of Object.entries(partial)) {
500
+ const existing = merged[key];
501
+ if (existing !== undefined && existing !== value) throw new Error(`Conflicting values for '${key}' while deriving create payload from Q filters.`);
502
+ merged[key] = value;
503
+ }
504
+ return merged;
505
+ }
506
+ query() {
507
+ return new ModelQuerySet(this.queryExecutor, {});
508
+ }
509
+ all() {
510
+ return this.query();
511
+ }
512
+ async findById(id) {
513
+ const filter = { [this.meta.pk]: id };
514
+ return this.query().filter(filter).fetchOne();
515
+ }
516
+ async getOrThrow(id) {
517
+ const result = await this.findById(id);
518
+ if (!result) throw new NotFoundError(`${this.model.metadata.name} with ${this.meta.pk}=${String(id)} not found`);
519
+ return result;
520
+ }
521
+ async getOrCreate(args) {
522
+ try {
523
+ const record$1 = await this.query().get(args.where);
524
+ return {
525
+ record: record$1,
526
+ created: false
527
+ };
528
+ } catch (error) {
529
+ if (!NotFoundError.isNotFoundError(error)) throw error;
530
+ }
531
+ const merged = ModelManager.mergeCreatePayloadFromWhere(this.model.metadata.name, this.meta.pk, args.where, args.defaults);
532
+ const record = await this.create(merged);
533
+ return {
534
+ record,
535
+ created: true
536
+ };
537
+ }
538
+ async updateOrCreate(args) {
539
+ let existing = null;
540
+ try {
541
+ existing = await this.query().get(args.where);
542
+ } catch (error) {
543
+ if (!NotFoundError.isNotFoundError(error)) throw error;
544
+ }
545
+ if (!existing) {
546
+ const merged = ModelManager.mergeCreatePayloadFromWhere(this.model.metadata.name, this.meta.pk, args.where, args.defaults);
547
+ const record$1 = await this.create(merged);
548
+ return {
549
+ record: record$1,
550
+ created: true,
551
+ updated: false
552
+ };
553
+ }
554
+ const patch = args.update ?? args.defaults ?? {};
555
+ if (Object.keys(patch).length === 0) return {
556
+ record: existing,
557
+ created: false,
558
+ updated: false
559
+ };
560
+ const id = existing[this.meta.pk];
561
+ const record = await this.update(id, patch);
562
+ return {
563
+ record,
564
+ created: false,
565
+ updated: true
566
+ };
567
+ }
568
+ async create(input) {
569
+ const prepared = await this.runBeforeCreate(input);
570
+ const preparedKeys = Object.keys(prepared);
571
+ if (preparedKeys.length === 0) throw new Error(`Cannot create ${this.model.metadata.name} without any values.`);
572
+ const validatedPlan = sqlSafetyAdapter.validate({
573
+ kind: InternalSqlValidationPlanKind.INSERT,
574
+ meta: this.meta,
575
+ writeKeys: preparedKeys
576
+ });
577
+ const compiled = this.mutationCompiler.compileInsert(validatedPlan, preparedKeys.map((key) => prepared[key]));
578
+ const result = await this.queryExecutor.client.query(compiled.sql, compiled.params);
579
+ const created = result.rows[0];
580
+ this.attachOwnRelatedManagers(created);
581
+ await this.model.hooks?.afterCreate?.({
582
+ record: created,
583
+ model: this.model,
584
+ manager: this,
585
+ transaction: this.getHookTransaction()
586
+ });
587
+ return created;
588
+ }
589
+ async update(id, patch) {
590
+ const current = await this.getOrThrow(id);
591
+ const prepared = await this.runBeforeUpdate(id, patch, current);
592
+ const preparedKeys = Object.keys(prepared);
593
+ if (preparedKeys.length === 0) throw new Error(`Cannot update ${this.model.metadata.name} without any values.`);
594
+ const validatedPlan = sqlSafetyAdapter.validate({
595
+ kind: InternalSqlValidationPlanKind.UPDATE,
596
+ meta: this.meta,
597
+ writeKeys: preparedKeys
598
+ });
599
+ const compiled = this.mutationCompiler.compileUpdate(validatedPlan, preparedKeys.map((key) => prepared[key]), id);
600
+ const result = await this.queryExecutor.client.query(compiled.sql, compiled.params);
601
+ const updated = result.rows[0];
602
+ this.attachOwnRelatedManagers(updated);
603
+ await this.model.hooks?.afterUpdate?.({
604
+ id,
605
+ patch: prepared,
606
+ previous: current,
607
+ record: updated,
608
+ model: this.model,
609
+ manager: this,
610
+ transaction: this.getHookTransaction()
611
+ });
612
+ return updated;
613
+ }
614
+ async delete(id) {
615
+ const current = await this.getOrThrow(id);
616
+ this.attachOwnRelatedManagers(current);
617
+ await this.model.hooks?.beforeDelete?.({
618
+ id,
619
+ current,
620
+ model: this.model,
621
+ manager: this,
622
+ transaction: this.getHookTransaction()
623
+ });
624
+ const validatedPlan = sqlSafetyAdapter.validate({
625
+ kind: InternalSqlValidationPlanKind.DELETE,
626
+ meta: this.meta
627
+ });
628
+ const compiled = this.mutationCompiler.compileDelete(validatedPlan, id);
629
+ await this.queryExecutor.client.query(compiled.sql, compiled.params);
630
+ await this.model.hooks?.afterDelete?.({
631
+ id,
632
+ previous: current,
633
+ model: this.model,
634
+ manager: this,
635
+ transaction: this.getHookTransaction()
636
+ });
637
+ }
638
+ /**
639
+ * Build a {@link ManyToManyRelatedManager} bound to a single owner record
640
+ * for the supplied many-to-many relation. The returned manager performs
641
+ * its INSERT/DELETE writes through the shared runtime-bound client, so
642
+ * mutations enroll in any active `transaction.atomic(...)` boundary.
643
+ */
644
+ createManyToManyRelatedManager(relationName, ownerPrimaryKey) {
645
+ const relation = this.requireManyToManyEdge(relationName);
646
+ const registry = ModelRegistry.getOwner(this.model);
647
+ const throughModel = registry.getByKey(relation.throughModelKey);
648
+ return ManyToManyRelatedManager.create({
649
+ ownerPrimaryKey,
650
+ relationName,
651
+ ownerModelLabel: this.model.metadata.name,
652
+ relation,
653
+ throughModelFields: throughModel.metadata.fields,
654
+ client: this.client,
655
+ mutationCompiler: this.mutationCompiler,
656
+ adapter: this.adapter,
657
+ sqlSafetyAdapter,
658
+ targetExecutorProvider: () => this.resolveTargetExecutor(relation.targetModelKey),
659
+ runAtomic: (work) => TransactionEngine.forRuntime(this.runtime).atomic(() => work())
660
+ });
661
+ }
662
+ async bulkCreate(inputs) {
663
+ if (inputs.length === 0) return [];
664
+ const perRowPrepared = await Promise.all(inputs.map((input) => this.runBeforeCreate(input)));
665
+ const batchPrepared = await this.model.hooks?.beforeBulkCreate?.({
666
+ rows: perRowPrepared,
667
+ model: this.model,
668
+ manager: this,
669
+ transaction: this.getHookTransaction()
670
+ }) ?? perRowPrepared;
671
+ const preparedKeys = Object.keys(batchPrepared[0] ?? {});
672
+ if (preparedKeys.length === 0) throw new Error(`Cannot create ${this.model.metadata.name} without any values.`);
673
+ const validatedPlan = sqlSafetyAdapter.validate({
674
+ kind: InternalSqlValidationPlanKind.INSERT,
675
+ meta: this.meta,
676
+ writeKeys: preparedKeys
677
+ });
678
+ const valueRows = batchPrepared.map((input) => preparedKeys.map((key) => input[key]));
679
+ const compiled = this.mutationCompiler.compileBulkInsert(validatedPlan, valueRows);
680
+ const result = await this.queryExecutor.client.query(compiled.sql, compiled.params);
681
+ for (const record of result.rows) this.attachOwnRelatedManagers(record);
682
+ await Promise.all(result.rows.map((record) => this.model.hooks?.afterCreate?.({
683
+ record,
684
+ model: this.model,
685
+ manager: this,
686
+ transaction: this.getHookTransaction()
687
+ })));
688
+ await this.model.hooks?.afterBulkCreate?.({
689
+ records: result.rows,
690
+ model: this.model,
691
+ manager: this,
692
+ transaction: this.getHookTransaction()
693
+ });
694
+ return result.rows;
695
+ }
696
+ /**
697
+ * Attach a {@link ManyToManyRelatedManager} as a non-enumerable property
698
+ * for every persisted many-to-many relation declared on the supplied
699
+ * record's model. Existing properties are left untouched so that prior
700
+ * hydration writes (such as prefetched arrays) survive the attach pass
701
+ * during the incremental rollout of related-manager hydration.
702
+ */
703
+ attachOwnRelatedManagers(record) {
704
+ this.attachManyToManyRelatedManagers(record, this.model.metadata.key);
705
+ }
706
+ attachManyToManyRelatedManagers(record, modelKey) {
707
+ if (!modelKey) return;
708
+ const targetManager = this.resolveManagerForModelKey(modelKey);
709
+ if (!targetManager) return;
710
+ const meta = targetManager.meta;
711
+ const relations = meta.relations;
712
+ if (!relations) return;
713
+ const ownerPrimaryKey = record[meta.pk];
714
+ if (ownerPrimaryKey === undefined || ownerPrimaryKey === null) return;
715
+ for (const [relationName, relation] of Object.entries(relations)) {
716
+ if (relation.kind !== InternalRelationKind.MANY_TO_MANY) continue;
717
+ if (Object.prototype.hasOwnProperty.call(record, relationName)) continue;
718
+ const relatedManager = targetManager.createManyToManyRelatedManager(relationName, ownerPrimaryKey);
719
+ Object.defineProperty(record, relationName, {
720
+ value: relatedManager,
721
+ writable: true,
722
+ configurable: true,
723
+ enumerable: false
724
+ });
725
+ }
726
+ }
727
+ resolveManagerForModelKey(modelKey) {
728
+ if (modelKey === this.model.metadata.key) return this;
729
+ const registry = ModelRegistry.getOwner(this.model);
730
+ const otherModel = registry.getByKey(modelKey);
731
+ if (!otherModel) return null;
732
+ const candidate = otherModel.objects;
733
+ return ModelManager.isModelManager(candidate) ? candidate : null;
734
+ }
735
+ resolveTargetExecutor(targetModelKey) {
736
+ const targetManager = this.resolveManagerForModelKey(targetModelKey);
737
+ if (!targetManager) return null;
738
+ return targetManager.queryExecutor;
739
+ }
740
+ requireManyToManyEdge(relationName) {
741
+ const rel = this.meta.relations?.[relationName];
742
+ if (!rel || rel.kind !== InternalRelationKind.MANY_TO_MANY || !rel.throughTable || !rel.throughSourceKey || !rel.throughTargetKey || !rel.throughModelKey) throw new Error(`Relation '${relationName}' on '${this.model.metadata.name}' is not a persisted many-to-many edge.`);
743
+ return rel;
744
+ }
745
+ async runBeforeCreate(data) {
746
+ return await this.model.hooks?.beforeCreate?.({
747
+ data,
748
+ model: this.model,
749
+ manager: this,
750
+ transaction: this.getHookTransaction()
751
+ }) ?? data;
752
+ }
753
+ async runBeforeUpdate(id, patch, current) {
754
+ return await this.model.hooks?.beforeUpdate?.({
755
+ id,
756
+ patch,
757
+ current,
758
+ model: this.model,
759
+ manager: this,
760
+ transaction: this.getHookTransaction()
761
+ }) ?? patch;
762
+ }
763
+ getHookTransaction() {
764
+ return TransactionEngine.forRuntime(this.runtime).getActiveTransaction();
765
+ }
766
+ };
767
+
768
+ //#endregion
769
+ //#region src/manager/registerModelObjects.ts
770
+ const managerCache = new WeakMap();
771
+ let hasRegisteredModelObjects = false;
772
+ function defineObjectsProperty(model) {
773
+ Object.defineProperty(model, "objects", {
774
+ configurable: true,
775
+ enumerable: true,
776
+ get() {
777
+ const runtime = getTangoRuntime();
778
+ const cached = managerCache.get(model);
779
+ if (cached && cached.runtime === runtime) return cached.manager;
780
+ const manager = new ModelManager(model, runtime);
781
+ managerCache.set(model, {
782
+ runtime,
783
+ manager
784
+ });
785
+ return manager;
786
+ }
787
+ });
788
+ }
789
+ function registerModelObjects() {
790
+ if (hasRegisteredModelObjects) return;
791
+ registerModelAugmentor(defineObjectsProperty);
792
+ hasRegisteredModelObjects = true;
793
+ }
794
+
795
+ //#endregion
796
+ export { ManyToManyRelatedManager, ManyToManyRelatedQuerySet, ModelManager, registerModelObjects };
797
+ //# sourceMappingURL=registerModelObjects-DxlBfuUN.js.map