arkormx 2.11.13 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,17 +26,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
 
27
27
  //#endregion
28
28
  let _h3ravel_collect_js = require("@h3ravel/collect.js");
29
+ let node_path = require("node:path");
30
+ let node_fs = require("node:fs");
31
+ let node_crypto = require("node:crypto");
29
32
  let async_hooks = require("async_hooks");
30
33
  let jiti = require("jiti");
31
34
  let node_url = require("node:url");
32
- let node_path = require("node:path");
33
35
  let module$1 = require("module");
34
36
  let fs = require("fs");
35
37
  let url = require("url");
36
38
  let path = require("path");
37
39
  path = __toESM(path, 1);
38
- let node_fs = require("node:fs");
39
- let node_crypto = require("node:crypto");
40
40
  let _h3ravel_support = require("@h3ravel/support");
41
41
  let node_async_hooks = require("node:async_hooks");
42
42
  let node_child_process = require("node:child_process");
@@ -92,1309 +92,981 @@ var RelationResolutionException = class extends ArkormException {
92
92
  };
93
93
 
94
94
  //#endregion
95
- //#region src/relationship/RelationTableLoader.ts
96
- /**
97
- * Utility class responsible for loading data from relation tables, which are used to
98
- * manage relationships between models in Arkorm.
99
- *
100
- * @author Legacy (3m1n3nc3)
101
- * @since 2.0.0-next.0
102
- */
103
- var RelationTableLoader = class {
104
- constructor(adapter) {
105
- this.adapter = adapter;
106
- }
107
- async selectRows(spec) {
108
- return await this.adapter.select({
109
- target: { table: spec.table },
110
- where: spec.where,
111
- columns: spec.columns,
112
- orderBy: spec.orderBy,
113
- limit: spec.limit,
114
- offset: spec.offset
115
- });
116
- }
117
- async selectRow(spec) {
118
- return await this.adapter.selectOne({
119
- target: { table: spec.table },
120
- where: spec.where,
121
- columns: spec.columns,
122
- orderBy: spec.orderBy,
123
- limit: spec.limit ?? 1,
124
- offset: spec.offset
125
- });
95
+ //#region src/helpers/migration-history.ts
96
+ const createEmptyAppliedMigrationsState = () => ({
97
+ version: 1,
98
+ migrations: [],
99
+ runs: []
100
+ });
101
+ const supportsDatabaseMigrationState = (adapter) => {
102
+ return typeof adapter?.readAppliedMigrationsState === "function" && typeof adapter?.writeAppliedMigrationsState === "function";
103
+ };
104
+ const resolveMigrationStateFilePath = (cwd, configuredPath) => {
105
+ if (configuredPath && configuredPath.trim().length > 0) return (0, node_path.resolve)(configuredPath);
106
+ return (0, node_path.join)(cwd, ".arkormx", "migrations.applied.json");
107
+ };
108
+ const buildMigrationIdentity = (filePath, className) => {
109
+ const fileName = filePath.split("/").pop()?.split("\\").pop() ?? filePath;
110
+ return `${fileName.slice(0, fileName.length - (0, node_path.extname)(fileName).length)}:${className}`;
111
+ };
112
+ const computeMigrationChecksum = (filePath) => {
113
+ if (!(0, node_fs.existsSync)(filePath)) return (0, node_crypto.createHash)("sha256").update(filePath).digest("hex");
114
+ const source = (0, node_fs.readFileSync)(filePath, "utf-8");
115
+ return (0, node_crypto.createHash)("sha256").update(source).digest("hex");
116
+ };
117
+ const readAppliedMigrationsState = (stateFilePath) => {
118
+ if (!(0, node_fs.existsSync)(stateFilePath)) return createEmptyAppliedMigrationsState();
119
+ try {
120
+ const parsed = JSON.parse((0, node_fs.readFileSync)(stateFilePath, "utf-8"));
121
+ if (!Array.isArray(parsed.migrations)) return createEmptyAppliedMigrationsState();
122
+ return {
123
+ version: 1,
124
+ migrations: parsed.migrations.filter((migration) => {
125
+ 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");
126
+ }),
127
+ runs: Array.isArray(parsed.runs) ? parsed.runs.filter((run) => {
128
+ return typeof run?.id === "string" && typeof run?.appliedAt === "string" && Array.isArray(run?.migrationIds) && run.migrationIds.every((item) => typeof item === "string");
129
+ }) : []
130
+ };
131
+ } catch {
132
+ return createEmptyAppliedMigrationsState();
126
133
  }
127
- async selectColumnValues(spec) {
128
- return (await this.selectRows({
129
- ...spec.lookup,
130
- columns: [{ column: spec.column }]
131
- })).map((row) => row[spec.column]);
134
+ };
135
+ const readAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
136
+ if (supportsDatabaseMigrationState(adapter)) return await adapter.readAppliedMigrationsState();
137
+ return readAppliedMigrationsState(stateFilePath);
138
+ };
139
+ const writeAppliedMigrationsState = (stateFilePath, state) => {
140
+ const directory = (0, node_path.dirname)(stateFilePath);
141
+ if (!(0, node_fs.existsSync)(directory)) (0, node_fs.mkdirSync)(directory, { recursive: true });
142
+ (0, node_fs.writeFileSync)(stateFilePath, JSON.stringify(state, null, 2));
143
+ };
144
+ const writeAppliedMigrationsStateToStore = async (adapter, stateFilePath, state) => {
145
+ if (supportsDatabaseMigrationState(adapter)) {
146
+ await adapter.writeAppliedMigrationsState(state);
147
+ return;
132
148
  }
133
- async selectColumnValue(spec) {
134
- return (await this.selectRow({
135
- ...spec.lookup,
136
- columns: [{ column: spec.column }],
137
- limit: 1
138
- }))?.[spec.column] ?? null;
149
+ writeAppliedMigrationsState(stateFilePath, state);
150
+ };
151
+ const deleteAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
152
+ if (supportsDatabaseMigrationState(adapter)) {
153
+ await adapter.writeAppliedMigrationsState(createEmptyAppliedMigrationsState());
154
+ return "database";
139
155
  }
156
+ if (!(0, node_fs.existsSync)(stateFilePath)) return "missing-file";
157
+ writeAppliedMigrationsState(stateFilePath, createEmptyAppliedMigrationsState());
158
+ return "file";
159
+ };
160
+ const isMigrationApplied = (state, identity, checksum) => {
161
+ const matched = state.migrations.find((migration) => migration.id === identity);
162
+ if (!matched) return false;
163
+ if (checksum && matched.checksum) return matched.checksum === checksum;
164
+ if (checksum && !matched.checksum) return false;
165
+ return true;
166
+ };
167
+ const findAppliedMigration = (state, identity) => {
168
+ return state.migrations.find((migration) => migration.id === identity);
169
+ };
170
+ const markMigrationApplied = (state, entry) => {
171
+ const next = state.migrations.filter((migration) => migration.id !== entry.id);
172
+ next.push(entry);
173
+ return {
174
+ version: 1,
175
+ migrations: next,
176
+ runs: state.runs ?? []
177
+ };
178
+ };
179
+ const removeAppliedMigration = (state, identity) => {
180
+ return {
181
+ version: 1,
182
+ migrations: state.migrations.filter((migration) => migration.id !== identity),
183
+ runs: (state.runs ?? []).map((run) => ({
184
+ ...run,
185
+ migrationIds: run.migrationIds.filter((id) => id !== identity)
186
+ })).filter((run) => run.migrationIds.length > 0)
187
+ };
188
+ };
189
+ const buildMigrationRunId = () => {
190
+ return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
191
+ };
192
+ const markMigrationRun = (state, run) => {
193
+ const nextRuns = (state.runs ?? []).filter((existing) => existing.id !== run.id);
194
+ nextRuns.push(run);
195
+ return {
196
+ version: 1,
197
+ migrations: state.migrations,
198
+ runs: nextRuns
199
+ };
200
+ };
201
+ const getLastMigrationRun = (state) => {
202
+ const runs = state.runs ?? [];
203
+ if (runs.length === 0) return void 0;
204
+ return runs.map((run, index) => ({
205
+ run,
206
+ index
207
+ })).sort((left, right) => {
208
+ const appliedAtOrder = right.run.appliedAt.localeCompare(left.run.appliedAt);
209
+ if (appliedAtOrder !== 0) return appliedAtOrder;
210
+ return right.index - left.index;
211
+ })[0]?.run;
212
+ };
213
+ const getLatestAppliedMigrations = (state, steps) => {
214
+ return state.migrations.map((migration, index) => ({
215
+ migration,
216
+ index
217
+ })).sort((left, right) => {
218
+ const appliedAtOrder = right.migration.appliedAt.localeCompare(left.migration.appliedAt);
219
+ if (appliedAtOrder !== 0) return appliedAtOrder;
220
+ return right.index - left.index;
221
+ }).slice(0, Math.max(0, steps)).map((entry) => entry.migration);
222
+ };
223
+ /**
224
+ * Groups the applied migrations into batches, oldest batch first, each batch
225
+ * holding its migration ids in application order.
226
+ *
227
+ * A "batch" is one `migrate` run. When no runs were recorded (e.g. legacy state
228
+ * written before run tracking), each applied migration is treated as its own
229
+ * batch so `--step` still behaves predictably.
230
+ *
231
+ * @param state
232
+ * @returns
233
+ */
234
+ const resolveAppliedBatches = (state) => {
235
+ const runs = state.runs ?? [];
236
+ if (runs.length > 0) return runs.map((run, index) => ({
237
+ run,
238
+ index
239
+ })).sort((left, right) => {
240
+ const appliedAtOrder = left.run.appliedAt.localeCompare(right.run.appliedAt);
241
+ if (appliedAtOrder !== 0) return appliedAtOrder;
242
+ return left.index - right.index;
243
+ }).map((entry) => entry.run.migrationIds);
244
+ return state.migrations.map((migration) => [migration.id]);
245
+ };
246
+ /**
247
+ * Resolves the migrations belonging to the most recent `batches` batches, ordered
248
+ * for rollback — **reverse of the order they were applied** (most recent batch
249
+ * first, and within each batch the migration that ran `up()` last runs `down()`
250
+ * first), so a rollback exactly reverses how the migrations were applied.
251
+ *
252
+ * Defaults to a single batch (`migrate:rollback` with no `--step`); `--step=N`
253
+ * rolls back the last N batches.
254
+ *
255
+ * @param state
256
+ * @param batches
257
+ * @returns
258
+ */
259
+ const getLastBatchMigrations = (state, batches = 1) => {
260
+ if (batches < 1) return [];
261
+ const allBatches = resolveAppliedBatches(state);
262
+ const selected = allBatches.slice(Math.max(0, allBatches.length - batches));
263
+ const byId = new Map(state.migrations.map((migration) => [migration.id, migration]));
264
+ return selected.reverse().flatMap((migrationIds) => [...migrationIds].reverse()).map((id) => byId.get(id)).filter((migration) => Boolean(migration));
140
265
  };
141
266
 
142
267
  //#endregion
143
- //#region src/relationship/SetBasedEagerLoader.ts
268
+ //#region src/database/ForeignKeyBuilder.ts
144
269
  /**
145
- * Utility class responsible for performing set-based eager loading of relationships for
146
- * a collection of models.
270
+ * The ForeignKeyBuilder class provides a fluent interface for defining
271
+ * foreign key constraints in a migration. It allows you to specify
272
+ * the referenced table and column, as well as actions to take on
273
+ * delete and aliases for the relation.
147
274
  *
148
275
  * @author Legacy (3m1n3nc3)
149
- * @since 2.0.0-next.2
276
+ * @since 0.2.2
150
277
  */
151
- var SetBasedEagerLoader = class SetBasedEagerLoader {
152
- constructor(models, relations) {
153
- this.models = models;
154
- this.relations = relations;
278
+ var ForeignKeyBuilder = class {
279
+ constructor(foreignKey) {
280
+ this.foreignKey = foreignKey;
155
281
  }
156
282
  /**
157
- * Performs eager loading of all specified relationships for the set of models.
283
+ * Defines the referenced table and column for this foreign key constraint.
158
284
  *
285
+ * @param table
286
+ * @param column
159
287
  * @returns
160
288
  */
161
- async load() {
162
- if (this.models.length === 0) return;
163
- const relationTree = this.buildRelationTree(this.relations);
164
- await Promise.all(Array.from(relationTree.entries()).map(async ([name, node]) => {
165
- await this.loadRelationNode(name, node);
166
- }));
167
- }
168
- async loadRelationNode(name, node) {
169
- await this.loadRelation(name, node.constraint);
170
- if (node.children.size === 0) return;
171
- const relatedModels = this.collectLoadedRelationModels(name);
172
- if (relatedModels.length === 0) return;
173
- await new SetBasedEagerLoader(relatedModels, this.relationTreeToMap(node.children)).load();
289
+ references(table, column) {
290
+ this.foreignKey.referencesTable = table;
291
+ this.foreignKey.referencesColumn = column;
292
+ return this;
174
293
  }
175
294
  /**
176
- * Loads a specific relationship for the set of models based on the relationship name
177
- * and an optional constraint.
295
+ * Defines the action to take when a referenced record is deleted, such
296
+ * as "CASCADE", "SET NULL", or "RESTRICT".
178
297
  *
179
- * @param name The name of the relationship to load.
180
- * @param constraint An optional constraint to apply to the query.
181
- * @returns A promise that resolves when the relationship is loaded.
298
+ * @param action
299
+ * @returns
182
300
  */
183
- async loadRelation(name, constraint) {
184
- const resolver = this.resolveRelationResolver(name);
185
- if (!resolver) return;
186
- const metadata = resolver.call(this.models[0]).getMetadata();
187
- switch (metadata.type) {
188
- case "belongsTo":
189
- await this.loadBelongsTo(name, resolver, metadata, constraint);
190
- return;
191
- case "belongsToMany":
192
- await this.loadBelongsToMany(name, metadata, constraint);
193
- return;
194
- case "hasMany":
195
- await this.loadHasMany(name, metadata, constraint);
196
- return;
197
- case "hasOne":
198
- await this.loadHasOne(name, resolver, metadata, constraint);
199
- return;
200
- case "hasManyThrough":
201
- await this.loadHasManyThrough(name, metadata, constraint);
202
- return;
203
- case "hasOneThrough":
204
- await this.loadHasOneThrough(name, resolver, metadata, constraint);
205
- return;
206
- case "morphTo":
207
- await this.loadMorphTo(name, metadata, constraint);
208
- return;
209
- case "morphOne":
210
- await this.loadMorphOne(name, resolver, metadata, constraint);
211
- return;
212
- case "morphMany":
213
- await this.loadMorphMany(name, metadata, constraint);
214
- return;
215
- case "morphToMany":
216
- case "morphedByMany":
217
- await this.loadPolymorphicManyToMany(name, metadata, constraint);
218
- return;
219
- default: await this.loadIndividually(name, resolver, constraint);
220
- }
301
+ onDelete(action) {
302
+ this.foreignKey.onDelete = action;
303
+ return this;
221
304
  }
222
305
  /**
223
- * Resolves the relation resolver function for a given relationship name by inspecting
224
- * the first model in the set.
306
+ * Defines an alias for the relation represented by this foreign key, which
307
+ * can be used in the ORM for more intuitive access to related models.
225
308
  *
226
- * @param name The name of the relationship to resolve.
227
- * @returns The relation resolver function or null if not found.
309
+ * @param name
310
+ * @returns
228
311
  */
229
- resolveRelationResolver(name) {
230
- const resolver = this.models[0][name];
231
- if (typeof resolver !== "function") {
232
- const modelName = this.models[0].constructor?.name ?? "Model";
233
- throw new RelationResolutionException(`Relation [${name}] is not defined on the model.`, {
234
- operation: "eagerLoad",
235
- model: modelName,
236
- relation: name
237
- });
238
- }
239
- return resolver;
240
- }
241
- buildRelationTree(relations) {
242
- const tree = /* @__PURE__ */ new Map();
243
- Object.entries(relations).forEach(([path, constraint]) => {
244
- const segments = path.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
245
- if (segments.length === 0) return;
246
- let current = tree;
247
- segments.forEach((segment, index) => {
248
- const existing = current.get(segment) ?? {
249
- constraint: void 0,
250
- children: /* @__PURE__ */ new Map()
251
- };
252
- if (index === segments.length - 1 && constraint) existing.constraint = constraint;
253
- current.set(segment, existing);
254
- current = existing.children;
255
- });
256
- });
257
- return tree;
258
- }
259
- relationTreeToMap(tree, prefix = "") {
260
- return Array.from(tree.entries()).reduce((all, [name, node]) => {
261
- const path = prefix ? `${prefix}.${name}` : name;
262
- all[path] = node.constraint;
263
- Object.assign(all, this.relationTreeToMap(node.children, path));
264
- return all;
265
- }, {});
266
- }
267
- collectLoadedRelationModels(name) {
268
- return this.models.reduce((all, model) => {
269
- const loaded = model.getAttribute(name);
270
- if (loaded instanceof ArkormCollection) {
271
- loaded.all().forEach((item) => {
272
- if (this.isEagerLoadableModel(item)) all.push(item);
273
- });
274
- return all;
275
- }
276
- if (this.isEagerLoadableModel(loaded)) all.push(loaded);
277
- return all;
278
- }, []);
279
- }
280
- isEagerLoadableModel(value) {
281
- return typeof value === "object" && value !== null && typeof value.getAttribute === "function" && typeof value.setLoadedRelation === "function";
312
+ alias(name) {
313
+ this.foreignKey.relationAlias = name;
314
+ return this;
282
315
  }
283
316
  /**
284
- * Loads a "belongs to" relationship for the set of models.
317
+ * Defines an alias for the inverse relation represented by this foreign key.
285
318
  *
286
- * @param name The name of the relationship to load.
287
- * @param resolver The relation resolver function.
288
- * @param metadata The metadata for the relationship.
289
- * @param constraint An optional constraint to apply to the query.
290
- * @returns A promise that resolves when the relationship is loaded.
319
+ * @param name
320
+ * @returns
291
321
  */
292
- async loadBelongsTo(name, resolver, metadata, constraint) {
293
- const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.foreignKey));
294
- if (keys.length === 0) {
295
- this.models.forEach((model) => {
296
- model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
297
- });
298
- return;
299
- }
300
- let query = metadata.relatedModel.query().whereIn(metadata.ownerKey, keys);
301
- query = this.applyConstraint(query, constraint);
302
- const relatedModels = await this.getPartitionedModels(query, metadata.ownerKey, keys);
303
- const relatedByOwnerKey = /* @__PURE__ */ new Map();
304
- relatedModels.forEach((related) => {
305
- const value = this.readModelAttribute(related, metadata.ownerKey);
306
- if (value == null) return;
307
- const lookupKey = this.toLookupKey(value);
308
- if (!relatedByOwnerKey.has(lookupKey)) relatedByOwnerKey.set(lookupKey, related);
309
- });
310
- this.models.forEach((model) => {
311
- const foreignValue = model.getAttribute(metadata.foreignKey);
312
- const relationValue = foreignValue == null ? void 0 : relatedByOwnerKey.get(this.toLookupKey(foreignValue));
313
- model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
314
- });
322
+ inverseAlias(name) {
323
+ this.foreignKey.inverseRelationAlias = name;
324
+ return this;
315
325
  }
316
326
  /**
317
- * Loads a "has many" relationship for the set of models.
327
+ * Defines an alias for the foreign key field itself, which can be
328
+ * used in the ORM for more intuitive access to the foreign key value.
318
329
  *
319
- * @param name
320
- * @param metadata
321
- * @param constraint
330
+ * @param fieldName
322
331
  * @returns
323
332
  */
324
- async loadHasMany(name, metadata, constraint) {
325
- const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
326
- if (keys.length === 0) {
327
- this.models.forEach((model) => {
328
- model.setLoadedRelation(name, new ArkormCollection([]));
329
- });
330
- return;
331
- }
332
- let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
333
- query = this.applyConstraint(query, constraint);
334
- const relatedModels = await this.getPartitionedModels(query, metadata.foreignKey, keys);
335
- const relatedByForeignKey = /* @__PURE__ */ new Map();
336
- relatedModels.forEach((related) => {
337
- const value = this.readModelAttribute(related, metadata.foreignKey);
338
- if (value == null) return;
339
- const lookupKey = this.toLookupKey(value);
340
- const bucket = relatedByForeignKey.get(lookupKey) ?? [];
341
- bucket.push(related);
342
- relatedByForeignKey.set(lookupKey, bucket);
343
- });
344
- this.models.forEach((model) => {
345
- const localValue = model.getAttribute(metadata.localKey);
346
- const related = localValue == null ? [] : relatedByForeignKey.get(this.toLookupKey(localValue)) ?? [];
347
- model.setLoadedRelation(name, new ArkormCollection(related));
348
- });
333
+ as(fieldName) {
334
+ this.foreignKey.fieldAlias = fieldName;
335
+ return this;
336
+ }
337
+ };
338
+
339
+ //#endregion
340
+ //#region src/helpers/PrimaryKeyGenerationPlanner.ts
341
+ var PrimaryKeyGenerationPlanner = class {
342
+ static plan(column) {
343
+ if (!column.primary || column.default !== void 0) return void 0;
344
+ if (column.type === "uuid" || column.type === "string") return {
345
+ strategy: "uuid",
346
+ prismaDefault: "@default(uuid())",
347
+ databaseDefault: column.type === "uuid" ? "gen_random_uuid()" : "gen_random_uuid()::text",
348
+ runtimeFactory: "uuid"
349
+ };
350
+ }
351
+ static generate(generation) {
352
+ if (generation?.runtimeFactory === "uuid") return (0, node_crypto.randomUUID)();
353
+ }
354
+ };
355
+
356
+ //#endregion
357
+ //#region src/helpers/runtime-module-loader.ts
358
+ var RuntimeModuleLoader = class {
359
+ static async load(filePath, useDefault = false) {
360
+ const resolvedPath = (0, node_path.resolve)(filePath);
361
+ return await (0, jiti.createJiti)((0, node_url.pathToFileURL)(resolvedPath).href, {
362
+ interopDefault: false,
363
+ tsconfigPaths: true,
364
+ sourceMaps: true
365
+ }).import(resolvedPath, useDefault ? { default: true } : {});
349
366
  }
350
367
  /**
351
- * Loads a "belongs to many" relationship for the set of models.
368
+ * Load many modules through a single shared jiti context, retrying files
369
+ * that fail until no further progress is made.
352
370
  *
353
- * @param name
354
- * @param metadata
355
- * @param constraint
371
+ * A shared module cache resolves circular imports consistently: a file that
372
+ * fails because one of its dependencies was not yet cached (for example a
373
+ * trait whose module imports a model which imports the class that uses the
374
+ * trait) succeeds on a later pass, once a sibling load has populated that
375
+ * dependency. A fresh per-file context cannot do this, so the same set of
376
+ * files can load or fail depending purely on which file is the entry point.
377
+ *
378
+ * Each returned entry carries either the loaded module or the last error,
379
+ * so callers can surface genuine failures instead of silently dropping them.
380
+ *
381
+ * @param filePaths
382
+ * @param useDefault
356
383
  * @returns
357
384
  */
358
- async loadBelongsToMany(name, metadata, constraint) {
359
- const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.parentKey));
360
- if (parentKeys.length === 0) {
361
- this.models.forEach((model) => {
362
- model.setLoadedRelation(name, new ArkormCollection([]));
363
- });
364
- return;
365
- }
366
- const pivotRows = await this.createRelationTableLoader().selectRows({
367
- table: metadata.throughTable,
368
- where: this.buildBelongsToManyPivotWhere(metadata, parentKeys),
369
- columns: this.getBelongsToManyPivotColumns(metadata).map((column) => ({ column }))
385
+ static async loadAll(filePaths, useDefault = false) {
386
+ const jiti$1 = (0, jiti.createJiti)(`${(0, node_url.pathToFileURL)((0, node_path.resolve)(".")).href}/`, {
387
+ interopDefault: false,
388
+ tsconfigPaths: true,
389
+ sourceMaps: true
370
390
  });
371
- const relatedIds = this.collectUniqueRowValues(pivotRows, metadata.relatedPivotKey);
372
- if (relatedIds.length === 0) {
373
- this.models.forEach((model) => {
374
- model.setLoadedRelation(name, new ArkormCollection([]));
375
- });
376
- return;
377
- }
378
- let query = metadata.relatedModel.query().whereIn(metadata.relatedKey, relatedIds);
379
- query = this.applyConstraint(query, constraint);
380
- if (query.hasEagerLoadPagination() && parentKeys.length > 1 && await this.loadLimitedBelongsToMany(name, metadata, query, parentKeys, pivotRows)) return;
381
- const relatedModels = (await query.get()).all();
382
- const relatedByKey = /* @__PURE__ */ new Map();
383
- relatedModels.forEach((related) => {
384
- const relatedValue = this.readModelAttribute(related, metadata.relatedKey);
385
- if (relatedValue == null) return;
386
- relatedByKey.set(this.toLookupKey(relatedValue), related);
387
- });
388
- const relatedKeysByParent = /* @__PURE__ */ new Map();
389
- const pivotByParentAndRelated = /* @__PURE__ */ new Map();
390
- pivotRows.forEach((row) => {
391
- const parentValue = row[metadata.foreignPivotKey];
392
- const relatedValue = row[metadata.relatedPivotKey];
393
- if (parentValue == null || relatedValue == null) return;
394
- const bucket = relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? [];
395
- bucket.push(relatedValue);
396
- relatedKeysByParent.set(this.toLookupKey(parentValue), bucket);
397
- pivotByParentAndRelated.set(`${this.toLookupKey(parentValue)}:${this.toLookupKey(relatedValue)}`, row);
398
- });
399
- this.models.forEach((model) => {
400
- const parentValue = model.getAttribute(metadata.parentKey);
401
- const related = (parentValue == null ? [] : relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? []).reduce((all, relatedValue) => {
402
- const candidate = relatedByKey.get(this.toLookupKey(relatedValue));
403
- if (candidate) all.push(this.attachBelongsToManyPivot(metadata, candidate, pivotByParentAndRelated.get(`${this.toLookupKey(parentValue)}:${this.toLookupKey(relatedValue)}`)));
404
- return all;
405
- }, []);
406
- model.setLoadedRelation(name, new ArkormCollection(related));
407
- });
408
- }
409
- async loadLimitedBelongsToMany(name, metadata, query, parentKeys, pivotRows) {
410
- if (metadata.pivotWhere) {
411
- await this.loadLimitedBelongsToManyIndividually(name, metadata, query, pivotRows);
412
- return true;
413
- }
414
- const pivotColumns = this.getBelongsToManyPivotColumns(metadata);
415
- const pivotAliases = new Map(pivotColumns.map((column, index) => [column, `__arkorm_pivot_${index}`]));
416
- const parentAlias = pivotAliases.get(metadata.foreignPivotKey);
417
- const throughTable = metadata.throughTable;
418
- const relatedTable = metadata.relatedModel.getTable();
419
- const rows = await query.clone().innerJoin(throughTable, `${relatedTable}.${metadata.relatedModel.getColumnName(metadata.relatedKey)}`, "=", `${throughTable}.${metadata.relatedPivotKey}`).whereIn(`${throughTable}.${metadata.foreignPivotKey}`, parentKeys).addSelect({ ...Object.fromEntries([...pivotAliases].map(([column, alias]) => [`${throughTable}.${column}`, alias])) }).getRowsForEagerLoad([`${throughTable}.${metadata.foreignPivotKey}`]);
420
- if (!rows) {
421
- await this.loadLimitedBelongsToManyIndividually(name, metadata, query, pivotRows);
422
- return true;
423
- }
424
- const selectedIds = this.collectUniqueRowValues(rows, metadata.relatedKey);
425
- const relatedModels = (await query.clone().withoutPagination().whereIn(metadata.relatedKey, selectedIds).get()).all();
426
- const relatedByKey = /* @__PURE__ */ new Map();
427
- relatedModels.forEach((related) => {
428
- const key = this.readModelAttribute(related, metadata.relatedKey);
429
- if (key != null) relatedByKey.set(this.toLookupKey(key), related);
430
- });
431
- const relatedByParent = /* @__PURE__ */ new Map();
432
- rows.forEach((row) => {
433
- const parentKey = row[parentAlias];
434
- const relatedKey = row[metadata.relatedKey];
435
- if (parentKey == null || relatedKey == null) return;
436
- const related = relatedByKey.get(this.toLookupKey(relatedKey));
437
- if (!related) return;
438
- const pivot = Object.fromEntries([...pivotAliases].map(([column, alias]) => [column, row[alias]]));
439
- const bucket = relatedByParent.get(this.toLookupKey(parentKey)) ?? [];
440
- bucket.push(this.attachBelongsToManyPivot(metadata, related, pivot));
441
- relatedByParent.set(this.toLookupKey(parentKey), bucket);
442
- });
443
- this.models.forEach((model) => {
444
- const parentKey = model.getAttribute(metadata.parentKey);
445
- const related = parentKey == null ? [] : relatedByParent.get(this.toLookupKey(parentKey)) ?? [];
446
- model.setLoadedRelation(name, new ArkormCollection(related));
447
- });
448
- return true;
449
- }
450
- async loadLimitedBelongsToManyIndividually(name, metadata, query, pivotRows) {
451
- const rowsByParent = /* @__PURE__ */ new Map();
452
- pivotRows.forEach((row) => {
453
- const parentKey = row[metadata.foreignPivotKey];
454
- if (parentKey == null) return;
455
- const bucket = rowsByParent.get(this.toLookupKey(parentKey)) ?? [];
456
- bucket.push(row);
457
- rowsByParent.set(this.toLookupKey(parentKey), bucket);
458
- });
459
- await Promise.all(this.models.map(async (model) => {
460
- const parentKey = model.getAttribute(metadata.parentKey);
461
- const rows = parentKey == null ? [] : rowsByParent.get(this.toLookupKey(parentKey)) ?? [];
462
- const relatedIds = this.collectUniqueRowValues(rows, metadata.relatedPivotKey);
463
- const related = relatedIds.length ? (await query.clone().whereIn(metadata.relatedKey, relatedIds).get()).all() : [];
464
- const pivotByRelated = new Map(rows.map((row) => [this.toLookupKey(row[metadata.relatedPivotKey]), row]));
465
- model.setLoadedRelation(name, new ArkormCollection(related.map((entry) => this.attachBelongsToManyPivot(metadata, entry, pivotByRelated.get(this.toLookupKey(this.readModelAttribute(entry, metadata.relatedKey)))))));
466
- }));
467
- }
468
- buildBelongsToManyPivotWhere(metadata, parentKeys) {
469
- const baseCondition = {
470
- type: "comparison",
471
- column: metadata.foreignPivotKey,
472
- operator: "in",
473
- value: parentKeys
474
- };
475
- if (!metadata.pivotWhere) return baseCondition;
476
- return {
477
- type: "group",
478
- operator: "and",
479
- conditions: [baseCondition, metadata.pivotWhere]
480
- };
481
- }
482
- getBelongsToManyPivotColumns(metadata) {
483
- return [
484
- metadata.foreignPivotKey,
485
- metadata.relatedPivotKey,
486
- ...metadata.pivotColumns ?? []
487
- ].filter((column, index, all) => all.indexOf(column) === index);
488
- }
489
- shouldAttachBelongsToManyPivot(metadata) {
490
- return Boolean(metadata.pivotModel) || Boolean(metadata.pivotCreatedAtColumn) || Boolean(metadata.pivotUpdatedAtColumn) || (metadata.pivotColumns?.length ?? 0) > 0 || Boolean(metadata.pivotAccessor);
491
- }
492
- createBelongsToManyPivotRecord(metadata, row) {
493
- const attributes = this.getBelongsToManyPivotColumns(metadata).reduce((all, column) => {
494
- all[column] = row[column];
495
- return all;
496
- }, {});
497
- if (!metadata.pivotModel) return attributes;
498
- if (typeof metadata.pivotModel.hydrate === "function") return metadata.pivotModel.hydrate(attributes);
499
- return new metadata.pivotModel(attributes);
500
- }
501
- attachBelongsToManyPivot(metadata, related, row) {
502
- if (!row || !this.shouldAttachBelongsToManyPivot(metadata)) return related;
503
- const rawReader = related;
504
- if (typeof rawReader.getRawAttributes !== "function" || typeof rawReader.setAttribute !== "function") return related;
505
- const cloned = metadata.relatedModel.hydrate(rawReader.getRawAttributes());
506
- cloned.setAttribute(metadata.pivotAccessor ?? "pivot", this.createBelongsToManyPivotRecord(metadata, row));
507
- return cloned;
508
- }
509
- /**
510
- * Loads a "belongs to many" relationship for the set of models.
511
- *
512
- * @param name
513
- * @param resolver
514
- * @param metadata
515
- * @param constraint
516
- * @returns
517
- */
518
- async loadHasOne(name, resolver, metadata, constraint) {
519
- const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
520
- if (keys.length === 0) {
521
- this.models.forEach((model) => {
522
- model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
523
- });
524
- return;
525
- }
526
- let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
527
- query = this.applyConstraint(query, constraint);
528
- const relatedModels = await this.getPartitionedModels(query, metadata.foreignKey, keys);
529
- const relatedByForeignKey = /* @__PURE__ */ new Map();
530
- relatedModels.forEach((related) => {
531
- const value = this.readModelAttribute(related, metadata.foreignKey);
532
- if (value == null) return;
533
- const lookupKey = this.toLookupKey(value);
534
- if (!relatedByForeignKey.has(lookupKey)) relatedByForeignKey.set(lookupKey, related);
535
- });
536
- this.models.forEach((model) => {
537
- const localValue = model.getAttribute(metadata.localKey);
538
- const relationValue = localValue == null ? void 0 : relatedByForeignKey.get(this.toLookupKey(localValue));
539
- model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
540
- });
541
- }
542
- /**
543
- * Loads a "has many through" relationship for the set of models.
544
- *
545
- * @param name
546
- * @param metadata
547
- * @param constraint
548
- * @returns
549
- */
550
- async loadHasManyThrough(name, metadata, constraint) {
551
- const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
552
- if (parentKeys.length === 0) {
553
- this.models.forEach((model) => {
554
- model.setLoadedRelation(name, new ArkormCollection([]));
555
- });
556
- return;
557
- }
558
- const throughRows = await this.createRelationTableLoader().selectRows({
559
- table: metadata.throughTable,
560
- where: {
561
- type: "comparison",
562
- column: metadata.firstKey,
563
- operator: "in",
564
- value: parentKeys
391
+ const results = /* @__PURE__ */ new Map();
392
+ let pending = filePaths.map((file) => (0, node_path.resolve)(file));
393
+ while (pending.length > 0) {
394
+ const failed = [];
395
+ for (const file of pending) try {
396
+ const module = await jiti$1.import(file, useDefault ? { default: true } : {});
397
+ results.set(file, {
398
+ file,
399
+ module
400
+ });
401
+ } catch (error) {
402
+ failed.push(file);
403
+ results.set(file, {
404
+ file,
405
+ module: null,
406
+ error
407
+ });
565
408
  }
566
- });
567
- const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
568
- if (intermediateKeys.length === 0) {
569
- this.models.forEach((model) => {
570
- model.setLoadedRelation(name, new ArkormCollection([]));
571
- });
572
- return;
573
- }
574
- let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
575
- query = this.applyConstraint(query, constraint);
576
- if (query.hasEagerLoadPagination() && parentKeys.length > 1) {
577
- const limited = await this.loadLimitedThrough(metadata, query, parentKeys, throughRows);
578
- this.models.forEach((model) => {
579
- const parentKey = model.getAttribute(metadata.localKey);
580
- const related = parentKey == null ? [] : limited.get(this.toLookupKey(parentKey)) ?? [];
581
- model.setLoadedRelation(name, new ArkormCollection(related));
582
- });
583
- return;
409
+ if (failed.length === pending.length) break;
410
+ pending = failed;
584
411
  }
585
- const relatedModels = (await query.get()).all();
586
- const relatedByIntermediate = /* @__PURE__ */ new Map();
587
- relatedModels.forEach((related) => {
588
- const relatedValue = this.readModelAttribute(related, metadata.secondKey);
589
- if (relatedValue == null) return;
590
- const bucket = relatedByIntermediate.get(this.toLookupKey(relatedValue)) ?? [];
591
- bucket.push(related);
592
- relatedByIntermediate.set(this.toLookupKey(relatedValue), bucket);
593
- });
594
- const intermediateByParent = /* @__PURE__ */ new Map();
595
- throughRows.forEach((row) => {
596
- const parentValue = row[metadata.firstKey];
597
- const intermediateValue = row[metadata.secondLocalKey];
598
- if (parentValue == null || intermediateValue == null) return;
599
- const bucket = intermediateByParent.get(this.toLookupKey(parentValue)) ?? [];
600
- bucket.push(intermediateValue);
601
- intermediateByParent.set(this.toLookupKey(parentValue), bucket);
602
- });
603
- this.models.forEach((model) => {
604
- const parentValue = model.getAttribute(metadata.localKey);
605
- const related = (parentValue == null ? [] : intermediateByParent.get(this.toLookupKey(parentValue)) ?? []).flatMap((intermediateValue) => relatedByIntermediate.get(this.toLookupKey(intermediateValue)) ?? []);
606
- model.setLoadedRelation(name, new ArkormCollection(related));
412
+ return filePaths.map((file) => results.get((0, node_path.resolve)(file)));
413
+ }
414
+ };
415
+
416
+ //#endregion
417
+ //#region src/Exceptions/UnsupportedAdapterFeatureException.ts
418
+ var UnsupportedAdapterFeatureException = class extends ArkormException {
419
+ constructor(message, context = {}) {
420
+ super(message, {
421
+ code: "UNSUPPORTED_ADAPTER_FEATURE",
422
+ ...context
607
423
  });
424
+ this.name = "UnsupportedAdapterFeatureException";
608
425
  }
609
- /**
610
- * Loads a "has one through" relationship for the set of models.
611
- *
612
- * @param name
613
- * @param resolver
614
- * @param metadata
615
- * @param constraint
616
- * @returns
617
- */
618
- async loadHasOneThrough(name, resolver, metadata, constraint) {
619
- const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
620
- if (parentKeys.length === 0) {
621
- this.models.forEach((model) => {
622
- model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
623
- });
624
- return;
625
- }
626
- const throughRows = await this.createRelationTableLoader().selectRows({
627
- table: metadata.throughTable,
628
- where: {
629
- type: "comparison",
630
- column: metadata.firstKey,
631
- operator: "in",
632
- value: parentKeys
633
- }
634
- });
635
- const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
636
- if (intermediateKeys.length === 0) {
637
- this.models.forEach((model) => {
638
- model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
639
- });
640
- return;
641
- }
642
- let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
643
- query = this.applyConstraint(query, constraint);
644
- if (query.hasEagerLoadPagination() && parentKeys.length > 1) {
645
- const limited = await this.loadLimitedThrough(metadata, query, parentKeys, throughRows);
646
- this.models.forEach((model) => {
647
- const parentKey = model.getAttribute(metadata.localKey);
648
- const related = parentKey == null ? void 0 : limited.get(this.toLookupKey(parentKey))?.[0];
649
- model.setLoadedRelation(name, related ?? this.resolveSingleDefault(resolver, model));
650
- });
651
- return;
652
- }
653
- const relatedModels = (await query.get()).all();
654
- const relatedByIntermediate = /* @__PURE__ */ new Map();
655
- relatedModels.forEach((related) => {
656
- const relatedValue = this.readModelAttribute(related, metadata.secondKey);
657
- if (relatedValue == null) return;
658
- const lookupKey = this.toLookupKey(relatedValue);
659
- if (!relatedByIntermediate.has(lookupKey)) relatedByIntermediate.set(lookupKey, related);
660
- });
661
- const intermediateByParent = /* @__PURE__ */ new Map();
662
- throughRows.forEach((row) => {
663
- const parentValue = row[metadata.firstKey];
664
- const intermediateValue = row[metadata.secondLocalKey];
665
- if (parentValue == null || intermediateValue == null) return;
666
- const lookupKey = this.toLookupKey(parentValue);
667
- if (!intermediateByParent.has(lookupKey)) intermediateByParent.set(lookupKey, intermediateValue);
668
- });
669
- this.models.forEach((model) => {
670
- const parentValue = model.getAttribute(metadata.localKey);
671
- const intermediateValue = parentValue == null ? void 0 : intermediateByParent.get(this.toLookupKey(parentValue));
672
- const relationValue = intermediateValue == null ? void 0 : relatedByIntermediate.get(this.toLookupKey(intermediateValue));
673
- model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
674
- });
675
- }
676
- async loadLimitedThrough(metadata, query, parentKeys, throughRows) {
677
- const parentAlias = "__arkorm_parent_key";
678
- const throughTable = metadata.throughTable;
679
- const relatedTable = metadata.relatedModel.getTable();
680
- const rows = await query.clone().innerJoin(throughTable, `${relatedTable}.${metadata.relatedModel.getColumnName(metadata.secondKey)}`, "=", `${throughTable}.${metadata.secondLocalKey}`).whereIn(`${throughTable}.${metadata.firstKey}`, parentKeys).addSelect({ [`${throughTable}.${metadata.firstKey}`]: parentAlias }).getRowsForEagerLoad([`${throughTable}.${metadata.firstKey}`]);
681
- if (!rows) return await this.loadLimitedThroughIndividually(metadata, query, throughRows);
682
- const relatedKey = metadata.relatedModel.getPrimaryKey();
683
- const selectedIds = this.collectUniqueRowValues(rows, relatedKey);
684
- const relatedModels = (await query.clone().withoutPagination().whereIn(relatedKey, selectedIds).get()).all();
685
- const relatedByKey = /* @__PURE__ */ new Map();
686
- relatedModels.forEach((related) => {
687
- const key = this.readModelAttribute(related, relatedKey);
688
- if (key != null) relatedByKey.set(this.toLookupKey(key), related);
689
- });
690
- return rows.reduce((all, row) => {
691
- const parentKey = row[parentAlias];
692
- const key = row[relatedKey];
693
- if (parentKey == null || key == null) return all;
694
- const related = relatedByKey.get(this.toLookupKey(key));
695
- if (!related) return all;
696
- const lookup = this.toLookupKey(parentKey);
697
- const bucket = all.get(lookup) ?? [];
698
- bucket.push(related);
699
- all.set(lookup, bucket);
700
- return all;
701
- }, /* @__PURE__ */ new Map());
702
- }
703
- async loadLimitedThroughIndividually(metadata, query, throughRows) {
704
- const intermediateByParent = /* @__PURE__ */ new Map();
705
- throughRows.forEach((row) => {
706
- const parentKey = row[metadata.firstKey];
707
- const intermediateKey = row[metadata.secondLocalKey];
708
- if (parentKey == null || intermediateKey == null) return;
709
- const lookup = this.toLookupKey(parentKey);
710
- const bucket = intermediateByParent.get(lookup) ?? [];
711
- bucket.push(intermediateKey);
712
- intermediateByParent.set(lookup, bucket);
713
- });
714
- const entries = await Promise.all([...intermediateByParent].map(async ([parentKey, intermediateKeys]) => {
715
- return [parentKey, (await query.clone().whereIn(metadata.secondKey, intermediateKeys).get()).all()];
716
- }));
717
- return new Map(entries);
718
- }
719
- /**
720
- * Loads an inverse polymorphic ("morph to") relationship for the set of
721
- * models. Parents are grouped by their morph type, so each distinct type
722
- * runs a single batched query instead of one query per parent row.
723
- *
724
- * @param name
725
- * @param metadata
726
- * @param constraint
727
- * @returns
728
- */
729
- async loadMorphTo(name, metadata, constraint) {
730
- const idsByType = /* @__PURE__ */ new Map();
731
- const seenByType = /* @__PURE__ */ new Map();
732
- this.models.forEach((model) => {
733
- const morphType = this.readModelAttribute(model, metadata.morphTypeColumn);
734
- const morphId = this.readModelAttribute(model, metadata.morphIdColumn);
735
- if (typeof morphType !== "string" || morphType.length === 0 || morphId == null) return;
736
- const ids = idsByType.get(morphType) ?? [];
737
- const seen = seenByType.get(morphType) ?? /* @__PURE__ */ new Set();
738
- const lookupKey = this.toLookupKey(morphId);
739
- if (!seen.has(lookupKey)) {
740
- seen.add(lookupKey);
741
- ids.push(morphId);
742
- }
743
- idsByType.set(morphType, ids);
744
- seenByType.set(morphType, seen);
745
- });
746
- const relatedByTypeAndKey = /* @__PURE__ */ new Map();
747
- await Promise.all(Array.from(idsByType.entries()).map(async ([morphType, ids]) => {
748
- const relatedModel = metadata.resolveModel(morphType);
749
- const ownerKey = metadata.ownerKey ?? relatedModel.getPrimaryKey();
750
- let query = relatedModel.query().whereIn(ownerKey, ids);
751
- query = this.applyConstraint(query, constraint);
752
- (await this.getPartitionedModels(query, ownerKey, ids)).forEach((related) => {
753
- const value = this.readModelAttribute(related, ownerKey);
754
- if (value == null) return;
755
- relatedByTypeAndKey.set(`${morphType}:${this.toLookupKey(value)}`, related);
756
- });
757
- }));
758
- this.models.forEach((model) => {
759
- const morphType = this.readModelAttribute(model, metadata.morphTypeColumn);
760
- const morphId = this.readModelAttribute(model, metadata.morphIdColumn);
761
- const relationValue = typeof morphType !== "string" || morphId == null ? null : relatedByTypeAndKey.get(`${morphType}:${this.toLookupKey(morphId)}`) ?? null;
762
- model.setLoadedRelation(name, relationValue);
763
- });
764
- }
765
- /**
766
- * Loads a polymorphic one-to-one ("morph one") relationship for the set of
767
- * models. See {@link loadMorphChildren} for the batching strategy.
768
- *
769
- * @param name
770
- * @param resolver
771
- * @param metadata
772
- * @param constraint
773
- */
774
- async loadMorphOne(name, resolver, metadata, constraint) {
775
- const relatedByKey = await this.loadMorphChildren(metadata, constraint);
776
- this.models.forEach((model) => {
777
- const bucket = relatedByKey.get(this.morphParentKey(model, metadata.localKey));
778
- model.setLoadedRelation(name, bucket?.[0] ?? this.resolveSingleDefault(resolver, model));
779
- });
780
- }
781
- /**
782
- * Loads a polymorphic one-to-many ("morph many") relationship for the set of
783
- * models. See {@link loadMorphChildren} for the batching strategy.
784
- *
785
- * @param name
786
- * @param metadata
787
- * @param constraint
788
- */
789
- async loadMorphMany(name, metadata, constraint) {
790
- const relatedByKey = await this.loadMorphChildren(metadata, constraint);
791
- this.models.forEach((model) => {
792
- const bucket = relatedByKey.get(this.morphParentKey(model, metadata.localKey)) ?? [];
793
- model.setLoadedRelation(name, new ArkormCollection(bucket));
794
- });
795
- }
796
- async loadPolymorphicManyToMany(name, metadata, constraint) {
797
- const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.parentKey));
798
- if (parentKeys.length === 0) {
799
- this.models.forEach((model) => model.setLoadedRelation(name, new ArkormCollection([])));
800
- return;
801
- }
802
- const parentPivotKey = metadata.type === "morphToMany" ? metadata.morphIdColumn : metadata.foreignPivotKey;
803
- const morphType = metadata.type === "morphToMany" ? this.morphTypeOf(this.models[0]) : metadata.relatedModel.name;
804
- const pivotRows = await this.createRelationTableLoader().selectRows({
805
- table: metadata.throughTable,
806
- where: {
807
- type: "group",
808
- operator: "and",
809
- conditions: [{
810
- type: "comparison",
811
- column: parentPivotKey,
812
- operator: "in",
813
- value: parentKeys
814
- }, {
815
- type: "comparison",
816
- column: metadata.morphTypeColumn,
817
- operator: "=",
818
- value: morphType
819
- }]
820
- },
821
- columns: [parentPivotKey, metadata.relatedPivotKey].map((column) => ({ column }))
822
- });
823
- const relatedIds = this.collectUniqueRowValues(pivotRows, metadata.relatedPivotKey);
824
- if (relatedIds.length === 0) {
825
- this.models.forEach((model) => model.setLoadedRelation(name, new ArkormCollection([])));
826
- return;
827
- }
828
- let query = metadata.relatedModel.query().whereIn(metadata.relatedKey, relatedIds);
829
- query = this.applyConstraint(query, constraint);
830
- let pairs = pivotRows.map((row) => ({
831
- parent: row[parentPivotKey],
832
- related: row[metadata.relatedPivotKey]
833
- }));
834
- let relatedModels;
835
- if (query.hasEagerLoadPagination() && parentKeys.length > 1) {
836
- const parentAlias = "__arkorm_parent_key";
837
- const throughTable = metadata.throughTable;
838
- const relatedTable = metadata.relatedModel.getTable();
839
- const rows = await query.clone().innerJoin(throughTable, `${relatedTable}.${metadata.relatedModel.getColumnName(metadata.relatedKey)}`, "=", `${throughTable}.${metadata.relatedPivotKey}`).whereIn(`${throughTable}.${parentPivotKey}`, parentKeys).where({ [`${throughTable}.${metadata.morphTypeColumn}`]: morphType }).addSelect({ [`${throughTable}.${parentPivotKey}`]: parentAlias }).getRowsForEagerLoad([`${throughTable}.${parentPivotKey}`]);
840
- if (rows) {
841
- pairs = rows.map((row) => ({
842
- parent: row[parentAlias],
843
- related: row[metadata.relatedKey]
844
- }));
845
- const selectedIds = this.collectUniqueRowValues(rows, metadata.relatedKey);
846
- relatedModels = (await query.clone().withoutPagination().whereIn(metadata.relatedKey, selectedIds).get()).all();
847
- } else {
848
- await this.loadPolymorphicManyToManyIndividually(name, metadata, query, parentPivotKey, pivotRows);
849
- return;
850
- }
851
- } else relatedModels = (await query.get()).all();
852
- const relatedByKey = /* @__PURE__ */ new Map();
853
- relatedModels.forEach((related) => {
854
- const key = this.readModelAttribute(related, metadata.relatedKey);
855
- if (key != null) relatedByKey.set(this.toLookupKey(key), related);
856
- });
857
- const relatedByParent = /* @__PURE__ */ new Map();
858
- pairs.forEach(({ parent, related }) => {
859
- if (parent == null || related == null) return;
860
- const model = relatedByKey.get(this.toLookupKey(related));
861
- if (!model) return;
862
- const lookup = this.toLookupKey(parent);
863
- const bucket = relatedByParent.get(lookup) ?? [];
864
- bucket.push(model);
865
- relatedByParent.set(lookup, bucket);
866
- });
867
- this.models.forEach((model) => {
868
- const parentKey = model.getAttribute(metadata.parentKey);
869
- const related = parentKey == null ? [] : relatedByParent.get(this.toLookupKey(parentKey)) ?? [];
870
- model.setLoadedRelation(name, new ArkormCollection(related));
871
- });
872
- }
873
- async loadPolymorphicManyToManyIndividually(name, metadata, query, parentPivotKey, pivotRows) {
874
- await Promise.all(this.models.map(async (model) => {
875
- const parentKey = model.getAttribute(metadata.parentKey);
876
- const ids = pivotRows.filter((row) => this.toLookupKey(row[parentPivotKey]) === this.toLookupKey(parentKey)).map((row) => row[metadata.relatedPivotKey]);
877
- const related = ids.length ? (await query.clone().whereIn(metadata.relatedKey, ids).get()).all() : [];
878
- model.setLoadedRelation(name, new ArkormCollection(related));
879
- }));
880
- }
881
- /**
882
- * Batch-loads the children for a morphOne/morphMany relationship. Parents are
883
- * grouped by their own morph type (constructor name), so each distinct type
884
- * runs a single query constrained by `idColumn IN (...) AND typeColumn = type`
885
- * instead of one query per parent row. This also supports a heterogeneous
886
- * parent set (e.g. the result of a nested load after a morphTo).
887
- *
888
- * @param metadata
889
- * @param constraint
890
- * @returns A map of `"{type}:{localKey}" -> related[]`.
891
- */
892
- async loadMorphChildren(metadata, constraint) {
893
- const keysByType = /* @__PURE__ */ new Map();
894
- const seenByType = /* @__PURE__ */ new Map();
895
- this.models.forEach((model) => {
896
- const morphType = this.morphTypeOf(model);
897
- const localValue = model.getAttribute(metadata.localKey);
898
- if (morphType.length === 0 || localValue == null) return;
899
- const keys = keysByType.get(morphType) ?? [];
900
- const seen = seenByType.get(morphType) ?? /* @__PURE__ */ new Set();
901
- const lookupKey = this.toLookupKey(localValue);
902
- if (!seen.has(lookupKey)) {
903
- seen.add(lookupKey);
904
- keys.push(localValue);
905
- }
906
- keysByType.set(morphType, keys);
907
- seenByType.set(morphType, seen);
908
- });
909
- const relatedByKey = /* @__PURE__ */ new Map();
910
- await Promise.all(Array.from(keysByType.entries()).map(async ([morphType, keys]) => {
911
- let query = metadata.relatedModel.query().whereIn(metadata.morphIdColumn, keys).where({ [metadata.morphTypeColumn]: morphType });
912
- query = this.applyConstraint(query, constraint);
913
- (await this.getPartitionedModels(query, metadata.morphIdColumn, keys)).forEach((related) => {
914
- const value = this.readModelAttribute(related, metadata.morphIdColumn);
915
- if (value == null) return;
916
- const key = `${morphType}:${this.toLookupKey(value)}`;
917
- const bucket = relatedByKey.get(key) ?? [];
918
- bucket.push(related);
919
- relatedByKey.set(key, bucket);
920
- });
921
- }));
922
- return relatedByKey;
923
- }
924
- morphParentKey(model, localKey) {
925
- return `${this.morphTypeOf(model)}:${this.toLookupKey(model.getAttribute(localKey))}`;
926
- }
927
- morphTypeOf(model) {
928
- return model.constructor?.name ?? "";
929
- }
930
- /**
931
- * Fallback method to load relationships individually for each model when the
932
- * relationship type is not supported for set-based loading.
933
- *
934
- * @param name
935
- * @param resolver
936
- * @param constraint
937
- */
938
- async loadIndividually(name, resolver, constraint) {
939
- await Promise.all(this.models.map(async (model) => {
940
- const relation = resolver.call(model);
941
- if (constraint) relation.constrain(constraint);
942
- model.setLoadedRelation(name, await relation.getResults());
943
- }));
944
- }
945
- /**
946
- * Applies an eager load constraint to a query if provided.
947
- *
948
- * @param query
949
- * @param constraint
950
- * @returns
951
- */
952
- applyConstraint(query, constraint) {
953
- if (!constraint) return query;
954
- return constraint(query) ?? query;
955
- }
956
- async getPartitionedModels(query, partitionColumn, partitionValues) {
957
- if (partitionValues.length <= 1) return (await query.get()).all();
958
- const partitioned = await query.getForEagerLoad([partitionColumn]);
959
- if (partitioned) return partitioned.all();
960
- return (await Promise.all(partitionValues.map(async (value) => {
961
- return (await query.clone().where({ [partitionColumn]: value }).get()).all();
962
- }))).flat();
963
- }
964
- /**
965
- * Collects unique values from the set of models based on a resolver function, which
966
- * is used to extract the value from each model.
967
- *
968
- * @param resolve A function that takes a model and returns the value to be collected.
969
- * @returns An array of unique values.
970
- */
971
- collectUniqueKeys(resolve) {
972
- const seen = /* @__PURE__ */ new Set();
973
- const values = [];
974
- this.models.forEach((model) => {
975
- const value = resolve(model);
976
- if (value == null) return;
977
- const lookupKey = this.toLookupKey(value);
978
- if (seen.has(lookupKey)) return;
979
- seen.add(lookupKey);
980
- values.push(value);
981
- });
982
- return values;
983
- }
984
- /**
985
- * Collects unique values from an array of database rows based on a specified key, which
986
- * is used to extract the value from each row.
987
- *
988
- * @param rows An array of database rows.
989
- * @param key The key to extract values from each row.
990
- * @returns An array of unique values.
991
- */
992
- collectUniqueRowValues(rows, key) {
993
- const seen = /* @__PURE__ */ new Set();
994
- const values = [];
995
- rows.forEach((row) => {
996
- const value = row[key];
997
- if (value == null) return;
998
- const lookupKey = this.toLookupKey(value);
999
- if (seen.has(lookupKey)) return;
1000
- seen.add(lookupKey);
1001
- values.push(value);
1002
- });
1003
- return values;
1004
- }
1005
- /**
1006
- * Loads a "belongs to many" relationship for the set of models.
1007
- *
1008
- * @returns
1009
- */
1010
- createRelationTableLoader() {
1011
- return new RelationTableLoader(this.resolveAdapter());
1012
- }
1013
- /**
1014
- * Loads a "belongs to many" relationship for the set of models.
1015
- *
1016
- * @returns
1017
- */
1018
- resolveAdapter() {
1019
- const adapter = this.models[0].constructor.getAdapter?.();
1020
- if (!adapter) throw new Error("Set-based eager loading requires a configured adapter.");
1021
- return adapter;
1022
- }
1023
- /**
1024
- * Reads an attribute value from a model using the getAttribute method, which is used
1025
- * to access model attributes in a way that is compatible with Arkorm's internal model structure.
1026
- *
1027
- * @param model The model to read the attribute from.
1028
- * @param key The name of the attribute to read.
1029
- * @returns
1030
- */
1031
- readModelAttribute(model, key) {
1032
- const readable = model;
1033
- const value = readable.getAttribute?.(key);
1034
- if (value !== void 0) return value;
1035
- try {
1036
- const attribute = Object.entries(readable.constructor?.getColumnMap?.() ?? {}).find(([, column]) => column === key)?.[0];
1037
- return attribute ? readable.getAttribute?.(attribute) : value;
1038
- } catch {
1039
- return value;
1040
- }
1041
- }
1042
- /**
1043
- * Resolves the default result for a relationship when no related models are found.
1044
- *
1045
- * @param resolver
1046
- * @param model
1047
- * @returns
1048
- */
1049
- resolveSingleDefault(resolver, model) {
1050
- return resolver.call(model).resolveDefaultResult?.() ?? null;
1051
- }
1052
- /**
1053
- * Generates a unique lookup key for a given value, which is used to store and retrieve
1054
- * values in maps during the eager loading process.
1055
- *
1056
- * @param value The value to generate a lookup key for.
1057
- * @returns A unique string representing the value.
1058
- */
1059
- toLookupKey(value) {
1060
- if (value instanceof Date) return `date:${value.toISOString()}`;
1061
- return `${typeof value}:${String(value)}`;
1062
- }
1063
426
  };
1064
-
1065
- //#endregion
1066
- //#region src/Exceptions/UnsupportedAdapterFeatureException.ts
1067
- var UnsupportedAdapterFeatureException = class extends ArkormException {
1068
- constructor(message, context = {}) {
1069
- super(message, {
1070
- code: "UNSUPPORTED_ADAPTER_FEATURE",
1071
- ...context
1072
- });
1073
- this.name = "UnsupportedAdapterFeatureException";
1074
- }
427
+
428
+ //#endregion
429
+ //#region src/helpers/runtime-registry.ts
430
+ const createEmptyRegistry = () => ({
431
+ paths: {
432
+ models: [],
433
+ seeders: [],
434
+ migrations: [],
435
+ factories: []
436
+ },
437
+ migrations: [],
438
+ seeders: [],
439
+ models: [],
440
+ factories: []
441
+ });
442
+ const registry = createEmptyRegistry();
443
+ const pushUnique = (items, values) => {
444
+ values.forEach((value) => {
445
+ if (!items.includes(value)) items.push(value);
446
+ });
447
+ };
448
+ const normalizePathInput = (paths) => {
449
+ if (paths === void 0) return [];
450
+ return (Array.isArray(paths) ? paths : [paths]).filter((path) => typeof path === "string" && path.trim().length > 0);
451
+ };
452
+ const normalizeConstructors = (items) => items.flatMap((item) => Array.isArray(item) ? item : [item]).filter(Boolean);
453
+ /**
454
+ * Register additional runtime discovery paths without replacing configured paths.
455
+ *
456
+ * @param paths
457
+ */
458
+ const registerPaths = (paths) => {
459
+ Object.entries(paths).forEach(([key, value]) => {
460
+ pushUnique(registry.paths[key], normalizePathInput(value));
461
+ });
462
+ };
463
+ /**
464
+ * Register additional runtime discovery paths for migrations without replacing configured paths.
465
+ *
466
+ * @param paths
467
+ * @returns
468
+ */
469
+ const loadMigrationsFrom = (paths) => registerPaths({ migrations: paths });
470
+ /**
471
+ * Register additional runtime discovery paths for seeders without replacing configured paths.
472
+ *
473
+ * @param paths
474
+ * @returns
475
+ */
476
+ const loadSeedersFrom = (paths) => registerPaths({ seeders: paths });
477
+ /**
478
+ * Register additional runtime discovery paths for models without replacing configured paths.
479
+ *
480
+ * @param paths
481
+ * @returns
482
+ */
483
+ const loadModelsFrom = (paths) => registerPaths({ models: paths });
484
+ /**
485
+ * Register additional runtime discovery paths for factories without replacing configured paths.
486
+ *
487
+ * @param paths
488
+ * @returns
489
+ */
490
+ const loadFactoriesFrom = (paths) => registerPaths({ factories: paths });
491
+ /**
492
+ * Register migration constructors directly without relying on runtime discovery.
493
+ *
494
+ * @param migrations
495
+ */
496
+ const registerMigrations = (...migrations) => {
497
+ pushUnique(registry.migrations, normalizeConstructors(migrations));
498
+ };
499
+ /**
500
+ * Register seeder constructors directly without relying on runtime discovery.
501
+ *
502
+ * @param seeders
503
+ */
504
+ const registerSeeders = (...seeders) => {
505
+ pushUnique(registry.seeders, normalizeConstructors(seeders));
506
+ };
507
+ /**
508
+ * Register model constructors directly without relying on runtime discovery.
509
+ *
510
+ * @param models
511
+ */
512
+ const registerModels = (...models) => {
513
+ pushUnique(registry.models, normalizeConstructors(models));
514
+ };
515
+ /**
516
+ * Register factory constructors or instances directly without relying on runtime discovery.
517
+ *
518
+ * @param factories
519
+ */
520
+ const registerFactories = (...factories) => {
521
+ pushUnique(registry.factories, normalizeConstructors(factories));
522
+ };
523
+ /**
524
+ * Get registered runtime discovery paths or registered constructors for a specific type.
525
+ *
526
+ * @param key
527
+ * @returns
528
+ */
529
+ const getRegisteredPaths = (key) => {
530
+ if (key) return [...registry.paths[key]];
531
+ return {
532
+ models: [...registry.paths.models],
533
+ seeders: [...registry.paths.seeders],
534
+ migrations: [...registry.paths.migrations],
535
+ factories: [...registry.paths.factories]
536
+ };
537
+ };
538
+ /**
539
+ * Get registered migration constructors instances.
540
+ *
541
+ * @returns
542
+ */
543
+ const getRegisteredMigrations = () => [...registry.migrations];
544
+ /**
545
+ * Get registered seeder constructors instances.
546
+ *
547
+ * @returns
548
+ */
549
+ const getRegisteredSeeders = () => [...registry.seeders];
550
+ /**
551
+ * Get registered model constructors instances.
552
+ *
553
+ * @returns
554
+ */
555
+ const getRegisteredModels = () => [...registry.models];
556
+ const resolveRegisteredModel = (modelName, context = {}) => {
557
+ const normalized = modelName.trim();
558
+ const model = registry.models.find((registered) => registered.name === normalized);
559
+ if (!model) throw new RelationResolutionException(`Model [${normalized}] is not registered. Register it with Arkorm.registerModels() or load it through configured model paths.`, {
560
+ operation: context.operation ?? "relationship.resolveModel",
561
+ model: normalized,
562
+ relation: context.relation
563
+ });
564
+ return model;
565
+ };
566
+ /**
567
+ * Get registered factory constructors or instances.
568
+ *
569
+ * @returns
570
+ */
571
+ const getRegisteredFactories = () => [...registry.factories];
572
+ const resetRuntimeRegistryForTests = () => {
573
+ const empty = createEmptyRegistry();
574
+ registry.paths = empty.paths;
575
+ registry.migrations = empty.migrations;
576
+ registry.seeders = empty.seeders;
577
+ registry.models = empty.models;
578
+ registry.factories = empty.factories;
1075
579
  };
1076
580
 
1077
581
  //#endregion
1078
- //#region src/helpers/runtime-module-loader.ts
1079
- var RuntimeModuleLoader = class {
1080
- static async load(filePath, useDefault = false) {
1081
- const resolvedPath = (0, node_path.resolve)(filePath);
1082
- return await (0, jiti.createJiti)((0, node_url.pathToFileURL)(resolvedPath).href, {
1083
- interopDefault: false,
1084
- tsconfigPaths: true,
1085
- sourceMaps: true
1086
- }).import(resolvedPath, useDefault ? { default: true } : {});
582
+ //#region src/helpers/runtime-config.ts
583
+ const resolveDefaultStubsPath = () => {
584
+ let current = path.default.dirname((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
585
+ while (true) {
586
+ const packageJsonPath = path.default.join(current, "package.json");
587
+ const stubsPath = path.default.join(current, "stubs");
588
+ if ((0, fs.existsSync)(packageJsonPath) && (0, fs.existsSync)(stubsPath)) return stubsPath;
589
+ const parent = path.default.dirname(current);
590
+ if (parent === current) break;
591
+ current = parent;
1087
592
  }
1088
- /**
1089
- * Load many modules through a single shared jiti context, retrying files
1090
- * that fail until no further progress is made.
1091
- *
1092
- * A shared module cache resolves circular imports consistently: a file that
1093
- * fails because one of its dependencies was not yet cached (for example a
1094
- * trait whose module imports a model which imports the class that uses the
1095
- * trait) succeeds on a later pass, once a sibling load has populated that
1096
- * dependency. A fresh per-file context cannot do this, so the same set of
1097
- * files can load or fail depending purely on which file is the entry point.
1098
- *
1099
- * Each returned entry carries either the loaded module or the last error,
1100
- * so callers can surface genuine failures instead of silently dropping them.
1101
- *
1102
- * @param filePaths
1103
- * @param useDefault
1104
- * @returns
1105
- */
1106
- static async loadAll(filePaths, useDefault = false) {
1107
- const jiti$1 = (0, jiti.createJiti)(`${(0, node_url.pathToFileURL)((0, node_path.resolve)(".")).href}/`, {
1108
- interopDefault: false,
1109
- tsconfigPaths: true,
1110
- sourceMaps: true
1111
- });
1112
- const results = /* @__PURE__ */ new Map();
1113
- let pending = filePaths.map((file) => (0, node_path.resolve)(file));
1114
- while (pending.length > 0) {
1115
- const failed = [];
1116
- for (const file of pending) try {
1117
- const module = await jiti$1.import(file, useDefault ? { default: true } : {});
1118
- results.set(file, {
1119
- file,
1120
- module
1121
- });
1122
- } catch (error) {
1123
- failed.push(file);
1124
- results.set(file, {
1125
- file,
1126
- module: null,
1127
- error
1128
- });
1129
- }
1130
- if (failed.length === pending.length) break;
1131
- pending = failed;
1132
- }
1133
- return filePaths.map((file) => results.get((0, node_path.resolve)(file)));
593
+ return path.default.join(process.cwd(), "stubs");
594
+ };
595
+ const baseConfig = {
596
+ naming: { case: "snake" },
597
+ features: {
598
+ persistedColumnMappings: true,
599
+ persistedEnums: true
600
+ },
601
+ paths: {
602
+ stubs: resolveDefaultStubsPath(),
603
+ seeders: path.default.join(process.cwd(), "database", "seeders"),
604
+ models: path.default.join(process.cwd(), "src", "models"),
605
+ migrations: path.default.join(process.cwd(), "database", "migrations"),
606
+ factories: path.default.join(process.cwd(), "database", "factories"),
607
+ buildOutput: path.default.join(process.cwd(), "dist")
608
+ },
609
+ outputExt: "ts"
610
+ };
611
+ const userConfig = {
612
+ ...baseConfig,
613
+ naming: { ...baseConfig.naming ?? {} },
614
+ features: { ...baseConfig.features ?? {} },
615
+ paths: { ...baseConfig.paths ?? {} }
616
+ };
617
+ let runtimeConfigLoaded = false;
618
+ let runtimeConfigLoadingPromise;
619
+ let runtimeModelRegistrationPromise;
620
+ let runtimeModelRegistrationGeneration = 0;
621
+ let runtimeClientResolver;
622
+ let runtimeAdapter;
623
+ let runtimePaginationURLDriverFactory;
624
+ let runtimePaginationCurrentPageResolver;
625
+ let runtimeDebugHandler;
626
+ const transactionClientStorage = new async_hooks.AsyncLocalStorage();
627
+ const transactionAdapterStorage = new async_hooks.AsyncLocalStorage();
628
+ const defaultDebugHandler = (event) => {
629
+ const prefix = `[arkorm:${event.adapter}] ${event.operation}${event.target ? ` [${event.target}]` : ""}`;
630
+ const payload = {
631
+ phase: event.phase,
632
+ durationMs: event.durationMs,
633
+ inspection: event.inspection ?? void 0,
634
+ meta: event.meta,
635
+ error: event.error
636
+ };
637
+ if (event.phase === "error") {
638
+ console.error(prefix, payload);
639
+ return;
1134
640
  }
641
+ console.debug(prefix, payload);
1135
642
  };
1136
-
1137
- //#endregion
1138
- //#region src/helpers/migration-history.ts
1139
- const createEmptyAppliedMigrationsState = () => ({
1140
- version: 1,
1141
- migrations: [],
1142
- runs: []
1143
- });
1144
- const supportsDatabaseMigrationState = (adapter) => {
1145
- return typeof adapter?.readAppliedMigrationsState === "function" && typeof adapter?.writeAppliedMigrationsState === "function";
643
+ const resolveDebugHandler = (debug) => {
644
+ if (debug === true) return defaultDebugHandler;
645
+ return typeof debug === "function" ? debug : void 0;
646
+ };
647
+ const mergeNamingConfig = (naming) => {
648
+ const defaults = baseConfig.naming ?? {};
649
+ const current = userConfig.naming ?? {};
650
+ const resolvedCase = naming?.case ?? naming?.modelTableCase ?? current.case ?? current.modelTableCase ?? defaults.case ?? defaults.modelTableCase ?? "snake";
651
+ return {
652
+ ...defaults,
653
+ ...current,
654
+ ...naming ?? {},
655
+ case: resolvedCase
656
+ };
657
+ };
658
+ const mergePathConfig = (paths) => {
659
+ const defaults = baseConfig.paths ?? {};
660
+ const current = userConfig.paths ?? {};
661
+ const incoming = Object.entries(paths ?? {}).reduce((all, [key, value]) => {
662
+ if (typeof value === "string" && value.trim().length > 0) all[key] = path.default.isAbsolute(value) ? value : path.default.resolve(process.cwd(), value);
663
+ return all;
664
+ }, {});
665
+ return {
666
+ ...defaults,
667
+ ...current,
668
+ ...incoming
669
+ };
670
+ };
671
+ /**
672
+ * Merge the feature configuration from the base defaults, user configuration, and provided options.
673
+ *
674
+ * @param features
675
+ * @returns
676
+ */
677
+ const mergeFeatureConfig = (features) => {
678
+ const defaults = baseConfig.features ?? {};
679
+ const current = userConfig.features ?? {};
680
+ return {
681
+ ...defaults,
682
+ ...current,
683
+ ...features ?? {}
684
+ };
685
+ };
686
+ const resolveRuntimeModelsDirectory = (directory) => {
687
+ if ((0, fs.existsSync)(directory)) return directory;
688
+ const buildOutput = userConfig.paths?.buildOutput;
689
+ if (typeof buildOutput !== "string" || buildOutput.trim().length === 0) return directory;
690
+ const relativeSource = path.default.relative(process.cwd(), directory);
691
+ if (!relativeSource || relativeSource.startsWith("..")) return directory;
692
+ const mappedDirectory = path.default.join(buildOutput, relativeSource);
693
+ return (0, fs.existsSync)(mappedDirectory) ? mappedDirectory : directory;
694
+ };
695
+ const collectRuntimeModelFiles = (directory) => {
696
+ if (!(0, fs.existsSync)(directory)) return [];
697
+ return (0, fs.readdirSync)(directory, { withFileTypes: true }).flatMap((entry) => {
698
+ const entryPath = path.default.join(directory, entry.name);
699
+ if (entry.isDirectory()) return collectRuntimeModelFiles(entryPath);
700
+ if (!entry.isFile() || !/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i.test(entry.name)) return [];
701
+ if (/\.d\.(ts|mts|cts)$/i.test(entry.name)) return [];
702
+ return [entryPath];
703
+ });
704
+ };
705
+ const isModelConstructor = (value) => {
706
+ if (typeof value !== "function") return false;
707
+ const candidate = value;
708
+ return typeof candidate.query === "function" && typeof candidate.hydrate === "function" && typeof candidate.getTable === "function" && typeof candidate.getPrimaryKey === "function";
709
+ };
710
+ const registerConfiguredModels = async (generation) => {
711
+ const configured = userConfig.paths?.models;
712
+ const additional = getRegisteredPaths("models");
713
+ const files = [...typeof configured === "string" ? [configured] : [], ...additional].map((directory) => resolveRuntimeModelsDirectory(path.default.isAbsolute(directory) ? directory : path.default.resolve(process.cwd(), directory))).filter((directory, index, all) => all.indexOf(directory) === index).flatMap(collectRuntimeModelFiles);
714
+ const loaded = await RuntimeModuleLoader.loadAll(files);
715
+ loaded.forEach(({ file, error }) => {
716
+ if (error !== void 0) console.warn(`[arkorm] Failed to load model file for registration: ${file}\n ${error instanceof Error ? error.message : String(error)}\n Models in this file will not be registered (morphTo resolution may fail). This is often a circular import — avoid importing models at the top level of trait modules.`);
717
+ });
718
+ const models = loaded.flatMap(({ module }) => module ? Object.values(module).filter(isModelConstructor) : []);
719
+ if (generation === runtimeModelRegistrationGeneration) registerModels(models);
720
+ };
721
+ const scheduleConfiguredModelRegistration = () => {
722
+ const generation = runtimeModelRegistrationGeneration;
723
+ runtimeModelRegistrationPromise = (runtimeModelRegistrationPromise ?? Promise.resolve()).then(async () => await registerConfiguredModels(generation));
724
+ return runtimeModelRegistrationPromise;
725
+ };
726
+ const awaitConfiguredModelsRegistration = async () => {
727
+ if (runtimeModelRegistrationPromise) await runtimeModelRegistrationPromise;
728
+ };
729
+ /**
730
+ * Define the ArkORM runtime configuration. This function can be used to provide.
731
+ *
732
+ * @param config The ArkORM configuration object.
733
+ * @returns The same configuration object.
734
+ */
735
+ const defineConfig = (config) => {
736
+ return config;
737
+ };
738
+ /**
739
+ * Bind a database adapter instance to an array of models that support adapter binding.
740
+ *
741
+ * @param adapter
742
+ * @param models
743
+ * @returns
744
+ */
745
+ const bindAdapterToModels = (adapter, models) => {
746
+ models.forEach((model) => {
747
+ model.setAdapter(adapter);
748
+ });
749
+ return adapter;
1146
750
  };
1147
- const resolveMigrationStateFilePath = (cwd, configuredPath) => {
1148
- if (configuredPath && configuredPath.trim().length > 0) return (0, node_path.resolve)(configuredPath);
1149
- return (0, node_path.join)(cwd, ".arkormx", "migrations.applied.json");
751
+ /**
752
+ * Get the user-provided ArkORM configuration.
753
+ *
754
+ * @param key Optional specific configuration key to retrieve. If omitted, the entire configuration object is returned.
755
+ * @returns The user-provided ArkORM configuration object.
756
+ */
757
+ const getUserConfig = (key) => {
758
+ if (key) return userConfig[key];
759
+ return userConfig;
1150
760
  };
1151
- const buildMigrationIdentity = (filePath, className) => {
1152
- const fileName = filePath.split("/").pop()?.split("\\").pop() ?? filePath;
1153
- return `${fileName.slice(0, fileName.length - (0, node_path.extname)(fileName).length)}:${className}`;
761
+ /**
762
+ * Configure the ArkORM runtime with the provided runtime client resolver and
763
+ * adapter-first options.
764
+ *
765
+ * @param client
766
+ * @param options
767
+ */
768
+ const configureArkormRuntime = (client, options = {}) => {
769
+ const resolvedClient = client ?? options.client;
770
+ const nextConfig = {
771
+ ...userConfig,
772
+ naming: mergeNamingConfig(options.naming),
773
+ features: mergeFeatureConfig(options.features),
774
+ paths: mergePathConfig(options.paths)
775
+ };
776
+ nextConfig.client = resolvedClient;
777
+ nextConfig.prisma = resolvedClient;
778
+ if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
779
+ if (options.adapter !== void 0) nextConfig.adapter = options.adapter;
780
+ if (options.boot !== void 0) nextConfig.boot = options.boot;
781
+ if (options.debug !== void 0) nextConfig.debug = options.debug;
782
+ if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
783
+ Object.assign(userConfig, { ...nextConfig });
784
+ runtimeClientResolver = resolvedClient;
785
+ runtimeAdapter = options.adapter;
786
+ runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
787
+ runtimePaginationCurrentPageResolver = nextConfig.pagination?.resolveCurrentPage;
788
+ runtimeDebugHandler = resolveDebugHandler(nextConfig.debug);
789
+ scheduleConfiguredModelRegistration();
790
+ const bootClient = resolveClient(resolvedClient);
791
+ options.boot?.({
792
+ client: bootClient,
793
+ prisma: bootClient,
794
+ bindAdapter: bindAdapterToModels
795
+ });
1154
796
  };
1155
- const computeMigrationChecksum = (filePath) => {
1156
- if (!(0, node_fs.existsSync)(filePath)) return (0, node_crypto.createHash)("sha256").update(filePath).digest("hex");
1157
- const source = (0, node_fs.readFileSync)(filePath, "utf-8");
1158
- return (0, node_crypto.createHash)("sha256").update(source).digest("hex");
797
+ /**
798
+ * Reset the ArkORM runtime configuration.
799
+ * This is primarily intended for testing purposes.
800
+ */
801
+ const resetArkormRuntimeForTests = () => {
802
+ Object.assign(userConfig, {
803
+ ...baseConfig,
804
+ naming: { ...baseConfig.naming ?? {} },
805
+ features: { ...baseConfig.features ?? {} },
806
+ paths: { ...baseConfig.paths ?? {} }
807
+ });
808
+ runtimeConfigLoaded = false;
809
+ runtimeConfigLoadingPromise = void 0;
810
+ runtimeModelRegistrationPromise = void 0;
811
+ runtimeModelRegistrationGeneration++;
812
+ runtimeClientResolver = void 0;
813
+ runtimeAdapter = void 0;
814
+ runtimePaginationURLDriverFactory = void 0;
815
+ runtimePaginationCurrentPageResolver = void 0;
816
+ runtimeDebugHandler = void 0;
817
+ resetPersistedColumnMappingsCache();
818
+ resetRuntimeRegistryForTests();
1159
819
  };
1160
- const readAppliedMigrationsState = (stateFilePath) => {
1161
- if (!(0, node_fs.existsSync)(stateFilePath)) return createEmptyAppliedMigrationsState();
1162
- try {
1163
- const parsed = JSON.parse((0, node_fs.readFileSync)(stateFilePath, "utf-8"));
1164
- if (!Array.isArray(parsed.migrations)) return createEmptyAppliedMigrationsState();
1165
- return {
1166
- version: 1,
1167
- migrations: parsed.migrations.filter((migration) => {
1168
- 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");
1169
- }),
1170
- runs: Array.isArray(parsed.runs) ? parsed.runs.filter((run) => {
1171
- return typeof run?.id === "string" && typeof run?.appliedAt === "string" && Array.isArray(run?.migrationIds) && run.migrationIds.every((item) => typeof item === "string");
1172
- }) : []
1173
- };
1174
- } catch {
1175
- return createEmptyAppliedMigrationsState();
1176
- }
820
+ /**
821
+ * Resolve a runtime client instance from the provided resolver, which can be either
822
+ * a direct client instance or a function that returns a client instance.
823
+ *
824
+ * @param resolver
825
+ * @returns
826
+ */
827
+ const resolveClient = (resolver) => {
828
+ if (!resolver) return void 0;
829
+ const client = typeof resolver === "function" ? resolver() : resolver;
830
+ if (!client || typeof client !== "object") return void 0;
831
+ return client;
1177
832
  };
1178
- const readAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
1179
- if (supportsDatabaseMigrationState(adapter)) return await adapter.readAppliedMigrationsState();
1180
- return readAppliedMigrationsState(stateFilePath);
833
+ /**
834
+ * Resolve and apply the ArkORM configuration from an imported module.
835
+ * This function checks for a default export and falls back to the module itself, then validates
836
+ * the configuration object and applies it to the runtime if valid.
837
+ *
838
+ * @param imported
839
+ * @returns
840
+ */
841
+ const resolveAndApplyConfig = (imported) => {
842
+ const config = imported?.default ?? imported;
843
+ if (!config || typeof config !== "object") return;
844
+ const runtimeClient = config.client ?? config.prisma;
845
+ configureArkormRuntime(runtimeClient, {
846
+ client: runtimeClient,
847
+ adapter: config.adapter,
848
+ boot: config.boot,
849
+ debug: config.debug,
850
+ naming: config.naming,
851
+ features: config.features,
852
+ pagination: config.pagination,
853
+ paths: config.paths,
854
+ outputExt: config.outputExt
855
+ });
856
+ runtimeConfigLoaded = true;
1181
857
  };
1182
- const writeAppliedMigrationsState = (stateFilePath, state) => {
1183
- const directory = (0, node_path.dirname)(stateFilePath);
1184
- if (!(0, node_fs.existsSync)(directory)) (0, node_fs.mkdirSync)(directory, { recursive: true });
1185
- (0, node_fs.writeFileSync)(stateFilePath, JSON.stringify(state, null, 2));
858
+ /**
859
+ * Dynamically import a configuration file.
860
+ * A cache-busting query parameter is appended to ensure the latest version is loaded.
861
+ *
862
+ * @param configPath
863
+ * @returns A promise that resolves to the imported configuration module.
864
+ */
865
+ const importConfigFile = (configPath) => {
866
+ return RuntimeModuleLoader.load(configPath);
1186
867
  };
1187
- const writeAppliedMigrationsStateToStore = async (adapter, stateFilePath, state) => {
1188
- if (supportsDatabaseMigrationState(adapter)) {
1189
- await adapter.writeAppliedMigrationsState(state);
1190
- return;
868
+ const loadRuntimeConfigSync = () => {
869
+ const require = (0, module$1.createRequire)(require("url").pathToFileURL(__filename).href);
870
+ const syncConfigPaths = [path.default.join(process.cwd(), "arkormx.config.cjs")];
871
+ for (const configPath of syncConfigPaths) {
872
+ if (!(0, fs.existsSync)(configPath)) continue;
873
+ try {
874
+ resolveAndApplyConfig(require(configPath));
875
+ return true;
876
+ } catch {
877
+ continue;
878
+ }
1191
879
  }
1192
- writeAppliedMigrationsState(stateFilePath, state);
880
+ return false;
1193
881
  };
1194
- const deleteAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
1195
- if (supportsDatabaseMigrationState(adapter)) {
1196
- await adapter.writeAppliedMigrationsState(createEmptyAppliedMigrationsState());
1197
- return "database";
882
+ /**
883
+ * Load the ArkORM configuration by searching for configuration files in the
884
+ * current working directory.
885
+ * @returns
886
+ */
887
+ const loadArkormConfig = async () => {
888
+ if (runtimeConfigLoaded) {
889
+ await awaitConfiguredModelsRegistration();
890
+ return;
1198
891
  }
1199
- if (!(0, node_fs.existsSync)(stateFilePath)) return "missing-file";
1200
- writeAppliedMigrationsState(stateFilePath, createEmptyAppliedMigrationsState());
1201
- return "file";
892
+ if (runtimeConfigLoadingPromise) return await runtimeConfigLoadingPromise;
893
+ if (loadRuntimeConfigSync()) {
894
+ await awaitConfiguredModelsRegistration();
895
+ return;
896
+ }
897
+ runtimeConfigLoadingPromise = (async () => {
898
+ const configPaths = [path.default.join(process.cwd(), "arkormx.config.js"), path.default.join(process.cwd(), "arkormx.config.ts")];
899
+ for (const configPath of configPaths) {
900
+ if (!(0, fs.existsSync)(configPath)) continue;
901
+ try {
902
+ resolveAndApplyConfig(await importConfigFile(configPath));
903
+ await awaitConfiguredModelsRegistration();
904
+ return;
905
+ } catch {
906
+ continue;
907
+ }
908
+ }
909
+ runtimeConfigLoaded = true;
910
+ await scheduleConfiguredModelRegistration();
911
+ await awaitConfiguredModelsRegistration();
912
+ })();
913
+ await runtimeConfigLoadingPromise;
1202
914
  };
1203
- const isMigrationApplied = (state, identity, checksum) => {
1204
- const matched = state.migrations.find((migration) => migration.id === identity);
1205
- if (!matched) return false;
1206
- if (checksum && matched.checksum) return matched.checksum === checksum;
1207
- if (checksum && !matched.checksum) return false;
1208
- return true;
915
+ /**
916
+ * Ensure that the ArkORM configuration is loaded.
917
+ * This function can be called to trigger the loading process if it hasn't already been initiated.
918
+ * If the configuration is already loaded, it will return immediately.
919
+ *
920
+ * @returns
921
+ */
922
+ const ensureArkormConfigLoading = () => {
923
+ if (runtimeConfigLoaded) return;
924
+ if (!runtimeConfigLoadingPromise) loadArkormConfig();
1209
925
  };
1210
- const findAppliedMigration = (state, identity) => {
1211
- return state.migrations.find((migration) => migration.id === identity);
926
+ const getDefaultStubsPath = () => {
927
+ return resolveDefaultStubsPath();
1212
928
  };
1213
- const markMigrationApplied = (state, entry) => {
1214
- const next = state.migrations.filter((migration) => migration.id !== entry.id);
1215
- next.push(entry);
1216
- return {
1217
- version: 1,
1218
- migrations: next,
1219
- runs: state.runs ?? []
1220
- };
929
+ /**
930
+ * Get the runtime compatibility client.
931
+ * This function will trigger the loading of the ArkORM configuration if
932
+ * it hasn't already been loaded.
933
+ *
934
+ * @returns
935
+ */
936
+ const getRuntimeClient = () => {
937
+ const activeTransactionClient = transactionClientStorage.getStore();
938
+ if (activeTransactionClient) return activeTransactionClient;
939
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
940
+ return resolveClient(runtimeClientResolver);
941
+ };
942
+ /**
943
+ * @deprecated Use getRuntimeClient instead.
944
+ */
945
+ const getRuntimePrismaClient = getRuntimeClient;
946
+ /**
947
+ * Get the currently configured runtime adapter, if any.
948
+ *
949
+ * @returns
950
+ */
951
+ const getRuntimeAdapter = () => {
952
+ const activeTransactionAdapter = transactionAdapterStorage.getStore();
953
+ if (activeTransactionAdapter) return activeTransactionAdapter;
954
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
955
+ return runtimeAdapter;
1221
956
  };
1222
- const removeAppliedMigration = (state, identity) => {
1223
- return {
1224
- version: 1,
1225
- migrations: state.migrations.filter((migration) => migration.id !== identity),
1226
- runs: (state.runs ?? []).map((run) => ({
1227
- ...run,
1228
- migrationIds: run.migrationIds.filter((id) => id !== identity)
1229
- })).filter((run) => run.migrationIds.length > 0)
1230
- };
957
+ /**
958
+ * Releases the database resources held by the configured runtime — the adapter's
959
+ * connection pool and/or the configured client. Intended for short-lived
960
+ * processes (the CLI) so the Node event loop drains and the process exits
961
+ * promptly instead of hanging on pool idle timeouts. Teardown failures are
962
+ * swallowed because the process is shutting down anyway.
963
+ */
964
+ const disposeArkormRuntime = async () => {
965
+ let adapterDisposed = false;
966
+ try {
967
+ if (runtimeAdapter && typeof runtimeAdapter.dispose === "function") {
968
+ await runtimeAdapter.dispose();
969
+ adapterDisposed = true;
970
+ }
971
+ } catch {}
972
+ const client = getRuntimeClient();
973
+ if (!client) return;
974
+ try {
975
+ if (typeof client.$disconnect === "function") await client.$disconnect();
976
+ else if (!adapterDisposed && typeof client.destroy === "function") await client.destroy();
977
+ else if (!adapterDisposed && typeof client.end === "function") await client.end();
978
+ } catch {}
1231
979
  };
1232
- const buildMigrationRunId = () => {
1233
- return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
980
+ const getActiveTransactionClient = () => {
981
+ return transactionClientStorage.getStore();
1234
982
  };
1235
- const markMigrationRun = (state, run) => {
1236
- const nextRuns = (state.runs ?? []).filter((existing) => existing.id !== run.id);
1237
- nextRuns.push(run);
1238
- return {
1239
- version: 1,
1240
- migrations: state.migrations,
1241
- runs: nextRuns
1242
- };
983
+ const getActiveTransactionAdapter = () => {
984
+ return transactionAdapterStorage.getStore();
1243
985
  };
1244
- const getLastMigrationRun = (state) => {
1245
- const runs = state.runs ?? [];
1246
- if (runs.length === 0) return void 0;
1247
- return runs.map((run, index) => ({
1248
- run,
1249
- index
1250
- })).sort((left, right) => {
1251
- const appliedAtOrder = right.run.appliedAt.localeCompare(left.run.appliedAt);
1252
- if (appliedAtOrder !== 0) return appliedAtOrder;
1253
- return right.index - left.index;
1254
- })[0]?.run;
986
+ const isTransactionCapableClient = (value) => {
987
+ if (!value || typeof value !== "object") return false;
988
+ return typeof value.$transaction === "function";
1255
989
  };
1256
- const getLatestAppliedMigrations = (state, steps) => {
1257
- return state.migrations.map((migration, index) => ({
1258
- migration,
1259
- index
1260
- })).sort((left, right) => {
1261
- const appliedAtOrder = right.migration.appliedAt.localeCompare(left.migration.appliedAt);
1262
- if (appliedAtOrder !== 0) return appliedAtOrder;
1263
- return right.index - left.index;
1264
- }).slice(0, Math.max(0, steps)).map((entry) => entry.migration);
990
+ const runArkormTransaction = async (callback, options = {}, preferredAdapter) => {
991
+ const activeTransactionAdapter = transactionAdapterStorage.getStore();
992
+ const activeTransactionClient = transactionClientStorage.getStore();
993
+ if (activeTransactionAdapter || activeTransactionClient) return await callback({
994
+ adapter: activeTransactionAdapter,
995
+ client: activeTransactionClient
996
+ });
997
+ const adapter = preferredAdapter ?? getRuntimeAdapter();
998
+ if (adapter) return await adapter.transaction(async (transactionAdapter) => {
999
+ return await transactionAdapterStorage.run(transactionAdapter, async () => {
1000
+ return await callback({
1001
+ adapter: transactionAdapter,
1002
+ client: transactionClientStorage.getStore()
1003
+ });
1004
+ });
1005
+ }, options);
1006
+ const client = getRuntimeClient();
1007
+ if (!client) throw new ArkormException("Cannot start a transaction without a configured runtime client or adapter.", {
1008
+ code: "CLIENT_NOT_CONFIGURED",
1009
+ operation: "transaction"
1010
+ });
1011
+ if (!isTransactionCapableClient(client)) throw new UnsupportedAdapterFeatureException("Transactions are not supported by the current adapter.", {
1012
+ code: "TRANSACTION_NOT_SUPPORTED",
1013
+ operation: "transaction"
1014
+ });
1015
+ return await client.$transaction(async (transactionClient) => {
1016
+ return await transactionClientStorage.run(transactionClient, async () => {
1017
+ return await callback({ client: transactionClient });
1018
+ });
1019
+ }, options);
1265
1020
  };
1266
1021
  /**
1267
- * Groups the applied migrations into batches, oldest batch first, each batch
1268
- * holding its migration ids in application order.
1269
- *
1270
- * A "batch" is one `migrate` run. When no runs were recorded (e.g. legacy state
1271
- * written before run tracking), each applied migration is treated as its own
1272
- * batch so `--step` still behaves predictably.
1022
+ * Get the configured pagination URL driver factory from runtime config.
1273
1023
  *
1274
- * @param state
1275
1024
  * @returns
1276
1025
  */
1277
- const resolveAppliedBatches = (state) => {
1278
- const runs = state.runs ?? [];
1279
- if (runs.length > 0) return runs.map((run, index) => ({
1280
- run,
1281
- index
1282
- })).sort((left, right) => {
1283
- const appliedAtOrder = left.run.appliedAt.localeCompare(right.run.appliedAt);
1284
- if (appliedAtOrder !== 0) return appliedAtOrder;
1285
- return left.index - right.index;
1286
- }).map((entry) => entry.run.migrationIds);
1287
- return state.migrations.map((migration) => [migration.id]);
1026
+ const getRuntimePaginationURLDriverFactory = () => {
1027
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
1028
+ return runtimePaginationURLDriverFactory;
1288
1029
  };
1289
1030
  /**
1290
- * Resolves the migrations belonging to the most recent `batches` batches, ordered
1291
- * for rollback — **reverse of the order they were applied** (most recent batch
1292
- * first, and within each batch the migration that ran `up()` last runs `down()`
1293
- * first), so a rollback exactly reverses how the migrations were applied.
1294
- *
1295
- * Defaults to a single batch (`migrate:rollback` with no `--step`); `--step=N`
1296
- * rolls back the last N batches.
1031
+ * Get the configured current-page resolver from runtime config.
1297
1032
  *
1298
- * @param state
1299
- * @param batches
1300
1033
  * @returns
1301
1034
  */
1302
- const getLastBatchMigrations = (state, batches = 1) => {
1303
- if (batches < 1) return [];
1304
- const allBatches = resolveAppliedBatches(state);
1305
- const selected = allBatches.slice(Math.max(0, allBatches.length - batches));
1306
- const byId = new Map(state.migrations.map((migration) => [migration.id, migration]));
1307
- return selected.reverse().flatMap((migrationIds) => [...migrationIds].reverse()).map((id) => byId.get(id)).filter((migration) => Boolean(migration));
1035
+ const getRuntimePaginationCurrentPageResolver = () => {
1036
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
1037
+ return runtimePaginationCurrentPageResolver;
1038
+ };
1039
+ const getRuntimeDebugHandler = () => {
1040
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
1041
+ return runtimeDebugHandler;
1042
+ };
1043
+ const emitRuntimeDebugEvent = (event) => {
1044
+ getRuntimeDebugHandler()?.(event);
1308
1045
  };
1309
-
1310
- //#endregion
1311
- //#region src/database/ForeignKeyBuilder.ts
1312
1046
  /**
1313
- * The ForeignKeyBuilder class provides a fluent interface for defining
1314
- * foreign key constraints in a migration. It allows you to specify
1315
- * the referenced table and column, as well as actions to take on
1316
- * delete and aliases for the relation.
1047
+ * Check if a given value matches Arkorm's query-schema contract
1048
+ * by verifying the presence of common delegate methods.
1317
1049
  *
1318
- * @author Legacy (3m1n3nc3)
1319
- * @since 0.2.2
1050
+ * @param value The value to check.
1051
+ * @returns True if the value matches the query-schema contract, false otherwise.
1320
1052
  */
1321
- var ForeignKeyBuilder = class {
1322
- constructor(foreignKey) {
1323
- this.foreignKey = foreignKey;
1324
- }
1325
- /**
1326
- * Defines the referenced table and column for this foreign key constraint.
1327
- *
1328
- * @param table
1329
- * @param column
1330
- * @returns
1331
- */
1332
- references(table, column) {
1333
- this.foreignKey.referencesTable = table;
1334
- this.foreignKey.referencesColumn = column;
1335
- return this;
1336
- }
1337
- /**
1338
- * Defines the action to take when a referenced record is deleted, such
1339
- * as "CASCADE", "SET NULL", or "RESTRICT".
1340
- *
1341
- * @param action
1342
- * @returns
1343
- */
1344
- onDelete(action) {
1345
- this.foreignKey.onDelete = action;
1346
- return this;
1347
- }
1348
- /**
1349
- * Defines an alias for the relation represented by this foreign key, which
1350
- * can be used in the ORM for more intuitive access to related models.
1351
- *
1352
- * @param name
1353
- * @returns
1354
- */
1355
- alias(name) {
1356
- this.foreignKey.relationAlias = name;
1357
- return this;
1358
- }
1359
- /**
1360
- * Defines an alias for the inverse relation represented by this foreign key.
1361
- *
1362
- * @param name
1363
- * @returns
1364
- */
1365
- inverseAlias(name) {
1366
- this.foreignKey.inverseRelationAlias = name;
1367
- return this;
1368
- }
1369
- /**
1370
- * Defines an alias for the foreign key field itself, which can be
1371
- * used in the ORM for more intuitive access to the foreign key value.
1372
- *
1373
- * @param fieldName
1374
- * @returns
1375
- */
1376
- as(fieldName) {
1377
- this.foreignKey.fieldAlias = fieldName;
1378
- return this;
1379
- }
1380
- };
1381
-
1382
- //#endregion
1383
- //#region src/helpers/PrimaryKeyGenerationPlanner.ts
1384
- var PrimaryKeyGenerationPlanner = class {
1385
- static plan(column) {
1386
- if (!column.primary || column.default !== void 0) return void 0;
1387
- if (column.type === "uuid" || column.type === "string") return {
1388
- strategy: "uuid",
1389
- prismaDefault: "@default(uuid())",
1390
- databaseDefault: column.type === "uuid" ? "gen_random_uuid()" : "gen_random_uuid()::text",
1391
- runtimeFactory: "uuid"
1392
- };
1393
- }
1394
- static generate(generation) {
1395
- if (generation?.runtimeFactory === "uuid") return (0, node_crypto.randomUUID)();
1396
- }
1053
+ const isQuerySchemaLike = (value) => {
1054
+ if (!value || typeof value !== "object") return false;
1055
+ const candidate = value;
1056
+ return [
1057
+ "findMany",
1058
+ "findFirst",
1059
+ "create",
1060
+ "update",
1061
+ "delete",
1062
+ "count"
1063
+ ].every((method) => typeof candidate[method] === "function");
1397
1064
  };
1065
+ /**
1066
+ * @deprecated Use isQuerySchemaLike instead.
1067
+ */
1068
+ const isDelegateLike = isQuerySchemaLike;
1069
+ loadArkormConfig();
1398
1070
 
1399
1071
  //#endregion
1400
1072
  //#region src/Expression.ts
@@ -3650,886 +3322,1230 @@ const normalizePersistedTimestampColumns = (value) => {
3650
3322
  }, []);
3651
3323
  return columns.length > 0 ? columns : void 0;
3652
3324
  };
3653
- const normalizePersistedColumnMappingsState = (state) => {
3654
- return {
3655
- version: 1,
3656
- tables: Object.entries(state?.tables ?? {}).reduce((all, [tableName, tableMetadata]) => {
3657
- if (tableName.trim().length === 0) return all;
3658
- const normalized = normalizePersistedTableMetadata(tableMetadata);
3659
- if (Object.keys(normalized.columns).length > 0 || Object.keys(normalized.enums).length > 0 || normalized.primaryKeyGeneration || normalized.timestampColumns?.length) all[tableName] = normalized;
3325
+ const normalizePersistedColumnMappingsState = (state) => {
3326
+ return {
3327
+ version: 1,
3328
+ tables: Object.entries(state?.tables ?? {}).reduce((all, [tableName, tableMetadata]) => {
3329
+ if (tableName.trim().length === 0) return all;
3330
+ const normalized = normalizePersistedTableMetadata(tableMetadata);
3331
+ if (Object.keys(normalized.columns).length > 0 || Object.keys(normalized.enums).length > 0 || normalized.primaryKeyGeneration || normalized.timestampColumns?.length) all[tableName] = normalized;
3332
+ return all;
3333
+ }, {})
3334
+ };
3335
+ };
3336
+ const buildPersistedFeatureDisabledError = (feature, table) => {
3337
+ 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.*.`, {
3338
+ operation: "metadata.persisted",
3339
+ meta: {
3340
+ feature,
3341
+ table
3342
+ }
3343
+ });
3344
+ };
3345
+ const assertPersistedTableMetadataEnabled = (table, metadata, features, strict) => {
3346
+ if (!strict) return;
3347
+ if (!features.persistedColumnMappings && Object.keys(metadata.columns).length > 0) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
3348
+ if (!features.persistedEnums && Object.keys(metadata.enums).length > 0) throw buildPersistedFeatureDisabledError("persistedEnums", table);
3349
+ };
3350
+ const buildEnumUnionType = (values) => {
3351
+ return values.map((value) => {
3352
+ return `'${value.replace(/'/g, String.raw`\'`)}'`;
3353
+ }).join(" | ");
3354
+ };
3355
+ const resetPersistedColumnMappingsCache = () => {
3356
+ cachedColumnMappingsPath = void 0;
3357
+ cachedColumnMappingsState = void 0;
3358
+ };
3359
+ const readPersistedColumnMappingsState = (filePath) => {
3360
+ if (cachedColumnMappingsPath === filePath && cachedColumnMappingsState) return cachedColumnMappingsState;
3361
+ if (!(0, node_fs.existsSync)(filePath)) {
3362
+ const empty = createEmptyPersistedColumnMappingsState();
3363
+ cachedColumnMappingsPath = filePath;
3364
+ cachedColumnMappingsState = empty;
3365
+ return empty;
3366
+ }
3367
+ try {
3368
+ const normalized = normalizePersistedColumnMappingsState(JSON.parse((0, node_fs.readFileSync)(filePath, "utf-8")));
3369
+ cachedColumnMappingsPath = filePath;
3370
+ cachedColumnMappingsState = normalized;
3371
+ return normalized;
3372
+ } catch {
3373
+ const empty = createEmptyPersistedColumnMappingsState();
3374
+ cachedColumnMappingsPath = filePath;
3375
+ cachedColumnMappingsState = empty;
3376
+ return empty;
3377
+ }
3378
+ };
3379
+ const writePersistedColumnMappingsState = (filePath, state) => {
3380
+ const normalized = normalizePersistedColumnMappingsState(state);
3381
+ const directory = (0, node_path.dirname)(filePath);
3382
+ if (!(0, node_fs.existsSync)(directory)) (0, node_fs.mkdirSync)(directory, { recursive: true });
3383
+ (0, node_fs.writeFileSync)(filePath, JSON.stringify(normalized, null, 2));
3384
+ cachedColumnMappingsPath = filePath;
3385
+ cachedColumnMappingsState = normalized;
3386
+ };
3387
+ const deletePersistedColumnMappingsState = (filePath) => {
3388
+ if ((0, node_fs.existsSync)(filePath)) (0, node_fs.rmSync)(filePath, { force: true });
3389
+ resetPersistedColumnMappingsCache();
3390
+ };
3391
+ const getPersistedTableMetadata = (table, options = {}) => {
3392
+ const metadata = readPersistedColumnMappingsState(resolveColumnMappingsFilePath(options.cwd ?? process.cwd(), options.configuredPath)).tables[table] ?? {
3393
+ columns: {},
3394
+ enums: {}
3395
+ };
3396
+ assertPersistedTableMetadataEnabled(table, metadata, options.features ?? resolvePersistedMetadataFeatures(), options.strict ?? false);
3397
+ return {
3398
+ columns: { ...metadata.columns },
3399
+ enums: Object.entries(metadata.enums).reduce((all, [columnName, values]) => {
3400
+ all[columnName] = [...values];
3401
+ return all;
3402
+ }, {}),
3403
+ primaryKeyGeneration: metadata.primaryKeyGeneration ? { ...metadata.primaryKeyGeneration } : void 0,
3404
+ timestampColumns: metadata.timestampColumns?.map((column) => ({ ...column }))
3405
+ };
3406
+ };
3407
+ const getPersistedColumnMap = (table, options = {}) => {
3408
+ return getPersistedTableMetadata(table, options).columns;
3409
+ };
3410
+ const getPersistedEnumMap = (table, options = {}) => {
3411
+ return getPersistedTableMetadata(table, options).enums;
3412
+ };
3413
+ const getPersistedPrimaryKeyGeneration = (table, options = {}) => {
3414
+ return getPersistedTableMetadata(table, options).primaryKeyGeneration;
3415
+ };
3416
+ const getPersistedTimestampColumns = (table, options = {}) => {
3417
+ return getPersistedTableMetadata(table, options).timestampColumns ?? [];
3418
+ };
3419
+ const applyMappedColumn = (tableColumns, column, features, table) => {
3420
+ if (typeof column.map === "string" && column.map.trim().length > 0 && column.map !== column.name) {
3421
+ if (!features.persistedColumnMappings) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
3422
+ tableColumns[column.name] = column.map;
3423
+ return;
3424
+ }
3425
+ delete tableColumns[column.name];
3426
+ };
3427
+ const applyEnumColumn = (tableEnums, column, features, table) => {
3428
+ const values = column.enumValues ?? [];
3429
+ if (column.type === "enum" && values.length > 0) {
3430
+ if (!features.persistedEnums) throw buildPersistedFeatureDisabledError("persistedEnums", table);
3431
+ tableEnums[column.name] = [...values];
3432
+ return;
3433
+ }
3434
+ delete tableEnums[column.name];
3435
+ };
3436
+ const removePersistedColumnMetadata = (tableMetadata, columnName) => {
3437
+ delete tableMetadata.columns[columnName];
3438
+ delete tableMetadata.enums[columnName];
3439
+ Object.entries(tableMetadata.columns).forEach(([attribute, mappedColumn]) => {
3440
+ if (mappedColumn === columnName) delete tableMetadata.columns[attribute];
3441
+ });
3442
+ if (tableMetadata.primaryKeyGeneration?.column === columnName) delete tableMetadata.primaryKeyGeneration;
3443
+ if (tableMetadata.timestampColumns) {
3444
+ tableMetadata.timestampColumns = tableMetadata.timestampColumns.filter((column) => column.column !== columnName);
3445
+ if (tableMetadata.timestampColumns.length === 0) delete tableMetadata.timestampColumns;
3446
+ }
3447
+ };
3448
+ const applyPrimaryKeyGeneration = (tableMetadata, column) => {
3449
+ if (!column.primary || !column.primaryKeyGeneration) {
3450
+ if (tableMetadata.primaryKeyGeneration?.column === column.name) delete tableMetadata.primaryKeyGeneration;
3451
+ return;
3452
+ }
3453
+ tableMetadata.primaryKeyGeneration = {
3454
+ column: column.name,
3455
+ ...column.primaryKeyGeneration
3456
+ };
3457
+ };
3458
+ const applyTimestampColumn = (tableMetadata, column) => {
3459
+ if (column.type !== "timestamp" || column.default !== "now()" && column.updatedAt !== true) {
3460
+ if (tableMetadata.timestampColumns) {
3461
+ tableMetadata.timestampColumns = tableMetadata.timestampColumns.filter((entry) => entry.column !== column.name);
3462
+ if (tableMetadata.timestampColumns.length === 0) delete tableMetadata.timestampColumns;
3463
+ }
3464
+ return;
3465
+ }
3466
+ const nextColumn = {
3467
+ column: column.name,
3468
+ ...column.default === "now()" ? { default: "now()" } : {},
3469
+ ...column.updatedAt ? { updatedAt: true } : {}
3470
+ };
3471
+ tableMetadata.timestampColumns = [...(tableMetadata.timestampColumns ?? []).filter((entry) => entry.column !== column.name), nextColumn];
3472
+ };
3473
+ const applyOperationsToPersistedColumnMappingsState = (state, operations, features = resolvePersistedMetadataFeatures()) => {
3474
+ const nextTables = Object.entries(state.tables).reduce((all, [table, metadata]) => {
3475
+ all[table] = {
3476
+ columns: { ...metadata.columns },
3477
+ enums: Object.entries(metadata.enums).reduce((nextEnums, [columnName, values]) => {
3478
+ nextEnums[columnName] = [...values];
3479
+ return nextEnums;
3480
+ }, {}),
3481
+ primaryKeyGeneration: metadata.primaryKeyGeneration ? { ...metadata.primaryKeyGeneration } : void 0,
3482
+ timestampColumns: metadata.timestampColumns?.map((column) => ({ ...column }))
3483
+ };
3484
+ return all;
3485
+ }, {});
3486
+ operations.forEach((operation) => {
3487
+ if (operation.type === "createTable") {
3488
+ const tableMetadata = nextTables[operation.table] ?? {
3489
+ columns: {},
3490
+ enums: {}
3491
+ };
3492
+ operation.columns.forEach((column) => {
3493
+ applyMappedColumn(tableMetadata.columns, column, features, operation.table);
3494
+ applyEnumColumn(tableMetadata.enums, column, features, operation.table);
3495
+ applyPrimaryKeyGeneration(tableMetadata, column);
3496
+ applyTimestampColumn(tableMetadata, column);
3497
+ });
3498
+ if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0 || tableMetadata.primaryKeyGeneration || tableMetadata.timestampColumns?.length) nextTables[operation.table] = tableMetadata;
3499
+ else delete nextTables[operation.table];
3500
+ return;
3501
+ }
3502
+ if (operation.type === "alterTable") {
3503
+ const tableMetadata = nextTables[operation.table] ?? {
3504
+ columns: {},
3505
+ enums: {}
3506
+ };
3507
+ operation.addColumns.forEach((column) => {
3508
+ applyMappedColumn(tableMetadata.columns, column, features, operation.table);
3509
+ applyEnumColumn(tableMetadata.enums, column, features, operation.table);
3510
+ applyPrimaryKeyGeneration(tableMetadata, column);
3511
+ applyTimestampColumn(tableMetadata, column);
3512
+ });
3513
+ operation.dropColumns.forEach((columnName) => {
3514
+ removePersistedColumnMetadata(tableMetadata, columnName);
3515
+ });
3516
+ if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0 || tableMetadata.primaryKeyGeneration || tableMetadata.timestampColumns?.length) nextTables[operation.table] = tableMetadata;
3517
+ else delete nextTables[operation.table];
3518
+ return;
3519
+ }
3520
+ delete nextTables[operation.table];
3521
+ });
3522
+ return {
3523
+ version: 1,
3524
+ tables: nextTables
3525
+ };
3526
+ };
3527
+ const rebuildPersistedColumnMappingsState = async (state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
3528
+ const availableByIdentity = new Map(availableMigrations.map(([migrationClass, file]) => [buildMigrationIdentity(file, migrationClass.name), migrationClass]));
3529
+ let nextState = createEmptyPersistedColumnMappingsState();
3530
+ const orderedMigrations = state.migrations.map((migration, index) => ({
3531
+ migration,
3532
+ index
3533
+ })).sort((left, right) => {
3534
+ const appliedAtOrder = left.migration.appliedAt.localeCompare(right.migration.appliedAt);
3535
+ if (appliedAtOrder !== 0) return appliedAtOrder;
3536
+ return left.index - right.index;
3537
+ });
3538
+ for (const { migration } of orderedMigrations) {
3539
+ const migrationClass = availableByIdentity.get(migration.id);
3540
+ if (!migrationClass) throw new ArkormException(`Unable to rebuild persisted column mappings because migration [${migration.id}] could not be resolved from the current migration files.`, {
3541
+ operation: "migration.columnMappings",
3542
+ meta: {
3543
+ migrationId: migration.id,
3544
+ file: migration.file,
3545
+ className: migration.className
3546
+ }
3547
+ });
3548
+ const operations = await getMigrationPlan(migrationClass, "up", { inert: true });
3549
+ nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
3550
+ }
3551
+ return nextState;
3552
+ };
3553
+ const syncPersistedColumnMappingsFromState = async (cwd, state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
3554
+ const filePath = resolveColumnMappingsFilePath(cwd);
3555
+ const nextState = await rebuildPersistedColumnMappingsState(state, availableMigrations, features);
3556
+ if (Object.keys(nextState.tables).length === 0) {
3557
+ deletePersistedColumnMappingsState(filePath);
3558
+ return;
3559
+ }
3560
+ writePersistedColumnMappingsState(filePath, nextState);
3561
+ };
3562
+ const validatePersistedMetadataFeaturesForMigrations = async (migrations, features = resolvePersistedMetadataFeatures()) => {
3563
+ let nextState = createEmptyPersistedColumnMappingsState();
3564
+ for (const [migrationClass] of migrations) {
3565
+ const operations = await getMigrationPlan(migrationClass, "up", { inert: true });
3566
+ nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
3567
+ }
3568
+ };
3569
+ const getPersistedEnumTsType = (values) => {
3570
+ return buildEnumUnionType(values);
3571
+ };
3572
+
3573
+ //#endregion
3574
+ //#region src/relationship/RelationTableLoader.ts
3575
+ /**
3576
+ * Utility class responsible for loading data from relation tables, which are used to
3577
+ * manage relationships between models in Arkorm.
3578
+ *
3579
+ * @author Legacy (3m1n3nc3)
3580
+ * @since 2.0.0-next.0
3581
+ */
3582
+ var RelationTableLoader = class {
3583
+ constructor(adapter) {
3584
+ this.adapter = adapter;
3585
+ }
3586
+ buildTarget(table) {
3587
+ return {
3588
+ table,
3589
+ columns: getPersistedTableMetadata(table, { features: resolvePersistedMetadataFeatures(getUserConfig("features")) }).columns
3590
+ };
3591
+ }
3592
+ async selectRows(spec) {
3593
+ return await this.adapter.select({
3594
+ target: this.buildTarget(spec.table),
3595
+ where: spec.where,
3596
+ columns: spec.columns,
3597
+ orderBy: spec.orderBy,
3598
+ limit: spec.limit,
3599
+ offset: spec.offset
3600
+ });
3601
+ }
3602
+ async selectRow(spec) {
3603
+ return await this.adapter.selectOne({
3604
+ target: this.buildTarget(spec.table),
3605
+ where: spec.where,
3606
+ columns: spec.columns,
3607
+ orderBy: spec.orderBy,
3608
+ limit: spec.limit ?? 1,
3609
+ offset: spec.offset
3610
+ });
3611
+ }
3612
+ async selectColumnValues(spec) {
3613
+ return (await this.selectRows({
3614
+ ...spec.lookup,
3615
+ columns: [{ column: spec.column }]
3616
+ })).map((row) => row[spec.column]);
3617
+ }
3618
+ async selectColumnValue(spec) {
3619
+ return (await this.selectRow({
3620
+ ...spec.lookup,
3621
+ columns: [{ column: spec.column }],
3622
+ limit: 1
3623
+ }))?.[spec.column] ?? null;
3624
+ }
3625
+ };
3626
+
3627
+ //#endregion
3628
+ //#region src/relationship/SetBasedEagerLoader.ts
3629
+ /**
3630
+ * Utility class responsible for performing set-based eager loading of relationships for
3631
+ * a collection of models.
3632
+ *
3633
+ * @author Legacy (3m1n3nc3)
3634
+ * @since 2.0.0-next.2
3635
+ */
3636
+ var SetBasedEagerLoader = class SetBasedEagerLoader {
3637
+ constructor(models, relations) {
3638
+ this.models = models;
3639
+ this.relations = relations;
3640
+ }
3641
+ /**
3642
+ * Performs eager loading of all specified relationships for the set of models.
3643
+ *
3644
+ * @returns
3645
+ */
3646
+ async load() {
3647
+ if (this.models.length === 0) return;
3648
+ const relationTree = this.buildRelationTree(this.relations);
3649
+ await Promise.all(Array.from(relationTree.entries()).map(async ([name, node]) => {
3650
+ await this.loadRelationNode(name, node);
3651
+ }));
3652
+ }
3653
+ async loadRelationNode(name, node) {
3654
+ await this.loadRelation(name, node.constraint);
3655
+ if (node.children.size === 0) return;
3656
+ const relatedModels = this.collectLoadedRelationModels(name);
3657
+ if (relatedModels.length === 0) return;
3658
+ await new SetBasedEagerLoader(relatedModels, this.relationTreeToMap(node.children)).load();
3659
+ }
3660
+ /**
3661
+ * Loads a specific relationship for the set of models based on the relationship name
3662
+ * and an optional constraint.
3663
+ *
3664
+ * @param name The name of the relationship to load.
3665
+ * @param constraint An optional constraint to apply to the query.
3666
+ * @returns A promise that resolves when the relationship is loaded.
3667
+ */
3668
+ async loadRelation(name, constraint) {
3669
+ const resolver = this.resolveRelationResolver(name);
3670
+ if (!resolver) return;
3671
+ const metadata = resolver.call(this.models[0]).getMetadata();
3672
+ switch (metadata.type) {
3673
+ case "belongsTo":
3674
+ await this.loadBelongsTo(name, resolver, metadata, constraint);
3675
+ return;
3676
+ case "belongsToMany":
3677
+ await this.loadBelongsToMany(name, metadata, constraint);
3678
+ return;
3679
+ case "hasMany":
3680
+ await this.loadHasMany(name, metadata, constraint);
3681
+ return;
3682
+ case "hasOne":
3683
+ await this.loadHasOne(name, resolver, metadata, constraint);
3684
+ return;
3685
+ case "hasManyThrough":
3686
+ await this.loadHasManyThrough(name, metadata, constraint);
3687
+ return;
3688
+ case "hasOneThrough":
3689
+ await this.loadHasOneThrough(name, resolver, metadata, constraint);
3690
+ return;
3691
+ case "morphTo":
3692
+ await this.loadMorphTo(name, metadata, constraint);
3693
+ return;
3694
+ case "morphOne":
3695
+ await this.loadMorphOne(name, resolver, metadata, constraint);
3696
+ return;
3697
+ case "morphMany":
3698
+ await this.loadMorphMany(name, metadata, constraint);
3699
+ return;
3700
+ case "morphToMany":
3701
+ case "morphedByMany":
3702
+ await this.loadPolymorphicManyToMany(name, metadata, constraint);
3703
+ return;
3704
+ default: await this.loadIndividually(name, resolver, constraint);
3705
+ }
3706
+ }
3707
+ /**
3708
+ * Resolves the relation resolver function for a given relationship name by inspecting
3709
+ * the first model in the set.
3710
+ *
3711
+ * @param name The name of the relationship to resolve.
3712
+ * @returns The relation resolver function or null if not found.
3713
+ */
3714
+ resolveRelationResolver(name) {
3715
+ const resolver = this.models[0][name];
3716
+ if (typeof resolver !== "function") {
3717
+ const modelName = this.models[0].constructor?.name ?? "Model";
3718
+ throw new RelationResolutionException(`Relation [${name}] is not defined on the model.`, {
3719
+ operation: "eagerLoad",
3720
+ model: modelName,
3721
+ relation: name
3722
+ });
3723
+ }
3724
+ return resolver;
3725
+ }
3726
+ buildRelationTree(relations) {
3727
+ const tree = /* @__PURE__ */ new Map();
3728
+ Object.entries(relations).forEach(([path, constraint]) => {
3729
+ const segments = path.split(".").map((segment) => segment.trim()).filter((segment) => segment.length > 0);
3730
+ if (segments.length === 0) return;
3731
+ let current = tree;
3732
+ segments.forEach((segment, index) => {
3733
+ const existing = current.get(segment) ?? {
3734
+ constraint: void 0,
3735
+ children: /* @__PURE__ */ new Map()
3736
+ };
3737
+ if (index === segments.length - 1 && constraint) existing.constraint = constraint;
3738
+ current.set(segment, existing);
3739
+ current = existing.children;
3740
+ });
3741
+ });
3742
+ return tree;
3743
+ }
3744
+ relationTreeToMap(tree, prefix = "") {
3745
+ return Array.from(tree.entries()).reduce((all, [name, node]) => {
3746
+ const path = prefix ? `${prefix}.${name}` : name;
3747
+ all[path] = node.constraint;
3748
+ Object.assign(all, this.relationTreeToMap(node.children, path));
3749
+ return all;
3750
+ }, {});
3751
+ }
3752
+ collectLoadedRelationModels(name) {
3753
+ return this.models.reduce((all, model) => {
3754
+ const loaded = model.getAttribute(name);
3755
+ if (loaded instanceof ArkormCollection) {
3756
+ loaded.all().forEach((item) => {
3757
+ if (this.isEagerLoadableModel(item)) all.push(item);
3758
+ });
3759
+ return all;
3760
+ }
3761
+ if (this.isEagerLoadableModel(loaded)) all.push(loaded);
3762
+ return all;
3763
+ }, []);
3764
+ }
3765
+ isEagerLoadableModel(value) {
3766
+ return typeof value === "object" && value !== null && typeof value.getAttribute === "function" && typeof value.setLoadedRelation === "function";
3767
+ }
3768
+ /**
3769
+ * Loads a "belongs to" relationship for the set of models.
3770
+ *
3771
+ * @param name The name of the relationship to load.
3772
+ * @param resolver The relation resolver function.
3773
+ * @param metadata The metadata for the relationship.
3774
+ * @param constraint An optional constraint to apply to the query.
3775
+ * @returns A promise that resolves when the relationship is loaded.
3776
+ */
3777
+ async loadBelongsTo(name, resolver, metadata, constraint) {
3778
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.foreignKey));
3779
+ if (keys.length === 0) {
3780
+ this.models.forEach((model) => {
3781
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
3782
+ });
3783
+ return;
3784
+ }
3785
+ let query = metadata.relatedModel.query().whereIn(metadata.ownerKey, keys);
3786
+ query = this.applyConstraint(query, constraint);
3787
+ const relatedModels = await this.getPartitionedModels(query, metadata.ownerKey, keys);
3788
+ const relatedByOwnerKey = /* @__PURE__ */ new Map();
3789
+ relatedModels.forEach((related) => {
3790
+ const value = this.readModelAttribute(related, metadata.ownerKey);
3791
+ if (value == null) return;
3792
+ const lookupKey = this.toLookupKey(value);
3793
+ if (!relatedByOwnerKey.has(lookupKey)) relatedByOwnerKey.set(lookupKey, related);
3794
+ });
3795
+ this.models.forEach((model) => {
3796
+ const foreignValue = model.getAttribute(metadata.foreignKey);
3797
+ const relationValue = foreignValue == null ? void 0 : relatedByOwnerKey.get(this.toLookupKey(foreignValue));
3798
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
3799
+ });
3800
+ }
3801
+ /**
3802
+ * Loads a "has many" relationship for the set of models.
3803
+ *
3804
+ * @param name
3805
+ * @param metadata
3806
+ * @param constraint
3807
+ * @returns
3808
+ */
3809
+ async loadHasMany(name, metadata, constraint) {
3810
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
3811
+ if (keys.length === 0) {
3812
+ this.models.forEach((model) => {
3813
+ model.setLoadedRelation(name, new ArkormCollection([]));
3814
+ });
3815
+ return;
3816
+ }
3817
+ let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
3818
+ query = this.applyConstraint(query, constraint);
3819
+ const relatedModels = await this.getPartitionedModels(query, metadata.foreignKey, keys);
3820
+ const relatedByForeignKey = /* @__PURE__ */ new Map();
3821
+ relatedModels.forEach((related) => {
3822
+ const value = this.readModelAttribute(related, metadata.foreignKey);
3823
+ if (value == null) return;
3824
+ const lookupKey = this.toLookupKey(value);
3825
+ const bucket = relatedByForeignKey.get(lookupKey) ?? [];
3826
+ bucket.push(related);
3827
+ relatedByForeignKey.set(lookupKey, bucket);
3828
+ });
3829
+ this.models.forEach((model) => {
3830
+ const localValue = model.getAttribute(metadata.localKey);
3831
+ const related = localValue == null ? [] : relatedByForeignKey.get(this.toLookupKey(localValue)) ?? [];
3832
+ model.setLoadedRelation(name, new ArkormCollection(related));
3833
+ });
3834
+ }
3835
+ /**
3836
+ * Loads a "belongs to many" relationship for the set of models.
3837
+ *
3838
+ * @param name
3839
+ * @param metadata
3840
+ * @param constraint
3841
+ * @returns
3842
+ */
3843
+ async loadBelongsToMany(name, metadata, constraint) {
3844
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.parentKey));
3845
+ if (parentKeys.length === 0) {
3846
+ this.models.forEach((model) => {
3847
+ model.setLoadedRelation(name, new ArkormCollection([]));
3848
+ });
3849
+ return;
3850
+ }
3851
+ const pivotRows = await this.createRelationTableLoader().selectRows({
3852
+ table: metadata.throughTable,
3853
+ where: this.buildBelongsToManyPivotWhere(metadata, parentKeys),
3854
+ columns: this.getBelongsToManyPivotColumns(metadata).map((column) => ({ column }))
3855
+ });
3856
+ const relatedIds = this.collectUniqueRowValues(pivotRows, metadata.relatedPivotKey);
3857
+ if (relatedIds.length === 0) {
3858
+ this.models.forEach((model) => {
3859
+ model.setLoadedRelation(name, new ArkormCollection([]));
3860
+ });
3861
+ return;
3862
+ }
3863
+ let query = metadata.relatedModel.query().whereIn(metadata.relatedKey, relatedIds);
3864
+ query = this.applyConstraint(query, constraint);
3865
+ if (query.hasEagerLoadPagination() && parentKeys.length > 1 && await this.loadLimitedBelongsToMany(name, metadata, query, parentKeys, pivotRows)) return;
3866
+ const relatedModels = (await query.get()).all();
3867
+ const relatedByKey = /* @__PURE__ */ new Map();
3868
+ relatedModels.forEach((related) => {
3869
+ const relatedValue = this.readModelAttribute(related, metadata.relatedKey);
3870
+ if (relatedValue == null) return;
3871
+ relatedByKey.set(this.toLookupKey(relatedValue), related);
3872
+ });
3873
+ const relatedKeysByParent = /* @__PURE__ */ new Map();
3874
+ const pivotByParentAndRelated = /* @__PURE__ */ new Map();
3875
+ pivotRows.forEach((row) => {
3876
+ const parentValue = row[metadata.foreignPivotKey];
3877
+ const relatedValue = row[metadata.relatedPivotKey];
3878
+ if (parentValue == null || relatedValue == null) return;
3879
+ const bucket = relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? [];
3880
+ bucket.push(relatedValue);
3881
+ relatedKeysByParent.set(this.toLookupKey(parentValue), bucket);
3882
+ pivotByParentAndRelated.set(`${this.toLookupKey(parentValue)}:${this.toLookupKey(relatedValue)}`, row);
3883
+ });
3884
+ this.models.forEach((model) => {
3885
+ const parentValue = model.getAttribute(metadata.parentKey);
3886
+ const related = (parentValue == null ? [] : relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? []).reduce((all, relatedValue) => {
3887
+ const candidate = relatedByKey.get(this.toLookupKey(relatedValue));
3888
+ if (candidate) all.push(this.attachBelongsToManyPivot(metadata, candidate, pivotByParentAndRelated.get(`${this.toLookupKey(parentValue)}:${this.toLookupKey(relatedValue)}`)));
3889
+ return all;
3890
+ }, []);
3891
+ model.setLoadedRelation(name, new ArkormCollection(related));
3892
+ });
3893
+ }
3894
+ async loadLimitedBelongsToMany(name, metadata, query, parentKeys, pivotRows) {
3895
+ if (metadata.pivotWhere) {
3896
+ await this.loadLimitedBelongsToManyIndividually(name, metadata, query, pivotRows);
3897
+ return true;
3898
+ }
3899
+ const pivotColumns = this.getBelongsToManyPivotColumns(metadata);
3900
+ const pivotAliases = new Map(pivotColumns.map((column, index) => [column, `__arkorm_pivot_${index}`]));
3901
+ const parentAlias = pivotAliases.get(metadata.foreignPivotKey);
3902
+ const throughTable = metadata.throughTable;
3903
+ const relatedTable = metadata.relatedModel.getTable();
3904
+ const rows = await query.clone().innerJoin(throughTable, `${relatedTable}.${metadata.relatedModel.getColumnName(metadata.relatedKey)}`, "=", `${throughTable}.${metadata.relatedPivotKey}`).whereIn(`${throughTable}.${metadata.foreignPivotKey}`, parentKeys).addSelect({ ...Object.fromEntries([...pivotAliases].map(([column, alias]) => [`${throughTable}.${column}`, alias])) }).getRowsForEagerLoad([`${throughTable}.${metadata.foreignPivotKey}`]);
3905
+ if (!rows) {
3906
+ await this.loadLimitedBelongsToManyIndividually(name, metadata, query, pivotRows);
3907
+ return true;
3908
+ }
3909
+ const selectedIds = this.collectUniqueRowValues(rows, metadata.relatedKey);
3910
+ const relatedModels = (await query.clone().withoutPagination().whereIn(metadata.relatedKey, selectedIds).get()).all();
3911
+ const relatedByKey = /* @__PURE__ */ new Map();
3912
+ relatedModels.forEach((related) => {
3913
+ const key = this.readModelAttribute(related, metadata.relatedKey);
3914
+ if (key != null) relatedByKey.set(this.toLookupKey(key), related);
3915
+ });
3916
+ const relatedByParent = /* @__PURE__ */ new Map();
3917
+ rows.forEach((row) => {
3918
+ const parentKey = row[parentAlias];
3919
+ const relatedKey = row[metadata.relatedKey];
3920
+ if (parentKey == null || relatedKey == null) return;
3921
+ const related = relatedByKey.get(this.toLookupKey(relatedKey));
3922
+ if (!related) return;
3923
+ const pivot = Object.fromEntries([...pivotAliases].map(([column, alias]) => [column, row[alias]]));
3924
+ const bucket = relatedByParent.get(this.toLookupKey(parentKey)) ?? [];
3925
+ bucket.push(this.attachBelongsToManyPivot(metadata, related, pivot));
3926
+ relatedByParent.set(this.toLookupKey(parentKey), bucket);
3927
+ });
3928
+ this.models.forEach((model) => {
3929
+ const parentKey = model.getAttribute(metadata.parentKey);
3930
+ const related = parentKey == null ? [] : relatedByParent.get(this.toLookupKey(parentKey)) ?? [];
3931
+ model.setLoadedRelation(name, new ArkormCollection(related));
3932
+ });
3933
+ return true;
3934
+ }
3935
+ async loadLimitedBelongsToManyIndividually(name, metadata, query, pivotRows) {
3936
+ const rowsByParent = /* @__PURE__ */ new Map();
3937
+ pivotRows.forEach((row) => {
3938
+ const parentKey = row[metadata.foreignPivotKey];
3939
+ if (parentKey == null) return;
3940
+ const bucket = rowsByParent.get(this.toLookupKey(parentKey)) ?? [];
3941
+ bucket.push(row);
3942
+ rowsByParent.set(this.toLookupKey(parentKey), bucket);
3943
+ });
3944
+ await Promise.all(this.models.map(async (model) => {
3945
+ const parentKey = model.getAttribute(metadata.parentKey);
3946
+ const rows = parentKey == null ? [] : rowsByParent.get(this.toLookupKey(parentKey)) ?? [];
3947
+ const relatedIds = this.collectUniqueRowValues(rows, metadata.relatedPivotKey);
3948
+ const related = relatedIds.length ? (await query.clone().whereIn(metadata.relatedKey, relatedIds).get()).all() : [];
3949
+ const pivotByRelated = new Map(rows.map((row) => [this.toLookupKey(row[metadata.relatedPivotKey]), row]));
3950
+ model.setLoadedRelation(name, new ArkormCollection(related.map((entry) => this.attachBelongsToManyPivot(metadata, entry, pivotByRelated.get(this.toLookupKey(this.readModelAttribute(entry, metadata.relatedKey)))))));
3951
+ }));
3952
+ }
3953
+ buildBelongsToManyPivotWhere(metadata, parentKeys) {
3954
+ const baseCondition = {
3955
+ type: "comparison",
3956
+ column: metadata.foreignPivotKey,
3957
+ operator: "in",
3958
+ value: parentKeys
3959
+ };
3960
+ if (!metadata.pivotWhere) return baseCondition;
3961
+ return {
3962
+ type: "group",
3963
+ operator: "and",
3964
+ conditions: [baseCondition, metadata.pivotWhere]
3965
+ };
3966
+ }
3967
+ getBelongsToManyPivotColumns(metadata) {
3968
+ return [
3969
+ metadata.foreignPivotKey,
3970
+ metadata.relatedPivotKey,
3971
+ ...metadata.pivotColumns ?? []
3972
+ ].filter((column, index, all) => all.indexOf(column) === index);
3973
+ }
3974
+ shouldAttachBelongsToManyPivot(metadata) {
3975
+ return Boolean(metadata.pivotModel) || Boolean(metadata.pivotCreatedAtColumn) || Boolean(metadata.pivotUpdatedAtColumn) || (metadata.pivotColumns?.length ?? 0) > 0 || Boolean(metadata.pivotAccessor);
3976
+ }
3977
+ createBelongsToManyPivotRecord(metadata, row) {
3978
+ const attributes = this.getBelongsToManyPivotColumns(metadata).reduce((all, column) => {
3979
+ all[column] = row[column];
3660
3980
  return all;
3661
- }, {})
3662
- };
3663
- };
3664
- const buildPersistedFeatureDisabledError = (feature, table) => {
3665
- 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.*.`, {
3666
- operation: "metadata.persisted",
3667
- meta: {
3668
- feature,
3669
- table
3981
+ }, {});
3982
+ if (!metadata.pivotModel) return attributes;
3983
+ if (typeof metadata.pivotModel.hydrate === "function") return metadata.pivotModel.hydrate(attributes);
3984
+ return new metadata.pivotModel(attributes);
3985
+ }
3986
+ attachBelongsToManyPivot(metadata, related, row) {
3987
+ if (!row || !this.shouldAttachBelongsToManyPivot(metadata)) return related;
3988
+ const rawReader = related;
3989
+ if (typeof rawReader.getRawAttributes !== "function" || typeof rawReader.setAttribute !== "function") return related;
3990
+ const cloned = metadata.relatedModel.hydrate(rawReader.getRawAttributes());
3991
+ cloned.setAttribute(metadata.pivotAccessor ?? "pivot", this.createBelongsToManyPivotRecord(metadata, row));
3992
+ return cloned;
3993
+ }
3994
+ /**
3995
+ * Loads a "belongs to many" relationship for the set of models.
3996
+ *
3997
+ * @param name
3998
+ * @param resolver
3999
+ * @param metadata
4000
+ * @param constraint
4001
+ * @returns
4002
+ */
4003
+ async loadHasOne(name, resolver, metadata, constraint) {
4004
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
4005
+ if (keys.length === 0) {
4006
+ this.models.forEach((model) => {
4007
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
4008
+ });
4009
+ return;
3670
4010
  }
3671
- });
3672
- };
3673
- const assertPersistedTableMetadataEnabled = (table, metadata, features, strict) => {
3674
- if (!strict) return;
3675
- if (!features.persistedColumnMappings && Object.keys(metadata.columns).length > 0) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
3676
- if (!features.persistedEnums && Object.keys(metadata.enums).length > 0) throw buildPersistedFeatureDisabledError("persistedEnums", table);
3677
- };
3678
- const buildEnumUnionType = (values) => {
3679
- return values.map((value) => {
3680
- return `'${value.replace(/'/g, String.raw`\'`)}'`;
3681
- }).join(" | ");
3682
- };
3683
- const resetPersistedColumnMappingsCache = () => {
3684
- cachedColumnMappingsPath = void 0;
3685
- cachedColumnMappingsState = void 0;
3686
- };
3687
- const readPersistedColumnMappingsState = (filePath) => {
3688
- if (cachedColumnMappingsPath === filePath && cachedColumnMappingsState) return cachedColumnMappingsState;
3689
- if (!(0, node_fs.existsSync)(filePath)) {
3690
- const empty = createEmptyPersistedColumnMappingsState();
3691
- cachedColumnMappingsPath = filePath;
3692
- cachedColumnMappingsState = empty;
3693
- return empty;
4011
+ let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
4012
+ query = this.applyConstraint(query, constraint);
4013
+ const relatedModels = await this.getPartitionedModels(query, metadata.foreignKey, keys);
4014
+ const relatedByForeignKey = /* @__PURE__ */ new Map();
4015
+ relatedModels.forEach((related) => {
4016
+ const value = this.readModelAttribute(related, metadata.foreignKey);
4017
+ if (value == null) return;
4018
+ const lookupKey = this.toLookupKey(value);
4019
+ if (!relatedByForeignKey.has(lookupKey)) relatedByForeignKey.set(lookupKey, related);
4020
+ });
4021
+ this.models.forEach((model) => {
4022
+ const localValue = model.getAttribute(metadata.localKey);
4023
+ const relationValue = localValue == null ? void 0 : relatedByForeignKey.get(this.toLookupKey(localValue));
4024
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
4025
+ });
4026
+ }
4027
+ /**
4028
+ * Loads a "has many through" relationship for the set of models.
4029
+ *
4030
+ * @param name
4031
+ * @param metadata
4032
+ * @param constraint
4033
+ * @returns
4034
+ */
4035
+ async loadHasManyThrough(name, metadata, constraint) {
4036
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
4037
+ if (parentKeys.length === 0) {
4038
+ this.models.forEach((model) => {
4039
+ model.setLoadedRelation(name, new ArkormCollection([]));
4040
+ });
4041
+ return;
4042
+ }
4043
+ const throughRows = await this.createRelationTableLoader().selectRows({
4044
+ table: metadata.throughTable,
4045
+ where: {
4046
+ type: "comparison",
4047
+ column: metadata.firstKey,
4048
+ operator: "in",
4049
+ value: parentKeys
4050
+ }
4051
+ });
4052
+ const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
4053
+ if (intermediateKeys.length === 0) {
4054
+ this.models.forEach((model) => {
4055
+ model.setLoadedRelation(name, new ArkormCollection([]));
4056
+ });
4057
+ return;
4058
+ }
4059
+ let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
4060
+ query = this.applyConstraint(query, constraint);
4061
+ if (query.hasEagerLoadPagination() && parentKeys.length > 1) {
4062
+ const limited = await this.loadLimitedThrough(metadata, query, parentKeys, throughRows);
4063
+ this.models.forEach((model) => {
4064
+ const parentKey = model.getAttribute(metadata.localKey);
4065
+ const related = parentKey == null ? [] : limited.get(this.toLookupKey(parentKey)) ?? [];
4066
+ model.setLoadedRelation(name, new ArkormCollection(related));
4067
+ });
4068
+ return;
4069
+ }
4070
+ const relatedModels = (await query.get()).all();
4071
+ const relatedByIntermediate = /* @__PURE__ */ new Map();
4072
+ relatedModels.forEach((related) => {
4073
+ const relatedValue = this.readModelAttribute(related, metadata.secondKey);
4074
+ if (relatedValue == null) return;
4075
+ const bucket = relatedByIntermediate.get(this.toLookupKey(relatedValue)) ?? [];
4076
+ bucket.push(related);
4077
+ relatedByIntermediate.set(this.toLookupKey(relatedValue), bucket);
4078
+ });
4079
+ const intermediateByParent = /* @__PURE__ */ new Map();
4080
+ throughRows.forEach((row) => {
4081
+ const parentValue = row[metadata.firstKey];
4082
+ const intermediateValue = row[metadata.secondLocalKey];
4083
+ if (parentValue == null || intermediateValue == null) return;
4084
+ const bucket = intermediateByParent.get(this.toLookupKey(parentValue)) ?? [];
4085
+ bucket.push(intermediateValue);
4086
+ intermediateByParent.set(this.toLookupKey(parentValue), bucket);
4087
+ });
4088
+ this.models.forEach((model) => {
4089
+ const parentValue = model.getAttribute(metadata.localKey);
4090
+ const related = (parentValue == null ? [] : intermediateByParent.get(this.toLookupKey(parentValue)) ?? []).flatMap((intermediateValue) => relatedByIntermediate.get(this.toLookupKey(intermediateValue)) ?? []);
4091
+ model.setLoadedRelation(name, new ArkormCollection(related));
4092
+ });
3694
4093
  }
3695
- try {
3696
- const normalized = normalizePersistedColumnMappingsState(JSON.parse((0, node_fs.readFileSync)(filePath, "utf-8")));
3697
- cachedColumnMappingsPath = filePath;
3698
- cachedColumnMappingsState = normalized;
3699
- return normalized;
3700
- } catch {
3701
- const empty = createEmptyPersistedColumnMappingsState();
3702
- cachedColumnMappingsPath = filePath;
3703
- cachedColumnMappingsState = empty;
3704
- return empty;
4094
+ /**
4095
+ * Loads a "has one through" relationship for the set of models.
4096
+ *
4097
+ * @param name
4098
+ * @param resolver
4099
+ * @param metadata
4100
+ * @param constraint
4101
+ * @returns
4102
+ */
4103
+ async loadHasOneThrough(name, resolver, metadata, constraint) {
4104
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
4105
+ if (parentKeys.length === 0) {
4106
+ this.models.forEach((model) => {
4107
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
4108
+ });
4109
+ return;
4110
+ }
4111
+ const throughRows = await this.createRelationTableLoader().selectRows({
4112
+ table: metadata.throughTable,
4113
+ where: {
4114
+ type: "comparison",
4115
+ column: metadata.firstKey,
4116
+ operator: "in",
4117
+ value: parentKeys
4118
+ }
4119
+ });
4120
+ const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
4121
+ if (intermediateKeys.length === 0) {
4122
+ this.models.forEach((model) => {
4123
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
4124
+ });
4125
+ return;
4126
+ }
4127
+ let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
4128
+ query = this.applyConstraint(query, constraint);
4129
+ if (query.hasEagerLoadPagination() && parentKeys.length > 1) {
4130
+ const limited = await this.loadLimitedThrough(metadata, query, parentKeys, throughRows);
4131
+ this.models.forEach((model) => {
4132
+ const parentKey = model.getAttribute(metadata.localKey);
4133
+ const related = parentKey == null ? void 0 : limited.get(this.toLookupKey(parentKey))?.[0];
4134
+ model.setLoadedRelation(name, related ?? this.resolveSingleDefault(resolver, model));
4135
+ });
4136
+ return;
4137
+ }
4138
+ const relatedModels = (await query.get()).all();
4139
+ const relatedByIntermediate = /* @__PURE__ */ new Map();
4140
+ relatedModels.forEach((related) => {
4141
+ const relatedValue = this.readModelAttribute(related, metadata.secondKey);
4142
+ if (relatedValue == null) return;
4143
+ const lookupKey = this.toLookupKey(relatedValue);
4144
+ if (!relatedByIntermediate.has(lookupKey)) relatedByIntermediate.set(lookupKey, related);
4145
+ });
4146
+ const intermediateByParent = /* @__PURE__ */ new Map();
4147
+ throughRows.forEach((row) => {
4148
+ const parentValue = row[metadata.firstKey];
4149
+ const intermediateValue = row[metadata.secondLocalKey];
4150
+ if (parentValue == null || intermediateValue == null) return;
4151
+ const lookupKey = this.toLookupKey(parentValue);
4152
+ if (!intermediateByParent.has(lookupKey)) intermediateByParent.set(lookupKey, intermediateValue);
4153
+ });
4154
+ this.models.forEach((model) => {
4155
+ const parentValue = model.getAttribute(metadata.localKey);
4156
+ const intermediateValue = parentValue == null ? void 0 : intermediateByParent.get(this.toLookupKey(parentValue));
4157
+ const relationValue = intermediateValue == null ? void 0 : relatedByIntermediate.get(this.toLookupKey(intermediateValue));
4158
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
4159
+ });
3705
4160
  }
3706
- };
3707
- const writePersistedColumnMappingsState = (filePath, state) => {
3708
- const normalized = normalizePersistedColumnMappingsState(state);
3709
- const directory = (0, node_path.dirname)(filePath);
3710
- if (!(0, node_fs.existsSync)(directory)) (0, node_fs.mkdirSync)(directory, { recursive: true });
3711
- (0, node_fs.writeFileSync)(filePath, JSON.stringify(normalized, null, 2));
3712
- cachedColumnMappingsPath = filePath;
3713
- cachedColumnMappingsState = normalized;
3714
- };
3715
- const deletePersistedColumnMappingsState = (filePath) => {
3716
- if ((0, node_fs.existsSync)(filePath)) (0, node_fs.rmSync)(filePath, { force: true });
3717
- resetPersistedColumnMappingsCache();
3718
- };
3719
- const getPersistedTableMetadata = (table, options = {}) => {
3720
- const metadata = readPersistedColumnMappingsState(resolveColumnMappingsFilePath(options.cwd ?? process.cwd(), options.configuredPath)).tables[table] ?? {
3721
- columns: {},
3722
- enums: {}
3723
- };
3724
- assertPersistedTableMetadataEnabled(table, metadata, options.features ?? resolvePersistedMetadataFeatures(), options.strict ?? false);
3725
- return {
3726
- columns: { ...metadata.columns },
3727
- enums: Object.entries(metadata.enums).reduce((all, [columnName, values]) => {
3728
- all[columnName] = [...values];
4161
+ async loadLimitedThrough(metadata, query, parentKeys, throughRows) {
4162
+ const parentAlias = "__arkorm_parent_key";
4163
+ const throughTable = metadata.throughTable;
4164
+ const relatedTable = metadata.relatedModel.getTable();
4165
+ const rows = await query.clone().innerJoin(throughTable, `${relatedTable}.${metadata.relatedModel.getColumnName(metadata.secondKey)}`, "=", `${throughTable}.${metadata.secondLocalKey}`).whereIn(`${throughTable}.${metadata.firstKey}`, parentKeys).addSelect({ [`${throughTable}.${metadata.firstKey}`]: parentAlias }).getRowsForEagerLoad([`${throughTable}.${metadata.firstKey}`]);
4166
+ if (!rows) return await this.loadLimitedThroughIndividually(metadata, query, throughRows);
4167
+ const relatedKey = metadata.relatedModel.getPrimaryKey();
4168
+ const selectedIds = this.collectUniqueRowValues(rows, relatedKey);
4169
+ const relatedModels = (await query.clone().withoutPagination().whereIn(relatedKey, selectedIds).get()).all();
4170
+ const relatedByKey = /* @__PURE__ */ new Map();
4171
+ relatedModels.forEach((related) => {
4172
+ const key = this.readModelAttribute(related, relatedKey);
4173
+ if (key != null) relatedByKey.set(this.toLookupKey(key), related);
4174
+ });
4175
+ return rows.reduce((all, row) => {
4176
+ const parentKey = row[parentAlias];
4177
+ const key = row[relatedKey];
4178
+ if (parentKey == null || key == null) return all;
4179
+ const related = relatedByKey.get(this.toLookupKey(key));
4180
+ if (!related) return all;
4181
+ const lookup = this.toLookupKey(parentKey);
4182
+ const bucket = all.get(lookup) ?? [];
4183
+ bucket.push(related);
4184
+ all.set(lookup, bucket);
3729
4185
  return all;
3730
- }, {}),
3731
- primaryKeyGeneration: metadata.primaryKeyGeneration ? { ...metadata.primaryKeyGeneration } : void 0,
3732
- timestampColumns: metadata.timestampColumns?.map((column) => ({ ...column }))
3733
- };
3734
- };
3735
- const getPersistedColumnMap = (table, options = {}) => {
3736
- return getPersistedTableMetadata(table, options).columns;
3737
- };
3738
- const getPersistedEnumMap = (table, options = {}) => {
3739
- return getPersistedTableMetadata(table, options).enums;
3740
- };
3741
- const getPersistedPrimaryKeyGeneration = (table, options = {}) => {
3742
- return getPersistedTableMetadata(table, options).primaryKeyGeneration;
3743
- };
3744
- const getPersistedTimestampColumns = (table, options = {}) => {
3745
- return getPersistedTableMetadata(table, options).timestampColumns ?? [];
3746
- };
3747
- const applyMappedColumn = (tableColumns, column, features, table) => {
3748
- if (typeof column.map === "string" && column.map.trim().length > 0 && column.map !== column.name) {
3749
- if (!features.persistedColumnMappings) throw buildPersistedFeatureDisabledError("persistedColumnMappings", table);
3750
- tableColumns[column.name] = column.map;
3751
- return;
4186
+ }, /* @__PURE__ */ new Map());
3752
4187
  }
3753
- delete tableColumns[column.name];
3754
- };
3755
- const applyEnumColumn = (tableEnums, column, features, table) => {
3756
- const values = column.enumValues ?? [];
3757
- if (column.type === "enum" && values.length > 0) {
3758
- if (!features.persistedEnums) throw buildPersistedFeatureDisabledError("persistedEnums", table);
3759
- tableEnums[column.name] = [...values];
3760
- return;
4188
+ async loadLimitedThroughIndividually(metadata, query, throughRows) {
4189
+ const intermediateByParent = /* @__PURE__ */ new Map();
4190
+ throughRows.forEach((row) => {
4191
+ const parentKey = row[metadata.firstKey];
4192
+ const intermediateKey = row[metadata.secondLocalKey];
4193
+ if (parentKey == null || intermediateKey == null) return;
4194
+ const lookup = this.toLookupKey(parentKey);
4195
+ const bucket = intermediateByParent.get(lookup) ?? [];
4196
+ bucket.push(intermediateKey);
4197
+ intermediateByParent.set(lookup, bucket);
4198
+ });
4199
+ const entries = await Promise.all([...intermediateByParent].map(async ([parentKey, intermediateKeys]) => {
4200
+ return [parentKey, (await query.clone().whereIn(metadata.secondKey, intermediateKeys).get()).all()];
4201
+ }));
4202
+ return new Map(entries);
3761
4203
  }
3762
- delete tableEnums[column.name];
3763
- };
3764
- const removePersistedColumnMetadata = (tableMetadata, columnName) => {
3765
- delete tableMetadata.columns[columnName];
3766
- delete tableMetadata.enums[columnName];
3767
- Object.entries(tableMetadata.columns).forEach(([attribute, mappedColumn]) => {
3768
- if (mappedColumn === columnName) delete tableMetadata.columns[attribute];
3769
- });
3770
- if (tableMetadata.primaryKeyGeneration?.column === columnName) delete tableMetadata.primaryKeyGeneration;
3771
- if (tableMetadata.timestampColumns) {
3772
- tableMetadata.timestampColumns = tableMetadata.timestampColumns.filter((column) => column.column !== columnName);
3773
- if (tableMetadata.timestampColumns.length === 0) delete tableMetadata.timestampColumns;
4204
+ /**
4205
+ * Loads an inverse polymorphic ("morph to") relationship for the set of
4206
+ * models. Parents are grouped by their morph type, so each distinct type
4207
+ * runs a single batched query instead of one query per parent row.
4208
+ *
4209
+ * @param name
4210
+ * @param metadata
4211
+ * @param constraint
4212
+ * @returns
4213
+ */
4214
+ async loadMorphTo(name, metadata, constraint) {
4215
+ const idsByType = /* @__PURE__ */ new Map();
4216
+ const seenByType = /* @__PURE__ */ new Map();
4217
+ this.models.forEach((model) => {
4218
+ const morphType = this.readModelAttribute(model, metadata.morphTypeColumn);
4219
+ const morphId = this.readModelAttribute(model, metadata.morphIdColumn);
4220
+ if (typeof morphType !== "string" || morphType.length === 0 || morphId == null) return;
4221
+ const ids = idsByType.get(morphType) ?? [];
4222
+ const seen = seenByType.get(morphType) ?? /* @__PURE__ */ new Set();
4223
+ const lookupKey = this.toLookupKey(morphId);
4224
+ if (!seen.has(lookupKey)) {
4225
+ seen.add(lookupKey);
4226
+ ids.push(morphId);
4227
+ }
4228
+ idsByType.set(morphType, ids);
4229
+ seenByType.set(morphType, seen);
4230
+ });
4231
+ const relatedByTypeAndKey = /* @__PURE__ */ new Map();
4232
+ await Promise.all(Array.from(idsByType.entries()).map(async ([morphType, ids]) => {
4233
+ const relatedModel = metadata.resolveModel(morphType);
4234
+ const ownerKey = metadata.ownerKey ?? relatedModel.getPrimaryKey();
4235
+ let query = relatedModel.query().whereIn(ownerKey, ids);
4236
+ query = this.applyConstraint(query, constraint);
4237
+ (await this.getPartitionedModels(query, ownerKey, ids)).forEach((related) => {
4238
+ const value = this.readModelAttribute(related, ownerKey);
4239
+ if (value == null) return;
4240
+ relatedByTypeAndKey.set(`${morphType}:${this.toLookupKey(value)}`, related);
4241
+ });
4242
+ }));
4243
+ this.models.forEach((model) => {
4244
+ const morphType = this.readModelAttribute(model, metadata.morphTypeColumn);
4245
+ const morphId = this.readModelAttribute(model, metadata.morphIdColumn);
4246
+ const relationValue = typeof morphType !== "string" || morphId == null ? null : relatedByTypeAndKey.get(`${morphType}:${this.toLookupKey(morphId)}`) ?? null;
4247
+ model.setLoadedRelation(name, relationValue);
4248
+ });
3774
4249
  }
3775
- };
3776
- const applyPrimaryKeyGeneration = (tableMetadata, column) => {
3777
- if (!column.primary || !column.primaryKeyGeneration) {
3778
- if (tableMetadata.primaryKeyGeneration?.column === column.name) delete tableMetadata.primaryKeyGeneration;
3779
- return;
4250
+ /**
4251
+ * Loads a polymorphic one-to-one ("morph one") relationship for the set of
4252
+ * models. See {@link loadMorphChildren} for the batching strategy.
4253
+ *
4254
+ * @param name
4255
+ * @param resolver
4256
+ * @param metadata
4257
+ * @param constraint
4258
+ */
4259
+ async loadMorphOne(name, resolver, metadata, constraint) {
4260
+ const relatedByKey = await this.loadMorphChildren(metadata, constraint);
4261
+ this.models.forEach((model) => {
4262
+ const bucket = relatedByKey.get(this.morphParentKey(model, metadata.localKey));
4263
+ model.setLoadedRelation(name, bucket?.[0] ?? this.resolveSingleDefault(resolver, model));
4264
+ });
3780
4265
  }
3781
- tableMetadata.primaryKeyGeneration = {
3782
- column: column.name,
3783
- ...column.primaryKeyGeneration
3784
- };
3785
- };
3786
- const applyTimestampColumn = (tableMetadata, column) => {
3787
- if (column.type !== "timestamp" || column.default !== "now()" && column.updatedAt !== true) {
3788
- if (tableMetadata.timestampColumns) {
3789
- tableMetadata.timestampColumns = tableMetadata.timestampColumns.filter((entry) => entry.column !== column.name);
3790
- if (tableMetadata.timestampColumns.length === 0) delete tableMetadata.timestampColumns;
3791
- }
3792
- return;
4266
+ /**
4267
+ * Loads a polymorphic one-to-many ("morph many") relationship for the set of
4268
+ * models. See {@link loadMorphChildren} for the batching strategy.
4269
+ *
4270
+ * @param name
4271
+ * @param metadata
4272
+ * @param constraint
4273
+ */
4274
+ async loadMorphMany(name, metadata, constraint) {
4275
+ const relatedByKey = await this.loadMorphChildren(metadata, constraint);
4276
+ this.models.forEach((model) => {
4277
+ const bucket = relatedByKey.get(this.morphParentKey(model, metadata.localKey)) ?? [];
4278
+ model.setLoadedRelation(name, new ArkormCollection(bucket));
4279
+ });
3793
4280
  }
3794
- const nextColumn = {
3795
- column: column.name,
3796
- ...column.default === "now()" ? { default: "now()" } : {},
3797
- ...column.updatedAt ? { updatedAt: true } : {}
3798
- };
3799
- tableMetadata.timestampColumns = [...(tableMetadata.timestampColumns ?? []).filter((entry) => entry.column !== column.name), nextColumn];
3800
- };
3801
- const applyOperationsToPersistedColumnMappingsState = (state, operations, features = resolvePersistedMetadataFeatures()) => {
3802
- const nextTables = Object.entries(state.tables).reduce((all, [table, metadata]) => {
3803
- all[table] = {
3804
- columns: { ...metadata.columns },
3805
- enums: Object.entries(metadata.enums).reduce((nextEnums, [columnName, values]) => {
3806
- nextEnums[columnName] = [...values];
3807
- return nextEnums;
3808
- }, {}),
3809
- primaryKeyGeneration: metadata.primaryKeyGeneration ? { ...metadata.primaryKeyGeneration } : void 0,
3810
- timestampColumns: metadata.timestampColumns?.map((column) => ({ ...column }))
3811
- };
3812
- return all;
3813
- }, {});
3814
- operations.forEach((operation) => {
3815
- if (operation.type === "createTable") {
3816
- const tableMetadata = nextTables[operation.table] ?? {
3817
- columns: {},
3818
- enums: {}
3819
- };
3820
- operation.columns.forEach((column) => {
3821
- applyMappedColumn(tableMetadata.columns, column, features, operation.table);
3822
- applyEnumColumn(tableMetadata.enums, column, features, operation.table);
3823
- applyPrimaryKeyGeneration(tableMetadata, column);
3824
- applyTimestampColumn(tableMetadata, column);
3825
- });
3826
- if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0 || tableMetadata.primaryKeyGeneration || tableMetadata.timestampColumns?.length) nextTables[operation.table] = tableMetadata;
3827
- else delete nextTables[operation.table];
4281
+ async loadPolymorphicManyToMany(name, metadata, constraint) {
4282
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.parentKey));
4283
+ if (parentKeys.length === 0) {
4284
+ this.models.forEach((model) => model.setLoadedRelation(name, new ArkormCollection([])));
3828
4285
  return;
3829
4286
  }
3830
- if (operation.type === "alterTable") {
3831
- const tableMetadata = nextTables[operation.table] ?? {
3832
- columns: {},
3833
- enums: {}
3834
- };
3835
- operation.addColumns.forEach((column) => {
3836
- applyMappedColumn(tableMetadata.columns, column, features, operation.table);
3837
- applyEnumColumn(tableMetadata.enums, column, features, operation.table);
3838
- applyPrimaryKeyGeneration(tableMetadata, column);
3839
- applyTimestampColumn(tableMetadata, column);
3840
- });
3841
- operation.dropColumns.forEach((columnName) => {
3842
- removePersistedColumnMetadata(tableMetadata, columnName);
3843
- });
3844
- if (Object.keys(tableMetadata.columns).length > 0 || Object.keys(tableMetadata.enums).length > 0 || tableMetadata.primaryKeyGeneration || tableMetadata.timestampColumns?.length) nextTables[operation.table] = tableMetadata;
3845
- else delete nextTables[operation.table];
4287
+ const parentPivotKey = metadata.type === "morphToMany" ? metadata.morphIdColumn : metadata.foreignPivotKey;
4288
+ const morphType = metadata.type === "morphToMany" ? this.morphTypeOf(this.models[0]) : metadata.relatedModel.name;
4289
+ const pivotRows = await this.createRelationTableLoader().selectRows({
4290
+ table: metadata.throughTable,
4291
+ where: {
4292
+ type: "group",
4293
+ operator: "and",
4294
+ conditions: [{
4295
+ type: "comparison",
4296
+ column: parentPivotKey,
4297
+ operator: "in",
4298
+ value: parentKeys
4299
+ }, {
4300
+ type: "comparison",
4301
+ column: metadata.morphTypeColumn,
4302
+ operator: "=",
4303
+ value: morphType
4304
+ }]
4305
+ },
4306
+ columns: [parentPivotKey, metadata.relatedPivotKey].map((column) => ({ column }))
4307
+ });
4308
+ const relatedIds = this.collectUniqueRowValues(pivotRows, metadata.relatedPivotKey);
4309
+ if (relatedIds.length === 0) {
4310
+ this.models.forEach((model) => model.setLoadedRelation(name, new ArkormCollection([])));
3846
4311
  return;
3847
4312
  }
3848
- delete nextTables[operation.table];
3849
- });
3850
- return {
3851
- version: 1,
3852
- tables: nextTables
3853
- };
3854
- };
3855
- const rebuildPersistedColumnMappingsState = async (state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
3856
- const availableByIdentity = new Map(availableMigrations.map(([migrationClass, file]) => [buildMigrationIdentity(file, migrationClass.name), migrationClass]));
3857
- let nextState = createEmptyPersistedColumnMappingsState();
3858
- const orderedMigrations = state.migrations.map((migration, index) => ({
3859
- migration,
3860
- index
3861
- })).sort((left, right) => {
3862
- const appliedAtOrder = left.migration.appliedAt.localeCompare(right.migration.appliedAt);
3863
- if (appliedAtOrder !== 0) return appliedAtOrder;
3864
- return left.index - right.index;
3865
- });
3866
- for (const { migration } of orderedMigrations) {
3867
- const migrationClass = availableByIdentity.get(migration.id);
3868
- if (!migrationClass) throw new ArkormException(`Unable to rebuild persisted column mappings because migration [${migration.id}] could not be resolved from the current migration files.`, {
3869
- operation: "migration.columnMappings",
3870
- meta: {
3871
- migrationId: migration.id,
3872
- file: migration.file,
3873
- className: migration.className
4313
+ let query = metadata.relatedModel.query().whereIn(metadata.relatedKey, relatedIds);
4314
+ query = this.applyConstraint(query, constraint);
4315
+ let pairs = pivotRows.map((row) => ({
4316
+ parent: row[parentPivotKey],
4317
+ related: row[metadata.relatedPivotKey]
4318
+ }));
4319
+ let relatedModels;
4320
+ if (query.hasEagerLoadPagination() && parentKeys.length > 1) {
4321
+ const parentAlias = "__arkorm_parent_key";
4322
+ const throughTable = metadata.throughTable;
4323
+ const relatedTable = metadata.relatedModel.getTable();
4324
+ const rows = await query.clone().innerJoin(throughTable, `${relatedTable}.${metadata.relatedModel.getColumnName(metadata.relatedKey)}`, "=", `${throughTable}.${metadata.relatedPivotKey}`).whereIn(`${throughTable}.${parentPivotKey}`, parentKeys).where({ [`${throughTable}.${metadata.morphTypeColumn}`]: morphType }).addSelect({ [`${throughTable}.${parentPivotKey}`]: parentAlias }).getRowsForEagerLoad([`${throughTable}.${parentPivotKey}`]);
4325
+ if (rows) {
4326
+ pairs = rows.map((row) => ({
4327
+ parent: row[parentAlias],
4328
+ related: row[metadata.relatedKey]
4329
+ }));
4330
+ const selectedIds = this.collectUniqueRowValues(rows, metadata.relatedKey);
4331
+ relatedModels = (await query.clone().withoutPagination().whereIn(metadata.relatedKey, selectedIds).get()).all();
4332
+ } else {
4333
+ await this.loadPolymorphicManyToManyIndividually(name, metadata, query, parentPivotKey, pivotRows);
4334
+ return;
3874
4335
  }
4336
+ } else relatedModels = (await query.get()).all();
4337
+ const relatedByKey = /* @__PURE__ */ new Map();
4338
+ relatedModels.forEach((related) => {
4339
+ const key = this.readModelAttribute(related, metadata.relatedKey);
4340
+ if (key != null) relatedByKey.set(this.toLookupKey(key), related);
4341
+ });
4342
+ const relatedByParent = /* @__PURE__ */ new Map();
4343
+ pairs.forEach(({ parent, related }) => {
4344
+ if (parent == null || related == null) return;
4345
+ const model = relatedByKey.get(this.toLookupKey(related));
4346
+ if (!model) return;
4347
+ const lookup = this.toLookupKey(parent);
4348
+ const bucket = relatedByParent.get(lookup) ?? [];
4349
+ bucket.push(model);
4350
+ relatedByParent.set(lookup, bucket);
4351
+ });
4352
+ this.models.forEach((model) => {
4353
+ const parentKey = model.getAttribute(metadata.parentKey);
4354
+ const related = parentKey == null ? [] : relatedByParent.get(this.toLookupKey(parentKey)) ?? [];
4355
+ model.setLoadedRelation(name, new ArkormCollection(related));
3875
4356
  });
3876
- const operations = await getMigrationPlan(migrationClass, "up", { inert: true });
3877
- nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
3878
4357
  }
3879
- return nextState;
3880
- };
3881
- const syncPersistedColumnMappingsFromState = async (cwd, state, availableMigrations, features = resolvePersistedMetadataFeatures()) => {
3882
- const filePath = resolveColumnMappingsFilePath(cwd);
3883
- const nextState = await rebuildPersistedColumnMappingsState(state, availableMigrations, features);
3884
- if (Object.keys(nextState.tables).length === 0) {
3885
- deletePersistedColumnMappingsState(filePath);
3886
- return;
4358
+ async loadPolymorphicManyToManyIndividually(name, metadata, query, parentPivotKey, pivotRows) {
4359
+ await Promise.all(this.models.map(async (model) => {
4360
+ const parentKey = model.getAttribute(metadata.parentKey);
4361
+ const ids = pivotRows.filter((row) => this.toLookupKey(row[parentPivotKey]) === this.toLookupKey(parentKey)).map((row) => row[metadata.relatedPivotKey]);
4362
+ const related = ids.length ? (await query.clone().whereIn(metadata.relatedKey, ids).get()).all() : [];
4363
+ model.setLoadedRelation(name, new ArkormCollection(related));
4364
+ }));
4365
+ }
4366
+ /**
4367
+ * Batch-loads the children for a morphOne/morphMany relationship. Parents are
4368
+ * grouped by their own morph type (constructor name), so each distinct type
4369
+ * runs a single query constrained by `idColumn IN (...) AND typeColumn = type`
4370
+ * instead of one query per parent row. This also supports a heterogeneous
4371
+ * parent set (e.g. the result of a nested load after a morphTo).
4372
+ *
4373
+ * @param metadata
4374
+ * @param constraint
4375
+ * @returns A map of `"{type}:{localKey}" -> related[]`.
4376
+ */
4377
+ async loadMorphChildren(metadata, constraint) {
4378
+ const keysByType = /* @__PURE__ */ new Map();
4379
+ const seenByType = /* @__PURE__ */ new Map();
4380
+ this.models.forEach((model) => {
4381
+ const morphType = this.morphTypeOf(model);
4382
+ const localValue = model.getAttribute(metadata.localKey);
4383
+ if (morphType.length === 0 || localValue == null) return;
4384
+ const keys = keysByType.get(morphType) ?? [];
4385
+ const seen = seenByType.get(morphType) ?? /* @__PURE__ */ new Set();
4386
+ const lookupKey = this.toLookupKey(localValue);
4387
+ if (!seen.has(lookupKey)) {
4388
+ seen.add(lookupKey);
4389
+ keys.push(localValue);
4390
+ }
4391
+ keysByType.set(morphType, keys);
4392
+ seenByType.set(morphType, seen);
4393
+ });
4394
+ const relatedByKey = /* @__PURE__ */ new Map();
4395
+ await Promise.all(Array.from(keysByType.entries()).map(async ([morphType, keys]) => {
4396
+ let query = metadata.relatedModel.query().whereIn(metadata.morphIdColumn, keys).where({ [metadata.morphTypeColumn]: morphType });
4397
+ query = this.applyConstraint(query, constraint);
4398
+ (await this.getPartitionedModels(query, metadata.morphIdColumn, keys)).forEach((related) => {
4399
+ const value = this.readModelAttribute(related, metadata.morphIdColumn);
4400
+ if (value == null) return;
4401
+ const key = `${morphType}:${this.toLookupKey(value)}`;
4402
+ const bucket = relatedByKey.get(key) ?? [];
4403
+ bucket.push(related);
4404
+ relatedByKey.set(key, bucket);
4405
+ });
4406
+ }));
4407
+ return relatedByKey;
4408
+ }
4409
+ morphParentKey(model, localKey) {
4410
+ return `${this.morphTypeOf(model)}:${this.toLookupKey(model.getAttribute(localKey))}`;
3887
4411
  }
3888
- writePersistedColumnMappingsState(filePath, nextState);
3889
- };
3890
- const validatePersistedMetadataFeaturesForMigrations = async (migrations, features = resolvePersistedMetadataFeatures()) => {
3891
- let nextState = createEmptyPersistedColumnMappingsState();
3892
- for (const [migrationClass] of migrations) {
3893
- const operations = await getMigrationPlan(migrationClass, "up", { inert: true });
3894
- nextState = applyOperationsToPersistedColumnMappingsState(nextState, operations, features);
4412
+ morphTypeOf(model) {
4413
+ return model.constructor?.name ?? "";
3895
4414
  }
3896
- };
3897
- const getPersistedEnumTsType = (values) => {
3898
- return buildEnumUnionType(values);
3899
- };
3900
-
3901
- //#endregion
3902
- //#region src/helpers/runtime-registry.ts
3903
- const createEmptyRegistry = () => ({
3904
- paths: {
3905
- models: [],
3906
- seeders: [],
3907
- migrations: [],
3908
- factories: []
3909
- },
3910
- migrations: [],
3911
- seeders: [],
3912
- models: [],
3913
- factories: []
3914
- });
3915
- const registry = createEmptyRegistry();
3916
- const pushUnique = (items, values) => {
3917
- values.forEach((value) => {
3918
- if (!items.includes(value)) items.push(value);
3919
- });
3920
- };
3921
- const normalizePathInput = (paths) => {
3922
- if (paths === void 0) return [];
3923
- return (Array.isArray(paths) ? paths : [paths]).filter((path) => typeof path === "string" && path.trim().length > 0);
3924
- };
3925
- const normalizeConstructors = (items) => items.flatMap((item) => Array.isArray(item) ? item : [item]).filter(Boolean);
3926
- /**
3927
- * Register additional runtime discovery paths without replacing configured paths.
3928
- *
3929
- * @param paths
3930
- */
3931
- const registerPaths = (paths) => {
3932
- Object.entries(paths).forEach(([key, value]) => {
3933
- pushUnique(registry.paths[key], normalizePathInput(value));
3934
- });
3935
- };
3936
- /**
3937
- * Register additional runtime discovery paths for migrations without replacing configured paths.
3938
- *
3939
- * @param paths
3940
- * @returns
3941
- */
3942
- const loadMigrationsFrom = (paths) => registerPaths({ migrations: paths });
3943
- /**
3944
- * Register additional runtime discovery paths for seeders without replacing configured paths.
3945
- *
3946
- * @param paths
3947
- * @returns
3948
- */
3949
- const loadSeedersFrom = (paths) => registerPaths({ seeders: paths });
3950
- /**
3951
- * Register additional runtime discovery paths for models without replacing configured paths.
3952
- *
3953
- * @param paths
3954
- * @returns
3955
- */
3956
- const loadModelsFrom = (paths) => registerPaths({ models: paths });
3957
- /**
3958
- * Register additional runtime discovery paths for factories without replacing configured paths.
3959
- *
3960
- * @param paths
3961
- * @returns
3962
- */
3963
- const loadFactoriesFrom = (paths) => registerPaths({ factories: paths });
3964
- /**
3965
- * Register migration constructors directly without relying on runtime discovery.
3966
- *
3967
- * @param migrations
3968
- */
3969
- const registerMigrations = (...migrations) => {
3970
- pushUnique(registry.migrations, normalizeConstructors(migrations));
3971
- };
3972
- /**
3973
- * Register seeder constructors directly without relying on runtime discovery.
3974
- *
3975
- * @param seeders
3976
- */
3977
- const registerSeeders = (...seeders) => {
3978
- pushUnique(registry.seeders, normalizeConstructors(seeders));
3979
- };
3980
- /**
3981
- * Register model constructors directly without relying on runtime discovery.
3982
- *
3983
- * @param models
3984
- */
3985
- const registerModels = (...models) => {
3986
- pushUnique(registry.models, normalizeConstructors(models));
3987
- };
3988
- /**
3989
- * Register factory constructors or instances directly without relying on runtime discovery.
3990
- *
3991
- * @param factories
3992
- */
3993
- const registerFactories = (...factories) => {
3994
- pushUnique(registry.factories, normalizeConstructors(factories));
3995
- };
3996
- /**
3997
- * Get registered runtime discovery paths or registered constructors for a specific type.
3998
- *
3999
- * @param key
4000
- * @returns
4001
- */
4002
- const getRegisteredPaths = (key) => {
4003
- if (key) return [...registry.paths[key]];
4004
- return {
4005
- models: [...registry.paths.models],
4006
- seeders: [...registry.paths.seeders],
4007
- migrations: [...registry.paths.migrations],
4008
- factories: [...registry.paths.factories]
4009
- };
4010
- };
4011
- /**
4012
- * Get registered migration constructors instances.
4013
- *
4014
- * @returns
4015
- */
4016
- const getRegisteredMigrations = () => [...registry.migrations];
4017
- /**
4018
- * Get registered seeder constructors instances.
4019
- *
4020
- * @returns
4021
- */
4022
- const getRegisteredSeeders = () => [...registry.seeders];
4023
- /**
4024
- * Get registered model constructors instances.
4025
- *
4026
- * @returns
4027
- */
4028
- const getRegisteredModels = () => [...registry.models];
4029
- /**
4030
- * Get registered factory constructors or instances.
4031
- *
4032
- * @returns
4033
- */
4034
- const getRegisteredFactories = () => [...registry.factories];
4035
- const resetRuntimeRegistryForTests = () => {
4036
- const empty = createEmptyRegistry();
4037
- registry.paths = empty.paths;
4038
- registry.migrations = empty.migrations;
4039
- registry.seeders = empty.seeders;
4040
- registry.models = empty.models;
4041
- registry.factories = empty.factories;
4042
- };
4043
-
4044
- //#endregion
4045
- //#region src/helpers/runtime-config.ts
4046
- const resolveDefaultStubsPath = () => {
4047
- let current = path.default.dirname((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
4048
- while (true) {
4049
- const packageJsonPath = path.default.join(current, "package.json");
4050
- const stubsPath = path.default.join(current, "stubs");
4051
- if ((0, fs.existsSync)(packageJsonPath) && (0, fs.existsSync)(stubsPath)) return stubsPath;
4052
- const parent = path.default.dirname(current);
4053
- if (parent === current) break;
4054
- current = parent;
4415
+ /**
4416
+ * Fallback method to load relationships individually for each model when the
4417
+ * relationship type is not supported for set-based loading.
4418
+ *
4419
+ * @param name
4420
+ * @param resolver
4421
+ * @param constraint
4422
+ */
4423
+ async loadIndividually(name, resolver, constraint) {
4424
+ await Promise.all(this.models.map(async (model) => {
4425
+ const relation = resolver.call(model);
4426
+ if (constraint) relation.constrain(constraint);
4427
+ model.setLoadedRelation(name, await relation.getResults());
4428
+ }));
4055
4429
  }
4056
- return path.default.join(process.cwd(), "stubs");
4057
- };
4058
- const baseConfig = {
4059
- naming: { case: "snake" },
4060
- features: {
4061
- persistedColumnMappings: true,
4062
- persistedEnums: true
4063
- },
4064
- paths: {
4065
- stubs: resolveDefaultStubsPath(),
4066
- seeders: path.default.join(process.cwd(), "database", "seeders"),
4067
- models: path.default.join(process.cwd(), "src", "models"),
4068
- migrations: path.default.join(process.cwd(), "database", "migrations"),
4069
- factories: path.default.join(process.cwd(), "database", "factories"),
4070
- buildOutput: path.default.join(process.cwd(), "dist")
4071
- },
4072
- outputExt: "ts"
4073
- };
4074
- const userConfig = {
4075
- ...baseConfig,
4076
- naming: { ...baseConfig.naming ?? {} },
4077
- features: { ...baseConfig.features ?? {} },
4078
- paths: { ...baseConfig.paths ?? {} }
4079
- };
4080
- let runtimeConfigLoaded = false;
4081
- let runtimeConfigLoadingPromise;
4082
- let runtimeModelRegistrationPromise;
4083
- let runtimeModelRegistrationGeneration = 0;
4084
- let runtimeClientResolver;
4085
- let runtimeAdapter;
4086
- let runtimePaginationURLDriverFactory;
4087
- let runtimePaginationCurrentPageResolver;
4088
- let runtimeDebugHandler;
4089
- const transactionClientStorage = new async_hooks.AsyncLocalStorage();
4090
- const transactionAdapterStorage = new async_hooks.AsyncLocalStorage();
4091
- const defaultDebugHandler = (event) => {
4092
- const prefix = `[arkorm:${event.adapter}] ${event.operation}${event.target ? ` [${event.target}]` : ""}`;
4093
- const payload = {
4094
- phase: event.phase,
4095
- durationMs: event.durationMs,
4096
- inspection: event.inspection ?? void 0,
4097
- meta: event.meta,
4098
- error: event.error
4099
- };
4100
- if (event.phase === "error") {
4101
- console.error(prefix, payload);
4102
- return;
4430
+ /**
4431
+ * Applies an eager load constraint to a query if provided.
4432
+ *
4433
+ * @param query
4434
+ * @param constraint
4435
+ * @returns
4436
+ */
4437
+ applyConstraint(query, constraint) {
4438
+ if (!constraint) return query;
4439
+ return constraint(query) ?? query;
4440
+ }
4441
+ async getPartitionedModels(query, partitionColumn, partitionValues) {
4442
+ if (partitionValues.length <= 1) return (await query.get()).all();
4443
+ const partitioned = await query.getForEagerLoad([partitionColumn]);
4444
+ if (partitioned) return partitioned.all();
4445
+ return (await Promise.all(partitionValues.map(async (value) => {
4446
+ return (await query.clone().where({ [partitionColumn]: value }).get()).all();
4447
+ }))).flat();
4448
+ }
4449
+ /**
4450
+ * Collects unique values from the set of models based on a resolver function, which
4451
+ * is used to extract the value from each model.
4452
+ *
4453
+ * @param resolve A function that takes a model and returns the value to be collected.
4454
+ * @returns An array of unique values.
4455
+ */
4456
+ collectUniqueKeys(resolve) {
4457
+ const seen = /* @__PURE__ */ new Set();
4458
+ const values = [];
4459
+ this.models.forEach((model) => {
4460
+ const value = resolve(model);
4461
+ if (value == null) return;
4462
+ const lookupKey = this.toLookupKey(value);
4463
+ if (seen.has(lookupKey)) return;
4464
+ seen.add(lookupKey);
4465
+ values.push(value);
4466
+ });
4467
+ return values;
4103
4468
  }
4104
- console.debug(prefix, payload);
4105
- };
4106
- const resolveDebugHandler = (debug) => {
4107
- if (debug === true) return defaultDebugHandler;
4108
- return typeof debug === "function" ? debug : void 0;
4109
- };
4110
- const mergeNamingConfig = (naming) => {
4111
- const defaults = baseConfig.naming ?? {};
4112
- const current = userConfig.naming ?? {};
4113
- const resolvedCase = naming?.case ?? naming?.modelTableCase ?? current.case ?? current.modelTableCase ?? defaults.case ?? defaults.modelTableCase ?? "snake";
4114
- return {
4115
- ...defaults,
4116
- ...current,
4117
- ...naming ?? {},
4118
- case: resolvedCase
4119
- };
4120
- };
4121
- const mergePathConfig = (paths) => {
4122
- const defaults = baseConfig.paths ?? {};
4123
- const current = userConfig.paths ?? {};
4124
- const incoming = Object.entries(paths ?? {}).reduce((all, [key, value]) => {
4125
- if (typeof value === "string" && value.trim().length > 0) all[key] = path.default.isAbsolute(value) ? value : path.default.resolve(process.cwd(), value);
4126
- return all;
4127
- }, {});
4128
- return {
4129
- ...defaults,
4130
- ...current,
4131
- ...incoming
4132
- };
4133
- };
4134
- /**
4135
- * Merge the feature configuration from the base defaults, user configuration, and provided options.
4136
- *
4137
- * @param features
4138
- * @returns
4139
- */
4140
- const mergeFeatureConfig = (features) => {
4141
- const defaults = baseConfig.features ?? {};
4142
- const current = userConfig.features ?? {};
4143
- return {
4144
- ...defaults,
4145
- ...current,
4146
- ...features ?? {}
4147
- };
4148
- };
4149
- const resolveRuntimeModelsDirectory = (directory) => {
4150
- if ((0, fs.existsSync)(directory)) return directory;
4151
- const buildOutput = userConfig.paths?.buildOutput;
4152
- if (typeof buildOutput !== "string" || buildOutput.trim().length === 0) return directory;
4153
- const relativeSource = path.default.relative(process.cwd(), directory);
4154
- if (!relativeSource || relativeSource.startsWith("..")) return directory;
4155
- const mappedDirectory = path.default.join(buildOutput, relativeSource);
4156
- return (0, fs.existsSync)(mappedDirectory) ? mappedDirectory : directory;
4157
- };
4158
- const collectRuntimeModelFiles = (directory) => {
4159
- if (!(0, fs.existsSync)(directory)) return [];
4160
- return (0, fs.readdirSync)(directory, { withFileTypes: true }).flatMap((entry) => {
4161
- const entryPath = path.default.join(directory, entry.name);
4162
- if (entry.isDirectory()) return collectRuntimeModelFiles(entryPath);
4163
- if (!entry.isFile() || !/\.(ts|tsx|mts|cts|js|mjs|cjs)$/i.test(entry.name)) return [];
4164
- if (/\.d\.(ts|mts|cts)$/i.test(entry.name)) return [];
4165
- return [entryPath];
4166
- });
4167
- };
4168
- const isModelConstructor = (value) => {
4169
- if (typeof value !== "function") return false;
4170
- const candidate = value;
4171
- return typeof candidate.query === "function" && typeof candidate.hydrate === "function" && typeof candidate.getTable === "function" && typeof candidate.getPrimaryKey === "function";
4172
- };
4173
- const registerConfiguredModels = async (generation) => {
4174
- const configured = userConfig.paths?.models;
4175
- const additional = getRegisteredPaths("models");
4176
- const files = [...typeof configured === "string" ? [configured] : [], ...additional].map((directory) => resolveRuntimeModelsDirectory(path.default.isAbsolute(directory) ? directory : path.default.resolve(process.cwd(), directory))).filter((directory, index, all) => all.indexOf(directory) === index).flatMap(collectRuntimeModelFiles);
4177
- const loaded = await RuntimeModuleLoader.loadAll(files);
4178
- loaded.forEach(({ file, error }) => {
4179
- if (error !== void 0) console.warn(`[arkorm] Failed to load model file for registration: ${file}\n ${error instanceof Error ? error.message : String(error)}\n Models in this file will not be registered (morphTo resolution may fail). This is often a circular import — avoid importing models at the top level of trait modules.`);
4180
- });
4181
- const models = loaded.flatMap(({ module }) => module ? Object.values(module).filter(isModelConstructor) : []);
4182
- if (generation === runtimeModelRegistrationGeneration) registerModels(models);
4183
- };
4184
- const scheduleConfiguredModelRegistration = () => {
4185
- const generation = runtimeModelRegistrationGeneration;
4186
- runtimeModelRegistrationPromise = (runtimeModelRegistrationPromise ?? Promise.resolve()).then(async () => await registerConfiguredModels(generation));
4187
- return runtimeModelRegistrationPromise;
4188
- };
4189
- const awaitConfiguredModelsRegistration = async () => {
4190
- if (runtimeModelRegistrationPromise) await runtimeModelRegistrationPromise;
4191
- };
4192
- /**
4193
- * Define the ArkORM runtime configuration. This function can be used to provide.
4194
- *
4195
- * @param config The ArkORM configuration object.
4196
- * @returns The same configuration object.
4197
- */
4198
- const defineConfig = (config) => {
4199
- return config;
4200
- };
4201
- /**
4202
- * Bind a database adapter instance to an array of models that support adapter binding.
4203
- *
4204
- * @param adapter
4205
- * @param models
4206
- * @returns
4207
- */
4208
- const bindAdapterToModels = (adapter, models) => {
4209
- models.forEach((model) => {
4210
- model.setAdapter(adapter);
4211
- });
4212
- return adapter;
4213
- };
4214
- /**
4215
- * Get the user-provided ArkORM configuration.
4216
- *
4217
- * @param key Optional specific configuration key to retrieve. If omitted, the entire configuration object is returned.
4218
- * @returns The user-provided ArkORM configuration object.
4219
- */
4220
- const getUserConfig = (key) => {
4221
- if (key) return userConfig[key];
4222
- return userConfig;
4223
- };
4224
- /**
4225
- * Configure the ArkORM runtime with the provided runtime client resolver and
4226
- * adapter-first options.
4227
- *
4228
- * @param client
4229
- * @param options
4230
- */
4231
- const configureArkormRuntime = (client, options = {}) => {
4232
- const resolvedClient = client ?? options.client;
4233
- const nextConfig = {
4234
- ...userConfig,
4235
- naming: mergeNamingConfig(options.naming),
4236
- features: mergeFeatureConfig(options.features),
4237
- paths: mergePathConfig(options.paths)
4238
- };
4239
- nextConfig.client = resolvedClient;
4240
- nextConfig.prisma = resolvedClient;
4241
- if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
4242
- if (options.adapter !== void 0) nextConfig.adapter = options.adapter;
4243
- if (options.boot !== void 0) nextConfig.boot = options.boot;
4244
- if (options.debug !== void 0) nextConfig.debug = options.debug;
4245
- if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
4246
- Object.assign(userConfig, { ...nextConfig });
4247
- runtimeClientResolver = resolvedClient;
4248
- runtimeAdapter = options.adapter;
4249
- runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
4250
- runtimePaginationCurrentPageResolver = nextConfig.pagination?.resolveCurrentPage;
4251
- runtimeDebugHandler = resolveDebugHandler(nextConfig.debug);
4252
- scheduleConfiguredModelRegistration();
4253
- const bootClient = resolveClient(resolvedClient);
4254
- options.boot?.({
4255
- client: bootClient,
4256
- prisma: bootClient,
4257
- bindAdapter: bindAdapterToModels
4258
- });
4259
- };
4260
- /**
4261
- * Reset the ArkORM runtime configuration.
4262
- * This is primarily intended for testing purposes.
4263
- */
4264
- const resetArkormRuntimeForTests = () => {
4265
- Object.assign(userConfig, {
4266
- ...baseConfig,
4267
- naming: { ...baseConfig.naming ?? {} },
4268
- features: { ...baseConfig.features ?? {} },
4269
- paths: { ...baseConfig.paths ?? {} }
4270
- });
4271
- runtimeConfigLoaded = false;
4272
- runtimeConfigLoadingPromise = void 0;
4273
- runtimeModelRegistrationPromise = void 0;
4274
- runtimeModelRegistrationGeneration++;
4275
- runtimeClientResolver = void 0;
4276
- runtimeAdapter = void 0;
4277
- runtimePaginationURLDriverFactory = void 0;
4278
- runtimePaginationCurrentPageResolver = void 0;
4279
- runtimeDebugHandler = void 0;
4280
- resetPersistedColumnMappingsCache();
4281
- resetRuntimeRegistryForTests();
4282
- };
4283
- /**
4284
- * Resolve a runtime client instance from the provided resolver, which can be either
4285
- * a direct client instance or a function that returns a client instance.
4286
- *
4287
- * @param resolver
4288
- * @returns
4289
- */
4290
- const resolveClient = (resolver) => {
4291
- if (!resolver) return void 0;
4292
- const client = typeof resolver === "function" ? resolver() : resolver;
4293
- if (!client || typeof client !== "object") return void 0;
4294
- return client;
4295
- };
4296
- /**
4297
- * Resolve and apply the ArkORM configuration from an imported module.
4298
- * This function checks for a default export and falls back to the module itself, then validates
4299
- * the configuration object and applies it to the runtime if valid.
4300
- *
4301
- * @param imported
4302
- * @returns
4303
- */
4304
- const resolveAndApplyConfig = (imported) => {
4305
- const config = imported?.default ?? imported;
4306
- if (!config || typeof config !== "object") return;
4307
- const runtimeClient = config.client ?? config.prisma;
4308
- configureArkormRuntime(runtimeClient, {
4309
- client: runtimeClient,
4310
- adapter: config.adapter,
4311
- boot: config.boot,
4312
- debug: config.debug,
4313
- naming: config.naming,
4314
- features: config.features,
4315
- pagination: config.pagination,
4316
- paths: config.paths,
4317
- outputExt: config.outputExt
4318
- });
4319
- runtimeConfigLoaded = true;
4320
- };
4321
- /**
4322
- * Dynamically import a configuration file.
4323
- * A cache-busting query parameter is appended to ensure the latest version is loaded.
4324
- *
4325
- * @param configPath
4326
- * @returns A promise that resolves to the imported configuration module.
4327
- */
4328
- const importConfigFile = (configPath) => {
4329
- return RuntimeModuleLoader.load(configPath);
4330
- };
4331
- const loadRuntimeConfigSync = () => {
4332
- const require = (0, module$1.createRequire)(require("url").pathToFileURL(__filename).href);
4333
- const syncConfigPaths = [path.default.join(process.cwd(), "arkormx.config.cjs")];
4334
- for (const configPath of syncConfigPaths) {
4335
- if (!(0, fs.existsSync)(configPath)) continue;
4469
+ /**
4470
+ * Collects unique values from an array of database rows based on a specified key, which
4471
+ * is used to extract the value from each row.
4472
+ *
4473
+ * @param rows An array of database rows.
4474
+ * @param key The key to extract values from each row.
4475
+ * @returns An array of unique values.
4476
+ */
4477
+ collectUniqueRowValues(rows, key) {
4478
+ const seen = /* @__PURE__ */ new Set();
4479
+ const values = [];
4480
+ rows.forEach((row) => {
4481
+ const value = row[key];
4482
+ if (value == null) return;
4483
+ const lookupKey = this.toLookupKey(value);
4484
+ if (seen.has(lookupKey)) return;
4485
+ seen.add(lookupKey);
4486
+ values.push(value);
4487
+ });
4488
+ return values;
4489
+ }
4490
+ /**
4491
+ * Loads a "belongs to many" relationship for the set of models.
4492
+ *
4493
+ * @returns
4494
+ */
4495
+ createRelationTableLoader() {
4496
+ return new RelationTableLoader(this.resolveAdapter());
4497
+ }
4498
+ /**
4499
+ * Loads a "belongs to many" relationship for the set of models.
4500
+ *
4501
+ * @returns
4502
+ */
4503
+ resolveAdapter() {
4504
+ const adapter = this.models[0].constructor.getAdapter?.();
4505
+ if (!adapter) throw new Error("Set-based eager loading requires a configured adapter.");
4506
+ return adapter;
4507
+ }
4508
+ /**
4509
+ * Reads an attribute value from a model using the getAttribute method, which is used
4510
+ * to access model attributes in a way that is compatible with Arkorm's internal model structure.
4511
+ *
4512
+ * @param model The model to read the attribute from.
4513
+ * @param key The name of the attribute to read.
4514
+ * @returns
4515
+ */
4516
+ readModelAttribute(model, key) {
4517
+ const readable = model;
4518
+ const value = readable.getAttribute?.(key);
4519
+ if (value !== void 0) return value;
4336
4520
  try {
4337
- resolveAndApplyConfig(require(configPath));
4338
- return true;
4521
+ const attribute = Object.entries(readable.constructor?.getColumnMap?.() ?? {}).find(([, column]) => column === key)?.[0];
4522
+ return attribute ? readable.getAttribute?.(attribute) : value;
4339
4523
  } catch {
4340
- continue;
4524
+ return value;
4341
4525
  }
4342
4526
  }
4343
- return false;
4344
- };
4345
- /**
4346
- * Load the ArkORM configuration by searching for configuration files in the
4347
- * current working directory.
4348
- * @returns
4349
- */
4350
- const loadArkormConfig = async () => {
4351
- if (runtimeConfigLoaded) {
4352
- await awaitConfiguredModelsRegistration();
4353
- return;
4527
+ /**
4528
+ * Resolves the default result for a relationship when no related models are found.
4529
+ *
4530
+ * @param resolver
4531
+ * @param model
4532
+ * @returns
4533
+ */
4534
+ resolveSingleDefault(resolver, model) {
4535
+ return resolver.call(model).resolveDefaultResult?.() ?? null;
4354
4536
  }
4355
- if (runtimeConfigLoadingPromise) return await runtimeConfigLoadingPromise;
4356
- if (loadRuntimeConfigSync()) {
4357
- await awaitConfiguredModelsRegistration();
4358
- return;
4537
+ /**
4538
+ * Generates a unique lookup key for a given value, which is used to store and retrieve
4539
+ * values in maps during the eager loading process.
4540
+ *
4541
+ * @param value The value to generate a lookup key for.
4542
+ * @returns A unique string representing the value.
4543
+ */
4544
+ toLookupKey(value) {
4545
+ if (value instanceof Date) return `date:${value.toISOString()}`;
4546
+ return `${typeof value}:${String(value)}`;
4359
4547
  }
4360
- runtimeConfigLoadingPromise = (async () => {
4361
- const configPaths = [path.default.join(process.cwd(), "arkormx.config.js"), path.default.join(process.cwd(), "arkormx.config.ts")];
4362
- for (const configPath of configPaths) {
4363
- if (!(0, fs.existsSync)(configPath)) continue;
4364
- try {
4365
- resolveAndApplyConfig(await importConfigFile(configPath));
4366
- await awaitConfiguredModelsRegistration();
4367
- return;
4368
- } catch {
4369
- continue;
4370
- }
4371
- }
4372
- runtimeConfigLoaded = true;
4373
- await scheduleConfiguredModelRegistration();
4374
- await awaitConfiguredModelsRegistration();
4375
- })();
4376
- await runtimeConfigLoadingPromise;
4377
- };
4378
- /**
4379
- * Ensure that the ArkORM configuration is loaded.
4380
- * This function can be called to trigger the loading process if it hasn't already been initiated.
4381
- * If the configuration is already loaded, it will return immediately.
4382
- *
4383
- * @returns
4384
- */
4385
- const ensureArkormConfigLoading = () => {
4386
- if (runtimeConfigLoaded) return;
4387
- if (!runtimeConfigLoadingPromise) loadArkormConfig();
4388
- };
4389
- const getDefaultStubsPath = () => {
4390
- return resolveDefaultStubsPath();
4391
- };
4392
- /**
4393
- * Get the runtime compatibility client.
4394
- * This function will trigger the loading of the ArkORM configuration if
4395
- * it hasn't already been loaded.
4396
- *
4397
- * @returns
4398
- */
4399
- const getRuntimeClient = () => {
4400
- const activeTransactionClient = transactionClientStorage.getStore();
4401
- if (activeTransactionClient) return activeTransactionClient;
4402
- if (!runtimeConfigLoaded) loadRuntimeConfigSync();
4403
- return resolveClient(runtimeClientResolver);
4404
- };
4405
- /**
4406
- * @deprecated Use getRuntimeClient instead.
4407
- */
4408
- const getRuntimePrismaClient = getRuntimeClient;
4409
- /**
4410
- * Get the currently configured runtime adapter, if any.
4411
- *
4412
- * @returns
4413
- */
4414
- const getRuntimeAdapter = () => {
4415
- const activeTransactionAdapter = transactionAdapterStorage.getStore();
4416
- if (activeTransactionAdapter) return activeTransactionAdapter;
4417
- if (!runtimeConfigLoaded) loadRuntimeConfigSync();
4418
- return runtimeAdapter;
4419
- };
4420
- /**
4421
- * Releases the database resources held by the configured runtime — the adapter's
4422
- * connection pool and/or the configured client. Intended for short-lived
4423
- * processes (the CLI) so the Node event loop drains and the process exits
4424
- * promptly instead of hanging on pool idle timeouts. Teardown failures are
4425
- * swallowed because the process is shutting down anyway.
4426
- */
4427
- const disposeArkormRuntime = async () => {
4428
- let adapterDisposed = false;
4429
- try {
4430
- if (runtimeAdapter && typeof runtimeAdapter.dispose === "function") {
4431
- await runtimeAdapter.dispose();
4432
- adapterDisposed = true;
4433
- }
4434
- } catch {}
4435
- const client = getRuntimeClient();
4436
- if (!client) return;
4437
- try {
4438
- if (typeof client.$disconnect === "function") await client.$disconnect();
4439
- else if (!adapterDisposed && typeof client.destroy === "function") await client.destroy();
4440
- else if (!adapterDisposed && typeof client.end === "function") await client.end();
4441
- } catch {}
4442
- };
4443
- const getActiveTransactionClient = () => {
4444
- return transactionClientStorage.getStore();
4445
- };
4446
- const getActiveTransactionAdapter = () => {
4447
- return transactionAdapterStorage.getStore();
4448
- };
4449
- const isTransactionCapableClient = (value) => {
4450
- if (!value || typeof value !== "object") return false;
4451
- return typeof value.$transaction === "function";
4452
- };
4453
- const runArkormTransaction = async (callback, options = {}, preferredAdapter) => {
4454
- const activeTransactionAdapter = transactionAdapterStorage.getStore();
4455
- const activeTransactionClient = transactionClientStorage.getStore();
4456
- if (activeTransactionAdapter || activeTransactionClient) return await callback({
4457
- adapter: activeTransactionAdapter,
4458
- client: activeTransactionClient
4459
- });
4460
- const adapter = preferredAdapter ?? getRuntimeAdapter();
4461
- if (adapter) return await adapter.transaction(async (transactionAdapter) => {
4462
- return await transactionAdapterStorage.run(transactionAdapter, async () => {
4463
- return await callback({
4464
- adapter: transactionAdapter,
4465
- client: transactionClientStorage.getStore()
4466
- });
4467
- });
4468
- }, options);
4469
- const client = getRuntimeClient();
4470
- if (!client) throw new ArkormException("Cannot start a transaction without a configured runtime client or adapter.", {
4471
- code: "CLIENT_NOT_CONFIGURED",
4472
- operation: "transaction"
4473
- });
4474
- if (!isTransactionCapableClient(client)) throw new UnsupportedAdapterFeatureException("Transactions are not supported by the current adapter.", {
4475
- code: "TRANSACTION_NOT_SUPPORTED",
4476
- operation: "transaction"
4477
- });
4478
- return await client.$transaction(async (transactionClient) => {
4479
- return await transactionClientStorage.run(transactionClient, async () => {
4480
- return await callback({ client: transactionClient });
4481
- });
4482
- }, options);
4483
- };
4484
- /**
4485
- * Get the configured pagination URL driver factory from runtime config.
4486
- *
4487
- * @returns
4488
- */
4489
- const getRuntimePaginationURLDriverFactory = () => {
4490
- if (!runtimeConfigLoaded) loadRuntimeConfigSync();
4491
- return runtimePaginationURLDriverFactory;
4492
- };
4493
- /**
4494
- * Get the configured current-page resolver from runtime config.
4495
- *
4496
- * @returns
4497
- */
4498
- const getRuntimePaginationCurrentPageResolver = () => {
4499
- if (!runtimeConfigLoaded) loadRuntimeConfigSync();
4500
- return runtimePaginationCurrentPageResolver;
4501
- };
4502
- const getRuntimeDebugHandler = () => {
4503
- if (!runtimeConfigLoaded) loadRuntimeConfigSync();
4504
- return runtimeDebugHandler;
4505
- };
4506
- const emitRuntimeDebugEvent = (event) => {
4507
- getRuntimeDebugHandler()?.(event);
4508
- };
4509
- /**
4510
- * Check if a given value matches Arkorm's query-schema contract
4511
- * by verifying the presence of common delegate methods.
4512
- *
4513
- * @param value The value to check.
4514
- * @returns True if the value matches the query-schema contract, false otherwise.
4515
- */
4516
- const isQuerySchemaLike = (value) => {
4517
- if (!value || typeof value !== "object") return false;
4518
- const candidate = value;
4519
- return [
4520
- "findMany",
4521
- "findFirst",
4522
- "create",
4523
- "update",
4524
- "delete",
4525
- "count"
4526
- ].every((method) => typeof candidate[method] === "function");
4527
4548
  };
4528
- /**
4529
- * @deprecated Use isQuerySchemaLike instead.
4530
- */
4531
- const isDelegateLike = isQuerySchemaLike;
4532
- loadArkormConfig();
4533
4549
 
4534
4550
  //#endregion
4535
4551
  //#region src/URLDriver.ts
@@ -7893,6 +7909,12 @@ Object.defineProperty(exports, 'resolvePrismaType', {
7893
7909
  return resolvePrismaType;
7894
7910
  }
7895
7911
  });
7912
+ Object.defineProperty(exports, 'resolveRegisteredModel', {
7913
+ enumerable: true,
7914
+ get: function () {
7915
+ return resolveRegisteredModel;
7916
+ }
7917
+ });
7896
7918
  Object.defineProperty(exports, 'runArkormTransaction', {
7897
7919
  enumerable: true,
7898
7920
  get: function () {