arkormx 2.0.6 → 2.0.8

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.
@@ -0,0 +1,4459 @@
1
+ import { Collection } from "@h3ravel/collect.js";
2
+ import { AsyncLocalStorage } from "async_hooks";
3
+ import { createJiti } from "jiti";
4
+ import { pathToFileURL } from "node:url";
5
+ import { dirname, extname, join, resolve } from "node:path";
6
+ import { createRequire } from "module";
7
+ import { existsSync } from "fs";
8
+ import { fileURLToPath } from "url";
9
+ import path from "path";
10
+ import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
11
+ import { createHash, randomUUID } from "node:crypto";
12
+ import { spawnSync } from "node:child_process";
13
+ import { str } from "@h3ravel/support";
14
+
15
+ //#region src/Exceptions/ArkormException.ts
16
+ var ArkormException = class extends Error {
17
+ code;
18
+ operation;
19
+ model;
20
+ delegate;
21
+ relation;
22
+ scope;
23
+ meta;
24
+ constructor(message, context = {}) {
25
+ super(message, context.cause === void 0 ? void 0 : { cause: context.cause });
26
+ this.name = "ArkormException";
27
+ this.code = context.code;
28
+ this.operation = context.operation;
29
+ this.model = context.model;
30
+ this.delegate = context.delegate;
31
+ this.relation = context.relation;
32
+ this.scope = context.scope;
33
+ this.meta = context.meta;
34
+ }
35
+ getContext() {
36
+ return {
37
+ code: this.code,
38
+ operation: this.operation,
39
+ model: this.model,
40
+ delegate: this.delegate,
41
+ relation: this.relation,
42
+ scope: this.scope,
43
+ meta: this.meta,
44
+ cause: this.cause
45
+ };
46
+ }
47
+ toJSON() {
48
+ return {
49
+ name: this.name,
50
+ message: this.message,
51
+ ...this.getContext()
52
+ };
53
+ }
54
+ };
55
+
56
+ //#endregion
57
+ //#region src/Collection.ts
58
+ var ArkormCollection = class extends Collection {};
59
+
60
+ //#endregion
61
+ //#region src/Exceptions/RelationResolutionException.ts
62
+ var RelationResolutionException = class extends ArkormException {
63
+ constructor(message, context = {}) {
64
+ super(message, {
65
+ code: "RELATION_RESOLUTION_FAILED",
66
+ ...context
67
+ });
68
+ this.name = "RelationResolutionException";
69
+ }
70
+ };
71
+
72
+ //#endregion
73
+ //#region src/relationship/RelationTableLoader.ts
74
+ /**
75
+ * Utility class responsible for loading data from relation tables, which are used to
76
+ * manage relationships between models in Arkorm.
77
+ *
78
+ * @author Legacy (3m1n3nc3)
79
+ * @since 2.0.0-next.0
80
+ */
81
+ var RelationTableLoader = class {
82
+ constructor(adapter) {
83
+ this.adapter = adapter;
84
+ }
85
+ async selectRows(spec) {
86
+ return await this.adapter.select({
87
+ target: { table: spec.table },
88
+ where: spec.where,
89
+ columns: spec.columns,
90
+ orderBy: spec.orderBy,
91
+ limit: spec.limit,
92
+ offset: spec.offset
93
+ });
94
+ }
95
+ async selectRow(spec) {
96
+ return await this.adapter.selectOne({
97
+ target: { table: spec.table },
98
+ where: spec.where,
99
+ columns: spec.columns,
100
+ orderBy: spec.orderBy,
101
+ limit: spec.limit ?? 1,
102
+ offset: spec.offset
103
+ });
104
+ }
105
+ async selectColumnValues(spec) {
106
+ return (await this.selectRows({
107
+ ...spec.lookup,
108
+ columns: [{ column: spec.column }]
109
+ })).map((row) => row[spec.column]);
110
+ }
111
+ async selectColumnValue(spec) {
112
+ return (await this.selectRow({
113
+ ...spec.lookup,
114
+ columns: [{ column: spec.column }],
115
+ limit: 1
116
+ }))?.[spec.column] ?? null;
117
+ }
118
+ };
119
+
120
+ //#endregion
121
+ //#region src/relationship/SetBasedEagerLoader.ts
122
+ /**
123
+ * Utility class responsible for performing set-based eager loading of relationships for
124
+ * a collection of models.
125
+ *
126
+ * @author Legacy (3m1n3nc3)
127
+ * @since 2.0.0-next.2
128
+ */
129
+ var SetBasedEagerLoader = class SetBasedEagerLoader {
130
+ constructor(models, relations) {
131
+ this.models = models;
132
+ this.relations = relations;
133
+ }
134
+ /**
135
+ * Performs eager loading of all specified relationships for the set of models.
136
+ *
137
+ * @returns
138
+ */
139
+ async load() {
140
+ if (this.models.length === 0) return;
141
+ const relationTree = this.buildRelationTree(this.relations);
142
+ await Promise.all(Array.from(relationTree.entries()).map(async ([name, node]) => {
143
+ await this.loadRelationNode(name, node);
144
+ }));
145
+ }
146
+ async loadRelationNode(name, node) {
147
+ await this.loadRelation(name, node.constraint);
148
+ if (node.children.size === 0) return;
149
+ const relatedModels = this.collectLoadedRelationModels(name);
150
+ if (relatedModels.length === 0) return;
151
+ await new SetBasedEagerLoader(relatedModels, this.relationTreeToMap(node.children)).load();
152
+ }
153
+ /**
154
+ * Loads a specific relationship for the set of models based on the relationship name
155
+ * and an optional constraint.
156
+ *
157
+ * @param name The name of the relationship to load.
158
+ * @param constraint An optional constraint to apply to the query.
159
+ * @returns A promise that resolves when the relationship is loaded.
160
+ */
161
+ async loadRelation(name, constraint) {
162
+ const resolver = this.resolveRelationResolver(name);
163
+ if (!resolver) return;
164
+ const metadata = resolver.call(this.models[0]).getMetadata();
165
+ switch (metadata.type) {
166
+ case "belongsTo":
167
+ await this.loadBelongsTo(name, resolver, metadata, constraint);
168
+ return;
169
+ case "belongsToMany":
170
+ await this.loadBelongsToMany(name, metadata, constraint);
171
+ return;
172
+ case "hasMany":
173
+ await this.loadHasMany(name, metadata, constraint);
174
+ return;
175
+ case "hasOne":
176
+ await this.loadHasOne(name, resolver, metadata, constraint);
177
+ return;
178
+ case "hasManyThrough":
179
+ await this.loadHasManyThrough(name, metadata, constraint);
180
+ return;
181
+ case "hasOneThrough":
182
+ await this.loadHasOneThrough(name, resolver, metadata, constraint);
183
+ return;
184
+ default: await this.loadIndividually(name, resolver, constraint);
185
+ }
186
+ }
187
+ /**
188
+ * Resolves the relation resolver function for a given relationship name by inspecting
189
+ * the first model in the set.
190
+ *
191
+ * @param name The name of the relationship to resolve.
192
+ * @returns The relation resolver function or null if not found.
193
+ */
194
+ resolveRelationResolver(name) {
195
+ const resolver = this.models[0][name];
196
+ if (typeof resolver !== "function") {
197
+ const modelName = this.models[0].constructor?.name ?? "Model";
198
+ throw new RelationResolutionException(`Relation [${name}] is not defined on the model.`, {
199
+ operation: "eagerLoad",
200
+ model: modelName,
201
+ relation: name
202
+ });
203
+ }
204
+ return resolver;
205
+ }
206
+ buildRelationTree(relations) {
207
+ const tree = /* @__PURE__ */ new Map();
208
+ Object.entries(relations).forEach(([path, constraint]) => {
209
+ const segments = path.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
210
+ if (segments.length === 0) return;
211
+ let current = tree;
212
+ segments.forEach((segment, index) => {
213
+ const existing = current.get(segment) ?? {
214
+ constraint: void 0,
215
+ children: /* @__PURE__ */ new Map()
216
+ };
217
+ if (index === segments.length - 1 && constraint) existing.constraint = constraint;
218
+ current.set(segment, existing);
219
+ current = existing.children;
220
+ });
221
+ });
222
+ return tree;
223
+ }
224
+ relationTreeToMap(tree, prefix = "") {
225
+ return Array.from(tree.entries()).reduce((all, [name, node]) => {
226
+ const path = prefix ? `${prefix}.${name}` : name;
227
+ all[path] = node.constraint;
228
+ Object.assign(all, this.relationTreeToMap(node.children, path));
229
+ return all;
230
+ }, {});
231
+ }
232
+ collectLoadedRelationModels(name) {
233
+ return this.models.reduce((all, model) => {
234
+ const loaded = model.getAttribute(name);
235
+ if (loaded instanceof ArkormCollection) {
236
+ loaded.all().forEach((item) => {
237
+ if (this.isEagerLoadableModel(item)) all.push(item);
238
+ });
239
+ return all;
240
+ }
241
+ if (this.isEagerLoadableModel(loaded)) all.push(loaded);
242
+ return all;
243
+ }, []);
244
+ }
245
+ isEagerLoadableModel(value) {
246
+ return typeof value === "object" && value !== null && typeof value.getAttribute === "function" && typeof value.setLoadedRelation === "function";
247
+ }
248
+ /**
249
+ * Loads a "belongs to" relationship for the set of models.
250
+ *
251
+ * @param name The name of the relationship to load.
252
+ * @param resolver The relation resolver function.
253
+ * @param metadata The metadata for the relationship.
254
+ * @param constraint An optional constraint to apply to the query.
255
+ * @returns A promise that resolves when the relationship is loaded.
256
+ */
257
+ async loadBelongsTo(name, resolver, metadata, constraint) {
258
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.foreignKey));
259
+ if (keys.length === 0) {
260
+ this.models.forEach((model) => {
261
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
262
+ });
263
+ return;
264
+ }
265
+ let query = metadata.relatedModel.query().whereIn(metadata.ownerKey, keys);
266
+ query = this.applyConstraint(query, constraint);
267
+ const relatedModels = (await query.get()).all();
268
+ const relatedByOwnerKey = /* @__PURE__ */ new Map();
269
+ relatedModels.forEach((related) => {
270
+ const value = this.readModelAttribute(related, metadata.ownerKey);
271
+ if (value == null) return;
272
+ const lookupKey = this.toLookupKey(value);
273
+ if (!relatedByOwnerKey.has(lookupKey)) relatedByOwnerKey.set(lookupKey, related);
274
+ });
275
+ this.models.forEach((model) => {
276
+ const foreignValue = model.getAttribute(metadata.foreignKey);
277
+ const relationValue = foreignValue == null ? void 0 : relatedByOwnerKey.get(this.toLookupKey(foreignValue));
278
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
279
+ });
280
+ }
281
+ /**
282
+ * Loads a "has many" relationship for the set of models.
283
+ *
284
+ * @param name
285
+ * @param metadata
286
+ * @param constraint
287
+ * @returns
288
+ */
289
+ async loadHasMany(name, metadata, constraint) {
290
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
291
+ if (keys.length === 0) {
292
+ this.models.forEach((model) => {
293
+ model.setLoadedRelation(name, new ArkormCollection([]));
294
+ });
295
+ return;
296
+ }
297
+ let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
298
+ query = this.applyConstraint(query, constraint);
299
+ const relatedModels = (await query.get()).all();
300
+ const relatedByForeignKey = /* @__PURE__ */ new Map();
301
+ relatedModels.forEach((related) => {
302
+ const value = this.readModelAttribute(related, metadata.foreignKey);
303
+ if (value == null) return;
304
+ const lookupKey = this.toLookupKey(value);
305
+ const bucket = relatedByForeignKey.get(lookupKey) ?? [];
306
+ bucket.push(related);
307
+ relatedByForeignKey.set(lookupKey, bucket);
308
+ });
309
+ this.models.forEach((model) => {
310
+ const localValue = model.getAttribute(metadata.localKey);
311
+ const related = localValue == null ? [] : relatedByForeignKey.get(this.toLookupKey(localValue)) ?? [];
312
+ model.setLoadedRelation(name, new ArkormCollection(related));
313
+ });
314
+ }
315
+ /**
316
+ * Loads a "belongs to many" relationship for the set of models.
317
+ *
318
+ * @param name
319
+ * @param metadata
320
+ * @param constraint
321
+ * @returns
322
+ */
323
+ async loadBelongsToMany(name, metadata, constraint) {
324
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.parentKey));
325
+ if (parentKeys.length === 0) {
326
+ this.models.forEach((model) => {
327
+ model.setLoadedRelation(name, new ArkormCollection([]));
328
+ });
329
+ return;
330
+ }
331
+ const pivotRows = await this.createRelationTableLoader().selectRows({
332
+ table: metadata.throughTable,
333
+ where: this.buildBelongsToManyPivotWhere(metadata, parentKeys),
334
+ columns: this.getBelongsToManyPivotColumns(metadata).map((column) => ({ column }))
335
+ });
336
+ const relatedIds = this.collectUniqueRowValues(pivotRows, metadata.relatedPivotKey);
337
+ if (relatedIds.length === 0) {
338
+ this.models.forEach((model) => {
339
+ model.setLoadedRelation(name, new ArkormCollection([]));
340
+ });
341
+ return;
342
+ }
343
+ let query = metadata.relatedModel.query().whereIn(metadata.relatedKey, relatedIds);
344
+ query = this.applyConstraint(query, constraint);
345
+ const relatedModels = (await query.get()).all();
346
+ const relatedByKey = /* @__PURE__ */ new Map();
347
+ relatedModels.forEach((related) => {
348
+ const relatedValue = this.readModelAttribute(related, metadata.relatedKey);
349
+ if (relatedValue == null) return;
350
+ relatedByKey.set(this.toLookupKey(relatedValue), related);
351
+ });
352
+ const relatedKeysByParent = /* @__PURE__ */ new Map();
353
+ const pivotByParentAndRelated = /* @__PURE__ */ new Map();
354
+ pivotRows.forEach((row) => {
355
+ const parentValue = row[metadata.foreignPivotKey];
356
+ const relatedValue = row[metadata.relatedPivotKey];
357
+ if (parentValue == null || relatedValue == null) return;
358
+ const bucket = relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? [];
359
+ bucket.push(relatedValue);
360
+ relatedKeysByParent.set(this.toLookupKey(parentValue), bucket);
361
+ pivotByParentAndRelated.set(`${this.toLookupKey(parentValue)}:${this.toLookupKey(relatedValue)}`, row);
362
+ });
363
+ this.models.forEach((model) => {
364
+ const parentValue = model.getAttribute(metadata.parentKey);
365
+ const related = (parentValue == null ? [] : relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? []).reduce((all, relatedValue) => {
366
+ const candidate = relatedByKey.get(this.toLookupKey(relatedValue));
367
+ if (candidate) all.push(this.attachBelongsToManyPivot(metadata, candidate, pivotByParentAndRelated.get(`${this.toLookupKey(parentValue)}:${this.toLookupKey(relatedValue)}`)));
368
+ return all;
369
+ }, []);
370
+ model.setLoadedRelation(name, new ArkormCollection(related));
371
+ });
372
+ }
373
+ buildBelongsToManyPivotWhere(metadata, parentKeys) {
374
+ const baseCondition = {
375
+ type: "comparison",
376
+ column: metadata.foreignPivotKey,
377
+ operator: "in",
378
+ value: parentKeys
379
+ };
380
+ if (!metadata.pivotWhere) return baseCondition;
381
+ return {
382
+ type: "group",
383
+ operator: "and",
384
+ conditions: [baseCondition, metadata.pivotWhere]
385
+ };
386
+ }
387
+ getBelongsToManyPivotColumns(metadata) {
388
+ return [
389
+ metadata.foreignPivotKey,
390
+ metadata.relatedPivotKey,
391
+ ...metadata.pivotColumns ?? []
392
+ ].filter((column, index, all) => all.indexOf(column) === index);
393
+ }
394
+ shouldAttachBelongsToManyPivot(metadata) {
395
+ return Boolean(metadata.pivotModel) || Boolean(metadata.pivotCreatedAtColumn) || Boolean(metadata.pivotUpdatedAtColumn) || (metadata.pivotColumns?.length ?? 0) > 0 || Boolean(metadata.pivotAccessor);
396
+ }
397
+ createBelongsToManyPivotRecord(metadata, row) {
398
+ const attributes = this.getBelongsToManyPivotColumns(metadata).reduce((all, column) => {
399
+ all[column] = row[column];
400
+ return all;
401
+ }, {});
402
+ if (!metadata.pivotModel) return attributes;
403
+ if (typeof metadata.pivotModel.hydrate === "function") return metadata.pivotModel.hydrate(attributes);
404
+ return new metadata.pivotModel(attributes);
405
+ }
406
+ attachBelongsToManyPivot(metadata, related, row) {
407
+ if (!row || !this.shouldAttachBelongsToManyPivot(metadata)) return related;
408
+ const rawReader = related;
409
+ if (typeof rawReader.getRawAttributes !== "function" || typeof rawReader.setAttribute !== "function") return related;
410
+ const cloned = metadata.relatedModel.hydrate(rawReader.getRawAttributes());
411
+ cloned.setAttribute(metadata.pivotAccessor ?? "pivot", this.createBelongsToManyPivotRecord(metadata, row));
412
+ return cloned;
413
+ }
414
+ /**
415
+ * Loads a "belongs to many" relationship for the set of models.
416
+ *
417
+ * @param name
418
+ * @param resolver
419
+ * @param metadata
420
+ * @param constraint
421
+ * @returns
422
+ */
423
+ async loadHasOne(name, resolver, metadata, constraint) {
424
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
425
+ if (keys.length === 0) {
426
+ this.models.forEach((model) => {
427
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
428
+ });
429
+ return;
430
+ }
431
+ let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
432
+ query = this.applyConstraint(query, constraint);
433
+ const relatedModels = (await query.get()).all();
434
+ const relatedByForeignKey = /* @__PURE__ */ new Map();
435
+ relatedModels.forEach((related) => {
436
+ const value = this.readModelAttribute(related, metadata.foreignKey);
437
+ if (value == null) return;
438
+ const lookupKey = this.toLookupKey(value);
439
+ if (!relatedByForeignKey.has(lookupKey)) relatedByForeignKey.set(lookupKey, related);
440
+ });
441
+ this.models.forEach((model) => {
442
+ const localValue = model.getAttribute(metadata.localKey);
443
+ const relationValue = localValue == null ? void 0 : relatedByForeignKey.get(this.toLookupKey(localValue));
444
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
445
+ });
446
+ }
447
+ /**
448
+ * Loads a "has many through" relationship for the set of models.
449
+ *
450
+ * @param name
451
+ * @param metadata
452
+ * @param constraint
453
+ * @returns
454
+ */
455
+ async loadHasManyThrough(name, metadata, constraint) {
456
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
457
+ if (parentKeys.length === 0) {
458
+ this.models.forEach((model) => {
459
+ model.setLoadedRelation(name, new ArkormCollection([]));
460
+ });
461
+ return;
462
+ }
463
+ const throughRows = await this.createRelationTableLoader().selectRows({
464
+ table: metadata.throughTable,
465
+ where: {
466
+ type: "comparison",
467
+ column: metadata.firstKey,
468
+ operator: "in",
469
+ value: parentKeys
470
+ }
471
+ });
472
+ const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
473
+ if (intermediateKeys.length === 0) {
474
+ this.models.forEach((model) => {
475
+ model.setLoadedRelation(name, new ArkormCollection([]));
476
+ });
477
+ return;
478
+ }
479
+ let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
480
+ query = this.applyConstraint(query, constraint);
481
+ const relatedModels = (await query.get()).all();
482
+ const relatedByIntermediate = /* @__PURE__ */ new Map();
483
+ relatedModels.forEach((related) => {
484
+ const relatedValue = this.readModelAttribute(related, metadata.secondKey);
485
+ if (relatedValue == null) return;
486
+ const bucket = relatedByIntermediate.get(this.toLookupKey(relatedValue)) ?? [];
487
+ bucket.push(related);
488
+ relatedByIntermediate.set(this.toLookupKey(relatedValue), bucket);
489
+ });
490
+ const intermediateByParent = /* @__PURE__ */ new Map();
491
+ throughRows.forEach((row) => {
492
+ const parentValue = row[metadata.firstKey];
493
+ const intermediateValue = row[metadata.secondLocalKey];
494
+ if (parentValue == null || intermediateValue == null) return;
495
+ const bucket = intermediateByParent.get(this.toLookupKey(parentValue)) ?? [];
496
+ bucket.push(intermediateValue);
497
+ intermediateByParent.set(this.toLookupKey(parentValue), bucket);
498
+ });
499
+ this.models.forEach((model) => {
500
+ const parentValue = model.getAttribute(metadata.localKey);
501
+ const related = (parentValue == null ? [] : intermediateByParent.get(this.toLookupKey(parentValue)) ?? []).flatMap((intermediateValue) => relatedByIntermediate.get(this.toLookupKey(intermediateValue)) ?? []);
502
+ model.setLoadedRelation(name, new ArkormCollection(related));
503
+ });
504
+ }
505
+ /**
506
+ * Loads a "has one through" relationship for the set of models.
507
+ *
508
+ * @param name
509
+ * @param resolver
510
+ * @param metadata
511
+ * @param constraint
512
+ * @returns
513
+ */
514
+ async loadHasOneThrough(name, resolver, metadata, constraint) {
515
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
516
+ if (parentKeys.length === 0) {
517
+ this.models.forEach((model) => {
518
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
519
+ });
520
+ return;
521
+ }
522
+ const throughRows = await this.createRelationTableLoader().selectRows({
523
+ table: metadata.throughTable,
524
+ where: {
525
+ type: "comparison",
526
+ column: metadata.firstKey,
527
+ operator: "in",
528
+ value: parentKeys
529
+ }
530
+ });
531
+ const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
532
+ if (intermediateKeys.length === 0) {
533
+ this.models.forEach((model) => {
534
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
535
+ });
536
+ return;
537
+ }
538
+ let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
539
+ query = this.applyConstraint(query, constraint);
540
+ const relatedModels = (await query.get()).all();
541
+ const relatedByIntermediate = /* @__PURE__ */ new Map();
542
+ relatedModels.forEach((related) => {
543
+ const relatedValue = this.readModelAttribute(related, metadata.secondKey);
544
+ if (relatedValue == null) return;
545
+ const lookupKey = this.toLookupKey(relatedValue);
546
+ if (!relatedByIntermediate.has(lookupKey)) relatedByIntermediate.set(lookupKey, related);
547
+ });
548
+ const intermediateByParent = /* @__PURE__ */ new Map();
549
+ throughRows.forEach((row) => {
550
+ const parentValue = row[metadata.firstKey];
551
+ const intermediateValue = row[metadata.secondLocalKey];
552
+ if (parentValue == null || intermediateValue == null) return;
553
+ const lookupKey = this.toLookupKey(parentValue);
554
+ if (!intermediateByParent.has(lookupKey)) intermediateByParent.set(lookupKey, intermediateValue);
555
+ });
556
+ this.models.forEach((model) => {
557
+ const parentValue = model.getAttribute(metadata.localKey);
558
+ const intermediateValue = parentValue == null ? void 0 : intermediateByParent.get(this.toLookupKey(parentValue));
559
+ const relationValue = intermediateValue == null ? void 0 : relatedByIntermediate.get(this.toLookupKey(intermediateValue));
560
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
561
+ });
562
+ }
563
+ /**
564
+ * Fallback method to load relationships individually for each model when the
565
+ * relationship type is not supported for set-based loading.
566
+ *
567
+ * @param name
568
+ * @param resolver
569
+ * @param constraint
570
+ */
571
+ async loadIndividually(name, resolver, constraint) {
572
+ await Promise.all(this.models.map(async (model) => {
573
+ const relation = resolver.call(model);
574
+ if (constraint) relation.constrain(constraint);
575
+ model.setLoadedRelation(name, await relation.getResults());
576
+ }));
577
+ }
578
+ /**
579
+ * Applies an eager load constraint to a query if provided.
580
+ *
581
+ * @param query
582
+ * @param constraint
583
+ * @returns
584
+ */
585
+ applyConstraint(query, constraint) {
586
+ if (!constraint) return query;
587
+ return constraint(query) ?? query;
588
+ }
589
+ /**
590
+ * Collects unique values from the set of models based on a resolver function, which
591
+ * is used to extract the value from each model.
592
+ *
593
+ * @param resolve A function that takes a model and returns the value to be collected.
594
+ * @returns An array of unique values.
595
+ */
596
+ collectUniqueKeys(resolve) {
597
+ const seen = /* @__PURE__ */ new Set();
598
+ const values = [];
599
+ this.models.forEach((model) => {
600
+ const value = resolve(model);
601
+ if (value == null) return;
602
+ const lookupKey = this.toLookupKey(value);
603
+ if (seen.has(lookupKey)) return;
604
+ seen.add(lookupKey);
605
+ values.push(value);
606
+ });
607
+ return values;
608
+ }
609
+ /**
610
+ * Collects unique values from an array of database rows based on a specified key, which
611
+ * is used to extract the value from each row.
612
+ *
613
+ * @param rows An array of database rows.
614
+ * @param key The key to extract values from each row.
615
+ * @returns An array of unique values.
616
+ */
617
+ collectUniqueRowValues(rows, key) {
618
+ const seen = /* @__PURE__ */ new Set();
619
+ const values = [];
620
+ rows.forEach((row) => {
621
+ const value = row[key];
622
+ if (value == null) return;
623
+ const lookupKey = this.toLookupKey(value);
624
+ if (seen.has(lookupKey)) return;
625
+ seen.add(lookupKey);
626
+ values.push(value);
627
+ });
628
+ return values;
629
+ }
630
+ /**
631
+ * Loads a "belongs to many" relationship for the set of models.
632
+ *
633
+ * @returns
634
+ */
635
+ createRelationTableLoader() {
636
+ return new RelationTableLoader(this.resolveAdapter());
637
+ }
638
+ /**
639
+ * Loads a "belongs to many" relationship for the set of models.
640
+ *
641
+ * @returns
642
+ */
643
+ resolveAdapter() {
644
+ const adapter = this.models[0].constructor.getAdapter?.();
645
+ if (!adapter) throw new Error("Set-based eager loading requires a configured adapter.");
646
+ return adapter;
647
+ }
648
+ /**
649
+ * Reads an attribute value from a model using the getAttribute method, which is used
650
+ * to access model attributes in a way that is compatible with Arkorm's internal model structure.
651
+ *
652
+ * @param model The model to read the attribute from.
653
+ * @param key The name of the attribute to read.
654
+ * @returns
655
+ */
656
+ readModelAttribute(model, key) {
657
+ return model.getAttribute?.(key);
658
+ }
659
+ /**
660
+ * Resolves the default result for a relationship when no related models are found.
661
+ *
662
+ * @param resolver
663
+ * @param model
664
+ * @returns
665
+ */
666
+ resolveSingleDefault(resolver, model) {
667
+ return resolver.call(model).resolveDefaultResult?.() ?? null;
668
+ }
669
+ /**
670
+ * Generates a unique lookup key for a given value, which is used to store and retrieve
671
+ * values in maps during the eager loading process.
672
+ *
673
+ * @param value The value to generate a lookup key for.
674
+ * @returns A unique string representing the value.
675
+ */
676
+ toLookupKey(value) {
677
+ if (value instanceof Date) return `date:${value.toISOString()}`;
678
+ return `${typeof value}:${String(value)}`;
679
+ }
680
+ };
681
+
682
+ //#endregion
683
+ //#region src/Exceptions/UnsupportedAdapterFeatureException.ts
684
+ var UnsupportedAdapterFeatureException = class extends ArkormException {
685
+ constructor(message, context = {}) {
686
+ super(message, {
687
+ code: "UNSUPPORTED_ADAPTER_FEATURE",
688
+ ...context
689
+ });
690
+ this.name = "UnsupportedAdapterFeatureException";
691
+ }
692
+ };
693
+
694
+ //#endregion
695
+ //#region src/helpers/runtime-module-loader.ts
696
+ var RuntimeModuleLoader = class {
697
+ static async load(filePath) {
698
+ const resolvedPath = resolve(filePath);
699
+ return await createJiti(pathToFileURL(resolvedPath).href, {
700
+ interopDefault: false,
701
+ tsconfigPaths: true,
702
+ sourceMaps: true
703
+ }).import(resolvedPath, { default: true });
704
+ }
705
+ };
706
+
707
+ //#endregion
708
+ //#region src/helpers/migration-history.ts
709
+ const createEmptyAppliedMigrationsState = () => ({
710
+ version: 1,
711
+ migrations: [],
712
+ runs: []
713
+ });
714
+ const supportsDatabaseMigrationState = (adapter) => {
715
+ return typeof adapter?.readAppliedMigrationsState === "function" && typeof adapter?.writeAppliedMigrationsState === "function";
716
+ };
717
+ const resolveMigrationStateFilePath = (cwd, configuredPath) => {
718
+ if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
719
+ return join(cwd, ".arkormx", "migrations.applied.json");
720
+ };
721
+ const buildMigrationIdentity = (filePath, className) => {
722
+ const fileName = filePath.split("/").pop()?.split("\\").pop() ?? filePath;
723
+ return `${fileName.slice(0, fileName.length - extname(fileName).length)}:${className}`;
724
+ };
725
+ const computeMigrationChecksum = (filePath) => {
726
+ const source = readFileSync$1(filePath, "utf-8");
727
+ return createHash("sha256").update(source).digest("hex");
728
+ };
729
+ const readAppliedMigrationsState = (stateFilePath) => {
730
+ if (!existsSync$1(stateFilePath)) return createEmptyAppliedMigrationsState();
731
+ try {
732
+ const parsed = JSON.parse(readFileSync$1(stateFilePath, "utf-8"));
733
+ if (!Array.isArray(parsed.migrations)) return createEmptyAppliedMigrationsState();
734
+ return {
735
+ version: 1,
736
+ migrations: parsed.migrations.filter((migration) => {
737
+ return typeof migration?.id === "string" && typeof migration?.file === "string" && typeof migration?.className === "string" && typeof migration?.appliedAt === "string" && (migration?.checksum === void 0 || typeof migration?.checksum === "string");
738
+ }),
739
+ runs: Array.isArray(parsed.runs) ? parsed.runs.filter((run) => {
740
+ return typeof run?.id === "string" && typeof run?.appliedAt === "string" && Array.isArray(run?.migrationIds) && run.migrationIds.every((item) => typeof item === "string");
741
+ }) : []
742
+ };
743
+ } catch {
744
+ return createEmptyAppliedMigrationsState();
745
+ }
746
+ };
747
+ const readAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
748
+ if (supportsDatabaseMigrationState(adapter)) return await adapter.readAppliedMigrationsState();
749
+ return readAppliedMigrationsState(stateFilePath);
750
+ };
751
+ const writeAppliedMigrationsState = (stateFilePath, state) => {
752
+ const directory = dirname(stateFilePath);
753
+ if (!existsSync$1(directory)) mkdirSync$1(directory, { recursive: true });
754
+ writeFileSync$1(stateFilePath, JSON.stringify(state, null, 2));
755
+ };
756
+ const writeAppliedMigrationsStateToStore = async (adapter, stateFilePath, state) => {
757
+ if (supportsDatabaseMigrationState(adapter)) {
758
+ await adapter.writeAppliedMigrationsState(state);
759
+ return;
760
+ }
761
+ writeAppliedMigrationsState(stateFilePath, state);
762
+ };
763
+ const deleteAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
764
+ if (supportsDatabaseMigrationState(adapter)) {
765
+ await adapter.writeAppliedMigrationsState(createEmptyAppliedMigrationsState());
766
+ return "database";
767
+ }
768
+ if (!existsSync$1(stateFilePath)) return "missing-file";
769
+ writeAppliedMigrationsState(stateFilePath, createEmptyAppliedMigrationsState());
770
+ return "file";
771
+ };
772
+ const isMigrationApplied = (state, identity, checksum) => {
773
+ const matched = state.migrations.find((migration) => migration.id === identity);
774
+ if (!matched) return false;
775
+ if (checksum && matched.checksum) return matched.checksum === checksum;
776
+ if (checksum && !matched.checksum) return false;
777
+ return true;
778
+ };
779
+ const findAppliedMigration = (state, identity) => {
780
+ return state.migrations.find((migration) => migration.id === identity);
781
+ };
782
+ const markMigrationApplied = (state, entry) => {
783
+ const next = state.migrations.filter((migration) => migration.id !== entry.id);
784
+ next.push(entry);
785
+ return {
786
+ version: 1,
787
+ migrations: next,
788
+ runs: state.runs ?? []
789
+ };
790
+ };
791
+ const removeAppliedMigration = (state, identity) => {
792
+ return {
793
+ version: 1,
794
+ migrations: state.migrations.filter((migration) => migration.id !== identity),
795
+ runs: (state.runs ?? []).map((run) => ({
796
+ ...run,
797
+ migrationIds: run.migrationIds.filter((id) => id !== identity)
798
+ })).filter((run) => run.migrationIds.length > 0)
799
+ };
800
+ };
801
+ const buildMigrationRunId = () => {
802
+ return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
803
+ };
804
+ const markMigrationRun = (state, run) => {
805
+ const nextRuns = (state.runs ?? []).filter((existing) => existing.id !== run.id);
806
+ nextRuns.push(run);
807
+ return {
808
+ version: 1,
809
+ migrations: state.migrations,
810
+ runs: nextRuns
811
+ };
812
+ };
813
+ const getLastMigrationRun = (state) => {
814
+ const runs = state.runs ?? [];
815
+ if (runs.length === 0) return void 0;
816
+ return runs.map((run, index) => ({
817
+ run,
818
+ index
819
+ })).sort((left, right) => {
820
+ const appliedAtOrder = right.run.appliedAt.localeCompare(left.run.appliedAt);
821
+ if (appliedAtOrder !== 0) return appliedAtOrder;
822
+ return right.index - left.index;
823
+ })[0]?.run;
824
+ };
825
+ const getLatestAppliedMigrations = (state, steps) => {
826
+ return state.migrations.map((migration, index) => ({
827
+ migration,
828
+ index
829
+ })).sort((left, right) => {
830
+ const appliedAtOrder = right.migration.appliedAt.localeCompare(left.migration.appliedAt);
831
+ if (appliedAtOrder !== 0) return appliedAtOrder;
832
+ return right.index - left.index;
833
+ }).slice(0, Math.max(0, steps)).map((entry) => entry.migration);
834
+ };
835
+
836
+ //#endregion
837
+ //#region src/helpers/PrimaryKeyGenerationPlanner.ts
838
+ var PrimaryKeyGenerationPlanner = class {
839
+ static plan(column) {
840
+ if (!column.primary || column.default !== void 0) return void 0;
841
+ if (column.type === "uuid" || column.type === "string") return {
842
+ strategy: "uuid",
843
+ prismaDefault: "@default(uuid())",
844
+ databaseDefault: column.type === "uuid" ? "gen_random_uuid()" : "gen_random_uuid()::text",
845
+ runtimeFactory: "uuid"
846
+ };
847
+ }
848
+ static generate(generation) {
849
+ if (generation?.runtimeFactory === "uuid") return randomUUID();
850
+ }
851
+ };
852
+
853
+ //#endregion
854
+ //#region src/database/ForeignKeyBuilder.ts
855
+ /**
856
+ * The ForeignKeyBuilder class provides a fluent interface for defining
857
+ * foreign key constraints in a migration. It allows you to specify
858
+ * the referenced table and column, as well as actions to take on
859
+ * delete and aliases for the relation.
860
+ *
861
+ * @author Legacy (3m1n3nc3)
862
+ * @since 0.2.2
863
+ */
864
+ var ForeignKeyBuilder = class {
865
+ foreignKey;
866
+ constructor(foreignKey) {
867
+ this.foreignKey = foreignKey;
868
+ }
869
+ /**
870
+ * Defines the referenced table and column for this foreign key constraint.
871
+ *
872
+ * @param table
873
+ * @param column
874
+ * @returns
875
+ */
876
+ references(table, column) {
877
+ this.foreignKey.referencesTable = table;
878
+ this.foreignKey.referencesColumn = column;
879
+ return this;
880
+ }
881
+ /**
882
+ * Defines the action to take when a referenced record is deleted, such
883
+ * as "CASCADE", "SET NULL", or "RESTRICT".
884
+ *
885
+ * @param action
886
+ * @returns
887
+ */
888
+ onDelete(action) {
889
+ this.foreignKey.onDelete = action;
890
+ return this;
891
+ }
892
+ /**
893
+ * Defines an alias for the relation represented by this foreign key, which
894
+ * can be used in the ORM for more intuitive access to related models.
895
+ *
896
+ * @param name
897
+ * @returns
898
+ */
899
+ alias(name) {
900
+ this.foreignKey.relationAlias = name;
901
+ return this;
902
+ }
903
+ /**
904
+ * Defines an alias for the inverse relation represented by this foreign key.
905
+ *
906
+ * @param name
907
+ * @returns
908
+ */
909
+ inverseAlias(name) {
910
+ this.foreignKey.inverseRelationAlias = name;
911
+ return this;
912
+ }
913
+ /**
914
+ * Defines an alias for the foreign key field itself, which can be
915
+ * used in the ORM for more intuitive access to the foreign key value.
916
+ *
917
+ * @param fieldName
918
+ * @returns
919
+ */
920
+ as(fieldName) {
921
+ this.foreignKey.fieldAlias = fieldName;
922
+ return this;
923
+ }
924
+ };
925
+
926
+ //#endregion
927
+ //#region src/database/TableBuilder.ts
928
+ const PRISMA_ENUM_MEMBER_REGEX$1 = /^[A-Za-z][A-Za-z0-9_]*$/;
929
+ const normalizeEnumMember = (columnName, value) => {
930
+ const normalized = value.trim();
931
+ if (!normalized) throw new Error(`Enum column [${columnName}] must define only non-empty values.`);
932
+ if (!PRISMA_ENUM_MEMBER_REGEX$1.test(normalized)) throw new Error(`Enum column [${columnName}] contains invalid Prisma enum value [${normalized}].`);
933
+ return normalized;
934
+ };
935
+ const normalizeEnumMembers = (columnName, values) => {
936
+ const normalizedValues = values.map((value) => normalizeEnumMember(columnName, value));
937
+ const seen = /* @__PURE__ */ new Set();
938
+ for (const value of normalizedValues) {
939
+ if (seen.has(value)) throw new Error(`Enum column [${columnName}] contains duplicate enum value [${value}].`);
940
+ seen.add(value);
941
+ }
942
+ return normalizedValues;
943
+ };
944
+ /**
945
+ * The EnumBuilder class provides a fluent interface for configuring enum columns
946
+ * after they are defined on a table.
947
+ *
948
+ * @author Legacy (3m1n3nc3)
949
+ * @since 0.2.3
950
+ */
951
+ var EnumBuilder = class {
952
+ tableBuilder;
953
+ columnName;
954
+ constructor(tableBuilder, columnName) {
955
+ this.tableBuilder = tableBuilder;
956
+ this.columnName = columnName;
957
+ }
958
+ /**
959
+ * Defines the Prisma enum name for this column.
960
+ *
961
+ * @param name
962
+ * @returns
963
+ */
964
+ enumName(name) {
965
+ this.tableBuilder.enumName(name, this.columnName);
966
+ return this;
967
+ }
968
+ /**
969
+ * Marks the enum column as nullable.
970
+ *
971
+ * @returns
972
+ */
973
+ nullable() {
974
+ this.tableBuilder.nullable(this.columnName);
975
+ return this;
976
+ }
977
+ /**
978
+ * Marks the enum column as unique.
979
+ *
980
+ * @returns
981
+ */
982
+ unique() {
983
+ this.tableBuilder.unique(this.columnName);
984
+ return this;
985
+ }
986
+ /**
987
+ * Sets a default value for the enum column.
988
+ *
989
+ * @param value
990
+ * @returns
991
+ */
992
+ default(value) {
993
+ this.tableBuilder.default(value, this.columnName);
994
+ return this;
995
+ }
996
+ /**
997
+ * Positions the enum column after another column when supported.
998
+ *
999
+ * @param referenceColumn
1000
+ * @returns
1001
+ */
1002
+ after(referenceColumn) {
1003
+ this.tableBuilder.after(referenceColumn, this.columnName);
1004
+ return this;
1005
+ }
1006
+ /**
1007
+ * Maps the enum column to a custom database column name.
1008
+ *
1009
+ * @param name
1010
+ * @returns
1011
+ */
1012
+ map(name) {
1013
+ this.tableBuilder.map(name, this.columnName);
1014
+ return this;
1015
+ }
1016
+ };
1017
+ /**
1018
+ * The TableBuilder class provides a fluent interface for defining
1019
+ * the structure of a database table in a migration, including columns to add or drop.
1020
+ *
1021
+ * @author Legacy (3m1n3nc3)
1022
+ * @since 0.1.0
1023
+ */
1024
+ var TableBuilder = class {
1025
+ columns = [];
1026
+ dropColumnNames = [];
1027
+ indexes = [];
1028
+ foreignKeys = [];
1029
+ latestColumnName;
1030
+ /**
1031
+ * Defines a primary key column in the table.
1032
+ *
1033
+ * @param columnNameOrOptions
1034
+ * @param options
1035
+ * @returns
1036
+ */
1037
+ primary(columnNameOrOptions, options) {
1038
+ const config = typeof columnNameOrOptions === "string" ? {
1039
+ columnName: columnNameOrOptions,
1040
+ ...options ?? {}
1041
+ } : columnNameOrOptions ?? {};
1042
+ const column = this.resolveColumn(config.columnName);
1043
+ column.primary = true;
1044
+ if (typeof config.autoIncrement === "boolean") column.autoIncrement = config.autoIncrement;
1045
+ if (Object.prototype.hasOwnProperty.call(config, "default")) column.default = config.default;
1046
+ column.primaryKeyGeneration = PrimaryKeyGenerationPlanner.plan(column);
1047
+ return this;
1048
+ }
1049
+ /**
1050
+ * Defines an auto-incrementing primary key column.
1051
+ *
1052
+ * @param name The name of the primary key column.
1053
+ * @default 'id'
1054
+ * @returns The current TableBuilder instance for chaining.
1055
+ */
1056
+ id(name = "id", type = "id") {
1057
+ return this.column(name, type, { primary: true });
1058
+ }
1059
+ /**
1060
+ * Defines a UUID column in the table.
1061
+ *
1062
+ * @param name The name of the UUID column.
1063
+ * @param options Additional options for the UUID column.
1064
+ * @returns The current TableBuilder instance for chaining.
1065
+ */
1066
+ uuid(name, options = {}) {
1067
+ return this.column(name, "uuid", options);
1068
+ }
1069
+ /**
1070
+ * Defines an enum column in the table.
1071
+ *
1072
+ * @param name The name of the enum column.
1073
+ * @param values Either an array of string values for the enum or the name of an existing enum to reuse.
1074
+ * @param options Additional options for the enum column.
1075
+ * @returns
1076
+ */
1077
+ enum(name, valuesOrEnumName, options = {}) {
1078
+ const isEnumReuse = typeof valuesOrEnumName === "string";
1079
+ if (!isEnumReuse && valuesOrEnumName.length === 0) throw new Error(`Enum column [${name}] must define at least one value.`);
1080
+ const normalizedEnumValues = isEnumReuse ? void 0 : normalizeEnumMembers(name, valuesOrEnumName);
1081
+ const enumName = isEnumReuse ? valuesOrEnumName.trim() : options.enumName?.trim();
1082
+ if (isEnumReuse && !enumName) throw new Error(`Enum column [${name}] must define an enum name.`);
1083
+ this.column(name, "enum", {
1084
+ ...options,
1085
+ enumName,
1086
+ enumValues: normalizedEnumValues
1087
+ });
1088
+ return new EnumBuilder(this, name);
1089
+ }
1090
+ /**
1091
+ * Defines a string column in the table.
1092
+ *
1093
+ * @param name The name of the string column.
1094
+ * @param options Additional options for the string column.
1095
+ * @returns The current TableBuilder instance for chaining.
1096
+ */
1097
+ string(name, options = {}) {
1098
+ return this.column(name, "string", options);
1099
+ }
1100
+ /**
1101
+ * Defines a text column in the table.
1102
+ *
1103
+ * @param name The name of the text column.
1104
+ * @param options Additional options for the text column.
1105
+ * @returns The current TableBuilder instance for chaining.
1106
+ */
1107
+ text(name, options = {}) {
1108
+ return this.column(name, "text", options);
1109
+ }
1110
+ /**
1111
+ * Defines an integer column in the table.
1112
+ *
1113
+ * @param name The name of the integer column.
1114
+ * @param options Additional options for the integer column.
1115
+ * @returns The current TableBuilder instance for chaining.
1116
+ */
1117
+ integer(name, options = {}) {
1118
+ return this.column(name, "integer", options);
1119
+ }
1120
+ /**
1121
+ * Defines a big integer column in the table.
1122
+ *
1123
+ * @param name The name of the big integer column.
1124
+ * @param options Additional options for the big integer column.
1125
+ * @returns The current TableBuilder instance for chaining.
1126
+ */
1127
+ bigInteger(name, options = {}) {
1128
+ return this.column(name, "bigInteger", options);
1129
+ }
1130
+ /**
1131
+ * Defines a float column in the table.
1132
+ *
1133
+ * @param name The name of the float column.
1134
+ * @param options Additional options for the float column.
1135
+ * @returns The current TableBuilder instance for chaining.
1136
+ */
1137
+ float(name, options = {}) {
1138
+ return this.column(name, "float", options);
1139
+ }
1140
+ /**
1141
+ * Marks a column as unique in the table.
1142
+ *
1143
+ * @param name Optional explicit column name.
1144
+ * When omitted, applies to the latest defined column.
1145
+ * @returns The current TableBuilder instance for chaining.
1146
+ */
1147
+ unique(name) {
1148
+ const column = this.resolveColumn(name);
1149
+ column.unique = true;
1150
+ return this;
1151
+ }
1152
+ /**
1153
+ * Defines a boolean column in the table.
1154
+ *
1155
+ * @param name The name of the boolean column.
1156
+ * @param options Additional options for the boolean column.
1157
+ * @returns The current TableBuilder instance for chaining.
1158
+ */
1159
+ boolean(name, options = {}) {
1160
+ return this.column(name, "boolean", options);
1161
+ }
1162
+ /**
1163
+ * Defines a JSON column in the table.
1164
+ *
1165
+ * @param name The name of the JSON column.
1166
+ * @param options Additional options for the JSON column.
1167
+ * @returns
1168
+ */
1169
+ json(name, options = {}) {
1170
+ return this.column(name, "json", options);
1171
+ }
1172
+ /**
1173
+ * Defines a date column in the table.
1174
+ *
1175
+ * @param name The name of the date column.
1176
+ * @param options Additional options for the date column.
1177
+ * @returns
1178
+ */
1179
+ date(name, options = {}) {
1180
+ return this.column(name, "date", options);
1181
+ }
1182
+ /**
1183
+ * Defines colonns for a polymorphic relationship in the table.
1184
+ *
1185
+ * @param name The base name for the polymorphic relationship columns.
1186
+ * @returns
1187
+ */
1188
+ morphs(name, nullable = false) {
1189
+ this.string(`${name}Type`, { nullable });
1190
+ this.integer(`${name}Id`, { nullable });
1191
+ return this;
1192
+ }
1193
+ /**
1194
+ * Defines nullable columns for a polymorphic relationship in the table.
1195
+ *
1196
+ * @param name The base name for the polymorphic relationship columns.
1197
+ * @returns
1198
+ */
1199
+ nullableMorphs(name) {
1200
+ return this.morphs(name, true);
1201
+ }
1202
+ /**
1203
+ * Defines a timestamp column in the table.
1204
+ *
1205
+ * @param name The name of the timestamp column.
1206
+ * @param options Additional options for the timestamp column.
1207
+ * @returns
1208
+ */
1209
+ timestamp(name, options = {}) {
1210
+ return this.column(name, "timestamp", options);
1211
+ }
1212
+ /**
1213
+ * Defines both createdAt and updatedAt timestamp columns in the table.
1214
+ *
1215
+ * @returns
1216
+ */
1217
+ timestamps() {
1218
+ this.timestamp("createdAt", {
1219
+ nullable: false,
1220
+ default: "now()"
1221
+ });
1222
+ this.timestamp("updatedAt", {
1223
+ nullable: false,
1224
+ updatedAt: true
1225
+ });
1226
+ return this;
1227
+ }
1228
+ /**
1229
+ * Defines a soft delete timestamp column in the table.
1230
+ *
1231
+ * @param column The name of the soft delete column.
1232
+ * @returns
1233
+ */
1234
+ softDeletes(column = "deletedAt") {
1235
+ this.timestamp(column, { nullable: true });
1236
+ return this;
1237
+ }
1238
+ /**
1239
+ * Defines a column to be dropped from the table in an alterTable operation.
1240
+ *
1241
+ * @param name The name of the column to drop.
1242
+ * @returns
1243
+ */
1244
+ dropColumn(name) {
1245
+ this.dropColumnNames.push(name);
1246
+ return this;
1247
+ }
1248
+ /**
1249
+ * Marks a column as nullable.
1250
+ *
1251
+ * @param columnName Optional explicit column name. When omitted, applies to the latest defined column.
1252
+ * @returns The current TableBuilder instance for chaining.
1253
+ */
1254
+ nullable(columnName) {
1255
+ const column = this.resolveColumn(columnName);
1256
+ column.nullable = true;
1257
+ return this;
1258
+ }
1259
+ /**
1260
+ * Sets the Prisma enum name for an enum column.
1261
+ *
1262
+ * @param name The enum name to assign.
1263
+ * @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
1264
+ * @returns The current TableBuilder instance for chaining.
1265
+ */
1266
+ enumName(name, columnName) {
1267
+ const column = this.resolveColumn(columnName);
1268
+ if (column.type !== "enum") throw new Error(`Column [${column.name}] is not an enum column.`);
1269
+ const enumName = name.trim();
1270
+ if (!enumName) throw new Error(`Enum column [${column.name}] must define an enum name.`);
1271
+ column.enumName = enumName;
1272
+ return this;
1273
+ }
1274
+ /**
1275
+ * Sets a default value for a column.
1276
+ *
1277
+ * @param value The default scalar value or Prisma expression (e.g. 'now()').
1278
+ * @param columnName Optional explicit column name. When omitted, applies to the latest defined column.
1279
+ * @returns The current TableBuilder instance for chaining.
1280
+ */
1281
+ default(value, columnName) {
1282
+ const column = this.resolveColumn(columnName);
1283
+ column.default = value;
1284
+ return this;
1285
+ }
1286
+ /**
1287
+ * Sets the column position to appear after another column when possible.
1288
+ *
1289
+ * @param referenceColumn The column that the target column should be placed after.
1290
+ * @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
1291
+ * @returns The current TableBuilder instance for chaining.
1292
+ */
1293
+ after(referenceColumn, columnName) {
1294
+ const column = this.resolveColumn(columnName);
1295
+ column.after = referenceColumn;
1296
+ return this;
1297
+ }
1298
+ /**
1299
+ * Maps the column to a custom database column name.
1300
+ *
1301
+ * @param name The custom database column name.
1302
+ * @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
1303
+ * @returns The current TableBuilder instance for chaining.
1304
+ */
1305
+ map(name, columnName) {
1306
+ const column = this.resolveColumn(columnName);
1307
+ column.map = name;
1308
+ return this;
1309
+ }
1310
+ /**
1311
+ * Defines an index on one or more columns.
1312
+ *
1313
+ * @param columns Optional target columns. When omitted, applies to the latest defined column.
1314
+ * @param name Optional index name.
1315
+ * @returns The current TableBuilder instance for chaining.
1316
+ */
1317
+ index(columns, name) {
1318
+ const columnList = Array.isArray(columns) ? columns : typeof columns === "string" ? [columns] : [this.resolveColumn().name];
1319
+ this.indexes.push({
1320
+ columns: [...columnList],
1321
+ name
1322
+ });
1323
+ return this;
1324
+ }
1325
+ /**
1326
+ * Defines a foreign key relation for an existing column.
1327
+ *
1328
+ * @param column The local foreign key column name.
1329
+ * @returns A fluent foreign key builder.
1330
+ */
1331
+ foreignKey(column) {
1332
+ const entry = {
1333
+ column,
1334
+ referencesTable: "",
1335
+ referencesColumn: "id"
1336
+ };
1337
+ this.foreignKeys.push(entry);
1338
+ return new ForeignKeyBuilder(entry);
1339
+ }
1340
+ /**
1341
+ * Defines a foreign key relation for a column, using a
1342
+ * conventional naming pattern.
1343
+ *
1344
+ * @param column
1345
+ * @returns
1346
+ */
1347
+ foreign(column) {
1348
+ const columnName = this.resolveColumn(column).name;
1349
+ return this.foreignKey(column ?? columnName);
1350
+ }
1351
+ /**
1352
+ * Returns a deep copy of the defined columns for the table.
1353
+ *
1354
+ * @returns
1355
+ */
1356
+ getColumns() {
1357
+ return this.columns.map((column) => ({
1358
+ ...column,
1359
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
1360
+ }));
1361
+ }
1362
+ /**
1363
+ * Returns a copy of the defined column names to be dropped from the table.
1364
+ *
1365
+ * @returns
1366
+ */
1367
+ getDropColumns() {
1368
+ return [...this.dropColumnNames];
1369
+ }
1370
+ /**
1371
+ * Returns a deep copy of the defined indexes for the table.
1372
+ *
1373
+ * @returns
1374
+ */
1375
+ getIndexes() {
1376
+ return this.indexes.map((index) => ({
1377
+ ...index,
1378
+ columns: [...index.columns]
1379
+ }));
1380
+ }
1381
+ /**
1382
+ * Returns a deep copy of the defined foreign keys for the table.
1383
+ *
1384
+ * @returns
1385
+ */
1386
+ getForeignKeys() {
1387
+ return this.foreignKeys.map((foreignKey) => ({ ...foreignKey }));
1388
+ }
1389
+ /**
1390
+ * Defines a column in the table with the given name.
1391
+ *
1392
+ * @param name The name of the column.
1393
+ * @param type The type of the column.
1394
+ * @param options Additional options for the column.
1395
+ * @returns
1396
+ */
1397
+ column(name, type, options) {
1398
+ this.columns.push({
1399
+ name,
1400
+ type,
1401
+ enumName: options.enumName,
1402
+ enumValues: options.enumValues ? [...options.enumValues] : void 0,
1403
+ map: options.map,
1404
+ nullable: options.nullable,
1405
+ unique: options.unique,
1406
+ primary: options.primary,
1407
+ autoIncrement: options.autoIncrement,
1408
+ after: options.after,
1409
+ default: options.default,
1410
+ updatedAt: options.updatedAt,
1411
+ primaryKeyGeneration: options.primaryKeyGeneration
1412
+ });
1413
+ const column = this.columns[this.columns.length - 1];
1414
+ column.primaryKeyGeneration = PrimaryKeyGenerationPlanner.plan(column);
1415
+ this.latestColumnName = name;
1416
+ return this;
1417
+ }
1418
+ /**
1419
+ * Resolve a target column by name or fallback to the latest defined column.
1420
+ *
1421
+ * @param columnName
1422
+ * @returns
1423
+ */
1424
+ resolveColumn(columnName) {
1425
+ const targetName = columnName ?? this.latestColumnName;
1426
+ if (!targetName) throw new Error("No column available for this operation.");
1427
+ const column = this.columns.find((item) => item.name === targetName);
1428
+ if (!column) throw new Error(`Column [${targetName}] was not found in the table definition.`);
1429
+ return column;
1430
+ }
1431
+ };
1432
+
1433
+ //#endregion
1434
+ //#region src/database/SchemaBuilder.ts
1435
+ /**
1436
+ * The SchemaBuilder class provides methods for defining the operations to be
1437
+ * performed in a migration, such as creating, altering, or dropping tables.
1438
+ *
1439
+ * @author Legacy (3m1n3nc3)
1440
+ * @since 0.1.0
1441
+ */
1442
+ var SchemaBuilder = class {
1443
+ operations = [];
1444
+ /**
1445
+ * Defines a new table to be created in the migration.
1446
+ *
1447
+ * @param table The name of the table to create.
1448
+ * @param callback A callback function to define the table's columns and structure.
1449
+ * @returns The current SchemaBuilder instance for chaining.
1450
+ */
1451
+ createTable(table, callback) {
1452
+ const builder = new TableBuilder();
1453
+ callback(builder);
1454
+ this.operations.push({
1455
+ type: "createTable",
1456
+ table,
1457
+ columns: builder.getColumns(),
1458
+ indexes: builder.getIndexes(),
1459
+ foreignKeys: builder.getForeignKeys()
1460
+ });
1461
+ return this;
1462
+ }
1463
+ /**
1464
+ * Defines alterations to an existing table in the migration.
1465
+ *
1466
+ * @param table The name of the table to alter.
1467
+ * @param callback A callback function to define the alterations to the table's columns and structure.
1468
+ * @returns The current SchemaBuilder instance for chaining.
1469
+ */
1470
+ alterTable(table, callback) {
1471
+ const builder = new TableBuilder();
1472
+ callback(builder);
1473
+ this.operations.push({
1474
+ type: "alterTable",
1475
+ table,
1476
+ addColumns: builder.getColumns(),
1477
+ dropColumns: builder.getDropColumns(),
1478
+ addIndexes: builder.getIndexes(),
1479
+ addForeignKeys: builder.getForeignKeys()
1480
+ });
1481
+ return this;
1482
+ }
1483
+ /**
1484
+ * Defines a table to be dropped in the migration.
1485
+ *
1486
+ * @param table The name of the table to drop.
1487
+ * @returns The current SchemaBuilder instance for chaining.
1488
+ */
1489
+ dropTable(table) {
1490
+ this.operations.push({
1491
+ type: "dropTable",
1492
+ table
1493
+ });
1494
+ return this;
1495
+ }
1496
+ /**
1497
+ * Returns a deep copy of the defined schema operations for the migration/
1498
+ *
1499
+ * @returns An array of schema operations for the migration.
1500
+ */
1501
+ getOperations() {
1502
+ return this.operations.map((operation) => {
1503
+ if (operation.type === "createTable") return {
1504
+ ...operation,
1505
+ columns: operation.columns.map((column) => ({
1506
+ ...column,
1507
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
1508
+ })),
1509
+ indexes: operation.indexes.map((index) => ({
1510
+ ...index,
1511
+ columns: [...index.columns]
1512
+ })),
1513
+ foreignKeys: operation.foreignKeys.map((foreignKey) => ({ ...foreignKey }))
1514
+ };
1515
+ if (operation.type === "alterTable") return {
1516
+ ...operation,
1517
+ addColumns: operation.addColumns.map((column) => ({
1518
+ ...column,
1519
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
1520
+ })),
1521
+ dropColumns: [...operation.dropColumns],
1522
+ addIndexes: operation.addIndexes.map((index) => ({
1523
+ ...index,
1524
+ columns: [...index.columns]
1525
+ })),
1526
+ addForeignKeys: operation.addForeignKeys.map((foreignKey) => ({ ...foreignKey }))
1527
+ };
1528
+ return { ...operation };
1529
+ });
1530
+ }
1531
+ };
1532
+
1533
+ //#endregion
1534
+ //#region src/helpers/migrations.ts
1535
+ const PRISMA_MODEL_REGEX = /model\s+(\w+)\s*\{[\s\S]*?\n\}/g;
1536
+ const PRISMA_ENUM_REGEX = /enum\s+(\w+)\s*\{[\s\S]*?\n\}/g;
1537
+ const PRISMA_ENUM_MEMBER_REGEX = /^[A-Za-z][A-Za-z0-9_]*$/;
1538
+ /**
1539
+ * Convert a table name to a PascalCase model name, with basic singularization.
1540
+ *
1541
+ * @param tableName The name of the table to convert.
1542
+ * @returns The corresponding PascalCase model name.
1543
+ */
1544
+ const toModelName = (tableName) => {
1545
+ const normalized = tableName.replace(/[^a-zA-Z0-9]+/g, " ").trim();
1546
+ const parts = (normalized.endsWith("s") && normalized.length > 1 ? normalized.slice(0, -1) : normalized).split(/\s+/g).filter(Boolean);
1547
+ if (parts.length === 0) return "GeneratedModel";
1548
+ return parts.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
1549
+ };
1550
+ /**
1551
+ * Escape special characters in a string for use in a regular expression.
1552
+ *
1553
+ * @param value
1554
+ * @returns
1555
+ */
1556
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1557
+ /**
1558
+ * Convert a SchemaColumn definition to a Prisma field type string, including modifiers.
1559
+ *
1560
+ * @param column
1561
+ * @returns
1562
+ */
1563
+ const resolvePrismaType = (column) => {
1564
+ if (column.type === "id") return "Int";
1565
+ if (column.type === "enum") return resolveEnumName(column);
1566
+ if (column.type === "uuid") return "String";
1567
+ if (column.type === "string" || column.type === "text") return "String";
1568
+ if (column.type === "integer") return "Int";
1569
+ if (column.type === "bigInteger") return "BigInt";
1570
+ if (column.type === "float") return "Float";
1571
+ if (column.type === "boolean") return "Boolean";
1572
+ if (column.type === "json") return "Json";
1573
+ return "DateTime";
1574
+ };
1575
+ const resolveEnumName = (column) => {
1576
+ if (column.type !== "enum") throw new ArkormException(`Column [${column.name}] is not an enum column.`);
1577
+ if (column.enumName && column.enumName.trim().length > 0) return column.enumName.trim();
1578
+ throw new ArkormException(`Enum column [${column.name}] must define an enum name.`);
1579
+ };
1580
+ /**
1581
+ * Format a default value for inclusion in a Prisma schema field definition, based on its type.
1582
+ *
1583
+ * @param value
1584
+ * @returns
1585
+ */
1586
+ const formatDefaultValue = (value) => {
1587
+ if (value == null) return void 0;
1588
+ if (value === "now()") return "@default(now())";
1589
+ if (typeof value === "string") return `@default("${value.replace(/"/g, "\\\"")}")`;
1590
+ if (typeof value === "number" || typeof value === "bigint") return `@default(${value})`;
1591
+ if (typeof value === "boolean") return `@default(${value ? "true" : "false"})`;
1592
+ };
1593
+ /**
1594
+ * Format a default value for an enum column as a Prisma @default attribute, validating that it is a non-empty string.
1595
+ *
1596
+ * @param value
1597
+ * @returns
1598
+ */
1599
+ const formatEnumDefaultValue = (value) => {
1600
+ if (value == null) return void 0;
1601
+ if (typeof value !== "string" || value.trim().length === 0) throw new ArkormException("Enum default values must be provided as non-empty strings.");
1602
+ return `@default(${value.trim()})`;
1603
+ };
1604
+ /**
1605
+ * Normalize an enum value by ensuring it is a non-empty string and trimming whitespace.
1606
+ *
1607
+ * @param value
1608
+ * @returns
1609
+ */
1610
+ const normalizeEnumValue = (value) => {
1611
+ if (typeof value !== "string" || value.trim().length === 0) throw new ArkormException("Enum values must be provided as non-empty strings.");
1612
+ const normalized = value.trim();
1613
+ if (!PRISMA_ENUM_MEMBER_REGEX.test(normalized)) throw new ArkormException(`Enum value [${normalized}] is not a valid Prisma enum member name.`);
1614
+ return normalized;
1615
+ };
1616
+ /**
1617
+ * Extract the enum values from a Prisma enum block string.
1618
+ *
1619
+ * @param block
1620
+ * @returns
1621
+ */
1622
+ const extractEnumBlockValues = (block) => {
1623
+ return block.split("\n").slice(1, -1).map((line) => line.trim()).filter(Boolean);
1624
+ };
1625
+ const validateEnumValues = (column, enumName, enumValues) => {
1626
+ const normalizedValues = enumValues.map(normalizeEnumValue);
1627
+ const seen = /* @__PURE__ */ new Set();
1628
+ for (const value of normalizedValues) {
1629
+ if (seen.has(value)) throw new ArkormException(`Prisma enum [${enumName}] for column [${column.name}] contains duplicate value [${value}].`);
1630
+ seen.add(value);
1631
+ }
1632
+ return normalizedValues;
1633
+ };
1634
+ /**
1635
+ * Validate that a default value for an enum column is included in the defined enum values.
1636
+ *
1637
+ * @param column
1638
+ * @param enumName
1639
+ * @param enumValues
1640
+ * @returns
1641
+ */
1642
+ const validateEnumDefaultValue = (column, enumName, enumValues) => {
1643
+ if (column.default == null) return;
1644
+ const normalizedDefault = normalizeEnumValue(column.default);
1645
+ if (enumValues.includes(normalizedDefault)) return;
1646
+ throw new ArkormException(`Enum default value [${normalizedDefault}] is not defined in Prisma enum [${enumName}] for column [${column.name}].`);
1647
+ };
1648
+ /**
1649
+ * Build a single line of a Prisma model field definition based on a SchemaColumn, including type and modifiers.
1650
+ *
1651
+ * @param column
1652
+ * @returns
1653
+ */
1654
+ const buildFieldLine = (column) => {
1655
+ if (column.type === "id") {
1656
+ const primary = column.primary === false ? "" : " @id";
1657
+ const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
1658
+ const configuredDefault = formatDefaultValue(column.default);
1659
+ const shouldAutoIncrement = column.autoIncrement ?? column.primary !== false;
1660
+ const defaultSuffix = configuredDefault ? ` ${configuredDefault}` : shouldAutoIncrement && primary ? " @default(autoincrement())" : "";
1661
+ return ` ${column.name} Int${primary}${defaultSuffix}${mapped}`;
1662
+ }
1663
+ const scalar = resolvePrismaType(column);
1664
+ const nullable = column.nullable ? "?" : "";
1665
+ const unique = column.unique ? " @unique" : "";
1666
+ const primary = column.primary ? " @id" : "";
1667
+ const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
1668
+ const updatedAt = column.updatedAt ? " @updatedAt" : "";
1669
+ const defaultValue = column.type === "enum" ? formatEnumDefaultValue(column.default) : column.primaryKeyGeneration?.prismaDefault ?? formatDefaultValue(column.default);
1670
+ const defaultSuffix = defaultValue ? ` ${defaultValue}` : "";
1671
+ return ` ${column.name} ${scalar}${nullable}${primary}${unique}${defaultSuffix}${updatedAt}${mapped}`;
1672
+ };
1673
+ /**
1674
+ * Build a Prisma enum block string based on an enum name and its values, validating that
1675
+ * at least one value is provided.
1676
+ *
1677
+ * @param enumName The name of the enum to create.
1678
+ * @param values The array of values for the enum.
1679
+ * @returns The Prisma enum block string.
1680
+ */
1681
+ const buildEnumBlock = (enumName, values) => {
1682
+ if (values.length === 0) throw new ArkormException(`Enum [${enumName}] must define at least one value.`);
1683
+ return `enum ${enumName} {\n${values.map((value) => ` ${value}`).join("\n")}\n}`;
1684
+ };
1685
+ /**
1686
+ * Find the Prisma enum block in a schema string that corresponds to a given enum
1687
+ * name, returning its details if found.
1688
+ *
1689
+ * @param schema The Prisma schema string to search for the enum block.
1690
+ * @param enumName The name of the enum to find in the schema.
1691
+ * @returns
1692
+ */
1693
+ const findEnumBlock = (schema, enumName) => {
1694
+ const candidates = [...schema.matchAll(PRISMA_ENUM_REGEX)];
1695
+ for (const match of candidates) {
1696
+ const block = match[0];
1697
+ const matchedEnumName = match[1];
1698
+ const start = match.index ?? 0;
1699
+ const end = start + block.length;
1700
+ if (matchedEnumName === enumName) return {
1701
+ enumName: matchedEnumName,
1702
+ block,
1703
+ start,
1704
+ end
1705
+ };
1706
+ }
1707
+ return null;
1708
+ };
1709
+ /**
1710
+ * Ensure that Prisma enum blocks exist in the schema for any enum columns defined in a
1711
+ * create or alter table operation, adding them if necessary and validating against
1712
+ * existing blocks.
1713
+ *
1714
+ * @param schema The current Prisma schema string to check and modify.
1715
+ * @param columns The array of schema column definitions to check for enum types and ensure corresponding blocks exist for.
1716
+ * @returns
1717
+ */
1718
+ const ensureEnumBlocks = (schema, columns) => {
1719
+ let nextSchema = schema;
1720
+ for (const column of columns) {
1721
+ if (column.type !== "enum") continue;
1722
+ const enumName = resolveEnumName(column);
1723
+ const enumValues = column.enumValues ?? [];
1724
+ const existing = findEnumBlock(nextSchema, enumName);
1725
+ if (existing) {
1726
+ const existingValues = validateEnumValues(column, enumName, extractEnumBlockValues(existing.block));
1727
+ if (enumValues.length === 0) {
1728
+ validateEnumDefaultValue(column, enumName, existingValues);
1729
+ continue;
1730
+ }
1731
+ const normalizedEnumValues = validateEnumValues(column, enumName, enumValues);
1732
+ if (existingValues.join("|") !== normalizedEnumValues.join("|")) throw new ArkormException(`Prisma enum [${enumName}] already exists with different values.`);
1733
+ validateEnumDefaultValue(column, enumName, existingValues);
1734
+ continue;
1735
+ }
1736
+ if (enumValues.length === 0) throw new ArkormException(`Prisma enum [${enumName}] was not found for column [${column.name}].`);
1737
+ const normalizedEnumValues = validateEnumValues(column, enumName, enumValues);
1738
+ validateEnumDefaultValue(column, enumName, normalizedEnumValues);
1739
+ const block = buildEnumBlock(enumName, normalizedEnumValues);
1740
+ nextSchema = `${nextSchema.trimEnd()}\n\n${block}\n`;
1741
+ }
1742
+ return nextSchema;
1743
+ };
1744
+ /**
1745
+ * Build a Prisma model-level @@index definition line.
1746
+ *
1747
+ * @param index The schema index definition to convert to a Prisma \@\@index line.
1748
+ * @returns
1749
+ */
1750
+ const buildIndexLine = (index) => {
1751
+ return ` @@index([${index.columns.join(", ")}]${typeof index.name === "string" && index.name.trim().length > 0 ? `, name: "${index.name.replace(/"/g, "\\\"")}"` : ""})`;
1752
+ };
1753
+ /**
1754
+ * Derive a relation field name from a foreign key column name by applying
1755
+ * common conventions, such as removing "Id" suffixes and converting to camelCase.
1756
+ *
1757
+ * @param columnName The name of the foreign key column.
1758
+ * @returns The derived relation field name.
1759
+ */
1760
+ const deriveRelationFieldName = (columnName) => {
1761
+ const trimmed = columnName.trim();
1762
+ if (!trimmed) return "relation";
1763
+ if (trimmed.endsWith("Id") && trimmed.length > 2) {
1764
+ const root = trimmed.slice(0, -2);
1765
+ return `${root.charAt(0).toLowerCase()}${root.slice(1)}`;
1766
+ }
1767
+ if (trimmed.endsWith("_id") && trimmed.length > 3) return trimmed.slice(0, -3).replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
1768
+ return `${trimmed.charAt(0).toLowerCase()}${trimmed.slice(1)}`;
1769
+ };
1770
+ /**
1771
+ * Derive a relation name for both sides of a relation based on the
1772
+ * source and target model names, using an explicit alias if provided or a
1773
+ * convention of combining the full source model name with the target model name.
1774
+ *
1775
+ * @param sourceModelName The name of the source model in the relation.
1776
+ * @param targetModelName The name of the target model in the relation.
1777
+ * @param explicitAlias An optional explicit alias for the relation.
1778
+ * @returns The derived or explicit relation alias.
1779
+ */
1780
+ const deriveRelationAlias = (sourceModelName, targetModelName, explicitAlias) => {
1781
+ if (explicitAlias && explicitAlias.trim().length > 0) return explicitAlias.trim();
1782
+ return [sourceModelName, targetModelName].sort((left, right) => left.localeCompare(right)).join("");
1783
+ };
1784
+ const deriveInverseRelationAlias = deriveRelationAlias;
1785
+ const deriveSingularFieldName = (modelName) => {
1786
+ if (!modelName) return "item";
1787
+ return `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}`;
1788
+ };
1789
+ const deriveCollectionFieldName = (modelName) => {
1790
+ if (!modelName) return "items";
1791
+ const camel = `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}`;
1792
+ if (camel.endsWith("s")) return `${camel}es`;
1793
+ return `${camel}s`;
1794
+ };
1795
+ const resolveForeignKeyColumn = (columns, foreignKey) => {
1796
+ return columns.find((column) => column.name === foreignKey.column);
1797
+ };
1798
+ const isOneToOneForeignKey = (column) => {
1799
+ return Boolean(column?.unique || column?.primary);
1800
+ };
1801
+ /**
1802
+ * Format a SchemaForeignKeyAction value as a Prisma onDelete action string.
1803
+ *
1804
+ * @param action The foreign key action to format.
1805
+ * @returns The corresponding Prisma onDelete action string.
1806
+ */
1807
+ const formatRelationAction = (action) => {
1808
+ if (action === "cascade") return "Cascade";
1809
+ if (action === "restrict") return "Restrict";
1810
+ if (action === "setNull") return "SetNull";
1811
+ if (action === "setDefault") return "SetDefault";
1812
+ return "NoAction";
1813
+ };
1814
+ /**
1815
+ * Build a Prisma relation field line based on a SchemaForeignKey
1816
+ * definition, including relation name and onDelete action.
1817
+ *
1818
+ * @param foreignKey The foreign key definition to convert to a relation line.
1819
+ * @returns The corresponding Prisma schema line for the relation field.
1820
+ */
1821
+ const buildRelationLine = (sourceModelName, foreignKey, columns = []) => {
1822
+ if (!foreignKey.referencesTable.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced table.`);
1823
+ if (!foreignKey.referencesColumn.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced column.`);
1824
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1825
+ const fieldName = foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column);
1826
+ const targetModel = toModelName(foreignKey.referencesTable);
1827
+ const relationName = deriveRelationAlias(sourceModelName, targetModel, foreignKey.relationAlias?.trim());
1828
+ const optional = sourceColumn?.nullable ? "?" : "";
1829
+ const onDelete = foreignKey.onDelete ? `, onDelete: ${formatRelationAction(foreignKey.onDelete)}` : "";
1830
+ return ` ${fieldName} ${targetModel}${optional} @relation("${relationName.replace(/"/g, "\\\"")}", fields: [${foreignKey.column}], references: [${foreignKey.referencesColumn}]${onDelete})`;
1831
+ };
1832
+ /**
1833
+ * Build a Prisma relation field line for the inverse side of a relation, based
1834
+ * on the source and target model names and the foreign key definition, using
1835
+ * naming conventions and any explicit inverse alias provided.
1836
+ *
1837
+ * @param sourceModelName The name of the source model in the relation.
1838
+ * @param targetModelName The name of the target model in the relation.
1839
+ * @param foreignKey The foreign key definition for the relation.
1840
+ * @returns The Prisma schema line for the inverse relation field.
1841
+ */
1842
+ const buildInverseRelationLine = (sourceModelName, targetModelName, foreignKey, columns = []) => {
1843
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1844
+ const fieldName = isOneToOneForeignKey(sourceColumn) ? deriveSingularFieldName(sourceModelName) : deriveCollectionFieldName(sourceModelName);
1845
+ const relationName = deriveRelationAlias(sourceModelName, targetModelName, foreignKey.relationAlias?.trim());
1846
+ return ` ${fieldName} ${isOneToOneForeignKey(sourceColumn) ? `${sourceModelName}?` : `${sourceModelName}[]`} @relation("${relationName.replace(/"/g, "\\\"")}")`;
1847
+ };
1848
+ /**
1849
+ * Inject a line into the body of a Prisma model block if it does not already
1850
+ * exist, using a provided existence check function to determine if the line
1851
+ * is already present.
1852
+ *
1853
+ * @param bodyLines The lines of the model block body to modify.
1854
+ * @param line The line to inject if it does not already exist.
1855
+ * @param exists A function that checks if a given line already exists in the body.
1856
+ * @returns
1857
+ */
1858
+ const injectLineIntoModelBody = (bodyLines, line, exists) => {
1859
+ if (bodyLines.some(exists)) return bodyLines;
1860
+ const insertIndex = Math.max(1, bodyLines.length - 1);
1861
+ bodyLines.splice(insertIndex, 0, line);
1862
+ return bodyLines;
1863
+ };
1864
+ /**
1865
+ * Apply inverse relation definitions to a Prisma schema string based on the
1866
+ * foreign keys defined in a create or alter table operation, ensuring that
1867
+ * related models have corresponding relation fields for bi-directional navigation.
1868
+ *
1869
+ * @param schema The Prisma schema string to modify.
1870
+ * @param sourceModelName The name of the source model in the relation.
1871
+ * @param foreignKeys An array of foreign key definitions to process.
1872
+ * @returns The updated Prisma schema string with inverse relations applied.
1873
+ */
1874
+ const applyInverseRelations = (schema, sourceModelName, foreignKeys, columns = []) => {
1875
+ let nextSchema = schema;
1876
+ for (const foreignKey of foreignKeys) {
1877
+ const targetModel = findModelBlock(nextSchema, foreignKey.referencesTable);
1878
+ if (!targetModel) continue;
1879
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1880
+ const inverseLine = buildInverseRelationLine(sourceModelName, targetModel.modelName, foreignKey, columns);
1881
+ const targetBodyLines = targetModel.block.split("\n");
1882
+ const fieldName = isOneToOneForeignKey(sourceColumn) ? deriveSingularFieldName(sourceModelName) : deriveCollectionFieldName(sourceModelName);
1883
+ const fieldRegex = new RegExp(`^\\s*${escapeRegex(fieldName)}\\s+`);
1884
+ injectLineIntoModelBody(targetBodyLines, inverseLine, (line) => fieldRegex.test(line));
1885
+ const updatedTarget = targetBodyLines.join("\n");
1886
+ nextSchema = `${nextSchema.slice(0, targetModel.start)}${updatedTarget}${nextSchema.slice(targetModel.end)}`;
1887
+ }
1888
+ return nextSchema;
1889
+ };
1890
+ /**
1891
+ * Build a Prisma model block string based on a SchemaTableCreateOperation, including
1892
+ * all fields and any necessary mapping.
1893
+ *
1894
+ * @param operation The schema table create operation to convert.
1895
+ * @returns The corresponding Prisma model block string.
1896
+ */
1897
+ const buildModelBlock = (operation) => {
1898
+ const modelName = toModelName(operation.table);
1899
+ const mapped = operation.table !== modelName.toLowerCase();
1900
+ const fields = operation.columns.map(buildFieldLine);
1901
+ const relations = (operation.foreignKeys ?? []).map((foreignKey) => buildRelationLine(modelName, foreignKey, operation.columns));
1902
+ const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${str(operation.table).snake()}")`] : []];
1903
+ return `model ${modelName} {\n${(metadata.length > 0 ? [
1904
+ ...fields,
1905
+ ...relations,
1906
+ "",
1907
+ ...metadata
1908
+ ] : [...fields, ...relations]).join("\n")}\n}`;
1909
+ };
1910
+ /**
1911
+ * Find the Prisma model block in a schema string that corresponds to a given
1912
+ * table name, using both explicit mapping and naming conventions.
1913
+ *
1914
+ * @param schema
1915
+ * @param table
1916
+ * @returns
1917
+ */
1918
+ const findModelBlock = (schema, table) => {
1919
+ const candidates = [...schema.matchAll(PRISMA_MODEL_REGEX)];
1920
+ const explicitMapRegex = new RegExp(`@@map\\("${escapeRegex(table)}"\\)`);
1921
+ for (const match of candidates) {
1922
+ const block = match[0];
1923
+ const modelName = match[1];
1924
+ const start = match.index ?? 0;
1925
+ const end = start + block.length;
1926
+ if (explicitMapRegex.test(block)) return {
1927
+ modelName,
1928
+ block,
1929
+ start,
1930
+ end
1931
+ };
1932
+ if (modelName.toLowerCase() === table.toLowerCase()) return {
1933
+ modelName,
1934
+ block,
1935
+ start,
1936
+ end
1937
+ };
1938
+ if (modelName.toLowerCase() === toModelName(table).toLowerCase()) return {
1939
+ modelName,
1940
+ block,
1941
+ start,
1942
+ end
1943
+ };
1944
+ }
1945
+ return null;
1946
+ };
1947
+ /**
1948
+ * Apply a create table operation to a Prisma schema string, adding a new model
1949
+ * block for the specified table and fields.
1950
+ *
1951
+ * @param schema The current Prisma schema string.
1952
+ * @param operation The schema table create operation to apply.
1953
+ * @returns The updated Prisma schema string with the new model block.
1954
+ */
1955
+ const applyCreateTableOperation = (schema, operation) => {
1956
+ if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
1957
+ const schemaWithEnums = ensureEnumBlocks(schema, operation.columns);
1958
+ const block = buildModelBlock(operation);
1959
+ return applyInverseRelations(`${schemaWithEnums.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? [], operation.columns);
1960
+ };
1961
+ /**
1962
+ * Apply an alter table operation to a Prisma schema string, modifying the model
1963
+ * block for the specified table by adding and removing fields as needed.
1964
+ *
1965
+ * @param schema The current Prisma schema string.
1966
+ * @param operation The schema table alter operation to apply.
1967
+ * @returns The updated Prisma schema string with the modified model block.
1968
+ */
1969
+ const applyAlterTableOperation = (schema, operation) => {
1970
+ const model = findModelBlock(schema, operation.table);
1971
+ if (!model) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
1972
+ const schemaWithEnums = ensureEnumBlocks(schema, operation.addColumns);
1973
+ const refreshedModel = findModelBlock(schemaWithEnums, operation.table);
1974
+ if (!refreshedModel) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
1975
+ let block = refreshedModel.block;
1976
+ const bodyLines = block.split("\n");
1977
+ operation.dropColumns.forEach((column) => {
1978
+ const columnRegex = new RegExp(`^\\s*${escapeRegex(column)}\\s+`);
1979
+ for (let index = 0; index < bodyLines.length; index += 1) if (columnRegex.test(bodyLines[index])) {
1980
+ bodyLines.splice(index, 1);
1981
+ return;
1982
+ }
1983
+ });
1984
+ operation.addColumns.forEach((column) => {
1985
+ const fieldLine = buildFieldLine(column);
1986
+ const columnRegex = new RegExp(`^\\s*${escapeRegex(column.name)}\\s+`);
1987
+ if (bodyLines.some((line) => columnRegex.test(line))) return;
1988
+ const defaultInsertIndex = Math.max(1, bodyLines.length - 1);
1989
+ const afterInsertIndex = typeof column.after === "string" && column.after.length > 0 ? bodyLines.findIndex((line) => new RegExp(`^\\s*${escapeRegex(column.after)}\\s+`).test(line)) : -1;
1990
+ const insertIndex = afterInsertIndex > 0 ? Math.min(afterInsertIndex + 1, defaultInsertIndex) : defaultInsertIndex;
1991
+ bodyLines.splice(insertIndex, 0, fieldLine);
1992
+ });
1993
+ (operation.addIndexes ?? []).forEach((index) => {
1994
+ const indexLine = buildIndexLine(index);
1995
+ if (bodyLines.some((line) => line.trim() === indexLine.trim())) return;
1996
+ const insertIndex = Math.max(1, bodyLines.length - 1);
1997
+ bodyLines.splice(insertIndex, 0, indexLine);
1998
+ });
1999
+ for (const foreignKey of operation.addForeignKeys ?? []) {
2000
+ const relationLine = buildRelationLine(model.modelName, foreignKey, operation.addColumns);
2001
+ const relationRegex = new RegExp(`^\\s*${escapeRegex(foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column))}\\s+`);
2002
+ injectLineIntoModelBody(bodyLines, relationLine, (line) => relationRegex.test(line));
2003
+ }
2004
+ block = bodyLines.join("\n");
2005
+ return applyInverseRelations(`${schemaWithEnums.slice(0, refreshedModel.start)}${block}${schemaWithEnums.slice(refreshedModel.end)}`, model.modelName, operation.addForeignKeys ?? [], operation.addColumns);
2006
+ };
2007
+ /**
2008
+ * Apply a drop table operation to a Prisma schema string, removing the model block
2009
+ * for the specified table.
2010
+ */
2011
+ const applyDropTableOperation = (schema, operation) => {
2012
+ const model = findModelBlock(schema, operation.table);
2013
+ if (!model) return schema;
2014
+ const before = schema.slice(0, model.start).trimEnd();
2015
+ const after = schema.slice(model.end).trimStart();
2016
+ return `${before}${before && after ? "\n\n" : ""}${after}`;
2017
+ };
2018
+ /**
2019
+ * The SchemaBuilder class provides a fluent interface for defining
2020
+ * database schema operations in a migration, such as creating, altering, and
2021
+ * dropping tables.
2022
+ *
2023
+ * @param schema The current Prisma schema string.
2024
+ * @param operations The list of schema operations to apply.
2025
+ * @returns The updated Prisma schema string after applying all operations.
2026
+ */
2027
+ const applyOperationsToPrismaSchema = (schema, operations) => {
2028
+ return operations.reduce((current, operation) => {
2029
+ if (operation.type === "createTable") return applyCreateTableOperation(current, operation);
2030
+ if (operation.type === "alterTable") return applyAlterTableOperation(current, operation);
2031
+ return applyDropTableOperation(current, operation);
2032
+ }, schema);
2033
+ };
2034
+ /**
2035
+ * Run a Prisma CLI command using npx, capturing and throwing any errors that occur.
2036
+ *
2037
+ * @param args The arguments to pass to the Prisma CLI command.
2038
+ * @param cwd The current working directory to run the command in.
2039
+ * @returns void
2040
+ */
2041
+ const runPrismaCommand = (args, cwd) => {
2042
+ const command = spawnSync("npx", ["prisma", ...args], {
2043
+ cwd,
2044
+ encoding: "utf-8"
2045
+ });
2046
+ if (command.status === 0) return;
2047
+ const errorOutput = [command.stdout, command.stderr].filter(Boolean).join("\n").trim();
2048
+ throw new ArkormException(errorOutput ? `Prisma command failed: prisma ${args.join(" ")}\n${errorOutput}` : `Prisma command failed: prisma ${args.join(" ")}`);
2049
+ };
2050
+ /**
2051
+ * Generate a new migration file with a given name and options, including
2052
+ * writing the file to disk if specified.
2053
+ *
2054
+ * @param name
2055
+ * @returns
2056
+ */
2057
+ const resolveMigrationClassName = (name) => {
2058
+ const cleaned = name.replace(/[^a-zA-Z0-9]+/g, " ").trim();
2059
+ if (!cleaned) return "GeneratedMigration";
2060
+ return `${cleaned.split(/\s+/g).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("")}Migration`;
2061
+ };
2062
+ /**
2063
+ * Pad a number with leading zeros to ensure it is at least two digits, for
2064
+ * use in migration timestamps.
2065
+ *
2066
+ * @param value
2067
+ * @returns
2068
+ */
2069
+ const pad = (value) => String(value).padStart(2, "0");
2070
+ /**
2071
+ * Create a timestamp string in the format YYYYMMDDHHMMSS for use in migration
2072
+ * file names, based on the current date and time or a provided date.
2073
+ *
2074
+ * @param date
2075
+ * @returns
2076
+ */
2077
+ const createMigrationTimestamp = (date = /* @__PURE__ */ new Date()) => {
2078
+ return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
2079
+ };
2080
+ /**
2081
+ * Convert a migration name to a slug suitable for use in a file name, by
2082
+ * lowercasing and replacing non-alphanumeric characters with underscores.
2083
+ *
2084
+ * @param name
2085
+ * @returns
2086
+ */
2087
+ const toMigrationFileSlug = (name) => {
2088
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "migration";
2089
+ };
2090
+ /**
2091
+ * Build the source code for a new migration file based on a given class
2092
+ * name, using a template with empty up and down methods.
2093
+ *
2094
+ * @param className
2095
+ * @returns
2096
+ */
2097
+ const buildMigrationSource = (className, extension = "ts") => {
2098
+ if (extension === "js") return [
2099
+ "import { Migration } from 'arkormx'",
2100
+ "",
2101
+ `export default class ${className} extends Migration {`,
2102
+ " /**",
2103
+ " * @param {import('arkormx').SchemaBuilder} schema",
2104
+ " * @returns {Promise<void>}",
2105
+ " */",
2106
+ " async up (schema) {",
2107
+ " }",
2108
+ "",
2109
+ " /**",
2110
+ " * @param {import('arkormx').SchemaBuilder} schema",
2111
+ " * @returns {Promise<void>}",
2112
+ " */",
2113
+ " async down (schema) {",
2114
+ " }",
2115
+ "}",
2116
+ ""
2117
+ ].join("\n");
2118
+ return [
2119
+ "import { Migration, SchemaBuilder } from 'arkormx'",
2120
+ "",
2121
+ `export default class ${className} extends Migration {`,
2122
+ " public async up (schema: SchemaBuilder): Promise<void> {",
2123
+ " }",
2124
+ "",
2125
+ " public async down (schema: SchemaBuilder): Promise<void> {",
2126
+ " }",
2127
+ "}",
2128
+ ""
2129
+ ].join("\n");
2130
+ };
2131
+ /**
2132
+ * Generate a new migration file with a given name and options, including
2133
+ * writing the file to disk if specified, and return the details of the generated file.
2134
+ *
2135
+ * @param name
2136
+ * @param options
2137
+ * @returns
2138
+ */
2139
+ const generateMigrationFile = (name, options = {}) => {
2140
+ const timestamp = createMigrationTimestamp(/* @__PURE__ */ new Date());
2141
+ const fileSlug = toMigrationFileSlug(name);
2142
+ const className = resolveMigrationClassName(name);
2143
+ const extension = options.extension ?? "ts";
2144
+ const directory = options.directory ?? join(process.cwd(), "database", "migrations");
2145
+ const fileName = `${timestamp}_${fileSlug}.${extension}`;
2146
+ const filePath = join(directory, fileName);
2147
+ const content = buildMigrationSource(className, extension);
2148
+ if (options.write ?? true) {
2149
+ if (!existsSync$1(directory)) mkdirSync$1(directory, { recursive: true });
2150
+ if (existsSync$1(filePath)) throw new ArkormException(`Migration file already exists: ${filePath}`);
2151
+ writeFileSync$1(filePath, content);
2152
+ }
2153
+ return {
2154
+ fileName,
2155
+ filePath,
2156
+ className,
2157
+ content
2158
+ };
2159
+ };
2160
+ /**
2161
+ * Get the list of schema operations that would be performed by a given migration class when run in a specified direction (up or down), without actually applying them.
2162
+ *
2163
+ * @param migration The migration class or instance to analyze.
2164
+ * @param direction The direction of the migration to plan for ('up' or 'down').
2165
+ * @returns A promise that resolves to an array of schema operations that would be performed.
2166
+ */
2167
+ const getMigrationPlan = async (migration, direction = "up") => {
2168
+ const instance = typeof migration === "function" ? new migration() : migration;
2169
+ const schema = new SchemaBuilder();
2170
+ if (direction === "up") await instance.up(schema);
2171
+ else await instance.down(schema);
2172
+ return schema.getOperations();
2173
+ };
2174
+ const supportsDatabaseMigrationExecution = (adapter) => {
2175
+ return typeof adapter?.executeSchemaOperations === "function";
2176
+ };
2177
+ const supportsDatabaseReset = (adapter) => {
2178
+ return typeof adapter?.resetDatabase === "function";
2179
+ };
2180
+ const stripPrismaSchemaModelsAndEnums = (schema) => {
2181
+ const stripped = schema.replace(PRISMA_MODEL_REGEX, "").replace(PRISMA_ENUM_REGEX, "").replace(/\n{3,}/g, "\n\n").trimEnd();
2182
+ return stripped.length > 0 ? `${stripped}\n` : "";
2183
+ };
2184
+ const applyMigrationToDatabase = async (adapter, migration) => {
2185
+ if (!supportsDatabaseMigrationExecution(adapter)) throw new ArkormException("The configured adapter does not support database-backed migration execution.");
2186
+ const operations = await getMigrationPlan(migration, "up");
2187
+ await adapter.executeSchemaOperations(operations);
2188
+ return { operations };
2189
+ };
2190
+ const applyMigrationRollbackToDatabase = async (adapter, migration) => {
2191
+ if (!supportsDatabaseMigrationExecution(adapter)) throw new ArkormException("The configured adapter does not support database-backed migration execution.");
2192
+ const operations = await getMigrationPlan(migration, "down");
2193
+ await adapter.executeSchemaOperations(operations);
2194
+ return { operations };
2195
+ };
2196
+ /**
2197
+ * Apply the schema operations defined in a migration to a Prisma schema
2198
+ * file, updating the file on disk if specified, and return the updated
2199
+ * schema and list of operations applied.
2200
+ *
2201
+ * @param migration The migration class or instance to apply.
2202
+ * @param options Options for applying the migration, including schema path and write flag.
2203
+ * @returns A promise that resolves to an object containing the updated schema, schema path, and list of operations applied.
2204
+ */
2205
+ const applyMigrationToPrismaSchema = async (migration, options = {}) => {
2206
+ const schemaPath = options.schemaPath ?? join(process.cwd(), "prisma", "schema.prisma");
2207
+ if (!existsSync$1(schemaPath)) throw new ArkormException(`Prisma schema file not found: ${schemaPath}`);
2208
+ const source = readFileSync$1(schemaPath, "utf-8");
2209
+ const operations = await getMigrationPlan(migration, "up");
2210
+ const schema = applyOperationsToPrismaSchema(source, operations);
2211
+ if (options.write ?? true) writeFileSync$1(schemaPath, schema);
2212
+ return {
2213
+ schema,
2214
+ schemaPath,
2215
+ operations
2216
+ };
2217
+ };
2218
+ /**
2219
+ * Apply the rollback (down) operations defined in a migration to a Prisma schema file.
2220
+ *
2221
+ * @param migration The migration class or instance to rollback.
2222
+ * @param options Options for applying the rollback, including schema path and write flag.
2223
+ * @returns A promise that resolves to an object containing the updated schema, schema path, and rollback operations applied.
2224
+ */
2225
+ const applyMigrationRollbackToPrismaSchema = async (migration, options = {}) => {
2226
+ const schemaPath = options.schemaPath ?? join(process.cwd(), "prisma", "schema.prisma");
2227
+ if (!existsSync$1(schemaPath)) throw new ArkormException(`Prisma schema file not found: ${schemaPath}`);
2228
+ const source = readFileSync$1(schemaPath, "utf-8");
2229
+ const operations = await getMigrationPlan(migration, "down");
2230
+ const schema = applyOperationsToPrismaSchema(source, operations);
2231
+ if (options.write ?? true) writeFileSync$1(schemaPath, schema);
2232
+ return {
2233
+ schema,
2234
+ schemaPath,
2235
+ operations
2236
+ };
2237
+ };
2238
+ /**
2239
+ * Run a migration by applying its schema operations to a Prisma schema
2240
+ * file, optionally generating Prisma client code and running migrations after
2241
+ * applying the schema changes.
2242
+ *
2243
+ * @param migration The migration class or instance to run.
2244
+ * @param options Options for running the migration, including schema path, write flag, and Prisma commands.
2245
+ * @returns A promise that resolves to an object containing the schema path and list of operations applied.
2246
+ */
2247
+ const runMigrationWithPrisma = async (migration, options = {}) => {
2248
+ const cwd = options.cwd ?? process.cwd();
2249
+ const applied = await applyMigrationToPrismaSchema(migration, {
2250
+ schemaPath: options.schemaPath ?? join(cwd, "prisma", "schema.prisma"),
2251
+ write: options.write
2252
+ });
2253
+ const shouldGenerate = options.runGenerate ?? true;
2254
+ const shouldMigrate = options.runMigrate ?? true;
2255
+ const mode = options.migrateMode ?? "dev";
2256
+ if (shouldGenerate) runPrismaCommand(["generate"], cwd);
2257
+ if (shouldMigrate) if (mode === "deploy") runPrismaCommand(["migrate", "deploy"], cwd);
2258
+ else runPrismaCommand([
2259
+ "migrate",
2260
+ "dev",
2261
+ "--name",
2262
+ options.migrationName ?? `arkorm_${createMigrationTimestamp()}`
2263
+ ], cwd);
2264
+ return {
2265
+ schemaPath: applied.schemaPath,
2266
+ operations: applied.operations
2267
+ };
2268
+ };
2269
+
2270
+ //#endregion
2271
+ //#region src/helpers/column-mappings.ts
2272
+ let cachedColumnMappingsPath;
2273
+ let cachedColumnMappingsState;
2274
+ const resolvePersistedMetadataFeatures = (features) => {
2275
+ return {
2276
+ persistedColumnMappings: features?.persistedColumnMappings !== false,
2277
+ persistedEnums: features?.persistedEnums !== false
2278
+ };
2279
+ };
2280
+ const createEmptyPersistedColumnMappingsState = () => ({
2281
+ version: 1,
2282
+ tables: {}
2283
+ });
2284
+ const resolveColumnMappingsFilePath = (cwd, configuredPath) => {
2285
+ if (configuredPath && configuredPath.trim().length > 0) return resolve(configuredPath);
2286
+ return join(cwd, ".arkormx", "column-mappings.json");
2287
+ };
2288
+ const normalizePersistedEnumValues = (values) => {
2289
+ if (!Array.isArray(values)) return [];
2290
+ return values.filter((value) => typeof value === "string" && value.trim().length > 0);
2291
+ };
2292
+ const normalizeLegacyTableColumns = (columns) => {
2293
+ return Object.entries(columns).reduce((mapped, [attribute, column]) => {
2294
+ if (attribute.trim().length === 0) return mapped;
2295
+ if (typeof column !== "string" || column.trim().length === 0) return mapped;
2296
+ mapped[attribute] = column;
2297
+ return mapped;
2298
+ }, {});
2299
+ };
2300
+ const normalizePersistedTableMetadata = (table) => {
2301
+ if (!table || typeof table !== "object" || Array.isArray(table)) return {
2302
+ columns: {},
2303
+ enums: {}
2304
+ };
2305
+ const candidate = table;
2306
+ if (!(Object.prototype.hasOwnProperty.call(candidate, "columns") || Object.prototype.hasOwnProperty.call(candidate, "enums") || Object.prototype.hasOwnProperty.call(candidate, "primaryKeyGeneration") || Object.prototype.hasOwnProperty.call(candidate, "timestampColumns"))) return {
2307
+ columns: normalizeLegacyTableColumns(candidate),
2308
+ enums: {}
2309
+ };
2310
+ return {
2311
+ columns: normalizeLegacyTableColumns(candidate.columns ?? {}),
2312
+ enums: Object.entries(candidate.enums ?? {}).reduce((all, [columnName, values]) => {
2313
+ if (columnName.trim().length === 0) return all;
2314
+ const normalizedValues = normalizePersistedEnumValues(values);
2315
+ if (normalizedValues.length > 0) all[columnName] = normalizedValues;
2316
+ return all;
2317
+ }, {}),
2318
+ primaryKeyGeneration: normalizePersistedPrimaryKeyGeneration(candidate.primaryKeyGeneration),
2319
+ timestampColumns: normalizePersistedTimestampColumns(candidate.timestampColumns)
2320
+ };
2321
+ };
2322
+ const normalizePersistedPrimaryKeyGeneration = (value) => {
2323
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2324
+ const candidate = value;
2325
+ if (candidate.strategy !== "uuid" || typeof candidate.column !== "string" || candidate.column.trim().length === 0) return void 0;
2326
+ return {
2327
+ column: candidate.column,
2328
+ strategy: "uuid",
2329
+ prismaDefault: typeof candidate.prismaDefault === "string" && candidate.prismaDefault.trim().length > 0 ? candidate.prismaDefault : void 0,
2330
+ databaseDefault: typeof candidate.databaseDefault === "string" && candidate.databaseDefault.trim().length > 0 ? candidate.databaseDefault : void 0,
2331
+ runtimeFactory: candidate.runtimeFactory === "uuid" ? "uuid" : void 0
2332
+ };
2333
+ };
2334
+ const normalizePersistedTimestampColumns = (value) => {
2335
+ if (!Array.isArray(value)) return void 0;
2336
+ const columns = value.reduce((all, entry) => {
2337
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return all;
2338
+ const candidate = entry;
2339
+ if (typeof candidate.column !== "string" || candidate.column.trim().length === 0) return all;
2340
+ const normalized = { column: candidate.column };
2341
+ if (candidate.default === "now()") normalized.default = "now()";
2342
+ if (candidate.updatedAt === true) normalized.updatedAt = true;
2343
+ if (!normalized.default && !normalized.updatedAt) return all;
2344
+ all.push(normalized);
2345
+ return all;
2346
+ }, []);
2347
+ return columns.length > 0 ? columns : void 0;
2348
+ };
2349
+ const normalizePersistedColumnMappingsState = (state) => {
2350
+ return {
2351
+ version: 1,
2352
+ tables: Object.entries(state?.tables ?? {}).reduce((all, [tableName, tableMetadata]) => {
2353
+ if (tableName.trim().length === 0) return all;
2354
+ const normalized = normalizePersistedTableMetadata(tableMetadata);
2355
+ if (Object.keys(normalized.columns).length > 0 || Object.keys(normalized.enums).length > 0 || normalized.primaryKeyGeneration || normalized.timestampColumns?.length) all[tableName] = normalized;
2356
+ return all;
2357
+ }, {})
2358
+ };
2359
+ };
2360
+ const buildPersistedFeatureDisabledError = (feature, table) => {
2361
+ return new ArkormException(`Table [${table}] requires ${feature === "persistedColumnMappings" ? "persisted column mappings" : "persisted enum metadata"}, but ${feature === "persistedColumnMappings" ? "features.persistedColumnMappings" : "features.persistedEnums"} is disabled in arkormx.config.*.`, {
2362
+ operation: "metadata.persisted",
2363
+ meta: {
2364
+ feature,
2365
+ table
2366
+ }
2367
+ });
2368
+ };
2369
+ const assertPersistedTableMetadataEnabled = (table, metadata, features, strict) => {
2370
+ if (!strict) return;
2371
+ if (!features.persistedColumnMappings && Object.keys(metadata.columns).length > 0) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
2372
+ if (!features.persistedEnums && Object.keys(metadata.enums).length > 0) throw buildPersistedFeatureDisabledError("persistedEnums", table);
2373
+ };
2374
+ const buildEnumUnionType = (values) => {
2375
+ return values.map((value) => {
2376
+ return `'${value.replace(/'/g, String.raw`\'`)}'`;
2377
+ }).join(" | ");
2378
+ };
2379
+ const resetPersistedColumnMappingsCache = () => {
2380
+ cachedColumnMappingsPath = void 0;
2381
+ cachedColumnMappingsState = void 0;
2382
+ };
2383
+ const readPersistedColumnMappingsState = (filePath) => {
2384
+ if (cachedColumnMappingsPath === filePath && cachedColumnMappingsState) return cachedColumnMappingsState;
2385
+ if (!existsSync$1(filePath)) {
2386
+ const empty = createEmptyPersistedColumnMappingsState();
2387
+ cachedColumnMappingsPath = filePath;
2388
+ cachedColumnMappingsState = empty;
2389
+ return empty;
2390
+ }
2391
+ try {
2392
+ const normalized = normalizePersistedColumnMappingsState(JSON.parse(readFileSync$1(filePath, "utf-8")));
2393
+ cachedColumnMappingsPath = filePath;
2394
+ cachedColumnMappingsState = normalized;
2395
+ return normalized;
2396
+ } catch {
2397
+ const empty = createEmptyPersistedColumnMappingsState();
2398
+ cachedColumnMappingsPath = filePath;
2399
+ cachedColumnMappingsState = empty;
2400
+ return empty;
2401
+ }
2402
+ };
2403
+ const writePersistedColumnMappingsState = (filePath, state) => {
2404
+ const normalized = normalizePersistedColumnMappingsState(state);
2405
+ const directory = dirname(filePath);
2406
+ if (!existsSync$1(directory)) mkdirSync$1(directory, { recursive: true });
2407
+ writeFileSync$1(filePath, JSON.stringify(normalized, null, 2));
2408
+ cachedColumnMappingsPath = filePath;
2409
+ cachedColumnMappingsState = normalized;
2410
+ };
2411
+ const deletePersistedColumnMappingsState = (filePath) => {
2412
+ if (existsSync$1(filePath)) rmSync$1(filePath, { force: true });
2413
+ resetPersistedColumnMappingsCache();
2414
+ };
2415
+ const getPersistedTableMetadata = (table, options = {}) => {
2416
+ const metadata = readPersistedColumnMappingsState(resolveColumnMappingsFilePath(options.cwd ?? process.cwd(), options.configuredPath)).tables[table] ?? {
2417
+ columns: {},
2418
+ enums: {}
2419
+ };
2420
+ assertPersistedTableMetadataEnabled(table, metadata, options.features ?? resolvePersistedMetadataFeatures(), options.strict ?? false);
2421
+ return {
2422
+ columns: { ...metadata.columns },
2423
+ enums: Object.entries(metadata.enums).reduce((all, [columnName, values]) => {
2424
+ all[columnName] = [...values];
2425
+ return all;
2426
+ }, {}),
2427
+ primaryKeyGeneration: metadata.primaryKeyGeneration ? { ...metadata.primaryKeyGeneration } : void 0,
2428
+ timestampColumns: metadata.timestampColumns?.map((column) => ({ ...column }))
2429
+ };
2430
+ };
2431
+ const getPersistedColumnMap = (table, options = {}) => {
2432
+ return getPersistedTableMetadata(table, options).columns;
2433
+ };
2434
+ const getPersistedEnumMap = (table, options = {}) => {
2435
+ return getPersistedTableMetadata(table, options).enums;
2436
+ };
2437
+ const getPersistedPrimaryKeyGeneration = (table, options = {}) => {
2438
+ return getPersistedTableMetadata(table, options).primaryKeyGeneration;
2439
+ };
2440
+ const getPersistedTimestampColumns = (table, options = {}) => {
2441
+ return getPersistedTableMetadata(table, options).timestampColumns ?? [];
2442
+ };
2443
+ const applyMappedColumn = (tableColumns, column, features, table) => {
2444
+ if (typeof column.map === "string" && column.map.trim().length > 0 && column.map !== column.name) {
2445
+ if (!features.persistedColumnMappings) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
2446
+ tableColumns[column.name] = column.map;
2447
+ return;
2448
+ }
2449
+ delete tableColumns[column.name];
2450
+ };
2451
+ const applyEnumColumn = (tableEnums, column, features, table) => {
2452
+ const values = column.enumValues ?? [];
2453
+ if (column.type === "enum" && values.length > 0) {
2454
+ if (!features.persistedEnums) throw buildPersistedFeatureDisabledError("persistedEnums", table);
2455
+ tableEnums[column.name] = [...values];
2456
+ return;
2457
+ }
2458
+ delete tableEnums[column.name];
2459
+ };
2460
+ const removePersistedColumnMetadata = (tableMetadata, columnName) => {
2461
+ delete tableMetadata.columns[columnName];
2462
+ delete tableMetadata.enums[columnName];
2463
+ Object.entries(tableMetadata.columns).forEach(([attribute, mappedColumn]) => {
2464
+ if (mappedColumn === columnName) delete tableMetadata.columns[attribute];
2465
+ });
2466
+ if (tableMetadata.primaryKeyGeneration?.column === columnName) delete tableMetadata.primaryKeyGeneration;
2467
+ if (tableMetadata.timestampColumns) {
2468
+ tableMetadata.timestampColumns = tableMetadata.timestampColumns.filter((column) => column.column !== columnName);
2469
+ if (tableMetadata.timestampColumns.length === 0) delete tableMetadata.timestampColumns;
2470
+ }
2471
+ };
2472
+ const applyPrimaryKeyGeneration = (tableMetadata, column) => {
2473
+ if (!column.primary || !column.primaryKeyGeneration) {
2474
+ if (tableMetadata.primaryKeyGeneration?.column === column.name) delete tableMetadata.primaryKeyGeneration;
2475
+ return;
2476
+ }
2477
+ tableMetadata.primaryKeyGeneration = {
2478
+ column: column.name,
2479
+ ...column.primaryKeyGeneration
2480
+ };
2481
+ };
2482
+ const applyTimestampColumn = (tableMetadata, column) => {
2483
+ if (column.type !== "timestamp" || column.default !== "now()" && column.updatedAt !== true) {
2484
+ if (tableMetadata.timestampColumns) {
2485
+ tableMetadata.timestampColumns = tableMetadata.timestampColumns.filter((entry) => entry.column !== column.name);
2486
+ if (tableMetadata.timestampColumns.length === 0) delete tableMetadata.timestampColumns;
2487
+ }
2488
+ return;
2489
+ }
2490
+ const nextColumn = {
2491
+ column: column.name,
2492
+ ...column.default === "now()" ? { default: "now()" } : {},
2493
+ ...column.updatedAt ? { updatedAt: true } : {}
2494
+ };
2495
+ tableMetadata.timestampColumns = [...(tableMetadata.timestampColumns ?? []).filter((entry) => entry.column !== column.name), nextColumn];
2496
+ };
2497
+ const applyOperationsToPersistedColumnMappingsState = (state, operations, features = resolvePersistedMetadataFeatures()) => {
2498
+ const nextTables = Object.entries(state.tables).reduce((all, [table, metadata]) => {
2499
+ all[table] = {
2500
+ columns: { ...metadata.columns },
2501
+ enums: Object.entries(metadata.enums).reduce((nextEnums, [columnName, values]) => {
2502
+ nextEnums[columnName] = [...values];
2503
+ return nextEnums;
2504
+ }, {}),
2505
+ primaryKeyGeneration: metadata.primaryKeyGeneration ? { ...metadata.primaryKeyGeneration } : void 0,
2506
+ timestampColumns: metadata.timestampColumns?.map((column) => ({ ...column }))
2507
+ };
2508
+ return all;
2509
+ }, {});
2510
+ operations.forEach((operation) => {
2511
+ if (operation.type === "createTable") {
2512
+ const tableMetadata = nextTables[operation.table] ?? {
2513
+ columns: {},
2514
+ enums: {}
2515
+ };
2516
+ operation.columns.forEach((column) => {
2517
+ applyMappedColumn(tableMetadata.columns, column, features, operation.table);
2518
+ applyEnumColumn(tableMetadata.enums, column, features, operation.table);
2519
+ applyPrimaryKeyGeneration(tableMetadata, column);
2520
+ applyTimestampColumn(tableMetadata, column);
2521
+ });
2522
+ if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0 || tableMetadata.primaryKeyGeneration || tableMetadata.timestampColumns?.length) nextTables[operation.table] = tableMetadata;
2523
+ else delete nextTables[operation.table];
2524
+ return;
2525
+ }
2526
+ if (operation.type === "alterTable") {
2527
+ const tableMetadata = nextTables[operation.table] ?? {
2528
+ columns: {},
2529
+ enums: {}
2530
+ };
2531
+ operation.addColumns.forEach((column) => {
2532
+ applyMappedColumn(tableMetadata.columns, column, features, operation.table);
2533
+ applyEnumColumn(tableMetadata.enums, column, features, operation.table);
2534
+ applyPrimaryKeyGeneration(tableMetadata, column);
2535
+ applyTimestampColumn(tableMetadata, column);
2536
+ });
2537
+ operation.dropColumns.forEach((columnName) => {
2538
+ removePersistedColumnMetadata(tableMetadata, columnName);
2539
+ });
2540
+ if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0 || tableMetadata.primaryKeyGeneration || tableMetadata.timestampColumns?.length) nextTables[operation.table] = tableMetadata;
2541
+ else delete nextTables[operation.table];
2542
+ return;
2543
+ }
2544
+ delete nextTables[operation.table];
2545
+ });
2546
+ return {
2547
+ version: 1,
2548
+ tables: nextTables
2549
+ };
2550
+ };
2551
+ const rebuildPersistedColumnMappingsState = async (state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
2552
+ const availableByIdentity = new Map(availableMigrations.map(([migrationClass, file]) => [buildMigrationIdentity(file, migrationClass.name), migrationClass]));
2553
+ let nextState = createEmptyPersistedColumnMappingsState();
2554
+ const orderedMigrations = state.migrations.map((migration, index) => ({
2555
+ migration,
2556
+ index
2557
+ })).sort((left, right) => {
2558
+ const appliedAtOrder = left.migration.appliedAt.localeCompare(right.migration.appliedAt);
2559
+ if (appliedAtOrder !== 0) return appliedAtOrder;
2560
+ return left.index - right.index;
2561
+ });
2562
+ for (const { migration } of orderedMigrations) {
2563
+ const migrationClass = availableByIdentity.get(migration.id);
2564
+ if (!migrationClass) throw new ArkormException(`Unable to rebuild persisted column mappings because migration [${migration.id}] could not be resolved from the current migration files.`, {
2565
+ operation: "migration.columnMappings",
2566
+ meta: {
2567
+ migrationId: migration.id,
2568
+ file: migration.file,
2569
+ className: migration.className
2570
+ }
2571
+ });
2572
+ const operations = await getMigrationPlan(migrationClass, "up");
2573
+ nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
2574
+ }
2575
+ return nextState;
2576
+ };
2577
+ const syncPersistedColumnMappingsFromState = async (cwd, state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
2578
+ const filePath = resolveColumnMappingsFilePath(cwd);
2579
+ const nextState = await rebuildPersistedColumnMappingsState(state, availableMigrations, features);
2580
+ if (Object.keys(nextState.tables).length === 0) {
2581
+ deletePersistedColumnMappingsState(filePath);
2582
+ return;
2583
+ }
2584
+ writePersistedColumnMappingsState(filePath, nextState);
2585
+ };
2586
+ const validatePersistedMetadataFeaturesForMigrations = async (migrations, features = resolvePersistedMetadataFeatures()) => {
2587
+ let nextState = createEmptyPersistedColumnMappingsState();
2588
+ for (const [migrationClass] of migrations) {
2589
+ const operations = await getMigrationPlan(migrationClass, "up");
2590
+ nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
2591
+ }
2592
+ };
2593
+ const getPersistedEnumTsType = (values) => {
2594
+ return buildEnumUnionType(values);
2595
+ };
2596
+
2597
+ //#endregion
2598
+ //#region src/helpers/runtime-config.ts
2599
+ const resolveDefaultStubsPath = () => {
2600
+ let current = path.dirname(fileURLToPath(import.meta.url));
2601
+ while (true) {
2602
+ const packageJsonPath = path.join(current, "package.json");
2603
+ const stubsPath = path.join(current, "stubs");
2604
+ if (existsSync(packageJsonPath) && existsSync(stubsPath)) return stubsPath;
2605
+ const parent = path.dirname(current);
2606
+ if (parent === current) break;
2607
+ current = parent;
2608
+ }
2609
+ return path.join(process.cwd(), "stubs");
2610
+ };
2611
+ const baseConfig = {
2612
+ naming: { modelTableCase: "snake" },
2613
+ features: {
2614
+ persistedColumnMappings: true,
2615
+ persistedEnums: true
2616
+ },
2617
+ paths: {
2618
+ stubs: resolveDefaultStubsPath(),
2619
+ seeders: path.join(process.cwd(), "database", "seeders"),
2620
+ models: path.join(process.cwd(), "src", "models"),
2621
+ migrations: path.join(process.cwd(), "database", "migrations"),
2622
+ factories: path.join(process.cwd(), "database", "factories"),
2623
+ buildOutput: path.join(process.cwd(), "dist")
2624
+ },
2625
+ outputExt: "ts"
2626
+ };
2627
+ const userConfig = {
2628
+ ...baseConfig,
2629
+ naming: { ...baseConfig.naming ?? {} },
2630
+ features: { ...baseConfig.features ?? {} },
2631
+ paths: { ...baseConfig.paths ?? {} }
2632
+ };
2633
+ let runtimeConfigLoaded = false;
2634
+ let runtimeConfigLoadingPromise;
2635
+ let runtimeClientResolver;
2636
+ let runtimeAdapter;
2637
+ let runtimePaginationURLDriverFactory;
2638
+ let runtimePaginationCurrentPageResolver;
2639
+ let runtimeDebugHandler;
2640
+ const transactionClientStorage = new AsyncLocalStorage();
2641
+ const transactionAdapterStorage = new AsyncLocalStorage();
2642
+ const defaultDebugHandler = (event) => {
2643
+ const prefix = `[arkorm:${event.adapter}] ${event.operation}${event.target ? ` [${event.target}]` : ""}`;
2644
+ const payload = {
2645
+ phase: event.phase,
2646
+ durationMs: event.durationMs,
2647
+ inspection: event.inspection ?? void 0,
2648
+ meta: event.meta,
2649
+ error: event.error
2650
+ };
2651
+ if (event.phase === "error") {
2652
+ console.error(prefix, payload);
2653
+ return;
2654
+ }
2655
+ console.debug(prefix, payload);
2656
+ };
2657
+ const resolveDebugHandler = (debug) => {
2658
+ if (debug === true) return defaultDebugHandler;
2659
+ return typeof debug === "function" ? debug : void 0;
2660
+ };
2661
+ const mergeNamingConfig = (naming) => {
2662
+ const defaults = baseConfig.naming ?? {};
2663
+ const current = userConfig.naming ?? {};
2664
+ return {
2665
+ ...defaults,
2666
+ ...current,
2667
+ ...naming ?? {}
2668
+ };
2669
+ };
2670
+ const mergePathConfig = (paths) => {
2671
+ const defaults = baseConfig.paths ?? {};
2672
+ const current = userConfig.paths ?? {};
2673
+ const incoming = Object.entries(paths ?? {}).reduce((all, [key, value]) => {
2674
+ if (typeof value === "string" && value.trim().length > 0) all[key] = path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
2675
+ return all;
2676
+ }, {});
2677
+ return {
2678
+ ...defaults,
2679
+ ...current,
2680
+ ...incoming
2681
+ };
2682
+ };
2683
+ /**
2684
+ * Merge the feature configuration from the base defaults, user configuration, and provided options.
2685
+ *
2686
+ * @param features
2687
+ * @returns
2688
+ */
2689
+ const mergeFeatureConfig = (features) => {
2690
+ const defaults = baseConfig.features ?? {};
2691
+ const current = userConfig.features ?? {};
2692
+ return {
2693
+ ...defaults,
2694
+ ...current,
2695
+ ...features ?? {}
2696
+ };
2697
+ };
2698
+ /**
2699
+ * Define the ArkORM runtime configuration. This function can be used to provide.
2700
+ *
2701
+ * @param config The ArkORM configuration object.
2702
+ * @returns The same configuration object.
2703
+ */
2704
+ const defineConfig = (config) => {
2705
+ return config;
2706
+ };
2707
+ /**
2708
+ * Bind a database adapter instance to an array of models that support adapter binding.
2709
+ *
2710
+ * @param adapter
2711
+ * @param models
2712
+ * @returns
2713
+ */
2714
+ const bindAdapterToModels = (adapter, models) => {
2715
+ models.forEach((model) => {
2716
+ model.setAdapter(adapter);
2717
+ });
2718
+ return adapter;
2719
+ };
2720
+ /**
2721
+ * Get the user-provided ArkORM configuration.
2722
+ *
2723
+ * @param key Optional specific configuration key to retrieve. If omitted, the entire configuration object is returned.
2724
+ * @returns The user-provided ArkORM configuration object.
2725
+ */
2726
+ const getUserConfig = (key) => {
2727
+ if (key) return userConfig[key];
2728
+ return userConfig;
2729
+ };
2730
+ /**
2731
+ * Configure the ArkORM runtime with the provided runtime client resolver and
2732
+ * adapter-first options.
2733
+ *
2734
+ * @param client
2735
+ * @param options
2736
+ */
2737
+ const configureArkormRuntime = (client, options = {}) => {
2738
+ const resolvedClient = client ?? options.client;
2739
+ const nextConfig = {
2740
+ ...userConfig,
2741
+ naming: mergeNamingConfig(options.naming),
2742
+ features: mergeFeatureConfig(options.features),
2743
+ paths: mergePathConfig(options.paths)
2744
+ };
2745
+ nextConfig.client = resolvedClient;
2746
+ nextConfig.prisma = resolvedClient;
2747
+ if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
2748
+ if (options.adapter !== void 0) nextConfig.adapter = options.adapter;
2749
+ if (options.boot !== void 0) nextConfig.boot = options.boot;
2750
+ if (options.debug !== void 0) nextConfig.debug = options.debug;
2751
+ if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
2752
+ Object.assign(userConfig, { ...nextConfig });
2753
+ runtimeClientResolver = resolvedClient;
2754
+ runtimeAdapter = options.adapter;
2755
+ runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
2756
+ runtimePaginationCurrentPageResolver = nextConfig.pagination?.resolveCurrentPage;
2757
+ runtimeDebugHandler = resolveDebugHandler(nextConfig.debug);
2758
+ const bootClient = resolveClient(resolvedClient);
2759
+ options.boot?.({
2760
+ client: bootClient,
2761
+ prisma: bootClient,
2762
+ bindAdapter: bindAdapterToModels
2763
+ });
2764
+ };
2765
+ /**
2766
+ * Reset the ArkORM runtime configuration.
2767
+ * This is primarily intended for testing purposes.
2768
+ */
2769
+ const resetArkormRuntimeForTests = () => {
2770
+ Object.assign(userConfig, {
2771
+ ...baseConfig,
2772
+ naming: { ...baseConfig.naming ?? {} },
2773
+ features: { ...baseConfig.features ?? {} },
2774
+ paths: { ...baseConfig.paths ?? {} }
2775
+ });
2776
+ runtimeConfigLoaded = false;
2777
+ runtimeConfigLoadingPromise = void 0;
2778
+ runtimeClientResolver = void 0;
2779
+ runtimeAdapter = void 0;
2780
+ runtimePaginationURLDriverFactory = void 0;
2781
+ runtimePaginationCurrentPageResolver = void 0;
2782
+ runtimeDebugHandler = void 0;
2783
+ resetPersistedColumnMappingsCache();
2784
+ };
2785
+ /**
2786
+ * Resolve a runtime client instance from the provided resolver, which can be either
2787
+ * a direct client instance or a function that returns a client instance.
2788
+ *
2789
+ * @param resolver
2790
+ * @returns
2791
+ */
2792
+ const resolveClient = (resolver) => {
2793
+ if (!resolver) return void 0;
2794
+ const client = typeof resolver === "function" ? resolver() : resolver;
2795
+ if (!client || typeof client !== "object") return void 0;
2796
+ return client;
2797
+ };
2798
+ /**
2799
+ * Resolve and apply the ArkORM configuration from an imported module.
2800
+ * This function checks for a default export and falls back to the module itself, then validates
2801
+ * the configuration object and applies it to the runtime if valid.
2802
+ *
2803
+ * @param imported
2804
+ * @returns
2805
+ */
2806
+ const resolveAndApplyConfig = (imported) => {
2807
+ const config = imported?.default ?? imported;
2808
+ if (!config || typeof config !== "object") return;
2809
+ const runtimeClient = config.client ?? config.prisma;
2810
+ configureArkormRuntime(runtimeClient, {
2811
+ client: runtimeClient,
2812
+ adapter: config.adapter,
2813
+ boot: config.boot,
2814
+ debug: config.debug,
2815
+ naming: config.naming,
2816
+ features: config.features,
2817
+ pagination: config.pagination,
2818
+ paths: config.paths,
2819
+ outputExt: config.outputExt
2820
+ });
2821
+ runtimeConfigLoaded = true;
2822
+ };
2823
+ /**
2824
+ * Dynamically import a configuration file.
2825
+ * A cache-busting query parameter is appended to ensure the latest version is loaded.
2826
+ *
2827
+ * @param configPath
2828
+ * @returns A promise that resolves to the imported configuration module.
2829
+ */
2830
+ const importConfigFile = (configPath) => {
2831
+ return RuntimeModuleLoader.load(configPath);
2832
+ };
2833
+ const loadRuntimeConfigSync = () => {
2834
+ const require = createRequire(import.meta.url);
2835
+ const syncConfigPaths = [path.join(process.cwd(), "arkormx.config.cjs")];
2836
+ for (const configPath of syncConfigPaths) {
2837
+ if (!existsSync(configPath)) continue;
2838
+ try {
2839
+ resolveAndApplyConfig(require(configPath));
2840
+ return true;
2841
+ } catch {
2842
+ continue;
2843
+ }
2844
+ }
2845
+ return false;
2846
+ };
2847
+ /**
2848
+ * Load the ArkORM configuration by searching for configuration files in the
2849
+ * current working directory.
2850
+ * @returns
2851
+ */
2852
+ const loadArkormConfig = async () => {
2853
+ if (runtimeConfigLoaded) return;
2854
+ if (runtimeConfigLoadingPromise) return await runtimeConfigLoadingPromise;
2855
+ if (loadRuntimeConfigSync()) return;
2856
+ runtimeConfigLoadingPromise = (async () => {
2857
+ const configPaths = [path.join(process.cwd(), "arkormx.config.js"), path.join(process.cwd(), "arkormx.config.ts")];
2858
+ for (const configPath of configPaths) {
2859
+ if (!existsSync(configPath)) continue;
2860
+ try {
2861
+ resolveAndApplyConfig(await importConfigFile(configPath));
2862
+ return;
2863
+ } catch {
2864
+ continue;
2865
+ }
2866
+ }
2867
+ runtimeConfigLoaded = true;
2868
+ })();
2869
+ await runtimeConfigLoadingPromise;
2870
+ };
2871
+ /**
2872
+ * Ensure that the ArkORM configuration is loaded.
2873
+ * This function can be called to trigger the loading process if it hasn't already been initiated.
2874
+ * If the configuration is already loaded, it will return immediately.
2875
+ *
2876
+ * @returns
2877
+ */
2878
+ const ensureArkormConfigLoading = () => {
2879
+ if (runtimeConfigLoaded) return;
2880
+ if (!runtimeConfigLoadingPromise) loadArkormConfig();
2881
+ };
2882
+ const getDefaultStubsPath = () => {
2883
+ return resolveDefaultStubsPath();
2884
+ };
2885
+ /**
2886
+ * Get the runtime compatibility client.
2887
+ * This function will trigger the loading of the ArkORM configuration if
2888
+ * it hasn't already been loaded.
2889
+ *
2890
+ * @returns
2891
+ */
2892
+ const getRuntimeClient = () => {
2893
+ const activeTransactionClient = transactionClientStorage.getStore();
2894
+ if (activeTransactionClient) return activeTransactionClient;
2895
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
2896
+ return resolveClient(runtimeClientResolver);
2897
+ };
2898
+ /**
2899
+ * @deprecated Use getRuntimeClient instead.
2900
+ */
2901
+ const getRuntimePrismaClient = getRuntimeClient;
2902
+ const getRuntimeAdapter = () => {
2903
+ const activeTransactionAdapter = transactionAdapterStorage.getStore();
2904
+ if (activeTransactionAdapter) return activeTransactionAdapter;
2905
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
2906
+ return runtimeAdapter;
2907
+ };
2908
+ const getActiveTransactionClient = () => {
2909
+ return transactionClientStorage.getStore();
2910
+ };
2911
+ const getActiveTransactionAdapter = () => {
2912
+ return transactionAdapterStorage.getStore();
2913
+ };
2914
+ const isTransactionCapableClient = (value) => {
2915
+ if (!value || typeof value !== "object") return false;
2916
+ return typeof value.$transaction === "function";
2917
+ };
2918
+ const runArkormTransaction = async (callback, options = {}, preferredAdapter) => {
2919
+ const activeTransactionAdapter = transactionAdapterStorage.getStore();
2920
+ const activeTransactionClient = transactionClientStorage.getStore();
2921
+ if (activeTransactionAdapter || activeTransactionClient) return await callback({
2922
+ adapter: activeTransactionAdapter,
2923
+ client: activeTransactionClient
2924
+ });
2925
+ const adapter = preferredAdapter ?? getRuntimeAdapter();
2926
+ if (adapter) return await adapter.transaction(async (transactionAdapter) => {
2927
+ return await transactionAdapterStorage.run(transactionAdapter, async () => {
2928
+ return await callback({
2929
+ adapter: transactionAdapter,
2930
+ client: transactionClientStorage.getStore()
2931
+ });
2932
+ });
2933
+ }, options);
2934
+ const client = getRuntimeClient();
2935
+ if (!client) throw new ArkormException("Cannot start a transaction without a configured runtime client or adapter.", {
2936
+ code: "CLIENT_NOT_CONFIGURED",
2937
+ operation: "transaction"
2938
+ });
2939
+ if (!isTransactionCapableClient(client)) throw new UnsupportedAdapterFeatureException("Transactions are not supported by the current adapter.", {
2940
+ code: "TRANSACTION_NOT_SUPPORTED",
2941
+ operation: "transaction"
2942
+ });
2943
+ return await client.$transaction(async (transactionClient) => {
2944
+ return await transactionClientStorage.run(transactionClient, async () => {
2945
+ return await callback({ client: transactionClient });
2946
+ });
2947
+ }, options);
2948
+ };
2949
+ /**
2950
+ * Get the configured pagination URL driver factory from runtime config.
2951
+ *
2952
+ * @returns
2953
+ */
2954
+ const getRuntimePaginationURLDriverFactory = () => {
2955
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
2956
+ return runtimePaginationURLDriverFactory;
2957
+ };
2958
+ /**
2959
+ * Get the configured current-page resolver from runtime config.
2960
+ *
2961
+ * @returns
2962
+ */
2963
+ const getRuntimePaginationCurrentPageResolver = () => {
2964
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
2965
+ return runtimePaginationCurrentPageResolver;
2966
+ };
2967
+ const getRuntimeDebugHandler = () => {
2968
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
2969
+ return runtimeDebugHandler;
2970
+ };
2971
+ const emitRuntimeDebugEvent = (event) => {
2972
+ getRuntimeDebugHandler()?.(event);
2973
+ };
2974
+ /**
2975
+ * Check if a given value matches Arkorm's query-schema contract
2976
+ * by verifying the presence of common delegate methods.
2977
+ *
2978
+ * @param value The value to check.
2979
+ * @returns True if the value matches the query-schema contract, false otherwise.
2980
+ */
2981
+ const isQuerySchemaLike = (value) => {
2982
+ if (!value || typeof value !== "object") return false;
2983
+ const candidate = value;
2984
+ return [
2985
+ "findMany",
2986
+ "findFirst",
2987
+ "create",
2988
+ "update",
2989
+ "delete",
2990
+ "count"
2991
+ ].every((method) => typeof candidate[method] === "function");
2992
+ };
2993
+ /**
2994
+ * @deprecated Use isQuerySchemaLike instead.
2995
+ */
2996
+ const isDelegateLike = isQuerySchemaLike;
2997
+ loadArkormConfig();
2998
+
2999
+ //#endregion
3000
+ //#region src/URLDriver.ts
3001
+ /**
3002
+ * URLDriver builds pagination URLs from paginator options.
3003
+ *
3004
+ * @author Legacy (3m1n3nc3)
3005
+ * @since 0.1.0
3006
+ */
3007
+ var URLDriver = class URLDriver {
3008
+ static DEFAULT_PAGE_NAME = "page";
3009
+ path;
3010
+ query;
3011
+ fragment;
3012
+ pageName;
3013
+ constructor(options = {}) {
3014
+ this.path = options.path ?? "/";
3015
+ this.query = options.query ?? {};
3016
+ this.fragment = options.fragment ?? "";
3017
+ this.pageName = options.pageName ?? URLDriver.DEFAULT_PAGE_NAME;
3018
+ }
3019
+ getPageName() {
3020
+ return this.pageName;
3021
+ }
3022
+ url(page) {
3023
+ const targetPage = Math.max(1, page);
3024
+ const [basePath, pathQuery = ""] = this.path.split("?");
3025
+ const search = new URLSearchParams(pathQuery);
3026
+ Object.entries(this.query).forEach(([key, value]) => {
3027
+ if (value == null) {
3028
+ search.delete(key);
3029
+ return;
3030
+ }
3031
+ search.set(key, String(value));
3032
+ });
3033
+ search.set(this.pageName, String(targetPage));
3034
+ const queryString = search.toString();
3035
+ const normalizedFragment = this.fragment.replace(/^#/, "");
3036
+ if (!queryString && !normalizedFragment) return basePath;
3037
+ if (!normalizedFragment) return `${basePath}?${queryString}`;
3038
+ if (!queryString) return `${basePath}#${normalizedFragment}`;
3039
+ return `${basePath}?${queryString}#${normalizedFragment}`;
3040
+ }
3041
+ };
3042
+
3043
+ //#endregion
3044
+ //#region src/Paginator.ts
3045
+ /**
3046
+ * The LengthAwarePaginator class encapsulates paginated results with full
3047
+ * metadata including the total result count and last page.
3048
+ *
3049
+ * @template T The type of the data being paginated.
3050
+ * @author Legacy (3m1n3nc3)
3051
+ * @since 0.1.0
3052
+ */
3053
+ var LengthAwarePaginator = class {
3054
+ data;
3055
+ meta;
3056
+ urlDriver;
3057
+ /**
3058
+ * Creates a new LengthAwarePaginator instance.
3059
+ *
3060
+ * @param data The collection of data being paginated.
3061
+ * @param total The total number of items.
3062
+ * @param perPage The number of items per page.
3063
+ * @param currentPage The current page number.
3064
+ * @param options URL generation options.
3065
+ */
3066
+ constructor(data, total, perPage, currentPage, options = {}) {
3067
+ const lastPage = Math.max(1, Math.ceil(total / perPage));
3068
+ const from = total === 0 ? null : (currentPage - 1) * perPage + 1;
3069
+ const to = total === 0 ? null : Math.min(currentPage * perPage, total);
3070
+ this.data = data;
3071
+ const urlDriverFactory = getRuntimePaginationURLDriverFactory();
3072
+ this.urlDriver = urlDriverFactory ? urlDriverFactory(options) : new URLDriver(options);
3073
+ this.meta = {
3074
+ total,
3075
+ perPage,
3076
+ currentPage,
3077
+ lastPage,
3078
+ from,
3079
+ to
3080
+ };
3081
+ }
3082
+ getPageName() {
3083
+ return this.urlDriver.getPageName();
3084
+ }
3085
+ url(page) {
3086
+ return this.urlDriver.url(page);
3087
+ }
3088
+ nextPageUrl() {
3089
+ if (this.meta.currentPage >= this.meta.lastPage) return null;
3090
+ return this.url(this.meta.currentPage + 1);
3091
+ }
3092
+ previousPageUrl() {
3093
+ if (this.meta.currentPage <= 1) return null;
3094
+ return this.url(this.meta.currentPage - 1);
3095
+ }
3096
+ firstPageUrl() {
3097
+ return this.url(1);
3098
+ }
3099
+ lastPageUrl() {
3100
+ return this.url(this.meta.lastPage);
3101
+ }
3102
+ /**
3103
+ * Converts the paginator instance to a JSON-serializable object.
3104
+ *
3105
+ * @returns
3106
+ */
3107
+ toJSON() {
3108
+ return {
3109
+ data: this.data,
3110
+ meta: this.meta,
3111
+ links: {
3112
+ first: this.firstPageUrl(),
3113
+ last: this.lastPageUrl(),
3114
+ prev: this.previousPageUrl(),
3115
+ next: this.nextPageUrl()
3116
+ }
3117
+ };
3118
+ }
3119
+ };
3120
+ /**
3121
+ * The Paginator class encapsulates simple pagination results without total count.
3122
+ *
3123
+ * @template T The type of the data being paginated.
3124
+ */
3125
+ var Paginator = class {
3126
+ data;
3127
+ meta;
3128
+ urlDriver;
3129
+ /**
3130
+ * Creates a new simple Paginator instance.
3131
+ *
3132
+ * @param data The collection of data being paginated.
3133
+ * @param perPage The number of items per page.
3134
+ * @param currentPage The current page number.
3135
+ * @param hasMorePages Indicates whether additional pages exist.
3136
+ * @param options URL generation options.
3137
+ */
3138
+ constructor(data, perPage, currentPage, hasMorePages, options = {}) {
3139
+ const count = data.all().length;
3140
+ const from = count === 0 ? null : (currentPage - 1) * perPage + 1;
3141
+ const to = count === 0 ? null : (from ?? 1) + count - 1;
3142
+ this.data = data;
3143
+ const urlDriverFactory = getRuntimePaginationURLDriverFactory();
3144
+ this.urlDriver = urlDriverFactory ? urlDriverFactory(options) : new URLDriver(options);
3145
+ this.meta = {
3146
+ perPage,
3147
+ currentPage,
3148
+ from,
3149
+ to,
3150
+ hasMorePages
3151
+ };
3152
+ }
3153
+ getPageName() {
3154
+ return this.urlDriver.getPageName();
3155
+ }
3156
+ url(page) {
3157
+ return this.urlDriver.url(page);
3158
+ }
3159
+ nextPageUrl() {
3160
+ if (!this.meta.hasMorePages) return null;
3161
+ return this.url(this.meta.currentPage + 1);
3162
+ }
3163
+ previousPageUrl() {
3164
+ if (this.meta.currentPage <= 1) return null;
3165
+ return this.url(this.meta.currentPage - 1);
3166
+ }
3167
+ toJSON() {
3168
+ return {
3169
+ data: this.data,
3170
+ meta: this.meta,
3171
+ links: {
3172
+ prev: this.previousPageUrl(),
3173
+ next: this.nextPageUrl()
3174
+ }
3175
+ };
3176
+ }
3177
+ };
3178
+
3179
+ //#endregion
3180
+ //#region src/relationship/Relation.ts
3181
+ /**
3182
+ * Base class for all relationship types. Not meant to be used directly.
3183
+ *
3184
+ * @author Legacy (3m1n3nc3)
3185
+ * @since 0.1.0
3186
+ */
3187
+ var Relation = class {
3188
+ constraint = null;
3189
+ getRelationAdapter() {
3190
+ const adapter = this.getRelatedModel().getAdapter();
3191
+ if (!adapter) throw new UnsupportedAdapterFeatureException("Relationship resolution requires a configured adapter.", { operation: "relation.adapter" });
3192
+ return adapter;
3193
+ }
3194
+ getRelatedModel() {
3195
+ return this.related;
3196
+ }
3197
+ createRelationTableLoader() {
3198
+ return new RelationTableLoader(this.getRelationAdapter());
3199
+ }
3200
+ /**
3201
+ * Apply a constraint to the relationship query.
3202
+ *
3203
+ * @param constraint The constraint function to apply to the query.
3204
+ * @returns The current relation instance.
3205
+ */
3206
+ constrain(constraint) {
3207
+ if (!this.constraint) {
3208
+ this.constraint = constraint;
3209
+ return this;
3210
+ }
3211
+ const previousConstraint = this.constraint;
3212
+ this.constraint = (query) => {
3213
+ const constrained = previousConstraint(query) ?? query;
3214
+ return constraint(constrained) ?? constrained;
3215
+ };
3216
+ return this;
3217
+ }
3218
+ /**
3219
+ * Add a where clause to the relationship query.
3220
+ *
3221
+ * @param where
3222
+ * @returns
3223
+ */
3224
+ where(where) {
3225
+ return this.constrain((query) => query.where(where));
3226
+ }
3227
+ /**
3228
+ * Add a strongly-typed where key clause to the relationship query.
3229
+ *
3230
+ * @param key
3231
+ * @param value
3232
+ * @returns
3233
+ */
3234
+ whereKey(key, value) {
3235
+ return this.constrain((query) => query.whereKey(key, value));
3236
+ }
3237
+ /**
3238
+ * Add a strongly-typed where in clause to the relationship query.
3239
+ *
3240
+ * @param key
3241
+ * @param values
3242
+ * @returns
3243
+ */
3244
+ whereIn(key, values) {
3245
+ return this.constrain((query) => query.whereIn(key, values));
3246
+ }
3247
+ /**
3248
+ * Add a string contains clause to the relationship query.
3249
+ *
3250
+ * @param key
3251
+ * @param value
3252
+ * @returns
3253
+ */
3254
+ whereLike(key, value) {
3255
+ return this.constrain((query) => query.whereLike(key, value));
3256
+ }
3257
+ /**
3258
+ * Add a string starts-with clause to the relationship query.
3259
+ *
3260
+ * @param key
3261
+ * @param value
3262
+ * @returns
3263
+ */
3264
+ whereStartsWith(key, value) {
3265
+ return this.constrain((query) => query.whereStartsWith(key, value));
3266
+ }
3267
+ /**
3268
+ * Add a string ends-with clause to the relationship query.
3269
+ *
3270
+ * @param key
3271
+ * @param value
3272
+ * @returns
3273
+ */
3274
+ whereEndsWith(key, value) {
3275
+ return this.constrain((query) => query.whereEndsWith(key, value));
3276
+ }
3277
+ /**
3278
+ * Add an order by clause to the relationship query.
3279
+ *
3280
+ * @param orderBy
3281
+ * @returns
3282
+ */
3283
+ orderBy(orderBy) {
3284
+ return this.constrain((query) => query.orderBy(orderBy));
3285
+ }
3286
+ /**
3287
+ * Add an include clause to the relationship query.
3288
+ *
3289
+ * @param include
3290
+ * @returns
3291
+ */
3292
+ include(include) {
3293
+ return this.constrain((query) => query.include(include));
3294
+ }
3295
+ /**
3296
+ * Add eager loading relations to the relationship query.
3297
+ *
3298
+ * @param relations
3299
+ * @returns
3300
+ */
3301
+ with(relations) {
3302
+ return this.constrain((query) => query.with(relations));
3303
+ }
3304
+ /**
3305
+ * Add a select clause to the relationship query.
3306
+ *
3307
+ * @param select
3308
+ * @returns
3309
+ */
3310
+ select(select) {
3311
+ return this.constrain((query) => query.select(select));
3312
+ }
3313
+ /**
3314
+ * Add a skip clause to the relationship query.
3315
+ *
3316
+ * @param skip
3317
+ * @returns
3318
+ */
3319
+ skip(skip) {
3320
+ return this.constrain((query) => query.skip(skip));
3321
+ }
3322
+ /**
3323
+ * Add a take clause to the relationship query.
3324
+ *
3325
+ * @param take
3326
+ * @returns
3327
+ */
3328
+ take(take) {
3329
+ return this.constrain((query) => query.take(take));
3330
+ }
3331
+ /**
3332
+ * Include soft-deleted records in the relationship query.
3333
+ *
3334
+ * @returns
3335
+ */
3336
+ withTrashed() {
3337
+ return this.constrain((query) => query.withTrashed());
3338
+ }
3339
+ /**
3340
+ * Limit relationship query to only soft-deleted records.
3341
+ *
3342
+ * @returns
3343
+ */
3344
+ onlyTrashed() {
3345
+ return this.constrain((query) => query.onlyTrashed());
3346
+ }
3347
+ /**
3348
+ * Exclude soft-deleted records from the relationship query.
3349
+ *
3350
+ * @returns
3351
+ */
3352
+ withoutTrashed() {
3353
+ return this.constrain((query) => query.withoutTrashed());
3354
+ }
3355
+ /**
3356
+ * Apply a scope to the relationship query.
3357
+ *
3358
+ * @param name
3359
+ * @param args
3360
+ * @returns
3361
+ */
3362
+ scope(name, ...args) {
3363
+ return this.constrain((query) => query.scope(name, ...args));
3364
+ }
3365
+ /**
3366
+ * Apply the defined constraint to the given query, if any.
3367
+ *
3368
+ * @param query The query builder instance to apply the constraint to.
3369
+ *
3370
+ * @returns The query builder instance with the constraint applied, if any.
3371
+ */
3372
+ applyConstraint(query) {
3373
+ if (!this.constraint) return query;
3374
+ return this.constraint(query) ?? query;
3375
+ }
3376
+ /**
3377
+ * Execute the relationship query and return relation results.
3378
+ *
3379
+ * @returns
3380
+ */
3381
+ async get() {
3382
+ return this.getResults();
3383
+ }
3384
+ /**
3385
+ * Execute the relationship query and return the first related model.
3386
+ *
3387
+ * @returns
3388
+ */
3389
+ async first() {
3390
+ const results = await this.getResults();
3391
+ if (results instanceof ArkormCollection) return results.all()[0] ?? null;
3392
+ return results;
3393
+ }
3394
+ /**
3395
+ * Count records that match the relationship query.
3396
+ *
3397
+ * @returns
3398
+ */
3399
+ async count() {
3400
+ return (await this.getQuery()).count();
3401
+ }
3402
+ /**
3403
+ * Determine whether the relationship query has any matching records.
3404
+ *
3405
+ * @returns
3406
+ */
3407
+ async exists() {
3408
+ return (await this.getQuery()).exists();
3409
+ }
3410
+ /**
3411
+ * Determine whether the relationship query has no matching records.
3412
+ *
3413
+ * @returns
3414
+ */
3415
+ async doesntExist() {
3416
+ return !await this.exists();
3417
+ }
3418
+ };
3419
+
3420
+ //#endregion
3421
+ //#region src/relationship/BelongsToManyRelation.ts
3422
+ /**
3423
+ * Defines a many-to-many relationship.
3424
+ *
3425
+ * @author Legacy (3m1n3nc3)
3426
+ * @since 0.1.0
3427
+ */
3428
+ var BelongsToManyRelation = class BelongsToManyRelation extends Relation {
3429
+ static queryDecorationMarker = Symbol("belongsToManyQueryDecoration");
3430
+ pivotColumns = /* @__PURE__ */ new Set();
3431
+ pivotAccessor = "pivot";
3432
+ pivotCreatedAtColumn;
3433
+ pivotUpdatedAtColumn;
3434
+ pivotWhere;
3435
+ pivotModel;
3436
+ shouldAttachPivot = false;
3437
+ constructor(parent, related, throughTable, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
3438
+ super();
3439
+ this.parent = parent;
3440
+ this.related = related;
3441
+ this.throughTable = throughTable;
3442
+ this.foreignPivotKey = foreignPivotKey;
3443
+ this.relatedPivotKey = relatedPivotKey;
3444
+ this.parentKey = parentKey;
3445
+ this.relatedKey = relatedKey;
3446
+ }
3447
+ /**
3448
+ * Specifies additional pivot columns to include on the related models.
3449
+ *
3450
+ * @param columns The pivot columns to include on the related models.
3451
+ * @returns
3452
+ */
3453
+ withPivot(...columns) {
3454
+ columns.flat().forEach((column) => {
3455
+ if (typeof column !== "string" || column.trim().length === 0) return;
3456
+ this.pivotColumns.add(column.trim());
3457
+ });
3458
+ this.shouldAttachPivot = true;
3459
+ return this;
3460
+ }
3461
+ /**
3462
+ * Specifies that the pivot table contains timestamp columns and optionally
3463
+ * allows customizing the names of those columns.
3464
+ *
3465
+ * @param createdAtColumn The name of the "created at" timestamp column.
3466
+ * @param updatedAtColumn The name of the "updated at" timestamp column.
3467
+ * @returns The current instance of the relationship.
3468
+ */
3469
+ withTimestamps(createdAtColumn = "createdAt", updatedAtColumn = "updatedAt") {
3470
+ this.pivotCreatedAtColumn = createdAtColumn;
3471
+ this.pivotUpdatedAtColumn = updatedAtColumn;
3472
+ return this.withPivot(createdAtColumn, updatedAtColumn);
3473
+ }
3474
+ /**
3475
+ * Specifies a custom accessor name for the pivot attributes on the related models.
3476
+ * By default, pivot attributes are accessible via the `pivot` property on the
3477
+ * related models.
3478
+ *
3479
+ * @param accessor The custom accessor name for the pivot attributes.
3480
+ * @returns The current instance of the relationship.
3481
+ */
3482
+ as(accessor) {
3483
+ const normalized = accessor.trim();
3484
+ if (normalized.length === 0) return this;
3485
+ this.pivotAccessor = normalized;
3486
+ this.shouldAttachPivot = true;
3487
+ return this;
3488
+ }
3489
+ /**
3490
+ * Specifies a custom pivot model to use for the pivot records. The pivot model can
3491
+ * be used to define custom behavior or methods on the pivot records, as well as to
3492
+ * specify a custom hydration method for the pivot records.
3493
+ *
3494
+ * @param pivotModel The custom pivot model to use.
3495
+ * @returns The current instance of the relationship.
3496
+ */
3497
+ using(pivotModel) {
3498
+ this.pivotModel = pivotModel;
3499
+ this.shouldAttachPivot = true;
3500
+ return this;
3501
+ }
3502
+ wherePivot(column, operatorOrValue, value) {
3503
+ const normalizedColumn = column.trim();
3504
+ if (normalizedColumn.length === 0) return this;
3505
+ if (arguments.length === 2) return this.addPivotWhere(this.makePivotComparison(normalizedColumn, "=", operatorOrValue));
3506
+ return this.addPivotWhere(this.makePivotComparison(normalizedColumn, operatorOrValue, value));
3507
+ }
3508
+ /**
3509
+ * Adds a "pivot column in" condition to the relationship query.
3510
+ *
3511
+ * @param column
3512
+ * @param values
3513
+ * @returns
3514
+ */
3515
+ wherePivotNotIn(column, values) {
3516
+ return this.addPivotWhere(this.makePivotComparison(column, "not-in", values));
3517
+ }
3518
+ /**
3519
+ * Adds a "pivot column between" condition to the relationship query.
3520
+ *
3521
+ * @param column
3522
+ * @param range
3523
+ * @returns
3524
+ */
3525
+ wherePivotBetween(column, range) {
3526
+ return this.addPivotWhere({
3527
+ type: "group",
3528
+ operator: "and",
3529
+ conditions: [this.makePivotComparison(column, ">=", range[0]), this.makePivotComparison(column, "<=", range[1])]
3530
+ });
3531
+ }
3532
+ /**
3533
+ * Adds a "pivot column not between" condition to the relationship query.
3534
+ *
3535
+ * @param column
3536
+ * @param range
3537
+ * @returns
3538
+ */
3539
+ wherePivotNotBetween(column, range) {
3540
+ return this.addPivotWhere({
3541
+ type: "not",
3542
+ condition: {
3543
+ type: "group",
3544
+ operator: "and",
3545
+ conditions: [this.makePivotComparison(column, ">=", range[0]), this.makePivotComparison(column, "<=", range[1])]
3546
+ }
3547
+ });
3548
+ }
3549
+ /**
3550
+ * Adds a "pivot column is null" condition to the relationship query.
3551
+ *
3552
+ * @param column
3553
+ * @returns
3554
+ */
3555
+ wherePivotNull(column) {
3556
+ return this.addPivotWhere(this.makePivotComparison(column, "is-null"));
3557
+ }
3558
+ /**
3559
+ * Adds a "pivot column is not null" condition to the relationship query.
3560
+ *
3561
+ * @param column
3562
+ * @returns
3563
+ */
3564
+ wherePivotNotNull(column) {
3565
+ return this.addPivotWhere(this.makePivotComparison(column, "is-not-null"));
3566
+ }
3567
+ addPivotWhere(condition) {
3568
+ if (!this.pivotWhere) {
3569
+ this.pivotWhere = condition;
3570
+ return this;
3571
+ }
3572
+ this.pivotWhere = {
3573
+ type: "group",
3574
+ operator: "and",
3575
+ conditions: [this.pivotWhere, condition]
3576
+ };
3577
+ return this;
3578
+ }
3579
+ makePivotComparison(column, operator, value) {
3580
+ const normalizedColumn = column.trim();
3581
+ if (operator === "is-null" || operator === "is-not-null") return {
3582
+ type: "comparison",
3583
+ column: normalizedColumn,
3584
+ operator
3585
+ };
3586
+ return {
3587
+ type: "comparison",
3588
+ column: normalizedColumn,
3589
+ operator,
3590
+ value
3591
+ };
3592
+ }
3593
+ buildPivotWhere(parentValue) {
3594
+ const baseCondition = {
3595
+ type: "comparison",
3596
+ column: this.foreignPivotKey,
3597
+ operator: "=",
3598
+ value: parentValue
3599
+ };
3600
+ if (!this.pivotWhere) return baseCondition;
3601
+ return {
3602
+ type: "group",
3603
+ operator: "and",
3604
+ conditions: [baseCondition, this.pivotWhere]
3605
+ };
3606
+ }
3607
+ buildPivotTarget() {
3608
+ return {
3609
+ table: this.throughTable,
3610
+ primaryKey: this.relatedPivotKey
3611
+ };
3612
+ }
3613
+ buildRelatedPivotCondition(relatedValues) {
3614
+ const normalizedValues = relatedValues.filter((value) => value != null);
3615
+ if (normalizedValues.length === 0) return null;
3616
+ if (normalizedValues.length === 1) return {
3617
+ type: "comparison",
3618
+ column: this.relatedPivotKey,
3619
+ operator: "=",
3620
+ value: normalizedValues[0]
3621
+ };
3622
+ return {
3623
+ type: "comparison",
3624
+ column: this.relatedPivotKey,
3625
+ operator: "in",
3626
+ value: normalizedValues
3627
+ };
3628
+ }
3629
+ buildPivotMutationWhere(relatedValues = []) {
3630
+ const baseCondition = this.buildPivotWhere(this.resolveParentPivotValue());
3631
+ const relatedCondition = this.buildRelatedPivotCondition(relatedValues);
3632
+ if (!relatedCondition) return baseCondition;
3633
+ return {
3634
+ type: "group",
3635
+ operator: "and",
3636
+ conditions: [baseCondition, relatedCondition]
3637
+ };
3638
+ }
3639
+ normalizeIdentifierValue(value) {
3640
+ if (typeof value === "string" && /^-?\d+$/.test(value)) return Number(value);
3641
+ return value;
3642
+ }
3643
+ isPlainObject(value) {
3644
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
3645
+ }
3646
+ isModelLike(value) {
3647
+ return this.isPlainObject(value) && typeof value.getAttribute === "function";
3648
+ }
3649
+ normalizeRelatedItems(related) {
3650
+ return Array.isArray(related) ? related : [related];
3651
+ }
3652
+ normalizeSyncEntries(related, pivotAttributes = {}) {
3653
+ if (Array.isArray(related)) return related.map((item) => ({
3654
+ related: item,
3655
+ attributes: { ...pivotAttributes }
3656
+ }));
3657
+ if (this.isPlainObject(related) && !this.isModelLike(related)) return Object.entries(related).map(([key, attributes]) => ({
3658
+ related: this.normalizeIdentifierValue(key),
3659
+ attributes: this.isPlainObject(attributes) ? attributes : {}
3660
+ }));
3661
+ return [{
3662
+ related,
3663
+ attributes: { ...pivotAttributes }
3664
+ }];
3665
+ }
3666
+ resolveParentPivotValue() {
3667
+ return this.parent.getAttribute(this.parentKey);
3668
+ }
3669
+ resolveRelatedPivotValue(related) {
3670
+ if (related && typeof related === "object" && "getAttribute" in related) return related.getAttribute(this.relatedKey);
3671
+ return related;
3672
+ }
3673
+ buildPivotInsertValues(related, attributes = {}) {
3674
+ const values = {
3675
+ ...attributes,
3676
+ [this.foreignPivotKey]: this.resolveParentPivotValue(),
3677
+ [this.relatedPivotKey]: this.resolveRelatedPivotValue(related)
3678
+ };
3679
+ if (this.pivotCreatedAtColumn && !(this.pivotCreatedAtColumn in values)) values[this.pivotCreatedAtColumn] = /* @__PURE__ */ new Date();
3680
+ if (this.pivotUpdatedAtColumn && !(this.pivotUpdatedAtColumn in values)) values[this.pivotUpdatedAtColumn] = /* @__PURE__ */ new Date();
3681
+ return values;
3682
+ }
3683
+ attachPivotToSingleResult(related, pivotRow) {
3684
+ if (!this.shouldAttachPivotAttributes()) return related;
3685
+ related.setAttribute(this.pivotAccessor, this.createPivotRecord(pivotRow));
3686
+ return related;
3687
+ }
3688
+ async insertPivotRow(values, adapter = this.getRelationAdapter()) {
3689
+ await adapter.insert({
3690
+ target: this.buildPivotTarget(),
3691
+ values
3692
+ });
3693
+ }
3694
+ async selectPivotRows(where, adapter = this.getRelationAdapter()) {
3695
+ return await adapter.select({
3696
+ target: this.buildPivotTarget(),
3697
+ where
3698
+ });
3699
+ }
3700
+ async deletePivotRows(where, adapter = this.getRelationAdapter()) {
3701
+ if (typeof adapter.deleteMany === "function") return await adapter.deleteMany({
3702
+ target: this.buildPivotTarget(),
3703
+ where
3704
+ });
3705
+ const rows = await this.selectPivotRows(where, adapter);
3706
+ await Promise.all(rows.map(async (row) => {
3707
+ await adapter.delete({
3708
+ target: this.buildPivotTarget(),
3709
+ where: {
3710
+ type: "group",
3711
+ operator: "and",
3712
+ conditions: Object.entries(row).map(([column, value]) => ({
3713
+ type: "comparison",
3714
+ column,
3715
+ operator: "=",
3716
+ value
3717
+ }))
3718
+ }
3719
+ });
3720
+ }));
3721
+ return rows.length;
3722
+ }
3723
+ buildPivotUpdateValues(attributes = {}) {
3724
+ const values = { ...attributes };
3725
+ if (this.pivotUpdatedAtColumn && !(this.pivotUpdatedAtColumn in values)) values[this.pivotUpdatedAtColumn] = /* @__PURE__ */ new Date();
3726
+ return values;
3727
+ }
3728
+ async updatePivotRows(related, attributes, adapter = this.getRelationAdapter()) {
3729
+ const values = this.buildPivotUpdateValues(attributes);
3730
+ if (Object.keys(values).length === 0) return 0;
3731
+ const where = this.buildPivotMutationWhere([this.resolveRelatedPivotValue(related)]);
3732
+ if (typeof adapter.updateMany === "function") return await adapter.updateMany({
3733
+ target: this.buildPivotTarget(),
3734
+ where,
3735
+ values
3736
+ });
3737
+ const rows = await this.selectPivotRows(where, adapter);
3738
+ await Promise.all(rows.map(async (row) => {
3739
+ await adapter.update({
3740
+ target: this.buildPivotTarget(),
3741
+ where: {
3742
+ type: "group",
3743
+ operator: "and",
3744
+ conditions: Object.entries(row).map(([column, value]) => ({
3745
+ type: "comparison",
3746
+ column,
3747
+ operator: "=",
3748
+ value
3749
+ }))
3750
+ },
3751
+ values
3752
+ });
3753
+ }));
3754
+ return rows.length;
3755
+ }
3756
+ /**
3757
+ * Creates a new instance of the related model with the given attributes and attaches
3758
+ * pivot attributes if pivot attributes should be included.
3759
+ *
3760
+ * @param attributes The attributes to initialize the related model with.
3761
+ * @returns A new instance of the related model.
3762
+ */
3763
+ make(attributes = {}) {
3764
+ return this.related.hydrate(attributes);
3765
+ }
3766
+ /**
3767
+ * Creates a new related model record with the given attributes, creates a pivot record
3768
+ * with the given pivot attributes, and attaches pivot attributes if pivot attributes
3769
+ * should be included.
3770
+ *
3771
+ * @param attributes The attributes to initialize the related model with.
3772
+ * @param pivotAttributes The attributes to initialize the pivot record with.
3773
+ * @returns A new instance of the related model with pivot attributes attached.
3774
+ */
3775
+ async create(attributes = {}, pivotAttributes = {}) {
3776
+ const related = await this.related.query().create(attributes);
3777
+ const pivotRow = this.buildPivotInsertValues(related, pivotAttributes);
3778
+ await this.insertPivotRow(pivotRow);
3779
+ return this.attachPivotToSingleResult(related, pivotRow);
3780
+ }
3781
+ /**
3782
+ * Saves a related model record, creates a pivot record with the given pivot attributes
3783
+ * if the related model was not previously persisted, and attaches pivot attributes if
3784
+ * pivot attributes should be included.
3785
+ *
3786
+ * @param related The related model instance to save.
3787
+ * @param pivotAttributes The attributes to initialize the pivot record with.
3788
+ * @returns A new instance of the related model with pivot attributes attached.
3789
+ */
3790
+ async save(related, pivotAttributes = {}) {
3791
+ const saveable = related;
3792
+ let persisted = related;
3793
+ if (typeof saveable.save === "function") try {
3794
+ persisted = await saveable.save();
3795
+ } catch (error) {
3796
+ if (!(typeof error === "object" && error !== null && "code" in error && error.code === "MODEL_NOT_FOUND")) throw error;
3797
+ const attributes = typeof saveable.getRawAttributes === "function" ? saveable.getRawAttributes() : {};
3798
+ persisted = await this.related.query().create(attributes);
3799
+ }
3800
+ const pivotRow = this.buildPivotInsertValues(persisted, pivotAttributes);
3801
+ await this.insertPivotRow(pivotRow);
3802
+ return this.attachPivotToSingleResult(persisted, pivotRow);
3803
+ }
3804
+ /**
3805
+ * Attaches one or more related model records to the parent model by creating pivot
3806
+ * records with the given pivot attributes if pivot attributes should be included.
3807
+ *
3808
+ * @param related The related model instance(s) to attach.
3809
+ * @param pivotAttributes The attributes to initialize the pivot record with.
3810
+ * @returns The number of related model records attached.
3811
+ */
3812
+ async attach(related, pivotAttributes = {}) {
3813
+ const items = Array.isArray(related) ? related : [related];
3814
+ await Promise.all(items.map(async (item) => {
3815
+ await this.insertPivotRow(this.buildPivotInsertValues(item, pivotAttributes));
3816
+ }));
3817
+ return items.length;
3818
+ }
3819
+ /**
3820
+ * Detaches one or more related model records from the parent model by deleting
3821
+ * matching pivot rows. When no related value is provided, all matching pivot rows
3822
+ * for the parent are removed.
3823
+ *
3824
+ * @param related
3825
+ * @returns
3826
+ */
3827
+ async detach(related) {
3828
+ const where = related === void 0 ? this.buildPivotWhere(this.resolveParentPivotValue()) : this.buildPivotMutationWhere(this.normalizeRelatedItems(related).map((item) => this.resolveRelatedPivotValue(item)));
3829
+ return await this.deletePivotRows(where);
3830
+ }
3831
+ /**
3832
+ * Synchronizes the pivot table so only the provided related values remain attached.
3833
+ * Existing matching rows can receive updated pivot attributes during the operation.
3834
+ *
3835
+ * @param related
3836
+ * @param pivotAttributes
3837
+ * @returns
3838
+ */
3839
+ async sync(related, pivotAttributes = {}) {
3840
+ return await this.getRelationAdapter().transaction(async (transaction) => {
3841
+ const existingRows = await this.selectPivotRows(this.buildPivotWhere(this.resolveParentPivotValue()), transaction);
3842
+ const desiredEntries = /* @__PURE__ */ new Map();
3843
+ this.normalizeSyncEntries(related, pivotAttributes).forEach((entry) => {
3844
+ const relatedValue = this.resolveRelatedPivotValue(entry.related);
3845
+ if (relatedValue == null) return;
3846
+ desiredEntries.set(String(relatedValue), {
3847
+ related: relatedValue,
3848
+ attributes: entry.attributes
3849
+ });
3850
+ });
3851
+ let detached = 0;
3852
+ let attached = 0;
3853
+ let updated = 0;
3854
+ const existingKeys = /* @__PURE__ */ new Set();
3855
+ for (const row of existingRows) {
3856
+ const relatedValue = row[this.relatedPivotKey];
3857
+ if (relatedValue == null) continue;
3858
+ const relatedKey = String(relatedValue);
3859
+ existingKeys.add(relatedKey);
3860
+ if (!desiredEntries.has(relatedKey)) detached += await this.deletePivotRows(this.buildPivotMutationWhere([relatedValue]), transaction);
3861
+ }
3862
+ for (const [relatedKey, entry] of desiredEntries) {
3863
+ if (!existingKeys.has(relatedKey)) {
3864
+ await this.insertPivotRow(this.buildPivotInsertValues(entry.related, entry.attributes), transaction);
3865
+ attached += 1;
3866
+ continue;
3867
+ }
3868
+ if (Object.keys(entry.attributes).length === 0) continue;
3869
+ updated += await this.updatePivotRows(entry.related, entry.attributes, transaction);
3870
+ }
3871
+ return {
3872
+ attached,
3873
+ detached,
3874
+ updated
3875
+ };
3876
+ });
3877
+ }
3878
+ shouldAttachPivotAttributes() {
3879
+ return this.shouldAttachPivot || this.pivotColumns.size > 0 || Boolean(this.pivotCreatedAtColumn) || Boolean(this.pivotUpdatedAtColumn) || Boolean(this.pivotModel);
3880
+ }
3881
+ getPivotColumnSelection() {
3882
+ return [
3883
+ this.foreignPivotKey,
3884
+ this.relatedPivotKey,
3885
+ ...this.pivotColumns
3886
+ ].filter((column, index, all) => all.indexOf(column) === index);
3887
+ }
3888
+ /**
3889
+ * Creates a pivot record from a row of data.
3890
+ *
3891
+ * @param row The row of data containing pivot attributes.
3892
+ * @returns The pivot record.
3893
+ */
3894
+ createPivotRecord(row) {
3895
+ const attributes = this.getPivotColumnSelection().reduce((all, column) => {
3896
+ all[column] = row[column];
3897
+ return all;
3898
+ }, {});
3899
+ if (!this.pivotModel) return attributes;
3900
+ if (typeof this.pivotModel.hydrate === "function") return this.pivotModel.hydrate(attributes);
3901
+ return new this.pivotModel(attributes);
3902
+ }
3903
+ /**
3904
+ * Attaches pivot attributes to the related models if pivot attributes should be included.
3905
+ *
3906
+ * @param results
3907
+ * @param pivotRows
3908
+ * @returns
3909
+ */
3910
+ attachPivotToResults(results, pivotRows) {
3911
+ if (!this.shouldAttachPivotAttributes()) return results;
3912
+ const pivotByRelatedKey = /* @__PURE__ */ new Map();
3913
+ pivotRows.forEach((row) => {
3914
+ const relatedValue = row[this.relatedPivotKey];
3915
+ if (relatedValue == null) return;
3916
+ pivotByRelatedKey.set(String(relatedValue), row);
3917
+ });
3918
+ results.all().forEach((related) => {
3919
+ const model = related;
3920
+ const relatedValue = model.getAttribute(this.relatedKey);
3921
+ if (relatedValue == null) return;
3922
+ const pivotRow = pivotByRelatedKey.get(String(relatedValue));
3923
+ if (!pivotRow) return;
3924
+ model.setAttribute(this.pivotAccessor, this.createPivotRecord(pivotRow));
3925
+ });
3926
+ return results;
3927
+ }
3928
+ attachPivotToModel(model, pivotRows) {
3929
+ if (!model) return model;
3930
+ return this.attachPivotToResults(new ArkormCollection([model]), pivotRows).all()[0] ?? null;
3931
+ }
3932
+ decorateQueryBuilder(query, pivotRows) {
3933
+ const decorated = query;
3934
+ if (decorated[BelongsToManyRelation.queryDecorationMarker]) return query;
3935
+ const originalGet = query.get.bind(query);
3936
+ const originalFirst = query.first.bind(query);
3937
+ const originalPaginate = query.paginate.bind(query);
3938
+ const originalSimplePaginate = query.simplePaginate.bind(query);
3939
+ const originalClone = query.clone.bind(query);
3940
+ decorated.get = (async () => {
3941
+ const results = await originalGet();
3942
+ return this.attachPivotToResults(results, pivotRows);
3943
+ });
3944
+ decorated.first = (async () => {
3945
+ const result = await originalFirst();
3946
+ return this.attachPivotToModel(result, pivotRows);
3947
+ });
3948
+ decorated.paginate = (async (perPage = 15, page, options = {}) => {
3949
+ const paginator = await originalPaginate(perPage, page, options);
3950
+ return new LengthAwarePaginator(this.attachPivotToResults(paginator.data, pivotRows), paginator.meta.total, paginator.meta.perPage, paginator.meta.currentPage, options);
3951
+ });
3952
+ decorated.simplePaginate = (async (perPage = 15, page, options = {}) => {
3953
+ const paginator = await originalSimplePaginate(perPage, page, options);
3954
+ return new Paginator(this.attachPivotToResults(paginator.data, pivotRows), paginator.meta.perPage, paginator.meta.currentPage, paginator.meta.hasMorePages, options);
3955
+ });
3956
+ decorated.clone = (() => {
3957
+ return this.decorateQueryBuilder(originalClone(), pivotRows);
3958
+ });
3959
+ decorated[BelongsToManyRelation.queryDecorationMarker] = true;
3960
+ return query;
3961
+ }
3962
+ async loadPivotRowsForParent() {
3963
+ const parentValue = this.resolveParentPivotValue();
3964
+ return await this.createRelationTableLoader().selectRows({
3965
+ table: this.throughTable,
3966
+ where: this.buildPivotWhere(parentValue),
3967
+ columns: this.getPivotColumnSelection().map((column) => ({ column }))
3968
+ });
3969
+ }
3970
+ /**
3971
+ * Build the relationship query.
3972
+ *
3973
+ * @returns
3974
+ */
3975
+ async getQuery() {
3976
+ const pivotRows = await this.loadPivotRowsForParent();
3977
+ const ids = pivotRows.map((row) => row[this.relatedPivotKey]);
3978
+ return this.decorateQueryBuilder(this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } })), pivotRows);
3979
+ }
3980
+ getMetadata() {
3981
+ const shouldAttachPivot = this.shouldAttachPivotAttributes();
3982
+ return {
3983
+ type: "belongsToMany",
3984
+ relatedModel: this.related,
3985
+ throughTable: this.throughTable,
3986
+ foreignPivotKey: this.foreignPivotKey,
3987
+ relatedPivotKey: this.relatedPivotKey,
3988
+ parentKey: this.parentKey,
3989
+ relatedKey: this.relatedKey,
3990
+ pivotAccessor: shouldAttachPivot ? this.pivotAccessor : void 0,
3991
+ pivotColumns: [...this.pivotColumns],
3992
+ pivotCreatedAtColumn: this.pivotCreatedAtColumn,
3993
+ pivotUpdatedAtColumn: this.pivotUpdatedAtColumn,
3994
+ pivotWhere: this.pivotWhere,
3995
+ pivotModel: this.pivotModel
3996
+ };
3997
+ }
3998
+ /**
3999
+ * Fetches the related models for this relationship.
4000
+ *
4001
+ * @returns
4002
+ */
4003
+ async getResults() {
4004
+ return (await this.getQuery()).get();
4005
+ }
4006
+ };
4007
+
4008
+ //#endregion
4009
+ //#region src/relationship/SingleResultRelation.ts
4010
+ /**
4011
+ * Base class for relationships that resolve to a single related model.
4012
+ *
4013
+ * @author Legacy (3m1n3nc3)
4014
+ * @since 1.3.0
4015
+ */
4016
+ var SingleResultRelation = class extends Relation {
4017
+ defaultValue;
4018
+ constructor(parent, related) {
4019
+ super();
4020
+ this.parent = parent;
4021
+ this.related = related;
4022
+ }
4023
+ /**
4024
+ * Defines a default value to return when the relationship does not find a related model.
4025
+ *
4026
+ * @param value The default value or a callback that returns the default value.
4027
+ * @returns The current instance for method chaining.
4028
+ */
4029
+ withDefault(value = {}) {
4030
+ this.defaultValue = value;
4031
+ return this;
4032
+ }
4033
+ resolveDefaultResult() {
4034
+ if (typeof this.defaultValue === "undefined") return null;
4035
+ const resolved = typeof this.defaultValue === "function" ? this.defaultValue(this.parent) : this.defaultValue;
4036
+ if (resolved instanceof this.related) return resolved;
4037
+ return this.related.hydrate(resolved);
4038
+ }
4039
+ };
4040
+
4041
+ //#endregion
4042
+ //#region src/relationship/BelongsToRelation.ts
4043
+ /**
4044
+ * Defines an inverse one-to-one or many relationship.
4045
+ *
4046
+ * @author Legacy (3m1n3nc3)
4047
+ * @since 0.1.0
4048
+ */
4049
+ var BelongsToRelation = class extends SingleResultRelation {
4050
+ constructor(parent, related, foreignKey, ownerKey) {
4051
+ super(parent, related);
4052
+ this.foreignKey = foreignKey;
4053
+ this.ownerKey = ownerKey;
4054
+ }
4055
+ /**
4056
+ * Build the relationship query.
4057
+ *
4058
+ * @returns
4059
+ */
4060
+ async getQuery() {
4061
+ const foreignValue = this.parent.getAttribute(this.foreignKey);
4062
+ return this.applyConstraint(this.related.query().where({ [this.ownerKey]: foreignValue }));
4063
+ }
4064
+ getMetadata() {
4065
+ return {
4066
+ type: "belongsTo",
4067
+ relatedModel: this.related,
4068
+ foreignKey: this.foreignKey,
4069
+ ownerKey: this.ownerKey
4070
+ };
4071
+ }
4072
+ /**
4073
+ * Fetches the related models for this relationship.
4074
+ *
4075
+ * @returns
4076
+ */
4077
+ async getResults() {
4078
+ return await (await this.getQuery()).first() ?? this.resolveDefaultResult();
4079
+ }
4080
+ };
4081
+
4082
+ //#endregion
4083
+ //#region src/relationship/HasManyRelation.ts
4084
+ /**
4085
+ * Defines a one-to-many relationship.
4086
+ *
4087
+ * @author Legacy (3m1n3nc3)
4088
+ * @since 0.1.0
4089
+ */
4090
+ var HasManyRelation = class extends Relation {
4091
+ constructor(parent, related, foreignKey, localKey) {
4092
+ super();
4093
+ this.parent = parent;
4094
+ this.related = related;
4095
+ this.foreignKey = foreignKey;
4096
+ this.localKey = localKey;
4097
+ }
4098
+ /**
4099
+ * Build the relationship query.
4100
+ *
4101
+ * @returns
4102
+ */
4103
+ async getQuery() {
4104
+ const localValue = this.parent.getAttribute(this.localKey);
4105
+ return this.applyConstraint(this.related.query().where({ [this.foreignKey]: localValue }));
4106
+ }
4107
+ getMetadata() {
4108
+ return {
4109
+ type: "hasMany",
4110
+ relatedModel: this.related,
4111
+ foreignKey: this.foreignKey,
4112
+ localKey: this.localKey
4113
+ };
4114
+ }
4115
+ /**
4116
+ * Fetches the related models for this relationship.
4117
+ *
4118
+ * @returns
4119
+ */
4120
+ async getResults() {
4121
+ return (await this.getQuery()).get();
4122
+ }
4123
+ };
4124
+
4125
+ //#endregion
4126
+ //#region src/relationship/HasManyThroughRelation.ts
4127
+ /**
4128
+ * Defines a has-many-through relationship, which provides a convenient way to access
4129
+ * distant relations via an intermediate relation.
4130
+ *
4131
+ * @author Legacy (3m1n3nc3)
4132
+ * @since 0.1.0
4133
+ */
4134
+ var HasManyThroughRelation = class extends Relation {
4135
+ constructor(parent, related, throughTable, firstKey, secondKey, localKey, secondLocalKey) {
4136
+ super();
4137
+ this.parent = parent;
4138
+ this.related = related;
4139
+ this.throughTable = throughTable;
4140
+ this.firstKey = firstKey;
4141
+ this.secondKey = secondKey;
4142
+ this.localKey = localKey;
4143
+ this.secondLocalKey = secondLocalKey;
4144
+ }
4145
+ /**
4146
+ * Build the relationship query.
4147
+ *
4148
+ * @returns
4149
+ */
4150
+ async getQuery() {
4151
+ const localValue = this.parent.getAttribute(this.localKey);
4152
+ const keys = await this.createRelationTableLoader().selectColumnValues({
4153
+ lookup: {
4154
+ table: this.throughTable,
4155
+ where: {
4156
+ type: "comparison",
4157
+ column: this.firstKey,
4158
+ operator: "=",
4159
+ value: localValue
4160
+ }
4161
+ },
4162
+ column: this.secondLocalKey
4163
+ });
4164
+ return this.applyConstraint(this.related.query().where({ [this.secondKey]: { in: keys } }));
4165
+ }
4166
+ getMetadata() {
4167
+ return {
4168
+ type: "hasManyThrough",
4169
+ relatedModel: this.related,
4170
+ throughTable: this.throughTable,
4171
+ firstKey: this.firstKey,
4172
+ secondKey: this.secondKey,
4173
+ localKey: this.localKey,
4174
+ secondLocalKey: this.secondLocalKey
4175
+ };
4176
+ }
4177
+ /**
4178
+ * Fetches the related models for this relationship.
4179
+ *
4180
+ * @returns
4181
+ */
4182
+ async getResults() {
4183
+ return (await this.getQuery()).get();
4184
+ }
4185
+ };
4186
+
4187
+ //#endregion
4188
+ //#region src/relationship/HasOneRelation.ts
4189
+ /**
4190
+ * Represents a "has one" relationship between two models.
4191
+ *
4192
+ * @author Legacy (3m1n3nc3)
4193
+ * @since 0.1.0
4194
+ */
4195
+ var HasOneRelation = class extends SingleResultRelation {
4196
+ constructor(parent, related, foreignKey, localKey) {
4197
+ super(parent, related);
4198
+ this.foreignKey = foreignKey;
4199
+ this.localKey = localKey;
4200
+ }
4201
+ /**
4202
+ * Build the relationship query.
4203
+ *
4204
+ * @returns
4205
+ */
4206
+ async getQuery() {
4207
+ const localValue = this.parent.getAttribute(this.localKey);
4208
+ return this.applyConstraint(this.related.query().where({ [this.foreignKey]: localValue }));
4209
+ }
4210
+ getMetadata() {
4211
+ return {
4212
+ type: "hasOne",
4213
+ relatedModel: this.related,
4214
+ foreignKey: this.foreignKey,
4215
+ localKey: this.localKey
4216
+ };
4217
+ }
4218
+ /**
4219
+ * Fetches the related models for this relationship.
4220
+ *
4221
+ * @returns
4222
+ */
4223
+ async getResults() {
4224
+ return await (await this.getQuery()).first() ?? this.resolveDefaultResult();
4225
+ }
4226
+ };
4227
+
4228
+ //#endregion
4229
+ //#region src/relationship/HasOneThroughRelation.ts
4230
+ /**
4231
+ * Represents a "has one through" relationship, where the parent model is related
4232
+ * to exactly one instance of the related model through an intermediate model.
4233
+ *
4234
+ * @author Legacy (3m1n3nc3)
4235
+ * @since 0.1.0
4236
+ */
4237
+ var HasOneThroughRelation = class extends SingleResultRelation {
4238
+ constructor(parent, related, throughTable, firstKey, secondKey, localKey, secondLocalKey) {
4239
+ super(parent, related);
4240
+ this.throughTable = throughTable;
4241
+ this.firstKey = firstKey;
4242
+ this.secondKey = secondKey;
4243
+ this.localKey = localKey;
4244
+ this.secondLocalKey = secondLocalKey;
4245
+ }
4246
+ /**
4247
+ * Build the relationship query.
4248
+ *
4249
+ * @returns
4250
+ */
4251
+ async getQuery() {
4252
+ const localValue = this.parent.getAttribute(this.localKey);
4253
+ const intermediateKey = await this.createRelationTableLoader().selectColumnValue({
4254
+ lookup: {
4255
+ table: this.throughTable,
4256
+ where: {
4257
+ type: "comparison",
4258
+ column: this.firstKey,
4259
+ operator: "=",
4260
+ value: localValue
4261
+ }
4262
+ },
4263
+ column: this.secondLocalKey
4264
+ });
4265
+ if (intermediateKey == null) return this.applyConstraint(this.related.query().where({ [this.secondKey]: { in: [] } }));
4266
+ return this.applyConstraint(this.related.query().where({ [this.secondKey]: intermediateKey }));
4267
+ }
4268
+ getMetadata() {
4269
+ return {
4270
+ type: "hasOneThrough",
4271
+ relatedModel: this.related,
4272
+ throughTable: this.throughTable,
4273
+ firstKey: this.firstKey,
4274
+ secondKey: this.secondKey,
4275
+ localKey: this.localKey,
4276
+ secondLocalKey: this.secondLocalKey
4277
+ };
4278
+ }
4279
+ /**
4280
+ * Fetches the related models for this relationship.
4281
+ *
4282
+ * @returns
4283
+ */
4284
+ async getResults() {
4285
+ return await (await this.getQuery()).first() ?? this.resolveDefaultResult();
4286
+ }
4287
+ };
4288
+
4289
+ //#endregion
4290
+ //#region src/relationship/MorphManyRelation.ts
4291
+ /**
4292
+ * Defines a polymorphic one-to-many relationship.
4293
+ *
4294
+ * @author Legacy (3m1n3nc3)
4295
+ * @since 0.1.0
4296
+ */
4297
+ var MorphManyRelation = class extends Relation {
4298
+ constructor(parent, related, morphName, localKey) {
4299
+ super();
4300
+ this.parent = parent;
4301
+ this.related = related;
4302
+ this.morphName = morphName;
4303
+ this.localKey = localKey;
4304
+ }
4305
+ /**
4306
+ * Build the relationship query.
4307
+ *
4308
+ * @returns
4309
+ */
4310
+ async getQuery() {
4311
+ const id = this.parent.getAttribute(this.localKey);
4312
+ const type = this.parent.constructor.name;
4313
+ return this.applyConstraint(this.related.query().where({
4314
+ [`${this.morphName}Id`]: id,
4315
+ [`${this.morphName}Type`]: type
4316
+ }));
4317
+ }
4318
+ getMetadata() {
4319
+ return {
4320
+ type: "morphMany",
4321
+ relatedModel: this.related,
4322
+ morphName: this.morphName,
4323
+ morphIdColumn: `${this.morphName}Id`,
4324
+ morphTypeColumn: `${this.morphName}Type`,
4325
+ localKey: this.localKey
4326
+ };
4327
+ }
4328
+ /**
4329
+ * Fetches the related models for this relationship.
4330
+ *
4331
+ * @returns
4332
+ */
4333
+ async getResults() {
4334
+ return (await this.getQuery()).get();
4335
+ }
4336
+ };
4337
+
4338
+ //#endregion
4339
+ //#region src/relationship/MorphOneRelation.ts
4340
+ /**
4341
+ * Defines a polymorphic one-to-one relationship.
4342
+ *
4343
+ * @author Legacy (3m1n3nc3)
4344
+ * @since 0.1.0
4345
+ */
4346
+ var MorphOneRelation = class extends SingleResultRelation {
4347
+ constructor(parent, related, morphName, localKey) {
4348
+ super(parent, related);
4349
+ this.morphName = morphName;
4350
+ this.localKey = localKey;
4351
+ }
4352
+ /**
4353
+ * Build the relationship query.
4354
+ *
4355
+ * @returns
4356
+ */
4357
+ async getQuery() {
4358
+ const id = this.parent.getAttribute(this.localKey);
4359
+ const type = this.parent.constructor.name;
4360
+ return this.applyConstraint(this.related.query().where({
4361
+ [`${this.morphName}Id`]: id,
4362
+ [`${this.morphName}Type`]: type
4363
+ }));
4364
+ }
4365
+ getMetadata() {
4366
+ return {
4367
+ type: "morphOne",
4368
+ relatedModel: this.related,
4369
+ morphName: this.morphName,
4370
+ morphIdColumn: `${this.morphName}Id`,
4371
+ morphTypeColumn: `${this.morphName}Type`,
4372
+ localKey: this.localKey
4373
+ };
4374
+ }
4375
+ /**
4376
+ * Fetches the related models for this relationship.
4377
+ *
4378
+ * @returns
4379
+ */
4380
+ async getResults() {
4381
+ return await (await this.getQuery()).first() ?? this.resolveDefaultResult();
4382
+ }
4383
+ };
4384
+
4385
+ //#endregion
4386
+ //#region src/relationship/MorphToManyRelation.ts
4387
+ /**
4388
+ * Defines a polymorphic many-to-many relationship.
4389
+ *
4390
+ * @author Legacy (3m1n3nc3)
4391
+ * @since 0.1.0
4392
+ */
4393
+ var MorphToManyRelation = class extends Relation {
4394
+ constructor(parent, related, throughTable, morphName, relatedPivotKey, parentKey, relatedKey) {
4395
+ super();
4396
+ this.parent = parent;
4397
+ this.related = related;
4398
+ this.throughTable = throughTable;
4399
+ this.morphName = morphName;
4400
+ this.relatedPivotKey = relatedPivotKey;
4401
+ this.parentKey = parentKey;
4402
+ this.relatedKey = relatedKey;
4403
+ }
4404
+ /**
4405
+ * Build the relationship query.
4406
+ *
4407
+ * @returns
4408
+ */
4409
+ async getQuery() {
4410
+ const parentValue = this.parent.getAttribute(this.parentKey);
4411
+ const morphType = this.parent.constructor.name;
4412
+ const ids = await this.createRelationTableLoader().selectColumnValues({
4413
+ lookup: {
4414
+ table: this.throughTable,
4415
+ where: {
4416
+ type: "group",
4417
+ operator: "and",
4418
+ conditions: [{
4419
+ type: "comparison",
4420
+ column: `${this.morphName}Id`,
4421
+ operator: "=",
4422
+ value: parentValue
4423
+ }, {
4424
+ type: "comparison",
4425
+ column: `${this.morphName}Type`,
4426
+ operator: "=",
4427
+ value: morphType
4428
+ }]
4429
+ }
4430
+ },
4431
+ column: this.relatedPivotKey
4432
+ });
4433
+ return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } }));
4434
+ }
4435
+ getMetadata() {
4436
+ return {
4437
+ type: "morphToMany",
4438
+ relatedModel: this.related,
4439
+ throughTable: this.throughTable,
4440
+ morphName: this.morphName,
4441
+ morphIdColumn: `${this.morphName}Id`,
4442
+ morphTypeColumn: `${this.morphName}Type`,
4443
+ relatedPivotKey: this.relatedPivotKey,
4444
+ parentKey: this.parentKey,
4445
+ relatedKey: this.relatedKey
4446
+ };
4447
+ }
4448
+ /**
4449
+ * Fetches the related models for this relationship.
4450
+ *
4451
+ * @returns
4452
+ */
4453
+ async getResults() {
4454
+ return (await this.getQuery()).get();
4455
+ }
4456
+ };
4457
+
4458
+ //#endregion
4459
+ export { PRISMA_ENUM_MEMBER_REGEX as $, getLastMigrationRun as $t, isDelegateLike as A, getMigrationPlan as At, getPersistedEnumMap as B, toMigrationFileSlug as Bt, getRuntimeAdapter as C, escapeRegex as Ct, getRuntimePaginationURLDriverFactory as D, formatEnumDefaultValue as Dt, getRuntimePaginationCurrentPageResolver as E, formatDefaultValue as Et, runArkormTransaction as F, runMigrationWithPrisma as Ft, readPersistedColumnMappingsState as G, ForeignKeyBuilder as Gt, getPersistedPrimaryKeyGeneration as H, SchemaBuilder as Ht, applyOperationsToPersistedColumnMappingsState as I, runPrismaCommand as It, resolveColumnMappingsFilePath as J, buildMigrationRunId as Jt, rebuildPersistedColumnMappingsState as K, PrimaryKeyGenerationPlanner as Kt, createEmptyPersistedColumnMappingsState as L, stripPrismaSchemaModelsAndEnums as Lt, isTransactionCapableClient as M, resolveEnumName as Mt, loadArkormConfig as N, resolveMigrationClassName as Nt, getRuntimePrismaClient as O, formatRelationAction as Ot, resetArkormRuntimeForTests as P, resolvePrismaType as Pt, writePersistedColumnMappingsState as Q, findAppliedMigration as Qt, deletePersistedColumnMappingsState as R, supportsDatabaseMigrationExecution as Rt, getDefaultStubsPath as S, deriveSingularFieldName as St, getRuntimeDebugHandler as T, findModelBlock as Tt, getPersistedTableMetadata as U, EnumBuilder as Ut, getPersistedEnumTsType as V, toModelName as Vt, getPersistedTimestampColumns as W, TableBuilder as Wt, syncPersistedColumnMappingsFromState as X, createEmptyAppliedMigrationsState as Xt, resolvePersistedMetadataFeatures as Y, computeMigrationChecksum as Yt, validatePersistedMetadataFeaturesForMigrations as Z, deleteAppliedMigrationsStateFromStore as Zt, defineConfig as _, ArkormException as _n, createMigrationTimestamp as _t, HasOneRelation as a, readAppliedMigrationsStateFromStore as an, applyMigrationRollbackToDatabase as at, getActiveTransactionAdapter as b, deriveRelationAlias as bt, BelongsToRelation as c, supportsDatabaseMigrationState as cn, applyMigrationToPrismaSchema as ct, Relation as d, RuntimeModuleLoader as dn, buildFieldLine as dt, getLatestAppliedMigrations as en, PRISMA_ENUM_REGEX as et, LengthAwarePaginator as f, UnsupportedAdapterFeatureException as fn, buildIndexLine as ft, configureArkormRuntime as g, ArkormCollection as gn, buildRelationLine as gt, bindAdapterToModels as h, RelationResolutionException as hn, buildModelBlock as ht, HasOneThroughRelation as i, readAppliedMigrationsState as in, applyDropTableOperation as it, isQuerySchemaLike as j, pad as jt, getUserConfig as k, generateMigrationFile as kt, SingleResultRelation as l, writeAppliedMigrationsState as ln, applyOperationsToPrismaSchema as lt, URLDriver as m, RelationTableLoader as mn, buildMigrationSource as mt, MorphOneRelation as n, markMigrationApplied as nn, applyAlterTableOperation as nt, HasManyThroughRelation as o, removeAppliedMigration as on, applyMigrationRollbackToPrismaSchema as ot, Paginator as p, SetBasedEagerLoader as pn, buildInverseRelationLine as pt, resetPersistedColumnMappingsCache as q, buildMigrationIdentity as qt, MorphManyRelation as r, markMigrationRun as rn, applyCreateTableOperation as rt, HasManyRelation as s, resolveMigrationStateFilePath as sn, applyMigrationToDatabase as st, MorphToManyRelation as t, isMigrationApplied as tn, PRISMA_MODEL_REGEX as tt, BelongsToManyRelation as u, writeAppliedMigrationsStateToStore as un, buildEnumBlock as ut, emitRuntimeDebugEvent as v, deriveCollectionFieldName as vt, getRuntimeClient as w, findEnumBlock as wt, getActiveTransactionClient as x, deriveRelationFieldName as xt, ensureArkormConfigLoading as y, deriveInverseRelationAlias as yt, getPersistedColumnMap as z, supportsDatabaseReset as zt };