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