arkormx 1.0.0 → 1.2.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
@@ -209,6 +209,95 @@ var ForeignKeyBuilder = class {
209
209
 
210
210
  //#endregion
211
211
  //#region src/database/TableBuilder.ts
212
+ const PRISMA_ENUM_MEMBER_REGEX$1 = /^[A-Za-z][A-Za-z0-9_]*$/;
213
+ const normalizeEnumMember = (columnName, value) => {
214
+ const normalized = value.trim();
215
+ if (!normalized) throw new Error(`Enum column [${columnName}] must define only non-empty values.`);
216
+ if (!PRISMA_ENUM_MEMBER_REGEX$1.test(normalized)) throw new Error(`Enum column [${columnName}] contains invalid Prisma enum value [${normalized}].`);
217
+ return normalized;
218
+ };
219
+ const normalizeEnumMembers = (columnName, values) => {
220
+ const normalizedValues = values.map((value) => normalizeEnumMember(columnName, value));
221
+ const seen = /* @__PURE__ */ new Set();
222
+ for (const value of normalizedValues) {
223
+ if (seen.has(value)) throw new Error(`Enum column [${columnName}] contains duplicate enum value [${value}].`);
224
+ seen.add(value);
225
+ }
226
+ return normalizedValues;
227
+ };
228
+ /**
229
+ * The EnumBuilder class provides a fluent interface for configuring enum columns
230
+ * after they are defined on a table.
231
+ *
232
+ * @author Legacy (3m1n3nc3)
233
+ * @since 0.2.3
234
+ */
235
+ var EnumBuilder = class {
236
+ tableBuilder;
237
+ columnName;
238
+ constructor(tableBuilder, columnName) {
239
+ this.tableBuilder = tableBuilder;
240
+ this.columnName = columnName;
241
+ }
242
+ /**
243
+ * Defines the Prisma enum name for this column.
244
+ *
245
+ * @param name
246
+ * @returns
247
+ */
248
+ enumName(name) {
249
+ this.tableBuilder.enumName(name, this.columnName);
250
+ return this;
251
+ }
252
+ /**
253
+ * Marks the enum column as nullable.
254
+ *
255
+ * @returns
256
+ */
257
+ nullable() {
258
+ this.tableBuilder.nullable(this.columnName);
259
+ return this;
260
+ }
261
+ /**
262
+ * Marks the enum column as unique.
263
+ *
264
+ * @returns
265
+ */
266
+ unique() {
267
+ this.tableBuilder.unique(this.columnName);
268
+ return this;
269
+ }
270
+ /**
271
+ * Sets a default value for the enum column.
272
+ *
273
+ * @param value
274
+ * @returns
275
+ */
276
+ default(value) {
277
+ this.tableBuilder.default(value, this.columnName);
278
+ return this;
279
+ }
280
+ /**
281
+ * Positions the enum column after another column when supported.
282
+ *
283
+ * @param referenceColumn
284
+ * @returns
285
+ */
286
+ after(referenceColumn) {
287
+ this.tableBuilder.after(referenceColumn, this.columnName);
288
+ return this;
289
+ }
290
+ /**
291
+ * Maps the enum column to a custom database column name.
292
+ *
293
+ * @param name
294
+ * @returns
295
+ */
296
+ map(name) {
297
+ this.tableBuilder.map(name, this.columnName);
298
+ return this;
299
+ }
300
+ };
212
301
  /**
213
302
  * The TableBuilder class provides a fluent interface for defining
214
303
  * the structure of a database table in a migration, including columns to add or drop.
@@ -261,6 +350,27 @@ var TableBuilder = class {
261
350
  return this.column(name, "uuid", options);
262
351
  }
263
352
  /**
353
+ * Defines an enum column in the table.
354
+ *
355
+ * @param name The name of the enum column.
356
+ * @param values Either an array of string values for the enum or the name of an existing enum to reuse.
357
+ * @param options Additional options for the enum column.
358
+ * @returns
359
+ */
360
+ enum(name, valuesOrEnumName, options = {}) {
361
+ const isEnumReuse = typeof valuesOrEnumName === "string";
362
+ if (!isEnumReuse && valuesOrEnumName.length === 0) throw new Error(`Enum column [${name}] must define at least one value.`);
363
+ const normalizedEnumValues = isEnumReuse ? void 0 : normalizeEnumMembers(name, valuesOrEnumName);
364
+ const enumName = isEnumReuse ? valuesOrEnumName.trim() : options.enumName?.trim();
365
+ if (isEnumReuse && !enumName) throw new Error(`Enum column [${name}] must define an enum name.`);
366
+ this.column(name, "enum", {
367
+ ...options,
368
+ enumName,
369
+ enumValues: normalizedEnumValues
370
+ });
371
+ return new EnumBuilder(this, name);
372
+ }
373
+ /**
264
374
  * Defines a string column in the table.
265
375
  *
266
376
  * @param name The name of the string column.
@@ -430,6 +540,21 @@ var TableBuilder = class {
430
540
  return this;
431
541
  }
432
542
  /**
543
+ * Sets the Prisma enum name for an enum column.
544
+ *
545
+ * @param name The enum name to assign.
546
+ * @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
547
+ * @returns The current TableBuilder instance for chaining.
548
+ */
549
+ enumName(name, columnName) {
550
+ const column = this.resolveColumn(columnName);
551
+ if (column.type !== "enum") throw new Error(`Column [${column.name}] is not an enum column.`);
552
+ const enumName = name.trim();
553
+ if (!enumName) throw new Error(`Enum column [${column.name}] must define an enum name.`);
554
+ column.enumName = enumName;
555
+ return this;
556
+ }
557
+ /**
433
558
  * Sets a default value for a column.
434
559
  *
435
560
  * @param value The default scalar value or Prisma expression (e.g. 'now()').
@@ -512,7 +637,10 @@ var TableBuilder = class {
512
637
  * @returns
513
638
  */
514
639
  getColumns() {
515
- return this.columns.map((column) => ({ ...column }));
640
+ return this.columns.map((column) => ({
641
+ ...column,
642
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
643
+ }));
516
644
  }
517
645
  /**
518
646
  * Returns a copy of the defined column names to be dropped from the table.
@@ -553,6 +681,8 @@ var TableBuilder = class {
553
681
  this.columns.push({
554
682
  name,
555
683
  type,
684
+ enumName: options.enumName,
685
+ enumValues: options.enumValues ? [...options.enumValues] : void 0,
556
686
  map: options.map,
557
687
  nullable: options.nullable,
558
688
  unique: options.unique,
@@ -652,7 +782,10 @@ var SchemaBuilder = class {
652
782
  return this.operations.map((operation) => {
653
783
  if (operation.type === "createTable") return {
654
784
  ...operation,
655
- columns: operation.columns.map((column) => ({ ...column })),
785
+ columns: operation.columns.map((column) => ({
786
+ ...column,
787
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
788
+ })),
656
789
  indexes: operation.indexes.map((index) => ({
657
790
  ...index,
658
791
  columns: [...index.columns]
@@ -661,7 +794,10 @@ var SchemaBuilder = class {
661
794
  };
662
795
  if (operation.type === "alterTable") return {
663
796
  ...operation,
664
- addColumns: operation.addColumns.map((column) => ({ ...column })),
797
+ addColumns: operation.addColumns.map((column) => ({
798
+ ...column,
799
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
800
+ })),
665
801
  dropColumns: [...operation.dropColumns],
666
802
  addIndexes: operation.addIndexes.map((index) => ({
667
803
  ...index,
@@ -677,6 +813,8 @@ var SchemaBuilder = class {
677
813
  //#endregion
678
814
  //#region src/helpers/migrations.ts
679
815
  const PRISMA_MODEL_REGEX = /model\s+(\w+)\s*\{[\s\S]*?\n\}/g;
816
+ const PRISMA_ENUM_REGEX = /enum\s+(\w+)\s*\{[\s\S]*?\n\}/g;
817
+ const PRISMA_ENUM_MEMBER_REGEX = /^[A-Za-z][A-Za-z0-9_]*$/;
680
818
  /**
681
819
  * Convert a table name to a PascalCase model name, with basic singularization.
682
820
  *
@@ -704,6 +842,7 @@ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
704
842
  */
705
843
  const resolvePrismaType = (column) => {
706
844
  if (column.type === "id") return "Int";
845
+ if (column.type === "enum") return resolveEnumName(column);
707
846
  if (column.type === "uuid") return "String";
708
847
  if (column.type === "string" || column.type === "text") return "String";
709
848
  if (column.type === "integer") return "Int";
@@ -713,6 +852,11 @@ const resolvePrismaType = (column) => {
713
852
  if (column.type === "json") return "Json";
714
853
  return "DateTime";
715
854
  };
855
+ const resolveEnumName = (column) => {
856
+ if (column.type !== "enum") throw new ArkormException(`Column [${column.name}] is not an enum column.`);
857
+ if (column.enumName && column.enumName.trim().length > 0) return column.enumName.trim();
858
+ throw new ArkormException(`Enum column [${column.name}] must define an enum name.`);
859
+ };
716
860
  /**
717
861
  * Format a default value for inclusion in a Prisma schema field definition, based on its type.
718
862
  *
@@ -727,6 +871,61 @@ const formatDefaultValue = (value) => {
727
871
  if (typeof value === "boolean") return `@default(${value ? "true" : "false"})`;
728
872
  };
729
873
  /**
874
+ * Format a default value for an enum column as a Prisma @default attribute, validating that it is a non-empty string.
875
+ *
876
+ * @param value
877
+ * @returns
878
+ */
879
+ const formatEnumDefaultValue = (value) => {
880
+ if (value == null) return void 0;
881
+ if (typeof value !== "string" || value.trim().length === 0) throw new ArkormException("Enum default values must be provided as non-empty strings.");
882
+ return `@default(${value.trim()})`;
883
+ };
884
+ /**
885
+ * Normalize an enum value by ensuring it is a non-empty string and trimming whitespace.
886
+ *
887
+ * @param value
888
+ * @returns
889
+ */
890
+ const normalizeEnumValue = (value) => {
891
+ if (typeof value !== "string" || value.trim().length === 0) throw new ArkormException("Enum values must be provided as non-empty strings.");
892
+ const normalized = value.trim();
893
+ if (!PRISMA_ENUM_MEMBER_REGEX.test(normalized)) throw new ArkormException(`Enum value [${normalized}] is not a valid Prisma enum member name.`);
894
+ return normalized;
895
+ };
896
+ /**
897
+ * Extract the enum values from a Prisma enum block string.
898
+ *
899
+ * @param block
900
+ * @returns
901
+ */
902
+ const extractEnumBlockValues = (block) => {
903
+ return block.split("\n").slice(1, -1).map((line) => line.trim()).filter(Boolean);
904
+ };
905
+ const validateEnumValues = (column, enumName, enumValues) => {
906
+ const normalizedValues = enumValues.map(normalizeEnumValue);
907
+ const seen = /* @__PURE__ */ new Set();
908
+ for (const value of normalizedValues) {
909
+ if (seen.has(value)) throw new ArkormException(`Prisma enum [${enumName}] for column [${column.name}] contains duplicate value [${value}].`);
910
+ seen.add(value);
911
+ }
912
+ return normalizedValues;
913
+ };
914
+ /**
915
+ * Validate that a default value for an enum column is included in the defined enum values.
916
+ *
917
+ * @param column
918
+ * @param enumName
919
+ * @param enumValues
920
+ * @returns
921
+ */
922
+ const validateEnumDefaultValue = (column, enumName, enumValues) => {
923
+ if (column.default == null) return;
924
+ const normalizedDefault = normalizeEnumValue(column.default);
925
+ if (enumValues.includes(normalizedDefault)) return;
926
+ throw new ArkormException(`Enum default value [${normalizedDefault}] is not defined in Prisma enum [${enumName}] for column [${column.name}].`);
927
+ };
928
+ /**
730
929
  * Build a single line of a Prisma model field definition based on a SchemaColumn, including type and modifiers.
731
930
  *
732
931
  * @param column
@@ -747,11 +946,82 @@ const buildFieldLine = (column) => {
747
946
  const primary = column.primary ? " @id" : "";
748
947
  const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
749
948
  const updatedAt = column.updatedAt ? " @updatedAt" : "";
750
- const defaultValue = formatDefaultValue(column.default) ?? (column.type === "uuid" && column.primary ? "@default(uuid())" : void 0);
949
+ const defaultValue = column.type === "enum" ? formatEnumDefaultValue(column.default) : formatDefaultValue(column.default) ?? (column.type === "uuid" && column.primary ? "@default(uuid())" : void 0);
751
950
  const defaultSuffix = defaultValue ? ` ${defaultValue}` : "";
752
951
  return ` ${column.name} ${scalar}${nullable}${primary}${unique}${defaultSuffix}${updatedAt}${mapped}`;
753
952
  };
754
953
  /**
954
+ * Build a Prisma enum block string based on an enum name and its values, validating that
955
+ * at least one value is provided.
956
+ *
957
+ * @param enumName The name of the enum to create.
958
+ * @param values The array of values for the enum.
959
+ * @returns The Prisma enum block string.
960
+ */
961
+ const buildEnumBlock = (enumName, values) => {
962
+ if (values.length === 0) throw new ArkormException(`Enum [${enumName}] must define at least one value.`);
963
+ return `enum ${enumName} {\n${values.map((value) => ` ${value}`).join("\n")}\n}`;
964
+ };
965
+ /**
966
+ * Find the Prisma enum block in a schema string that corresponds to a given enum
967
+ * name, returning its details if found.
968
+ *
969
+ * @param schema The Prisma schema string to search for the enum block.
970
+ * @param enumName The name of the enum to find in the schema.
971
+ * @returns
972
+ */
973
+ const findEnumBlock = (schema, enumName) => {
974
+ const candidates = [...schema.matchAll(PRISMA_ENUM_REGEX)];
975
+ for (const match of candidates) {
976
+ const block = match[0];
977
+ const matchedEnumName = match[1];
978
+ const start = match.index ?? 0;
979
+ const end = start + block.length;
980
+ if (matchedEnumName === enumName) return {
981
+ enumName: matchedEnumName,
982
+ block,
983
+ start,
984
+ end
985
+ };
986
+ }
987
+ return null;
988
+ };
989
+ /**
990
+ * Ensure that Prisma enum blocks exist in the schema for any enum columns defined in a
991
+ * create or alter table operation, adding them if necessary and validating against
992
+ * existing blocks.
993
+ *
994
+ * @param schema The current Prisma schema string to check and modify.
995
+ * @param columns The array of schema column definitions to check for enum types and ensure corresponding blocks exist for.
996
+ * @returns
997
+ */
998
+ const ensureEnumBlocks = (schema, columns) => {
999
+ let nextSchema = schema;
1000
+ for (const column of columns) {
1001
+ if (column.type !== "enum") continue;
1002
+ const enumName = resolveEnumName(column);
1003
+ const enumValues = column.enumValues ?? [];
1004
+ const existing = findEnumBlock(nextSchema, enumName);
1005
+ if (existing) {
1006
+ const existingValues = validateEnumValues(column, enumName, extractEnumBlockValues(existing.block));
1007
+ if (enumValues.length === 0) {
1008
+ validateEnumDefaultValue(column, enumName, existingValues);
1009
+ continue;
1010
+ }
1011
+ const normalizedEnumValues = validateEnumValues(column, enumName, enumValues);
1012
+ if (existingValues.join("|") !== normalizedEnumValues.join("|")) throw new ArkormException(`Prisma enum [${enumName}] already exists with different values.`);
1013
+ validateEnumDefaultValue(column, enumName, existingValues);
1014
+ continue;
1015
+ }
1016
+ if (enumValues.length === 0) throw new ArkormException(`Prisma enum [${enumName}] was not found for column [${column.name}].`);
1017
+ const normalizedEnumValues = validateEnumValues(column, enumName, enumValues);
1018
+ validateEnumDefaultValue(column, enumName, normalizedEnumValues);
1019
+ const block = buildEnumBlock(enumName, normalizedEnumValues);
1020
+ nextSchema = `${nextSchema.trimEnd()}\n\n${block}\n`;
1021
+ }
1022
+ return nextSchema;
1023
+ };
1024
+ /**
755
1025
  * Build a Prisma model-level @@index definition line.
756
1026
  *
757
1027
  * @param index The schema index definition to convert to a Prisma \@\@index line.
@@ -953,8 +1223,9 @@ const findModelBlock = (schema, table) => {
953
1223
  */
954
1224
  const applyCreateTableOperation = (schema, operation) => {
955
1225
  if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
1226
+ const schemaWithEnums = ensureEnumBlocks(schema, operation.columns);
956
1227
  const block = buildModelBlock(operation);
957
- return applyInverseRelations(`${schema.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? []);
1228
+ return applyInverseRelations(`${schemaWithEnums.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? []);
958
1229
  };
959
1230
  /**
960
1231
  * Apply an alter table operation to a Prisma schema string, modifying the model
@@ -967,7 +1238,10 @@ const applyCreateTableOperation = (schema, operation) => {
967
1238
  const applyAlterTableOperation = (schema, operation) => {
968
1239
  const model = findModelBlock(schema, operation.table);
969
1240
  if (!model) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
970
- let block = model.block;
1241
+ const schemaWithEnums = ensureEnumBlocks(schema, operation.addColumns);
1242
+ const refreshedModel = findModelBlock(schemaWithEnums, operation.table);
1243
+ if (!refreshedModel) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
1244
+ let block = refreshedModel.block;
971
1245
  const bodyLines = block.split("\n");
972
1246
  operation.dropColumns.forEach((column) => {
973
1247
  const columnRegex = new RegExp(`^\\s*${escapeRegex(column)}\\s+`);
@@ -997,7 +1271,7 @@ const applyAlterTableOperation = (schema, operation) => {
997
1271
  injectLineIntoModelBody(bodyLines, relationLine, (line) => relationRegex.test(line));
998
1272
  }
999
1273
  block = bodyLines.join("\n");
1000
- return applyInverseRelations(`${schema.slice(0, model.start)}${block}${schema.slice(model.end)}`, model.modelName, operation.addForeignKeys ?? []);
1274
+ return applyInverseRelations(`${schemaWithEnums.slice(0, refreshedModel.start)}${block}${schemaWithEnums.slice(refreshedModel.end)}`, model.modelName, operation.addForeignKeys ?? []);
1001
1275
  };
1002
1276
  /**
1003
1277
  * Apply a drop table operation to a Prisma schema string, removing the model block
@@ -5098,8 +5372,14 @@ var Model = class Model {
5098
5372
  visible = [];
5099
5373
  appends = [];
5100
5374
  attributes;
5375
+ original;
5376
+ changes;
5377
+ touchedAttributes;
5101
5378
  constructor(attributes = {}) {
5102
5379
  this.attributes = {};
5380
+ this.original = {};
5381
+ this.changes = {};
5382
+ this.touchedAttributes = /* @__PURE__ */ new Set();
5103
5383
  this.fill(attributes);
5104
5384
  return new Proxy(this, {
5105
5385
  get: (target, key, receiver) => {
@@ -5399,7 +5679,10 @@ var Model = class Model {
5399
5679
  * @returns
5400
5680
  */
5401
5681
  static hydrate(attributes) {
5402
- return new this(attributes);
5682
+ const model = new this(attributes);
5683
+ model.syncOriginal();
5684
+ model.syncChanges({});
5685
+ return model;
5403
5686
  }
5404
5687
  /**
5405
5688
  * Hydrate multiple model instances from an array of plain objects of attributes.
@@ -5466,6 +5749,7 @@ var Model = class Model {
5466
5749
  else if (mutator) resolved = mutator.call(this, resolved);
5467
5750
  if (cast) resolved = resolveCast(cast).set(resolved);
5468
5751
  this.attributes[key] = resolved;
5752
+ this.touchedAttributes.add(key);
5469
5753
  return this;
5470
5754
  }
5471
5755
  /**
@@ -5478,12 +5762,15 @@ var Model = class Model {
5478
5762
  async save() {
5479
5763
  const identifier = this.getAttribute("id");
5480
5764
  const payload = this.getRawAttributes();
5765
+ const previousOriginal = this.getOriginal();
5481
5766
  const constructor = this.constructor;
5482
5767
  if (identifier == null) {
5483
5768
  await Model.dispatchEvent(constructor, "saving", this);
5484
5769
  await Model.dispatchEvent(constructor, "creating", this);
5485
5770
  const model = await constructor.query().create(payload);
5486
5771
  this.fill(model.getRawAttributes());
5772
+ this.syncChanges(previousOriginal);
5773
+ this.syncOriginal();
5487
5774
  await Model.dispatchEvent(constructor, "created", this);
5488
5775
  await Model.dispatchEvent(constructor, "saved", this);
5489
5776
  return this;
@@ -5492,6 +5779,8 @@ var Model = class Model {
5492
5779
  await Model.dispatchEvent(constructor, "updating", this);
5493
5780
  const model = await constructor.query().where({ id: identifier }).update(payload);
5494
5781
  this.fill(model.getRawAttributes());
5782
+ this.syncChanges(previousOriginal);
5783
+ this.syncOriginal();
5495
5784
  await Model.dispatchEvent(constructor, "updated", this);
5496
5785
  await Model.dispatchEvent(constructor, "saved", this);
5497
5786
  return this;
@@ -5515,17 +5804,22 @@ var Model = class Model {
5515
5804
  async delete() {
5516
5805
  const identifier = this.getAttribute("id");
5517
5806
  if (identifier == null) throw new ArkormException("Cannot delete a model without an id.");
5807
+ const previousOriginal = this.getOriginal();
5518
5808
  const constructor = this.constructor;
5519
5809
  await Model.dispatchEvent(constructor, "deleting", this);
5520
5810
  const softDeleteConfig = constructor.getSoftDeleteConfig();
5521
5811
  if (softDeleteConfig.enabled) {
5522
5812
  const model = await constructor.query().where({ id: identifier }).update({ [softDeleteConfig.column]: /* @__PURE__ */ new Date() });
5523
5813
  this.fill(model.getRawAttributes());
5814
+ this.syncChanges(previousOriginal);
5815
+ this.syncOriginal();
5524
5816
  await Model.dispatchEvent(constructor, "deleted", this);
5525
5817
  return this;
5526
5818
  }
5527
5819
  const deleted = await constructor.query().where({ id: identifier }).delete();
5528
5820
  this.fill(deleted.getRawAttributes());
5821
+ this.syncChanges(previousOriginal);
5822
+ this.syncOriginal();
5529
5823
  await Model.dispatchEvent(constructor, "deleted", this);
5530
5824
  return this;
5531
5825
  }
@@ -5546,11 +5840,14 @@ var Model = class Model {
5546
5840
  async forceDelete() {
5547
5841
  const identifier = this.getAttribute("id");
5548
5842
  if (identifier == null) throw new ArkormException("Cannot force delete a model without an id.");
5843
+ const previousOriginal = this.getOriginal();
5549
5844
  const constructor = this.constructor;
5550
5845
  await Model.dispatchEvent(constructor, "forceDeleting", this);
5551
5846
  await Model.dispatchEvent(constructor, "deleting", this);
5552
5847
  const deleted = await constructor.query().withTrashed().where({ id: identifier }).delete();
5553
5848
  this.fill(deleted.getRawAttributes());
5849
+ this.syncChanges(previousOriginal);
5850
+ this.syncOriginal();
5554
5851
  await Model.dispatchEvent(constructor, "deleted", this);
5555
5852
  await Model.dispatchEvent(constructor, "forceDeleted", this);
5556
5853
  return this;
@@ -5574,9 +5871,12 @@ var Model = class Model {
5574
5871
  const constructor = this.constructor;
5575
5872
  const softDeleteConfig = constructor.getSoftDeleteConfig();
5576
5873
  if (!softDeleteConfig.enabled) return this;
5874
+ const previousOriginal = this.getOriginal();
5577
5875
  await Model.dispatchEvent(constructor, "restoring", this);
5578
5876
  const model = await constructor.query().withTrashed().where({ id: identifier }).update({ [softDeleteConfig.column]: null });
5579
5877
  this.fill(model.getRawAttributes());
5878
+ this.syncChanges(previousOriginal);
5879
+ this.syncOriginal();
5580
5880
  await Model.dispatchEvent(constructor, "restored", this);
5581
5881
  return this;
5582
5882
  }
@@ -5614,6 +5914,42 @@ var Model = class Model {
5614
5914
  getRawAttributes() {
5615
5915
  return { ...this.attributes };
5616
5916
  }
5917
+ getOriginal(key) {
5918
+ if (typeof key === "string") return Model.cloneAttributeValue(this.original[key]);
5919
+ return Object.entries(this.original).reduce((all, [originalKey, value]) => {
5920
+ all[originalKey] = Model.cloneAttributeValue(value);
5921
+ return all;
5922
+ }, {});
5923
+ }
5924
+ /**
5925
+ * Determine whether the model has unsaved attribute changes.
5926
+ *
5927
+ * @param keys
5928
+ * @returns
5929
+ */
5930
+ isDirty(keys) {
5931
+ return Object.keys(this.getDirtyAttributes(keys)).length > 0;
5932
+ }
5933
+ /**
5934
+ * Determine whether the model has no unsaved attribute changes.
5935
+ *
5936
+ * @param keys
5937
+ * @returns
5938
+ */
5939
+ isClean(keys) {
5940
+ return !this.isDirty(keys);
5941
+ }
5942
+ /**
5943
+ * Determine whether the model changed during the last successful persistence operation.
5944
+ *
5945
+ * @param keys
5946
+ * @returns
5947
+ */
5948
+ wasChanged(keys) {
5949
+ const keyList = this.normalizeAttributeKeys(keys);
5950
+ if (keyList.length === 0) return Object.keys(this.changes).length > 0;
5951
+ return keyList.some((key) => Object.prototype.hasOwnProperty.call(this.changes, key));
5952
+ }
5617
5953
  /**
5618
5954
  * Convert the model instance to a plain object, applying visibility
5619
5955
  * rules, appends, and mutators.
@@ -5804,6 +6140,34 @@ var Model = class Model {
5804
6140
  return typeof method === "function" ? method : null;
5805
6141
  }
5806
6142
  /**
6143
+ * Build a map of dirty attributes, optionally limited to specific keys.
6144
+ *
6145
+ * @param keys
6146
+ * @returns
6147
+ */
6148
+ getDirtyAttributes(keys) {
6149
+ const requestedKeys = this.normalizeAttributeKeys(keys);
6150
+ return (requestedKeys.length > 0 ? requestedKeys : Array.from(new Set([...Object.keys(this.original), ...this.touchedAttributes]))).reduce((dirty, key) => {
6151
+ const currentValue = this.attributes[key];
6152
+ const originalValue = this.original[key];
6153
+ const hasCurrent = Object.prototype.hasOwnProperty.call(this.attributes, key);
6154
+ const hasOriginal = Object.prototype.hasOwnProperty.call(this.original, key);
6155
+ if (!hasCurrent && !hasOriginal) return dirty;
6156
+ if (hasCurrent !== hasOriginal || !Model.areAttributeValuesEqual(currentValue, originalValue)) dirty[key] = Model.cloneAttributeValue(currentValue);
6157
+ return dirty;
6158
+ }, {});
6159
+ }
6160
+ /**
6161
+ * Normalize a key or key list for dirty/change lookups.
6162
+ *
6163
+ * @param keys
6164
+ * @returns
6165
+ */
6166
+ normalizeAttributeKeys(keys) {
6167
+ if (typeof keys === "undefined") return [];
6168
+ return Array.isArray(keys) ? keys : [keys];
6169
+ }
6170
+ /**
5807
6171
  * Resolve an Attribute object mutator method for a given key, if it exists.
5808
6172
  *
5809
6173
  * @param key
@@ -5845,6 +6209,66 @@ var Model = class Model {
5845
6209
  if (!Object.prototype.hasOwnProperty.call(this, "eventListeners")) this.eventListeners = { ...this.eventListeners || {} };
5846
6210
  }
5847
6211
  /**
6212
+ * Clone an attribute value to keep snapshot state isolated from live mutations.
6213
+ *
6214
+ * @param value
6215
+ * @returns
6216
+ */
6217
+ static cloneAttributeValue(value) {
6218
+ if (value instanceof Date) return new Date(value.getTime());
6219
+ if (Array.isArray(value)) return value.map((item) => Model.cloneAttributeValue(item));
6220
+ if (value && typeof value === "object") return Object.entries(value).reduce((all, [key, nestedValue]) => {
6221
+ all[key] = Model.cloneAttributeValue(nestedValue);
6222
+ return all;
6223
+ }, {});
6224
+ return value;
6225
+ }
6226
+ /**
6227
+ * Compare attribute values for dirty/change detection.
6228
+ *
6229
+ * @param left
6230
+ * @param right
6231
+ * @returns
6232
+ */
6233
+ static areAttributeValuesEqual(left, right) {
6234
+ if (left === right) return true;
6235
+ if (left instanceof Date && right instanceof Date) return left.getTime() === right.getTime();
6236
+ if (Array.isArray(left) && Array.isArray(right)) {
6237
+ if (left.length !== right.length) return false;
6238
+ return left.every((value, index) => Model.areAttributeValuesEqual(value, right[index]));
6239
+ }
6240
+ if (left && right && typeof left === "object" && typeof right === "object") {
6241
+ const leftEntries = Object.entries(left);
6242
+ const rightEntries = Object.entries(right);
6243
+ if (leftEntries.length !== rightEntries.length) return false;
6244
+ return leftEntries.every(([key, value]) => {
6245
+ return Object.prototype.hasOwnProperty.call(right, key) && Model.areAttributeValuesEqual(value, right[key]);
6246
+ });
6247
+ }
6248
+ return false;
6249
+ }
6250
+ /**
6251
+ * Sync the original snapshot to the model's current raw attributes.
6252
+ */
6253
+ syncOriginal() {
6254
+ this.original = Object.entries(this.attributes).reduce((all, [key, value]) => {
6255
+ all[key] = Model.cloneAttributeValue(value);
6256
+ return all;
6257
+ }, {});
6258
+ this.touchedAttributes.clear();
6259
+ }
6260
+ /**
6261
+ * Sync the last-changed snapshot from a previous original state.
6262
+ *
6263
+ * @param previousOriginal
6264
+ */
6265
+ syncChanges(previousOriginal) {
6266
+ this.changes = Object.entries(this.getDirtyAttributes()).reduce((all, [key, value]) => {
6267
+ if (!Object.prototype.hasOwnProperty.call(previousOriginal, key) || !Model.areAttributeValuesEqual(value, previousOriginal[key])) all[key] = Model.cloneAttributeValue(value);
6268
+ return all;
6269
+ }, {});
6270
+ }
6271
+ /**
5848
6272
  * Resolve lifecycle state for the provided model class.
5849
6273
  *
5850
6274
  * @param modelClass
@@ -5952,4 +6376,4 @@ var Model = class Model {
5952
6376
  };
5953
6377
 
5954
6378
  //#endregion
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 };
6379
+ export { ArkormCollection, ArkormException, Attribute, CliApp, EnumBuilder, ForeignKeyBuilder, InitCommand, InlineFactory, LengthAwarePaginator, MIGRATION_BRAND, MakeFactoryCommand, MakeMigrationCommand, MakeModelCommand, MakeSeederCommand, MigrateCommand, MigrateRollbackCommand, Migration, MigrationHistoryCommand, MissingDelegateException, Model, ModelFactory, ModelNotFoundException, ModelsSyncCommand, PRISMA_ENUM_MEMBER_REGEX, PRISMA_ENUM_REGEX, PRISMA_MODEL_REGEX, Paginator, QueryBuilder, QueryConstraintException, RelationResolutionException, SEEDER_BRAND, SchemaBuilder, ScopeNotDefinedException, SeedCommand, Seeder, TableBuilder, URLDriver, UniqueConstraintResolutionException, UnsupportedAdapterFeatureException, applyAlterTableOperation, applyCreateTableOperation, applyDropTableOperation, applyMigrationRollbackToPrismaSchema, applyMigrationToPrismaSchema, applyOperationsToPrismaSchema, buildEnumBlock, buildFieldLine, buildIndexLine, buildInverseRelationLine, buildMigrationIdentity, buildMigrationRunId, buildMigrationSource, buildModelBlock, buildRelationLine, computeMigrationChecksum, configureArkormRuntime, createMigrationTimestamp, createPrismaAdapter, createPrismaDelegateMap, defineConfig, defineFactory, deriveCollectionFieldName, deriveInverseRelationAlias, deriveRelationFieldName, ensureArkormConfigLoading, escapeRegex, findAppliedMigration, findEnumBlock, findModelBlock, formatDefaultValue, formatEnumDefaultValue, formatRelationAction, generateMigrationFile, getActiveTransactionClient, getDefaultStubsPath, getLastMigrationRun, getLatestAppliedMigrations, getMigrationPlan, getRuntimePaginationCurrentPageResolver, getRuntimePaginationURLDriverFactory, getRuntimePrismaClient, getUserConfig, inferDelegateName, isDelegateLike, isMigrationApplied, isTransactionCapableClient, loadArkormConfig, markMigrationApplied, markMigrationRun, pad, readAppliedMigrationsState, removeAppliedMigration, resetArkormRuntimeForTests, resolveCast, resolveEnumName, resolveMigrationClassName, resolveMigrationStateFilePath, resolvePrismaType, runArkormTransaction, runMigrationWithPrisma, runPrismaCommand, toMigrationFileSlug, toModelName, writeAppliedMigrationsState };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkormx",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Modern TypeScript-first ORM for Node.js.",
5
5
  "keywords": [
6
6
  "orm",