arkormx 2.0.0-next.1 → 2.0.0-next.3

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.
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  }) : target, mod));
27
27
 
28
28
  //#endregion
29
+ let _h3ravel_support = require("@h3ravel/support");
29
30
  let kysely = require("kysely");
30
31
  let async_hooks = require("async_hooks");
31
32
  let _rexxars_jiti = require("@rexxars/jiti");
@@ -36,7 +37,6 @@ let fs = require("fs");
36
37
  let url = require("url");
37
38
  let path = require("path");
38
39
  path = __toESM(path);
39
- let _h3ravel_support = require("@h3ravel/support");
40
40
  let node_fs = require("node:fs");
41
41
  let node_child_process = require("node:child_process");
42
42
  let _h3ravel_shared = require("@h3ravel/shared");
@@ -99,24 +99,223 @@ var UnsupportedAdapterFeatureException = class extends ArkormException {
99
99
 
100
100
  //#endregion
101
101
  //#region src/adapters/KyselyDatabaseAdapter.ts
102
+ /**
103
+ * Database adapter implementation for Kysely, allowing Arkorm to execute queries using Kysely
104
+ * as the underlying query builder and executor.
105
+ *
106
+ * @author Legacy (3m1n3nc3)
107
+ * @since 2.0.0-next.0
108
+ */
102
109
  var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
110
+ static migrationStateTable = "arkormx_migrations";
111
+ static migrationRunTable = "arkormx_migration_runs";
103
112
  capabilities = {
104
113
  transactions: true,
105
114
  returning: true,
106
115
  insertMany: true,
116
+ upsert: true,
107
117
  updateMany: true,
108
118
  deleteMany: true,
109
119
  exists: true,
110
120
  relationLoads: false,
111
- relationAggregates: false,
112
- relationFilters: false,
121
+ relationAggregates: true,
122
+ relationFilters: true,
113
123
  rawWhere: false
114
124
  };
115
- constructor(db) {
125
+ constructor(db, mapping = {}) {
116
126
  this.db = db;
127
+ this.mapping = mapping;
128
+ }
129
+ quoteIdentifier(value) {
130
+ return `"${value.replace(/"/g, "\"\"")}"`;
131
+ }
132
+ quoteLiteral(value) {
133
+ if (value == null) return "null";
134
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
135
+ if (typeof value === "boolean") return value ? "true" : "false";
136
+ if (value instanceof Date) return `'${value.toISOString().replace(/'/g, "''")}'`;
137
+ return `'${String(value).replace(/'/g, "''")}'`;
138
+ }
139
+ async executeRawStatement(statement, executor = this.db) {
140
+ await kysely.sql.raw(statement).execute(executor);
141
+ }
142
+ resolveSchemaColumnName(columnName, columns = []) {
143
+ return columns.find((column) => column.name === columnName)?.map ?? columnName;
144
+ }
145
+ resolveSchemaIndexName(table, index) {
146
+ if (typeof index.name === "string" && index.name.trim().length > 0) return index.name;
147
+ return `${table}_${index.columns.join("_")}_idx`;
148
+ }
149
+ resolveSchemaForeignKeyName(table, foreignKey) {
150
+ return `${table}_${foreignKey.column}_fkey`;
151
+ }
152
+ resolveSchemaColumnType(column) {
153
+ if (column.type === "id") return "integer";
154
+ if (column.type === "uuid") return "uuid";
155
+ if (column.type === "enum") return this.quoteIdentifier(column.enumName ?? `${column.name}_enum`);
156
+ if (column.type === "string") return "varchar(255)";
157
+ if (column.type === "text") return "text";
158
+ if (column.type === "integer") return "integer";
159
+ if (column.type === "bigInteger") return "bigint";
160
+ if (column.type === "float") return "double precision";
161
+ if (column.type === "boolean") return "boolean";
162
+ if (column.type === "json") return "jsonb";
163
+ if (column.type === "date") return "date";
164
+ return "timestamptz";
165
+ }
166
+ resolveSchemaColumnDefault(column) {
167
+ const value = column.default ?? (column.updatedAt ? "now()" : void 0);
168
+ if (value === void 0) return null;
169
+ if (value === "now()") return "now()";
170
+ return this.quoteLiteral(value);
171
+ }
172
+ shouldUseIdentity(column) {
173
+ if (column.autoIncrement === false) return false;
174
+ if (column.type === "id") return column.default === void 0;
175
+ return column.autoIncrement === true;
176
+ }
177
+ buildSchemaColumnDefinition(column) {
178
+ const parts = [this.quoteIdentifier(column.map ?? column.name), this.resolveSchemaColumnType(column)];
179
+ if (this.shouldUseIdentity(column)) parts.push("generated by default as identity");
180
+ const defaultValue = this.resolveSchemaColumnDefault(column);
181
+ if (defaultValue && !this.shouldUseIdentity(column)) parts.push(`default ${defaultValue}`);
182
+ if (column.primary) parts.push("primary key");
183
+ else if (column.unique) parts.push("unique");
184
+ if (!column.nullable && !column.primary) parts.push("not null");
185
+ return parts.join(" ");
186
+ }
187
+ buildSchemaForeignKeyConstraint(table, foreignKey, columns = []) {
188
+ const localColumn = this.resolveSchemaColumnName(foreignKey.column, columns);
189
+ const referencedTable = this.resolveMappedTable(foreignKey.referencesTable);
190
+ const action = foreignKey.onDelete ? ` on delete ${foreignKey.onDelete === "setNull" ? "set null" : foreignKey.onDelete === "setDefault" ? "set default" : foreignKey.onDelete}` : "";
191
+ return `constraint ${this.quoteIdentifier(this.resolveSchemaForeignKeyName(table, foreignKey))} foreign key (${this.quoteIdentifier(localColumn)}) references ${this.quoteIdentifier(referencedTable)} (${this.quoteIdentifier(foreignKey.referencesColumn)})${action}`;
192
+ }
193
+ buildSchemaIndexStatement(table, index, columns = []) {
194
+ const mappedColumns = index.columns.map((column) => this.quoteIdentifier(this.resolveSchemaColumnName(column, columns))).join(", ");
195
+ return `create index if not exists ${this.quoteIdentifier(this.resolveSchemaIndexName(table, index))} on ${this.quoteIdentifier(table)} (${mappedColumns})`;
196
+ }
197
+ async ensureEnumTypes(columns, executor = this.db) {
198
+ for (const column of columns) {
199
+ if (column.type !== "enum") continue;
200
+ const enumName = column.enumName ?? `${column.name}_enum`;
201
+ if ((await kysely.sql`
202
+ select exists(
203
+ select 1
204
+ from pg_type
205
+ where typname = ${enumName}
206
+ ) as exists
207
+ `.execute(executor)).rows[0]?.exists) continue;
208
+ const values = column.enumValues ?? [];
209
+ if (values.length === 0) throw new ArkormException(`Enum column [${column.name}] requires enum values for database-backed migrations.`);
210
+ await this.executeRawStatement(`create type ${this.quoteIdentifier(enumName)} as enum (${values.map((value) => this.quoteLiteral(value)).join(", ")})`, executor);
211
+ }
212
+ }
213
+ async executeCreateTableOperation(operation, executor) {
214
+ const table = this.resolveMappedTable(operation.table);
215
+ await this.ensureEnumTypes(operation.columns, executor);
216
+ const columnDefinitions = operation.columns.map((column) => this.buildSchemaColumnDefinition(column));
217
+ const foreignKeys = (operation.foreignKeys ?? []).map((foreignKey) => this.buildSchemaForeignKeyConstraint(table, foreignKey, operation.columns));
218
+ const definitions = [...columnDefinitions, ...foreignKeys].join(", ");
219
+ await this.executeRawStatement(`create table if not exists ${this.quoteIdentifier(table)} (${definitions})`, executor);
220
+ for (const index of operation.indexes ?? []) await this.executeRawStatement(this.buildSchemaIndexStatement(table, index, operation.columns), executor);
221
+ }
222
+ async executeAlterTableOperation(operation, executor) {
223
+ const table = this.resolveMappedTable(operation.table);
224
+ await this.ensureEnumTypes(operation.addColumns, executor);
225
+ for (const column of operation.addColumns) await this.executeRawStatement(`alter table ${this.quoteIdentifier(table)} add column if not exists ${this.buildSchemaColumnDefinition(column)}`, executor);
226
+ for (const column of operation.dropColumns) await this.executeRawStatement(`alter table ${this.quoteIdentifier(table)} drop column if exists ${this.quoteIdentifier(column)}`, executor);
227
+ for (const foreignKey of operation.addForeignKeys ?? []) await this.executeRawStatement(`alter table ${this.quoteIdentifier(table)} add ${this.buildSchemaForeignKeyConstraint(table, foreignKey, operation.addColumns)}`, executor);
228
+ for (const index of operation.addIndexes ?? []) await this.executeRawStatement(this.buildSchemaIndexStatement(table, index, operation.addColumns), executor);
229
+ }
230
+ async executeDropTableOperation(operation, executor) {
231
+ const table = this.resolveMappedTable(operation.table);
232
+ await this.executeRawStatement(`drop table if exists ${this.quoteIdentifier(table)} cascade`, executor);
233
+ }
234
+ async ensureMigrationStateTables(executor = this.db) {
235
+ await this.executeRawStatement(`
236
+ create table if not exists ${this.quoteIdentifier(KyselyDatabaseAdapter.migrationStateTable)} (
237
+ id text primary key,
238
+ file text not null,
239
+ class_name text not null,
240
+ applied_at timestamptz not null,
241
+ checksum text null
242
+ )
243
+ `, executor);
244
+ await this.executeRawStatement(`
245
+ create table if not exists ${this.quoteIdentifier(KyselyDatabaseAdapter.migrationRunTable)} (
246
+ id text primary key,
247
+ applied_at timestamptz not null,
248
+ migration_ids jsonb not null
249
+ )
250
+ `, executor);
251
+ }
252
+ async writeAppliedMigrationsStateInternal(state, executor) {
253
+ await this.ensureMigrationStateTables(executor);
254
+ await this.executeRawStatement(`delete from ${this.quoteIdentifier(KyselyDatabaseAdapter.migrationRunTable)}`, executor);
255
+ await this.executeRawStatement(`delete from ${this.quoteIdentifier(KyselyDatabaseAdapter.migrationStateTable)}`, executor);
256
+ for (const migration of state.migrations) await kysely.sql`
257
+ insert into ${kysely.sql.table(KyselyDatabaseAdapter.migrationStateTable)} (id, file, class_name, applied_at, checksum)
258
+ values (${migration.id}, ${migration.file}, ${migration.className}, ${migration.appliedAt}, ${migration.checksum ?? null})
259
+ `.execute(executor);
260
+ for (const run of state.runs ?? []) await kysely.sql`
261
+ insert into ${kysely.sql.table(KyselyDatabaseAdapter.migrationRunTable)} (id, applied_at, migration_ids)
262
+ values (${run.id}, ${run.appliedAt}, cast(${JSON.stringify(run.migrationIds)} as jsonb))
263
+ `.execute(executor);
264
+ }
265
+ async resetDatabaseInternal(executor) {
266
+ const tablesResult = await kysely.sql`
267
+ select table_name, table_schema
268
+ from information_schema.tables
269
+ where table_schema = current_schema()
270
+ and table_type = 'BASE TABLE'
271
+ order by table_name asc
272
+ `.execute(executor);
273
+ for (const row of tablesResult.rows) await this.executeRawStatement(`drop table if exists ${this.quoteIdentifier(row.table_schema)}.${this.quoteIdentifier(row.table_name)} cascade`, executor);
274
+ const enumTypesResult = await kysely.sql`
275
+ select t.typname as enum_name, n.nspname as enum_schema
276
+ from pg_type t
277
+ inner join pg_namespace n on n.oid = t.typnamespace
278
+ where t.typtype = 'e'
279
+ and n.nspname = current_schema()
280
+ order by t.typname asc
281
+ `.execute(executor);
282
+ for (const row of enumTypesResult.rows) await this.executeRawStatement(`drop type if exists ${this.quoteIdentifier(row.enum_schema)}.${this.quoteIdentifier(row.enum_name)} cascade`, executor);
283
+ }
284
+ introspectionTypeToTs(typeName, enumValues) {
285
+ if (enumValues && enumValues.length > 0) return enumValues.map((value) => `'${value.replace(/'/g, "\\'")}'`).join(" | ");
286
+ switch (typeName) {
287
+ case "bool": return "boolean";
288
+ case "int2":
289
+ case "int4":
290
+ case "int8":
291
+ case "float4":
292
+ case "float8":
293
+ case "numeric":
294
+ case "money": return "number";
295
+ case "json":
296
+ case "jsonb": return "Record<string, unknown> | unknown[]";
297
+ case "date":
298
+ case "timestamp":
299
+ case "timestamptz": return "Date";
300
+ case "bytea": return "Uint8Array";
301
+ case "uuid":
302
+ case "varchar":
303
+ case "bpchar":
304
+ case "char":
305
+ case "text":
306
+ case "citext":
307
+ case "time":
308
+ case "timetz":
309
+ case "interval":
310
+ case "inet":
311
+ case "cidr":
312
+ case "macaddr":
313
+ case "macaddr8": return "string";
314
+ default: return "unknown";
315
+ }
117
316
  }
118
317
  resolveTable(target) {
119
- if (target.table && target.table.trim().length > 0) return target.table;
318
+ if (target.table && target.table.trim().length > 0) return this.mapping[target.table] ?? target.table;
120
319
  throw new ArkormException("Kysely adapter requires a concrete target table.", {
121
320
  operation: "adapter.table",
122
321
  model: target.modelName,
@@ -225,29 +424,223 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
225
424
  if (clauses.length === 0) return kysely.sql``;
226
425
  return kysely.sql.join(clauses, kysely.sql``);
227
426
  }
427
+ buildColumnReference(table, column) {
428
+ return kysely.sql`${kysely.sql.table(table)}.${kysely.sql.id(column)}`;
429
+ }
430
+ buildRelatedTargetFromRelation(target, relation) {
431
+ const metadata = target.model?.getRelationMetadata(relation);
432
+ if (!metadata) throw new UnsupportedAdapterFeatureException(`Relation [${relation}] could not be resolved for SQL-backed relation execution.`, {
433
+ operation: "adapter.relation.metadata",
434
+ model: target.modelName,
435
+ relation
436
+ });
437
+ if (metadata.type !== "hasMany" && metadata.type !== "hasOne" && metadata.type !== "belongsTo" && metadata.type !== "belongsToMany" && metadata.type !== "hasOneThrough" && metadata.type !== "hasManyThrough") throw new UnsupportedAdapterFeatureException(`Relation [${relation}] is not supported for SQL-backed relation execution by the Kysely adapter yet.`, {
438
+ operation: "adapter.relation.metadata",
439
+ model: target.modelName,
440
+ relation,
441
+ meta: {
442
+ feature: "relationFilters",
443
+ relationType: metadata.type
444
+ }
445
+ });
446
+ const relatedMetadata = metadata.relatedModel.getModelMetadata();
447
+ return {
448
+ metadata,
449
+ relatedTarget: {
450
+ model: metadata.relatedModel,
451
+ modelName: metadata.relatedModel.name,
452
+ table: relatedMetadata.table,
453
+ primaryKey: relatedMetadata.primaryKey,
454
+ columns: relatedMetadata.columns,
455
+ softDelete: relatedMetadata.softDelete
456
+ }
457
+ };
458
+ }
459
+ resolveMappedTable(table) {
460
+ return this.mapping[table] ?? table;
461
+ }
462
+ buildBelongsToManyJoinSource(outerTarget, relatedTarget, metadata) {
463
+ const outerTable = this.resolveTable(outerTarget);
464
+ const relatedTable = this.resolveTable(relatedTarget);
465
+ const pivotTable = this.resolveMappedTable(metadata.throughTable);
466
+ return {
467
+ from: kysely.sql`${kysely.sql.table(relatedTable)} inner join ${kysely.sql.table(pivotTable)} on ${this.buildColumnReference(relatedTable, this.mapColumn(relatedTarget, metadata.relatedKey))} = ${this.buildColumnReference(pivotTable, metadata.relatedPivotKey)}`,
468
+ condition: kysely.sql`
469
+ ${this.buildColumnReference(pivotTable, metadata.foreignPivotKey)}
470
+ =
471
+ ${this.buildColumnReference(outerTable, this.mapColumn(outerTarget, metadata.parentKey))}
472
+ `
473
+ };
474
+ }
475
+ buildThroughJoinSource(outerTarget, relatedTarget, metadata) {
476
+ const outerTable = this.resolveTable(outerTarget);
477
+ const relatedTable = this.resolveTable(relatedTarget);
478
+ const throughTable = this.resolveMappedTable(metadata.throughTable);
479
+ return {
480
+ from: kysely.sql`${kysely.sql.table(relatedTable)} inner join ${kysely.sql.table(throughTable)} on ${this.buildColumnReference(relatedTable, this.mapColumn(relatedTarget, metadata.secondKey))} = ${this.buildColumnReference(throughTable, metadata.secondLocalKey)}`,
481
+ condition: kysely.sql`
482
+ ${this.buildColumnReference(throughTable, metadata.firstKey)}
483
+ =
484
+ ${this.buildColumnReference(outerTable, this.mapColumn(outerTarget, metadata.localKey))}
485
+ `
486
+ };
487
+ }
488
+ buildRelatedJoinCondition(outerTarget, relation) {
489
+ const { metadata, relatedTarget } = this.buildRelatedTargetFromRelation(outerTarget, relation);
490
+ const outerTable = this.resolveTable(outerTarget);
491
+ const relatedTable = this.resolveTable(relatedTarget);
492
+ if (metadata.type === "belongsToMany") {
493
+ const joinSource = this.buildBelongsToManyJoinSource(outerTarget, relatedTarget, metadata);
494
+ return {
495
+ relatedTarget,
496
+ from: joinSource.from,
497
+ condition: joinSource.condition
498
+ };
499
+ }
500
+ if (metadata.type === "hasOneThrough" || metadata.type === "hasManyThrough") {
501
+ const joinSource = this.buildThroughJoinSource(outerTarget, relatedTarget, metadata);
502
+ return {
503
+ relatedTarget,
504
+ from: joinSource.from,
505
+ condition: joinSource.condition
506
+ };
507
+ }
508
+ if (metadata.type === "hasMany" || metadata.type === "hasOne") return {
509
+ relatedTarget,
510
+ from: kysely.sql`${kysely.sql.table(relatedTable)}`,
511
+ condition: kysely.sql`
512
+ ${this.buildColumnReference(relatedTable, this.mapColumn(relatedTarget, metadata.foreignKey))}
513
+ =
514
+ ${this.buildColumnReference(outerTable, this.mapColumn(outerTarget, metadata.localKey))}
515
+ `
516
+ };
517
+ return {
518
+ relatedTarget,
519
+ from: kysely.sql`${kysely.sql.table(relatedTable)}`,
520
+ condition: kysely.sql`
521
+ ${this.buildColumnReference(relatedTable, this.mapColumn(relatedTarget, metadata.ownerKey))}
522
+ =
523
+ ${this.buildColumnReference(outerTable, this.mapColumn(outerTarget, metadata.foreignKey))}
524
+ `
525
+ };
526
+ }
527
+ combineConditions(conditions) {
528
+ const filtered = conditions.filter((condition) => Boolean(condition));
529
+ if (filtered.length === 0) return kysely.sql`1 = 1`;
530
+ if (filtered.length === 1) return filtered[0];
531
+ return kysely.sql`(${kysely.sql.join(filtered, kysely.sql` and `)})`;
532
+ }
533
+ buildRelationFilterExpression(target, filter) {
534
+ const { relatedTarget, from, condition } = this.buildRelatedJoinCondition(target, filter.relation);
535
+ return kysely.sql`(
536
+ select count(*)::int
537
+ from ${from}
538
+ where ${this.combineConditions([condition, filter.where ? this.buildWhereCondition(relatedTarget, filter.where) : void 0])}
539
+ ) ${filter.operator === "!=" ? kysely.sql.raw("!=") : kysely.sql.raw(filter.operator)} ${filter.count}`;
540
+ }
541
+ buildRelationFilterCondition(target, relationFilters) {
542
+ if (!relationFilters || relationFilters.length === 0) return kysely.sql`1 = 1`;
543
+ let expression = null;
544
+ relationFilters.forEach((filter) => {
545
+ const next = this.buildRelationFilterExpression(target, filter);
546
+ if (!expression) {
547
+ expression = next;
548
+ return;
549
+ }
550
+ expression = filter.boolean === "OR" ? kysely.sql`(${expression} or ${next})` : kysely.sql`(${expression} and ${next})`;
551
+ });
552
+ return expression ?? kysely.sql`1 = 1`;
553
+ }
554
+ buildQueryFilterCondition(target, condition, relationFilters) {
555
+ let expression = condition ? this.buildWhereCondition(target, condition) : null;
556
+ relationFilters?.forEach((filter) => {
557
+ const next = this.buildRelationFilterExpression(target, filter);
558
+ if (!expression) {
559
+ expression = next;
560
+ return;
561
+ }
562
+ expression = filter.boolean === "OR" ? kysely.sql`(${expression} or ${next})` : kysely.sql`(${expression} and ${next})`;
563
+ });
564
+ return expression ?? kysely.sql`1 = 1`;
565
+ }
566
+ buildRelationAggregateSelectList(target, relationAggregates) {
567
+ if (!relationAggregates || relationAggregates.length === 0) return kysely.sql``;
568
+ return kysely.sql.join(relationAggregates.map((aggregate) => {
569
+ const { relatedTarget, from, condition } = this.buildRelatedJoinCondition(target, aggregate.relation);
570
+ const relatedTable = this.resolveTable(relatedTarget);
571
+ const whereCondition = this.combineConditions([condition, aggregate.where ? this.buildWhereCondition(relatedTarget, aggregate.where) : void 0]);
572
+ if (aggregate.type === "exists") return kysely.sql`, exists(
573
+ select 1
574
+ from ${from}
575
+ where ${whereCondition}
576
+ ) as ${kysely.sql.id(aggregate.alias ?? `${aggregate.relation}Exists`)}`;
577
+ const selectedColumn = aggregate.column ? this.buildColumnReference(relatedTable, this.mapColumn(relatedTarget, aggregate.column)) : kysely.sql.raw("*");
578
+ return kysely.sql`, (
579
+ select ${aggregate.type === "count" ? kysely.sql`count(*)::int` : aggregate.type === "sum" ? kysely.sql`sum(${selectedColumn})::double precision` : aggregate.type === "avg" ? kysely.sql`avg(${selectedColumn})::double precision` : aggregate.type === "min" ? kysely.sql`min(${selectedColumn})` : kysely.sql`max(${selectedColumn})`}
580
+ from ${from}
581
+ where ${whereCondition}
582
+ ) as ${kysely.sql.id(aggregate.alias ?? `${aggregate.relation}${aggregate.type}`)}`;
583
+ }), kysely.sql``);
584
+ }
585
+ buildCombinedWhereClause(target, condition, relationFilters) {
586
+ if (!condition && (!relationFilters || relationFilters.length === 0)) return kysely.sql``;
587
+ return kysely.sql` where ${this.buildQueryFilterCondition(target, condition, relationFilters)}`;
588
+ }
589
+ buildSingleRowTargetCte(target, where) {
590
+ const primaryKey = this.resolvePrimaryKey(target);
591
+ return kysely.sql`target_row as (
592
+ select ${kysely.sql.id(primaryKey)}
593
+ from ${kysely.sql.table(this.resolveTable(target))}
594
+ where ${this.buildWhereCondition(target, where)}
595
+ limit 1
596
+ )`;
597
+ }
228
598
  assertNoRelationLoads(spec) {
229
599
  if ("relationLoads" in spec && spec.relationLoads && spec.relationLoads.length > 0) throw new UnsupportedAdapterFeatureException("Kysely adapter relation-load execution is planned for a later phase.", {
230
600
  operation: "adapter.relationLoads",
231
601
  meta: { feature: "relationLoads" }
232
602
  });
233
603
  }
604
+ /**
605
+ * Selects records from the database matching the specified criteria and returns
606
+ * them as an array of database rows.
607
+ *
608
+ * @param spec The specification defining the selection criteria.
609
+ * @returns A promise that resolves to an array of database rows.
610
+ */
234
611
  async select(spec) {
235
612
  this.assertNoRelationLoads(spec);
236
613
  const result = await kysely.sql`
237
614
  select ${this.buildSelectList(spec.target, spec.columns)}
615
+ ${this.buildRelationAggregateSelectList(spec.target, spec.relationAggregates)}
238
616
  from ${kysely.sql.table(this.resolveTable(spec.target))}
239
- ${this.buildWhereClause(spec.target, spec.where)}
617
+ ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
240
618
  ${this.buildOrderBy(spec.target, spec.orderBy)}
241
619
  ${this.buildPaginationClause(spec)}
242
620
  `.execute(this.db);
243
621
  return this.mapRows(spec.target, result.rows);
244
622
  }
623
+ /**
624
+ * Selects a single record from the database matching the specified criteria and returns it as
625
+ * a database row. If multiple records match the criteria, only the first one is returned.
626
+ * If no records match, null is returned.
627
+ *
628
+ * @param spec The specification defining the selection criteria.
629
+ * @returns A promise that resolves to a database row or null if no records match.
630
+ */
245
631
  async selectOne(spec) {
246
632
  return (await this.select({
247
633
  ...spec,
248
634
  limit: spec.limit ?? 1
249
635
  }))[0] ?? null;
250
636
  }
637
+ /**
638
+ * Inserts a new record into the database with the specified values and returns the
639
+ * inserted record as a database row.
640
+ *
641
+ * @param spec
642
+ * @returns
643
+ */
251
644
  async insert(spec) {
252
645
  const values = this.mapValues(spec.target, spec.values);
253
646
  const columns = Object.keys(values);
@@ -262,6 +655,13 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
262
655
  `.execute(this.db);
263
656
  return this.mapRow(spec.target, result.rows[0]);
264
657
  }
658
+ /**
659
+ * Inserts multiple records into the database with the specified values and returns the number
660
+ * of records successfully inserted.
661
+ *
662
+ * @param spec The specification defining the values to be inserted.
663
+ * @returns A promise that resolves to the number of records successfully inserted.
664
+ */
265
665
  async insertMany(spec) {
266
666
  if (spec.values.length === 0) return 0;
267
667
  const rows = spec.values.map((row) => this.mapValues(spec.target, row));
@@ -282,6 +682,39 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
282
682
  returning ${kysely.sql.id(this.resolvePrimaryKey(spec.target))}
283
683
  `.execute(this.db)).rows.length;
284
684
  }
685
+ async upsert(spec) {
686
+ if (spec.values.length === 0) return 0;
687
+ const rows = spec.values.map((row) => this.mapValues(spec.target, row));
688
+ const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
689
+ const uniqueColumns = spec.uniqueBy.map((column) => this.mapColumn(spec.target, column));
690
+ const updateColumns = (spec.updateColumns ?? []).map((column) => this.mapColumn(spec.target, column)).filter((column) => !uniqueColumns.includes(column));
691
+ const conflictTarget = kysely.sql.join(uniqueColumns.map((column) => kysely.sql.id(column)), kysely.sql`, `);
692
+ if (columns.length === 0) {
693
+ await kysely.sql`
694
+ insert into ${kysely.sql.table(this.resolveTable(spec.target))}
695
+ default values
696
+ on conflict (${conflictTarget}) do nothing
697
+ `.execute(this.db);
698
+ return spec.values.length;
699
+ }
700
+ const values = kysely.sql.join(rows.map((row) => {
701
+ return kysely.sql`(${kysely.sql.join(columns.map((column) => row[column] ?? null), kysely.sql`, `)})`;
702
+ }), kysely.sql`, `);
703
+ const conflictAction = updateColumns.length === 0 ? kysely.sql`do nothing` : kysely.sql`do update set ${kysely.sql.join(updateColumns.map((column) => kysely.sql`${kysely.sql.id(column)} = excluded.${kysely.sql.id(column)}`), kysely.sql`, `)}`;
704
+ await kysely.sql`
705
+ insert into ${kysely.sql.table(this.resolveTable(spec.target))} (${kysely.sql.join(columns.map((column) => kysely.sql.id(column)), kysely.sql`, `)})
706
+ values ${values}
707
+ on conflict (${conflictTarget}) ${conflictAction}
708
+ `.execute(this.db);
709
+ return spec.values.length;
710
+ }
711
+ /**
712
+ * Updates records in the database matching the specified criteria with the given values
713
+ * and returns the updated record as a database row.
714
+ *
715
+ * @param spec The specification defining the update criteria and values.
716
+ * @returns A promise that resolves to the updated record as a database row, or null if no records match the criteria.
717
+ */
285
718
  async update(spec) {
286
719
  const values = this.mapValues(spec.target, spec.values);
287
720
  const assignments = Object.entries(values).map(([column, value]) => {
@@ -300,6 +733,41 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
300
733
  `.execute(this.db);
301
734
  return this.mapRow(spec.target, result.rows[0]);
302
735
  }
736
+ /**
737
+ * Updates a single record in the database matching the specified criteria with the given values.
738
+ *
739
+ * @param spec
740
+ * @returns
741
+ */
742
+ async updateFirst(spec) {
743
+ const values = this.mapValues(spec.target, spec.values);
744
+ const assignments = Object.entries(values).map(([column, value]) => {
745
+ return kysely.sql`${kysely.sql.id(column)} = ${value}`;
746
+ });
747
+ if (assignments.length === 0) return await this.selectOne({
748
+ target: spec.target,
749
+ where: spec.where,
750
+ limit: 1
751
+ });
752
+ const primaryKey = this.resolvePrimaryKey(spec.target);
753
+ const table = this.resolveTable(spec.target);
754
+ const result = await kysely.sql`
755
+ with ${this.buildSingleRowTargetCte(spec.target, spec.where)}
756
+ update ${kysely.sql.table(table)}
757
+ set ${kysely.sql.join(assignments, kysely.sql`, `)}
758
+ from target_row
759
+ where ${this.buildColumnReference(table, primaryKey)} = ${kysely.sql`target_row.${kysely.sql.id(primaryKey)}`}
760
+ returning ${kysely.sql.table(table)}.*
761
+ `.execute(this.db);
762
+ return this.mapRow(spec.target, result.rows[0]);
763
+ }
764
+ /**
765
+ * Updates multiple records in the database matching the specified criteria with the
766
+ * given values and returns the number of records successfully updated.
767
+ *
768
+ * @param spec The specification defining the update criteria and values.
769
+ * @returns A promise that resolves to the number of records successfully updated.
770
+ */
303
771
  async updateMany(spec) {
304
772
  const values = this.mapValues(spec.target, spec.values);
305
773
  const assignments = Object.entries(values).map(([column, value]) => {
@@ -313,6 +781,13 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
313
781
  returning ${kysely.sql.id(this.resolvePrimaryKey(spec.target))}
314
782
  `.execute(this.db)).rows.length;
315
783
  }
784
+ /**
785
+ * Deletes records from the database matching the specified criteria and returns the
786
+ * deleted record as a database row.
787
+ *
788
+ * @param spec The specification defining the delete criteria.
789
+ * @returns A promise that resolves to the deleted record as a database row, or null if no records match the criteria.
790
+ */
316
791
  async delete(spec) {
317
792
  const result = await kysely.sql`
318
793
  delete from ${kysely.sql.table(this.resolveTable(spec.target))}
@@ -321,6 +796,31 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
321
796
  `.execute(this.db);
322
797
  return this.mapRow(spec.target, result.rows[0]);
323
798
  }
799
+ /**
800
+ * Deletes a single record from the database matching the specified criteria and returns it as a database row.
801
+ *
802
+ * @param spec
803
+ * @returns
804
+ */
805
+ async deleteFirst(spec) {
806
+ const primaryKey = this.resolvePrimaryKey(spec.target);
807
+ const table = this.resolveTable(spec.target);
808
+ const result = await kysely.sql`
809
+ with ${this.buildSingleRowTargetCte(spec.target, spec.where)}
810
+ delete from ${kysely.sql.table(table)}
811
+ using target_row
812
+ where ${this.buildColumnReference(table, primaryKey)} = ${kysely.sql`target_row.${kysely.sql.id(primaryKey)}`}
813
+ returning ${kysely.sql.table(table)}.*
814
+ `.execute(this.db);
815
+ return this.mapRow(spec.target, result.rows[0]);
816
+ }
817
+ /**
818
+ * Deletes multiple records from the database matching the specified criteria and
819
+ * returns the number of records successfully deleted.
820
+ *
821
+ * @param spec The specification defining the delete criteria.
822
+ * @returns A promise that resolves to the number of records successfully deleted.
823
+ */
324
824
  async deleteMany(spec) {
325
825
  return (await kysely.sql`
326
826
  delete from ${kysely.sql.table(this.resolveTable(spec.target))}
@@ -328,36 +828,170 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
328
828
  returning ${kysely.sql.id(this.resolvePrimaryKey(spec.target))}
329
829
  `.execute(this.db)).rows.length;
330
830
  }
831
+ /**
832
+ * Counts the number of records in the database matching the specified criteria and returns
833
+ * the count as a number.
834
+ *
835
+ * @param spec The specification defining the count criteria.
836
+ * @returns A promise that resolves to the number of records matching the criteria.
837
+ */
331
838
  async count(spec) {
332
839
  const result = await kysely.sql`
333
840
  select count(*)::int as count
334
841
  from ${kysely.sql.table(this.resolveTable(spec.target))}
335
- ${this.buildWhereClause(spec.target, spec.where)}
842
+ ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
336
843
  `.execute(this.db);
337
844
  return Number(result.rows[0]?.count ?? 0);
338
845
  }
846
+ /**
847
+ * Checks for the existence of records matching the specified criteria.
848
+ *
849
+ * @param spec The specification defining the existence criteria.
850
+ * @returns A promise that resolves to a boolean indicating whether any records match the criteria.
851
+ */
339
852
  async exists(spec) {
340
853
  const result = await kysely.sql`
341
854
  select exists(
342
855
  select 1
343
856
  from ${kysely.sql.table(this.resolveTable(spec.target))}
344
- ${this.buildWhereClause(spec.target, spec.where)}
857
+ ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
345
858
  limit 1
346
859
  ) as exists
347
860
  `.execute(this.db);
348
861
  return Boolean(result.rows[0]?.exists);
349
862
  }
863
+ async introspectModels(options = {}) {
864
+ const tables = options.tables?.filter(Boolean) ?? [];
865
+ const result = await kysely.sql`
866
+ select
867
+ cls.relname as table_name,
868
+ att.attname as column_name,
869
+ not att.attnotnull as is_nullable,
870
+ typ.typname as type_name,
871
+ case when typ.typcategory = 'A' then elem.typname else null end as element_type_name,
872
+ case when typ.typtype = 'e'
873
+ then array(select enumlabel from pg_enum where enumtypid = typ.oid order by enumsortorder)
874
+ else null
875
+ end as enum_values,
876
+ case when elem.typtype = 'e'
877
+ then array(select enumlabel from pg_enum where enumtypid = elem.oid order by enumsortorder)
878
+ else null
879
+ end as element_enum_values
880
+ from pg_attribute att
881
+ inner join pg_class cls on cls.oid = att.attrelid
882
+ inner join pg_namespace ns on ns.oid = cls.relnamespace
883
+ inner join pg_type typ on typ.oid = att.atttypid
884
+ left join pg_type elem on elem.oid = typ.typelem and typ.typcategory = 'A'
885
+ where cls.relkind in ('r', 'p')
886
+ and att.attnum > 0
887
+ and not att.attisdropped
888
+ and ns.nspname not in ('pg_catalog', 'information_schema')
889
+ ${tables.length > 0 ? kysely.sql` and cls.relname in (${kysely.sql.join(tables)})` : kysely.sql``}
890
+ order by cls.relname asc, att.attnum asc
891
+ `.execute(this.db);
892
+ const models = /* @__PURE__ */ new Map();
893
+ result.rows.forEach((row) => {
894
+ const existing = models.get(row.table_name) ?? {
895
+ name: (0, _h3ravel_support.str)(row.table_name).studly().singular().toString(),
896
+ table: row.table_name,
897
+ fields: []
898
+ };
899
+ const isArray = row.element_type_name !== null;
900
+ const baseType = isArray ? this.introspectionTypeToTs(row.element_type_name ?? "unknown", row.element_enum_values) : this.introspectionTypeToTs(row.type_name, row.enum_values);
901
+ existing.fields.push({
902
+ name: row.column_name,
903
+ type: isArray ? `Array<${baseType}>` : baseType,
904
+ nullable: row.is_nullable
905
+ });
906
+ models.set(row.table_name, existing);
907
+ });
908
+ return [...models.values()];
909
+ }
910
+ async executeSchemaOperations(operations) {
911
+ if (operations.length === 0) return;
912
+ await this.transaction(async (adapter) => {
913
+ const transactionAdapter = adapter;
914
+ for (const operation of operations) {
915
+ if (operation.type === "createTable") {
916
+ await transactionAdapter.executeCreateTableOperation(operation, transactionAdapter.db);
917
+ continue;
918
+ }
919
+ if (operation.type === "alterTable") {
920
+ await transactionAdapter.executeAlterTableOperation(operation, transactionAdapter.db);
921
+ continue;
922
+ }
923
+ await transactionAdapter.executeDropTableOperation(operation, transactionAdapter.db);
924
+ }
925
+ });
926
+ }
927
+ async resetDatabase() {
928
+ await this.transaction(async (adapter) => {
929
+ const transactionAdapter = adapter;
930
+ await transactionAdapter.resetDatabaseInternal(transactionAdapter.db);
931
+ });
932
+ }
933
+ async readAppliedMigrationsState() {
934
+ await this.ensureMigrationStateTables();
935
+ const migrationsResult = await kysely.sql`
936
+ select id, file, class_name, applied_at, checksum
937
+ from ${kysely.sql.table(KyselyDatabaseAdapter.migrationStateTable)}
938
+ order by applied_at asc, id asc
939
+ `.execute(this.db);
940
+ const runsResult = await kysely.sql`
941
+ select id, applied_at, migration_ids
942
+ from ${kysely.sql.table(KyselyDatabaseAdapter.migrationRunTable)}
943
+ order by applied_at asc, id asc
944
+ `.execute(this.db);
945
+ return {
946
+ version: 1,
947
+ migrations: migrationsResult.rows.map((row) => ({
948
+ id: row.id,
949
+ file: row.file,
950
+ className: row.class_name,
951
+ appliedAt: row.applied_at instanceof Date ? row.applied_at.toISOString() : String(row.applied_at),
952
+ checksum: row.checksum ?? void 0
953
+ })),
954
+ runs: runsResult.rows.map((row) => ({
955
+ id: row.id,
956
+ appliedAt: row.applied_at instanceof Date ? row.applied_at.toISOString() : String(row.applied_at),
957
+ migrationIds: Array.isArray(row.migration_ids) ? row.migration_ids.filter((value) => typeof value === "string") : typeof row.migration_ids === "string" ? JSON.parse(row.migration_ids) : []
958
+ }))
959
+ };
960
+ }
961
+ async writeAppliedMigrationsState(state) {
962
+ await this.transaction(async (adapter) => {
963
+ const transactionAdapter = adapter;
964
+ await transactionAdapter.writeAppliedMigrationsStateInternal(state, transactionAdapter.db);
965
+ });
966
+ }
967
+ /**
968
+ * Executes a series of database operations within a transaction.
969
+ * The provided callback function is called with a new instance of the
970
+ * KyselyDatabaseAdapter that is bound to the transaction context.
971
+ *
972
+ * @param callback The callback function containing the database operations to be executed within the transaction.
973
+ * @param context The transaction context specifying options such as read-only mode and isolation level.
974
+ * @returns A promise that resolves to the result of the callback function.
975
+ */
350
976
  async transaction(callback, context = {}) {
351
977
  let transactionBuilder = this.db.transaction();
352
978
  if (context.readOnly !== void 0) transactionBuilder = transactionBuilder.setAccessMode(context.readOnly ? "read only" : "read write");
353
979
  if (context.isolationLevel) transactionBuilder = transactionBuilder.setIsolationLevel(context.isolationLevel);
354
980
  return await transactionBuilder.execute(async (transaction) => {
355
- return await callback(new KyselyDatabaseAdapter(transaction));
981
+ return await callback(new KyselyDatabaseAdapter(transaction, this.mapping));
356
982
  });
357
983
  }
358
984
  };
359
- const createKyselyAdapter = (db) => {
360
- return new KyselyDatabaseAdapter(db);
985
+ /**
986
+ * Factory function to create a KyselyDatabaseAdapter instance with the given Kysely executor
987
+ * and optional table name mapping.
988
+ *
989
+ * @param db The Kysely executor to be used by the adapter.
990
+ * @param mapping Optional table name mapping for the adapter.
991
+ * @returns A new instance of KyselyDatabaseAdapter.
992
+ */
993
+ const createKyselyAdapter = (db, mapping = {}) => {
994
+ return new KyselyDatabaseAdapter(db, mapping);
361
995
  };
362
996
 
363
997
  //#endregion
@@ -416,6 +1050,7 @@ const userConfig = {
416
1050
  let runtimeConfigLoaded = false;
417
1051
  let runtimeConfigLoadingPromise;
418
1052
  let runtimeClientResolver;
1053
+ let runtimeAdapter;
419
1054
  let runtimePaginationURLDriverFactory;
420
1055
  let runtimePaginationCurrentPageResolver;
421
1056
  const transactionClientStorage = new async_hooks.AsyncLocalStorage();
@@ -441,6 +1076,12 @@ const mergePathConfig = (paths) => {
441
1076
  const defineConfig = (config) => {
442
1077
  return config;
443
1078
  };
1079
+ const bindAdapterToModels = (adapter, models) => {
1080
+ models.forEach((model) => {
1081
+ model.setAdapter(adapter);
1082
+ });
1083
+ return adapter;
1084
+ };
444
1085
  /**
445
1086
  * Get the user-provided ArkORM configuration.
446
1087
  *
@@ -460,15 +1101,22 @@ const getUserConfig = (key) => {
460
1101
  const configureArkormRuntime = (prisma, options = {}) => {
461
1102
  const nextConfig = {
462
1103
  ...userConfig,
463
- prisma,
464
1104
  paths: mergePathConfig(options.paths)
465
1105
  };
1106
+ nextConfig.prisma = prisma;
466
1107
  if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
1108
+ if (options.adapter !== void 0) nextConfig.adapter = options.adapter;
1109
+ if (options.boot !== void 0) nextConfig.boot = options.boot;
467
1110
  if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
468
1111
  Object.assign(userConfig, { ...nextConfig });
469
1112
  runtimeClientResolver = prisma;
1113
+ runtimeAdapter = options.adapter;
470
1114
  runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
471
1115
  runtimePaginationCurrentPageResolver = nextConfig.pagination?.resolveCurrentPage;
1116
+ options.boot?.({
1117
+ prisma: resolveClient(prisma),
1118
+ bindAdapter: bindAdapterToModels
1119
+ });
472
1120
  };
473
1121
  /**
474
1122
  * Reset the ArkORM runtime configuration.
@@ -482,6 +1130,7 @@ const resetArkormRuntimeForTests = () => {
482
1130
  runtimeConfigLoaded = false;
483
1131
  runtimeConfigLoadingPromise = void 0;
484
1132
  runtimeClientResolver = void 0;
1133
+ runtimeAdapter = void 0;
485
1134
  runtimePaginationURLDriverFactory = void 0;
486
1135
  runtimePaginationCurrentPageResolver = void 0;
487
1136
  };
@@ -508,8 +1157,10 @@ const resolveClient = (resolver) => {
508
1157
  */
509
1158
  const resolveAndApplyConfig = (imported) => {
510
1159
  const config = imported?.default ?? imported;
511
- if (!config || typeof config !== "object" || !config.prisma) return;
1160
+ if (!config || typeof config !== "object") return;
512
1161
  configureArkormRuntime(config.prisma, {
1162
+ adapter: config.adapter,
1163
+ boot: config.boot,
513
1164
  pagination: config.pagination,
514
1165
  paths: config.paths,
515
1166
  outputExt: config.outputExt
@@ -591,6 +1242,10 @@ const getRuntimePrismaClient = () => {
591
1242
  if (!runtimeConfigLoaded) loadRuntimeConfigSync();
592
1243
  return resolveClient(runtimeClientResolver);
593
1244
  };
1245
+ const getRuntimeAdapter = () => {
1246
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
1247
+ return runtimeAdapter;
1248
+ };
594
1249
  const getActiveTransactionClient = () => {
595
1250
  return transactionClientStorage.getStore();
596
1251
  };
@@ -658,9 +1313,11 @@ loadArkormConfig();
658
1313
  //#endregion
659
1314
  //#region src/helpers/prisma.ts
660
1315
  /**
661
- * Create an adapter to convert a Prisma client instance into a format
1316
+ * Create an adapter to convert a Prisma client instance into a format
662
1317
  * compatible with ArkORM's expectations.
663
- *
1318
+ *
1319
+ * @deprecated Prefer createPrismaDatabaseAdapter(prisma) for runtime usage.
1320
+ *
664
1321
  * @param prisma The Prisma client instance to adapt.
665
1322
  * @param mapping An optional mapping of Prisma delegate names to ArkORM delegate names.
666
1323
  * @returns A record of adapted Prisma delegates compatible with ArkORM.
@@ -675,6 +1332,9 @@ function createPrismaAdapter(prisma) {
675
1332
  /**
676
1333
  * Create a delegate mapping record for Model.setClient() from a Prisma client.
677
1334
  *
1335
+ * @deprecated Prefer createPrismaDatabaseAdapter(prisma, mapping) and bind the
1336
+ * resulting adapter with Model.setAdapter(...).
1337
+ *
678
1338
  * @param prisma The Prisma client instance.
679
1339
  * @param mapping Optional mapping of Arkormˣ delegate names to Prisma delegate names.
680
1340
  * @returns A delegate map keyed by Arkormˣ delegate names.
@@ -701,6 +1361,13 @@ function inferDelegateName(modelName) {
701
1361
 
702
1362
  //#endregion
703
1363
  //#region src/adapters/PrismaDatabaseAdapter.ts
1364
+ /**
1365
+ * Database adapter implementation for Prisma, allowing Arkorm to execute queries using Prisma
1366
+ * as the underlying query builder and executor.
1367
+ *
1368
+ * @author Legacy (3m1n3nc3)
1369
+ * @since 2.0.0-next.0
1370
+ */
704
1371
  var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
705
1372
  capabilities;
706
1373
  delegates;
@@ -715,6 +1382,7 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
715
1382
  this.capabilities = {
716
1383
  transactions: this.hasTransactionSupport(prisma),
717
1384
  insertMany: Object.values(this.delegates).some((delegate) => typeof delegate.createMany === "function"),
1385
+ upsert: false,
718
1386
  updateMany: Object.values(this.delegates).some((delegate) => typeof delegate.updateMany === "function"),
719
1387
  deleteMany: false,
720
1388
  exists: true,
@@ -734,6 +1402,27 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
734
1402
  unique(values) {
735
1403
  return [...new Set(values.filter(Boolean))];
736
1404
  }
1405
+ runtimeModelTypeToTs(typeName, kind, enumValues) {
1406
+ if (kind === "enum" && enumValues && enumValues.length > 0) return enumValues.map((value) => `'${value.replace(/'/g, "\\'")}'`).join(" | ");
1407
+ switch (typeName) {
1408
+ case "Int":
1409
+ case "Float":
1410
+ case "Decimal":
1411
+ case "BigInt": return "number";
1412
+ case "Boolean": return "boolean";
1413
+ case "DateTime": return "Date";
1414
+ case "Json": return "Record<string, unknown> | unknown[]";
1415
+ case "Bytes": return "Uint8Array";
1416
+ case "String":
1417
+ case "UUID": return "string";
1418
+ default: return "string";
1419
+ }
1420
+ }
1421
+ getRuntimeDataModel() {
1422
+ const runtimeDataModel = this.prisma._runtimeDataModel;
1423
+ if (runtimeDataModel && typeof runtimeDataModel === "object") return runtimeDataModel;
1424
+ return null;
1425
+ }
737
1426
  toQuerySelect(columns) {
738
1427
  if (!columns || columns.length === 0) return void 0;
739
1428
  return columns.reduce((select, column) => {
@@ -815,6 +1504,29 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
815
1504
  return include;
816
1505
  }, {});
817
1506
  }
1507
+ async introspectModels(options = {}) {
1508
+ const runtimeDataModel = this.getRuntimeDataModel();
1509
+ if (!runtimeDataModel?.models) return [];
1510
+ const requestedTables = new Set(options.tables?.filter(Boolean) ?? []);
1511
+ const enums = runtimeDataModel.enums ?? {};
1512
+ return Object.entries(runtimeDataModel.models).flatMap(([name, model]) => {
1513
+ const table = model.dbName ?? `${(0, _h3ravel_support.str)(name).camel().plural()}`;
1514
+ if (requestedTables.size > 0 && !requestedTables.has(table)) return [];
1515
+ return [{
1516
+ name,
1517
+ table,
1518
+ fields: (model.fields ?? []).filter((field) => field.kind !== "object").map((field) => {
1519
+ const enumValues = field.kind === "enum" ? (enums[field.type]?.values ?? []).map((value) => typeof value === "string" ? value : value.name ?? "").filter(Boolean) : null;
1520
+ const baseType = this.runtimeModelTypeToTs(field.type, field.kind, enumValues);
1521
+ return {
1522
+ name: field.name,
1523
+ type: field.isList ? `Array<${baseType}>` : baseType,
1524
+ nullable: field.isRequired === false
1525
+ };
1526
+ })
1527
+ }];
1528
+ });
1529
+ }
818
1530
  resolveDelegate(target) {
819
1531
  const tableName = target.table ? this.normalizeCandidate(target.table) : "";
820
1532
  const singularTableName = tableName ? `${(0, _h3ravel_support.str)(tableName).singular()}` : "";
@@ -846,15 +1558,41 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
846
1558
  }
847
1559
  });
848
1560
  }
1561
+ /**
1562
+ * @todo Implement relationLoads by performing separate queries and merging results
1563
+ * in-memory, since Prisma does not support nested reads with constraints, ordering, or
1564
+ * pagination on related models as of now.
1565
+ *
1566
+ * @param spec
1567
+ * @returns
1568
+ */
849
1569
  async select(spec) {
850
1570
  return await this.resolveDelegate(spec.target).findMany(this.buildFindArgs(spec));
851
1571
  }
1572
+ /**
1573
+ * Selects a single record matching the specified criteria.
1574
+ *
1575
+ * @param spec
1576
+ * @returns
1577
+ */
852
1578
  async selectOne(spec) {
853
1579
  return await this.resolveDelegate(spec.target).findFirst(this.buildFindArgs(spec));
854
1580
  }
1581
+ /**
1582
+ * Inserts a single record into the database and returns the created record.
1583
+ *
1584
+ * @param spec
1585
+ * @returns
1586
+ */
855
1587
  async insert(spec) {
856
1588
  return await this.resolveDelegate(spec.target).create({ data: spec.values });
857
1589
  }
1590
+ /**
1591
+ * Inserts multiple records into the database.
1592
+ *
1593
+ * @param spec
1594
+ * @returns
1595
+ */
858
1596
  async insertMany(spec) {
859
1597
  const delegate = this.resolveDelegate(spec.target);
860
1598
  if (typeof delegate.createMany === "function") {
@@ -874,6 +1612,12 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
874
1612
  }
875
1613
  return spec.ignoreDuplicates ? inserted : spec.values.length;
876
1614
  }
1615
+ /**
1616
+ * Updates a single record matching the specified criteria and returns the updated record.
1617
+ *
1618
+ * @param spec
1619
+ * @returns
1620
+ */
877
1621
  async update(spec) {
878
1622
  const delegate = this.resolveDelegate(spec.target);
879
1623
  const where = this.toQueryWhere(spec.where);
@@ -883,6 +1627,12 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
883
1627
  data: spec.values
884
1628
  });
885
1629
  }
1630
+ /**
1631
+ * Updates multiple records matching the specified criteria.
1632
+ *
1633
+ * @param spec
1634
+ * @returns
1635
+ */
886
1636
  async updateMany(spec) {
887
1637
  const delegate = this.resolveDelegate(spec.target);
888
1638
  const where = this.toQueryWhere(spec.where);
@@ -903,12 +1653,24 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
903
1653
  }));
904
1654
  return rows.length;
905
1655
  }
1656
+ /**
1657
+ * Deletes a single record matching the specified criteria and returns the deleted record.
1658
+ *
1659
+ * @param spec
1660
+ * @returns
1661
+ */
906
1662
  async delete(spec) {
907
1663
  const delegate = this.resolveDelegate(spec.target);
908
1664
  const where = this.toQueryWhere(spec.where);
909
1665
  if (!where) return null;
910
1666
  return await delegate.delete({ where });
911
1667
  }
1668
+ /**
1669
+ * Deletes multiple records matching the specified criteria.
1670
+ *
1671
+ * @param spec
1672
+ * @returns
1673
+ */
912
1674
  async deleteMany(spec) {
913
1675
  const delegate = this.resolveDelegate(spec.target);
914
1676
  const where = this.toQueryWhere(spec.where);
@@ -918,21 +1680,46 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
918
1680
  }));
919
1681
  return rows.length;
920
1682
  }
1683
+ /**
1684
+ * Counts the number of records matching the specified criteria.
1685
+ *
1686
+ * @param spec
1687
+ * @returns
1688
+ */
921
1689
  async count(spec) {
922
1690
  return await this.resolveDelegate(spec.target).count({ where: this.toQueryWhere(spec.where) });
923
1691
  }
1692
+ /**
1693
+ * Checks for the existence of records matching the specified criteria.
1694
+ *
1695
+ * @param spec
1696
+ * @returns
1697
+ */
924
1698
  async exists(spec) {
925
1699
  return await this.selectOne({
926
1700
  ...spec,
927
1701
  limit: 1
928
1702
  }) != null;
929
1703
  }
1704
+ /**
1705
+ * Loads related models for a batch of parent records based on the specified relation load plans.
1706
+ *
1707
+ * @param _spec
1708
+ */
930
1709
  async loadRelations(_spec) {
931
1710
  throw new UnsupportedAdapterFeatureException("Relation batch loading is not supported by the Prisma compatibility adapter yet.", {
932
1711
  operation: "adapter.loadRelations",
933
1712
  meta: { feature: "relationLoads" }
934
1713
  });
935
1714
  }
1715
+ /**
1716
+ * Executes a series of database operations within a transaction.
1717
+ * If the underlying Prisma client does not support transactions, an exception is thrown.
1718
+ *
1719
+ * @param callback
1720
+ * @param context
1721
+ * @returns
1722
+ */
936
1723
  async transaction(callback, context = {}) {
937
1724
  if (!this.hasTransactionSupport(this.prisma)) throw new UnsupportedAdapterFeatureException("Transactions are not supported by the Prisma compatibility adapter.", {
938
1725
  operation: "adapter.transaction",
@@ -947,9 +1734,25 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
947
1734
  });
948
1735
  }
949
1736
  };
1737
+ /**
1738
+ * Factory function to create a PrismaDatabaseAdapter instance with the given
1739
+ * Prisma client and optional delegate name mapping.
1740
+ *
1741
+ * @param prisma The Prisma client instance to be used by the adapter.
1742
+ * @param mapping Optional mapping of delegate names.
1743
+ * @returns A new instance of PrismaDatabaseAdapter.
1744
+ */
950
1745
  const createPrismaDatabaseAdapter = (prisma, mapping = {}) => {
951
1746
  return new PrismaDatabaseAdapter(prisma, mapping);
952
1747
  };
1748
+ /**
1749
+ * Alias for createPrismaDatabaseAdapter to maintain backward compatibility with
1750
+ * previous versions of Arkorm that exported the adapter factory under a different name.
1751
+ *
1752
+ * @param prisma The Prisma client instance to be used by the adapter.
1753
+ * @param mapping Optional mapping of delegate names.
1754
+ * @returns A new instance of PrismaDatabaseAdapter.
1755
+ */
953
1756
  const createPrismaCompatibilityAdapter = createPrismaDatabaseAdapter;
954
1757
 
955
1758
  //#endregion
@@ -2349,6 +3152,28 @@ const getMigrationPlan = async (migration, direction = "up") => {
2349
3152
  else await instance.down(schema);
2350
3153
  return schema.getOperations();
2351
3154
  };
3155
+ const supportsDatabaseMigrationExecution = (adapter) => {
3156
+ return typeof adapter?.executeSchemaOperations === "function";
3157
+ };
3158
+ const supportsDatabaseReset = (adapter) => {
3159
+ return typeof adapter?.resetDatabase === "function";
3160
+ };
3161
+ const stripPrismaSchemaModelsAndEnums = (schema) => {
3162
+ const stripped = schema.replace(PRISMA_MODEL_REGEX, "").replace(PRISMA_ENUM_REGEX, "").replace(/\n{3,}/g, "\n\n").trimEnd();
3163
+ return stripped.length > 0 ? `${stripped}\n` : "";
3164
+ };
3165
+ const applyMigrationToDatabase = async (adapter, migration) => {
3166
+ if (!supportsDatabaseMigrationExecution(adapter)) throw new ArkormException("The configured adapter does not support database-backed migration execution.");
3167
+ const operations = await getMigrationPlan(migration, "up");
3168
+ await adapter.executeSchemaOperations(operations);
3169
+ return { operations };
3170
+ };
3171
+ const applyMigrationRollbackToDatabase = async (adapter, migration) => {
3172
+ if (!supportsDatabaseMigrationExecution(adapter)) throw new ArkormException("The configured adapter does not support database-backed migration execution.");
3173
+ const operations = await getMigrationPlan(migration, "down");
3174
+ await adapter.executeSchemaOperations(operations);
3175
+ return { operations };
3176
+ };
2352
3177
  /**
2353
3178
  * Apply the schema operations defined in a migration to a Prisma schema
2354
3179
  * file, updating the file on disk if specified, and return the updated
@@ -2921,6 +3746,40 @@ var CliApp = class {
2921
3746
  lines.splice(insertionIndex, 0, `import type { ${enumTypes.join(", ")} } from '@prisma/client'`);
2922
3747
  return lines.join("\n");
2923
3748
  }
3749
+ parseModelSyncSource(modelSource) {
3750
+ const classMatch = modelSource.match(/export\s+class\s+(\w+)\s+extends\s+Model(?:<[^\n]+>)?\s*\{/);
3751
+ if (!classMatch) return null;
3752
+ const className = classMatch[1];
3753
+ const tableMatch = modelSource.match(/protected\s+static\s+override\s+table\s*=\s*['"]([^'"]+)['"]/) ?? modelSource.match(/static\s+table\s*=\s*['"]([^'"]+)['"]/);
3754
+ const delegateMatch = modelSource.match(/protected\s+static\s+override\s+delegate\s*=\s*['"]([^'"]+)['"]/) ?? modelSource.match(/static\s+delegate\s*=\s*['"]([^'"]+)['"]/);
3755
+ return {
3756
+ className,
3757
+ table: tableMatch?.[1] ?? delegateMatch?.[1] ?? (0, _h3ravel_support.str)(className).camel().plural().toString()
3758
+ };
3759
+ }
3760
+ syncModelFiles(modelFiles, resolveStructure, enums) {
3761
+ const updated = [];
3762
+ const skipped = [];
3763
+ modelFiles.forEach((filePath) => {
3764
+ const source = (0, fs.readFileSync)(filePath, "utf-8");
3765
+ const structure = resolveStructure(filePath, source);
3766
+ if (!structure || structure.fields.length === 0) {
3767
+ skipped.push(filePath);
3768
+ return;
3769
+ }
3770
+ const synced = this.syncModelDeclarations(source, structure.fields, enums);
3771
+ if (!synced.updated) {
3772
+ skipped.push(filePath);
3773
+ return;
3774
+ }
3775
+ (0, fs.writeFileSync)(filePath, synced.content);
3776
+ updated.push(filePath);
3777
+ });
3778
+ return {
3779
+ updated,
3780
+ skipped
3781
+ };
3782
+ }
2924
3783
  /**
2925
3784
  * Parse Prisma enum definitions from a schema and return their member names.
2926
3785
  *
@@ -3013,7 +3872,7 @@ var CliApp = class {
3013
3872
  */
3014
3873
  syncModelDeclarations(modelSource, declarations, enums) {
3015
3874
  const lines = modelSource.split("\n");
3016
- const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model<.+>\s*\{/.test(line));
3875
+ const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model(?:<[^\n]+>)?\s*\{/.test(line));
3017
3876
  if (classIndex < 0) return {
3018
3877
  content: modelSource,
3019
3878
  updated: false
@@ -3067,6 +3926,36 @@ var CliApp = class {
3067
3926
  updated: contentWithImports !== modelSource
3068
3927
  };
3069
3928
  }
3929
+ async syncModels(options = {}) {
3930
+ const modelsDir = options.modelsDir ?? this.resolveConfigPath("models", (0, path.join)(process.cwd(), "src", "models"));
3931
+ if (!(0, fs.existsSync)(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
3932
+ const modelFiles = (0, fs.readdirSync)(modelsDir).filter((file) => file.endsWith(".ts")).map((file) => (0, path.join)(modelsDir, file));
3933
+ const adapter = this.getConfig("adapter");
3934
+ if (adapter && typeof adapter.introspectModels === "function") {
3935
+ const sources = modelFiles.reduce((all, filePath) => {
3936
+ const parsed = this.parseModelSyncSource((0, fs.readFileSync)(filePath, "utf-8"));
3937
+ if (parsed) all.set(filePath, parsed);
3938
+ return all;
3939
+ }, /* @__PURE__ */ new Map());
3940
+ const discovered = await adapter.introspectModels({ tables: [...new Set([...sources.values()].map((source) => source.table))] });
3941
+ const structuresByTable = new Map(discovered.map((model) => [model.table, model]));
3942
+ const result = this.syncModelFiles(modelFiles, (filePath) => {
3943
+ const parsed = sources.get(filePath);
3944
+ return parsed ? structuresByTable.get(parsed.table) : void 0;
3945
+ }, /* @__PURE__ */ new Map());
3946
+ return {
3947
+ source: "adapter",
3948
+ modelsDir,
3949
+ total: modelFiles.length,
3950
+ updated: result.updated,
3951
+ skipped: result.skipped
3952
+ };
3953
+ }
3954
+ return {
3955
+ source: "prisma",
3956
+ ...this.syncModelsFromPrisma(options)
3957
+ };
3958
+ }
3070
3959
  /**
3071
3960
  * Sync model attribute declarations in model files based on the Prisma schema.
3072
3961
  * This method reads the Prisma schema to extract model definitions and their
@@ -3085,39 +3974,19 @@ var CliApp = class {
3085
3974
  if (!(0, fs.existsSync)(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
3086
3975
  const schema = (0, fs.readFileSync)(schemaPath, "utf-8");
3087
3976
  const prismaEnums = this.parsePrismaEnums(schema);
3088
- const prismaModels = this.parsePrismaModels(schema);
3089
- const modelFiles = (0, fs.readdirSync)(modelsDir).filter((file) => file.endsWith(".ts"));
3090
- const updated = [];
3091
- const skipped = [];
3092
- modelFiles.forEach((file) => {
3093
- const filePath = (0, path.join)(modelsDir, file);
3094
- const source = (0, fs.readFileSync)(filePath, "utf-8");
3095
- const classMatch = source.match(/export\s+class\s+(\w+)\s+extends\s+Model<'([^']+)'>/);
3096
- if (!classMatch) {
3097
- skipped.push(filePath);
3098
- return;
3099
- }
3100
- const className = classMatch[1];
3101
- const delegate = classMatch[2];
3102
- const prismaModel = prismaModels.find((model) => model.table === delegate) ?? prismaModels.find((model) => model.name === className);
3103
- if (!prismaModel || prismaModel.fields.length === 0) {
3104
- skipped.push(filePath);
3105
- return;
3106
- }
3107
- const synced = this.syncModelDeclarations(source, prismaModel.fields, prismaEnums);
3108
- if (!synced.updated) {
3109
- skipped.push(filePath);
3110
- return;
3111
- }
3112
- (0, fs.writeFileSync)(filePath, synced.content);
3113
- updated.push(filePath);
3114
- });
3977
+ const prismaModels = this.parsePrismaModels(schema);
3978
+ const modelFiles = (0, fs.readdirSync)(modelsDir).filter((file) => file.endsWith(".ts")).map((file) => (0, path.join)(modelsDir, file));
3979
+ const result = this.syncModelFiles(modelFiles, (filePath, source) => {
3980
+ const parsed = this.parseModelSyncSource(source);
3981
+ if (!parsed) return void 0;
3982
+ return prismaModels.find((model) => model.table === parsed.table) ?? prismaModels.find((model) => model.name === parsed.className);
3983
+ }, prismaEnums);
3115
3984
  return {
3116
3985
  schemaPath,
3117
3986
  modelsDir,
3118
3987
  total: modelFiles.length,
3119
- updated,
3120
- skipped
3988
+ updated: result.updated,
3989
+ skipped: result.skipped
3121
3990
  };
3122
3991
  }
3123
3992
  };
@@ -3285,10 +4154,13 @@ var MakeSeederCommand = class extends _h3ravel_musket.Command {
3285
4154
 
3286
4155
  //#endregion
3287
4156
  //#region src/helpers/migration-history.ts
3288
- const DEFAULT_STATE = {
4157
+ const createEmptyAppliedMigrationsState = () => ({
3289
4158
  version: 1,
3290
4159
  migrations: [],
3291
4160
  runs: []
4161
+ });
4162
+ const supportsDatabaseMigrationState = (adapter) => {
4163
+ return typeof adapter?.readAppliedMigrationsState === "function" && typeof adapter?.writeAppliedMigrationsState === "function";
3292
4164
  };
3293
4165
  const resolveMigrationStateFilePath = (cwd, configuredPath) => {
3294
4166
  if (configuredPath && configuredPath.trim().length > 0) return (0, node_path.resolve)(configuredPath);
@@ -3303,10 +4175,10 @@ const computeMigrationChecksum = (filePath) => {
3303
4175
  return (0, node_crypto.createHash)("sha256").update(source).digest("hex");
3304
4176
  };
3305
4177
  const readAppliedMigrationsState = (stateFilePath) => {
3306
- if (!(0, node_fs.existsSync)(stateFilePath)) return { ...DEFAULT_STATE };
4178
+ if (!(0, node_fs.existsSync)(stateFilePath)) return createEmptyAppliedMigrationsState();
3307
4179
  try {
3308
4180
  const parsed = JSON.parse((0, node_fs.readFileSync)(stateFilePath, "utf-8"));
3309
- if (!Array.isArray(parsed.migrations)) return { ...DEFAULT_STATE };
4181
+ if (!Array.isArray(parsed.migrations)) return createEmptyAppliedMigrationsState();
3310
4182
  return {
3311
4183
  version: 1,
3312
4184
  migrations: parsed.migrations.filter((migration) => {
@@ -3317,14 +4189,34 @@ const readAppliedMigrationsState = (stateFilePath) => {
3317
4189
  }) : []
3318
4190
  };
3319
4191
  } catch {
3320
- return { ...DEFAULT_STATE };
4192
+ return createEmptyAppliedMigrationsState();
3321
4193
  }
3322
4194
  };
4195
+ const readAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
4196
+ if (supportsDatabaseMigrationState(adapter)) return await adapter.readAppliedMigrationsState();
4197
+ return readAppliedMigrationsState(stateFilePath);
4198
+ };
3323
4199
  const writeAppliedMigrationsState = (stateFilePath, state) => {
3324
4200
  const directory = (0, node_path.dirname)(stateFilePath);
3325
4201
  if (!(0, node_fs.existsSync)(directory)) (0, node_fs.mkdirSync)(directory, { recursive: true });
3326
4202
  (0, node_fs.writeFileSync)(stateFilePath, JSON.stringify(state, null, 2));
3327
4203
  };
4204
+ const writeAppliedMigrationsStateToStore = async (adapter, stateFilePath, state) => {
4205
+ if (supportsDatabaseMigrationState(adapter)) {
4206
+ await adapter.writeAppliedMigrationsState(state);
4207
+ return;
4208
+ }
4209
+ writeAppliedMigrationsState(stateFilePath, state);
4210
+ };
4211
+ const deleteAppliedMigrationsStateFromStore = async (adapter, stateFilePath) => {
4212
+ if (supportsDatabaseMigrationState(adapter)) {
4213
+ await adapter.writeAppliedMigrationsState(createEmptyAppliedMigrationsState());
4214
+ return "database";
4215
+ }
4216
+ if (!(0, node_fs.existsSync)(stateFilePath)) return "missing-file";
4217
+ writeAppliedMigrationsState(stateFilePath, createEmptyAppliedMigrationsState());
4218
+ return "file";
4219
+ };
3328
4220
  const isMigrationApplied = (state, identity, checksum) => {
3329
4221
  const matched = state.migrations.find((migration) => migration.id === identity);
3330
4222
  if (!matched) return false;
@@ -3443,7 +4335,9 @@ var MigrateCommand = class extends _h3ravel_musket.Command {
3443
4335
  const classes = this.option("all") || !this.argument("name") ? await this.loadAllMigrations(migrationsDir) : (await this.loadNamedMigration(migrationsDir, this.argument("name"))).filter(([cls]) => cls !== void 0);
3444
4336
  if (classes.length === 0) return void this.error("Error: No migration classes found to run.");
3445
4337
  const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
3446
- let appliedState = readAppliedMigrationsState(stateFilePath);
4338
+ let appliedState = await readAppliedMigrationsStateFromStore(this.app.getConfig("adapter"), stateFilePath);
4339
+ const adapter = this.app.getConfig("adapter");
4340
+ const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
3447
4341
  const skipped = [];
3448
4342
  const changed = [];
3449
4343
  const pending = classes.filter(([migrationClass, file]) => {
@@ -3465,10 +4359,16 @@ var MigrateCommand = class extends _h3ravel_musket.Command {
3465
4359
  this.success("No pending migration classes to apply.");
3466
4360
  return;
3467
4361
  }
3468
- for (const [MigrationClassItem] of pending) await applyMigrationToPrismaSchema(MigrationClassItem, {
3469
- schemaPath,
3470
- write: true
3471
- });
4362
+ for (const [MigrationClassItem] of pending) {
4363
+ if (useDatabaseMigrations) {
4364
+ await applyMigrationToDatabase(adapter, MigrationClassItem);
4365
+ continue;
4366
+ }
4367
+ await applyMigrationToPrismaSchema(MigrationClassItem, {
4368
+ schemaPath,
4369
+ write: true
4370
+ });
4371
+ }
3472
4372
  if (appliedState) {
3473
4373
  const runAppliedIds = [];
3474
4374
  for (const [migrationClass, file] of pending) {
@@ -3487,10 +4387,10 @@ var MigrateCommand = class extends _h3ravel_musket.Command {
3487
4387
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
3488
4388
  migrationIds: runAppliedIds
3489
4389
  });
3490
- writeAppliedMigrationsState(stateFilePath, appliedState);
4390
+ await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
3491
4391
  }
3492
- if (!this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
3493
- if (!this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
4392
+ if (!useDatabaseMigrations && !this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
4393
+ if (!useDatabaseMigrations && !this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
3494
4394
  else runPrismaCommand([
3495
4395
  "migrate",
3496
4396
  "dev",
@@ -3550,6 +4450,85 @@ var MigrateCommand = class extends _h3ravel_musket.Command {
3550
4450
  }
3551
4451
  };
3552
4452
 
4453
+ //#endregion
4454
+ //#region src/cli/commands/MigrateFreshCommand.ts
4455
+ var MigrateFreshCommand = class extends _h3ravel_musket.Command {
4456
+ signature = `migrate:fresh
4457
+ {--skip-generate : Skip prisma generate}
4458
+ {--skip-migrate : Skip prisma database sync}
4459
+ {--state-file= : Path to applied migration state file}
4460
+ {--schema= : Explicit prisma schema path}
4461
+ `;
4462
+ description = "Reset the database and rerun all migration classes";
4463
+ async handle() {
4464
+ this.app.command = this;
4465
+ const configuredMigrationsDir = this.app.getConfig("paths")?.migrations ?? (0, node_path.join)(process.cwd(), "database", "migrations");
4466
+ const migrationsDir = this.app.resolveRuntimeDirectoryPath(configuredMigrationsDir);
4467
+ if (!(0, node_fs.existsSync)(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
4468
+ const adapter = this.app.getConfig("adapter");
4469
+ const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
4470
+ const schemaPath = this.option("schema") ? (0, node_path.resolve)(String(this.option("schema"))) : (0, node_path.join)(process.cwd(), "prisma", "schema.prisma");
4471
+ const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
4472
+ const migrations = await this.loadAllMigrations(migrationsDir);
4473
+ if (migrations.length === 0) return void this.error("Error: No migration classes found to run.");
4474
+ if (supportsDatabaseReset(adapter)) await adapter.resetDatabase();
4475
+ else {
4476
+ if (!(0, node_fs.existsSync)(schemaPath)) return void this.error(`Error: Prisma schema file not found: ${this.app.formatPathForLog(schemaPath)}`);
4477
+ (0, node_fs.writeFileSync)(schemaPath, stripPrismaSchemaModelsAndEnums((0, node_fs.readFileSync)(schemaPath, "utf-8")));
4478
+ }
4479
+ let appliedState = createEmptyAppliedMigrationsState();
4480
+ await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
4481
+ for (const [MigrationClassItem] of migrations) {
4482
+ if (useDatabaseMigrations) {
4483
+ await applyMigrationToDatabase(adapter, MigrationClassItem);
4484
+ continue;
4485
+ }
4486
+ await applyMigrationToPrismaSchema(MigrationClassItem, {
4487
+ schemaPath,
4488
+ write: true
4489
+ });
4490
+ }
4491
+ for (const [migrationClass, file] of migrations) appliedState = markMigrationApplied(appliedState, {
4492
+ id: buildMigrationIdentity(file, migrationClass.name),
4493
+ file,
4494
+ className: migrationClass.name,
4495
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
4496
+ checksum: computeMigrationChecksum(file)
4497
+ });
4498
+ appliedState = markMigrationRun(appliedState, {
4499
+ id: buildMigrationRunId(),
4500
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
4501
+ migrationIds: appliedState.migrations.map((migration) => migration.id)
4502
+ });
4503
+ await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
4504
+ if (!useDatabaseMigrations) {
4505
+ const schemaArgs = this.option("schema") ? ["--schema", schemaPath] : [];
4506
+ if (!this.option("skip-generate")) runPrismaCommand(["generate", ...schemaArgs], process.cwd());
4507
+ if (!this.option("skip-migrate")) runPrismaCommand([
4508
+ "db",
4509
+ "push",
4510
+ "--force-reset",
4511
+ ...schemaArgs
4512
+ ], process.cwd());
4513
+ }
4514
+ this.success(`Refreshed database with ${migrations.length} migration(s).`);
4515
+ migrations.forEach(([_, file]) => this.success(this.app.splitLogger("Migrated", file)));
4516
+ }
4517
+ async loadAllMigrations(migrationsDir) {
4518
+ const files = (0, node_fs.readdirSync)(migrationsDir).filter((file) => /\.(ts|js|mjs|cjs)$/i.test(file)).sort((left, right) => left.localeCompare(right)).map((file) => this.app.resolveRuntimeScriptPath((0, node_path.join)(migrationsDir, file)));
4519
+ return (await Promise.all(files.map(async (file) => (await this.loadMigrationClassesFromFile(file)).map((cls) => [cls, file])))).flat();
4520
+ }
4521
+ async loadMigrationClassesFromFile(filePath) {
4522
+ const imported = await RuntimeModuleLoader.load(filePath);
4523
+ return Object.values(imported).filter((value) => {
4524
+ if (typeof value !== "function") return false;
4525
+ const candidate = value;
4526
+ const prototype = candidate.prototype;
4527
+ return candidate[MIGRATION_BRAND] === true || typeof prototype?.up === "function" && typeof prototype?.down === "function";
4528
+ });
4529
+ }
4530
+ };
4531
+
3553
4532
  //#endregion
3554
4533
  //#region src/cli/commands/MigrateRollbackCommand.ts
3555
4534
  /**
@@ -3578,7 +4557,9 @@ var MigrateRollbackCommand = class extends _h3ravel_musket.Command {
3578
4557
  if (!(0, node_fs.existsSync)(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
3579
4558
  const schemaPath = this.option("schema") ? (0, node_path.resolve)(String(this.option("schema"))) : (0, node_path.join)(process.cwd(), "prisma", "schema.prisma");
3580
4559
  const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
3581
- let appliedState = readAppliedMigrationsState(stateFilePath);
4560
+ const adapter = this.app.getConfig("adapter");
4561
+ const useDatabaseMigrations = supportsDatabaseMigrationExecution(adapter);
4562
+ let appliedState = await readAppliedMigrationsStateFromStore(adapter, stateFilePath);
3582
4563
  const stepOption = this.option("step");
3583
4564
  const stepCount = stepOption == null ? void 0 : Number(stepOption);
3584
4565
  if (stepCount != null && (!Number.isFinite(stepCount) || stepCount <= 0 || !Number.isInteger(stepCount))) return void this.error("Error: --step must be a positive integer.");
@@ -3600,17 +4581,23 @@ var MigrateRollbackCommand = class extends _h3ravel_musket.Command {
3600
4581
  rollbackClasses.forEach(([_, file]) => this.success(this.app.splitLogger("WouldRollback", file)));
3601
4582
  return;
3602
4583
  }
3603
- for (const [MigrationClassItem] of rollbackClasses) await applyMigrationRollbackToPrismaSchema(MigrationClassItem, {
3604
- schemaPath,
3605
- write: true
3606
- });
4584
+ for (const [MigrationClassItem] of rollbackClasses) {
4585
+ if (useDatabaseMigrations) {
4586
+ await applyMigrationRollbackToDatabase(adapter, MigrationClassItem);
4587
+ continue;
4588
+ }
4589
+ await applyMigrationRollbackToPrismaSchema(MigrationClassItem, {
4590
+ schemaPath,
4591
+ write: true
4592
+ });
4593
+ }
3607
4594
  for (const [migrationClass, file] of rollbackClasses) {
3608
4595
  const identity = buildMigrationIdentity(file, migrationClass.name);
3609
4596
  appliedState = removeAppliedMigration(appliedState, identity);
3610
4597
  }
3611
- writeAppliedMigrationsState(stateFilePath, appliedState);
3612
- if (!this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
3613
- if (!this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
4598
+ await writeAppliedMigrationsStateToStore(adapter, stateFilePath, appliedState);
4599
+ if (!useDatabaseMigrations && !this.option("skip-generate")) runPrismaCommand(["generate"], process.cwd());
4600
+ if (!useDatabaseMigrations && !this.option("skip-migrate")) if (this.option("deploy")) runPrismaCommand(["migrate", "deploy"], process.cwd());
3614
4601
  else runPrismaCommand([
3615
4602
  "migrate",
3616
4603
  "dev",
@@ -3654,7 +4641,14 @@ var MigrationHistoryCommand = class extends _h3ravel_musket.Command {
3654
4641
  async handle() {
3655
4642
  this.app.command = this;
3656
4643
  const stateFilePath = resolveMigrationStateFilePath(process.cwd(), this.option("state-file") ? String(this.option("state-file")) : void 0);
4644
+ const adapter = this.app.getConfig("adapter");
4645
+ const usesDatabaseState = supportsDatabaseMigrationState(adapter);
3657
4646
  if (this.option("delete")) {
4647
+ if (usesDatabaseState) {
4648
+ await adapter.writeAppliedMigrationsState(createEmptyAppliedMigrationsState());
4649
+ this.success("Deleted tracked migration state from database.");
4650
+ return;
4651
+ }
3658
4652
  if (!(0, node_fs.existsSync)(stateFilePath)) {
3659
4653
  this.success(`No migration state file found at ${this.app.formatPathForLog(stateFilePath)}`);
3660
4654
  return;
@@ -3664,22 +4658,19 @@ var MigrationHistoryCommand = class extends _h3ravel_musket.Command {
3664
4658
  return;
3665
4659
  }
3666
4660
  if (this.option("reset")) {
3667
- writeAppliedMigrationsState(stateFilePath, {
3668
- version: 1,
3669
- migrations: []
3670
- });
3671
- this.success(`Reset migration state: ${this.app.formatPathForLog(stateFilePath)}`);
4661
+ await writeAppliedMigrationsStateToStore(adapter, stateFilePath, createEmptyAppliedMigrationsState());
4662
+ this.success(usesDatabaseState ? "Reset migration state in database." : `Reset migration state: ${this.app.formatPathForLog(stateFilePath)}`);
3672
4663
  return;
3673
4664
  }
3674
- const state = readAppliedMigrationsState(stateFilePath);
4665
+ const state = await readAppliedMigrationsStateFromStore(adapter, stateFilePath);
3675
4666
  if (this.option("json")) {
3676
4667
  this.success(JSON.stringify({
3677
- path: stateFilePath,
4668
+ path: usesDatabaseState ? "database" : stateFilePath,
3678
4669
  ...state
3679
4670
  }, null, 2));
3680
4671
  return;
3681
4672
  }
3682
- this.success(this.app.splitLogger("State", stateFilePath));
4673
+ this.success(this.app.splitLogger("State", usesDatabaseState ? "database" : stateFilePath));
3683
4674
  this.success(this.app.splitLogger("Tracked", String(state.migrations.length)));
3684
4675
  if (state.migrations.length === 0) {
3685
4676
  this.success("No tracked migrations found.");
@@ -3695,20 +4686,21 @@ var MigrationHistoryCommand = class extends _h3ravel_musket.Command {
3695
4686
  //#region src/cli/commands/ModelsSyncCommand.ts
3696
4687
  var ModelsSyncCommand = class extends _h3ravel_musket.Command {
3697
4688
  signature = `models:sync
3698
- {--schema= : Path to prisma schema file}
4689
+ {--schema= : Path to prisma schema file used when adapter introspection is unavailable}
3699
4690
  {--models= : Path to models directory}
3700
4691
  `;
3701
- description = "Sync model declare attributes from prisma schema for all model files";
4692
+ description = "Sync model declare attributes from the active adapter when supported, otherwise fall back to the Prisma schema";
3702
4693
  async handle() {
3703
4694
  this.app.command = this;
3704
- const result = this.app.syncModelsFromPrisma({
4695
+ const result = await this.app.syncModels({
3705
4696
  schemaPath: this.option("schema") ? (0, node_path.resolve)(String(this.option("schema"))) : void 0,
3706
4697
  modelsDir: this.option("models") ? (0, node_path.resolve)(String(this.option("models"))) : void 0
3707
4698
  });
3708
4699
  const updatedLines = result.updated.length === 0 ? [this.app.splitLogger("Updated", "none")] : result.updated.map((path) => this.app.splitLogger("Updated", path));
3709
4700
  this.success("SUCCESS: Model sync completed with the following results:");
3710
4701
  [
3711
- this.app.splitLogger("Schema", result.schemaPath),
4702
+ this.app.splitLogger("Source", result.source === "adapter" ? "adapter introspection" : "prisma schema"),
4703
+ ...result.schemaPath ? [this.app.splitLogger("Schema", result.schemaPath)] : [],
3712
4704
  this.app.splitLogger("Models", result.modelsDir),
3713
4705
  this.app.splitLogger("Processed", String(result.total)),
3714
4706
  ...updatedLines,
@@ -4068,6 +5060,13 @@ var UniqueConstraintResolutionException = class extends ArkormException {
4068
5060
 
4069
5061
  //#endregion
4070
5062
  //#region src/relationship/RelationTableLoader.ts
5063
+ /**
5064
+ * Utility class responsible for loading data from relation tables, which are used to
5065
+ * manage relationships between models in Arkorm.
5066
+ *
5067
+ * @author Legacy (3m1n3nc3)
5068
+ * @since 2.0.0-next.0
5069
+ */
4071
5070
  var RelationTableLoader = class {
4072
5071
  constructor(adapter) {
4073
5072
  this.adapter = adapter;
@@ -4829,6 +5828,472 @@ var MorphToManyRelation = class extends Relation {
4829
5828
  }
4830
5829
  };
4831
5830
 
5831
+ //#endregion
5832
+ //#region src/relationship/SetBasedEagerLoader.ts
5833
+ /**
5834
+ * Utility class responsible for performing set-based eager loading of relationships for
5835
+ * a collection of models.
5836
+ *
5837
+ * @author Legacy (3m1n3nc3)
5838
+ * @since 2.0.0-next.2
5839
+ */
5840
+ var SetBasedEagerLoader = class {
5841
+ constructor(models, relations) {
5842
+ this.models = models;
5843
+ this.relations = relations;
5844
+ }
5845
+ /**
5846
+ * Performs eager loading of all specified relationships for the set of models.
5847
+ *
5848
+ * @returns
5849
+ */
5850
+ async load() {
5851
+ if (this.models.length === 0) return;
5852
+ await Promise.all(Object.entries(this.relations).map(async ([name, constraint]) => {
5853
+ await this.loadRelation(name, constraint);
5854
+ }));
5855
+ }
5856
+ /**
5857
+ * Loads a specific relationship for the set of models based on the relationship name
5858
+ * and an optional constraint.
5859
+ *
5860
+ * @param name The name of the relationship to load.
5861
+ * @param constraint An optional constraint to apply to the query.
5862
+ * @returns A promise that resolves when the relationship is loaded.
5863
+ */
5864
+ async loadRelation(name, constraint) {
5865
+ const resolver = this.resolveRelationResolver(name);
5866
+ if (!resolver) return;
5867
+ const metadata = resolver.call(this.models[0]).getMetadata();
5868
+ switch (metadata.type) {
5869
+ case "belongsTo":
5870
+ await this.loadBelongsTo(name, resolver, metadata, constraint);
5871
+ return;
5872
+ case "belongsToMany":
5873
+ await this.loadBelongsToMany(name, metadata, constraint);
5874
+ return;
5875
+ case "hasMany":
5876
+ await this.loadHasMany(name, metadata, constraint);
5877
+ return;
5878
+ case "hasOne":
5879
+ await this.loadHasOne(name, resolver, metadata, constraint);
5880
+ return;
5881
+ case "hasManyThrough":
5882
+ await this.loadHasManyThrough(name, metadata, constraint);
5883
+ return;
5884
+ case "hasOneThrough":
5885
+ await this.loadHasOneThrough(name, resolver, metadata, constraint);
5886
+ return;
5887
+ default: await this.loadIndividually(name, resolver, constraint);
5888
+ }
5889
+ }
5890
+ /**
5891
+ * Resolves the relation resolver function for a given relationship name by inspecting
5892
+ * the first model in the set.
5893
+ *
5894
+ * @param name The name of the relationship to resolve.
5895
+ * @returns The relation resolver function or null if not found.
5896
+ */
5897
+ resolveRelationResolver(name) {
5898
+ const resolver = this.models[0][name];
5899
+ if (typeof resolver !== "function") return null;
5900
+ return resolver;
5901
+ }
5902
+ /**
5903
+ * Loads a "belongs to" relationship for the set of models.
5904
+ *
5905
+ * @param name The name of the relationship to load.
5906
+ * @param resolver The relation resolver function.
5907
+ * @param metadata The metadata for the relationship.
5908
+ * @param constraint An optional constraint to apply to the query.
5909
+ * @returns A promise that resolves when the relationship is loaded.
5910
+ */
5911
+ async loadBelongsTo(name, resolver, metadata, constraint) {
5912
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.foreignKey));
5913
+ if (keys.length === 0) {
5914
+ this.models.forEach((model) => {
5915
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
5916
+ });
5917
+ return;
5918
+ }
5919
+ let query = metadata.relatedModel.query().whereIn(metadata.ownerKey, keys);
5920
+ query = this.applyConstraint(query, constraint);
5921
+ const relatedModels = (await query.get()).all();
5922
+ const relatedByOwnerKey = /* @__PURE__ */ new Map();
5923
+ relatedModels.forEach((related) => {
5924
+ const value = this.readModelAttribute(related, metadata.ownerKey);
5925
+ if (value == null) return;
5926
+ const lookupKey = this.toLookupKey(value);
5927
+ if (!relatedByOwnerKey.has(lookupKey)) relatedByOwnerKey.set(lookupKey, related);
5928
+ });
5929
+ this.models.forEach((model) => {
5930
+ const foreignValue = model.getAttribute(metadata.foreignKey);
5931
+ const relationValue = foreignValue == null ? void 0 : relatedByOwnerKey.get(this.toLookupKey(foreignValue));
5932
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
5933
+ });
5934
+ }
5935
+ /**
5936
+ * Loads a "has many" relationship for the set of models.
5937
+ *
5938
+ * @param name
5939
+ * @param metadata
5940
+ * @param constraint
5941
+ * @returns
5942
+ */
5943
+ async loadHasMany(name, metadata, constraint) {
5944
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
5945
+ if (keys.length === 0) {
5946
+ this.models.forEach((model) => {
5947
+ model.setLoadedRelation(name, new ArkormCollection([]));
5948
+ });
5949
+ return;
5950
+ }
5951
+ let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
5952
+ query = this.applyConstraint(query, constraint);
5953
+ const relatedModels = (await query.get()).all();
5954
+ const relatedByForeignKey = /* @__PURE__ */ new Map();
5955
+ relatedModels.forEach((related) => {
5956
+ const value = this.readModelAttribute(related, metadata.foreignKey);
5957
+ if (value == null) return;
5958
+ const lookupKey = this.toLookupKey(value);
5959
+ const bucket = relatedByForeignKey.get(lookupKey) ?? [];
5960
+ bucket.push(related);
5961
+ relatedByForeignKey.set(lookupKey, bucket);
5962
+ });
5963
+ this.models.forEach((model) => {
5964
+ const localValue = model.getAttribute(metadata.localKey);
5965
+ const related = localValue == null ? [] : relatedByForeignKey.get(this.toLookupKey(localValue)) ?? [];
5966
+ model.setLoadedRelation(name, new ArkormCollection(related));
5967
+ });
5968
+ }
5969
+ /**
5970
+ * Loads a "belongs to many" relationship for the set of models.
5971
+ *
5972
+ * @param name
5973
+ * @param metadata
5974
+ * @param constraint
5975
+ * @returns
5976
+ */
5977
+ async loadBelongsToMany(name, metadata, constraint) {
5978
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.parentKey));
5979
+ if (parentKeys.length === 0) {
5980
+ this.models.forEach((model) => {
5981
+ model.setLoadedRelation(name, new ArkormCollection([]));
5982
+ });
5983
+ return;
5984
+ }
5985
+ const pivotRows = await this.createRelationTableLoader().selectRows({
5986
+ table: metadata.throughTable,
5987
+ where: {
5988
+ type: "comparison",
5989
+ column: metadata.foreignPivotKey,
5990
+ operator: "in",
5991
+ value: parentKeys
5992
+ }
5993
+ });
5994
+ const relatedIds = this.collectUniqueRowValues(pivotRows, metadata.relatedPivotKey);
5995
+ if (relatedIds.length === 0) {
5996
+ this.models.forEach((model) => {
5997
+ model.setLoadedRelation(name, new ArkormCollection([]));
5998
+ });
5999
+ return;
6000
+ }
6001
+ let query = metadata.relatedModel.query().whereIn(metadata.relatedKey, relatedIds);
6002
+ query = this.applyConstraint(query, constraint);
6003
+ const relatedModels = (await query.get()).all();
6004
+ const relatedByKey = /* @__PURE__ */ new Map();
6005
+ relatedModels.forEach((related) => {
6006
+ const relatedValue = this.readModelAttribute(related, metadata.relatedKey);
6007
+ if (relatedValue == null) return;
6008
+ relatedByKey.set(this.toLookupKey(relatedValue), related);
6009
+ });
6010
+ const relatedKeysByParent = /* @__PURE__ */ new Map();
6011
+ pivotRows.forEach((row) => {
6012
+ const parentValue = row[metadata.foreignPivotKey];
6013
+ const relatedValue = row[metadata.relatedPivotKey];
6014
+ if (parentValue == null || relatedValue == null) return;
6015
+ const bucket = relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? [];
6016
+ bucket.push(relatedValue);
6017
+ relatedKeysByParent.set(this.toLookupKey(parentValue), bucket);
6018
+ });
6019
+ this.models.forEach((model) => {
6020
+ const parentValue = model.getAttribute(metadata.parentKey);
6021
+ const related = (parentValue == null ? [] : relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? []).reduce((all, relatedValue) => {
6022
+ const candidate = relatedByKey.get(this.toLookupKey(relatedValue));
6023
+ if (candidate) all.push(candidate);
6024
+ return all;
6025
+ }, []);
6026
+ model.setLoadedRelation(name, new ArkormCollection(related));
6027
+ });
6028
+ }
6029
+ /**
6030
+ * Loads a "belongs to many" relationship for the set of models.
6031
+ *
6032
+ * @param name
6033
+ * @param resolver
6034
+ * @param metadata
6035
+ * @param constraint
6036
+ * @returns
6037
+ */
6038
+ async loadHasOne(name, resolver, metadata, constraint) {
6039
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
6040
+ if (keys.length === 0) {
6041
+ this.models.forEach((model) => {
6042
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
6043
+ });
6044
+ return;
6045
+ }
6046
+ let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
6047
+ query = this.applyConstraint(query, constraint);
6048
+ const relatedModels = (await query.get()).all();
6049
+ const relatedByForeignKey = /* @__PURE__ */ new Map();
6050
+ relatedModels.forEach((related) => {
6051
+ const value = this.readModelAttribute(related, metadata.foreignKey);
6052
+ if (value == null) return;
6053
+ const lookupKey = this.toLookupKey(value);
6054
+ if (!relatedByForeignKey.has(lookupKey)) relatedByForeignKey.set(lookupKey, related);
6055
+ });
6056
+ this.models.forEach((model) => {
6057
+ const localValue = model.getAttribute(metadata.localKey);
6058
+ const relationValue = localValue == null ? void 0 : relatedByForeignKey.get(this.toLookupKey(localValue));
6059
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
6060
+ });
6061
+ }
6062
+ /**
6063
+ * Loads a "has many through" relationship for the set of models.
6064
+ *
6065
+ * @param name
6066
+ * @param metadata
6067
+ * @param constraint
6068
+ * @returns
6069
+ */
6070
+ async loadHasManyThrough(name, metadata, constraint) {
6071
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
6072
+ if (parentKeys.length === 0) {
6073
+ this.models.forEach((model) => {
6074
+ model.setLoadedRelation(name, new ArkormCollection([]));
6075
+ });
6076
+ return;
6077
+ }
6078
+ const throughRows = await this.createRelationTableLoader().selectRows({
6079
+ table: metadata.throughTable,
6080
+ where: {
6081
+ type: "comparison",
6082
+ column: metadata.firstKey,
6083
+ operator: "in",
6084
+ value: parentKeys
6085
+ }
6086
+ });
6087
+ const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
6088
+ if (intermediateKeys.length === 0) {
6089
+ this.models.forEach((model) => {
6090
+ model.setLoadedRelation(name, new ArkormCollection([]));
6091
+ });
6092
+ return;
6093
+ }
6094
+ let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
6095
+ query = this.applyConstraint(query, constraint);
6096
+ const relatedModels = (await query.get()).all();
6097
+ const relatedByIntermediate = /* @__PURE__ */ new Map();
6098
+ relatedModels.forEach((related) => {
6099
+ const relatedValue = this.readModelAttribute(related, metadata.secondKey);
6100
+ if (relatedValue == null) return;
6101
+ const bucket = relatedByIntermediate.get(this.toLookupKey(relatedValue)) ?? [];
6102
+ bucket.push(related);
6103
+ relatedByIntermediate.set(this.toLookupKey(relatedValue), bucket);
6104
+ });
6105
+ const intermediateByParent = /* @__PURE__ */ new Map();
6106
+ throughRows.forEach((row) => {
6107
+ const parentValue = row[metadata.firstKey];
6108
+ const intermediateValue = row[metadata.secondLocalKey];
6109
+ if (parentValue == null || intermediateValue == null) return;
6110
+ const bucket = intermediateByParent.get(this.toLookupKey(parentValue)) ?? [];
6111
+ bucket.push(intermediateValue);
6112
+ intermediateByParent.set(this.toLookupKey(parentValue), bucket);
6113
+ });
6114
+ this.models.forEach((model) => {
6115
+ const parentValue = model.getAttribute(metadata.localKey);
6116
+ const related = (parentValue == null ? [] : intermediateByParent.get(this.toLookupKey(parentValue)) ?? []).flatMap((intermediateValue) => relatedByIntermediate.get(this.toLookupKey(intermediateValue)) ?? []);
6117
+ model.setLoadedRelation(name, new ArkormCollection(related));
6118
+ });
6119
+ }
6120
+ /**
6121
+ * Loads a "has one through" relationship for the set of models.
6122
+ *
6123
+ * @param name
6124
+ * @param resolver
6125
+ * @param metadata
6126
+ * @param constraint
6127
+ * @returns
6128
+ */
6129
+ async loadHasOneThrough(name, resolver, metadata, constraint) {
6130
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
6131
+ if (parentKeys.length === 0) {
6132
+ this.models.forEach((model) => {
6133
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
6134
+ });
6135
+ return;
6136
+ }
6137
+ const throughRows = await this.createRelationTableLoader().selectRows({
6138
+ table: metadata.throughTable,
6139
+ where: {
6140
+ type: "comparison",
6141
+ column: metadata.firstKey,
6142
+ operator: "in",
6143
+ value: parentKeys
6144
+ }
6145
+ });
6146
+ const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
6147
+ if (intermediateKeys.length === 0) {
6148
+ this.models.forEach((model) => {
6149
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
6150
+ });
6151
+ return;
6152
+ }
6153
+ let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
6154
+ query = this.applyConstraint(query, constraint);
6155
+ const relatedModels = (await query.get()).all();
6156
+ const relatedByIntermediate = /* @__PURE__ */ new Map();
6157
+ relatedModels.forEach((related) => {
6158
+ const relatedValue = this.readModelAttribute(related, metadata.secondKey);
6159
+ if (relatedValue == null) return;
6160
+ const lookupKey = this.toLookupKey(relatedValue);
6161
+ if (!relatedByIntermediate.has(lookupKey)) relatedByIntermediate.set(lookupKey, related);
6162
+ });
6163
+ const intermediateByParent = /* @__PURE__ */ new Map();
6164
+ throughRows.forEach((row) => {
6165
+ const parentValue = row[metadata.firstKey];
6166
+ const intermediateValue = row[metadata.secondLocalKey];
6167
+ if (parentValue == null || intermediateValue == null) return;
6168
+ const lookupKey = this.toLookupKey(parentValue);
6169
+ if (!intermediateByParent.has(lookupKey)) intermediateByParent.set(lookupKey, intermediateValue);
6170
+ });
6171
+ this.models.forEach((model) => {
6172
+ const parentValue = model.getAttribute(metadata.localKey);
6173
+ const intermediateValue = parentValue == null ? void 0 : intermediateByParent.get(this.toLookupKey(parentValue));
6174
+ const relationValue = intermediateValue == null ? void 0 : relatedByIntermediate.get(this.toLookupKey(intermediateValue));
6175
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
6176
+ });
6177
+ }
6178
+ /**
6179
+ * Fallback method to load relationships individually for each model when the
6180
+ * relationship type is not supported for set-based loading.
6181
+ *
6182
+ * @param name
6183
+ * @param resolver
6184
+ * @param constraint
6185
+ */
6186
+ async loadIndividually(name, resolver, constraint) {
6187
+ await Promise.all(this.models.map(async (model) => {
6188
+ const relation = resolver.call(model);
6189
+ if (constraint) relation.constrain(constraint);
6190
+ model.setLoadedRelation(name, await relation.getResults());
6191
+ }));
6192
+ }
6193
+ /**
6194
+ * Applies an eager load constraint to a query if provided.
6195
+ *
6196
+ * @param query
6197
+ * @param constraint
6198
+ * @returns
6199
+ */
6200
+ applyConstraint(query, constraint) {
6201
+ if (!constraint) return query;
6202
+ return constraint(query) ?? query;
6203
+ }
6204
+ /**
6205
+ * Collects unique values from the set of models based on a resolver function, which
6206
+ * is used to extract the value from each model.
6207
+ *
6208
+ * @param resolve A function that takes a model and returns the value to be collected.
6209
+ * @returns An array of unique values.
6210
+ */
6211
+ collectUniqueKeys(resolve) {
6212
+ const seen = /* @__PURE__ */ new Set();
6213
+ const values = [];
6214
+ this.models.forEach((model) => {
6215
+ const value = resolve(model);
6216
+ if (value == null) return;
6217
+ const lookupKey = this.toLookupKey(value);
6218
+ if (seen.has(lookupKey)) return;
6219
+ seen.add(lookupKey);
6220
+ values.push(value);
6221
+ });
6222
+ return values;
6223
+ }
6224
+ /**
6225
+ * Collects unique values from an array of database rows based on a specified key, which
6226
+ * is used to extract the value from each row.
6227
+ *
6228
+ * @param rows An array of database rows.
6229
+ * @param key The key to extract values from each row.
6230
+ * @returns An array of unique values.
6231
+ */
6232
+ collectUniqueRowValues(rows, key) {
6233
+ const seen = /* @__PURE__ */ new Set();
6234
+ const values = [];
6235
+ rows.forEach((row) => {
6236
+ const value = row[key];
6237
+ if (value == null) return;
6238
+ const lookupKey = this.toLookupKey(value);
6239
+ if (seen.has(lookupKey)) return;
6240
+ seen.add(lookupKey);
6241
+ values.push(value);
6242
+ });
6243
+ return values;
6244
+ }
6245
+ /**
6246
+ * Loads a "belongs to many" relationship for the set of models.
6247
+ *
6248
+ * @returns
6249
+ */
6250
+ createRelationTableLoader() {
6251
+ return new RelationTableLoader(this.resolveAdapter());
6252
+ }
6253
+ /**
6254
+ * Loads a "belongs to many" relationship for the set of models.
6255
+ *
6256
+ * @returns
6257
+ */
6258
+ resolveAdapter() {
6259
+ const adapter = this.models[0].constructor.getAdapter?.();
6260
+ if (!adapter) throw new Error("Set-based eager loading requires a configured adapter.");
6261
+ return adapter;
6262
+ }
6263
+ /**
6264
+ * Reads an attribute value from a model using the getAttribute method, which is used
6265
+ * to access model attributes in a way that is compatible with Arkorm's internal model structure.
6266
+ *
6267
+ * @param model The model to read the attribute from.
6268
+ * @param key The name of the attribute to read.
6269
+ * @returns
6270
+ */
6271
+ readModelAttribute(model, key) {
6272
+ return model.getAttribute?.(key);
6273
+ }
6274
+ /**
6275
+ * Resolves the default result for a relationship when no related models are found.
6276
+ *
6277
+ * @param resolver
6278
+ * @param model
6279
+ * @returns
6280
+ */
6281
+ resolveSingleDefault(resolver, model) {
6282
+ return resolver.call(model).resolveDefaultResult?.() ?? null;
6283
+ }
6284
+ /**
6285
+ * Generates a unique lookup key for a given value, which is used to store and retrieve
6286
+ * values in maps during the eager loading process.
6287
+ *
6288
+ * @param value The value to generate a lookup key for.
6289
+ * @returns A unique string representing the value.
6290
+ */
6291
+ toLookupKey(value) {
6292
+ if (value instanceof Date) return `date:${value.toISOString()}`;
6293
+ return `${typeof value}:${String(value)}`;
6294
+ }
6295
+ };
6296
+
4832
6297
  //#endregion
4833
6298
  //#region src/URLDriver.ts
4834
6299
  /**
@@ -5727,21 +7192,20 @@ var QueryBuilder = class QueryBuilder {
5727
7192
  * @returns
5728
7193
  */
5729
7194
  async get() {
7195
+ const useAdapterRelationFeatures = this.canExecuteRelationFeaturesInAdapter();
5730
7196
  const relationCache = /* @__PURE__ */ new WeakMap();
5731
7197
  const rows = await this.executeReadRows();
5732
7198
  const normalizedRows = this.randomOrderEnabled ? this.shuffleRows(rows) : rows;
5733
7199
  const models = await this.model.hydrateManyRetrieved(normalizedRows);
5734
7200
  let filteredModels = models;
5735
- if (this.hasRelationFilters()) if (this.hasOrRelationFilters() && this.hasBaseWhereConstraints()) {
7201
+ if (this.hasRelationFilters() && !useAdapterRelationFeatures) if (this.hasOrRelationFilters() && this.hasBaseWhereConstraints()) {
5736
7202
  const baseIds = new Set(models.map((model) => this.getModelId(model)).filter((id) => id != null));
5737
7203
  const allRows = await this.executeReadRows(this.buildSoftDeleteOnlyWhere(), true);
5738
7204
  const allModels = this.model.hydrateMany(allRows);
5739
7205
  filteredModels = await this.filterModelsByRelationConstraints(allModels, relationCache, baseIds);
5740
7206
  } else filteredModels = await this.filterModelsByRelationConstraints(models, relationCache);
5741
- if (this.hasRelationAggregates()) await this.applyRelationAggregates(filteredModels, relationCache);
5742
- await Promise.all(filteredModels.map(async (model) => {
5743
- await model.load(this.eagerLoads);
5744
- }));
7207
+ if (this.hasRelationAggregates() && !useAdapterRelationFeatures) await this.applyRelationAggregates(filteredModels, relationCache);
7208
+ await this.eagerLoadModels(filteredModels);
5745
7209
  return new ArkormCollection(filteredModels);
5746
7210
  }
5747
7211
  /**
@@ -5751,20 +7215,20 @@ var QueryBuilder = class QueryBuilder {
5751
7215
  * @returns
5752
7216
  */
5753
7217
  async first() {
5754
- if (this.hasRelationFilters() || this.hasRelationAggregates()) return (await this.get()).all()[0] ?? null;
7218
+ if ((this.hasRelationFilters() || this.hasRelationAggregates()) && !this.canExecuteRelationFeaturesInAdapter()) return (await this.get()).all()[0] ?? null;
5755
7219
  if (this.randomOrderEnabled) {
5756
7220
  const rows = await this.executeReadRows();
5757
7221
  if (rows.length === 0) return null;
5758
7222
  const row = this.shuffleRows(rows)[0];
5759
7223
  if (!row) return null;
5760
7224
  const model = await this.model.hydrateRetrieved(row);
5761
- await model.load(this.eagerLoads);
7225
+ await this.eagerLoadModels([model]);
5762
7226
  return model;
5763
7227
  }
5764
7228
  const row = await this.executeReadRow();
5765
7229
  if (!row) return null;
5766
7230
  const model = await this.model.hydrateRetrieved(row);
5767
- await model.load(this.eagerLoads);
7231
+ await this.eagerLoadModels([model]);
5768
7232
  return model;
5769
7233
  }
5770
7234
  /**
@@ -5928,6 +7392,13 @@ var QueryBuilder = class QueryBuilder {
5928
7392
  operation: "update",
5929
7393
  model: this.model.name
5930
7394
  });
7395
+ const directSpec = this.tryBuildUpdateSpec(where, data);
7396
+ const adapter = this.requireAdapter();
7397
+ if (!this.isUniqueWhere(where) && directSpec && typeof adapter.updateFirst === "function") {
7398
+ const updated = await adapter.updateFirst(directSpec);
7399
+ if (!updated) throw new ModelNotFoundException(this.model.name, "Record not found for update operation.", { operation: "update" });
7400
+ return this.model.hydrate(updated);
7401
+ }
5931
7402
  const uniqueWhere = await this.resolveUniqueWhere(where);
5932
7403
  const updated = await this.executeUpdateRow(uniqueWhere, data);
5933
7404
  return this.model.hydrate(updated);
@@ -5954,6 +7425,13 @@ var QueryBuilder = class QueryBuilder {
5954
7425
  * @returns
5955
7426
  */
5956
7427
  async updateOrInsert(attributes, values = {}) {
7428
+ if (typeof values !== "function" && this.adapter?.capabilities?.upsert && typeof this.requireAdapter().upsert === "function") {
7429
+ await this.executeUpsertRows([{
7430
+ ...attributes,
7431
+ ...values
7432
+ }], Object.keys(attributes), Object.keys(values));
7433
+ return true;
7434
+ }
5957
7435
  const exists = await this.clone().where(attributes).first() != null;
5958
7436
  const resolvedValues = typeof values === "function" ? await values(exists) : values;
5959
7437
  if (!exists) {
@@ -5976,6 +7454,7 @@ var QueryBuilder = class QueryBuilder {
5976
7454
  async upsert(values, uniqueBy, update = null) {
5977
7455
  if (values.length === 0) return 0;
5978
7456
  const uniqueKeys = Array.isArray(uniqueBy) ? uniqueBy : [uniqueBy];
7457
+ if (this.adapter?.capabilities?.upsert && typeof this.requireAdapter().upsert === "function") return await this.executeUpsertRows(values, uniqueKeys, update ?? void 0);
5979
7458
  let affected = 0;
5980
7459
  for (const row of values) {
5981
7460
  const attributes = uniqueKeys.reduce((all, key) => {
@@ -6003,6 +7482,13 @@ var QueryBuilder = class QueryBuilder {
6003
7482
  operation: "delete",
6004
7483
  model: this.model.name
6005
7484
  });
7485
+ const directSpec = this.tryBuildDeleteSpec(where);
7486
+ const adapter = this.requireAdapter();
7487
+ if (!this.isUniqueWhere(where) && directSpec && typeof adapter.deleteFirst === "function") {
7488
+ const deleted = await adapter.deleteFirst(directSpec);
7489
+ if (!deleted) throw new ModelNotFoundException(this.model.name, "Record not found for delete operation.", { operation: "delete" });
7490
+ return this.model.hydrate(deleted);
7491
+ }
6006
7492
  const uniqueWhere = await this.resolveUniqueWhere(where);
6007
7493
  const deleted = await this.executeDeleteRow(uniqueWhere);
6008
7494
  return this.model.hydrate(deleted);
@@ -6019,6 +7505,14 @@ var QueryBuilder = class QueryBuilder {
6019
7505
  values
6020
7506
  };
6021
7507
  }
7508
+ tryBuildUpsertSpec(values, uniqueBy, updateColumns) {
7509
+ return {
7510
+ target: this.buildQueryTarget(),
7511
+ values,
7512
+ uniqueBy,
7513
+ updateColumns
7514
+ };
7515
+ }
6022
7516
  tryBuildInsertOrIgnoreManySpec(values) {
6023
7517
  return {
6024
7518
  ...this.tryBuildInsertManySpec(values),
@@ -6057,7 +7551,7 @@ var QueryBuilder = class QueryBuilder {
6057
7551
  * @returns
6058
7552
  */
6059
7553
  async count() {
6060
- if (this.hasRelationFilters()) return (await this.get()).all().length;
7554
+ if (this.hasRelationFilters() && !this.canExecuteRelationFeaturesInAdapter()) return (await this.get()).all().length;
6061
7555
  return this.executeReadCount();
6062
7556
  }
6063
7557
  /**
@@ -6066,7 +7560,7 @@ var QueryBuilder = class QueryBuilder {
6066
7560
  * @returns
6067
7561
  */
6068
7562
  async exists() {
6069
- if (this.hasRelationFilters()) return await this.count() > 0;
7563
+ if (this.hasRelationFilters() && !this.canExecuteRelationFeaturesInAdapter()) return await this.count() > 0;
6070
7564
  return await this.executeReadExists();
6071
7565
  }
6072
7566
  /**
@@ -6241,7 +7735,7 @@ var QueryBuilder = class QueryBuilder {
6241
7735
  */
6242
7736
  async paginate(perPage = 15, page = void 0, options = {}) {
6243
7737
  const currentPage = this.resolvePaginationPage(page, options);
6244
- if (this.hasRelationFilters() || this.hasRelationAggregates()) {
7738
+ if ((this.hasRelationFilters() || this.hasRelationAggregates()) && !this.canExecuteRelationFeaturesInAdapter()) {
6245
7739
  const pageSize = Math.max(1, perPage);
6246
7740
  const rows = (await this.get()).all();
6247
7741
  const start = (currentPage - 1) * pageSize;
@@ -6260,7 +7754,7 @@ var QueryBuilder = class QueryBuilder {
6260
7754
  */
6261
7755
  async simplePaginate(perPage = 15, page = void 0, options = {}) {
6262
7756
  const currentPage = this.resolvePaginationPage(page, options);
6263
- if (this.hasRelationFilters() || this.hasRelationAggregates()) {
7757
+ if ((this.hasRelationFilters() || this.hasRelationAggregates()) && !this.canExecuteRelationFeaturesInAdapter()) {
6264
7758
  const pageSize = Math.max(1, perPage);
6265
7759
  const rows = (await this.get()).all();
6266
7760
  const start = (currentPage - 1) * pageSize;
@@ -6364,6 +7858,10 @@ var QueryBuilder = class QueryBuilder {
6364
7858
  };
6365
7859
  });
6366
7860
  }
7861
+ async eagerLoadModels(models) {
7862
+ if (models.length === 0 || Object.keys(this.eagerLoads).length === 0) return;
7863
+ await new SetBasedEagerLoader(models, this.eagerLoads).load();
7864
+ }
6367
7865
  normalizeRelationLoadSelect(select) {
6368
7866
  if (Array.isArray(select) || typeof select !== "object" || !select) return null;
6369
7867
  const entries = Object.entries(select);
@@ -6601,7 +8099,11 @@ var QueryBuilder = class QueryBuilder {
6601
8099
  const columns = this.tryBuildQuerySelectColumns();
6602
8100
  const orderBy = this.tryBuildQueryOrderBy();
6603
8101
  const condition = this.buildQueryWhereCondition(softDeleteOnly);
8102
+ const relationFilters = this.tryBuildRelationFilterSpecs();
8103
+ const relationAggregates = this.tryBuildRelationAggregateSpecs();
6604
8104
  if (columns === null || orderBy === null || condition === null) return null;
8105
+ if (this.hasRelationFilters() && this.canExecuteRelationFiltersInAdapter() && relationFilters === null) return null;
8106
+ if (this.hasRelationAggregates() && this.canExecuteRelationAggregatesInAdapter() && relationAggregates === null) return null;
6605
8107
  return {
6606
8108
  target: this.buildQueryTarget(),
6607
8109
  columns,
@@ -6609,15 +8111,20 @@ var QueryBuilder = class QueryBuilder {
6609
8111
  orderBy,
6610
8112
  limit: this.limitValue,
6611
8113
  offset: this.offsetValue,
6612
- relationLoads: this.queryRelationLoads
8114
+ relationLoads: this.queryRelationLoads,
8115
+ relationFilters: this.canExecuteRelationFiltersInAdapter() ? relationFilters ?? void 0 : void 0,
8116
+ relationAggregates: this.canExecuteRelationAggregatesInAdapter() ? relationAggregates ?? void 0 : void 0
6613
8117
  };
6614
8118
  }
6615
8119
  tryBuildAggregateSpec() {
6616
8120
  const condition = this.buildQueryWhereCondition(false);
8121
+ const relationFilters = this.tryBuildRelationFilterSpecs();
6617
8122
  if (condition === null) return null;
8123
+ if (this.hasRelationFilters() && this.canExecuteRelationFiltersInAdapter() && relationFilters === null) return null;
6618
8124
  return {
6619
8125
  target: this.buildQueryTarget(),
6620
8126
  where: condition,
8127
+ relationFilters: this.canExecuteRelationFiltersInAdapter() ? relationFilters ?? void 0 : void 0,
6621
8128
  aggregate: { type: "count" }
6622
8129
  };
6623
8130
  }
@@ -6687,6 +8194,14 @@ var QueryBuilder = class QueryBuilder {
6687
8194
  }
6688
8195
  return inserted;
6689
8196
  }
8197
+ async executeUpsertRows(values, uniqueBy, updateColumns) {
8198
+ const adapter = this.requireAdapter();
8199
+ if (typeof adapter.upsert !== "function") throw new UnsupportedAdapterFeatureException("Upsert is not supported by the current adapter.", {
8200
+ operation: "query.upsert",
8201
+ model: this.model.name
8202
+ });
8203
+ return await adapter.upsert(this.tryBuildUpsertSpec(values, uniqueBy, updateColumns));
8204
+ }
6690
8205
  async executeUpdateRow(where, values) {
6691
8206
  const adapter = this.requireAdapter();
6692
8207
  const spec = this.tryBuildUpdateSpec(where, values);
@@ -6815,6 +8330,71 @@ var QueryBuilder = class QueryBuilder {
6815
8330
  hasRelationAggregates() {
6816
8331
  return this.relationAggregates.length > 0;
6817
8332
  }
8333
+ canExecuteRelationFiltersInAdapter() {
8334
+ const adapter = this.adapter;
8335
+ if (!this.hasRelationFilters()) return false;
8336
+ return adapter?.capabilities?.relationFilters === true && this.tryBuildRelationFilterSpecs() !== null;
8337
+ }
8338
+ canExecuteRelationAggregatesInAdapter() {
8339
+ const adapter = this.adapter;
8340
+ if (!this.hasRelationAggregates()) return false;
8341
+ return adapter?.capabilities?.relationAggregates === true && this.tryBuildRelationAggregateSpecs() !== null;
8342
+ }
8343
+ canExecuteRelationFeaturesInAdapter() {
8344
+ const filtersSupported = !this.hasRelationFilters() || this.canExecuteRelationFiltersInAdapter();
8345
+ const aggregatesSupported = !this.hasRelationAggregates() || this.canExecuteRelationAggregatesInAdapter();
8346
+ return filtersSupported && aggregatesSupported;
8347
+ }
8348
+ tryBuildRelationFilterSpecs() {
8349
+ return this.relationFilters.reduce((specs, filter) => {
8350
+ if (!specs) return null;
8351
+ const metadata = this.model.getRelationMetadata(filter.relation);
8352
+ if (!this.isSqlRelationFeatureMetadata(metadata)) return null;
8353
+ const where = this.tryBuildRelationConstraintWhere(filter.relation, filter.callback);
8354
+ if (where === null) return null;
8355
+ specs.push({
8356
+ relation: filter.relation,
8357
+ operator: filter.operator,
8358
+ count: filter.count,
8359
+ boolean: filter.boolean,
8360
+ where
8361
+ });
8362
+ return specs;
8363
+ }, []);
8364
+ }
8365
+ tryBuildRelationAggregateSpecs() {
8366
+ return this.relationAggregates.reduce((specs, aggregate) => {
8367
+ if (!specs) return null;
8368
+ const metadata = this.model.getRelationMetadata(aggregate.relation);
8369
+ if (!this.isSqlRelationFeatureMetadata(metadata)) return null;
8370
+ const where = this.tryBuildRelationConstraintWhere(aggregate.relation);
8371
+ if (where === null) return null;
8372
+ specs.push({
8373
+ relation: aggregate.relation,
8374
+ type: aggregate.type,
8375
+ column: aggregate.column,
8376
+ alias: this.buildAggregateAttributeKey(aggregate),
8377
+ where
8378
+ });
8379
+ return specs;
8380
+ }, []);
8381
+ }
8382
+ tryBuildRelationConstraintWhere(relation, callback) {
8383
+ const metadata = this.model.getRelationMetadata(relation);
8384
+ if (!this.isSqlRelationFeatureMetadata(metadata)) return null;
8385
+ const relatedQuery = metadata?.relatedModel.query();
8386
+ if (!relatedQuery) return null;
8387
+ if (callback) {
8388
+ const constrained = callback(relatedQuery);
8389
+ if (constrained && constrained !== relatedQuery) return null;
8390
+ }
8391
+ if (relatedQuery.hasRelationFilters() || relatedQuery.hasRelationAggregates() || relatedQuery.queryRelationLoads || relatedQuery.querySelect || relatedQuery.queryOrderBy || relatedQuery.offsetValue !== void 0 || relatedQuery.limitValue !== void 0 || relatedQuery.randomOrderEnabled) return null;
8392
+ return relatedQuery.buildQueryWhereCondition(false);
8393
+ }
8394
+ isSqlRelationFeatureMetadata(metadata) {
8395
+ if (!metadata) return false;
8396
+ return metadata.type === "hasMany" || metadata.type === "hasOne" || metadata.type === "belongsTo" || metadata.type === "belongsToMany" || metadata.type === "hasOneThrough" || metadata.type === "hasManyThrough";
8397
+ }
6818
8398
  async filterModelsByRelationConstraints(models, relationCache, baseIds) {
6819
8399
  return (await Promise.all(models.map(async (model) => {
6820
8400
  let result = null;
@@ -6971,6 +8551,7 @@ var QueryBuilder = class QueryBuilder {
6971
8551
  */
6972
8552
  var Model = class Model {
6973
8553
  static lifecycleStates = /* @__PURE__ */ new WeakMap();
8554
+ static emittedDeprecationWarnings = /* @__PURE__ */ new Set();
6974
8555
  static eventsSuppressed = 0;
6975
8556
  static factoryClass;
6976
8557
  static adapter;
@@ -7014,12 +8595,24 @@ var Model = class Model {
7014
8595
  }
7015
8596
  });
7016
8597
  }
8598
+ static emitDeprecationWarning(code, message) {
8599
+ if (Model.emittedDeprecationWarnings.has(code)) return;
8600
+ Model.emittedDeprecationWarnings.add(code);
8601
+ process.emitWarning(message, {
8602
+ type: "DeprecationWarning",
8603
+ code
8604
+ });
8605
+ }
7017
8606
  /**
7018
- * Set the Prisma client delegates for all models.
7019
- *
7020
- * @param client
8607
+ * Set the Prisma client delegates for all models.
8608
+ *
8609
+ * @deprecated Use Model.setAdapter(createPrismaDatabaseAdapter(...)) or another
8610
+ * adapter-first bootstrap path instead.
8611
+ *
8612
+ * @param client
7021
8613
  */
7022
8614
  static setClient(client) {
8615
+ Model.emitDeprecationWarning("ARKORM_SET_CLIENT_DEPRECATED", "Model.setClient() is deprecated and will be removed in Arkorm 3.0. Use Model.setAdapter(createPrismaDatabaseAdapter(...)) or another adapter-first setup path instead.");
7023
8616
  this.client = client;
7024
8617
  }
7025
8618
  static setAdapter(adapter) {
@@ -7252,6 +8845,8 @@ var Model = class Model {
7252
8845
  static getAdapter() {
7253
8846
  ensureArkormConfigLoading();
7254
8847
  if (this.adapter) return this.adapter;
8848
+ const runtimeAdapter = getRuntimeAdapter();
8849
+ if (runtimeAdapter) return runtimeAdapter;
7255
8850
  const client = getActiveTransactionClient() ?? this.client ?? getRuntimePrismaClient();
7256
8851
  if (!client || typeof client !== "object") return void 0;
7257
8852
  return createPrismaCompatibilityAdapter(client);
@@ -7556,14 +9151,11 @@ var Model = class Model {
7556
9151
  */
7557
9152
  async load(relations) {
7558
9153
  const relationMap = this.normalizeRelationMap(relations);
7559
- await Promise.all(Object.entries(relationMap).map(async ([name, constraint]) => {
7560
- const resolver = this[name];
7561
- if (typeof resolver !== "function") return;
7562
- const relation = resolver.call(this);
7563
- if (constraint) relation.constrain(constraint);
7564
- const results = await relation.getResults();
7565
- this.attributes[name] = results;
7566
- }));
9154
+ await new SetBasedEagerLoader([this], relationMap).load();
9155
+ return this;
9156
+ }
9157
+ setLoadedRelation(name, value) {
9158
+ this.attributes[name] = value;
7567
9159
  return this;
7568
9160
  }
7569
9161
  /**
@@ -8061,6 +9653,7 @@ exports.MakeMigrationCommand = MakeMigrationCommand;
8061
9653
  exports.MakeModelCommand = MakeModelCommand;
8062
9654
  exports.MakeSeederCommand = MakeSeederCommand;
8063
9655
  exports.MigrateCommand = MigrateCommand;
9656
+ exports.MigrateFreshCommand = MigrateFreshCommand;
8064
9657
  exports.MigrateRollbackCommand = MigrateRollbackCommand;
8065
9658
  exports.Migration = Migration;
8066
9659
  exports.MigrationHistoryCommand = MigrationHistoryCommand;
@@ -8090,9 +9683,12 @@ exports.UnsupportedAdapterFeatureException = UnsupportedAdapterFeatureException;
8090
9683
  exports.applyAlterTableOperation = applyAlterTableOperation;
8091
9684
  exports.applyCreateTableOperation = applyCreateTableOperation;
8092
9685
  exports.applyDropTableOperation = applyDropTableOperation;
9686
+ exports.applyMigrationRollbackToDatabase = applyMigrationRollbackToDatabase;
8093
9687
  exports.applyMigrationRollbackToPrismaSchema = applyMigrationRollbackToPrismaSchema;
9688
+ exports.applyMigrationToDatabase = applyMigrationToDatabase;
8094
9689
  exports.applyMigrationToPrismaSchema = applyMigrationToPrismaSchema;
8095
9690
  exports.applyOperationsToPrismaSchema = applyOperationsToPrismaSchema;
9691
+ exports.bindAdapterToModels = bindAdapterToModels;
8096
9692
  exports.buildEnumBlock = buildEnumBlock;
8097
9693
  exports.buildFieldLine = buildFieldLine;
8098
9694
  exports.buildIndexLine = buildIndexLine;
@@ -8104,6 +9700,7 @@ exports.buildModelBlock = buildModelBlock;
8104
9700
  exports.buildRelationLine = buildRelationLine;
8105
9701
  exports.computeMigrationChecksum = computeMigrationChecksum;
8106
9702
  exports.configureArkormRuntime = configureArkormRuntime;
9703
+ exports.createEmptyAppliedMigrationsState = createEmptyAppliedMigrationsState;
8107
9704
  exports.createKyselyAdapter = createKyselyAdapter;
8108
9705
  exports.createMigrationTimestamp = createMigrationTimestamp;
8109
9706
  exports.createPrismaAdapter = createPrismaAdapter;
@@ -8112,6 +9709,7 @@ exports.createPrismaDatabaseAdapter = createPrismaDatabaseAdapter;
8112
9709
  exports.createPrismaDelegateMap = createPrismaDelegateMap;
8113
9710
  exports.defineConfig = defineConfig;
8114
9711
  exports.defineFactory = defineFactory;
9712
+ exports.deleteAppliedMigrationsStateFromStore = deleteAppliedMigrationsStateFromStore;
8115
9713
  exports.deriveCollectionFieldName = deriveCollectionFieldName;
8116
9714
  exports.deriveInverseRelationAlias = deriveInverseRelationAlias;
8117
9715
  exports.deriveRelationAlias = deriveRelationAlias;
@@ -8131,6 +9729,7 @@ exports.getDefaultStubsPath = getDefaultStubsPath;
8131
9729
  exports.getLastMigrationRun = getLastMigrationRun;
8132
9730
  exports.getLatestAppliedMigrations = getLatestAppliedMigrations;
8133
9731
  exports.getMigrationPlan = getMigrationPlan;
9732
+ exports.getRuntimeAdapter = getRuntimeAdapter;
8134
9733
  exports.getRuntimePaginationCurrentPageResolver = getRuntimePaginationCurrentPageResolver;
8135
9734
  exports.getRuntimePaginationURLDriverFactory = getRuntimePaginationURLDriverFactory;
8136
9735
  exports.getRuntimePrismaClient = getRuntimePrismaClient;
@@ -8144,6 +9743,7 @@ exports.markMigrationApplied = markMigrationApplied;
8144
9743
  exports.markMigrationRun = markMigrationRun;
8145
9744
  exports.pad = pad;
8146
9745
  exports.readAppliedMigrationsState = readAppliedMigrationsState;
9746
+ exports.readAppliedMigrationsStateFromStore = readAppliedMigrationsStateFromStore;
8147
9747
  exports.removeAppliedMigration = removeAppliedMigration;
8148
9748
  exports.resetArkormRuntimeForTests = resetArkormRuntimeForTests;
8149
9749
  exports.resolveCast = resolveCast;
@@ -8154,6 +9754,11 @@ exports.resolvePrismaType = resolvePrismaType;
8154
9754
  exports.runArkormTransaction = runArkormTransaction;
8155
9755
  exports.runMigrationWithPrisma = runMigrationWithPrisma;
8156
9756
  exports.runPrismaCommand = runPrismaCommand;
9757
+ exports.stripPrismaSchemaModelsAndEnums = stripPrismaSchemaModelsAndEnums;
9758
+ exports.supportsDatabaseMigrationExecution = supportsDatabaseMigrationExecution;
9759
+ exports.supportsDatabaseMigrationState = supportsDatabaseMigrationState;
9760
+ exports.supportsDatabaseReset = supportsDatabaseReset;
8157
9761
  exports.toMigrationFileSlug = toMigrationFileSlug;
8158
9762
  exports.toModelName = toModelName;
8159
- exports.writeAppliedMigrationsState = writeAppliedMigrationsState;
9763
+ exports.writeAppliedMigrationsState = writeAppliedMigrationsState;
9764
+ exports.writeAppliedMigrationsStateToStore = writeAppliedMigrationsStateToStore;