arkormx 2.0.6 → 2.0.7

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