arkormx 1.0.0 → 1.1.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
@@ -5127,8 +5127,14 @@ var Model = class Model {
5127
5127
  visible = [];
5128
5128
  appends = [];
5129
5129
  attributes;
5130
+ original;
5131
+ changes;
5132
+ touchedAttributes;
5130
5133
  constructor(attributes = {}) {
5131
5134
  this.attributes = {};
5135
+ this.original = {};
5136
+ this.changes = {};
5137
+ this.touchedAttributes = /* @__PURE__ */ new Set();
5132
5138
  this.fill(attributes);
5133
5139
  return new Proxy(this, {
5134
5140
  get: (target, key, receiver) => {
@@ -5428,7 +5434,10 @@ var Model = class Model {
5428
5434
  * @returns
5429
5435
  */
5430
5436
  static hydrate(attributes) {
5431
- return new this(attributes);
5437
+ const model = new this(attributes);
5438
+ model.syncOriginal();
5439
+ model.syncChanges({});
5440
+ return model;
5432
5441
  }
5433
5442
  /**
5434
5443
  * Hydrate multiple model instances from an array of plain objects of attributes.
@@ -5495,6 +5504,7 @@ var Model = class Model {
5495
5504
  else if (mutator) resolved = mutator.call(this, resolved);
5496
5505
  if (cast) resolved = resolveCast(cast).set(resolved);
5497
5506
  this.attributes[key] = resolved;
5507
+ this.touchedAttributes.add(key);
5498
5508
  return this;
5499
5509
  }
5500
5510
  /**
@@ -5507,12 +5517,15 @@ var Model = class Model {
5507
5517
  async save() {
5508
5518
  const identifier = this.getAttribute("id");
5509
5519
  const payload = this.getRawAttributes();
5520
+ const previousOriginal = this.getOriginal();
5510
5521
  const constructor = this.constructor;
5511
5522
  if (identifier == null) {
5512
5523
  await Model.dispatchEvent(constructor, "saving", this);
5513
5524
  await Model.dispatchEvent(constructor, "creating", this);
5514
5525
  const model = await constructor.query().create(payload);
5515
5526
  this.fill(model.getRawAttributes());
5527
+ this.syncChanges(previousOriginal);
5528
+ this.syncOriginal();
5516
5529
  await Model.dispatchEvent(constructor, "created", this);
5517
5530
  await Model.dispatchEvent(constructor, "saved", this);
5518
5531
  return this;
@@ -5521,6 +5534,8 @@ var Model = class Model {
5521
5534
  await Model.dispatchEvent(constructor, "updating", this);
5522
5535
  const model = await constructor.query().where({ id: identifier }).update(payload);
5523
5536
  this.fill(model.getRawAttributes());
5537
+ this.syncChanges(previousOriginal);
5538
+ this.syncOriginal();
5524
5539
  await Model.dispatchEvent(constructor, "updated", this);
5525
5540
  await Model.dispatchEvent(constructor, "saved", this);
5526
5541
  return this;
@@ -5544,17 +5559,22 @@ var Model = class Model {
5544
5559
  async delete() {
5545
5560
  const identifier = this.getAttribute("id");
5546
5561
  if (identifier == null) throw new ArkormException("Cannot delete a model without an id.");
5562
+ const previousOriginal = this.getOriginal();
5547
5563
  const constructor = this.constructor;
5548
5564
  await Model.dispatchEvent(constructor, "deleting", this);
5549
5565
  const softDeleteConfig = constructor.getSoftDeleteConfig();
5550
5566
  if (softDeleteConfig.enabled) {
5551
5567
  const model = await constructor.query().where({ id: identifier }).update({ [softDeleteConfig.column]: /* @__PURE__ */ new Date() });
5552
5568
  this.fill(model.getRawAttributes());
5569
+ this.syncChanges(previousOriginal);
5570
+ this.syncOriginal();
5553
5571
  await Model.dispatchEvent(constructor, "deleted", this);
5554
5572
  return this;
5555
5573
  }
5556
5574
  const deleted = await constructor.query().where({ id: identifier }).delete();
5557
5575
  this.fill(deleted.getRawAttributes());
5576
+ this.syncChanges(previousOriginal);
5577
+ this.syncOriginal();
5558
5578
  await Model.dispatchEvent(constructor, "deleted", this);
5559
5579
  return this;
5560
5580
  }
@@ -5575,11 +5595,14 @@ var Model = class Model {
5575
5595
  async forceDelete() {
5576
5596
  const identifier = this.getAttribute("id");
5577
5597
  if (identifier == null) throw new ArkormException("Cannot force delete a model without an id.");
5598
+ const previousOriginal = this.getOriginal();
5578
5599
  const constructor = this.constructor;
5579
5600
  await Model.dispatchEvent(constructor, "forceDeleting", this);
5580
5601
  await Model.dispatchEvent(constructor, "deleting", this);
5581
5602
  const deleted = await constructor.query().withTrashed().where({ id: identifier }).delete();
5582
5603
  this.fill(deleted.getRawAttributes());
5604
+ this.syncChanges(previousOriginal);
5605
+ this.syncOriginal();
5583
5606
  await Model.dispatchEvent(constructor, "deleted", this);
5584
5607
  await Model.dispatchEvent(constructor, "forceDeleted", this);
5585
5608
  return this;
@@ -5603,9 +5626,12 @@ var Model = class Model {
5603
5626
  const constructor = this.constructor;
5604
5627
  const softDeleteConfig = constructor.getSoftDeleteConfig();
5605
5628
  if (!softDeleteConfig.enabled) return this;
5629
+ const previousOriginal = this.getOriginal();
5606
5630
  await Model.dispatchEvent(constructor, "restoring", this);
5607
5631
  const model = await constructor.query().withTrashed().where({ id: identifier }).update({ [softDeleteConfig.column]: null });
5608
5632
  this.fill(model.getRawAttributes());
5633
+ this.syncChanges(previousOriginal);
5634
+ this.syncOriginal();
5609
5635
  await Model.dispatchEvent(constructor, "restored", this);
5610
5636
  return this;
5611
5637
  }
@@ -5643,6 +5669,42 @@ var Model = class Model {
5643
5669
  getRawAttributes() {
5644
5670
  return { ...this.attributes };
5645
5671
  }
5672
+ getOriginal(key) {
5673
+ if (typeof key === "string") return Model.cloneAttributeValue(this.original[key]);
5674
+ return Object.entries(this.original).reduce((all, [originalKey, value]) => {
5675
+ all[originalKey] = Model.cloneAttributeValue(value);
5676
+ return all;
5677
+ }, {});
5678
+ }
5679
+ /**
5680
+ * Determine whether the model has unsaved attribute changes.
5681
+ *
5682
+ * @param keys
5683
+ * @returns
5684
+ */
5685
+ isDirty(keys) {
5686
+ return Object.keys(this.getDirtyAttributes(keys)).length > 0;
5687
+ }
5688
+ /**
5689
+ * Determine whether the model has no unsaved attribute changes.
5690
+ *
5691
+ * @param keys
5692
+ * @returns
5693
+ */
5694
+ isClean(keys) {
5695
+ return !this.isDirty(keys);
5696
+ }
5697
+ /**
5698
+ * Determine whether the model changed during the last successful persistence operation.
5699
+ *
5700
+ * @param keys
5701
+ * @returns
5702
+ */
5703
+ wasChanged(keys) {
5704
+ const keyList = this.normalizeAttributeKeys(keys);
5705
+ if (keyList.length === 0) return Object.keys(this.changes).length > 0;
5706
+ return keyList.some((key) => Object.prototype.hasOwnProperty.call(this.changes, key));
5707
+ }
5646
5708
  /**
5647
5709
  * Convert the model instance to a plain object, applying visibility
5648
5710
  * rules, appends, and mutators.
@@ -5833,6 +5895,34 @@ var Model = class Model {
5833
5895
  return typeof method === "function" ? method : null;
5834
5896
  }
5835
5897
  /**
5898
+ * Build a map of dirty attributes, optionally limited to specific keys.
5899
+ *
5900
+ * @param keys
5901
+ * @returns
5902
+ */
5903
+ getDirtyAttributes(keys) {
5904
+ const requestedKeys = this.normalizeAttributeKeys(keys);
5905
+ return (requestedKeys.length > 0 ? requestedKeys : Array.from(new Set([...Object.keys(this.original), ...this.touchedAttributes]))).reduce((dirty, key) => {
5906
+ const currentValue = this.attributes[key];
5907
+ const originalValue = this.original[key];
5908
+ const hasCurrent = Object.prototype.hasOwnProperty.call(this.attributes, key);
5909
+ const hasOriginal = Object.prototype.hasOwnProperty.call(this.original, key);
5910
+ if (!hasCurrent && !hasOriginal) return dirty;
5911
+ if (hasCurrent !== hasOriginal || !Model.areAttributeValuesEqual(currentValue, originalValue)) dirty[key] = Model.cloneAttributeValue(currentValue);
5912
+ return dirty;
5913
+ }, {});
5914
+ }
5915
+ /**
5916
+ * Normalize a key or key list for dirty/change lookups.
5917
+ *
5918
+ * @param keys
5919
+ * @returns
5920
+ */
5921
+ normalizeAttributeKeys(keys) {
5922
+ if (typeof keys === "undefined") return [];
5923
+ return Array.isArray(keys) ? keys : [keys];
5924
+ }
5925
+ /**
5836
5926
  * Resolve an Attribute object mutator method for a given key, if it exists.
5837
5927
  *
5838
5928
  * @param key
@@ -5874,6 +5964,66 @@ var Model = class Model {
5874
5964
  if (!Object.prototype.hasOwnProperty.call(this, "eventListeners")) this.eventListeners = { ...this.eventListeners || {} };
5875
5965
  }
5876
5966
  /**
5967
+ * Clone an attribute value to keep snapshot state isolated from live mutations.
5968
+ *
5969
+ * @param value
5970
+ * @returns
5971
+ */
5972
+ static cloneAttributeValue(value) {
5973
+ if (value instanceof Date) return new Date(value.getTime());
5974
+ if (Array.isArray(value)) return value.map((item) => Model.cloneAttributeValue(item));
5975
+ if (value && typeof value === "object") return Object.entries(value).reduce((all, [key, nestedValue]) => {
5976
+ all[key] = Model.cloneAttributeValue(nestedValue);
5977
+ return all;
5978
+ }, {});
5979
+ return value;
5980
+ }
5981
+ /**
5982
+ * Compare attribute values for dirty/change detection.
5983
+ *
5984
+ * @param left
5985
+ * @param right
5986
+ * @returns
5987
+ */
5988
+ static areAttributeValuesEqual(left, right) {
5989
+ if (left === right) return true;
5990
+ if (left instanceof Date && right instanceof Date) return left.getTime() === right.getTime();
5991
+ if (Array.isArray(left) && Array.isArray(right)) {
5992
+ if (left.length !== right.length) return false;
5993
+ return left.every((value, index) => Model.areAttributeValuesEqual(value, right[index]));
5994
+ }
5995
+ if (left && right && typeof left === "object" && typeof right === "object") {
5996
+ const leftEntries = Object.entries(left);
5997
+ const rightEntries = Object.entries(right);
5998
+ if (leftEntries.length !== rightEntries.length) return false;
5999
+ return leftEntries.every(([key, value]) => {
6000
+ return Object.prototype.hasOwnProperty.call(right, key) && Model.areAttributeValuesEqual(value, right[key]);
6001
+ });
6002
+ }
6003
+ return false;
6004
+ }
6005
+ /**
6006
+ * Sync the original snapshot to the model's current raw attributes.
6007
+ */
6008
+ syncOriginal() {
6009
+ this.original = Object.entries(this.attributes).reduce((all, [key, value]) => {
6010
+ all[key] = Model.cloneAttributeValue(value);
6011
+ return all;
6012
+ }, {});
6013
+ this.touchedAttributes.clear();
6014
+ }
6015
+ /**
6016
+ * Sync the last-changed snapshot from a previous original state.
6017
+ *
6018
+ * @param previousOriginal
6019
+ */
6020
+ syncChanges(previousOriginal) {
6021
+ this.changes = Object.entries(this.getDirtyAttributes()).reduce((all, [key, value]) => {
6022
+ if (!Object.prototype.hasOwnProperty.call(previousOriginal, key) || !Model.areAttributeValuesEqual(value, previousOriginal[key])) all[key] = Model.cloneAttributeValue(value);
6023
+ return all;
6024
+ }, {});
6025
+ }
6026
+ /**
5877
6027
  * Resolve lifecycle state for the provided model class.
5878
6028
  *
5879
6029
  * @param modelClass
package/dist/index.d.cts CHANGED
@@ -594,6 +594,9 @@ declare abstract class Model<TSchema extends PrismaDelegateLike | Record<string,
594
594
  protected visible: string[];
595
595
  protected appends: string[];
596
596
  protected readonly attributes: Record<string, unknown>;
597
+ protected original: Record<string, unknown>;
598
+ protected changes: Record<string, unknown>;
599
+ protected readonly touchedAttributes: Set<string>;
597
600
  constructor(attributes?: Record<string, unknown>);
598
601
  /**
599
602
  * Set the Prisma client delegates for all models.
@@ -863,6 +866,37 @@ declare abstract class Model<TSchema extends PrismaDelegateLike | Record<string,
863
866
  * @returns
864
867
  */
865
868
  getRawAttributes(): Partial<TAttributes>;
869
+ /**
870
+ * Get the model's original persisted attributes.
871
+ *
872
+ * @returns
873
+ */
874
+ getOriginal(): Partial<TAttributes>;
875
+ /**
876
+ * @param key The attribute key to retrieve the original value for.
877
+ */
878
+ getOriginal<TKey extends keyof TAttributes & string>(key: TKey): TAttributes[TKey] | undefined;
879
+ /**
880
+ * Determine whether the model has unsaved attribute changes.
881
+ *
882
+ * @param keys
883
+ * @returns
884
+ */
885
+ isDirty(keys?: string | string[]): boolean;
886
+ /**
887
+ * Determine whether the model has no unsaved attribute changes.
888
+ *
889
+ * @param keys
890
+ * @returns
891
+ */
892
+ isClean(keys?: string | string[]): boolean;
893
+ /**
894
+ * Determine whether the model changed during the last successful persistence operation.
895
+ *
896
+ * @param keys
897
+ * @returns
898
+ */
899
+ wasChanged(keys?: string | string[]): boolean;
866
900
  /**
867
901
  * Convert the model instance to a plain object, applying visibility
868
902
  * rules, appends, and mutators.
@@ -1004,6 +1038,20 @@ declare abstract class Model<TSchema extends PrismaDelegateLike | Record<string,
1004
1038
  * @returns
1005
1039
  */
1006
1040
  private resolveGetMutator;
1041
+ /**
1042
+ * Build a map of dirty attributes, optionally limited to specific keys.
1043
+ *
1044
+ * @param keys
1045
+ * @returns
1046
+ */
1047
+ private getDirtyAttributes;
1048
+ /**
1049
+ * Normalize a key or key list for dirty/change lookups.
1050
+ *
1051
+ * @param keys
1052
+ * @returns
1053
+ */
1054
+ private normalizeAttributeKeys;
1007
1055
  /**
1008
1056
  * Resolve an Attribute object mutator method for a given key, if it exists.
1009
1057
  *
@@ -1026,6 +1074,31 @@ declare abstract class Model<TSchema extends PrismaDelegateLike | Record<string,
1026
1074
  * Ensures event listeners are own properties on subclass constructors.
1027
1075
  */
1028
1076
  private static ensureOwnEventListeners;
1077
+ /**
1078
+ * Clone an attribute value to keep snapshot state isolated from live mutations.
1079
+ *
1080
+ * @param value
1081
+ * @returns
1082
+ */
1083
+ private static cloneAttributeValue;
1084
+ /**
1085
+ * Compare attribute values for dirty/change detection.
1086
+ *
1087
+ * @param left
1088
+ * @param right
1089
+ * @returns
1090
+ */
1091
+ private static areAttributeValuesEqual;
1092
+ /**
1093
+ * Sync the original snapshot to the model's current raw attributes.
1094
+ */
1095
+ private syncOriginal;
1096
+ /**
1097
+ * Sync the last-changed snapshot from a previous original state.
1098
+ *
1099
+ * @param previousOriginal
1100
+ */
1101
+ private syncChanges;
1029
1102
  /**
1030
1103
  * Resolve lifecycle state for the provided model class.
1031
1104
  *
package/dist/index.d.mts CHANGED
@@ -594,6 +594,9 @@ declare abstract class Model<TSchema extends PrismaDelegateLike | Record<string,
594
594
  protected visible: string[];
595
595
  protected appends: string[];
596
596
  protected readonly attributes: Record<string, unknown>;
597
+ protected original: Record<string, unknown>;
598
+ protected changes: Record<string, unknown>;
599
+ protected readonly touchedAttributes: Set<string>;
597
600
  constructor(attributes?: Record<string, unknown>);
598
601
  /**
599
602
  * Set the Prisma client delegates for all models.
@@ -863,6 +866,37 @@ declare abstract class Model<TSchema extends PrismaDelegateLike | Record<string,
863
866
  * @returns
864
867
  */
865
868
  getRawAttributes(): Partial<TAttributes>;
869
+ /**
870
+ * Get the model's original persisted attributes.
871
+ *
872
+ * @returns
873
+ */
874
+ getOriginal(): Partial<TAttributes>;
875
+ /**
876
+ * @param key The attribute key to retrieve the original value for.
877
+ */
878
+ getOriginal<TKey extends keyof TAttributes & string>(key: TKey): TAttributes[TKey] | undefined;
879
+ /**
880
+ * Determine whether the model has unsaved attribute changes.
881
+ *
882
+ * @param keys
883
+ * @returns
884
+ */
885
+ isDirty(keys?: string | string[]): boolean;
886
+ /**
887
+ * Determine whether the model has no unsaved attribute changes.
888
+ *
889
+ * @param keys
890
+ * @returns
891
+ */
892
+ isClean(keys?: string | string[]): boolean;
893
+ /**
894
+ * Determine whether the model changed during the last successful persistence operation.
895
+ *
896
+ * @param keys
897
+ * @returns
898
+ */
899
+ wasChanged(keys?: string | string[]): boolean;
866
900
  /**
867
901
  * Convert the model instance to a plain object, applying visibility
868
902
  * rules, appends, and mutators.
@@ -1004,6 +1038,20 @@ declare abstract class Model<TSchema extends PrismaDelegateLike | Record<string,
1004
1038
  * @returns
1005
1039
  */
1006
1040
  private resolveGetMutator;
1041
+ /**
1042
+ * Build a map of dirty attributes, optionally limited to specific keys.
1043
+ *
1044
+ * @param keys
1045
+ * @returns
1046
+ */
1047
+ private getDirtyAttributes;
1048
+ /**
1049
+ * Normalize a key or key list for dirty/change lookups.
1050
+ *
1051
+ * @param keys
1052
+ * @returns
1053
+ */
1054
+ private normalizeAttributeKeys;
1007
1055
  /**
1008
1056
  * Resolve an Attribute object mutator method for a given key, if it exists.
1009
1057
  *
@@ -1026,6 +1074,31 @@ declare abstract class Model<TSchema extends PrismaDelegateLike | Record<string,
1026
1074
  * Ensures event listeners are own properties on subclass constructors.
1027
1075
  */
1028
1076
  private static ensureOwnEventListeners;
1077
+ /**
1078
+ * Clone an attribute value to keep snapshot state isolated from live mutations.
1079
+ *
1080
+ * @param value
1081
+ * @returns
1082
+ */
1083
+ private static cloneAttributeValue;
1084
+ /**
1085
+ * Compare attribute values for dirty/change detection.
1086
+ *
1087
+ * @param left
1088
+ * @param right
1089
+ * @returns
1090
+ */
1091
+ private static areAttributeValuesEqual;
1092
+ /**
1093
+ * Sync the original snapshot to the model's current raw attributes.
1094
+ */
1095
+ private syncOriginal;
1096
+ /**
1097
+ * Sync the last-changed snapshot from a previous original state.
1098
+ *
1099
+ * @param previousOriginal
1100
+ */
1101
+ private syncChanges;
1029
1102
  /**
1030
1103
  * Resolve lifecycle state for the provided model class.
1031
1104
  *
package/dist/index.mjs CHANGED
@@ -5098,8 +5098,14 @@ var Model = class Model {
5098
5098
  visible = [];
5099
5099
  appends = [];
5100
5100
  attributes;
5101
+ original;
5102
+ changes;
5103
+ touchedAttributes;
5101
5104
  constructor(attributes = {}) {
5102
5105
  this.attributes = {};
5106
+ this.original = {};
5107
+ this.changes = {};
5108
+ this.touchedAttributes = /* @__PURE__ */ new Set();
5103
5109
  this.fill(attributes);
5104
5110
  return new Proxy(this, {
5105
5111
  get: (target, key, receiver) => {
@@ -5399,7 +5405,10 @@ var Model = class Model {
5399
5405
  * @returns
5400
5406
  */
5401
5407
  static hydrate(attributes) {
5402
- return new this(attributes);
5408
+ const model = new this(attributes);
5409
+ model.syncOriginal();
5410
+ model.syncChanges({});
5411
+ return model;
5403
5412
  }
5404
5413
  /**
5405
5414
  * Hydrate multiple model instances from an array of plain objects of attributes.
@@ -5466,6 +5475,7 @@ var Model = class Model {
5466
5475
  else if (mutator) resolved = mutator.call(this, resolved);
5467
5476
  if (cast) resolved = resolveCast(cast).set(resolved);
5468
5477
  this.attributes[key] = resolved;
5478
+ this.touchedAttributes.add(key);
5469
5479
  return this;
5470
5480
  }
5471
5481
  /**
@@ -5478,12 +5488,15 @@ var Model = class Model {
5478
5488
  async save() {
5479
5489
  const identifier = this.getAttribute("id");
5480
5490
  const payload = this.getRawAttributes();
5491
+ const previousOriginal = this.getOriginal();
5481
5492
  const constructor = this.constructor;
5482
5493
  if (identifier == null) {
5483
5494
  await Model.dispatchEvent(constructor, "saving", this);
5484
5495
  await Model.dispatchEvent(constructor, "creating", this);
5485
5496
  const model = await constructor.query().create(payload);
5486
5497
  this.fill(model.getRawAttributes());
5498
+ this.syncChanges(previousOriginal);
5499
+ this.syncOriginal();
5487
5500
  await Model.dispatchEvent(constructor, "created", this);
5488
5501
  await Model.dispatchEvent(constructor, "saved", this);
5489
5502
  return this;
@@ -5492,6 +5505,8 @@ var Model = class Model {
5492
5505
  await Model.dispatchEvent(constructor, "updating", this);
5493
5506
  const model = await constructor.query().where({ id: identifier }).update(payload);
5494
5507
  this.fill(model.getRawAttributes());
5508
+ this.syncChanges(previousOriginal);
5509
+ this.syncOriginal();
5495
5510
  await Model.dispatchEvent(constructor, "updated", this);
5496
5511
  await Model.dispatchEvent(constructor, "saved", this);
5497
5512
  return this;
@@ -5515,17 +5530,22 @@ var Model = class Model {
5515
5530
  async delete() {
5516
5531
  const identifier = this.getAttribute("id");
5517
5532
  if (identifier == null) throw new ArkormException("Cannot delete a model without an id.");
5533
+ const previousOriginal = this.getOriginal();
5518
5534
  const constructor = this.constructor;
5519
5535
  await Model.dispatchEvent(constructor, "deleting", this);
5520
5536
  const softDeleteConfig = constructor.getSoftDeleteConfig();
5521
5537
  if (softDeleteConfig.enabled) {
5522
5538
  const model = await constructor.query().where({ id: identifier }).update({ [softDeleteConfig.column]: /* @__PURE__ */ new Date() });
5523
5539
  this.fill(model.getRawAttributes());
5540
+ this.syncChanges(previousOriginal);
5541
+ this.syncOriginal();
5524
5542
  await Model.dispatchEvent(constructor, "deleted", this);
5525
5543
  return this;
5526
5544
  }
5527
5545
  const deleted = await constructor.query().where({ id: identifier }).delete();
5528
5546
  this.fill(deleted.getRawAttributes());
5547
+ this.syncChanges(previousOriginal);
5548
+ this.syncOriginal();
5529
5549
  await Model.dispatchEvent(constructor, "deleted", this);
5530
5550
  return this;
5531
5551
  }
@@ -5546,11 +5566,14 @@ var Model = class Model {
5546
5566
  async forceDelete() {
5547
5567
  const identifier = this.getAttribute("id");
5548
5568
  if (identifier == null) throw new ArkormException("Cannot force delete a model without an id.");
5569
+ const previousOriginal = this.getOriginal();
5549
5570
  const constructor = this.constructor;
5550
5571
  await Model.dispatchEvent(constructor, "forceDeleting", this);
5551
5572
  await Model.dispatchEvent(constructor, "deleting", this);
5552
5573
  const deleted = await constructor.query().withTrashed().where({ id: identifier }).delete();
5553
5574
  this.fill(deleted.getRawAttributes());
5575
+ this.syncChanges(previousOriginal);
5576
+ this.syncOriginal();
5554
5577
  await Model.dispatchEvent(constructor, "deleted", this);
5555
5578
  await Model.dispatchEvent(constructor, "forceDeleted", this);
5556
5579
  return this;
@@ -5574,9 +5597,12 @@ var Model = class Model {
5574
5597
  const constructor = this.constructor;
5575
5598
  const softDeleteConfig = constructor.getSoftDeleteConfig();
5576
5599
  if (!softDeleteConfig.enabled) return this;
5600
+ const previousOriginal = this.getOriginal();
5577
5601
  await Model.dispatchEvent(constructor, "restoring", this);
5578
5602
  const model = await constructor.query().withTrashed().where({ id: identifier }).update({ [softDeleteConfig.column]: null });
5579
5603
  this.fill(model.getRawAttributes());
5604
+ this.syncChanges(previousOriginal);
5605
+ this.syncOriginal();
5580
5606
  await Model.dispatchEvent(constructor, "restored", this);
5581
5607
  return this;
5582
5608
  }
@@ -5614,6 +5640,42 @@ var Model = class Model {
5614
5640
  getRawAttributes() {
5615
5641
  return { ...this.attributes };
5616
5642
  }
5643
+ getOriginal(key) {
5644
+ if (typeof key === "string") return Model.cloneAttributeValue(this.original[key]);
5645
+ return Object.entries(this.original).reduce((all, [originalKey, value]) => {
5646
+ all[originalKey] = Model.cloneAttributeValue(value);
5647
+ return all;
5648
+ }, {});
5649
+ }
5650
+ /**
5651
+ * Determine whether the model has unsaved attribute changes.
5652
+ *
5653
+ * @param keys
5654
+ * @returns
5655
+ */
5656
+ isDirty(keys) {
5657
+ return Object.keys(this.getDirtyAttributes(keys)).length > 0;
5658
+ }
5659
+ /**
5660
+ * Determine whether the model has no unsaved attribute changes.
5661
+ *
5662
+ * @param keys
5663
+ * @returns
5664
+ */
5665
+ isClean(keys) {
5666
+ return !this.isDirty(keys);
5667
+ }
5668
+ /**
5669
+ * Determine whether the model changed during the last successful persistence operation.
5670
+ *
5671
+ * @param keys
5672
+ * @returns
5673
+ */
5674
+ wasChanged(keys) {
5675
+ const keyList = this.normalizeAttributeKeys(keys);
5676
+ if (keyList.length === 0) return Object.keys(this.changes).length > 0;
5677
+ return keyList.some((key) => Object.prototype.hasOwnProperty.call(this.changes, key));
5678
+ }
5617
5679
  /**
5618
5680
  * Convert the model instance to a plain object, applying visibility
5619
5681
  * rules, appends, and mutators.
@@ -5804,6 +5866,34 @@ var Model = class Model {
5804
5866
  return typeof method === "function" ? method : null;
5805
5867
  }
5806
5868
  /**
5869
+ * Build a map of dirty attributes, optionally limited to specific keys.
5870
+ *
5871
+ * @param keys
5872
+ * @returns
5873
+ */
5874
+ getDirtyAttributes(keys) {
5875
+ const requestedKeys = this.normalizeAttributeKeys(keys);
5876
+ return (requestedKeys.length > 0 ? requestedKeys : Array.from(new Set([...Object.keys(this.original), ...this.touchedAttributes]))).reduce((dirty, key) => {
5877
+ const currentValue = this.attributes[key];
5878
+ const originalValue = this.original[key];
5879
+ const hasCurrent = Object.prototype.hasOwnProperty.call(this.attributes, key);
5880
+ const hasOriginal = Object.prototype.hasOwnProperty.call(this.original, key);
5881
+ if (!hasCurrent && !hasOriginal) return dirty;
5882
+ if (hasCurrent !== hasOriginal || !Model.areAttributeValuesEqual(currentValue, originalValue)) dirty[key] = Model.cloneAttributeValue(currentValue);
5883
+ return dirty;
5884
+ }, {});
5885
+ }
5886
+ /**
5887
+ * Normalize a key or key list for dirty/change lookups.
5888
+ *
5889
+ * @param keys
5890
+ * @returns
5891
+ */
5892
+ normalizeAttributeKeys(keys) {
5893
+ if (typeof keys === "undefined") return [];
5894
+ return Array.isArray(keys) ? keys : [keys];
5895
+ }
5896
+ /**
5807
5897
  * Resolve an Attribute object mutator method for a given key, if it exists.
5808
5898
  *
5809
5899
  * @param key
@@ -5845,6 +5935,66 @@ var Model = class Model {
5845
5935
  if (!Object.prototype.hasOwnProperty.call(this, "eventListeners")) this.eventListeners = { ...this.eventListeners || {} };
5846
5936
  }
5847
5937
  /**
5938
+ * Clone an attribute value to keep snapshot state isolated from live mutations.
5939
+ *
5940
+ * @param value
5941
+ * @returns
5942
+ */
5943
+ static cloneAttributeValue(value) {
5944
+ if (value instanceof Date) return new Date(value.getTime());
5945
+ if (Array.isArray(value)) return value.map((item) => Model.cloneAttributeValue(item));
5946
+ if (value && typeof value === "object") return Object.entries(value).reduce((all, [key, nestedValue]) => {
5947
+ all[key] = Model.cloneAttributeValue(nestedValue);
5948
+ return all;
5949
+ }, {});
5950
+ return value;
5951
+ }
5952
+ /**
5953
+ * Compare attribute values for dirty/change detection.
5954
+ *
5955
+ * @param left
5956
+ * @param right
5957
+ * @returns
5958
+ */
5959
+ static areAttributeValuesEqual(left, right) {
5960
+ if (left === right) return true;
5961
+ if (left instanceof Date && right instanceof Date) return left.getTime() === right.getTime();
5962
+ if (Array.isArray(left) && Array.isArray(right)) {
5963
+ if (left.length !== right.length) return false;
5964
+ return left.every((value, index) => Model.areAttributeValuesEqual(value, right[index]));
5965
+ }
5966
+ if (left && right && typeof left === "object" && typeof right === "object") {
5967
+ const leftEntries = Object.entries(left);
5968
+ const rightEntries = Object.entries(right);
5969
+ if (leftEntries.length !== rightEntries.length) return false;
5970
+ return leftEntries.every(([key, value]) => {
5971
+ return Object.prototype.hasOwnProperty.call(right, key) && Model.areAttributeValuesEqual(value, right[key]);
5972
+ });
5973
+ }
5974
+ return false;
5975
+ }
5976
+ /**
5977
+ * Sync the original snapshot to the model's current raw attributes.
5978
+ */
5979
+ syncOriginal() {
5980
+ this.original = Object.entries(this.attributes).reduce((all, [key, value]) => {
5981
+ all[key] = Model.cloneAttributeValue(value);
5982
+ return all;
5983
+ }, {});
5984
+ this.touchedAttributes.clear();
5985
+ }
5986
+ /**
5987
+ * Sync the last-changed snapshot from a previous original state.
5988
+ *
5989
+ * @param previousOriginal
5990
+ */
5991
+ syncChanges(previousOriginal) {
5992
+ this.changes = Object.entries(this.getDirtyAttributes()).reduce((all, [key, value]) => {
5993
+ if (!Object.prototype.hasOwnProperty.call(previousOriginal, key) || !Model.areAttributeValuesEqual(value, previousOriginal[key])) all[key] = Model.cloneAttributeValue(value);
5994
+ return all;
5995
+ }, {});
5996
+ }
5997
+ /**
5848
5998
  * Resolve lifecycle state for the provided model class.
5849
5999
  *
5850
6000
  * @param modelClass
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkormx",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Modern TypeScript-first ORM for Node.js.",
5
5
  "keywords": [
6
6
  "orm",