arkormx 0.2.11 → 1.0.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
@@ -5,6 +5,7 @@ import { str } from "@h3ravel/support";
5
5
  import path, { dirname as dirname$1, extname as extname$1, join as join$1, relative } from "path";
6
6
  import { copyFileSync, existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
7
7
  import { fileURLToPath, pathToFileURL } from "url";
8
+ import { AsyncLocalStorage } from "async_hooks";
8
9
  import { createRequire } from "module";
9
10
  import { Logger } from "@h3ravel/shared";
10
11
  import { Command } from "@h3ravel/musket";
@@ -93,17 +94,43 @@ function resolveCast(definition) {
93
94
 
94
95
  //#endregion
95
96
  //#region src/Exceptions/ArkormException.ts
96
- /**
97
- * The ArkormException class is a custom error type for handling
98
- * exceptions specific to the Arkormˣ.
99
- *
100
- * @author Legacy (3m1n3nc3)
101
- * @since 0.1.0
102
- */
103
97
  var ArkormException = class extends Error {
104
- constructor(message) {
105
- super(message);
98
+ code;
99
+ operation;
100
+ model;
101
+ delegate;
102
+ relation;
103
+ scope;
104
+ meta;
105
+ constructor(message, context = {}) {
106
+ super(message, context.cause === void 0 ? void 0 : { cause: context.cause });
106
107
  this.name = "ArkormException";
108
+ this.code = context.code;
109
+ this.operation = context.operation;
110
+ this.model = context.model;
111
+ this.delegate = context.delegate;
112
+ this.relation = context.relation;
113
+ this.scope = context.scope;
114
+ this.meta = context.meta;
115
+ }
116
+ getContext() {
117
+ return {
118
+ code: this.code,
119
+ operation: this.operation,
120
+ model: this.model,
121
+ delegate: this.delegate,
122
+ relation: this.relation,
123
+ scope: this.scope,
124
+ meta: this.meta,
125
+ cause: this.cause
126
+ };
127
+ }
128
+ toJSON() {
129
+ return {
130
+ name: this.name,
131
+ message: this.message,
132
+ ...this.getContext()
133
+ };
107
134
  }
108
135
  };
109
136
 
@@ -1213,6 +1240,18 @@ const runMigrationWithPrisma = async (migration, options = {}) => {
1213
1240
  };
1214
1241
  };
1215
1242
 
1243
+ //#endregion
1244
+ //#region src/Exceptions/UnsupportedAdapterFeatureException.ts
1245
+ var UnsupportedAdapterFeatureException = class extends ArkormException {
1246
+ constructor(message, context = {}) {
1247
+ super(message, {
1248
+ code: "UNSUPPORTED_ADAPTER_FEATURE",
1249
+ ...context
1250
+ });
1251
+ this.name = "UnsupportedAdapterFeatureException";
1252
+ }
1253
+ };
1254
+
1216
1255
  //#endregion
1217
1256
  //#region src/helpers/runtime-config.ts
1218
1257
  const resolveDefaultStubsPath = () => {
@@ -1247,6 +1286,7 @@ let runtimeConfigLoadingPromise;
1247
1286
  let runtimeClientResolver;
1248
1287
  let runtimePaginationURLDriverFactory;
1249
1288
  let runtimePaginationCurrentPageResolver;
1289
+ const transactionClientStorage = new AsyncLocalStorage();
1250
1290
  const mergePathConfig = (paths) => {
1251
1291
  const defaults = baseConfig.paths ?? {};
1252
1292
  const current = userConfig.paths ?? {};
@@ -1414,9 +1454,36 @@ const getDefaultStubsPath = () => {
1414
1454
  * @returns
1415
1455
  */
1416
1456
  const getRuntimePrismaClient = () => {
1457
+ const activeTransactionClient = transactionClientStorage.getStore();
1458
+ if (activeTransactionClient) return activeTransactionClient;
1417
1459
  if (!runtimeConfigLoaded) loadRuntimeConfigSync();
1418
1460
  return resolveClient(runtimeClientResolver);
1419
1461
  };
1462
+ const getActiveTransactionClient = () => {
1463
+ return transactionClientStorage.getStore();
1464
+ };
1465
+ const isTransactionCapableClient = (value) => {
1466
+ if (!value || typeof value !== "object") return false;
1467
+ return typeof value.$transaction === "function";
1468
+ };
1469
+ const runArkormTransaction = async (callback, options = {}) => {
1470
+ const activeTransactionClient = transactionClientStorage.getStore();
1471
+ if (activeTransactionClient) return await callback(activeTransactionClient);
1472
+ const client = getRuntimePrismaClient();
1473
+ if (!client) throw new ArkormException("Cannot start a transaction without a configured Prisma client.", {
1474
+ code: "CLIENT_NOT_CONFIGURED",
1475
+ operation: "transaction"
1476
+ });
1477
+ if (!isTransactionCapableClient(client)) throw new UnsupportedAdapterFeatureException("Transactions are not supported by the current adapter.", {
1478
+ code: "TRANSACTION_NOT_SUPPORTED",
1479
+ operation: "transaction"
1480
+ });
1481
+ return await client.$transaction(async (transactionClient) => {
1482
+ return await transactionClientStorage.run(transactionClient, async () => {
1483
+ return await callback(transactionClient);
1484
+ });
1485
+ }, options);
1486
+ };
1420
1487
  /**
1421
1488
  * Get the configured pagination URL driver factory from runtime config.
1422
1489
  *
@@ -2772,6 +2839,18 @@ const defineFactory = (model, definition) => {
2772
2839
  return new InlineFactory(model, definition);
2773
2840
  };
2774
2841
 
2842
+ //#endregion
2843
+ //#region src/Exceptions/MissingDelegateException.ts
2844
+ var MissingDelegateException = class extends ArkormException {
2845
+ constructor(message, context = {}) {
2846
+ super(message, {
2847
+ code: "MISSING_DELEGATE",
2848
+ ...context
2849
+ });
2850
+ this.name = "MissingDelegateException";
2851
+ }
2852
+ };
2853
+
2775
2854
  //#endregion
2776
2855
  //#region src/Exceptions/ModelNotFoundException.ts
2777
2856
  /**
@@ -2783,8 +2862,12 @@ const defineFactory = (model, definition) => {
2783
2862
  */
2784
2863
  var ModelNotFoundException = class extends ArkormException {
2785
2864
  modelName;
2786
- constructor(modelName, message = "No query results for the given model.") {
2787
- super(message);
2865
+ constructor(modelName, message = "No query results for the given model.", context = {}) {
2866
+ super(message, {
2867
+ code: "MODEL_NOT_FOUND",
2868
+ model: modelName,
2869
+ ...context
2870
+ });
2788
2871
  this.name = "ModelNotFoundException";
2789
2872
  this.modelName = modelName;
2790
2873
  }
@@ -2793,6 +2876,54 @@ var ModelNotFoundException = class extends ArkormException {
2793
2876
  }
2794
2877
  };
2795
2878
 
2879
+ //#endregion
2880
+ //#region src/Exceptions/QueryConstraintException.ts
2881
+ var QueryConstraintException = class extends ArkormException {
2882
+ constructor(message, context = {}) {
2883
+ super(message, {
2884
+ code: "QUERY_CONSTRAINT",
2885
+ ...context
2886
+ });
2887
+ this.name = "QueryConstraintException";
2888
+ }
2889
+ };
2890
+
2891
+ //#endregion
2892
+ //#region src/Exceptions/RelationResolutionException.ts
2893
+ var RelationResolutionException = class extends ArkormException {
2894
+ constructor(message, context = {}) {
2895
+ super(message, {
2896
+ code: "RELATION_RESOLUTION_FAILED",
2897
+ ...context
2898
+ });
2899
+ this.name = "RelationResolutionException";
2900
+ }
2901
+ };
2902
+
2903
+ //#endregion
2904
+ //#region src/Exceptions/ScopeNotDefinedException.ts
2905
+ var ScopeNotDefinedException = class extends ArkormException {
2906
+ constructor(message, context = {}) {
2907
+ super(message, {
2908
+ code: "SCOPE_NOT_DEFINED",
2909
+ ...context
2910
+ });
2911
+ this.name = "ScopeNotDefinedException";
2912
+ }
2913
+ };
2914
+
2915
+ //#endregion
2916
+ //#region src/Exceptions/UniqueConstraintResolutionException.ts
2917
+ var UniqueConstraintResolutionException = class extends ArkormException {
2918
+ constructor(message, context = {}) {
2919
+ super(message, {
2920
+ code: "UNIQUE_CONSTRAINT_RESOLUTION_FAILED",
2921
+ ...context
2922
+ });
2923
+ this.name = "UniqueConstraintResolutionException";
2924
+ }
2925
+ };
2926
+
2796
2927
  //#endregion
2797
2928
  //#region src/helpers/prisma.ts
2798
2929
  /**
@@ -3033,6 +3164,7 @@ var BelongsToManyRelation = class extends Relation {
3033
3164
  async getResults() {
3034
3165
  const parentValue = this.parent.getAttribute(this.parentKey);
3035
3166
  const ids = (await this.related.getDelegate(this.throughDelegate).findMany({ where: { [this.foreignPivotKey]: parentValue } })).map((row) => row[this.relatedPivotKey]);
3167
+ if (ids.length === 0) return new ArkormCollection([]);
3036
3168
  return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } })).get();
3037
3169
  }
3038
3170
  };
@@ -3119,6 +3251,7 @@ var HasManyThroughRelation = class extends Relation {
3119
3251
  async getResults() {
3120
3252
  const localValue = this.parent.getAttribute(this.localKey);
3121
3253
  const keys = (await this.related.getDelegate(this.throughDelegate).findMany({ where: { [this.firstKey]: localValue } })).map((row) => row[this.secondLocalKey]);
3254
+ if (keys.length === 0) return new ArkormCollection([]);
3122
3255
  return this.applyConstraint(this.related.query().where({ [this.secondKey]: { in: keys } })).get();
3123
3256
  }
3124
3257
  };
@@ -3276,6 +3409,7 @@ var MorphToManyRelation = class extends Relation {
3276
3409
  [`${this.morphName}Id`]: parentValue,
3277
3410
  [`${this.morphName}Type`]: morphType
3278
3411
  } })).map((row) => row[this.relatedPivotKey]);
3412
+ if (ids.length === 0) return new ArkormCollection([]);
3279
3413
  return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } })).get();
3280
3414
  }
3281
3415
  };
@@ -4027,7 +4161,11 @@ var QueryBuilder = class QueryBuilder {
4027
4161
  scope(name, ...args) {
4028
4162
  const methodName = `scope${name.charAt(0).toUpperCase()}${name.slice(1)}`;
4029
4163
  const scope = this.model.prototype?.[methodName];
4030
- if (typeof scope !== "function") throw new ArkormException(`Scope [${name}] is not defined.`);
4164
+ if (typeof scope !== "function") throw new ScopeNotDefinedException(`Scope [${name}] is not defined.`, {
4165
+ operation: "scope",
4166
+ model: this.model.name,
4167
+ scope: name
4168
+ });
4031
4169
  const scoped = scope.call(void 0, this, ...args);
4032
4170
  if (scoped && scoped !== this) return scoped;
4033
4171
  return this;
@@ -4149,7 +4287,7 @@ var QueryBuilder = class QueryBuilder {
4149
4287
  const relationCache = /* @__PURE__ */ new WeakMap();
4150
4288
  const rows = await this.delegate.findMany(this.buildFindArgs());
4151
4289
  const normalizedRows = this.randomOrderEnabled ? this.shuffleRows(rows) : rows;
4152
- const models = this.model.hydrateMany(normalizedRows);
4290
+ const models = await this.model.hydrateManyRetrieved(normalizedRows);
4153
4291
  let filteredModels = models;
4154
4292
  if (this.hasRelationFilters()) if (this.hasOrRelationFilters() && this.args.where) {
4155
4293
  const baseIds = new Set(models.map((model) => this.getModelId(model)).filter((id) => id != null));
@@ -4179,13 +4317,13 @@ var QueryBuilder = class QueryBuilder {
4179
4317
  if (rows.length === 0) return null;
4180
4318
  const row = this.shuffleRows(rows)[0];
4181
4319
  if (!row) return null;
4182
- const model = this.model.hydrate(row);
4320
+ const model = await this.model.hydrateRetrieved(row);
4183
4321
  await model.load(this.eagerLoads);
4184
4322
  return model;
4185
4323
  }
4186
4324
  const row = await this.delegate.findFirst(this.buildFindArgs());
4187
4325
  if (!row) return null;
4188
- const model = this.model.hydrate(row);
4326
+ const model = await this.model.hydrateRetrieved(row);
4189
4327
  await model.load(this.eagerLoads);
4190
4328
  return model;
4191
4329
  }
@@ -4205,7 +4343,10 @@ var QueryBuilder = class QueryBuilder {
4205
4343
  async findOr(value, keyOrCallback, maybeCallback) {
4206
4344
  const key = typeof keyOrCallback === "string" ? keyOrCallback : "id";
4207
4345
  const callback = typeof keyOrCallback === "function" ? keyOrCallback : maybeCallback;
4208
- if (!callback) throw new ArkormException("findOr requires a fallback callback.");
4346
+ if (!callback) throw new QueryConstraintException("findOr requires a fallback callback.", {
4347
+ operation: "findOr",
4348
+ model: this.model.name
4349
+ });
4209
4350
  const found = await this.find(value, key);
4210
4351
  if (found) return found;
4211
4352
  return callback();
@@ -4319,7 +4460,11 @@ var QueryBuilder = class QueryBuilder {
4319
4460
  async insertGetId(values, sequence) {
4320
4461
  const created = await this.delegate.create({ data: values });
4321
4462
  const key = sequence ?? "id";
4322
- if (!(key in created)) throw new ArkormException(`Inserted record does not contain key [${key}].`);
4463
+ if (!(key in created)) throw new UniqueConstraintResolutionException(`Inserted record does not contain key [${key}].`, {
4464
+ operation: "insertGetId",
4465
+ model: this.model.name,
4466
+ meta: { key }
4467
+ });
4323
4468
  return created[key];
4324
4469
  }
4325
4470
  /**
@@ -4356,7 +4501,10 @@ var QueryBuilder = class QueryBuilder {
4356
4501
  */
4357
4502
  async update(data) {
4358
4503
  const where = this.buildWhere();
4359
- if (!where) throw new ArkormException("Update requires a where clause.");
4504
+ if (!where) throw new QueryConstraintException("Update requires a where clause.", {
4505
+ operation: "update",
4506
+ model: this.model.name
4507
+ });
4360
4508
  const uniqueWhere = await this.resolveUniqueWhere(where);
4361
4509
  const updated = await this.delegate.update({
4362
4510
  where: uniqueWhere,
@@ -4372,7 +4520,10 @@ var QueryBuilder = class QueryBuilder {
4372
4520
  */
4373
4521
  async updateFrom(data) {
4374
4522
  const where = this.buildWhere();
4375
- if (!where) throw new ArkormException("Update requires a where clause.");
4523
+ if (!where) throw new QueryConstraintException("Update requires a where clause.", {
4524
+ operation: "updateFrom",
4525
+ model: this.model.name
4526
+ });
4376
4527
  const delegate = this.delegate;
4377
4528
  if (typeof delegate.updateMany === "function") {
4378
4529
  const result = await delegate.updateMany({
@@ -4437,7 +4588,10 @@ var QueryBuilder = class QueryBuilder {
4437
4588
  */
4438
4589
  async delete() {
4439
4590
  const where = this.buildWhere();
4440
- if (!where) throw new ArkormException("Delete requires a where clause.");
4591
+ if (!where) throw new QueryConstraintException("Delete requires a where clause.", {
4592
+ operation: "delete",
4593
+ model: this.model.name
4594
+ });
4441
4595
  const uniqueWhere = await this.resolveUniqueWhere(where);
4442
4596
  const deleted = await this.delegate.delete({ where: uniqueWhere });
4443
4597
  return this.model.hydrate(deleted);
@@ -4503,7 +4657,10 @@ var QueryBuilder = class QueryBuilder {
4503
4657
  if (Array.isArray(source)) return source;
4504
4658
  }
4505
4659
  if (Array.isArray(source)) return source;
4506
- throw new ArkormException("insertUsing expects a query builder, array of records, or async resolver.");
4660
+ throw new QueryConstraintException("insertUsing expects a query builder, array of records, or async resolver.", {
4661
+ operation: "insertUsing",
4662
+ model: this.model.name
4663
+ });
4507
4664
  }
4508
4665
  /**
4509
4666
  * Execute callback when no records exist.
@@ -4587,7 +4744,11 @@ var QueryBuilder = class QueryBuilder {
4587
4744
  */
4588
4745
  whereRaw(sql, bindings = []) {
4589
4746
  const delegate = this.delegate;
4590
- if (typeof delegate.applyRawWhere !== "function") throw new ArkormException("Raw where clauses are not supported by the current adapter.");
4747
+ if (typeof delegate.applyRawWhere !== "function") throw new UnsupportedAdapterFeatureException("Raw where clauses are not supported by the current adapter.", {
4748
+ operation: "whereRaw",
4749
+ model: this.model.name,
4750
+ meta: { feature: "rawWhere" }
4751
+ });
4591
4752
  this.args.where = delegate.applyRawWhere(this.buildWhere(), sql, bindings);
4592
4753
  return this;
4593
4754
  }
@@ -4600,7 +4761,11 @@ var QueryBuilder = class QueryBuilder {
4600
4761
  */
4601
4762
  orWhereRaw(sql, bindings = []) {
4602
4763
  const delegate = this.delegate;
4603
- if (typeof delegate.applyRawWhere !== "function") throw new ArkormException("Raw where clauses are not supported by the current adapter.");
4764
+ if (typeof delegate.applyRawWhere !== "function") throw new UnsupportedAdapterFeatureException("Raw where clauses are not supported by the current adapter.", {
4765
+ operation: "orWhereRaw",
4766
+ model: this.model.name,
4767
+ meta: { feature: "rawWhere" }
4768
+ });
4604
4769
  const rawWhere = delegate.applyRawWhere(void 0, sql, bindings);
4605
4770
  return this.orWhere(rawWhere);
4606
4771
  }
@@ -4722,9 +4887,16 @@ var QueryBuilder = class QueryBuilder {
4722
4887
  async resolveUniqueWhere(where) {
4723
4888
  if (this.isUniqueWhere(where)) return where;
4724
4889
  const row = await this.delegate.findFirst({ where });
4725
- if (!row) throw new ArkormException("Record not found for update/delete operation.");
4890
+ if (!row) throw new ModelNotFoundException(this.model.name, "Record not found for update/delete operation.", {
4891
+ operation: "resolveUniqueWhere",
4892
+ meta: { where }
4893
+ });
4726
4894
  const record = row;
4727
- if (!Object.prototype.hasOwnProperty.call(record, "id")) throw new ArkormException("Unable to resolve a unique identifier for update/delete operation. Include an id in the query constraints.");
4895
+ if (!Object.prototype.hasOwnProperty.call(record, "id")) throw new UniqueConstraintResolutionException("Unable to resolve a unique identifier for update/delete operation. Include an id in the query constraints.", {
4896
+ operation: "resolveUniqueWhere",
4897
+ model: this.model.name,
4898
+ meta: { where }
4899
+ });
4728
4900
  return { id: record.id };
4729
4901
  }
4730
4902
  /**
@@ -4847,7 +5019,11 @@ var QueryBuilder = class QueryBuilder {
4847
5019
  if (cached) return await cached;
4848
5020
  const resolver = (async () => {
4849
5021
  const relationMethod = model[relation];
4850
- if (typeof relationMethod !== "function") throw new ArkormException(`Relation [${relation}] is not defined on the model.`);
5022
+ if (typeof relationMethod !== "function") throw new RelationResolutionException(`Relation [${relation}] is not defined on the model.`, {
5023
+ operation: "resolveRelatedResults",
5024
+ model: this.model.name,
5025
+ relation
5026
+ });
4851
5027
  const relationInstance = relationMethod.call(model);
4852
5028
  if (callback && typeof relationInstance.constrain === "function") relationInstance.constrain((query) => {
4853
5029
  return callback(query) ?? query;
@@ -4862,7 +5038,11 @@ var QueryBuilder = class QueryBuilder {
4862
5038
  if (results instanceof ArkormCollection) return results.all();
4863
5039
  return results;
4864
5040
  }
4865
- throw new ArkormException(`Relation [${relation}] does not support result resolution.`);
5041
+ throw new RelationResolutionException(`Relation [${relation}] does not support result resolution.`, {
5042
+ operation: "resolveRelatedResults",
5043
+ model: this.model.name,
5044
+ relation
5045
+ });
4866
5046
  })();
4867
5047
  callbackMap.set(callbackCacheKey, resolver);
4868
5048
  return await resolver;
@@ -4903,6 +5083,8 @@ var QueryBuilder = class QueryBuilder {
4903
5083
  * @since 0.1.0
4904
5084
  */
4905
5085
  var Model = class Model {
5086
+ static lifecycleStates = /* @__PURE__ */ new WeakMap();
5087
+ static eventsSuppressed = 0;
4906
5088
  static factoryClass;
4907
5089
  static client;
4908
5090
  static delegate;
@@ -4910,6 +5092,7 @@ var Model = class Model {
4910
5092
  static deletedAtColumn = "deletedAt";
4911
5093
  static globalScopes = {};
4912
5094
  static eventListeners = {};
5095
+ static dispatchesEvents = {};
4913
5096
  casts = {};
4914
5097
  hidden = [];
4915
5098
  visible = [];
@@ -4947,7 +5130,11 @@ var Model = class Model {
4947
5130
  }
4948
5131
  static factory(count) {
4949
5132
  const factoryClass = this.factoryClass;
4950
- if (!factoryClass) throw new ArkormException(`Factory is not configured for model [${this.name}].`);
5133
+ if (!factoryClass) throw new ArkormException(`Factory is not configured for model [${this.name}].`, {
5134
+ code: "FACTORY_NOT_CONFIGURED",
5135
+ operation: "factory",
5136
+ model: this.name
5137
+ });
4951
5138
  const factory = new factoryClass();
4952
5139
  if (typeof count === "number") factory.count(count);
4953
5140
  return factory;
@@ -4963,6 +5150,21 @@ var Model = class Model {
4963
5150
  this.globalScopes[name] = scope;
4964
5151
  }
4965
5152
  /**
5153
+ * Execute a callback without applying global scopes for the current model class.
5154
+ *
5155
+ * @param callback
5156
+ * @returns
5157
+ */
5158
+ static async withoutGlobalScopes(callback) {
5159
+ const state = Model.getLifecycleState(this);
5160
+ state.globalScopesSuppressed += 1;
5161
+ try {
5162
+ return await callback();
5163
+ } finally {
5164
+ state.globalScopesSuppressed = Math.max(0, state.globalScopesSuppressed - 1);
5165
+ }
5166
+ }
5167
+ /**
4966
5168
  * Remove a global scope by name.
4967
5169
  *
4968
5170
  * @param name
@@ -4984,11 +5186,60 @@ var Model = class Model {
4984
5186
  * @param listener
4985
5187
  */
4986
5188
  static on(event, listener) {
5189
+ Model.ensureModelBooted(this);
4987
5190
  this.ensureOwnEventListeners();
4988
5191
  if (!this.eventListeners[event]) this.eventListeners[event] = [];
4989
5192
  this.eventListeners[event]?.push(listener);
4990
5193
  }
4991
5194
  /**
5195
+ * Register a model lifecycle callback listener.
5196
+ *
5197
+ * @param event
5198
+ * @param listener
5199
+ */
5200
+ static event(event, listener) {
5201
+ this.on(event, listener);
5202
+ }
5203
+ static retrieved(listener) {
5204
+ this.event("retrieved", listener);
5205
+ }
5206
+ static saving(listener) {
5207
+ this.event("saving", listener);
5208
+ }
5209
+ static saved(listener) {
5210
+ this.event("saved", listener);
5211
+ }
5212
+ static creating(listener) {
5213
+ this.event("creating", listener);
5214
+ }
5215
+ static created(listener) {
5216
+ this.event("created", listener);
5217
+ }
5218
+ static updating(listener) {
5219
+ this.event("updating", listener);
5220
+ }
5221
+ static updated(listener) {
5222
+ this.event("updated", listener);
5223
+ }
5224
+ static deleting(listener) {
5225
+ this.event("deleting", listener);
5226
+ }
5227
+ static deleted(listener) {
5228
+ this.event("deleted", listener);
5229
+ }
5230
+ static restoring(listener) {
5231
+ this.event("restoring", listener);
5232
+ }
5233
+ static restored(listener) {
5234
+ this.event("restored", listener);
5235
+ }
5236
+ static forceDeleting(listener) {
5237
+ this.event("forceDeleting", listener);
5238
+ }
5239
+ static forceDeleted(listener) {
5240
+ this.event("forceDeleted", listener);
5241
+ }
5242
+ /**
4992
5243
  * Remove listeners for an event. If listener is omitted, all listeners for that event are removed.
4993
5244
  *
4994
5245
  * @param event
@@ -5009,6 +5260,34 @@ var Model = class Model {
5009
5260
  this.eventListeners = {};
5010
5261
  }
5011
5262
  /**
5263
+ * Execute a callback while suppressing lifecycle events for all models.
5264
+ *
5265
+ * @param callback
5266
+ * @returns
5267
+ */
5268
+ static async withoutEvents(callback) {
5269
+ Model.eventsSuppressed += 1;
5270
+ try {
5271
+ return await callback();
5272
+ } finally {
5273
+ Model.eventsSuppressed = Math.max(0, Model.eventsSuppressed - 1);
5274
+ }
5275
+ }
5276
+ /**
5277
+ * Execute a callback within a transaction scope.
5278
+ * Nested calls reuse the active transaction client.
5279
+ *
5280
+ * @param callback
5281
+ * @param options
5282
+ * @returns
5283
+ */
5284
+ static async transaction(callback, options = {}) {
5285
+ ensureArkormConfigLoading();
5286
+ return await runArkormTransaction(async (client) => {
5287
+ return await callback(client);
5288
+ }, options);
5289
+ }
5290
+ /**
5012
5291
  * Get the Prisma delegate for the model.
5013
5292
  * If a delegate name is provided, it will attempt to resolve that delegate.
5014
5293
  * Otherwise, it will attempt to resolve a delegate based on the model's name or
@@ -5026,9 +5305,20 @@ var Model = class Model {
5026
5305
  `${str(key).singular()}`,
5027
5306
  `${str(key).camel().singular()}`
5028
5307
  ];
5308
+ const activeTransactionClient = getActiveTransactionClient();
5029
5309
  const runtimeClient = getRuntimePrismaClient();
5030
- const resolved = candidates.map((name) => this.client?.[name] ?? runtimeClient?.[name]).find((candidate) => isDelegateLike(candidate));
5031
- if (!resolved) throw new ArkormException(`Database delegate [${key}] is not configured.`);
5310
+ const sources = activeTransactionClient ? [
5311
+ activeTransactionClient,
5312
+ this.client,
5313
+ runtimeClient
5314
+ ] : [this.client, runtimeClient];
5315
+ const resolved = candidates.flatMap((name) => sources.map((source) => source?.[name])).find((candidate) => isDelegateLike(candidate));
5316
+ if (!resolved) throw new MissingDelegateException(`Database delegate [${key}] is not configured.`, {
5317
+ operation: "getDelegate",
5318
+ model: this.name,
5319
+ delegate: key,
5320
+ meta: { candidates }
5321
+ });
5032
5322
  return resolved;
5033
5323
  }
5034
5324
  /**
@@ -5038,16 +5328,27 @@ var Model = class Model {
5038
5328
  * @returns
5039
5329
  */
5040
5330
  static query() {
5331
+ Model.ensureModelBooted(this);
5041
5332
  let builder = new QueryBuilder(this.getDelegate(), this);
5042
5333
  const modelClass = this;
5043
- modelClass.ensureOwnGlobalScopes();
5044
- Object.values(modelClass.globalScopes).forEach((scope) => {
5045
- const scoped = scope(builder);
5046
- if (scoped && scoped !== builder) builder = scoped;
5047
- });
5334
+ if (!Model.areGlobalScopesSuppressed(modelClass)) {
5335
+ modelClass.ensureOwnGlobalScopes();
5336
+ Object.values(modelClass.globalScopes).forEach((scope) => {
5337
+ const scoped = scope(builder);
5338
+ if (scoped && scoped !== builder) builder = scoped;
5339
+ });
5340
+ }
5048
5341
  return builder;
5049
5342
  }
5050
5343
  /**
5344
+ * Boot hook for subclasses to register scopes or perform one-time setup.
5345
+ */
5346
+ static boot() {}
5347
+ /**
5348
+ * Booted hook for subclasses to register callbacks after boot logic runs.
5349
+ */
5350
+ static booted() {}
5351
+ /**
5051
5352
  * Get a query builder instance that includes soft-deleted records.
5052
5353
  *
5053
5354
  * @param this
@@ -5110,6 +5411,36 @@ var Model = class Model {
5110
5411
  static hydrateMany(attributes) {
5111
5412
  return attributes.map((attribute) => new this(attribute));
5112
5413
  }
5414
+ /**
5415
+ * Hydrate a model instance and dispatch the retrieved lifecycle event.
5416
+ *
5417
+ * @param this
5418
+ * @param attributes
5419
+ * @returns
5420
+ */
5421
+ static async hydrateRetrieved(attributes) {
5422
+ Model.ensureModelBooted(this);
5423
+ if (!Model.hasEventListeners(this, "retrieved")) return this.hydrate(attributes);
5424
+ const model = this.hydrate(attributes);
5425
+ await Model.dispatchEvent(this, "retrieved", model);
5426
+ return model;
5427
+ }
5428
+ /**
5429
+ * Hydrate multiple model instances and dispatch the retrieved lifecycle event for each.
5430
+ *
5431
+ * @param this
5432
+ * @param attributes
5433
+ * @returns
5434
+ */
5435
+ static async hydrateManyRetrieved(attributes) {
5436
+ Model.ensureModelBooted(this);
5437
+ if (!Model.hasEventListeners(this, "retrieved")) return this.hydrateMany(attributes);
5438
+ const models = this.hydrateMany(attributes);
5439
+ await Promise.all(models.map(async (model) => {
5440
+ await Model.dispatchEvent(this, "retrieved", model);
5441
+ }));
5442
+ return models;
5443
+ }
5113
5444
  fill(attributes) {
5114
5445
  Object.entries(attributes).forEach(([key, value]) => {
5115
5446
  this.setAttribute(key, value);
@@ -5166,6 +5497,14 @@ var Model = class Model {
5166
5497
  return this;
5167
5498
  }
5168
5499
  /**
5500
+ * Save the model without dispatching lifecycle events.
5501
+ *
5502
+ * @returns
5503
+ */
5504
+ async saveQuietly() {
5505
+ return await Model.withoutEvents(() => this.save());
5506
+ }
5507
+ /**
5169
5508
  * Delete the model from the database.
5170
5509
  * If soft deletes are enabled, it will perform a soft delete by
5171
5510
  * setting the deleted at column to the current date.
@@ -5191,6 +5530,14 @@ var Model = class Model {
5191
5530
  return this;
5192
5531
  }
5193
5532
  /**
5533
+ * Delete the model without dispatching lifecycle events.
5534
+ *
5535
+ * @returns
5536
+ */
5537
+ async deleteQuietly() {
5538
+ return await Model.withoutEvents(() => this.delete());
5539
+ }
5540
+ /**
5194
5541
  * Permanently delete the model from the database, regardless of whether soft
5195
5542
  * deletes are enabled.
5196
5543
  *
@@ -5209,6 +5556,14 @@ var Model = class Model {
5209
5556
  return this;
5210
5557
  }
5211
5558
  /**
5559
+ * Force delete the model without dispatching lifecycle events.
5560
+ *
5561
+ * @returns
5562
+ */
5563
+ async forceDeleteQuietly() {
5564
+ return await Model.withoutEvents(() => this.forceDelete());
5565
+ }
5566
+ /**
5212
5567
  * Restore a soft-deleted model by setting the deleted at column to null.
5213
5568
  *
5214
5569
  * @returns
@@ -5226,6 +5581,14 @@ var Model = class Model {
5226
5581
  return this;
5227
5582
  }
5228
5583
  /**
5584
+ * Restore the model without dispatching lifecycle events.
5585
+ *
5586
+ * @returns
5587
+ */
5588
+ async restoreQuietly() {
5589
+ return await Model.withoutEvents(() => this.restore());
5590
+ }
5591
+ /**
5229
5592
  * Load related models onto the current model instance.
5230
5593
  *
5231
5594
  * @param relations
@@ -5278,6 +5641,47 @@ var Model = class Model {
5278
5641
  return this.toObject();
5279
5642
  }
5280
5643
  /**
5644
+ * Determine if another model represents the same persisted record.
5645
+ *
5646
+ * @param model
5647
+ * @returns
5648
+ */
5649
+ is(model) {
5650
+ if (!(model instanceof Model)) return false;
5651
+ if (this.constructor !== model.constructor) return false;
5652
+ const identifier = this.getAttribute("id");
5653
+ const otherIdentifier = model.getAttribute("id");
5654
+ if (identifier == null || otherIdentifier == null) return false;
5655
+ return identifier === otherIdentifier;
5656
+ }
5657
+ /**
5658
+ * Determine if another model does not represent the same persisted record.
5659
+ *
5660
+ * @param model
5661
+ * @returns
5662
+ */
5663
+ isNot(model) {
5664
+ return !this.is(model);
5665
+ }
5666
+ /**
5667
+ * Determine if another model is the same in-memory instance.
5668
+ *
5669
+ * @param model
5670
+ * @returns
5671
+ */
5672
+ isSame(model) {
5673
+ return this === model;
5674
+ }
5675
+ /**
5676
+ * Determine if another model is not the same in-memory instance.
5677
+ *
5678
+ * @param model
5679
+ * @returns
5680
+ */
5681
+ isNotSame(model) {
5682
+ return !this.isSame(model);
5683
+ }
5684
+ /**
5281
5685
  * Define a has one relationship.
5282
5686
  *
5283
5687
  * @param related
@@ -5441,6 +5845,84 @@ var Model = class Model {
5441
5845
  if (!Object.prototype.hasOwnProperty.call(this, "eventListeners")) this.eventListeners = { ...this.eventListeners || {} };
5442
5846
  }
5443
5847
  /**
5848
+ * Resolve lifecycle state for the provided model class.
5849
+ *
5850
+ * @param modelClass
5851
+ * @returns
5852
+ */
5853
+ static getLifecycleState(modelClass) {
5854
+ const existing = Model.lifecycleStates.get(modelClass);
5855
+ if (existing) return existing;
5856
+ const state = {
5857
+ booted: false,
5858
+ booting: false,
5859
+ globalScopesSuppressed: 0
5860
+ };
5861
+ Model.lifecycleStates.set(modelClass, state);
5862
+ return state;
5863
+ }
5864
+ /**
5865
+ * Ensure the target model class has executed its boot lifecycle.
5866
+ *
5867
+ * @param modelClass
5868
+ */
5869
+ static ensureModelBooted(modelClass) {
5870
+ const state = Model.getLifecycleState(modelClass);
5871
+ if (state.booted || state.booting) return;
5872
+ state.booting = true;
5873
+ try {
5874
+ const boot = modelClass.boot;
5875
+ if (boot !== Model.boot) boot.call(modelClass);
5876
+ const booted = modelClass.booted;
5877
+ if (booted !== Model.booted) booted.call(modelClass);
5878
+ state.booted = true;
5879
+ } finally {
5880
+ state.booting = false;
5881
+ }
5882
+ }
5883
+ /**
5884
+ * Determine if global scopes are currently suppressed for the model class.
5885
+ *
5886
+ * @param modelClass
5887
+ * @returns
5888
+ */
5889
+ static areGlobalScopesSuppressed(modelClass) {
5890
+ return Model.getLifecycleState(modelClass).globalScopesSuppressed > 0;
5891
+ }
5892
+ /**
5893
+ * Resolve configured class-based event handlers for a lifecycle event.
5894
+ *
5895
+ * @param modelClass
5896
+ * @param event
5897
+ * @returns
5898
+ */
5899
+ static resolveDispatchedEventListeners(modelClass, event) {
5900
+ const configured = modelClass.dispatchesEvents[event];
5901
+ if (!configured) return [];
5902
+ return (Array.isArray(configured) ? configured : [configured]).map((entry) => {
5903
+ const handler = typeof entry === "function" ? new entry() : entry;
5904
+ if (!handler || typeof handler.handle !== "function") throw new ArkormException(`Invalid event handler configured for [${modelClass.name}.${event}].`);
5905
+ return async (model) => {
5906
+ await handler.handle(model);
5907
+ };
5908
+ });
5909
+ }
5910
+ /**
5911
+ * Determine whether a lifecycle event has any registered listeners.
5912
+ *
5913
+ * @param modelClass
5914
+ * @param event
5915
+ * @returns
5916
+ */
5917
+ static hasEventListeners(modelClass, event) {
5918
+ if (Model.eventsSuppressed > 0) return false;
5919
+ modelClass.ensureOwnEventListeners();
5920
+ if ((modelClass.eventListeners[event] || []).length > 0) return true;
5921
+ const configuredDispatchers = modelClass.dispatchesEvents[event];
5922
+ if (!configuredDispatchers) return false;
5923
+ return Array.isArray(configuredDispatchers) ? configuredDispatchers.length > 0 : true;
5924
+ }
5925
+ /**
5444
5926
  * Dispatches lifecycle events to registered listeners.
5445
5927
  *
5446
5928
  * @param modelClass
@@ -5448,8 +5930,9 @@ var Model = class Model {
5448
5930
  * @param model
5449
5931
  */
5450
5932
  static async dispatchEvent(modelClass, event, model) {
5451
- modelClass.ensureOwnEventListeners();
5452
- const listeners = modelClass.eventListeners[event] || [];
5933
+ Model.ensureModelBooted(modelClass);
5934
+ if (!Model.hasEventListeners(modelClass, event)) return;
5935
+ const listeners = [...Model.resolveDispatchedEventListeners(modelClass, event), ...modelClass.eventListeners[event] || []];
5453
5936
  for (const listener of listeners) await listener(model);
5454
5937
  }
5455
5938
  /**
@@ -5469,4 +5952,4 @@ var Model = class Model {
5469
5952
  };
5470
5953
 
5471
5954
  //#endregion
5472
- export { ArkormCollection, ArkormException, Attribute, CliApp, ForeignKeyBuilder, InitCommand, InlineFactory, LengthAwarePaginator, MIGRATION_BRAND, MakeFactoryCommand, MakeMigrationCommand, MakeModelCommand, MakeSeederCommand, MigrateCommand, MigrateRollbackCommand, Migration, MigrationHistoryCommand, Model, ModelFactory, ModelNotFoundException, ModelsSyncCommand, PRISMA_MODEL_REGEX, Paginator, QueryBuilder, SEEDER_BRAND, SchemaBuilder, SeedCommand, Seeder, TableBuilder, URLDriver, applyAlterTableOperation, applyCreateTableOperation, applyDropTableOperation, applyMigrationRollbackToPrismaSchema, applyMigrationToPrismaSchema, applyOperationsToPrismaSchema, buildFieldLine, buildIndexLine, buildInverseRelationLine, buildMigrationIdentity, buildMigrationRunId, buildMigrationSource, buildModelBlock, buildRelationLine, computeMigrationChecksum, configureArkormRuntime, createMigrationTimestamp, createPrismaAdapter, createPrismaDelegateMap, defineConfig, defineFactory, deriveCollectionFieldName, deriveInverseRelationAlias, deriveRelationFieldName, ensureArkormConfigLoading, escapeRegex, findAppliedMigration, findModelBlock, formatDefaultValue, formatRelationAction, generateMigrationFile, getDefaultStubsPath, getLastMigrationRun, getLatestAppliedMigrations, getMigrationPlan, getRuntimePaginationCurrentPageResolver, getRuntimePaginationURLDriverFactory, getRuntimePrismaClient, getUserConfig, inferDelegateName, isDelegateLike, isMigrationApplied, loadArkormConfig, markMigrationApplied, markMigrationRun, pad, readAppliedMigrationsState, removeAppliedMigration, resetArkormRuntimeForTests, resolveCast, resolveMigrationClassName, resolveMigrationStateFilePath, resolvePrismaType, runMigrationWithPrisma, runPrismaCommand, toMigrationFileSlug, toModelName, writeAppliedMigrationsState };
5955
+ export { ArkormCollection, ArkormException, Attribute, CliApp, ForeignKeyBuilder, InitCommand, InlineFactory, LengthAwarePaginator, MIGRATION_BRAND, MakeFactoryCommand, MakeMigrationCommand, MakeModelCommand, MakeSeederCommand, MigrateCommand, MigrateRollbackCommand, Migration, MigrationHistoryCommand, MissingDelegateException, Model, ModelFactory, ModelNotFoundException, ModelsSyncCommand, PRISMA_MODEL_REGEX, Paginator, QueryBuilder, QueryConstraintException, RelationResolutionException, SEEDER_BRAND, SchemaBuilder, ScopeNotDefinedException, SeedCommand, Seeder, TableBuilder, URLDriver, UniqueConstraintResolutionException, UnsupportedAdapterFeatureException, applyAlterTableOperation, applyCreateTableOperation, applyDropTableOperation, applyMigrationRollbackToPrismaSchema, applyMigrationToPrismaSchema, applyOperationsToPrismaSchema, buildFieldLine, buildIndexLine, buildInverseRelationLine, buildMigrationIdentity, buildMigrationRunId, buildMigrationSource, buildModelBlock, buildRelationLine, computeMigrationChecksum, configureArkormRuntime, createMigrationTimestamp, createPrismaAdapter, createPrismaDelegateMap, defineConfig, defineFactory, deriveCollectionFieldName, deriveInverseRelationAlias, deriveRelationFieldName, ensureArkormConfigLoading, escapeRegex, findAppliedMigration, findModelBlock, formatDefaultValue, formatRelationAction, generateMigrationFile, getActiveTransactionClient, getDefaultStubsPath, getLastMigrationRun, getLatestAppliedMigrations, getMigrationPlan, getRuntimePaginationCurrentPageResolver, getRuntimePaginationURLDriverFactory, getRuntimePrismaClient, getUserConfig, inferDelegateName, isDelegateLike, isMigrationApplied, isTransactionCapableClient, loadArkormConfig, markMigrationApplied, markMigrationRun, pad, readAppliedMigrationsState, removeAppliedMigration, resetArkormRuntimeForTests, resolveCast, resolveMigrationClassName, resolveMigrationStateFilePath, resolvePrismaType, runArkormTransaction, runMigrationWithPrisma, runPrismaCommand, toMigrationFileSlug, toModelName, writeAppliedMigrationsState };