arkormx 2.6.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as applyOperationsToPersistedColumnMappingsState, $t as resolvePrismaType, A as getRuntimePrismaClient, An as writeAppliedMigrationsStateToStore, At as buildInverseRelationLine, B as getRegisteredModels, Bt as deriveRelationFieldName, C as getActiveTransactionClient, Cn as markMigrationRun, Ct as applyMigrationRollbackToPrismaSchema, D as getRuntimeDebugHandler, Dn as resolveMigrationStateFilePath, Dt as buildEnumBlock, E as getRuntimeClient, En as removeAppliedMigration, Et as applyOperationsToPrismaSchema, F as loadArkormConfig, Fn as RelationResolutionException, Ft as buildUniqueConstraintLine, G as loadModelsFrom, Gt as formatDefaultValue, H as getRegisteredSeeders, Ht as escapeRegex, I as resetArkormRuntimeForTests, In as ArkormCollection, It as createMigrationTimestamp, J as registerMigrations, Jt as generateMigrationFile, K as loadSeedersFrom, Kt as formatEnumDefaultValue, L as runArkormTransaction, Ln as ArkormException, Lt as deriveCollectionFieldName, M as isDelegateLike, Mn as UnsupportedAdapterFeatureException, Mt as buildModelBlock, N as isQuerySchemaLike, Nn as SetBasedEagerLoader, Nt as buildPrimaryKeyLine, O as getRuntimePaginationCurrentPageResolver, On as supportsDatabaseMigrationState, Ot as buildFieldLine, P as isTransactionCapableClient, Pt as buildRelationLine, Q as resetRuntimeRegistryForTests, Qt as resolveMigrationClassName, R as getRegisteredFactories, Rt as deriveInverseRelationAlias, S as getActiveTransactionAdapter, Sn as markMigrationApplied, St as applyMigrationRollbackToDatabase, T as getRuntimeAdapter, Tn as readAppliedMigrationsStateFromStore, Tt as applyMigrationToPrismaSchema, U as loadFactoriesFrom, Ut as findEnumBlock, V as getRegisteredPaths, Vt as deriveSingularFieldName, W as loadMigrationsFrom, Wt as findModelBlock, X as registerPaths, Xt as pad, Y as registerModels, Yt as getMigrationPlan, Z as registerSeeders, Zt as resolveEnumName, _ as bindAdapterToModels, _n as deleteAppliedMigrationsStateFromStore, _t as PRISMA_ENUM_REGEX, a as HasOneThroughRelation, an as supportsDatabaseReset, at as getPersistedPrimaryKeyGeneration, b as emitRuntimeDebugEvent, bn as getLatestAppliedMigrations, bt as applyCreateTableOperation, c as HasManyRelation, cn as SchemaBuilder, ct as readPersistedColumnMappingsState, d as BelongsToManyRelation, dn as PrimaryKeyGenerationPlanner, dt as resolveColumnMappingsFilePath, en as runMigrationWithPrisma, et as createEmptyPersistedColumnMappingsState, f as Relation, fn as ForeignKeyBuilder, ft as resolvePersistedMetadataFeatures, g as awaitConfiguredModelsRegistration, gn as createEmptyAppliedMigrationsState, gt as PRISMA_ENUM_MEMBER_REGEX, h as URLDriver, hn as computeMigrationChecksum, ht as writePersistedColumnMappingsState, i as MorphManyRelation, in as supportsDatabaseMigrationExecution, it as getPersistedEnumTsType, j as getUserConfig, jn as RuntimeModuleLoader, jt as buildMigrationSource, k as getRuntimePaginationURLDriverFactory, kn as writeAppliedMigrationsState, kt as buildIndexLine, l as BelongsToRelation, ln as EnumBuilder, lt as rebuildPersistedColumnMappingsState, m as Paginator, mn as buildMigrationRunId, mt as validatePersistedMetadataFeaturesForMigrations, n as MorphToManyRelation, nn as stripPrismaSchemaModelsAndEnums, nt as getPersistedColumnMap, o as HasOneRelation, on as toMigrationFileSlug, ot as getPersistedTableMetadata, p as LengthAwarePaginator, pn as buildMigrationIdentity, pt as syncPersistedColumnMappingsFromState, q as registerFactories, qt as formatRelationAction, r as MorphOneRelation, rn as supportsDatabaseCreation, rt as getPersistedEnumMap, s as HasManyThroughRelation, sn as toModelName, st as getPersistedTimestampColumns, t as MorphToRelation, tn as runPrismaCommand, tt as deletePersistedColumnMappingsState, un as TableBuilder, ut as resetPersistedColumnMappingsCache, v as configureArkormRuntime, vn as findAppliedMigration, vt as PRISMA_MODEL_REGEX, w as getDefaultStubsPath, wn as readAppliedMigrationsState, wt as applyMigrationToDatabase, x as ensureArkormConfigLoading, xn as isMigrationApplied, xt as applyDropTableOperation, y as defineConfig, yn as getLastMigrationRun, yt as applyAlterTableOperation, z as getRegisteredMigrations, zt as deriveRelationAlias } from "./relationship-DT4myOWX.mjs";
1
+ import { $ as applyOperationsToPersistedColumnMappingsState, $t as resolvePrismaType, A as getRuntimePrismaClient, An as writeAppliedMigrationsStateToStore, At as buildInverseRelationLine, B as getRegisteredModels, Bt as deriveRelationFieldName, C as getActiveTransactionClient, Cn as markMigrationRun, Ct as applyMigrationRollbackToPrismaSchema, D as getRuntimeDebugHandler, Dn as resolveMigrationStateFilePath, Dt as buildEnumBlock, E as getRuntimeClient, En as removeAppliedMigration, Et as applyOperationsToPrismaSchema, F as loadArkormConfig, Fn as RelationResolutionException, Ft as buildUniqueConstraintLine, G as loadModelsFrom, Gt as formatDefaultValue, H as getRegisteredSeeders, Ht as escapeRegex, I as resetArkormRuntimeForTests, In as ArkormCollection, It as createMigrationTimestamp, J as registerMigrations, Jt as generateMigrationFile, K as loadSeedersFrom, Kt as formatEnumDefaultValue, L as runArkormTransaction, Ln as ArkormException, Lt as deriveCollectionFieldName, M as isDelegateLike, Mn as UnsupportedAdapterFeatureException, Mt as buildModelBlock, N as isQuerySchemaLike, Nn as SetBasedEagerLoader, Nt as buildPrimaryKeyLine, O as getRuntimePaginationCurrentPageResolver, On as supportsDatabaseMigrationState, Ot as buildFieldLine, P as isTransactionCapableClient, Pt as buildRelationLine, Q as resetRuntimeRegistryForTests, Qt as resolveMigrationClassName, R as getRegisteredFactories, Rt as deriveInverseRelationAlias, S as getActiveTransactionAdapter, Sn as markMigrationApplied, St as applyMigrationRollbackToDatabase, T as getRuntimeAdapter, Tn as readAppliedMigrationsStateFromStore, Tt as applyMigrationToPrismaSchema, U as loadFactoriesFrom, Ut as findEnumBlock, V as getRegisteredPaths, Vt as deriveSingularFieldName, W as loadMigrationsFrom, Wt as findModelBlock, X as registerPaths, Xt as pad, Y as registerModels, Yt as getMigrationPlan, Z as registerSeeders, Zt as resolveEnumName, _ as bindAdapterToModels, _n as deleteAppliedMigrationsStateFromStore, _t as PRISMA_ENUM_REGEX, a as HasOneThroughRelation, an as supportsDatabaseReset, at as getPersistedPrimaryKeyGeneration, b as emitRuntimeDebugEvent, bn as getLatestAppliedMigrations, bt as applyCreateTableOperation, c as HasManyRelation, cn as SchemaBuilder, ct as readPersistedColumnMappingsState, d as BelongsToManyRelation, dn as PrimaryKeyGenerationPlanner, dt as resolveColumnMappingsFilePath, en as runMigrationWithPrisma, et as createEmptyPersistedColumnMappingsState, f as Relation, fn as ForeignKeyBuilder, ft as resolvePersistedMetadataFeatures, g as awaitConfiguredModelsRegistration, gn as createEmptyAppliedMigrationsState, gt as PRISMA_ENUM_MEMBER_REGEX, h as URLDriver, hn as computeMigrationChecksum, ht as writePersistedColumnMappingsState, i as MorphManyRelation, in as supportsDatabaseMigrationExecution, it as getPersistedEnumTsType, j as getUserConfig, jn as RuntimeModuleLoader, jt as buildMigrationSource, k as getRuntimePaginationURLDriverFactory, kn as writeAppliedMigrationsState, kt as buildIndexLine, l as BelongsToRelation, ln as EnumBuilder, lt as rebuildPersistedColumnMappingsState, m as Paginator, mn as buildMigrationRunId, mt as validatePersistedMetadataFeaturesForMigrations, n as MorphToManyRelation, nn as stripPrismaSchemaModelsAndEnums, nt as getPersistedColumnMap, o as HasOneRelation, on as toMigrationFileSlug, ot as getPersistedTableMetadata, p as LengthAwarePaginator, pn as buildMigrationIdentity, pt as syncPersistedColumnMappingsFromState, q as registerFactories, qt as formatRelationAction, r as MorphOneRelation, rn as supportsDatabaseCreation, rt as getPersistedEnumMap, s as HasManyThroughRelation, sn as toModelName, st as getPersistedTimestampColumns, t as MorphToRelation, tn as runPrismaCommand, tt as deletePersistedColumnMappingsState, un as TableBuilder, ut as resetPersistedColumnMappingsCache, v as configureArkormRuntime, vn as findAppliedMigration, vt as PRISMA_MODEL_REGEX, w as getDefaultStubsPath, wn as readAppliedMigrationsState, wt as applyMigrationToDatabase, x as ensureArkormConfigLoading, xn as isMigrationApplied, xt as applyDropTableOperation, y as defineConfig, yn as getLastMigrationRun, yt as applyAlterTableOperation, z as getRegisteredMigrations, zt as deriveRelationAlias } from "./relationship--l8RA_yy.mjs";
2
2
  import { Pool } from "pg";
3
3
  import { join, resolve } from "node:path";
4
4
  import { createRequire } from "module";
@@ -63,7 +63,8 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
63
63
  rawSelect: true,
64
64
  rawWhere: true,
65
65
  distinct: true,
66
- groupBy: true
66
+ groupBy: true,
67
+ joins: true
67
68
  };
68
69
  }
69
70
  resolveConfiguredDatabaseName(connectionString) {
@@ -88,6 +89,85 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
88
89
  quoteIdentifier(value) {
89
90
  return `"${value.replace(/"/g, "\"\"")}"`;
90
91
  }
92
+ /**
93
+ * Wraps bare camelCase identifiers in a fragment of raw SQL with double quotes
94
+ * so PostgreSQL preserves their casing instead of folding them to lower case.
95
+ *
96
+ * Identifiers that are already quoted, string literals, dollar-quoted bodies,
97
+ * comments and function names (a camelCase token immediately followed by `(`)
98
+ * are left untouched. Lower-case identifiers are also left alone since
99
+ * PostgreSQL folds them to the same value with or without quotes.
100
+ *
101
+ * @param sql The raw SQL fragment to normalize.
102
+ * @returns
103
+ */
104
+ quoteCamelCaseIdentifiers(sql) {
105
+ let result = "";
106
+ let index = 0;
107
+ const isIdentifierStart = (char) => /[A-Za-z_]/.test(char);
108
+ const isIdentifierPart = (char) => /[A-Za-z0-9_$]/.test(char);
109
+ const isMixedCase = (token) => /[A-Z]/.test(token) && /[a-z]/.test(token);
110
+ while (index < sql.length) {
111
+ const char = sql[index];
112
+ if (char === "'") {
113
+ const start = index;
114
+ index += 1;
115
+ while (index < sql.length) {
116
+ if (sql[index] === "'" && sql[index + 1] === "'") {
117
+ index += 2;
118
+ continue;
119
+ }
120
+ if (sql[index] === "'") {
121
+ index += 1;
122
+ break;
123
+ }
124
+ index += 1;
125
+ }
126
+ result += sql.slice(start, index);
127
+ continue;
128
+ }
129
+ if (char === "\"") {
130
+ const start = index;
131
+ index += 1;
132
+ while (index < sql.length) {
133
+ if (sql[index] === "\"" && sql[index + 1] === "\"") {
134
+ index += 2;
135
+ continue;
136
+ }
137
+ if (sql[index] === "\"") {
138
+ index += 1;
139
+ break;
140
+ }
141
+ index += 1;
142
+ }
143
+ result += sql.slice(start, index);
144
+ continue;
145
+ }
146
+ if (char === "$") {
147
+ const tagMatch = /^\$[A-Za-z0-9_]*\$/.exec(sql.slice(index));
148
+ if (tagMatch) {
149
+ const tag = tagMatch[0];
150
+ const closeIndex = sql.indexOf(tag, index + tag.length);
151
+ const end = closeIndex === -1 ? sql.length : closeIndex + tag.length;
152
+ result += sql.slice(index, end);
153
+ index = end;
154
+ continue;
155
+ }
156
+ }
157
+ if (isIdentifierStart(char)) {
158
+ const start = index;
159
+ while (index < sql.length && isIdentifierPart(sql[index])) index += 1;
160
+ const token = sql.slice(start, index);
161
+ const isFunctionCall = sql[index] === "(";
162
+ if (isMixedCase(token) && !isFunctionCall) result += this.quoteIdentifier(token);
163
+ else result += token;
164
+ continue;
165
+ }
166
+ result += char;
167
+ index += 1;
168
+ }
169
+ return result;
170
+ }
91
171
  quoteLiteral(value) {
92
172
  if (value == null) return "null";
93
173
  if (typeof value === "number" || typeof value === "bigint") return String(value);
@@ -107,10 +187,102 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
107
187
  async executeRawStatement(statement, executor = this.db) {
108
188
  await sql.raw(statement).execute(executor);
109
189
  }
190
+ /**
191
+ * Splits a SQL script into individual top-level statements.
192
+ *
193
+ * The PostgreSQL wire protocol used by Kysely rejects scripts that contain
194
+ * more than one command, so multi-statement raw SQL (for example a migration
195
+ * that mixes `do $$ ... $$` blocks with `alter table` statements) must be
196
+ * executed one statement at a time. Semicolons inside single-quoted strings,
197
+ * double-quoted identifiers, dollar-quoted bodies and comments are ignored.
198
+ *
199
+ * @param sql The raw SQL script to split.
200
+ * @returns
201
+ */
202
+ splitSqlStatements(sql) {
203
+ const statements = [];
204
+ let current = "";
205
+ let index = 0;
206
+ while (index < sql.length) {
207
+ const char = sql[index];
208
+ const next = sql[index + 1];
209
+ if (char === "-" && next === "-") {
210
+ const end = sql.indexOf("\n", index);
211
+ const stop = end === -1 ? sql.length : end;
212
+ current += sql.slice(index, stop);
213
+ index = stop;
214
+ continue;
215
+ }
216
+ if (char === "/" && next === "*") {
217
+ const end = sql.indexOf("*/", index + 2);
218
+ const stop = end === -1 ? sql.length : end + 2;
219
+ current += sql.slice(index, stop);
220
+ index = stop;
221
+ continue;
222
+ }
223
+ if (char === "'") {
224
+ const start = index;
225
+ index += 1;
226
+ while (index < sql.length) {
227
+ if (sql[index] === "'" && sql[index + 1] === "'") {
228
+ index += 2;
229
+ continue;
230
+ }
231
+ if (sql[index] === "'") {
232
+ index += 1;
233
+ break;
234
+ }
235
+ index += 1;
236
+ }
237
+ current += sql.slice(start, index);
238
+ continue;
239
+ }
240
+ if (char === "\"") {
241
+ const start = index;
242
+ index += 1;
243
+ while (index < sql.length) {
244
+ if (sql[index] === "\"" && sql[index + 1] === "\"") {
245
+ index += 2;
246
+ continue;
247
+ }
248
+ if (sql[index] === "\"") {
249
+ index += 1;
250
+ break;
251
+ }
252
+ index += 1;
253
+ }
254
+ current += sql.slice(start, index);
255
+ continue;
256
+ }
257
+ if (char === "$") {
258
+ const tagMatch = /^\$[A-Za-z0-9_]*\$/.exec(sql.slice(index));
259
+ if (tagMatch) {
260
+ const tag = tagMatch[0];
261
+ const closeIndex = sql.indexOf(tag, index + tag.length);
262
+ const end = closeIndex === -1 ? sql.length : closeIndex + tag.length;
263
+ current += sql.slice(index, end);
264
+ index = end;
265
+ continue;
266
+ }
267
+ }
268
+ if (char === ";") {
269
+ if (current.trim().length > 0) statements.push(current.trim());
270
+ current = "";
271
+ index += 1;
272
+ continue;
273
+ }
274
+ current += char;
275
+ index += 1;
276
+ }
277
+ if (current.trim().length > 0) statements.push(current.trim());
278
+ return statements;
279
+ }
110
280
  async rawQuery(spec) {
111
281
  const statement = this.interpolateRawSql(spec.sql, spec.bindings);
282
+ const statements = this.splitSqlStatements(statement);
283
+ if (statements.length > 1) return await this.runMultiStatementRawQuery(statements, statement);
112
284
  try {
113
- return (await sql.raw(statement).execute(this.db)).rows ?? [];
285
+ return (await sql.raw(statements[0] ?? statement).execute(this.db)).rows ?? [];
114
286
  } catch (error) {
115
287
  throw new QueryExecutionException("Raw query execution failed for the Kysely adapter.", {
116
288
  code: "QUERY_EXECUTION_FAILED",
@@ -122,6 +294,45 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
122
294
  });
123
295
  }
124
296
  }
297
+ /**
298
+ * Executes a multi-statement raw SQL script one statement at a time.
299
+ *
300
+ * Each statement is sent individually so the PostgreSQL extended protocol
301
+ * accepts it, and the rows of the last statement that returns any are used as
302
+ * the result. The script is wrapped in a transaction when the adapter is not
303
+ * already operating inside one so partial failures do not leave the database
304
+ * in a half-applied state.
305
+ *
306
+ * @param statements The individual statements to execute in order.
307
+ * @param fullScript The original (joined) script, used for error context.
308
+ * @returns
309
+ */
310
+ async runMultiStatementRawQuery(statements, fullScript) {
311
+ const execute = async (executor) => {
312
+ let rows = [];
313
+ for (const statement of statements) {
314
+ const result = await sql.raw(statement).execute(executor);
315
+ if (result.rows && result.rows.length > 0) rows = result.rows;
316
+ }
317
+ return rows;
318
+ };
319
+ try {
320
+ if (!this.db.isTransaction) return await this.db.transaction().execute((transaction) => execute(transaction));
321
+ return await execute(this.db);
322
+ } catch (error) {
323
+ throw new QueryExecutionException("Raw query execution failed for the Kysely adapter.", {
324
+ code: "QUERY_EXECUTION_FAILED",
325
+ operation: "adapter.rawQuery",
326
+ delegate: "raw",
327
+ inspection: this.tryInspectRawQuery(fullScript),
328
+ meta: {
329
+ sql: fullScript,
330
+ statements
331
+ },
332
+ cause: error
333
+ });
334
+ }
335
+ }
125
336
  tryInspectRawQuery(statement) {
126
337
  return {
127
338
  adapter: "kysely",
@@ -403,6 +614,70 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
403
614
  if (!groupBy || groupBy.length === 0) return sql``;
404
615
  return sql` group by ${sql.join(groupBy.map((column) => sql.ref(this.mapColumn(target, column))), sql`, `)}`;
405
616
  }
617
+ buildJoinClause(target, joins) {
618
+ if (!joins || joins.length === 0) return sql``;
619
+ return sql` ${sql.join(joins.map((join) => this.buildSingleJoin(target, join)), sql` `)}`;
620
+ }
621
+ joinKeyword(join) {
622
+ const base = {
623
+ inner: "inner join",
624
+ left: "left join",
625
+ right: "right join",
626
+ full: "full join",
627
+ cross: "cross join"
628
+ }[join.type];
629
+ return join.lateral ? `${base} lateral` : base;
630
+ }
631
+ buildJoinSource(join) {
632
+ if (join.subquery) {
633
+ const subquery = this.buildSelectStatement(join.subquery);
634
+ return join.alias ? sql`(${subquery}) as ${sql.id(join.alias)}` : sql`(${subquery})`;
635
+ }
636
+ if (join.subquerySql) return join.alias ? sql`(${sql.raw(join.subquerySql)}) as ${sql.id(join.alias)}` : sql.raw(`(${join.subquerySql})`);
637
+ const table = this.resolveMappedTable(join.table ?? "");
638
+ return join.alias ? sql`${sql.table(table)} as ${sql.id(join.alias)}` : sql.table(table);
639
+ }
640
+ buildSingleJoin(target, join) {
641
+ const keyword = sql.raw(this.joinKeyword(join));
642
+ const source = this.buildJoinSource(join);
643
+ if (join.type === "cross") return sql`${keyword} ${source}`;
644
+ if (join.constraints.length === 0) return join.lateral ? sql`${keyword} ${source} on true` : sql`${keyword} ${source}`;
645
+ return sql`${keyword} ${source} on ${this.buildJoinConstraints(target, join.constraints)}`;
646
+ }
647
+ buildJoinConstraints(target, constraints) {
648
+ const parts = [];
649
+ constraints.forEach((constraint, index) => {
650
+ if (index > 0) parts.push(sql.raw(` ${constraint.boolean} `));
651
+ parts.push(this.buildJoinConstraint(target, constraint));
652
+ });
653
+ return sql`${sql.join(parts, sql``)}`;
654
+ }
655
+ buildJoinConstraint(target, constraint) {
656
+ if (constraint.type === "column") return sql`${sql.ref(constraint.first)} ${sql.raw(constraint.operator)} ${sql.ref(constraint.second)}`;
657
+ if (constraint.type === "null") return constraint.not ? sql`${sql.ref(constraint.column)} is not null` : sql`${sql.ref(constraint.column)} is null`;
658
+ if (constraint.type === "raw") return this.buildRawWhereCondition({
659
+ type: "raw",
660
+ sql: constraint.sql,
661
+ bindings: constraint.bindings
662
+ });
663
+ if (constraint.type === "nested") return sql`(${this.buildJoinConstraints(target, constraint.constraints)})`;
664
+ const column = sql.ref(constraint.column);
665
+ const operator = constraint.operator;
666
+ if (operator === "is-null") return sql`${column} is null`;
667
+ if (operator === "is-not-null") return sql`${column} is not null`;
668
+ if (operator === "in") {
669
+ const values = this.buildConditionValueList(constraint.value);
670
+ return values.length === 0 ? sql`1 = 0` : sql`${column} in (${sql.join(values)})`;
671
+ }
672
+ if (operator === "not-in") {
673
+ const values = this.buildConditionValueList(constraint.value);
674
+ return values.length === 0 ? sql`1 = 1` : sql`${column} not in (${sql.join(values)})`;
675
+ }
676
+ if (operator === "contains") return sql`${column} like ${`%${String(constraint.value ?? "")}%`}`;
677
+ if (operator === "starts-with") return sql`${column} like ${`${String(constraint.value ?? "")}%`}`;
678
+ if (operator === "ends-with") return sql`${column} like ${`%${String(constraint.value ?? "")}`}`;
679
+ return sql`${column} ${sql.raw(operator)} ${constraint.value}`;
680
+ }
406
681
  buildConditionValueList(value) {
407
682
  if (Array.isArray(value)) return value;
408
683
  return typeof value === "undefined" ? [] : [value];
@@ -432,7 +707,7 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
432
707
  if (segments.length !== bindings.length + 1) throw new ArkormException("Raw where bindings do not match the number of placeholders.");
433
708
  const parts = [];
434
709
  segments.forEach((segment, index) => {
435
- if (segment.length > 0) parts.push(sql.raw(segment));
710
+ if (segment.length > 0) parts.push(sql.raw(this.quoteCamelCaseIdentifiers(segment)));
436
711
  if (index < bindings.length) parts.push(sql`${bindings[index]}`);
437
712
  });
438
713
  if (parts.length === 0) return sql`1 = 1`;
@@ -696,6 +971,7 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
696
971
  select ${spec.distinct ? sql`distinct ` : sql``}${this.buildSelectList(spec.target, spec.columns)}
697
972
  ${this.buildRelationAggregateSelectList(spec.target, spec.relationAggregates)}
698
973
  from ${sql.table(this.resolveTable(spec.target))}
974
+ ${this.buildJoinClause(spec.target, spec.joins)}
699
975
  ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
700
976
  ${this.buildGroupBy(spec.target, spec.groupBy)}
701
977
  ${this.buildOrderBy(spec.target, spec.orderBy)}
@@ -706,6 +982,7 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
706
982
  return sql`
707
983
  select count(*)::int as count
708
984
  from ${sql.table(this.resolveTable(spec.target))}
985
+ ${this.buildJoinClause(spec.target, spec.joins)}
709
986
  ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
710
987
  `;
711
988
  }
@@ -714,6 +991,7 @@ var KyselyDatabaseAdapter = class KyselyDatabaseAdapter {
714
991
  select exists(
715
992
  select 1
716
993
  from ${sql.table(this.resolveTable(spec.target))}
994
+ ${this.buildJoinClause(spec.target, spec.joins)}
717
995
  ${this.buildCombinedWhereClause(spec.target, spec.where, spec.relationFilters)}
718
996
  ${this.buildGroupBy(spec.target, spec.groupBy)}
719
997
  limit 1
@@ -1445,6 +1723,10 @@ var PrismaDatabaseAdapter = class PrismaDatabaseAdapter {
1445
1723
  operation: "adapter.select",
1446
1724
  meta: { feature: "groupBy" }
1447
1725
  });
1726
+ if (spec.joins?.length) throw new UnsupportedAdapterFeatureException("Join clauses are not supported by the Prisma compatibility adapter; use a SQL-backed adapter or DB.raw().", {
1727
+ operation: "adapter.select",
1728
+ meta: { feature: "joins" }
1729
+ });
1448
1730
  return {
1449
1731
  include: this.toQueryInclude(spec.relationLoads),
1450
1732
  where: this.toQueryWhere(spec.where),
@@ -3799,6 +4081,204 @@ const resolveRuntimeCompatibilityQuerySchemaOrThrow = (key, candidates, modelNam
3799
4081
  return resolved;
3800
4082
  };
3801
4083
 
4084
+ //#endregion
4085
+ //#region src/JoinClause.ts
4086
+ /**
4087
+ * A fluent builder for the `on`/`where` constraints of a join clause.
4088
+ *
4089
+ * Instances are handed to the closure form of the query builder join helpers
4090
+ * (for example `query.join('posts', join => join.on(...).where(...))`) and
4091
+ * mirror Laravel's `JoinClause` surface. Column identifiers are treated as raw
4092
+ * database identifiers (qualify them as `table.column` when needed).
4093
+ *
4094
+ * @author Legacy (3m1n3nc3)
4095
+ */
4096
+ var JoinClause = class JoinClause {
4097
+ constructor() {
4098
+ this.constraints = [];
4099
+ }
4100
+ /**
4101
+ * Adds a column-to-column `on` constraint, joined with `and`.
4102
+ *
4103
+ * Accepts either a closure (for a nested group) or a column comparison in
4104
+ * the `(first, second)` or `(first, operator, second)` form.
4105
+ *
4106
+ * @param first The left-hand column or a nested closure.
4107
+ * @param operator The comparison operator (defaults to `=`).
4108
+ * @param second The right-hand column.
4109
+ * @returns
4110
+ */
4111
+ on(first, operator, second) {
4112
+ return this.addOn("and", first, operator, second);
4113
+ }
4114
+ /**
4115
+ * Adds a column-to-column `on` constraint, joined with `or`.
4116
+ *
4117
+ * @param first The left-hand column or a nested closure.
4118
+ * @param operator The comparison operator (defaults to `=`).
4119
+ * @param second The right-hand column.
4120
+ * @returns
4121
+ */
4122
+ orOn(first, operator, second) {
4123
+ return this.addOn("or", first, operator, second);
4124
+ }
4125
+ /**
4126
+ * Adds a column-to-value constraint, joined with `and`.
4127
+ *
4128
+ * @param column The column being compared.
4129
+ * @param operator The comparison operator or the value when omitted.
4130
+ * @param value The value to compare against.
4131
+ * @returns
4132
+ */
4133
+ where(column, operator, value) {
4134
+ return this.addWhere("and", column, operator, value);
4135
+ }
4136
+ /**
4137
+ * Adds a column-to-value constraint, joined with `or`.
4138
+ *
4139
+ * @param column The column being compared.
4140
+ * @param operator The comparison operator or the value when omitted.
4141
+ * @param value The value to compare against.
4142
+ * @returns
4143
+ */
4144
+ orWhere(column, operator, value) {
4145
+ return this.addWhere("or", column, operator, value);
4146
+ }
4147
+ /**
4148
+ * Adds an `is null` constraint joined with `and`.
4149
+ *
4150
+ * @param column The column to test for null.
4151
+ * @returns
4152
+ */
4153
+ whereNull(column) {
4154
+ this.constraints.push({
4155
+ type: "null",
4156
+ boolean: "and",
4157
+ column,
4158
+ not: false
4159
+ });
4160
+ return this;
4161
+ }
4162
+ /**
4163
+ * Adds an `is null` constraint joined with `or`.
4164
+ *
4165
+ * @param column The column to test for null.
4166
+ * @returns
4167
+ */
4168
+ orWhereNull(column) {
4169
+ this.constraints.push({
4170
+ type: "null",
4171
+ boolean: "or",
4172
+ column,
4173
+ not: false
4174
+ });
4175
+ return this;
4176
+ }
4177
+ /**
4178
+ * Adds an `is not null` constraint joined with `and`.
4179
+ *
4180
+ * @param column The column to test for non-null.
4181
+ * @returns
4182
+ */
4183
+ whereNotNull(column) {
4184
+ this.constraints.push({
4185
+ type: "null",
4186
+ boolean: "and",
4187
+ column,
4188
+ not: true
4189
+ });
4190
+ return this;
4191
+ }
4192
+ /**
4193
+ * Adds an `is not null` constraint joined with `or`.
4194
+ *
4195
+ * @param column The column to test for non-null.
4196
+ * @returns
4197
+ */
4198
+ orWhereNotNull(column) {
4199
+ this.constraints.push({
4200
+ type: "null",
4201
+ boolean: "or",
4202
+ column,
4203
+ not: true
4204
+ });
4205
+ return this;
4206
+ }
4207
+ /**
4208
+ * Adds a raw constraint joined with `and`.
4209
+ *
4210
+ * @param sql The raw SQL fragment (with `?` placeholders for bindings).
4211
+ * @param bindings The values bound to the placeholders.
4212
+ * @returns
4213
+ */
4214
+ onRaw(sql, bindings = []) {
4215
+ this.constraints.push({
4216
+ type: "raw",
4217
+ boolean: "and",
4218
+ sql,
4219
+ bindings
4220
+ });
4221
+ return this;
4222
+ }
4223
+ /**
4224
+ * Adds a raw constraint joined with `or`.
4225
+ *
4226
+ * @param sql The raw SQL fragment (with `?` placeholders for bindings).
4227
+ * @param bindings The values bound to the placeholders.
4228
+ * @returns
4229
+ */
4230
+ orOnRaw(sql, bindings = []) {
4231
+ this.constraints.push({
4232
+ type: "raw",
4233
+ boolean: "or",
4234
+ sql,
4235
+ bindings
4236
+ });
4237
+ return this;
4238
+ }
4239
+ /**
4240
+ * Returns the accumulated constraints for this join clause.
4241
+ *
4242
+ * @returns
4243
+ */
4244
+ getConstraints() {
4245
+ return this.constraints;
4246
+ }
4247
+ addOn(boolean, first, operator, second) {
4248
+ if (typeof first === "function") {
4249
+ const nested = new JoinClause();
4250
+ first(nested);
4251
+ this.constraints.push({
4252
+ type: "nested",
4253
+ boolean,
4254
+ constraints: nested.getConstraints()
4255
+ });
4256
+ return this;
4257
+ }
4258
+ const [resolvedOperator, resolvedSecond] = second === void 0 ? ["=", operator] : [operator, second];
4259
+ if (typeof resolvedSecond !== "string") throw new Error("A join \"on\" constraint requires a second column.");
4260
+ this.constraints.push({
4261
+ type: "column",
4262
+ boolean,
4263
+ first,
4264
+ operator: resolvedOperator ?? "=",
4265
+ second: resolvedSecond
4266
+ });
4267
+ return this;
4268
+ }
4269
+ addWhere(boolean, column, operator, value) {
4270
+ const [resolvedOperator, resolvedValue] = value === void 0 ? ["=", operator] : [operator, value];
4271
+ this.constraints.push({
4272
+ type: "value",
4273
+ boolean,
4274
+ column,
4275
+ operator: resolvedOperator ?? "=",
4276
+ value: resolvedValue
4277
+ });
4278
+ return this;
4279
+ }
4280
+ };
4281
+
3802
4282
  //#endregion
3803
4283
  //#region src/Exceptions/ModelNotFoundException.ts
3804
4284
  /**
@@ -4770,7 +5250,248 @@ var QueryBuilder = class QueryBuilder {
4770
5250
  return this;
4771
5251
  }
4772
5252
  /**
4773
- * Adds a skip clause to the query for pagination.
5253
+ * Adds a join clause to the query.
5254
+ *
5255
+ * The `first`/`second` arguments are treated as raw database identifiers, so
5256
+ * qualify them as `table.column` when needed. Pass a closure as `first` to
5257
+ * build a compound `on` condition through a {@link JoinClause}.
5258
+ *
5259
+ * @param table The table (or aliased table) to join.
5260
+ * @param first The left-hand column or a closure receiving a JoinClause.
5261
+ * @param operator The comparison operator (defaults to `=`).
5262
+ * @param second The right-hand column.
5263
+ * @param type The join type (defaults to `inner`).
5264
+ * @returns
5265
+ */
5266
+ join(table, first, operator, second, type = "inner") {
5267
+ return this.addJoin(type, table, first, operator, second);
5268
+ }
5269
+ /**
5270
+ * Adds an inner join clause to the query.
5271
+ *
5272
+ * @param table The table (or aliased table) to join.
5273
+ * @param first The left-hand column or a closure receiving a JoinClause.
5274
+ * @param operator The comparison operator (defaults to `=`).
5275
+ * @param second The right-hand column.
5276
+ * @returns
5277
+ */
5278
+ innerJoin(table, first, operator, second) {
5279
+ return this.addJoin("inner", table, first, operator, second);
5280
+ }
5281
+ /**
5282
+ * Adds a left join clause to the query.
5283
+ *
5284
+ * @param table The table (or aliased table) to join.
5285
+ * @param first The left-hand column or a closure receiving a JoinClause.
5286
+ * @param operator The comparison operator (defaults to `=`).
5287
+ * @param second The right-hand column.
5288
+ * @returns
5289
+ */
5290
+ leftJoin(table, first, operator, second) {
5291
+ return this.addJoin("left", table, first, operator, second);
5292
+ }
5293
+ /**
5294
+ * Adds a right join clause to the query.
5295
+ *
5296
+ * @param table The table (or aliased table) to join.
5297
+ * @param first The left-hand column or a closure receiving a JoinClause.
5298
+ * @param operator The comparison operator (defaults to `=`).
5299
+ * @param second The right-hand column.
5300
+ * @returns
5301
+ */
5302
+ rightJoin(table, first, operator, second) {
5303
+ return this.addJoin("right", table, first, operator, second);
5304
+ }
5305
+ /**
5306
+ * Adds a cross join clause to the query.
5307
+ *
5308
+ * When a `first` column (or closure) is supplied the cross join is promoted
5309
+ * to an inner join with the given constraints, mirroring Laravel's behaviour.
5310
+ *
5311
+ * @param table The table (or aliased table) to join.
5312
+ * @param first Optional column or closure to constrain the join.
5313
+ * @returns
5314
+ */
5315
+ crossJoin(table, first) {
5316
+ if (first === void 0) return this.addJoin("cross", table);
5317
+ return this.addJoin("inner", table, first);
5318
+ }
5319
+ /**
5320
+ * Adds a join clause that compares a column to a value.
5321
+ *
5322
+ * @param table The table (or aliased table) to join.
5323
+ * @param first The column being compared.
5324
+ * @param operator The comparison operator.
5325
+ * @param value The value to compare against.
5326
+ * @param type The join type (defaults to `inner`).
5327
+ * @returns
5328
+ */
5329
+ joinWhere(table, first, operator, value, type = "inner") {
5330
+ return this.addJoinWhere(type, table, first, operator, value);
5331
+ }
5332
+ /**
5333
+ * Adds a left join clause that compares a column to a value.
5334
+ *
5335
+ * @param table The table (or aliased table) to join.
5336
+ * @param first The column being compared.
5337
+ * @param operator The comparison operator.
5338
+ * @param value The value to compare against.
5339
+ * @returns
5340
+ */
5341
+ leftJoinWhere(table, first, operator, value) {
5342
+ return this.addJoinWhere("left", table, first, operator, value);
5343
+ }
5344
+ /**
5345
+ * Adds a right join clause that compares a column to a value.
5346
+ *
5347
+ * @param table The table (or aliased table) to join.
5348
+ * @param first The column being compared.
5349
+ * @param operator The comparison operator.
5350
+ * @param value The value to compare against.
5351
+ * @returns
5352
+ */
5353
+ rightJoinWhere(table, first, operator, value) {
5354
+ return this.addJoinWhere("right", table, first, operator, value);
5355
+ }
5356
+ /**
5357
+ * Adds a subquery join clause to the query.
5358
+ *
5359
+ * @param query The subquery (a QueryBuilder instance or raw SQL string).
5360
+ * @param alias The alias assigned to the subquery.
5361
+ * @param first The left-hand column or a closure receiving a JoinClause.
5362
+ * @param operator The comparison operator (defaults to `=`).
5363
+ * @param second The right-hand column.
5364
+ * @param type The join type (defaults to `inner`).
5365
+ * @returns
5366
+ */
5367
+ joinSub(query, alias, first, operator, second, type = "inner") {
5368
+ return this.addJoinSub(type, query, alias, first, operator, second);
5369
+ }
5370
+ /**
5371
+ * Adds a subquery left join clause to the query.
5372
+ *
5373
+ * @param query The subquery (a QueryBuilder instance or raw SQL string).
5374
+ * @param alias The alias assigned to the subquery.
5375
+ * @param first The left-hand column or a closure receiving a JoinClause.
5376
+ * @param operator The comparison operator (defaults to `=`).
5377
+ * @param second The right-hand column.
5378
+ * @returns
5379
+ */
5380
+ leftJoinSub(query, alias, first, operator, second) {
5381
+ return this.addJoinSub("left", query, alias, first, operator, second);
5382
+ }
5383
+ /**
5384
+ * Adds a subquery right join clause to the query.
5385
+ *
5386
+ * @param query The subquery (a QueryBuilder instance or raw SQL string).
5387
+ * @param alias The alias assigned to the subquery.
5388
+ * @param first The left-hand column or a closure receiving a JoinClause.
5389
+ * @param operator The comparison operator (defaults to `=`).
5390
+ * @param second The right-hand column.
5391
+ * @returns
5392
+ */
5393
+ rightJoinSub(query, alias, first, operator, second) {
5394
+ return this.addJoinSub("right", query, alias, first, operator, second);
5395
+ }
5396
+ /**
5397
+ * Adds a cross subquery join clause to the query.
5398
+ *
5399
+ * @param query The subquery (a QueryBuilder instance or raw SQL string).
5400
+ * @param alias The alias assigned to the subquery.
5401
+ * @returns
5402
+ */
5403
+ crossJoinSub(query, alias) {
5404
+ return this.addJoinSub("cross", query, alias);
5405
+ }
5406
+ /**
5407
+ * Adds a lateral join clause to the query.
5408
+ *
5409
+ * @param query The subquery (a QueryBuilder instance or raw SQL string).
5410
+ * @param alias The alias assigned to the subquery.
5411
+ * @param type The join type (defaults to `inner`).
5412
+ * @returns
5413
+ */
5414
+ joinLateral(query, alias, type = "inner") {
5415
+ return this.addJoinSub(type, query, alias, void 0, void 0, void 0, true);
5416
+ }
5417
+ /**
5418
+ * Adds a lateral left join clause to the query.
5419
+ *
5420
+ * @param query The subquery (a QueryBuilder instance or raw SQL string).
5421
+ * @param alias The alias assigned to the subquery.
5422
+ * @returns
5423
+ */
5424
+ leftJoinLateral(query, alias) {
5425
+ return this.addJoinSub("left", query, alias, void 0, void 0, void 0, true);
5426
+ }
5427
+ /**
5428
+ * Builds a self-contained select specification used when this query is joined
5429
+ * as a subquery by another query builder.
5430
+ *
5431
+ * @returns
5432
+ */
5433
+ buildJoinSubquerySpec() {
5434
+ const spec = this.tryBuildSelectSpec(this.buildWhere());
5435
+ if (!spec) throw new UnsupportedAdapterFeatureException("Subquery join could not be compiled into an Arkorm select specification.", {
5436
+ operation: "query.joinSub",
5437
+ model: this.model.name
5438
+ });
5439
+ return spec;
5440
+ }
5441
+ guardJoinSupport() {
5442
+ if (!this.adapter?.capabilities?.joins) throw new UnsupportedAdapterFeatureException("Join clauses are not supported by the current adapter.", {
5443
+ operation: "join",
5444
+ model: this.model.name,
5445
+ meta: { feature: "joins" }
5446
+ });
5447
+ }
5448
+ pushJoin(join) {
5449
+ (this.queryJoins ??= []).push(join);
5450
+ }
5451
+ resolveJoinConstraints(first, operator, second) {
5452
+ if (first === void 0) return [];
5453
+ const clause = new JoinClause();
5454
+ if (typeof first === "function") first(clause);
5455
+ else clause.on(first, operator, second);
5456
+ return clause.getConstraints();
5457
+ }
5458
+ resolveJoinSource(query) {
5459
+ if (typeof query === "string") return { subquerySql: query };
5460
+ return { subquery: query.buildJoinSubquerySpec() };
5461
+ }
5462
+ addJoin(type, table, first, operator, second) {
5463
+ this.guardJoinSupport();
5464
+ this.pushJoin({
5465
+ type,
5466
+ table,
5467
+ constraints: this.resolveJoinConstraints(first, operator, second)
5468
+ });
5469
+ return this;
5470
+ }
5471
+ addJoinWhere(type, table, column, operator, value) {
5472
+ this.guardJoinSupport();
5473
+ const clause = new JoinClause();
5474
+ clause.where(column, operator, value);
5475
+ this.pushJoin({
5476
+ type,
5477
+ table,
5478
+ constraints: clause.getConstraints()
5479
+ });
5480
+ return this;
5481
+ }
5482
+ addJoinSub(type, query, alias, first, operator, second, lateral = false) {
5483
+ this.guardJoinSupport();
5484
+ this.pushJoin({
5485
+ type,
5486
+ alias,
5487
+ ...this.resolveJoinSource(query),
5488
+ ...lateral ? { lateral: true } : {},
5489
+ constraints: this.resolveJoinConstraints(first, operator, second)
5490
+ });
5491
+ return this;
5492
+ }
5493
+ /**
5494
+ * Adds a skip clause to the query for pagination.
4774
5495
  * This will overwrite any existing skip clause.
4775
5496
  *
4776
5497
  * @param skip
@@ -4913,6 +5634,56 @@ var QueryBuilder = class QueryBuilder {
4913
5634
  if (!model) throw new ModelNotFoundException(this.model.name, "Record not found.");
4914
5635
  return model;
4915
5636
  }
5637
+ /**
5638
+ * Returns the first record matching the given attributes or instantiates a
5639
+ * new, unpersisted model populated with the merged attributes and values.
5640
+ *
5641
+ * @param attributes
5642
+ * @param values
5643
+ * @returns
5644
+ */
5645
+ async firstOrNew(attributes, values = {}) {
5646
+ const existing = await this.clone().where(attributes).first();
5647
+ if (existing) return existing;
5648
+ const ModelConstructor = this.model;
5649
+ return new ModelConstructor({
5650
+ ...attributes,
5651
+ ...values
5652
+ });
5653
+ }
5654
+ /**
5655
+ * Returns the first record matching the given attributes or creates and
5656
+ * persists a new record populated with the merged attributes and values.
5657
+ *
5658
+ * @param attributes
5659
+ * @param values
5660
+ * @returns
5661
+ */
5662
+ async firstOrCreate(attributes, values = {}) {
5663
+ const existing = await this.clone().where(attributes).first();
5664
+ if (existing) return existing;
5665
+ return await this.create({
5666
+ ...attributes,
5667
+ ...values
5668
+ });
5669
+ }
5670
+ async firstOr(columnsOrCallback, maybeCallback) {
5671
+ const callback = typeof columnsOrCallback === "function" ? columnsOrCallback : maybeCallback;
5672
+ if (!callback) throw new QueryConstraintException("firstOr requires a fallback callback.", {
5673
+ operation: "firstOr",
5674
+ model: this.model.name
5675
+ });
5676
+ if (Array.isArray(columnsOrCallback) && columnsOrCallback.length > 0) {
5677
+ const select = columnsOrCallback.reduce((all, column) => {
5678
+ all[column] = true;
5679
+ return all;
5680
+ }, {});
5681
+ this.select(select);
5682
+ }
5683
+ const found = await this.first();
5684
+ if (found) return found;
5685
+ return callback();
5686
+ }
4916
5687
  async find(value, key) {
4917
5688
  const resolvedKey = key ?? this.model.getPrimaryKey();
4918
5689
  return this.where({ [resolvedKey]: value }).first();
@@ -5122,6 +5893,23 @@ var QueryBuilder = class QueryBuilder {
5122
5893
  }
5123
5894
  return await this.clone().where(attributes).update(resolvedValues) != null;
5124
5895
  }
5896
+ /**
5897
+ * Update the first record matching the given attributes, or create a new
5898
+ * record populated with the merged attributes and values when none exists.
5899
+ *
5900
+ * @param attributes
5901
+ * @param values
5902
+ * @returns
5903
+ */
5904
+ async updateOrCreate(attributes, values = {}) {
5905
+ const existing = await this.clone().where(attributes).first();
5906
+ if (!existing) return await this.create({
5907
+ ...attributes,
5908
+ ...values
5909
+ });
5910
+ if (Object.keys(values).length === 0) return existing;
5911
+ return await this.clone().where(attributes).update(values);
5912
+ }
5125
5913
  shouldFallbackUpdateOrInsertUpsert(error) {
5126
5914
  if (!(error instanceof QueryExecutionException)) return false;
5127
5915
  const cause = error.cause;
@@ -5173,13 +5961,25 @@ var QueryBuilder = class QueryBuilder {
5173
5961
  if (!this.isUniqueWhere(where) && directSpec && typeof adapter.deleteFirst === "function") {
5174
5962
  const deleted = await adapter.deleteFirst(directSpec);
5175
5963
  if (!deleted) return null;
5176
- return this.model.hydrate(deleted);
5964
+ return this.hydrateDeleted(deleted);
5177
5965
  }
5178
5966
  const uniqueWhere = await this.resolveUniqueWhere(where, false);
5179
5967
  if (!uniqueWhere) return null;
5180
5968
  const deleted = await this.executeDeleteRow(uniqueWhere, false);
5181
5969
  if (!deleted) return null;
5182
- return this.model.hydrate(deleted);
5970
+ return this.hydrateDeleted(deleted);
5971
+ }
5972
+ /**
5973
+ * Hydrate a row that was just deleted, marking the resulting model as no
5974
+ * longer existing in the database.
5975
+ *
5976
+ * @param attributes
5977
+ * @returns
5978
+ */
5979
+ hydrateDeleted(attributes) {
5980
+ const model = this.model.hydrate(attributes);
5981
+ model.exists = false;
5982
+ return model;
5183
5983
  }
5184
5984
  /**
5185
5985
  * Deletes the first record matching the current query constraints and throws
@@ -6025,6 +6825,7 @@ var QueryBuilder = class QueryBuilder {
6025
6825
  columns,
6026
6826
  distinct: this.queryDistinct || void 0,
6027
6827
  groupBy: this.queryGroupBy ? [...this.queryGroupBy] : void 0,
6828
+ joins: this.queryJoins ? [...this.queryJoins] : void 0,
6028
6829
  where: condition,
6029
6830
  orderBy,
6030
6831
  limit: this.limitValue,
@@ -6041,6 +6842,7 @@ var QueryBuilder = class QueryBuilder {
6041
6842
  if (this.hasRelationFilters() && this.canExecuteRelationFiltersInAdapter() && relationFilters === null) return null;
6042
6843
  return {
6043
6844
  target: this.buildQueryTarget(),
6845
+ joins: this.queryJoins ? [...this.queryJoins] : void 0,
6044
6846
  where: condition,
6045
6847
  relationFilters: this.canExecuteRelationFiltersInAdapter() ? relationFilters ?? void 0 : void 0,
6046
6848
  aggregate: { type: "count" }
@@ -6546,9 +7348,12 @@ var Model = class Model {
6546
7348
  this.hidden = [];
6547
7349
  this.visible = [];
6548
7350
  this.appends = [];
7351
+ this.exists = false;
7352
+ this.wasRecentlyCreated = false;
6549
7353
  this.attributes = {};
6550
7354
  this.original = {};
6551
7355
  this.changes = {};
7356
+ this.previous = {};
6552
7357
  this.touchedAttributes = /* @__PURE__ */ new Set();
6553
7358
  this.fill(attributes);
6554
7359
  return new Proxy(this, {
@@ -6908,6 +7713,68 @@ var Model = class Model {
6908
7713
  return this.query().scope(name, ...args);
6909
7714
  }
6910
7715
  /**
7716
+ * Start a query constrained by the given where clause.
7717
+ *
7718
+ * @param this
7719
+ * @param where
7720
+ * @returns
7721
+ */
7722
+ static where(where) {
7723
+ return this.query().where(where);
7724
+ }
7725
+ /**
7726
+ * Retrieve all records for the model.
7727
+ *
7728
+ * @param this
7729
+ * @returns
7730
+ */
7731
+ static async all() {
7732
+ return await this.query().get();
7733
+ }
7734
+ /**
7735
+ * Create and persist a new record, returning the hydrated model instance.
7736
+ *
7737
+ * @param this
7738
+ * @param data
7739
+ * @returns
7740
+ */
7741
+ static async create(data) {
7742
+ return await this.query().create(data);
7743
+ }
7744
+ /**
7745
+ * Insert new records or update existing records by one or more unique keys.
7746
+ *
7747
+ * @param this
7748
+ * @param values
7749
+ * @param uniqueBy
7750
+ * @param update
7751
+ * @returns
7752
+ */
7753
+ static async upsert(values, uniqueBy, update = null) {
7754
+ return await this.query().upsert(values, uniqueBy, update);
7755
+ }
7756
+ /**
7757
+ * Delete records by their primary key(s), dispatching model events for each
7758
+ * matched record. Returns the number of records deleted.
7759
+ *
7760
+ * @param this
7761
+ * @param ids
7762
+ * @returns
7763
+ */
7764
+ static async destroy(ids) {
7765
+ const constructor = this;
7766
+ const identifiers = (Array.isArray(ids) ? ids : [ids]).filter((identifier, index, all) => all.indexOf(identifier) === index);
7767
+ const primaryKey = constructor.getPrimaryKey();
7768
+ let deleted = 0;
7769
+ for (const identifier of identifiers) {
7770
+ const model = await constructor.query().where({ [primaryKey]: identifier }).first();
7771
+ if (!model) continue;
7772
+ await model.delete();
7773
+ deleted += 1;
7774
+ }
7775
+ return deleted;
7776
+ }
7777
+ /**
6911
7778
  * Get the soft delete configuration for the model, including whether
6912
7779
  * soft deletes are enabled and the name of the deleted at column.
6913
7780
  *
@@ -6930,6 +7797,7 @@ var Model = class Model {
6930
7797
  const model = new this(attributes);
6931
7798
  model.syncOriginal();
6932
7799
  model.syncChanges({});
7800
+ model.exists = true;
6933
7801
  return model;
6934
7802
  }
6935
7803
  /**
@@ -6940,7 +7808,8 @@ var Model = class Model {
6940
7808
  * @returns
6941
7809
  */
6942
7810
  static hydrateMany(attributes) {
6943
- return attributes.map((attribute) => new this(attribute));
7811
+ const constructor = this;
7812
+ return attributes.map((attribute) => constructor.hydrate(attribute));
6944
7813
  }
6945
7814
  /**
6946
7815
  * Hydrate a model instance and dispatch the retrieved lifecycle event.
@@ -6988,6 +7857,11 @@ var Model = class Model {
6988
7857
  return false;
6989
7858
  }
6990
7859
  }
7860
+ async updateOrFail(attributes) {
7861
+ const primaryKey = this.constructor.getPrimaryKey();
7862
+ if (this.getAttribute(primaryKey) == null) throw new ArkormException(primaryKey === "id" ? "Cannot update a model without an id." : `Cannot update a model without a [${primaryKey}] value.`);
7863
+ return await this.fill(attributes).saveOrFail();
7864
+ }
6991
7865
  getAttribute(key) {
6992
7866
  const attributeMutator = this.resolveAttributeMutator(key);
6993
7867
  const mutator = this.resolveGetMutator(key);
@@ -7011,29 +7885,35 @@ var Model = class Model {
7011
7885
  return this;
7012
7886
  }
7013
7887
  /**
7014
- * Save the model to the database.
7015
- * If the model has an identifier (id), it will perform an update.
7016
- * Otherwise, it will perform a create.
7017
- *
7018
- * @returns
7888
+ * Save the model to the database.
7889
+ * If the model already exists in the database it performs an update;
7890
+ * otherwise it performs an insert. Existence is tracked through the
7891
+ * `exists` flag rather than the presence of a primary-key value, so a model
7892
+ * built with an explicit primary key still inserts on its first save.
7893
+ *
7894
+ * @returns
7019
7895
  */
7020
7896
  async save() {
7021
7897
  const constructor = this.constructor;
7022
7898
  const primaryKey = constructor.getPrimaryKey();
7023
- const identifier = this.getAttribute(primaryKey);
7024
7899
  const previousOriginal = this.getOriginal();
7025
- if (identifier == null) {
7900
+ if (!this.exists) {
7026
7901
  await Model.dispatchEvent(constructor, "saving", this);
7027
7902
  await Model.dispatchEvent(constructor, "creating", this);
7028
7903
  const payload = this.normalizePersistenceAttributes(this.getRawAttributes());
7029
7904
  const model = await constructor.query().create(payload);
7030
7905
  this.fill(model.getRawAttributes());
7031
7906
  this.syncChanges(previousOriginal);
7907
+ this.syncPrevious(previousOriginal);
7032
7908
  this.syncOriginal();
7909
+ this.exists = true;
7910
+ this.wasRecentlyCreated = true;
7033
7911
  await Model.dispatchEvent(constructor, "created", this);
7034
7912
  await Model.dispatchEvent(constructor, "saved", this);
7035
7913
  return this;
7036
7914
  }
7915
+ const identifier = this.getAttribute(primaryKey);
7916
+ if (identifier == null) throw new ArkormException(primaryKey === "id" ? "Cannot update an existing model without an id." : `Cannot update an existing model without a [${primaryKey}] value.`);
7037
7917
  await Model.dispatchEvent(constructor, "saving", this);
7038
7918
  await Model.dispatchEvent(constructor, "updating", this);
7039
7919
  const payload = this.normalizePersistenceAttributes(this.getDirtyAttributes());
@@ -7041,6 +7921,7 @@ var Model = class Model {
7041
7921
  const model = await constructor.query().where({ [primaryKey]: identifier }).update(payload);
7042
7922
  this.fill(model.getRawAttributes());
7043
7923
  this.syncChanges(previousOriginal);
7924
+ this.syncPrevious(previousOriginal);
7044
7925
  this.syncOriginal();
7045
7926
  await Model.dispatchEvent(constructor, "updated", this);
7046
7927
  await Model.dispatchEvent(constructor, "saved", this);
@@ -7055,6 +7936,15 @@ var Model = class Model {
7055
7936
  return await Model.withoutEvents(() => this.save());
7056
7937
  }
7057
7938
  /**
7939
+ * Save the model within a transaction, rolling back and rethrowing if the
7940
+ * operation fails. Unlike update(), this never swallows errors.
7941
+ *
7942
+ * @returns
7943
+ */
7944
+ async saveOrFail() {
7945
+ return await this.constructor.transaction(async () => await this.save());
7946
+ }
7947
+ /**
7058
7948
  * Delete the model from the database.
7059
7949
  * If soft deletes are enabled, it will perform a soft delete by
7060
7950
  * setting the deleted at column to the current date.
@@ -7082,6 +7972,7 @@ var Model = class Model {
7082
7972
  this.fill(deleted.getRawAttributes());
7083
7973
  this.syncChanges(previousOriginal);
7084
7974
  this.syncOriginal();
7975
+ this.exists = false;
7085
7976
  await Model.dispatchEvent(constructor, "deleted", this);
7086
7977
  return this;
7087
7978
  }
@@ -7094,6 +7985,15 @@ var Model = class Model {
7094
7985
  return await Model.withoutEvents(() => this.delete());
7095
7986
  }
7096
7987
  /**
7988
+ * Delete the model within a transaction, rolling back and rethrowing if the
7989
+ * operation fails.
7990
+ *
7991
+ * @returns
7992
+ */
7993
+ async deleteOrFail() {
7994
+ return await this.constructor.transaction(async () => await this.delete());
7995
+ }
7996
+ /**
7097
7997
  * Permanently delete the model from the database, regardless of whether soft
7098
7998
  * deletes are enabled.
7099
7999
  *
@@ -7111,6 +8011,7 @@ var Model = class Model {
7111
8011
  this.fill(deleted.getRawAttributes());
7112
8012
  this.syncChanges(previousOriginal);
7113
8013
  this.syncOriginal();
8014
+ this.exists = false;
7114
8015
  await Model.dispatchEvent(constructor, "deleted", this);
7115
8016
  await Model.dispatchEvent(constructor, "forceDeleted", this);
7116
8017
  return this;
@@ -7265,6 +8166,25 @@ var Model = class Model {
7265
8166
  return keyList.some((key) => Object.prototype.hasOwnProperty.call(this.changes, key));
7266
8167
  }
7267
8168
  /**
8169
+ * Get the attributes that were changed during the last successful
8170
+ * persistence operation.
8171
+ *
8172
+ * @returns
8173
+ */
8174
+ getChanges() {
8175
+ return Object.entries(this.changes).reduce((all, [key, value]) => {
8176
+ all[key] = Model.cloneAttributeValue(value);
8177
+ return all;
8178
+ }, {});
8179
+ }
8180
+ getPrevious(key) {
8181
+ if (typeof key === "string") return Model.cloneAttributeValue(this.previous[key]);
8182
+ return Object.entries(this.previous).reduce((all, [previousKey, value]) => {
8183
+ all[previousKey] = Model.cloneAttributeValue(value);
8184
+ return all;
8185
+ }, {});
8186
+ }
8187
+ /**
7268
8188
  * Convert the model instance to a plain object, applying visibility
7269
8189
  * rules, appends, and mutators.
7270
8190
  *
@@ -7681,6 +8601,18 @@ var Model = class Model {
7681
8601
  }, {});
7682
8602
  }
7683
8603
  /**
8604
+ * Capture the attribute snapshot that was persisted before the most recent
8605
+ * save so it can be read back via getPrevious().
8606
+ *
8607
+ * @param previousOriginal
8608
+ */
8609
+ syncPrevious(previousOriginal) {
8610
+ this.previous = Object.entries(previousOriginal).reduce((all, [key, value]) => {
8611
+ all[key] = Model.cloneAttributeValue(value);
8612
+ return all;
8613
+ }, {});
8614
+ }
8615
+ /**
7684
8616
  * Resolve lifecycle state for the provided model class.
7685
8617
  *
7686
8618
  * @param modelClass
@@ -8354,4 +9286,4 @@ var PivotModel = class extends Model {
8354
9286
  };
8355
9287
 
8356
9288
  //#endregion
8357
- export { Arkorm, ArkormCollection, ArkormException, Arkormx, Attribute, CliApp, DB, EnumBuilder, ForeignKeyBuilder, InitCommand, InlineFactory, KyselyDatabaseAdapter, LengthAwarePaginator, MIGRATION_BRAND, MakeFactoryCommand, MakeMigrationCommand, MakeModelCommand, MakeSeederCommand, MigrateCommand, MigrateFreshCommand, MigrateRollbackCommand, Migration, MigrationHistoryCommand, MissingDelegateException, Model, ModelFactory, ModelNotFoundException, ModelsSyncCommand, PRISMA_ENUM_MEMBER_REGEX, PRISMA_ENUM_REGEX, PRISMA_MODEL_REGEX, Paginator, PivotModel, PrimaryKeyGenerationPlanner, PrismaDatabaseAdapter, QueryBuilder, QueryConstraintException, QueryExecutionException, RelationResolutionException, RuntimeModuleLoader, SEEDER_BRAND, SchemaBuilder, ScopeNotDefinedException, SeedCommand, Seeder, TableBuilder, URLDriver, UniqueConstraintResolutionException, UnsupportedAdapterFeatureException, applyAlterTableOperation, applyCreateTableOperation, applyDropTableOperation, applyMigrationRollbackToDatabase, applyMigrationRollbackToPrismaSchema, applyMigrationToDatabase, applyMigrationToPrismaSchema, applyOperationsToPersistedColumnMappingsState, applyOperationsToPrismaSchema, awaitConfiguredModelsRegistration, bindAdapterToModels, buildEnumBlock, buildFieldLine, buildIndexLine, buildInverseRelationLine, buildMigrationIdentity, buildMigrationRunId, buildMigrationSource, buildModelBlock, buildPrimaryKeyLine, buildRelationLine, buildUniqueConstraintLine, computeMigrationChecksum, configureArkormRuntime, createEmptyAppliedMigrationsState, createEmptyPersistedColumnMappingsState, createKyselyAdapter, createMigrationTimestamp, createPrismaAdapter, createPrismaCompatibilityAdapter, createPrismaDatabaseAdapter, createPrismaDelegateMap, defineConfig, defineFactory, deleteAppliedMigrationsStateFromStore, deletePersistedColumnMappingsState, deriveCollectionFieldName, deriveInverseRelationAlias, deriveRelationAlias, deriveRelationFieldName, deriveSingularFieldName, emitRuntimeDebugEvent, ensureArkormConfigLoading, escapeRegex, findAppliedMigration, findEnumBlock, findModelBlock, formatDefaultValue, formatEnumDefaultValue, formatRelationAction, generateMigrationFile, getActiveTransactionAdapter, getActiveTransactionClient, getDefaultStubsPath, getLastMigrationRun, getLatestAppliedMigrations, getMigrationPlan, getPersistedColumnMap, getPersistedEnumMap, getPersistedEnumTsType, getPersistedPrimaryKeyGeneration, getPersistedTableMetadata, getPersistedTimestampColumns, getRegisteredFactories, getRegisteredMigrations, getRegisteredModels, getRegisteredPaths, getRegisteredSeeders, getRuntimeAdapter, getRuntimeClient, getRuntimeCompatibilityAdapter, getRuntimeDebugHandler, getRuntimePaginationCurrentPageResolver, getRuntimePaginationURLDriverFactory, getRuntimePrismaClient, getUserConfig, inferDelegateName, isDelegateLike, isMigrationApplied, isQuerySchemaLike, isTransactionCapableClient, loadArkormConfig, loadFactoriesFrom, loadMigrationsFrom, loadModelsFrom, loadSeedersFrom, markMigrationApplied, markMigrationRun, pad, readAppliedMigrationsState, readAppliedMigrationsStateFromStore, readPersistedColumnMappingsState, rebuildPersistedColumnMappingsState, registerFactories, registerMigrations, registerModels, registerPaths, registerSeeders, removeAppliedMigration, resetArkormRuntimeForTests, resetPersistedColumnMappingsCache, resetRuntimeRegistryForTests, resolveCast, resolveColumnMappingsFilePath, resolveEnumName, resolveMigrationClassName, resolveMigrationStateFilePath, resolvePersistedMetadataFeatures, resolvePrismaType, resolveRuntimeCompatibilityQuerySchema, resolveRuntimeCompatibilityQuerySchemaOrThrow, runArkormTransaction, runMigrationWithPrisma, runPrismaCommand, stripPrismaSchemaModelsAndEnums, supportsDatabaseCreation, supportsDatabaseMigrationExecution, supportsDatabaseMigrationState, supportsDatabaseReset, syncPersistedColumnMappingsFromState, toMigrationFileSlug, toModelName, validatePersistedMetadataFeaturesForMigrations, writeAppliedMigrationsState, writeAppliedMigrationsStateToStore, writePersistedColumnMappingsState };
9289
+ export { Arkorm, ArkormCollection, ArkormException, Arkormx, Attribute, CliApp, DB, EnumBuilder, ForeignKeyBuilder, InitCommand, InlineFactory, JoinClause, KyselyDatabaseAdapter, LengthAwarePaginator, MIGRATION_BRAND, MakeFactoryCommand, MakeMigrationCommand, MakeModelCommand, MakeSeederCommand, MigrateCommand, MigrateFreshCommand, MigrateRollbackCommand, Migration, MigrationHistoryCommand, MissingDelegateException, Model, ModelFactory, ModelNotFoundException, ModelsSyncCommand, PRISMA_ENUM_MEMBER_REGEX, PRISMA_ENUM_REGEX, PRISMA_MODEL_REGEX, Paginator, PivotModel, PrimaryKeyGenerationPlanner, PrismaDatabaseAdapter, QueryBuilder, QueryConstraintException, QueryExecutionException, RelationResolutionException, RuntimeModuleLoader, SEEDER_BRAND, SchemaBuilder, ScopeNotDefinedException, SeedCommand, Seeder, TableBuilder, URLDriver, UniqueConstraintResolutionException, UnsupportedAdapterFeatureException, applyAlterTableOperation, applyCreateTableOperation, applyDropTableOperation, applyMigrationRollbackToDatabase, applyMigrationRollbackToPrismaSchema, applyMigrationToDatabase, applyMigrationToPrismaSchema, applyOperationsToPersistedColumnMappingsState, applyOperationsToPrismaSchema, awaitConfiguredModelsRegistration, bindAdapterToModels, buildEnumBlock, buildFieldLine, buildIndexLine, buildInverseRelationLine, buildMigrationIdentity, buildMigrationRunId, buildMigrationSource, buildModelBlock, buildPrimaryKeyLine, buildRelationLine, buildUniqueConstraintLine, computeMigrationChecksum, configureArkormRuntime, createEmptyAppliedMigrationsState, createEmptyPersistedColumnMappingsState, createKyselyAdapter, createMigrationTimestamp, createPrismaAdapter, createPrismaCompatibilityAdapter, createPrismaDatabaseAdapter, createPrismaDelegateMap, defineConfig, defineFactory, deleteAppliedMigrationsStateFromStore, deletePersistedColumnMappingsState, deriveCollectionFieldName, deriveInverseRelationAlias, deriveRelationAlias, deriveRelationFieldName, deriveSingularFieldName, emitRuntimeDebugEvent, ensureArkormConfigLoading, escapeRegex, findAppliedMigration, findEnumBlock, findModelBlock, formatDefaultValue, formatEnumDefaultValue, formatRelationAction, generateMigrationFile, getActiveTransactionAdapter, getActiveTransactionClient, getDefaultStubsPath, getLastMigrationRun, getLatestAppliedMigrations, getMigrationPlan, getPersistedColumnMap, getPersistedEnumMap, getPersistedEnumTsType, getPersistedPrimaryKeyGeneration, getPersistedTableMetadata, getPersistedTimestampColumns, getRegisteredFactories, getRegisteredMigrations, getRegisteredModels, getRegisteredPaths, getRegisteredSeeders, getRuntimeAdapter, getRuntimeClient, getRuntimeCompatibilityAdapter, getRuntimeDebugHandler, getRuntimePaginationCurrentPageResolver, getRuntimePaginationURLDriverFactory, getRuntimePrismaClient, getUserConfig, inferDelegateName, isDelegateLike, isMigrationApplied, isQuerySchemaLike, isTransactionCapableClient, loadArkormConfig, loadFactoriesFrom, loadMigrationsFrom, loadModelsFrom, loadSeedersFrom, markMigrationApplied, markMigrationRun, pad, readAppliedMigrationsState, readAppliedMigrationsStateFromStore, readPersistedColumnMappingsState, rebuildPersistedColumnMappingsState, registerFactories, registerMigrations, registerModels, registerPaths, registerSeeders, removeAppliedMigration, resetArkormRuntimeForTests, resetPersistedColumnMappingsCache, resetRuntimeRegistryForTests, resolveCast, resolveColumnMappingsFilePath, resolveEnumName, resolveMigrationClassName, resolveMigrationStateFilePath, resolvePersistedMetadataFeatures, resolvePrismaType, resolveRuntimeCompatibilityQuerySchema, resolveRuntimeCompatibilityQuerySchemaOrThrow, runArkormTransaction, runMigrationWithPrisma, runPrismaCommand, stripPrismaSchemaModelsAndEnums, supportsDatabaseCreation, supportsDatabaseMigrationExecution, supportsDatabaseMigrationState, supportsDatabaseReset, syncPersistedColumnMappingsFromState, toMigrationFileSlug, toModelName, validatePersistedMetadataFeaturesForMigrations, writeAppliedMigrationsState, writeAppliedMigrationsStateToStore, writePersistedColumnMappingsState };