arkormx 2.0.0-next.1 → 2.0.0-next.2

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,66 @@ 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 {
103
110
  capabilities = {
104
111
  transactions: true,
105
112
  returning: true,
106
113
  insertMany: true,
114
+ upsert: true,
107
115
  updateMany: true,
108
116
  deleteMany: true,
109
117
  exists: true,
110
118
  relationLoads: false,
111
- relationAggregates: false,
112
- relationFilters: false,
119
+ relationAggregates: true,
120
+ relationFilters: true,
113
121
  rawWhere: false
114
122
  };
115
- constructor(db) {
123
+ constructor(db, mapping = {}) {
116
124
  this.db = db;
125
+ this.mapping = mapping;
126
+ }
127
+ introspectionTypeToTs(typeName, enumValues) {
128
+ if (enumValues && enumValues.length > 0) return enumValues.map((value) => `'${value.replace(/'/g, "\\'")}'`).join(" | ");
129
+ switch (typeName) {
130
+ case "bool": return "boolean";
131
+ case "int2":
132
+ case "int4":
133
+ case "int8":
134
+ case "float4":
135
+ case "float8":
136
+ case "numeric":
137
+ case "money": return "number";
138
+ case "json":
139
+ case "jsonb": return "Record<string, unknown> | unknown[]";
140
+ case "date":
141
+ case "timestamp":
142
+ case "timestamptz": return "Date";
143
+ case "bytea": return "Uint8Array";
144
+ case "uuid":
145
+ case "varchar":
146
+ case "bpchar":
147
+ case "char":
148
+ case "text":
149
+ case "citext":
150
+ case "time":
151
+ case "timetz":
152
+ case "interval":
153
+ case "inet":
154
+ case "cidr":
155
+ case "macaddr":
156
+ case "macaddr8": return "string";
157
+ default: return "unknown";
158
+ }
117
159
  }
118
160
  resolveTable(target) {
119
- if (target.table && target.table.trim().length > 0) return target.table;
161
+ if (target.table && target.table.trim().length > 0) return this.mapping[target.table] ?? target.table;
120
162
  throw new ArkormException("Kysely adapter requires a concrete target table.", {
121
163
  operation: "adapter.table",
122
164
  model: target.modelName,
@@ -225,29 +267,223 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
225
267
  if (clauses.length === 0) return kysely.sql``;
226
268
  return kysely.sql.join(clauses, kysely.sql``);
227
269
  }
270
+ buildColumnReference(table, column) {
271
+ return kysely.sql`${kysely.sql.table(table)}.${kysely.sql.id(column)}`;
272
+ }
273
+ buildRelatedTargetFromRelation(target, relation) {
274
+ const metadata = target.model?.getRelationMetadata(relation);
275
+ if (!metadata) throw new UnsupportedAdapterFeatureException(`Relation [${relation}] could not be resolved for SQL-backed relation execution.`, {
276
+ operation: "adapter.relation.metadata",
277
+ model: target.modelName,
278
+ relation
279
+ });
280
+ 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.`, {
281
+ operation: "adapter.relation.metadata",
282
+ model: target.modelName,
283
+ relation,
284
+ meta: {
285
+ feature: "relationFilters",
286
+ relationType: metadata.type
287
+ }
288
+ });
289
+ const relatedMetadata = metadata.relatedModel.getModelMetadata();
290
+ return {
291
+ metadata,
292
+ relatedTarget: {
293
+ model: metadata.relatedModel,
294
+ modelName: metadata.relatedModel.name,
295
+ table: relatedMetadata.table,
296
+ primaryKey: relatedMetadata.primaryKey,
297
+ columns: relatedMetadata.columns,
298
+ softDelete: relatedMetadata.softDelete
299
+ }
300
+ };
301
+ }
302
+ resolveMappedTable(table) {
303
+ return this.mapping[table] ?? table;
304
+ }
305
+ buildBelongsToManyJoinSource(outerTarget, relatedTarget, metadata) {
306
+ const outerTable = this.resolveTable(outerTarget);
307
+ const relatedTable = this.resolveTable(relatedTarget);
308
+ const pivotTable = this.resolveMappedTable(metadata.throughTable);
309
+ return {
310
+ 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)}`,
311
+ condition: kysely.sql`
312
+ ${this.buildColumnReference(pivotTable, metadata.foreignPivotKey)}
313
+ =
314
+ ${this.buildColumnReference(outerTable, this.mapColumn(outerTarget, metadata.parentKey))}
315
+ `
316
+ };
317
+ }
318
+ buildThroughJoinSource(outerTarget, relatedTarget, metadata) {
319
+ const outerTable = this.resolveTable(outerTarget);
320
+ const relatedTable = this.resolveTable(relatedTarget);
321
+ const throughTable = this.resolveMappedTable(metadata.throughTable);
322
+ return {
323
+ 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)}`,
324
+ condition: kysely.sql`
325
+ ${this.buildColumnReference(throughTable, metadata.firstKey)}
326
+ =
327
+ ${this.buildColumnReference(outerTable, this.mapColumn(outerTarget, metadata.localKey))}
328
+ `
329
+ };
330
+ }
331
+ buildRelatedJoinCondition(outerTarget, relation) {
332
+ const { metadata, relatedTarget } = this.buildRelatedTargetFromRelation(outerTarget, relation);
333
+ const outerTable = this.resolveTable(outerTarget);
334
+ const relatedTable = this.resolveTable(relatedTarget);
335
+ if (metadata.type === "belongsToMany") {
336
+ const joinSource = this.buildBelongsToManyJoinSource(outerTarget, relatedTarget, metadata);
337
+ return {
338
+ relatedTarget,
339
+ from: joinSource.from,
340
+ condition: joinSource.condition
341
+ };
342
+ }
343
+ if (metadata.type === "hasOneThrough" || metadata.type === "hasManyThrough") {
344
+ const joinSource = this.buildThroughJoinSource(outerTarget, relatedTarget, metadata);
345
+ return {
346
+ relatedTarget,
347
+ from: joinSource.from,
348
+ condition: joinSource.condition
349
+ };
350
+ }
351
+ if (metadata.type === "hasMany" || metadata.type === "hasOne") return {
352
+ relatedTarget,
353
+ from: kysely.sql`${kysely.sql.table(relatedTable)}`,
354
+ condition: kysely.sql`
355
+ ${this.buildColumnReference(relatedTable, this.mapColumn(relatedTarget, metadata.foreignKey))}
356
+ =
357
+ ${this.buildColumnReference(outerTable, this.mapColumn(outerTarget, metadata.localKey))}
358
+ `
359
+ };
360
+ return {
361
+ relatedTarget,
362
+ from: kysely.sql`${kysely.sql.table(relatedTable)}`,
363
+ condition: kysely.sql`
364
+ ${this.buildColumnReference(relatedTable, this.mapColumn(relatedTarget, metadata.ownerKey))}
365
+ =
366
+ ${this.buildColumnReference(outerTable, this.mapColumn(outerTarget, metadata.foreignKey))}
367
+ `
368
+ };
369
+ }
370
+ combineConditions(conditions) {
371
+ const filtered = conditions.filter((condition) => Boolean(condition));
372
+ if (filtered.length === 0) return kysely.sql`1 = 1`;
373
+ if (filtered.length === 1) return filtered[0];
374
+ return kysely.sql`(${kysely.sql.join(filtered, kysely.sql` and `)})`;
375
+ }
376
+ buildRelationFilterExpression(target, filter) {
377
+ const { relatedTarget, from, condition } = this.buildRelatedJoinCondition(target, filter.relation);
378
+ return kysely.sql`(
379
+ select count(*)::int
380
+ from ${from}
381
+ where ${this.combineConditions([condition, filter.where ? this.buildWhereCondition(relatedTarget, filter.where) : void 0])}
382
+ ) ${filter.operator === "!=" ? kysely.sql.raw("!=") : kysely.sql.raw(filter.operator)} ${filter.count}`;
383
+ }
384
+ buildRelationFilterCondition(target, relationFilters) {
385
+ if (!relationFilters || relationFilters.length === 0) return kysely.sql`1 = 1`;
386
+ let expression = null;
387
+ relationFilters.forEach((filter) => {
388
+ const next = this.buildRelationFilterExpression(target, filter);
389
+ if (!expression) {
390
+ expression = next;
391
+ return;
392
+ }
393
+ expression = filter.boolean === "OR" ? kysely.sql`(${expression} or ${next})` : kysely.sql`(${expression} and ${next})`;
394
+ });
395
+ return expression ?? kysely.sql`1 = 1`;
396
+ }
397
+ buildQueryFilterCondition(target, condition, relationFilters) {
398
+ let expression = condition ? this.buildWhereCondition(target, condition) : null;
399
+ relationFilters?.forEach((filter) => {
400
+ const next = this.buildRelationFilterExpression(target, filter);
401
+ if (!expression) {
402
+ expression = next;
403
+ return;
404
+ }
405
+ expression = filter.boolean === "OR" ? kysely.sql`(${expression} or ${next})` : kysely.sql`(${expression} and ${next})`;
406
+ });
407
+ return expression ?? kysely.sql`1 = 1`;
408
+ }
409
+ buildRelationAggregateSelectList(target, relationAggregates) {
410
+ if (!relationAggregates || relationAggregates.length === 0) return kysely.sql``;
411
+ return kysely.sql.join(relationAggregates.map((aggregate) => {
412
+ const { relatedTarget, from, condition } = this.buildRelatedJoinCondition(target, aggregate.relation);
413
+ const relatedTable = this.resolveTable(relatedTarget);
414
+ const whereCondition = this.combineConditions([condition, aggregate.where ? this.buildWhereCondition(relatedTarget, aggregate.where) : void 0]);
415
+ if (aggregate.type === "exists") return kysely.sql`, exists(
416
+ select 1
417
+ from ${from}
418
+ where ${whereCondition}
419
+ ) as ${kysely.sql.id(aggregate.alias ?? `${aggregate.relation}Exists`)}`;
420
+ const selectedColumn = aggregate.column ? this.buildColumnReference(relatedTable, this.mapColumn(relatedTarget, aggregate.column)) : kysely.sql.raw("*");
421
+ return kysely.sql`, (
422
+ 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})`}
423
+ from ${from}
424
+ where ${whereCondition}
425
+ ) as ${kysely.sql.id(aggregate.alias ?? `${aggregate.relation}${aggregate.type}`)}`;
426
+ }), kysely.sql``);
427
+ }
428
+ buildCombinedWhereClause(target, condition, relationFilters) {
429
+ if (!condition && (!relationFilters || relationFilters.length === 0)) return kysely.sql``;
430
+ return kysely.sql` where ${this.buildQueryFilterCondition(target, condition, relationFilters)}`;
431
+ }
432
+ buildSingleRowTargetCte(target, where) {
433
+ const primaryKey = this.resolvePrimaryKey(target);
434
+ return kysely.sql`target_row as (
435
+ select ${kysely.sql.id(primaryKey)}
436
+ from ${kysely.sql.table(this.resolveTable(target))}
437
+ where ${this.buildWhereCondition(target, where)}
438
+ limit 1
439
+ )`;
440
+ }
228
441
  assertNoRelationLoads(spec) {
229
442
  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
443
  operation: "adapter.relationLoads",
231
444
  meta: { feature: "relationLoads" }
232
445
  });
233
446
  }
447
+ /**
448
+ * Selects records from the database matching the specified criteria and returns
449
+ * them as an array of database rows.
450
+ *
451
+ * @param spec The specification defining the selection criteria.
452
+ * @returns A promise that resolves to an array of database rows.
453
+ */
234
454
  async select(spec) {
235
455
  this.assertNoRelationLoads(spec);
236
456
  const result = await kysely.sql`
237
457
  select ${this.buildSelectList(spec.target, spec.columns)}
458
+ ${this.buildRelationAggregateSelectList(spec.target, spec.relationAggregates)}
238
459
  from ${kysely.sql.table(this.resolveTable(spec.target))}
239
- ${this.buildWhereClause(spec.target, spec.where)}
460
+ ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
240
461
  ${this.buildOrderBy(spec.target, spec.orderBy)}
241
462
  ${this.buildPaginationClause(spec)}
242
463
  `.execute(this.db);
243
464
  return this.mapRows(spec.target, result.rows);
244
465
  }
466
+ /**
467
+ * Selects a single record from the database matching the specified criteria and returns it as
468
+ * a database row. If multiple records match the criteria, only the first one is returned.
469
+ * If no records match, null is returned.
470
+ *
471
+ * @param spec The specification defining the selection criteria.
472
+ * @returns A promise that resolves to a database row or null if no records match.
473
+ */
245
474
  async selectOne(spec) {
246
475
  return (await this.select({
247
476
  ...spec,
248
477
  limit: spec.limit ?? 1
249
478
  }))[0] ?? null;
250
479
  }
480
+ /**
481
+ * Inserts a new record into the database with the specified values and returns the
482
+ * inserted record as a database row.
483
+ *
484
+ * @param spec
485
+ * @returns
486
+ */
251
487
  async insert(spec) {
252
488
  const values = this.mapValues(spec.target, spec.values);
253
489
  const columns = Object.keys(values);
@@ -262,6 +498,13 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
262
498
  `.execute(this.db);
263
499
  return this.mapRow(spec.target, result.rows[0]);
264
500
  }
501
+ /**
502
+ * Inserts multiple records into the database with the specified values and returns the number
503
+ * of records successfully inserted.
504
+ *
505
+ * @param spec The specification defining the values to be inserted.
506
+ * @returns A promise that resolves to the number of records successfully inserted.
507
+ */
265
508
  async insertMany(spec) {
266
509
  if (spec.values.length === 0) return 0;
267
510
  const rows = spec.values.map((row) => this.mapValues(spec.target, row));
@@ -282,6 +525,39 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
282
525
  returning ${kysely.sql.id(this.resolvePrimaryKey(spec.target))}
283
526
  `.execute(this.db)).rows.length;
284
527
  }
528
+ async upsert(spec) {
529
+ if (spec.values.length === 0) return 0;
530
+ const rows = spec.values.map((row) => this.mapValues(spec.target, row));
531
+ const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
532
+ const uniqueColumns = spec.uniqueBy.map((column) => this.mapColumn(spec.target, column));
533
+ const updateColumns = (spec.updateColumns ?? []).map((column) => this.mapColumn(spec.target, column)).filter((column) => !uniqueColumns.includes(column));
534
+ const conflictTarget = kysely.sql.join(uniqueColumns.map((column) => kysely.sql.id(column)), kysely.sql`, `);
535
+ if (columns.length === 0) {
536
+ await kysely.sql`
537
+ insert into ${kysely.sql.table(this.resolveTable(spec.target))}
538
+ default values
539
+ on conflict (${conflictTarget}) do nothing
540
+ `.execute(this.db);
541
+ return spec.values.length;
542
+ }
543
+ const values = kysely.sql.join(rows.map((row) => {
544
+ return kysely.sql`(${kysely.sql.join(columns.map((column) => row[column] ?? null), kysely.sql`, `)})`;
545
+ }), kysely.sql`, `);
546
+ 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`, `)}`;
547
+ await kysely.sql`
548
+ insert into ${kysely.sql.table(this.resolveTable(spec.target))} (${kysely.sql.join(columns.map((column) => kysely.sql.id(column)), kysely.sql`, `)})
549
+ values ${values}
550
+ on conflict (${conflictTarget}) ${conflictAction}
551
+ `.execute(this.db);
552
+ return spec.values.length;
553
+ }
554
+ /**
555
+ * Updates records in the database matching the specified criteria with the given values
556
+ * and returns the updated record as a database row.
557
+ *
558
+ * @param spec The specification defining the update criteria and values.
559
+ * @returns A promise that resolves to the updated record as a database row, or null if no records match the criteria.
560
+ */
285
561
  async update(spec) {
286
562
  const values = this.mapValues(spec.target, spec.values);
287
563
  const assignments = Object.entries(values).map(([column, value]) => {
@@ -300,6 +576,41 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
300
576
  `.execute(this.db);
301
577
  return this.mapRow(spec.target, result.rows[0]);
302
578
  }
579
+ /**
580
+ * Updates a single record in the database matching the specified criteria with the given values.
581
+ *
582
+ * @param spec
583
+ * @returns
584
+ */
585
+ async updateFirst(spec) {
586
+ const values = this.mapValues(spec.target, spec.values);
587
+ const assignments = Object.entries(values).map(([column, value]) => {
588
+ return kysely.sql`${kysely.sql.id(column)} = ${value}`;
589
+ });
590
+ if (assignments.length === 0) return await this.selectOne({
591
+ target: spec.target,
592
+ where: spec.where,
593
+ limit: 1
594
+ });
595
+ const primaryKey = this.resolvePrimaryKey(spec.target);
596
+ const table = this.resolveTable(spec.target);
597
+ const result = await kysely.sql`
598
+ with ${this.buildSingleRowTargetCte(spec.target, spec.where)}
599
+ update ${kysely.sql.table(table)}
600
+ set ${kysely.sql.join(assignments, kysely.sql`, `)}
601
+ from target_row
602
+ where ${this.buildColumnReference(table, primaryKey)} = ${kysely.sql`target_row.${kysely.sql.id(primaryKey)}`}
603
+ returning ${kysely.sql.table(table)}.*
604
+ `.execute(this.db);
605
+ return this.mapRow(spec.target, result.rows[0]);
606
+ }
607
+ /**
608
+ * Updates multiple records in the database matching the specified criteria with the
609
+ * given values and returns the number of records successfully updated.
610
+ *
611
+ * @param spec The specification defining the update criteria and values.
612
+ * @returns A promise that resolves to the number of records successfully updated.
613
+ */
303
614
  async updateMany(spec) {
304
615
  const values = this.mapValues(spec.target, spec.values);
305
616
  const assignments = Object.entries(values).map(([column, value]) => {
@@ -313,6 +624,13 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
313
624
  returning ${kysely.sql.id(this.resolvePrimaryKey(spec.target))}
314
625
  `.execute(this.db)).rows.length;
315
626
  }
627
+ /**
628
+ * Deletes records from the database matching the specified criteria and returns the
629
+ * deleted record as a database row.
630
+ *
631
+ * @param spec The specification defining the delete criteria.
632
+ * @returns A promise that resolves to the deleted record as a database row, or null if no records match the criteria.
633
+ */
316
634
  async delete(spec) {
317
635
  const result = await kysely.sql`
318
636
  delete from ${kysely.sql.table(this.resolveTable(spec.target))}
@@ -321,6 +639,31 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
321
639
  `.execute(this.db);
322
640
  return this.mapRow(spec.target, result.rows[0]);
323
641
  }
642
+ /**
643
+ * Deletes a single record from the database matching the specified criteria and returns it as a database row.
644
+ *
645
+ * @param spec
646
+ * @returns
647
+ */
648
+ async deleteFirst(spec) {
649
+ const primaryKey = this.resolvePrimaryKey(spec.target);
650
+ const table = this.resolveTable(spec.target);
651
+ const result = await kysely.sql`
652
+ with ${this.buildSingleRowTargetCte(spec.target, spec.where)}
653
+ delete from ${kysely.sql.table(table)}
654
+ using target_row
655
+ where ${this.buildColumnReference(table, primaryKey)} = ${kysely.sql`target_row.${kysely.sql.id(primaryKey)}`}
656
+ returning ${kysely.sql.table(table)}.*
657
+ `.execute(this.db);
658
+ return this.mapRow(spec.target, result.rows[0]);
659
+ }
660
+ /**
661
+ * Deletes multiple records from the database matching the specified criteria and
662
+ * returns the number of records successfully deleted.
663
+ *
664
+ * @param spec The specification defining the delete criteria.
665
+ * @returns A promise that resolves to the number of records successfully deleted.
666
+ */
324
667
  async deleteMany(spec) {
325
668
  return (await kysely.sql`
326
669
  delete from ${kysely.sql.table(this.resolveTable(spec.target))}
@@ -328,36 +671,113 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
328
671
  returning ${kysely.sql.id(this.resolvePrimaryKey(spec.target))}
329
672
  `.execute(this.db)).rows.length;
330
673
  }
674
+ /**
675
+ * Counts the number of records in the database matching the specified criteria and returns
676
+ * the count as a number.
677
+ *
678
+ * @param spec The specification defining the count criteria.
679
+ * @returns A promise that resolves to the number of records matching the criteria.
680
+ */
331
681
  async count(spec) {
332
682
  const result = await kysely.sql`
333
683
  select count(*)::int as count
334
684
  from ${kysely.sql.table(this.resolveTable(spec.target))}
335
- ${this.buildWhereClause(spec.target, spec.where)}
685
+ ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
336
686
  `.execute(this.db);
337
687
  return Number(result.rows[0]?.count ?? 0);
338
688
  }
689
+ /**
690
+ * Checks for the existence of records matching the specified criteria.
691
+ *
692
+ * @param spec The specification defining the existence criteria.
693
+ * @returns A promise that resolves to a boolean indicating whether any records match the criteria.
694
+ */
339
695
  async exists(spec) {
340
696
  const result = await kysely.sql`
341
697
  select exists(
342
698
  select 1
343
699
  from ${kysely.sql.table(this.resolveTable(spec.target))}
344
- ${this.buildWhereClause(spec.target, spec.where)}
700
+ ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
345
701
  limit 1
346
702
  ) as exists
347
703
  `.execute(this.db);
348
704
  return Boolean(result.rows[0]?.exists);
349
705
  }
706
+ async introspectModels(options = {}) {
707
+ const tables = options.tables?.filter(Boolean) ?? [];
708
+ const result = await kysely.sql`
709
+ select
710
+ cls.relname as table_name,
711
+ att.attname as column_name,
712
+ not att.attnotnull as is_nullable,
713
+ typ.typname as type_name,
714
+ case when typ.typcategory = 'A' then elem.typname else null end as element_type_name,
715
+ case when typ.typtype = 'e'
716
+ then array(select enumlabel from pg_enum where enumtypid = typ.oid order by enumsortorder)
717
+ else null
718
+ end as enum_values,
719
+ case when elem.typtype = 'e'
720
+ then array(select enumlabel from pg_enum where enumtypid = elem.oid order by enumsortorder)
721
+ else null
722
+ end as element_enum_values
723
+ from pg_attribute att
724
+ inner join pg_class cls on cls.oid = att.attrelid
725
+ inner join pg_namespace ns on ns.oid = cls.relnamespace
726
+ inner join pg_type typ on typ.oid = att.atttypid
727
+ left join pg_type elem on elem.oid = typ.typelem and typ.typcategory = 'A'
728
+ where cls.relkind in ('r', 'p')
729
+ and att.attnum > 0
730
+ and not att.attisdropped
731
+ and ns.nspname not in ('pg_catalog', 'information_schema')
732
+ ${tables.length > 0 ? kysely.sql` and cls.relname in (${kysely.sql.join(tables)})` : kysely.sql``}
733
+ order by cls.relname asc, att.attnum asc
734
+ `.execute(this.db);
735
+ const models = /* @__PURE__ */ new Map();
736
+ result.rows.forEach((row) => {
737
+ const existing = models.get(row.table_name) ?? {
738
+ name: (0, _h3ravel_support.str)(row.table_name).studly().singular().toString(),
739
+ table: row.table_name,
740
+ fields: []
741
+ };
742
+ const isArray = row.element_type_name !== null;
743
+ const baseType = isArray ? this.introspectionTypeToTs(row.element_type_name ?? "unknown", row.element_enum_values) : this.introspectionTypeToTs(row.type_name, row.enum_values);
744
+ existing.fields.push({
745
+ name: row.column_name,
746
+ type: isArray ? `Array<${baseType}>` : baseType,
747
+ nullable: row.is_nullable
748
+ });
749
+ models.set(row.table_name, existing);
750
+ });
751
+ return [...models.values()];
752
+ }
753
+ /**
754
+ * Executes a series of database operations within a transaction.
755
+ * The provided callback function is called with a new instance of the
756
+ * KyselyDatabaseAdapter that is bound to the transaction context.
757
+ *
758
+ * @param callback The callback function containing the database operations to be executed within the transaction.
759
+ * @param context The transaction context specifying options such as read-only mode and isolation level.
760
+ * @returns A promise that resolves to the result of the callback function.
761
+ */
350
762
  async transaction(callback, context = {}) {
351
763
  let transactionBuilder = this.db.transaction();
352
764
  if (context.readOnly !== void 0) transactionBuilder = transactionBuilder.setAccessMode(context.readOnly ? "read only" : "read write");
353
765
  if (context.isolationLevel) transactionBuilder = transactionBuilder.setIsolationLevel(context.isolationLevel);
354
766
  return await transactionBuilder.execute(async (transaction) => {
355
- return await callback(new KyselyDatabaseAdapter(transaction));
767
+ return await callback(new KyselyDatabaseAdapter(transaction, this.mapping));
356
768
  });
357
769
  }
358
770
  };
359
- const createKyselyAdapter = (db) => {
360
- return new KyselyDatabaseAdapter(db);
771
+ /**
772
+ * Factory function to create a KyselyDatabaseAdapter instance with the given Kysely executor
773
+ * and optional table name mapping.
774
+ *
775
+ * @param db The Kysely executor to be used by the adapter.
776
+ * @param mapping Optional table name mapping for the adapter.
777
+ * @returns A new instance of KyselyDatabaseAdapter.
778
+ */
779
+ const createKyselyAdapter = (db, mapping = {}) => {
780
+ return new KyselyDatabaseAdapter(db, mapping);
361
781
  };
362
782
 
363
783
  //#endregion
@@ -416,6 +836,7 @@ const userConfig = {
416
836
  let runtimeConfigLoaded = false;
417
837
  let runtimeConfigLoadingPromise;
418
838
  let runtimeClientResolver;
839
+ let runtimeAdapter;
419
840
  let runtimePaginationURLDriverFactory;
420
841
  let runtimePaginationCurrentPageResolver;
421
842
  const transactionClientStorage = new async_hooks.AsyncLocalStorage();
@@ -441,6 +862,12 @@ const mergePathConfig = (paths) => {
441
862
  const defineConfig = (config) => {
442
863
  return config;
443
864
  };
865
+ const bindAdapterToModels = (adapter, models) => {
866
+ models.forEach((model) => {
867
+ model.setAdapter(adapter);
868
+ });
869
+ return adapter;
870
+ };
444
871
  /**
445
872
  * Get the user-provided ArkORM configuration.
446
873
  *
@@ -460,15 +887,22 @@ const getUserConfig = (key) => {
460
887
  const configureArkormRuntime = (prisma, options = {}) => {
461
888
  const nextConfig = {
462
889
  ...userConfig,
463
- prisma,
464
890
  paths: mergePathConfig(options.paths)
465
891
  };
892
+ nextConfig.prisma = prisma;
466
893
  if (options.pagination !== void 0) nextConfig.pagination = options.pagination;
894
+ if (options.adapter !== void 0) nextConfig.adapter = options.adapter;
895
+ if (options.boot !== void 0) nextConfig.boot = options.boot;
467
896
  if (options.outputExt !== void 0) nextConfig.outputExt = options.outputExt;
468
897
  Object.assign(userConfig, { ...nextConfig });
469
898
  runtimeClientResolver = prisma;
899
+ runtimeAdapter = options.adapter;
470
900
  runtimePaginationURLDriverFactory = nextConfig.pagination?.urlDriver;
471
901
  runtimePaginationCurrentPageResolver = nextConfig.pagination?.resolveCurrentPage;
902
+ options.boot?.({
903
+ prisma: resolveClient(prisma),
904
+ bindAdapter: bindAdapterToModels
905
+ });
472
906
  };
473
907
  /**
474
908
  * Reset the ArkORM runtime configuration.
@@ -482,6 +916,7 @@ const resetArkormRuntimeForTests = () => {
482
916
  runtimeConfigLoaded = false;
483
917
  runtimeConfigLoadingPromise = void 0;
484
918
  runtimeClientResolver = void 0;
919
+ runtimeAdapter = void 0;
485
920
  runtimePaginationURLDriverFactory = void 0;
486
921
  runtimePaginationCurrentPageResolver = void 0;
487
922
  };
@@ -508,8 +943,10 @@ const resolveClient = (resolver) => {
508
943
  */
509
944
  const resolveAndApplyConfig = (imported) => {
510
945
  const config = imported?.default ?? imported;
511
- if (!config || typeof config !== "object" || !config.prisma) return;
946
+ if (!config || typeof config !== "object") return;
512
947
  configureArkormRuntime(config.prisma, {
948
+ adapter: config.adapter,
949
+ boot: config.boot,
513
950
  pagination: config.pagination,
514
951
  paths: config.paths,
515
952
  outputExt: config.outputExt
@@ -591,6 +1028,10 @@ const getRuntimePrismaClient = () => {
591
1028
  if (!runtimeConfigLoaded) loadRuntimeConfigSync();
592
1029
  return resolveClient(runtimeClientResolver);
593
1030
  };
1031
+ const getRuntimeAdapter = () => {
1032
+ if (!runtimeConfigLoaded) loadRuntimeConfigSync();
1033
+ return runtimeAdapter;
1034
+ };
594
1035
  const getActiveTransactionClient = () => {
595
1036
  return transactionClientStorage.getStore();
596
1037
  };
@@ -658,9 +1099,11 @@ loadArkormConfig();
658
1099
  //#endregion
659
1100
  //#region src/helpers/prisma.ts
660
1101
  /**
661
- * Create an adapter to convert a Prisma client instance into a format
1102
+ * Create an adapter to convert a Prisma client instance into a format
662
1103
  * compatible with ArkORM's expectations.
663
- *
1104
+ *
1105
+ * @deprecated Prefer createPrismaDatabaseAdapter(prisma) for runtime usage.
1106
+ *
664
1107
  * @param prisma The Prisma client instance to adapt.
665
1108
  * @param mapping An optional mapping of Prisma delegate names to ArkORM delegate names.
666
1109
  * @returns A record of adapted Prisma delegates compatible with ArkORM.
@@ -675,6 +1118,9 @@ function createPrismaAdapter(prisma) {
675
1118
  /**
676
1119
  * Create a delegate mapping record for Model.setClient() from a Prisma client.
677
1120
  *
1121
+ * @deprecated Prefer createPrismaDatabaseAdapter(prisma, mapping) and bind the
1122
+ * resulting adapter with Model.setAdapter(...).
1123
+ *
678
1124
  * @param prisma The Prisma client instance.
679
1125
  * @param mapping Optional mapping of Arkormˣ delegate names to Prisma delegate names.
680
1126
  * @returns A delegate map keyed by Arkormˣ delegate names.
@@ -701,6 +1147,13 @@ function inferDelegateName(modelName) {
701
1147
 
702
1148
  //#endregion
703
1149
  //#region src/adapters/PrismaDatabaseAdapter.ts
1150
+ /**
1151
+ * Database adapter implementation for Prisma, allowing Arkorm to execute queries using Prisma
1152
+ * as the underlying query builder and executor.
1153
+ *
1154
+ * @author Legacy (3m1n3nc3)
1155
+ * @since 2.0.0-next.0
1156
+ */
704
1157
  var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
705
1158
  capabilities;
706
1159
  delegates;
@@ -715,6 +1168,7 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
715
1168
  this.capabilities = {
716
1169
  transactions: this.hasTransactionSupport(prisma),
717
1170
  insertMany: Object.values(this.delegates).some((delegate) => typeof delegate.createMany === "function"),
1171
+ upsert: false,
718
1172
  updateMany: Object.values(this.delegates).some((delegate) => typeof delegate.updateMany === "function"),
719
1173
  deleteMany: false,
720
1174
  exists: true,
@@ -734,6 +1188,27 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
734
1188
  unique(values) {
735
1189
  return [...new Set(values.filter(Boolean))];
736
1190
  }
1191
+ runtimeModelTypeToTs(typeName, kind, enumValues) {
1192
+ if (kind === "enum" && enumValues && enumValues.length > 0) return enumValues.map((value) => `'${value.replace(/'/g, "\\'")}'`).join(" | ");
1193
+ switch (typeName) {
1194
+ case "Int":
1195
+ case "Float":
1196
+ case "Decimal":
1197
+ case "BigInt": return "number";
1198
+ case "Boolean": return "boolean";
1199
+ case "DateTime": return "Date";
1200
+ case "Json": return "Record<string, unknown> | unknown[]";
1201
+ case "Bytes": return "Uint8Array";
1202
+ case "String":
1203
+ case "UUID": return "string";
1204
+ default: return "string";
1205
+ }
1206
+ }
1207
+ getRuntimeDataModel() {
1208
+ const runtimeDataModel = this.prisma._runtimeDataModel;
1209
+ if (runtimeDataModel && typeof runtimeDataModel === "object") return runtimeDataModel;
1210
+ return null;
1211
+ }
737
1212
  toQuerySelect(columns) {
738
1213
  if (!columns || columns.length === 0) return void 0;
739
1214
  return columns.reduce((select, column) => {
@@ -815,6 +1290,29 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
815
1290
  return include;
816
1291
  }, {});
817
1292
  }
1293
+ async introspectModels(options = {}) {
1294
+ const runtimeDataModel = this.getRuntimeDataModel();
1295
+ if (!runtimeDataModel?.models) return [];
1296
+ const requestedTables = new Set(options.tables?.filter(Boolean) ?? []);
1297
+ const enums = runtimeDataModel.enums ?? {};
1298
+ return Object.entries(runtimeDataModel.models).flatMap(([name, model]) => {
1299
+ const table = model.dbName ?? `${(0, _h3ravel_support.str)(name).camel().plural()}`;
1300
+ if (requestedTables.size > 0 && !requestedTables.has(table)) return [];
1301
+ return [{
1302
+ name,
1303
+ table,
1304
+ fields: (model.fields ?? []).filter((field) => field.kind !== "object").map((field) => {
1305
+ const enumValues = field.kind === "enum" ? (enums[field.type]?.values ?? []).map((value) => typeof value === "string" ? value : value.name ?? "").filter(Boolean) : null;
1306
+ const baseType = this.runtimeModelTypeToTs(field.type, field.kind, enumValues);
1307
+ return {
1308
+ name: field.name,
1309
+ type: field.isList ? `Array<${baseType}>` : baseType,
1310
+ nullable: field.isRequired === false
1311
+ };
1312
+ })
1313
+ }];
1314
+ });
1315
+ }
818
1316
  resolveDelegate(target) {
819
1317
  const tableName = target.table ? this.normalizeCandidate(target.table) : "";
820
1318
  const singularTableName = tableName ? `${(0, _h3ravel_support.str)(tableName).singular()}` : "";
@@ -846,15 +1344,41 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
846
1344
  }
847
1345
  });
848
1346
  }
1347
+ /**
1348
+ * @todo Implement relationLoads by performing separate queries and merging results
1349
+ * in-memory, since Prisma does not support nested reads with constraints, ordering, or
1350
+ * pagination on related models as of now.
1351
+ *
1352
+ * @param spec
1353
+ * @returns
1354
+ */
849
1355
  async select(spec) {
850
1356
  return await this.resolveDelegate(spec.target).findMany(this.buildFindArgs(spec));
851
1357
  }
1358
+ /**
1359
+ * Selects a single record matching the specified criteria.
1360
+ *
1361
+ * @param spec
1362
+ * @returns
1363
+ */
852
1364
  async selectOne(spec) {
853
1365
  return await this.resolveDelegate(spec.target).findFirst(this.buildFindArgs(spec));
854
1366
  }
1367
+ /**
1368
+ * Inserts a single record into the database and returns the created record.
1369
+ *
1370
+ * @param spec
1371
+ * @returns
1372
+ */
855
1373
  async insert(spec) {
856
1374
  return await this.resolveDelegate(spec.target).create({ data: spec.values });
857
1375
  }
1376
+ /**
1377
+ * Inserts multiple records into the database.
1378
+ *
1379
+ * @param spec
1380
+ * @returns
1381
+ */
858
1382
  async insertMany(spec) {
859
1383
  const delegate = this.resolveDelegate(spec.target);
860
1384
  if (typeof delegate.createMany === "function") {
@@ -874,6 +1398,12 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
874
1398
  }
875
1399
  return spec.ignoreDuplicates ? inserted : spec.values.length;
876
1400
  }
1401
+ /**
1402
+ * Updates a single record matching the specified criteria and returns the updated record.
1403
+ *
1404
+ * @param spec
1405
+ * @returns
1406
+ */
877
1407
  async update(spec) {
878
1408
  const delegate = this.resolveDelegate(spec.target);
879
1409
  const where = this.toQueryWhere(spec.where);
@@ -883,6 +1413,12 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
883
1413
  data: spec.values
884
1414
  });
885
1415
  }
1416
+ /**
1417
+ * Updates multiple records matching the specified criteria.
1418
+ *
1419
+ * @param spec
1420
+ * @returns
1421
+ */
886
1422
  async updateMany(spec) {
887
1423
  const delegate = this.resolveDelegate(spec.target);
888
1424
  const where = this.toQueryWhere(spec.where);
@@ -903,12 +1439,24 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
903
1439
  }));
904
1440
  return rows.length;
905
1441
  }
1442
+ /**
1443
+ * Deletes a single record matching the specified criteria and returns the deleted record.
1444
+ *
1445
+ * @param spec
1446
+ * @returns
1447
+ */
906
1448
  async delete(spec) {
907
1449
  const delegate = this.resolveDelegate(spec.target);
908
1450
  const where = this.toQueryWhere(spec.where);
909
1451
  if (!where) return null;
910
1452
  return await delegate.delete({ where });
911
1453
  }
1454
+ /**
1455
+ * Deletes multiple records matching the specified criteria.
1456
+ *
1457
+ * @param spec
1458
+ * @returns
1459
+ */
912
1460
  async deleteMany(spec) {
913
1461
  const delegate = this.resolveDelegate(spec.target);
914
1462
  const where = this.toQueryWhere(spec.where);
@@ -918,21 +1466,46 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
918
1466
  }));
919
1467
  return rows.length;
920
1468
  }
1469
+ /**
1470
+ * Counts the number of records matching the specified criteria.
1471
+ *
1472
+ * @param spec
1473
+ * @returns
1474
+ */
921
1475
  async count(spec) {
922
1476
  return await this.resolveDelegate(spec.target).count({ where: this.toQueryWhere(spec.where) });
923
1477
  }
1478
+ /**
1479
+ * Checks for the existence of records matching the specified criteria.
1480
+ *
1481
+ * @param spec
1482
+ * @returns
1483
+ */
924
1484
  async exists(spec) {
925
1485
  return await this.selectOne({
926
1486
  ...spec,
927
1487
  limit: 1
928
1488
  }) != null;
929
1489
  }
1490
+ /**
1491
+ * Loads related models for a batch of parent records based on the specified relation load plans.
1492
+ *
1493
+ * @param _spec
1494
+ */
930
1495
  async loadRelations(_spec) {
931
1496
  throw new UnsupportedAdapterFeatureException("Relation batch loading is not supported by the Prisma compatibility adapter yet.", {
932
1497
  operation: "adapter.loadRelations",
933
1498
  meta: { feature: "relationLoads" }
934
1499
  });
935
1500
  }
1501
+ /**
1502
+ * Executes a series of database operations within a transaction.
1503
+ * If the underlying Prisma client does not support transactions, an exception is thrown.
1504
+ *
1505
+ * @param callback
1506
+ * @param context
1507
+ * @returns
1508
+ */
936
1509
  async transaction(callback, context = {}) {
937
1510
  if (!this.hasTransactionSupport(this.prisma)) throw new UnsupportedAdapterFeatureException("Transactions are not supported by the Prisma compatibility adapter.", {
938
1511
  operation: "adapter.transaction",
@@ -947,9 +1520,25 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
947
1520
  });
948
1521
  }
949
1522
  };
1523
+ /**
1524
+ * Factory function to create a PrismaDatabaseAdapter instance with the given
1525
+ * Prisma client and optional delegate name mapping.
1526
+ *
1527
+ * @param prisma The Prisma client instance to be used by the adapter.
1528
+ * @param mapping Optional mapping of delegate names.
1529
+ * @returns A new instance of PrismaDatabaseAdapter.
1530
+ */
950
1531
  const createPrismaDatabaseAdapter = (prisma, mapping = {}) => {
951
1532
  return new PrismaDatabaseAdapter(prisma, mapping);
952
1533
  };
1534
+ /**
1535
+ * Alias for createPrismaDatabaseAdapter to maintain backward compatibility with
1536
+ * previous versions of Arkorm that exported the adapter factory under a different name.
1537
+ *
1538
+ * @param prisma The Prisma client instance to be used by the adapter.
1539
+ * @param mapping Optional mapping of delegate names.
1540
+ * @returns A new instance of PrismaDatabaseAdapter.
1541
+ */
953
1542
  const createPrismaCompatibilityAdapter = createPrismaDatabaseAdapter;
954
1543
 
955
1544
  //#endregion
@@ -2921,6 +3510,40 @@ var CliApp = class {
2921
3510
  lines.splice(insertionIndex, 0, `import type { ${enumTypes.join(", ")} } from '@prisma/client'`);
2922
3511
  return lines.join("\n");
2923
3512
  }
3513
+ parseModelSyncSource(modelSource) {
3514
+ const classMatch = modelSource.match(/export\s+class\s+(\w+)\s+extends\s+Model(?:<[^\n]+>)?\s*\{/);
3515
+ if (!classMatch) return null;
3516
+ const className = classMatch[1];
3517
+ const tableMatch = modelSource.match(/protected\s+static\s+override\s+table\s*=\s*['"]([^'"]+)['"]/) ?? modelSource.match(/static\s+table\s*=\s*['"]([^'"]+)['"]/);
3518
+ const delegateMatch = modelSource.match(/protected\s+static\s+override\s+delegate\s*=\s*['"]([^'"]+)['"]/) ?? modelSource.match(/static\s+delegate\s*=\s*['"]([^'"]+)['"]/);
3519
+ return {
3520
+ className,
3521
+ table: tableMatch?.[1] ?? delegateMatch?.[1] ?? (0, _h3ravel_support.str)(className).camel().plural().toString()
3522
+ };
3523
+ }
3524
+ syncModelFiles(modelFiles, resolveStructure, enums) {
3525
+ const updated = [];
3526
+ const skipped = [];
3527
+ modelFiles.forEach((filePath) => {
3528
+ const source = (0, fs.readFileSync)(filePath, "utf-8");
3529
+ const structure = resolveStructure(filePath, source);
3530
+ if (!structure || structure.fields.length === 0) {
3531
+ skipped.push(filePath);
3532
+ return;
3533
+ }
3534
+ const synced = this.syncModelDeclarations(source, structure.fields, enums);
3535
+ if (!synced.updated) {
3536
+ skipped.push(filePath);
3537
+ return;
3538
+ }
3539
+ (0, fs.writeFileSync)(filePath, synced.content);
3540
+ updated.push(filePath);
3541
+ });
3542
+ return {
3543
+ updated,
3544
+ skipped
3545
+ };
3546
+ }
2924
3547
  /**
2925
3548
  * Parse Prisma enum definitions from a schema and return their member names.
2926
3549
  *
@@ -3013,7 +3636,7 @@ var CliApp = class {
3013
3636
  */
3014
3637
  syncModelDeclarations(modelSource, declarations, enums) {
3015
3638
  const lines = modelSource.split("\n");
3016
- const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model<.+>\s*\{/.test(line));
3639
+ const classIndex = lines.findIndex((line) => /export\s+class\s+\w+\s+extends\s+Model(?:<[^\n]+>)?\s*\{/.test(line));
3017
3640
  if (classIndex < 0) return {
3018
3641
  content: modelSource,
3019
3642
  updated: false
@@ -3067,6 +3690,36 @@ var CliApp = class {
3067
3690
  updated: contentWithImports !== modelSource
3068
3691
  };
3069
3692
  }
3693
+ async syncModels(options = {}) {
3694
+ const modelsDir = options.modelsDir ?? this.resolveConfigPath("models", (0, path.join)(process.cwd(), "src", "models"));
3695
+ if (!(0, fs.existsSync)(modelsDir)) throw new Error(`Models directory not found: ${modelsDir}`);
3696
+ const modelFiles = (0, fs.readdirSync)(modelsDir).filter((file) => file.endsWith(".ts")).map((file) => (0, path.join)(modelsDir, file));
3697
+ const adapter = this.getConfig("adapter");
3698
+ if (adapter && typeof adapter.introspectModels === "function") {
3699
+ const sources = modelFiles.reduce((all, filePath) => {
3700
+ const parsed = this.parseModelSyncSource((0, fs.readFileSync)(filePath, "utf-8"));
3701
+ if (parsed) all.set(filePath, parsed);
3702
+ return all;
3703
+ }, /* @__PURE__ */ new Map());
3704
+ const discovered = await adapter.introspectModels({ tables: [...new Set([...sources.values()].map((source) => source.table))] });
3705
+ const structuresByTable = new Map(discovered.map((model) => [model.table, model]));
3706
+ const result = this.syncModelFiles(modelFiles, (filePath) => {
3707
+ const parsed = sources.get(filePath);
3708
+ return parsed ? structuresByTable.get(parsed.table) : void 0;
3709
+ }, /* @__PURE__ */ new Map());
3710
+ return {
3711
+ source: "adapter",
3712
+ modelsDir,
3713
+ total: modelFiles.length,
3714
+ updated: result.updated,
3715
+ skipped: result.skipped
3716
+ };
3717
+ }
3718
+ return {
3719
+ source: "prisma",
3720
+ ...this.syncModelsFromPrisma(options)
3721
+ };
3722
+ }
3070
3723
  /**
3071
3724
  * Sync model attribute declarations in model files based on the Prisma schema.
3072
3725
  * This method reads the Prisma schema to extract model definitions and their
@@ -3086,38 +3739,18 @@ var CliApp = class {
3086
3739
  const schema = (0, fs.readFileSync)(schemaPath, "utf-8");
3087
3740
  const prismaEnums = this.parsePrismaEnums(schema);
3088
3741
  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
- });
3742
+ const modelFiles = (0, fs.readdirSync)(modelsDir).filter((file) => file.endsWith(".ts")).map((file) => (0, path.join)(modelsDir, file));
3743
+ const result = this.syncModelFiles(modelFiles, (filePath, source) => {
3744
+ const parsed = this.parseModelSyncSource(source);
3745
+ if (!parsed) return void 0;
3746
+ return prismaModels.find((model) => model.table === parsed.table) ?? prismaModels.find((model) => model.name === parsed.className);
3747
+ }, prismaEnums);
3115
3748
  return {
3116
3749
  schemaPath,
3117
3750
  modelsDir,
3118
3751
  total: modelFiles.length,
3119
- updated,
3120
- skipped
3752
+ updated: result.updated,
3753
+ skipped: result.skipped
3121
3754
  };
3122
3755
  }
3123
3756
  };
@@ -3695,20 +4328,21 @@ var MigrationHistoryCommand = class extends _h3ravel_musket.Command {
3695
4328
  //#region src/cli/commands/ModelsSyncCommand.ts
3696
4329
  var ModelsSyncCommand = class extends _h3ravel_musket.Command {
3697
4330
  signature = `models:sync
3698
- {--schema= : Path to prisma schema file}
4331
+ {--schema= : Path to prisma schema file used when adapter introspection is unavailable}
3699
4332
  {--models= : Path to models directory}
3700
4333
  `;
3701
- description = "Sync model declare attributes from prisma schema for all model files";
4334
+ description = "Sync model declare attributes from the active adapter when supported, otherwise fall back to the Prisma schema";
3702
4335
  async handle() {
3703
4336
  this.app.command = this;
3704
- const result = this.app.syncModelsFromPrisma({
4337
+ const result = await this.app.syncModels({
3705
4338
  schemaPath: this.option("schema") ? (0, node_path.resolve)(String(this.option("schema"))) : void 0,
3706
4339
  modelsDir: this.option("models") ? (0, node_path.resolve)(String(this.option("models"))) : void 0
3707
4340
  });
3708
4341
  const updatedLines = result.updated.length === 0 ? [this.app.splitLogger("Updated", "none")] : result.updated.map((path) => this.app.splitLogger("Updated", path));
3709
4342
  this.success("SUCCESS: Model sync completed with the following results:");
3710
4343
  [
3711
- this.app.splitLogger("Schema", result.schemaPath),
4344
+ this.app.splitLogger("Source", result.source === "adapter" ? "adapter introspection" : "prisma schema"),
4345
+ ...result.schemaPath ? [this.app.splitLogger("Schema", result.schemaPath)] : [],
3712
4346
  this.app.splitLogger("Models", result.modelsDir),
3713
4347
  this.app.splitLogger("Processed", String(result.total)),
3714
4348
  ...updatedLines,
@@ -4068,6 +4702,13 @@ var UniqueConstraintResolutionException = class extends ArkormException {
4068
4702
 
4069
4703
  //#endregion
4070
4704
  //#region src/relationship/RelationTableLoader.ts
4705
+ /**
4706
+ * Utility class responsible for loading data from relation tables, which are used to
4707
+ * manage relationships between models in Arkorm.
4708
+ *
4709
+ * @author Legacy (3m1n3nc3)
4710
+ * @since 2.0.0-next.0
4711
+ */
4071
4712
  var RelationTableLoader = class {
4072
4713
  constructor(adapter) {
4073
4714
  this.adapter = adapter;
@@ -4829,6 +5470,472 @@ var MorphToManyRelation = class extends Relation {
4829
5470
  }
4830
5471
  };
4831
5472
 
5473
+ //#endregion
5474
+ //#region src/relationship/SetBasedEagerLoader.ts
5475
+ /**
5476
+ * Utility class responsible for performing set-based eager loading of relationships for
5477
+ * a collection of models.
5478
+ *
5479
+ * @author Legacy (3m1n3nc3)
5480
+ * @since 2.0.0-next.2
5481
+ */
5482
+ var SetBasedEagerLoader = class {
5483
+ constructor(models, relations) {
5484
+ this.models = models;
5485
+ this.relations = relations;
5486
+ }
5487
+ /**
5488
+ * Performs eager loading of all specified relationships for the set of models.
5489
+ *
5490
+ * @returns
5491
+ */
5492
+ async load() {
5493
+ if (this.models.length === 0) return;
5494
+ await Promise.all(Object.entries(this.relations).map(async ([name, constraint]) => {
5495
+ await this.loadRelation(name, constraint);
5496
+ }));
5497
+ }
5498
+ /**
5499
+ * Loads a specific relationship for the set of models based on the relationship name
5500
+ * and an optional constraint.
5501
+ *
5502
+ * @param name The name of the relationship to load.
5503
+ * @param constraint An optional constraint to apply to the query.
5504
+ * @returns A promise that resolves when the relationship is loaded.
5505
+ */
5506
+ async loadRelation(name, constraint) {
5507
+ const resolver = this.resolveRelationResolver(name);
5508
+ if (!resolver) return;
5509
+ const metadata = resolver.call(this.models[0]).getMetadata();
5510
+ switch (metadata.type) {
5511
+ case "belongsTo":
5512
+ await this.loadBelongsTo(name, resolver, metadata, constraint);
5513
+ return;
5514
+ case "belongsToMany":
5515
+ await this.loadBelongsToMany(name, metadata, constraint);
5516
+ return;
5517
+ case "hasMany":
5518
+ await this.loadHasMany(name, metadata, constraint);
5519
+ return;
5520
+ case "hasOne":
5521
+ await this.loadHasOne(name, resolver, metadata, constraint);
5522
+ return;
5523
+ case "hasManyThrough":
5524
+ await this.loadHasManyThrough(name, metadata, constraint);
5525
+ return;
5526
+ case "hasOneThrough":
5527
+ await this.loadHasOneThrough(name, resolver, metadata, constraint);
5528
+ return;
5529
+ default: await this.loadIndividually(name, resolver, constraint);
5530
+ }
5531
+ }
5532
+ /**
5533
+ * Resolves the relation resolver function for a given relationship name by inspecting
5534
+ * the first model in the set.
5535
+ *
5536
+ * @param name The name of the relationship to resolve.
5537
+ * @returns The relation resolver function or null if not found.
5538
+ */
5539
+ resolveRelationResolver(name) {
5540
+ const resolver = this.models[0][name];
5541
+ if (typeof resolver !== "function") return null;
5542
+ return resolver;
5543
+ }
5544
+ /**
5545
+ * Loads a "belongs to" relationship for the set of models.
5546
+ *
5547
+ * @param name The name of the relationship to load.
5548
+ * @param resolver The relation resolver function.
5549
+ * @param metadata The metadata for the relationship.
5550
+ * @param constraint An optional constraint to apply to the query.
5551
+ * @returns A promise that resolves when the relationship is loaded.
5552
+ */
5553
+ async loadBelongsTo(name, resolver, metadata, constraint) {
5554
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.foreignKey));
5555
+ if (keys.length === 0) {
5556
+ this.models.forEach((model) => {
5557
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
5558
+ });
5559
+ return;
5560
+ }
5561
+ let query = metadata.relatedModel.query().whereIn(metadata.ownerKey, keys);
5562
+ query = this.applyConstraint(query, constraint);
5563
+ const relatedModels = (await query.get()).all();
5564
+ const relatedByOwnerKey = /* @__PURE__ */ new Map();
5565
+ relatedModels.forEach((related) => {
5566
+ const value = this.readModelAttribute(related, metadata.ownerKey);
5567
+ if (value == null) return;
5568
+ const lookupKey = this.toLookupKey(value);
5569
+ if (!relatedByOwnerKey.has(lookupKey)) relatedByOwnerKey.set(lookupKey, related);
5570
+ });
5571
+ this.models.forEach((model) => {
5572
+ const foreignValue = model.getAttribute(metadata.foreignKey);
5573
+ const relationValue = foreignValue == null ? void 0 : relatedByOwnerKey.get(this.toLookupKey(foreignValue));
5574
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
5575
+ });
5576
+ }
5577
+ /**
5578
+ * Loads a "has many" relationship for the set of models.
5579
+ *
5580
+ * @param name
5581
+ * @param metadata
5582
+ * @param constraint
5583
+ * @returns
5584
+ */
5585
+ async loadHasMany(name, metadata, constraint) {
5586
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
5587
+ if (keys.length === 0) {
5588
+ this.models.forEach((model) => {
5589
+ model.setLoadedRelation(name, new ArkormCollection([]));
5590
+ });
5591
+ return;
5592
+ }
5593
+ let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
5594
+ query = this.applyConstraint(query, constraint);
5595
+ const relatedModels = (await query.get()).all();
5596
+ const relatedByForeignKey = /* @__PURE__ */ new Map();
5597
+ relatedModels.forEach((related) => {
5598
+ const value = this.readModelAttribute(related, metadata.foreignKey);
5599
+ if (value == null) return;
5600
+ const lookupKey = this.toLookupKey(value);
5601
+ const bucket = relatedByForeignKey.get(lookupKey) ?? [];
5602
+ bucket.push(related);
5603
+ relatedByForeignKey.set(lookupKey, bucket);
5604
+ });
5605
+ this.models.forEach((model) => {
5606
+ const localValue = model.getAttribute(metadata.localKey);
5607
+ const related = localValue == null ? [] : relatedByForeignKey.get(this.toLookupKey(localValue)) ?? [];
5608
+ model.setLoadedRelation(name, new ArkormCollection(related));
5609
+ });
5610
+ }
5611
+ /**
5612
+ * Loads a "belongs to many" relationship for the set of models.
5613
+ *
5614
+ * @param name
5615
+ * @param metadata
5616
+ * @param constraint
5617
+ * @returns
5618
+ */
5619
+ async loadBelongsToMany(name, metadata, constraint) {
5620
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.parentKey));
5621
+ if (parentKeys.length === 0) {
5622
+ this.models.forEach((model) => {
5623
+ model.setLoadedRelation(name, new ArkormCollection([]));
5624
+ });
5625
+ return;
5626
+ }
5627
+ const pivotRows = await this.createRelationTableLoader().selectRows({
5628
+ table: metadata.throughTable,
5629
+ where: {
5630
+ type: "comparison",
5631
+ column: metadata.foreignPivotKey,
5632
+ operator: "in",
5633
+ value: parentKeys
5634
+ }
5635
+ });
5636
+ const relatedIds = this.collectUniqueRowValues(pivotRows, metadata.relatedPivotKey);
5637
+ if (relatedIds.length === 0) {
5638
+ this.models.forEach((model) => {
5639
+ model.setLoadedRelation(name, new ArkormCollection([]));
5640
+ });
5641
+ return;
5642
+ }
5643
+ let query = metadata.relatedModel.query().whereIn(metadata.relatedKey, relatedIds);
5644
+ query = this.applyConstraint(query, constraint);
5645
+ const relatedModels = (await query.get()).all();
5646
+ const relatedByKey = /* @__PURE__ */ new Map();
5647
+ relatedModels.forEach((related) => {
5648
+ const relatedValue = this.readModelAttribute(related, metadata.relatedKey);
5649
+ if (relatedValue == null) return;
5650
+ relatedByKey.set(this.toLookupKey(relatedValue), related);
5651
+ });
5652
+ const relatedKeysByParent = /* @__PURE__ */ new Map();
5653
+ pivotRows.forEach((row) => {
5654
+ const parentValue = row[metadata.foreignPivotKey];
5655
+ const relatedValue = row[metadata.relatedPivotKey];
5656
+ if (parentValue == null || relatedValue == null) return;
5657
+ const bucket = relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? [];
5658
+ bucket.push(relatedValue);
5659
+ relatedKeysByParent.set(this.toLookupKey(parentValue), bucket);
5660
+ });
5661
+ this.models.forEach((model) => {
5662
+ const parentValue = model.getAttribute(metadata.parentKey);
5663
+ const related = (parentValue == null ? [] : relatedKeysByParent.get(this.toLookupKey(parentValue)) ?? []).reduce((all, relatedValue) => {
5664
+ const candidate = relatedByKey.get(this.toLookupKey(relatedValue));
5665
+ if (candidate) all.push(candidate);
5666
+ return all;
5667
+ }, []);
5668
+ model.setLoadedRelation(name, new ArkormCollection(related));
5669
+ });
5670
+ }
5671
+ /**
5672
+ * Loads a "belongs to many" relationship for the set of models.
5673
+ *
5674
+ * @param name
5675
+ * @param resolver
5676
+ * @param metadata
5677
+ * @param constraint
5678
+ * @returns
5679
+ */
5680
+ async loadHasOne(name, resolver, metadata, constraint) {
5681
+ const keys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
5682
+ if (keys.length === 0) {
5683
+ this.models.forEach((model) => {
5684
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
5685
+ });
5686
+ return;
5687
+ }
5688
+ let query = metadata.relatedModel.query().whereIn(metadata.foreignKey, keys);
5689
+ query = this.applyConstraint(query, constraint);
5690
+ const relatedModels = (await query.get()).all();
5691
+ const relatedByForeignKey = /* @__PURE__ */ new Map();
5692
+ relatedModels.forEach((related) => {
5693
+ const value = this.readModelAttribute(related, metadata.foreignKey);
5694
+ if (value == null) return;
5695
+ const lookupKey = this.toLookupKey(value);
5696
+ if (!relatedByForeignKey.has(lookupKey)) relatedByForeignKey.set(lookupKey, related);
5697
+ });
5698
+ this.models.forEach((model) => {
5699
+ const localValue = model.getAttribute(metadata.localKey);
5700
+ const relationValue = localValue == null ? void 0 : relatedByForeignKey.get(this.toLookupKey(localValue));
5701
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
5702
+ });
5703
+ }
5704
+ /**
5705
+ * Loads a "has many through" relationship for the set of models.
5706
+ *
5707
+ * @param name
5708
+ * @param metadata
5709
+ * @param constraint
5710
+ * @returns
5711
+ */
5712
+ async loadHasManyThrough(name, metadata, constraint) {
5713
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
5714
+ if (parentKeys.length === 0) {
5715
+ this.models.forEach((model) => {
5716
+ model.setLoadedRelation(name, new ArkormCollection([]));
5717
+ });
5718
+ return;
5719
+ }
5720
+ const throughRows = await this.createRelationTableLoader().selectRows({
5721
+ table: metadata.throughTable,
5722
+ where: {
5723
+ type: "comparison",
5724
+ column: metadata.firstKey,
5725
+ operator: "in",
5726
+ value: parentKeys
5727
+ }
5728
+ });
5729
+ const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
5730
+ if (intermediateKeys.length === 0) {
5731
+ this.models.forEach((model) => {
5732
+ model.setLoadedRelation(name, new ArkormCollection([]));
5733
+ });
5734
+ return;
5735
+ }
5736
+ let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
5737
+ query = this.applyConstraint(query, constraint);
5738
+ const relatedModels = (await query.get()).all();
5739
+ const relatedByIntermediate = /* @__PURE__ */ new Map();
5740
+ relatedModels.forEach((related) => {
5741
+ const relatedValue = this.readModelAttribute(related, metadata.secondKey);
5742
+ if (relatedValue == null) return;
5743
+ const bucket = relatedByIntermediate.get(this.toLookupKey(relatedValue)) ?? [];
5744
+ bucket.push(related);
5745
+ relatedByIntermediate.set(this.toLookupKey(relatedValue), bucket);
5746
+ });
5747
+ const intermediateByParent = /* @__PURE__ */ new Map();
5748
+ throughRows.forEach((row) => {
5749
+ const parentValue = row[metadata.firstKey];
5750
+ const intermediateValue = row[metadata.secondLocalKey];
5751
+ if (parentValue == null || intermediateValue == null) return;
5752
+ const bucket = intermediateByParent.get(this.toLookupKey(parentValue)) ?? [];
5753
+ bucket.push(intermediateValue);
5754
+ intermediateByParent.set(this.toLookupKey(parentValue), bucket);
5755
+ });
5756
+ this.models.forEach((model) => {
5757
+ const parentValue = model.getAttribute(metadata.localKey);
5758
+ const related = (parentValue == null ? [] : intermediateByParent.get(this.toLookupKey(parentValue)) ?? []).flatMap((intermediateValue) => relatedByIntermediate.get(this.toLookupKey(intermediateValue)) ?? []);
5759
+ model.setLoadedRelation(name, new ArkormCollection(related));
5760
+ });
5761
+ }
5762
+ /**
5763
+ * Loads a "has one through" relationship for the set of models.
5764
+ *
5765
+ * @param name
5766
+ * @param resolver
5767
+ * @param metadata
5768
+ * @param constraint
5769
+ * @returns
5770
+ */
5771
+ async loadHasOneThrough(name, resolver, metadata, constraint) {
5772
+ const parentKeys = this.collectUniqueKeys((model) => model.getAttribute(metadata.localKey));
5773
+ if (parentKeys.length === 0) {
5774
+ this.models.forEach((model) => {
5775
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
5776
+ });
5777
+ return;
5778
+ }
5779
+ const throughRows = await this.createRelationTableLoader().selectRows({
5780
+ table: metadata.throughTable,
5781
+ where: {
5782
+ type: "comparison",
5783
+ column: metadata.firstKey,
5784
+ operator: "in",
5785
+ value: parentKeys
5786
+ }
5787
+ });
5788
+ const intermediateKeys = this.collectUniqueRowValues(throughRows, metadata.secondLocalKey);
5789
+ if (intermediateKeys.length === 0) {
5790
+ this.models.forEach((model) => {
5791
+ model.setLoadedRelation(name, this.resolveSingleDefault(resolver, model));
5792
+ });
5793
+ return;
5794
+ }
5795
+ let query = metadata.relatedModel.query().whereIn(metadata.secondKey, intermediateKeys);
5796
+ query = this.applyConstraint(query, constraint);
5797
+ const relatedModels = (await query.get()).all();
5798
+ const relatedByIntermediate = /* @__PURE__ */ new Map();
5799
+ relatedModels.forEach((related) => {
5800
+ const relatedValue = this.readModelAttribute(related, metadata.secondKey);
5801
+ if (relatedValue == null) return;
5802
+ const lookupKey = this.toLookupKey(relatedValue);
5803
+ if (!relatedByIntermediate.has(lookupKey)) relatedByIntermediate.set(lookupKey, related);
5804
+ });
5805
+ const intermediateByParent = /* @__PURE__ */ new Map();
5806
+ throughRows.forEach((row) => {
5807
+ const parentValue = row[metadata.firstKey];
5808
+ const intermediateValue = row[metadata.secondLocalKey];
5809
+ if (parentValue == null || intermediateValue == null) return;
5810
+ const lookupKey = this.toLookupKey(parentValue);
5811
+ if (!intermediateByParent.has(lookupKey)) intermediateByParent.set(lookupKey, intermediateValue);
5812
+ });
5813
+ this.models.forEach((model) => {
5814
+ const parentValue = model.getAttribute(metadata.localKey);
5815
+ const intermediateValue = parentValue == null ? void 0 : intermediateByParent.get(this.toLookupKey(parentValue));
5816
+ const relationValue = intermediateValue == null ? void 0 : relatedByIntermediate.get(this.toLookupKey(intermediateValue));
5817
+ model.setLoadedRelation(name, relationValue ?? this.resolveSingleDefault(resolver, model));
5818
+ });
5819
+ }
5820
+ /**
5821
+ * Fallback method to load relationships individually for each model when the
5822
+ * relationship type is not supported for set-based loading.
5823
+ *
5824
+ * @param name
5825
+ * @param resolver
5826
+ * @param constraint
5827
+ */
5828
+ async loadIndividually(name, resolver, constraint) {
5829
+ await Promise.all(this.models.map(async (model) => {
5830
+ const relation = resolver.call(model);
5831
+ if (constraint) relation.constrain(constraint);
5832
+ model.setLoadedRelation(name, await relation.getResults());
5833
+ }));
5834
+ }
5835
+ /**
5836
+ * Applies an eager load constraint to a query if provided.
5837
+ *
5838
+ * @param query
5839
+ * @param constraint
5840
+ * @returns
5841
+ */
5842
+ applyConstraint(query, constraint) {
5843
+ if (!constraint) return query;
5844
+ return constraint(query) ?? query;
5845
+ }
5846
+ /**
5847
+ * Collects unique values from the set of models based on a resolver function, which
5848
+ * is used to extract the value from each model.
5849
+ *
5850
+ * @param resolve A function that takes a model and returns the value to be collected.
5851
+ * @returns An array of unique values.
5852
+ */
5853
+ collectUniqueKeys(resolve) {
5854
+ const seen = /* @__PURE__ */ new Set();
5855
+ const values = [];
5856
+ this.models.forEach((model) => {
5857
+ const value = resolve(model);
5858
+ if (value == null) return;
5859
+ const lookupKey = this.toLookupKey(value);
5860
+ if (seen.has(lookupKey)) return;
5861
+ seen.add(lookupKey);
5862
+ values.push(value);
5863
+ });
5864
+ return values;
5865
+ }
5866
+ /**
5867
+ * Collects unique values from an array of database rows based on a specified key, which
5868
+ * is used to extract the value from each row.
5869
+ *
5870
+ * @param rows An array of database rows.
5871
+ * @param key The key to extract values from each row.
5872
+ * @returns An array of unique values.
5873
+ */
5874
+ collectUniqueRowValues(rows, key) {
5875
+ const seen = /* @__PURE__ */ new Set();
5876
+ const values = [];
5877
+ rows.forEach((row) => {
5878
+ const value = row[key];
5879
+ if (value == null) return;
5880
+ const lookupKey = this.toLookupKey(value);
5881
+ if (seen.has(lookupKey)) return;
5882
+ seen.add(lookupKey);
5883
+ values.push(value);
5884
+ });
5885
+ return values;
5886
+ }
5887
+ /**
5888
+ * Loads a "belongs to many" relationship for the set of models.
5889
+ *
5890
+ * @returns
5891
+ */
5892
+ createRelationTableLoader() {
5893
+ return new RelationTableLoader(this.resolveAdapter());
5894
+ }
5895
+ /**
5896
+ * Loads a "belongs to many" relationship for the set of models.
5897
+ *
5898
+ * @returns
5899
+ */
5900
+ resolveAdapter() {
5901
+ const adapter = this.models[0].constructor.getAdapter?.();
5902
+ if (!adapter) throw new Error("Set-based eager loading requires a configured adapter.");
5903
+ return adapter;
5904
+ }
5905
+ /**
5906
+ * Reads an attribute value from a model using the getAttribute method, which is used
5907
+ * to access model attributes in a way that is compatible with Arkorm's internal model structure.
5908
+ *
5909
+ * @param model The model to read the attribute from.
5910
+ * @param key The name of the attribute to read.
5911
+ * @returns
5912
+ */
5913
+ readModelAttribute(model, key) {
5914
+ return model.getAttribute?.(key);
5915
+ }
5916
+ /**
5917
+ * Resolves the default result for a relationship when no related models are found.
5918
+ *
5919
+ * @param resolver
5920
+ * @param model
5921
+ * @returns
5922
+ */
5923
+ resolveSingleDefault(resolver, model) {
5924
+ return resolver.call(model).resolveDefaultResult?.() ?? null;
5925
+ }
5926
+ /**
5927
+ * Generates a unique lookup key for a given value, which is used to store and retrieve
5928
+ * values in maps during the eager loading process.
5929
+ *
5930
+ * @param value The value to generate a lookup key for.
5931
+ * @returns A unique string representing the value.
5932
+ */
5933
+ toLookupKey(value) {
5934
+ if (value instanceof Date) return `date:${value.toISOString()}`;
5935
+ return `${typeof value}:${String(value)}`;
5936
+ }
5937
+ };
5938
+
4832
5939
  //#endregion
4833
5940
  //#region src/URLDriver.ts
4834
5941
  /**
@@ -5727,21 +6834,20 @@ var QueryBuilder = class QueryBuilder {
5727
6834
  * @returns
5728
6835
  */
5729
6836
  async get() {
6837
+ const useAdapterRelationFeatures = this.canExecuteRelationFeaturesInAdapter();
5730
6838
  const relationCache = /* @__PURE__ */ new WeakMap();
5731
6839
  const rows = await this.executeReadRows();
5732
6840
  const normalizedRows = this.randomOrderEnabled ? this.shuffleRows(rows) : rows;
5733
6841
  const models = await this.model.hydrateManyRetrieved(normalizedRows);
5734
6842
  let filteredModels = models;
5735
- if (this.hasRelationFilters()) if (this.hasOrRelationFilters() && this.hasBaseWhereConstraints()) {
6843
+ if (this.hasRelationFilters() && !useAdapterRelationFeatures) if (this.hasOrRelationFilters() && this.hasBaseWhereConstraints()) {
5736
6844
  const baseIds = new Set(models.map((model) => this.getModelId(model)).filter((id) => id != null));
5737
6845
  const allRows = await this.executeReadRows(this.buildSoftDeleteOnlyWhere(), true);
5738
6846
  const allModels = this.model.hydrateMany(allRows);
5739
6847
  filteredModels = await this.filterModelsByRelationConstraints(allModels, relationCache, baseIds);
5740
6848
  } 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
- }));
6849
+ if (this.hasRelationAggregates() && !useAdapterRelationFeatures) await this.applyRelationAggregates(filteredModels, relationCache);
6850
+ await this.eagerLoadModels(filteredModels);
5745
6851
  return new ArkormCollection(filteredModels);
5746
6852
  }
5747
6853
  /**
@@ -5751,20 +6857,20 @@ var QueryBuilder = class QueryBuilder {
5751
6857
  * @returns
5752
6858
  */
5753
6859
  async first() {
5754
- if (this.hasRelationFilters() || this.hasRelationAggregates()) return (await this.get()).all()[0] ?? null;
6860
+ if ((this.hasRelationFilters() || this.hasRelationAggregates()) && !this.canExecuteRelationFeaturesInAdapter()) return (await this.get()).all()[0] ?? null;
5755
6861
  if (this.randomOrderEnabled) {
5756
6862
  const rows = await this.executeReadRows();
5757
6863
  if (rows.length === 0) return null;
5758
6864
  const row = this.shuffleRows(rows)[0];
5759
6865
  if (!row) return null;
5760
6866
  const model = await this.model.hydrateRetrieved(row);
5761
- await model.load(this.eagerLoads);
6867
+ await this.eagerLoadModels([model]);
5762
6868
  return model;
5763
6869
  }
5764
6870
  const row = await this.executeReadRow();
5765
6871
  if (!row) return null;
5766
6872
  const model = await this.model.hydrateRetrieved(row);
5767
- await model.load(this.eagerLoads);
6873
+ await this.eagerLoadModels([model]);
5768
6874
  return model;
5769
6875
  }
5770
6876
  /**
@@ -5928,6 +7034,13 @@ var QueryBuilder = class QueryBuilder {
5928
7034
  operation: "update",
5929
7035
  model: this.model.name
5930
7036
  });
7037
+ const directSpec = this.tryBuildUpdateSpec(where, data);
7038
+ const adapter = this.requireAdapter();
7039
+ if (!this.isUniqueWhere(where) && directSpec && typeof adapter.updateFirst === "function") {
7040
+ const updated = await adapter.updateFirst(directSpec);
7041
+ if (!updated) throw new ModelNotFoundException(this.model.name, "Record not found for update operation.", { operation: "update" });
7042
+ return this.model.hydrate(updated);
7043
+ }
5931
7044
  const uniqueWhere = await this.resolveUniqueWhere(where);
5932
7045
  const updated = await this.executeUpdateRow(uniqueWhere, data);
5933
7046
  return this.model.hydrate(updated);
@@ -5954,6 +7067,13 @@ var QueryBuilder = class QueryBuilder {
5954
7067
  * @returns
5955
7068
  */
5956
7069
  async updateOrInsert(attributes, values = {}) {
7070
+ if (typeof values !== "function" && this.adapter?.capabilities?.upsert && typeof this.requireAdapter().upsert === "function") {
7071
+ await this.executeUpsertRows([{
7072
+ ...attributes,
7073
+ ...values
7074
+ }], Object.keys(attributes), Object.keys(values));
7075
+ return true;
7076
+ }
5957
7077
  const exists = await this.clone().where(attributes).first() != null;
5958
7078
  const resolvedValues = typeof values === "function" ? await values(exists) : values;
5959
7079
  if (!exists) {
@@ -5976,6 +7096,7 @@ var QueryBuilder = class QueryBuilder {
5976
7096
  async upsert(values, uniqueBy, update = null) {
5977
7097
  if (values.length === 0) return 0;
5978
7098
  const uniqueKeys = Array.isArray(uniqueBy) ? uniqueBy : [uniqueBy];
7099
+ if (this.adapter?.capabilities?.upsert && typeof this.requireAdapter().upsert === "function") return await this.executeUpsertRows(values, uniqueKeys, update ?? void 0);
5979
7100
  let affected = 0;
5980
7101
  for (const row of values) {
5981
7102
  const attributes = uniqueKeys.reduce((all, key) => {
@@ -6003,6 +7124,13 @@ var QueryBuilder = class QueryBuilder {
6003
7124
  operation: "delete",
6004
7125
  model: this.model.name
6005
7126
  });
7127
+ const directSpec = this.tryBuildDeleteSpec(where);
7128
+ const adapter = this.requireAdapter();
7129
+ if (!this.isUniqueWhere(where) && directSpec && typeof adapter.deleteFirst === "function") {
7130
+ const deleted = await adapter.deleteFirst(directSpec);
7131
+ if (!deleted) throw new ModelNotFoundException(this.model.name, "Record not found for delete operation.", { operation: "delete" });
7132
+ return this.model.hydrate(deleted);
7133
+ }
6006
7134
  const uniqueWhere = await this.resolveUniqueWhere(where);
6007
7135
  const deleted = await this.executeDeleteRow(uniqueWhere);
6008
7136
  return this.model.hydrate(deleted);
@@ -6019,6 +7147,14 @@ var QueryBuilder = class QueryBuilder {
6019
7147
  values
6020
7148
  };
6021
7149
  }
7150
+ tryBuildUpsertSpec(values, uniqueBy, updateColumns) {
7151
+ return {
7152
+ target: this.buildQueryTarget(),
7153
+ values,
7154
+ uniqueBy,
7155
+ updateColumns
7156
+ };
7157
+ }
6022
7158
  tryBuildInsertOrIgnoreManySpec(values) {
6023
7159
  return {
6024
7160
  ...this.tryBuildInsertManySpec(values),
@@ -6057,7 +7193,7 @@ var QueryBuilder = class QueryBuilder {
6057
7193
  * @returns
6058
7194
  */
6059
7195
  async count() {
6060
- if (this.hasRelationFilters()) return (await this.get()).all().length;
7196
+ if (this.hasRelationFilters() && !this.canExecuteRelationFeaturesInAdapter()) return (await this.get()).all().length;
6061
7197
  return this.executeReadCount();
6062
7198
  }
6063
7199
  /**
@@ -6066,7 +7202,7 @@ var QueryBuilder = class QueryBuilder {
6066
7202
  * @returns
6067
7203
  */
6068
7204
  async exists() {
6069
- if (this.hasRelationFilters()) return await this.count() > 0;
7205
+ if (this.hasRelationFilters() && !this.canExecuteRelationFeaturesInAdapter()) return await this.count() > 0;
6070
7206
  return await this.executeReadExists();
6071
7207
  }
6072
7208
  /**
@@ -6241,7 +7377,7 @@ var QueryBuilder = class QueryBuilder {
6241
7377
  */
6242
7378
  async paginate(perPage = 15, page = void 0, options = {}) {
6243
7379
  const currentPage = this.resolvePaginationPage(page, options);
6244
- if (this.hasRelationFilters() || this.hasRelationAggregates()) {
7380
+ if ((this.hasRelationFilters() || this.hasRelationAggregates()) && !this.canExecuteRelationFeaturesInAdapter()) {
6245
7381
  const pageSize = Math.max(1, perPage);
6246
7382
  const rows = (await this.get()).all();
6247
7383
  const start = (currentPage - 1) * pageSize;
@@ -6260,7 +7396,7 @@ var QueryBuilder = class QueryBuilder {
6260
7396
  */
6261
7397
  async simplePaginate(perPage = 15, page = void 0, options = {}) {
6262
7398
  const currentPage = this.resolvePaginationPage(page, options);
6263
- if (this.hasRelationFilters() || this.hasRelationAggregates()) {
7399
+ if ((this.hasRelationFilters() || this.hasRelationAggregates()) && !this.canExecuteRelationFeaturesInAdapter()) {
6264
7400
  const pageSize = Math.max(1, perPage);
6265
7401
  const rows = (await this.get()).all();
6266
7402
  const start = (currentPage - 1) * pageSize;
@@ -6364,6 +7500,10 @@ var QueryBuilder = class QueryBuilder {
6364
7500
  };
6365
7501
  });
6366
7502
  }
7503
+ async eagerLoadModels(models) {
7504
+ if (models.length === 0 || Object.keys(this.eagerLoads).length === 0) return;
7505
+ await new SetBasedEagerLoader(models, this.eagerLoads).load();
7506
+ }
6367
7507
  normalizeRelationLoadSelect(select) {
6368
7508
  if (Array.isArray(select) || typeof select !== "object" || !select) return null;
6369
7509
  const entries = Object.entries(select);
@@ -6601,7 +7741,11 @@ var QueryBuilder = class QueryBuilder {
6601
7741
  const columns = this.tryBuildQuerySelectColumns();
6602
7742
  const orderBy = this.tryBuildQueryOrderBy();
6603
7743
  const condition = this.buildQueryWhereCondition(softDeleteOnly);
7744
+ const relationFilters = this.tryBuildRelationFilterSpecs();
7745
+ const relationAggregates = this.tryBuildRelationAggregateSpecs();
6604
7746
  if (columns === null || orderBy === null || condition === null) return null;
7747
+ if (this.hasRelationFilters() && this.canExecuteRelationFiltersInAdapter() && relationFilters === null) return null;
7748
+ if (this.hasRelationAggregates() && this.canExecuteRelationAggregatesInAdapter() && relationAggregates === null) return null;
6605
7749
  return {
6606
7750
  target: this.buildQueryTarget(),
6607
7751
  columns,
@@ -6609,15 +7753,20 @@ var QueryBuilder = class QueryBuilder {
6609
7753
  orderBy,
6610
7754
  limit: this.limitValue,
6611
7755
  offset: this.offsetValue,
6612
- relationLoads: this.queryRelationLoads
7756
+ relationLoads: this.queryRelationLoads,
7757
+ relationFilters: this.canExecuteRelationFiltersInAdapter() ? relationFilters ?? void 0 : void 0,
7758
+ relationAggregates: this.canExecuteRelationAggregatesInAdapter() ? relationAggregates ?? void 0 : void 0
6613
7759
  };
6614
7760
  }
6615
7761
  tryBuildAggregateSpec() {
6616
7762
  const condition = this.buildQueryWhereCondition(false);
7763
+ const relationFilters = this.tryBuildRelationFilterSpecs();
6617
7764
  if (condition === null) return null;
7765
+ if (this.hasRelationFilters() && this.canExecuteRelationFiltersInAdapter() && relationFilters === null) return null;
6618
7766
  return {
6619
7767
  target: this.buildQueryTarget(),
6620
7768
  where: condition,
7769
+ relationFilters: this.canExecuteRelationFiltersInAdapter() ? relationFilters ?? void 0 : void 0,
6621
7770
  aggregate: { type: "count" }
6622
7771
  };
6623
7772
  }
@@ -6687,6 +7836,14 @@ var QueryBuilder = class QueryBuilder {
6687
7836
  }
6688
7837
  return inserted;
6689
7838
  }
7839
+ async executeUpsertRows(values, uniqueBy, updateColumns) {
7840
+ const adapter = this.requireAdapter();
7841
+ if (typeof adapter.upsert !== "function") throw new UnsupportedAdapterFeatureException("Upsert is not supported by the current adapter.", {
7842
+ operation: "query.upsert",
7843
+ model: this.model.name
7844
+ });
7845
+ return await adapter.upsert(this.tryBuildUpsertSpec(values, uniqueBy, updateColumns));
7846
+ }
6690
7847
  async executeUpdateRow(where, values) {
6691
7848
  const adapter = this.requireAdapter();
6692
7849
  const spec = this.tryBuildUpdateSpec(where, values);
@@ -6815,6 +7972,71 @@ var QueryBuilder = class QueryBuilder {
6815
7972
  hasRelationAggregates() {
6816
7973
  return this.relationAggregates.length > 0;
6817
7974
  }
7975
+ canExecuteRelationFiltersInAdapter() {
7976
+ const adapter = this.adapter;
7977
+ if (!this.hasRelationFilters()) return false;
7978
+ return adapter?.capabilities?.relationFilters === true && this.tryBuildRelationFilterSpecs() !== null;
7979
+ }
7980
+ canExecuteRelationAggregatesInAdapter() {
7981
+ const adapter = this.adapter;
7982
+ if (!this.hasRelationAggregates()) return false;
7983
+ return adapter?.capabilities?.relationAggregates === true && this.tryBuildRelationAggregateSpecs() !== null;
7984
+ }
7985
+ canExecuteRelationFeaturesInAdapter() {
7986
+ const filtersSupported = !this.hasRelationFilters() || this.canExecuteRelationFiltersInAdapter();
7987
+ const aggregatesSupported = !this.hasRelationAggregates() || this.canExecuteRelationAggregatesInAdapter();
7988
+ return filtersSupported && aggregatesSupported;
7989
+ }
7990
+ tryBuildRelationFilterSpecs() {
7991
+ return this.relationFilters.reduce((specs, filter) => {
7992
+ if (!specs) return null;
7993
+ const metadata = this.model.getRelationMetadata(filter.relation);
7994
+ if (!this.isSqlRelationFeatureMetadata(metadata)) return null;
7995
+ const where = this.tryBuildRelationConstraintWhere(filter.relation, filter.callback);
7996
+ if (where === null) return null;
7997
+ specs.push({
7998
+ relation: filter.relation,
7999
+ operator: filter.operator,
8000
+ count: filter.count,
8001
+ boolean: filter.boolean,
8002
+ where
8003
+ });
8004
+ return specs;
8005
+ }, []);
8006
+ }
8007
+ tryBuildRelationAggregateSpecs() {
8008
+ return this.relationAggregates.reduce((specs, aggregate) => {
8009
+ if (!specs) return null;
8010
+ const metadata = this.model.getRelationMetadata(aggregate.relation);
8011
+ if (!this.isSqlRelationFeatureMetadata(metadata)) return null;
8012
+ const where = this.tryBuildRelationConstraintWhere(aggregate.relation);
8013
+ if (where === null) return null;
8014
+ specs.push({
8015
+ relation: aggregate.relation,
8016
+ type: aggregate.type,
8017
+ column: aggregate.column,
8018
+ alias: this.buildAggregateAttributeKey(aggregate),
8019
+ where
8020
+ });
8021
+ return specs;
8022
+ }, []);
8023
+ }
8024
+ tryBuildRelationConstraintWhere(relation, callback) {
8025
+ const metadata = this.model.getRelationMetadata(relation);
8026
+ if (!this.isSqlRelationFeatureMetadata(metadata)) return null;
8027
+ const relatedQuery = metadata?.relatedModel.query();
8028
+ if (!relatedQuery) return null;
8029
+ if (callback) {
8030
+ const constrained = callback(relatedQuery);
8031
+ if (constrained && constrained !== relatedQuery) return null;
8032
+ }
8033
+ if (relatedQuery.hasRelationFilters() || relatedQuery.hasRelationAggregates() || relatedQuery.queryRelationLoads || relatedQuery.querySelect || relatedQuery.queryOrderBy || relatedQuery.offsetValue !== void 0 || relatedQuery.limitValue !== void 0 || relatedQuery.randomOrderEnabled) return null;
8034
+ return relatedQuery.buildQueryWhereCondition(false);
8035
+ }
8036
+ isSqlRelationFeatureMetadata(metadata) {
8037
+ if (!metadata) return false;
8038
+ return metadata.type === "hasMany" || metadata.type === "hasOne" || metadata.type === "belongsTo" || metadata.type === "belongsToMany" || metadata.type === "hasOneThrough" || metadata.type === "hasManyThrough";
8039
+ }
6818
8040
  async filterModelsByRelationConstraints(models, relationCache, baseIds) {
6819
8041
  return (await Promise.all(models.map(async (model) => {
6820
8042
  let result = null;
@@ -6971,6 +8193,7 @@ var QueryBuilder = class QueryBuilder {
6971
8193
  */
6972
8194
  var Model = class Model {
6973
8195
  static lifecycleStates = /* @__PURE__ */ new WeakMap();
8196
+ static emittedDeprecationWarnings = /* @__PURE__ */ new Set();
6974
8197
  static eventsSuppressed = 0;
6975
8198
  static factoryClass;
6976
8199
  static adapter;
@@ -7014,12 +8237,24 @@ var Model = class Model {
7014
8237
  }
7015
8238
  });
7016
8239
  }
8240
+ static emitDeprecationWarning(code, message) {
8241
+ if (Model.emittedDeprecationWarnings.has(code)) return;
8242
+ Model.emittedDeprecationWarnings.add(code);
8243
+ process.emitWarning(message, {
8244
+ type: "DeprecationWarning",
8245
+ code
8246
+ });
8247
+ }
7017
8248
  /**
7018
- * Set the Prisma client delegates for all models.
7019
- *
7020
- * @param client
8249
+ * Set the Prisma client delegates for all models.
8250
+ *
8251
+ * @deprecated Use Model.setAdapter(createPrismaDatabaseAdapter(...)) or another
8252
+ * adapter-first bootstrap path instead.
8253
+ *
8254
+ * @param client
7021
8255
  */
7022
8256
  static setClient(client) {
8257
+ 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
8258
  this.client = client;
7024
8259
  }
7025
8260
  static setAdapter(adapter) {
@@ -7252,6 +8487,8 @@ var Model = class Model {
7252
8487
  static getAdapter() {
7253
8488
  ensureArkormConfigLoading();
7254
8489
  if (this.adapter) return this.adapter;
8490
+ const runtimeAdapter = getRuntimeAdapter();
8491
+ if (runtimeAdapter) return runtimeAdapter;
7255
8492
  const client = getActiveTransactionClient() ?? this.client ?? getRuntimePrismaClient();
7256
8493
  if (!client || typeof client !== "object") return void 0;
7257
8494
  return createPrismaCompatibilityAdapter(client);
@@ -7556,14 +8793,11 @@ var Model = class Model {
7556
8793
  */
7557
8794
  async load(relations) {
7558
8795
  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
- }));
8796
+ await new SetBasedEagerLoader([this], relationMap).load();
8797
+ return this;
8798
+ }
8799
+ setLoadedRelation(name, value) {
8800
+ this.attributes[name] = value;
7567
8801
  return this;
7568
8802
  }
7569
8803
  /**
@@ -8093,6 +9327,7 @@ exports.applyDropTableOperation = applyDropTableOperation;
8093
9327
  exports.applyMigrationRollbackToPrismaSchema = applyMigrationRollbackToPrismaSchema;
8094
9328
  exports.applyMigrationToPrismaSchema = applyMigrationToPrismaSchema;
8095
9329
  exports.applyOperationsToPrismaSchema = applyOperationsToPrismaSchema;
9330
+ exports.bindAdapterToModels = bindAdapterToModels;
8096
9331
  exports.buildEnumBlock = buildEnumBlock;
8097
9332
  exports.buildFieldLine = buildFieldLine;
8098
9333
  exports.buildIndexLine = buildIndexLine;
@@ -8131,6 +9366,7 @@ exports.getDefaultStubsPath = getDefaultStubsPath;
8131
9366
  exports.getLastMigrationRun = getLastMigrationRun;
8132
9367
  exports.getLatestAppliedMigrations = getLatestAppliedMigrations;
8133
9368
  exports.getMigrationPlan = getMigrationPlan;
9369
+ exports.getRuntimeAdapter = getRuntimeAdapter;
8134
9370
  exports.getRuntimePaginationCurrentPageResolver = getRuntimePaginationCurrentPageResolver;
8135
9371
  exports.getRuntimePaginationURLDriverFactory = getRuntimePaginationURLDriverFactory;
8136
9372
  exports.getRuntimePrismaClient = getRuntimePrismaClient;