arkormx 1.1.0 → 1.2.1

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/cli.mjs CHANGED
@@ -129,6 +129,95 @@ var ForeignKeyBuilder = class {
129
129
 
130
130
  //#endregion
131
131
  //#region src/database/TableBuilder.ts
132
+ const PRISMA_ENUM_MEMBER_REGEX$1 = /^[A-Za-z][A-Za-z0-9_]*$/;
133
+ const normalizeEnumMember = (columnName, value) => {
134
+ const normalized = value.trim();
135
+ if (!normalized) throw new Error(`Enum column [${columnName}] must define only non-empty values.`);
136
+ if (!PRISMA_ENUM_MEMBER_REGEX$1.test(normalized)) throw new Error(`Enum column [${columnName}] contains invalid Prisma enum value [${normalized}].`);
137
+ return normalized;
138
+ };
139
+ const normalizeEnumMembers = (columnName, values) => {
140
+ const normalizedValues = values.map((value) => normalizeEnumMember(columnName, value));
141
+ const seen = /* @__PURE__ */ new Set();
142
+ for (const value of normalizedValues) {
143
+ if (seen.has(value)) throw new Error(`Enum column [${columnName}] contains duplicate enum value [${value}].`);
144
+ seen.add(value);
145
+ }
146
+ return normalizedValues;
147
+ };
148
+ /**
149
+ * The EnumBuilder class provides a fluent interface for configuring enum columns
150
+ * after they are defined on a table.
151
+ *
152
+ * @author Legacy (3m1n3nc3)
153
+ * @since 0.2.3
154
+ */
155
+ var EnumBuilder = class {
156
+ tableBuilder;
157
+ columnName;
158
+ constructor(tableBuilder, columnName) {
159
+ this.tableBuilder = tableBuilder;
160
+ this.columnName = columnName;
161
+ }
162
+ /**
163
+ * Defines the Prisma enum name for this column.
164
+ *
165
+ * @param name
166
+ * @returns
167
+ */
168
+ enumName(name) {
169
+ this.tableBuilder.enumName(name, this.columnName);
170
+ return this;
171
+ }
172
+ /**
173
+ * Marks the enum column as nullable.
174
+ *
175
+ * @returns
176
+ */
177
+ nullable() {
178
+ this.tableBuilder.nullable(this.columnName);
179
+ return this;
180
+ }
181
+ /**
182
+ * Marks the enum column as unique.
183
+ *
184
+ * @returns
185
+ */
186
+ unique() {
187
+ this.tableBuilder.unique(this.columnName);
188
+ return this;
189
+ }
190
+ /**
191
+ * Sets a default value for the enum column.
192
+ *
193
+ * @param value
194
+ * @returns
195
+ */
196
+ default(value) {
197
+ this.tableBuilder.default(value, this.columnName);
198
+ return this;
199
+ }
200
+ /**
201
+ * Positions the enum column after another column when supported.
202
+ *
203
+ * @param referenceColumn
204
+ * @returns
205
+ */
206
+ after(referenceColumn) {
207
+ this.tableBuilder.after(referenceColumn, this.columnName);
208
+ return this;
209
+ }
210
+ /**
211
+ * Maps the enum column to a custom database column name.
212
+ *
213
+ * @param name
214
+ * @returns
215
+ */
216
+ map(name) {
217
+ this.tableBuilder.map(name, this.columnName);
218
+ return this;
219
+ }
220
+ };
132
221
  /**
133
222
  * The TableBuilder class provides a fluent interface for defining
134
223
  * the structure of a database table in a migration, including columns to add or drop.
@@ -181,6 +270,27 @@ var TableBuilder = class {
181
270
  return this.column(name, "uuid", options);
182
271
  }
183
272
  /**
273
+ * Defines an enum column in the table.
274
+ *
275
+ * @param name The name of the enum column.
276
+ * @param values Either an array of string values for the enum or the name of an existing enum to reuse.
277
+ * @param options Additional options for the enum column.
278
+ * @returns
279
+ */
280
+ enum(name, valuesOrEnumName, options = {}) {
281
+ const isEnumReuse = typeof valuesOrEnumName === "string";
282
+ if (!isEnumReuse && valuesOrEnumName.length === 0) throw new Error(`Enum column [${name}] must define at least one value.`);
283
+ const normalizedEnumValues = isEnumReuse ? void 0 : normalizeEnumMembers(name, valuesOrEnumName);
284
+ const enumName = isEnumReuse ? valuesOrEnumName.trim() : options.enumName?.trim();
285
+ if (isEnumReuse && !enumName) throw new Error(`Enum column [${name}] must define an enum name.`);
286
+ this.column(name, "enum", {
287
+ ...options,
288
+ enumName,
289
+ enumValues: normalizedEnumValues
290
+ });
291
+ return new EnumBuilder(this, name);
292
+ }
293
+ /**
184
294
  * Defines a string column in the table.
185
295
  *
186
296
  * @param name The name of the string column.
@@ -350,6 +460,21 @@ var TableBuilder = class {
350
460
  return this;
351
461
  }
352
462
  /**
463
+ * Sets the Prisma enum name for an enum column.
464
+ *
465
+ * @param name The enum name to assign.
466
+ * @param columnName Optional explicit target column name. When omitted, applies to the latest defined column.
467
+ * @returns The current TableBuilder instance for chaining.
468
+ */
469
+ enumName(name, columnName) {
470
+ const column = this.resolveColumn(columnName);
471
+ if (column.type !== "enum") throw new Error(`Column [${column.name}] is not an enum column.`);
472
+ const enumName = name.trim();
473
+ if (!enumName) throw new Error(`Enum column [${column.name}] must define an enum name.`);
474
+ column.enumName = enumName;
475
+ return this;
476
+ }
477
+ /**
353
478
  * Sets a default value for a column.
354
479
  *
355
480
  * @param value The default scalar value or Prisma expression (e.g. 'now()').
@@ -424,7 +549,7 @@ var TableBuilder = class {
424
549
  */
425
550
  foreign(column) {
426
551
  const columnName = this.resolveColumn(column).name;
427
- return this.foreignKey(columnName + (column ? "" : "Id"));
552
+ return this.foreignKey(column ?? columnName);
428
553
  }
429
554
  /**
430
555
  * Returns a deep copy of the defined columns for the table.
@@ -432,7 +557,10 @@ var TableBuilder = class {
432
557
  * @returns
433
558
  */
434
559
  getColumns() {
435
- return this.columns.map((column) => ({ ...column }));
560
+ return this.columns.map((column) => ({
561
+ ...column,
562
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
563
+ }));
436
564
  }
437
565
  /**
438
566
  * Returns a copy of the defined column names to be dropped from the table.
@@ -473,6 +601,8 @@ var TableBuilder = class {
473
601
  this.columns.push({
474
602
  name,
475
603
  type,
604
+ enumName: options.enumName,
605
+ enumValues: options.enumValues ? [...options.enumValues] : void 0,
476
606
  map: options.map,
477
607
  nullable: options.nullable,
478
608
  unique: options.unique,
@@ -572,7 +702,10 @@ var SchemaBuilder = class {
572
702
  return this.operations.map((operation) => {
573
703
  if (operation.type === "createTable") return {
574
704
  ...operation,
575
- columns: operation.columns.map((column) => ({ ...column })),
705
+ columns: operation.columns.map((column) => ({
706
+ ...column,
707
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
708
+ })),
576
709
  indexes: operation.indexes.map((index) => ({
577
710
  ...index,
578
711
  columns: [...index.columns]
@@ -581,7 +714,10 @@ var SchemaBuilder = class {
581
714
  };
582
715
  if (operation.type === "alterTable") return {
583
716
  ...operation,
584
- addColumns: operation.addColumns.map((column) => ({ ...column })),
717
+ addColumns: operation.addColumns.map((column) => ({
718
+ ...column,
719
+ enumValues: column.enumValues ? [...column.enumValues] : void 0
720
+ })),
585
721
  dropColumns: [...operation.dropColumns],
586
722
  addIndexes: operation.addIndexes.map((index) => ({
587
723
  ...index,
@@ -597,6 +733,8 @@ var SchemaBuilder = class {
597
733
  //#endregion
598
734
  //#region src/helpers/migrations.ts
599
735
  const PRISMA_MODEL_REGEX = /model\s+(\w+)\s*\{[\s\S]*?\n\}/g;
736
+ const PRISMA_ENUM_REGEX = /enum\s+(\w+)\s*\{[\s\S]*?\n\}/g;
737
+ const PRISMA_ENUM_MEMBER_REGEX = /^[A-Za-z][A-Za-z0-9_]*$/;
600
738
  /**
601
739
  * Convert a table name to a PascalCase model name, with basic singularization.
602
740
  *
@@ -624,6 +762,7 @@ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
624
762
  */
625
763
  const resolvePrismaType = (column) => {
626
764
  if (column.type === "id") return "Int";
765
+ if (column.type === "enum") return resolveEnumName(column);
627
766
  if (column.type === "uuid") return "String";
628
767
  if (column.type === "string" || column.type === "text") return "String";
629
768
  if (column.type === "integer") return "Int";
@@ -633,6 +772,11 @@ const resolvePrismaType = (column) => {
633
772
  if (column.type === "json") return "Json";
634
773
  return "DateTime";
635
774
  };
775
+ const resolveEnumName = (column) => {
776
+ if (column.type !== "enum") throw new ArkormException(`Column [${column.name}] is not an enum column.`);
777
+ if (column.enumName && column.enumName.trim().length > 0) return column.enumName.trim();
778
+ throw new ArkormException(`Enum column [${column.name}] must define an enum name.`);
779
+ };
636
780
  /**
637
781
  * Format a default value for inclusion in a Prisma schema field definition, based on its type.
638
782
  *
@@ -647,6 +791,61 @@ const formatDefaultValue = (value) => {
647
791
  if (typeof value === "boolean") return `@default(${value ? "true" : "false"})`;
648
792
  };
649
793
  /**
794
+ * Format a default value for an enum column as a Prisma @default attribute, validating that it is a non-empty string.
795
+ *
796
+ * @param value
797
+ * @returns
798
+ */
799
+ const formatEnumDefaultValue = (value) => {
800
+ if (value == null) return void 0;
801
+ if (typeof value !== "string" || value.trim().length === 0) throw new ArkormException("Enum default values must be provided as non-empty strings.");
802
+ return `@default(${value.trim()})`;
803
+ };
804
+ /**
805
+ * Normalize an enum value by ensuring it is a non-empty string and trimming whitespace.
806
+ *
807
+ * @param value
808
+ * @returns
809
+ */
810
+ const normalizeEnumValue = (value) => {
811
+ if (typeof value !== "string" || value.trim().length === 0) throw new ArkormException("Enum values must be provided as non-empty strings.");
812
+ const normalized = value.trim();
813
+ if (!PRISMA_ENUM_MEMBER_REGEX.test(normalized)) throw new ArkormException(`Enum value [${normalized}] is not a valid Prisma enum member name.`);
814
+ return normalized;
815
+ };
816
+ /**
817
+ * Extract the enum values from a Prisma enum block string.
818
+ *
819
+ * @param block
820
+ * @returns
821
+ */
822
+ const extractEnumBlockValues = (block) => {
823
+ return block.split("\n").slice(1, -1).map((line) => line.trim()).filter(Boolean);
824
+ };
825
+ const validateEnumValues = (column, enumName, enumValues) => {
826
+ const normalizedValues = enumValues.map(normalizeEnumValue);
827
+ const seen = /* @__PURE__ */ new Set();
828
+ for (const value of normalizedValues) {
829
+ if (seen.has(value)) throw new ArkormException(`Prisma enum [${enumName}] for column [${column.name}] contains duplicate value [${value}].`);
830
+ seen.add(value);
831
+ }
832
+ return normalizedValues;
833
+ };
834
+ /**
835
+ * Validate that a default value for an enum column is included in the defined enum values.
836
+ *
837
+ * @param column
838
+ * @param enumName
839
+ * @param enumValues
840
+ * @returns
841
+ */
842
+ const validateEnumDefaultValue = (column, enumName, enumValues) => {
843
+ if (column.default == null) return;
844
+ const normalizedDefault = normalizeEnumValue(column.default);
845
+ if (enumValues.includes(normalizedDefault)) return;
846
+ throw new ArkormException(`Enum default value [${normalizedDefault}] is not defined in Prisma enum [${enumName}] for column [${column.name}].`);
847
+ };
848
+ /**
650
849
  * Build a single line of a Prisma model field definition based on a SchemaColumn, including type and modifiers.
651
850
  *
652
851
  * @param column
@@ -667,11 +866,82 @@ const buildFieldLine = (column) => {
667
866
  const primary = column.primary ? " @id" : "";
668
867
  const mapped = typeof column.map === "string" && column.map.trim().length > 0 ? ` @map("${column.map.replace(/"/g, "\\\"")}")` : "";
669
868
  const updatedAt = column.updatedAt ? " @updatedAt" : "";
670
- const defaultValue = formatDefaultValue(column.default) ?? (column.type === "uuid" && column.primary ? "@default(uuid())" : void 0);
869
+ const defaultValue = column.type === "enum" ? formatEnumDefaultValue(column.default) : formatDefaultValue(column.default) ?? (column.type === "uuid" && column.primary ? "@default(uuid())" : void 0);
671
870
  const defaultSuffix = defaultValue ? ` ${defaultValue}` : "";
672
871
  return ` ${column.name} ${scalar}${nullable}${primary}${unique}${defaultSuffix}${updatedAt}${mapped}`;
673
872
  };
674
873
  /**
874
+ * Build a Prisma enum block string based on an enum name and its values, validating that
875
+ * at least one value is provided.
876
+ *
877
+ * @param enumName The name of the enum to create.
878
+ * @param values The array of values for the enum.
879
+ * @returns The Prisma enum block string.
880
+ */
881
+ const buildEnumBlock = (enumName, values) => {
882
+ if (values.length === 0) throw new ArkormException(`Enum [${enumName}] must define at least one value.`);
883
+ return `enum ${enumName} {\n${values.map((value) => ` ${value}`).join("\n")}\n}`;
884
+ };
885
+ /**
886
+ * Find the Prisma enum block in a schema string that corresponds to a given enum
887
+ * name, returning its details if found.
888
+ *
889
+ * @param schema The Prisma schema string to search for the enum block.
890
+ * @param enumName The name of the enum to find in the schema.
891
+ * @returns
892
+ */
893
+ const findEnumBlock = (schema, enumName) => {
894
+ const candidates = [...schema.matchAll(PRISMA_ENUM_REGEX)];
895
+ for (const match of candidates) {
896
+ const block = match[0];
897
+ const matchedEnumName = match[1];
898
+ const start = match.index ?? 0;
899
+ const end = start + block.length;
900
+ if (matchedEnumName === enumName) return {
901
+ enumName: matchedEnumName,
902
+ block,
903
+ start,
904
+ end
905
+ };
906
+ }
907
+ return null;
908
+ };
909
+ /**
910
+ * Ensure that Prisma enum blocks exist in the schema for any enum columns defined in a
911
+ * create or alter table operation, adding them if necessary and validating against
912
+ * existing blocks.
913
+ *
914
+ * @param schema The current Prisma schema string to check and modify.
915
+ * @param columns The array of schema column definitions to check for enum types and ensure corresponding blocks exist for.
916
+ * @returns
917
+ */
918
+ const ensureEnumBlocks = (schema, columns) => {
919
+ let nextSchema = schema;
920
+ for (const column of columns) {
921
+ if (column.type !== "enum") continue;
922
+ const enumName = resolveEnumName(column);
923
+ const enumValues = column.enumValues ?? [];
924
+ const existing = findEnumBlock(nextSchema, enumName);
925
+ if (existing) {
926
+ const existingValues = validateEnumValues(column, enumName, extractEnumBlockValues(existing.block));
927
+ if (enumValues.length === 0) {
928
+ validateEnumDefaultValue(column, enumName, existingValues);
929
+ continue;
930
+ }
931
+ const normalizedEnumValues = validateEnumValues(column, enumName, enumValues);
932
+ if (existingValues.join("|") !== normalizedEnumValues.join("|")) throw new ArkormException(`Prisma enum [${enumName}] already exists with different values.`);
933
+ validateEnumDefaultValue(column, enumName, existingValues);
934
+ continue;
935
+ }
936
+ if (enumValues.length === 0) throw new ArkormException(`Prisma enum [${enumName}] was not found for column [${column.name}].`);
937
+ const normalizedEnumValues = validateEnumValues(column, enumName, enumValues);
938
+ validateEnumDefaultValue(column, enumName, normalizedEnumValues);
939
+ const block = buildEnumBlock(enumName, normalizedEnumValues);
940
+ nextSchema = `${nextSchema.trimEnd()}\n\n${block}\n`;
941
+ }
942
+ return nextSchema;
943
+ };
944
+ /**
675
945
  * Build a Prisma model-level @@index definition line.
676
946
  *
677
947
  * @param index The schema index definition to convert to a Prisma \@\@index line.
@@ -697,24 +967,23 @@ const deriveRelationFieldName = (columnName) => {
697
967
  if (trimmed.endsWith("_id") && trimmed.length > 3) return trimmed.slice(0, -3).replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
698
968
  return `${trimmed.charAt(0).toLowerCase()}${trimmed.slice(1)}`;
699
969
  };
700
- const pascalWords = (value) => {
701
- return value.match(/[A-Z][a-z0-9]*/g) ?? [value];
702
- };
703
970
  /**
704
- * Derive a relation name for the inverse side of a relation based on the
971
+ * Derive a relation name for both sides of a relation based on the
705
972
  * source and target model names, using an explicit alias if provided or a
706
- * convention of combining the target model name with the last segment of
707
- * the source model name.
973
+ * convention of combining the full source model name with the target model name.
708
974
  *
709
975
  * @param sourceModelName The name of the source model in the relation.
710
976
  * @param targetModelName The name of the target model in the relation.
711
- * @param explicitAlias An optional explicit alias for the inverse relation.
712
- * @returns The derived or explicit inverse relation alias.
977
+ * @param explicitAlias An optional explicit alias for the relation.
978
+ * @returns The derived or explicit relation alias.
713
979
  */
714
- const deriveInverseRelationAlias = (sourceModelName, targetModelName, explicitAlias) => {
980
+ const deriveRelationAlias = (sourceModelName, targetModelName, explicitAlias) => {
715
981
  if (explicitAlias && explicitAlias.trim().length > 0) return explicitAlias.trim();
716
- const sourceWords = pascalWords(sourceModelName);
717
- return `${sourceWords[sourceWords.length - 1] ?? sourceModelName}${targetModelName}`;
982
+ return [sourceModelName, targetModelName].sort((left, right) => left.localeCompare(right)).join("");
983
+ };
984
+ const deriveSingularFieldName = (modelName) => {
985
+ if (!modelName) return "item";
986
+ return `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}`;
718
987
  };
719
988
  const deriveCollectionFieldName = (modelName) => {
720
989
  if (!modelName) return "items";
@@ -722,6 +991,12 @@ const deriveCollectionFieldName = (modelName) => {
722
991
  if (camel.endsWith("s")) return `${camel}es`;
723
992
  return `${camel}s`;
724
993
  };
994
+ const resolveForeignKeyColumn = (columns, foreignKey) => {
995
+ return columns.find((column) => column.name === foreignKey.column);
996
+ };
997
+ const isOneToOneForeignKey = (column) => {
998
+ return Boolean(column?.unique || column?.primary);
999
+ };
725
1000
  /**
726
1001
  * Format a SchemaForeignKeyAction value as a Prisma onDelete action string.
727
1002
  *
@@ -742,15 +1017,16 @@ const formatRelationAction = (action) => {
742
1017
  * @param foreignKey The foreign key definition to convert to a relation line.
743
1018
  * @returns The corresponding Prisma schema line for the relation field.
744
1019
  */
745
- const buildRelationLine = (foreignKey) => {
1020
+ const buildRelationLine = (sourceModelName, foreignKey, columns = []) => {
746
1021
  if (!foreignKey.referencesTable.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced table.`);
747
1022
  if (!foreignKey.referencesColumn.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced column.`);
1023
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
748
1024
  const fieldName = foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column);
749
1025
  const targetModel = toModelName(foreignKey.referencesTable);
750
- const relationName = foreignKey.relationAlias?.trim();
751
- const relationPrefix = relationName ? `@relation("${relationName.replace(/"/g, "\\\"")}", ` : "@relation(";
1026
+ const relationName = deriveRelationAlias(sourceModelName, targetModel, foreignKey.relationAlias?.trim());
1027
+ const optional = sourceColumn?.nullable ? "?" : "";
752
1028
  const onDelete = foreignKey.onDelete ? `, onDelete: ${formatRelationAction(foreignKey.onDelete)}` : "";
753
- return ` ${fieldName} ${targetModel} ${relationPrefix}fields: [${foreignKey.column}], references: [${foreignKey.referencesColumn}]${onDelete})`;
1029
+ return ` ${fieldName} ${targetModel}${optional} @relation("${relationName.replace(/"/g, "\\\"")}", fields: [${foreignKey.column}], references: [${foreignKey.referencesColumn}]${onDelete})`;
754
1030
  };
755
1031
  /**
756
1032
  * Build a Prisma relation field line for the inverse side of a relation, based
@@ -762,8 +1038,11 @@ const buildRelationLine = (foreignKey) => {
762
1038
  * @param foreignKey The foreign key definition for the relation.
763
1039
  * @returns The Prisma schema line for the inverse relation field.
764
1040
  */
765
- const buildInverseRelationLine = (sourceModelName, targetModelName, foreignKey) => {
766
- return ` ${deriveCollectionFieldName(sourceModelName)} ${sourceModelName}[] @relation("${deriveInverseRelationAlias(sourceModelName, targetModelName, foreignKey.inverseRelationAlias).replace(/"/g, "\\\"")}")`;
1041
+ const buildInverseRelationLine = (sourceModelName, targetModelName, foreignKey, columns = []) => {
1042
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1043
+ const fieldName = isOneToOneForeignKey(sourceColumn) ? deriveSingularFieldName(sourceModelName) : deriveCollectionFieldName(sourceModelName);
1044
+ const relationName = deriveRelationAlias(sourceModelName, targetModelName, foreignKey.relationAlias?.trim());
1045
+ return ` ${fieldName} ${isOneToOneForeignKey(sourceColumn) ? `${sourceModelName}?` : `${sourceModelName}[]`} @relation("${relationName.replace(/"/g, "\\\"")}")`;
767
1046
  };
768
1047
  /**
769
1048
  * Inject a line into the body of a Prisma model block if it does not already
@@ -791,14 +1070,15 @@ const injectLineIntoModelBody = (bodyLines, line, exists) => {
791
1070
  * @param foreignKeys An array of foreign key definitions to process.
792
1071
  * @returns The updated Prisma schema string with inverse relations applied.
793
1072
  */
794
- const applyInverseRelations = (schema, sourceModelName, foreignKeys) => {
1073
+ const applyInverseRelations = (schema, sourceModelName, foreignKeys, columns = []) => {
795
1074
  let nextSchema = schema;
796
1075
  for (const foreignKey of foreignKeys) {
797
1076
  const targetModel = findModelBlock(nextSchema, foreignKey.referencesTable);
798
1077
  if (!targetModel) continue;
799
- const inverseLine = buildInverseRelationLine(sourceModelName, targetModel.modelName, foreignKey);
1078
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1079
+ const inverseLine = buildInverseRelationLine(sourceModelName, targetModel.modelName, foreignKey, columns);
800
1080
  const targetBodyLines = targetModel.block.split("\n");
801
- const fieldName = deriveCollectionFieldName(sourceModelName);
1081
+ const fieldName = isOneToOneForeignKey(sourceColumn) ? deriveSingularFieldName(sourceModelName) : deriveCollectionFieldName(sourceModelName);
802
1082
  const fieldRegex = new RegExp(`^\\s*${escapeRegex(fieldName)}\\s+`);
803
1083
  injectLineIntoModelBody(targetBodyLines, inverseLine, (line) => fieldRegex.test(line));
804
1084
  const updatedTarget = targetBodyLines.join("\n");
@@ -817,7 +1097,7 @@ const buildModelBlock = (operation) => {
817
1097
  const modelName = toModelName(operation.table);
818
1098
  const mapped = operation.table !== modelName.toLowerCase();
819
1099
  const fields = operation.columns.map(buildFieldLine);
820
- const relations = (operation.foreignKeys ?? []).map(buildRelationLine);
1100
+ const relations = (operation.foreignKeys ?? []).map((foreignKey) => buildRelationLine(modelName, foreignKey, operation.columns));
821
1101
  const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${str(operation.table).snake()}")`] : []];
822
1102
  return `model ${modelName} {\n${(metadata.length > 0 ? [
823
1103
  ...fields,
@@ -873,8 +1153,9 @@ const findModelBlock = (schema, table) => {
873
1153
  */
874
1154
  const applyCreateTableOperation = (schema, operation) => {
875
1155
  if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
1156
+ const schemaWithEnums = ensureEnumBlocks(schema, operation.columns);
876
1157
  const block = buildModelBlock(operation);
877
- return applyInverseRelations(`${schema.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? []);
1158
+ return applyInverseRelations(`${schemaWithEnums.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? [], operation.columns);
878
1159
  };
879
1160
  /**
880
1161
  * Apply an alter table operation to a Prisma schema string, modifying the model
@@ -887,7 +1168,10 @@ const applyCreateTableOperation = (schema, operation) => {
887
1168
  const applyAlterTableOperation = (schema, operation) => {
888
1169
  const model = findModelBlock(schema, operation.table);
889
1170
  if (!model) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
890
- let block = model.block;
1171
+ const schemaWithEnums = ensureEnumBlocks(schema, operation.addColumns);
1172
+ const refreshedModel = findModelBlock(schemaWithEnums, operation.table);
1173
+ if (!refreshedModel) throw new ArkormException(`Prisma model for table [${operation.table}] was not found.`);
1174
+ let block = refreshedModel.block;
891
1175
  const bodyLines = block.split("\n");
892
1176
  operation.dropColumns.forEach((column) => {
893
1177
  const columnRegex = new RegExp(`^\\s*${escapeRegex(column)}\\s+`);
@@ -912,12 +1196,12 @@ const applyAlterTableOperation = (schema, operation) => {
912
1196
  bodyLines.splice(insertIndex, 0, indexLine);
913
1197
  });
914
1198
  for (const foreignKey of operation.addForeignKeys ?? []) {
915
- const relationLine = buildRelationLine(foreignKey);
1199
+ const relationLine = buildRelationLine(model.modelName, foreignKey, operation.addColumns);
916
1200
  const relationRegex = new RegExp(`^\\s*${escapeRegex(foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column))}\\s+`);
917
1201
  injectLineIntoModelBody(bodyLines, relationLine, (line) => relationRegex.test(line));
918
1202
  }
919
1203
  block = bodyLines.join("\n");
920
- return applyInverseRelations(`${schema.slice(0, model.start)}${block}${schema.slice(model.end)}`, model.modelName, operation.addForeignKeys ?? []);
1204
+ return applyInverseRelations(`${schemaWithEnums.slice(0, refreshedModel.start)}${block}${schemaWithEnums.slice(refreshedModel.end)}`, model.modelName, operation.addForeignKeys ?? [], operation.addColumns);
921
1205
  };
922
1206
  /**
923
1207
  * Apply a drop table operation to a Prisma schema string, removing the model block