arkormx 0.2.1 → 0.2.3
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 +269 -12
- package/dist/index.cjs +276 -12
- package/dist/index.d.cts +149 -6
- package/dist/index.d.mts +149 -6
- package/dist/index.mjs +270 -13
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -26,6 +26,79 @@ var ArkormException = class extends Error {
|
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/database/ForeignKeyBuilder.ts
|
|
31
|
+
/**
|
|
32
|
+
* The ForeignKeyBuilder class provides a fluent interface for defining
|
|
33
|
+
* foreign key constraints in a migration. It allows you to specify
|
|
34
|
+
* the referenced table and column, as well as actions to take on
|
|
35
|
+
* delete and aliases for the relation.
|
|
36
|
+
*
|
|
37
|
+
* @author Legacy (3m1n3nc3)
|
|
38
|
+
* @since 0.2.2
|
|
39
|
+
*/
|
|
40
|
+
var ForeignKeyBuilder = class {
|
|
41
|
+
foreignKey;
|
|
42
|
+
constructor(foreignKey) {
|
|
43
|
+
this.foreignKey = foreignKey;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Defines the referenced table and column for this foreign key constraint.
|
|
47
|
+
*
|
|
48
|
+
* @param table
|
|
49
|
+
* @param column
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
references(table, column) {
|
|
53
|
+
this.foreignKey.referencesTable = table;
|
|
54
|
+
this.foreignKey.referencesColumn = column;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Defines the action to take when a referenced record is deleted, such
|
|
59
|
+
* as "CASCADE", "SET NULL", or "RESTRICT".
|
|
60
|
+
*
|
|
61
|
+
* @param action
|
|
62
|
+
* @returns
|
|
63
|
+
*/
|
|
64
|
+
onDelete(action) {
|
|
65
|
+
this.foreignKey.onDelete = action;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Defines an alias for the relation represented by this foreign key, which
|
|
70
|
+
* can be used in the ORM for more intuitive access to related models.
|
|
71
|
+
*
|
|
72
|
+
* @param name
|
|
73
|
+
* @returns
|
|
74
|
+
*/
|
|
75
|
+
alias(name) {
|
|
76
|
+
this.foreignKey.relationAlias = name;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Defines an alias for the inverse relation represented by this foreign key.
|
|
81
|
+
*
|
|
82
|
+
* @param name
|
|
83
|
+
* @returns
|
|
84
|
+
*/
|
|
85
|
+
inverseAlias(name) {
|
|
86
|
+
this.foreignKey.inverseRelationAlias = name;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Defines an alias for the foreign key field itself, which can be
|
|
91
|
+
* used in the ORM for more intuitive access to the foreign key value.
|
|
92
|
+
*
|
|
93
|
+
* @param fieldName
|
|
94
|
+
* @returns
|
|
95
|
+
*/
|
|
96
|
+
as(fieldName) {
|
|
97
|
+
this.foreignKey.fieldAlias = fieldName;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
29
102
|
//#endregion
|
|
30
103
|
//#region src/database/TableBuilder.ts
|
|
31
104
|
/**
|
|
@@ -39,6 +112,7 @@ var TableBuilder = class {
|
|
|
39
112
|
columns = [];
|
|
40
113
|
dropColumnNames = [];
|
|
41
114
|
indexes = [];
|
|
115
|
+
foreignKeys = [];
|
|
42
116
|
latestColumnName;
|
|
43
117
|
/**
|
|
44
118
|
* Defines a primary key column in the table.
|
|
@@ -103,7 +177,7 @@ var TableBuilder = class {
|
|
|
103
177
|
*
|
|
104
178
|
* @param name The name of the integer column.
|
|
105
179
|
* @param options Additional options for the integer column.
|
|
106
|
-
* @returns
|
|
180
|
+
* @returns The current TableBuilder instance for chaining.
|
|
107
181
|
*/
|
|
108
182
|
integer(name, options = {}) {
|
|
109
183
|
return this.column(name, "integer", options);
|
|
@@ -113,7 +187,7 @@ var TableBuilder = class {
|
|
|
113
187
|
*
|
|
114
188
|
* @param name The name of the big integer column.
|
|
115
189
|
* @param options Additional options for the big integer column.
|
|
116
|
-
* @returns
|
|
190
|
+
* @returns The current TableBuilder instance for chaining.
|
|
117
191
|
*/
|
|
118
192
|
bigInteger(name, options = {}) {
|
|
119
193
|
return this.column(name, "bigInteger", options);
|
|
@@ -123,17 +197,29 @@ var TableBuilder = class {
|
|
|
123
197
|
*
|
|
124
198
|
* @param name The name of the float column.
|
|
125
199
|
* @param options Additional options for the float column.
|
|
126
|
-
* @returns
|
|
200
|
+
* @returns The current TableBuilder instance for chaining.
|
|
127
201
|
*/
|
|
128
202
|
float(name, options = {}) {
|
|
129
203
|
return this.column(name, "float", options);
|
|
130
204
|
}
|
|
131
205
|
/**
|
|
206
|
+
* Marks a column as unique in the table.
|
|
207
|
+
*
|
|
208
|
+
* @param name Optional explicit column name.
|
|
209
|
+
* When omitted, applies to the latest defined column.
|
|
210
|
+
* @returns The current TableBuilder instance for chaining.
|
|
211
|
+
*/
|
|
212
|
+
unique(name) {
|
|
213
|
+
const column = this.resolveColumn(name);
|
|
214
|
+
column.unique = true;
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
132
218
|
* Defines a boolean column in the table.
|
|
133
219
|
*
|
|
134
220
|
* @param name The name of the boolean column.
|
|
135
221
|
* @param options Additional options for the boolean column.
|
|
136
|
-
* @returns
|
|
222
|
+
* @returns The current TableBuilder instance for chaining.
|
|
137
223
|
*/
|
|
138
224
|
boolean(name, options = {}) {
|
|
139
225
|
return this.column(name, "boolean", options);
|
|
@@ -269,6 +355,32 @@ var TableBuilder = class {
|
|
|
269
355
|
return this;
|
|
270
356
|
}
|
|
271
357
|
/**
|
|
358
|
+
* Defines a foreign key relation for an existing column.
|
|
359
|
+
*
|
|
360
|
+
* @param column The local foreign key column name.
|
|
361
|
+
* @returns A fluent foreign key builder.
|
|
362
|
+
*/
|
|
363
|
+
foreignKey(column) {
|
|
364
|
+
const entry = {
|
|
365
|
+
column,
|
|
366
|
+
referencesTable: "",
|
|
367
|
+
referencesColumn: "id"
|
|
368
|
+
};
|
|
369
|
+
this.foreignKeys.push(entry);
|
|
370
|
+
return new ForeignKeyBuilder(entry);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Defines a foreign key relation for a column, using a
|
|
374
|
+
* conventional naming pattern.
|
|
375
|
+
*
|
|
376
|
+
* @param column
|
|
377
|
+
* @returns
|
|
378
|
+
*/
|
|
379
|
+
foreign(column) {
|
|
380
|
+
const columnName = this.resolveColumn(column).name;
|
|
381
|
+
return this.foreignKey(columnName + (column ? "" : "Id"));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
272
384
|
* Returns a deep copy of the defined columns for the table.
|
|
273
385
|
*
|
|
274
386
|
* @returns
|
|
@@ -296,6 +408,14 @@ var TableBuilder = class {
|
|
|
296
408
|
}));
|
|
297
409
|
}
|
|
298
410
|
/**
|
|
411
|
+
* Returns a deep copy of the defined foreign keys for the table.
|
|
412
|
+
*
|
|
413
|
+
* @returns
|
|
414
|
+
*/
|
|
415
|
+
getForeignKeys() {
|
|
416
|
+
return this.foreignKeys.map((foreignKey) => ({ ...foreignKey }));
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
299
419
|
* Defines a column in the table with the given name.
|
|
300
420
|
*
|
|
301
421
|
* @param name The name of the column.
|
|
@@ -358,7 +478,8 @@ var SchemaBuilder = class {
|
|
|
358
478
|
type: "createTable",
|
|
359
479
|
table,
|
|
360
480
|
columns: builder.getColumns(),
|
|
361
|
-
indexes: builder.getIndexes()
|
|
481
|
+
indexes: builder.getIndexes(),
|
|
482
|
+
foreignKeys: builder.getForeignKeys()
|
|
362
483
|
});
|
|
363
484
|
return this;
|
|
364
485
|
}
|
|
@@ -377,7 +498,8 @@ var SchemaBuilder = class {
|
|
|
377
498
|
table,
|
|
378
499
|
addColumns: builder.getColumns(),
|
|
379
500
|
dropColumns: builder.getDropColumns(),
|
|
380
|
-
addIndexes: builder.getIndexes()
|
|
501
|
+
addIndexes: builder.getIndexes(),
|
|
502
|
+
addForeignKeys: builder.getForeignKeys()
|
|
381
503
|
});
|
|
382
504
|
return this;
|
|
383
505
|
}
|
|
@@ -407,7 +529,8 @@ var SchemaBuilder = class {
|
|
|
407
529
|
indexes: operation.indexes.map((index) => ({
|
|
408
530
|
...index,
|
|
409
531
|
columns: [...index.columns]
|
|
410
|
-
}))
|
|
532
|
+
})),
|
|
533
|
+
foreignKeys: operation.foreignKeys.map((foreignKey) => ({ ...foreignKey }))
|
|
411
534
|
};
|
|
412
535
|
if (operation.type === "alterTable") return {
|
|
413
536
|
...operation,
|
|
@@ -416,7 +539,8 @@ var SchemaBuilder = class {
|
|
|
416
539
|
addIndexes: operation.addIndexes.map((index) => ({
|
|
417
540
|
...index,
|
|
418
541
|
columns: [...index.columns]
|
|
419
|
-
}))
|
|
542
|
+
})),
|
|
543
|
+
addForeignKeys: operation.addForeignKeys.map((foreignKey) => ({ ...foreignKey }))
|
|
420
544
|
};
|
|
421
545
|
return { ...operation };
|
|
422
546
|
});
|
|
@@ -501,13 +625,139 @@ const buildFieldLine = (column) => {
|
|
|
501
625
|
/**
|
|
502
626
|
* Build a Prisma model-level @@index definition line.
|
|
503
627
|
*
|
|
504
|
-
* @param index
|
|
628
|
+
* @param index The schema index definition to convert to a Prisma \@\@index line.
|
|
505
629
|
* @returns
|
|
506
630
|
*/
|
|
507
631
|
const buildIndexLine = (index) => {
|
|
508
632
|
return ` @@index([${index.columns.join(", ")}]${typeof index.name === "string" && index.name.trim().length > 0 ? `, name: "${index.name.replace(/"/g, "\\\"")}"` : ""})`;
|
|
509
633
|
};
|
|
510
634
|
/**
|
|
635
|
+
* Derive a relation field name from a foreign key column name by applying
|
|
636
|
+
* common conventions, such as removing "Id" suffixes and converting to camelCase.
|
|
637
|
+
*
|
|
638
|
+
* @param columnName The name of the foreign key column.
|
|
639
|
+
* @returns The derived relation field name.
|
|
640
|
+
*/
|
|
641
|
+
const deriveRelationFieldName = (columnName) => {
|
|
642
|
+
const trimmed = columnName.trim();
|
|
643
|
+
if (!trimmed) return "relation";
|
|
644
|
+
if (trimmed.endsWith("Id") && trimmed.length > 2) {
|
|
645
|
+
const root = trimmed.slice(0, -2);
|
|
646
|
+
return `${root.charAt(0).toLowerCase()}${root.slice(1)}`;
|
|
647
|
+
}
|
|
648
|
+
if (trimmed.endsWith("_id") && trimmed.length > 3) return trimmed.slice(0, -3).replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
649
|
+
return `${trimmed.charAt(0).toLowerCase()}${trimmed.slice(1)}`;
|
|
650
|
+
};
|
|
651
|
+
const pascalWords = (value) => {
|
|
652
|
+
return value.match(/[A-Z][a-z0-9]*/g) ?? [value];
|
|
653
|
+
};
|
|
654
|
+
/**
|
|
655
|
+
* Derive a relation name for the inverse side of a relation based on the
|
|
656
|
+
* source and target model names, using an explicit alias if provided or a
|
|
657
|
+
* convention of combining the target model name with the last segment of
|
|
658
|
+
* the source model name.
|
|
659
|
+
*
|
|
660
|
+
* @param sourceModelName The name of the source model in the relation.
|
|
661
|
+
* @param targetModelName The name of the target model in the relation.
|
|
662
|
+
* @param explicitAlias An optional explicit alias for the inverse relation.
|
|
663
|
+
* @returns The derived or explicit inverse relation alias.
|
|
664
|
+
*/
|
|
665
|
+
const deriveInverseRelationAlias = (sourceModelName, targetModelName, explicitAlias) => {
|
|
666
|
+
if (explicitAlias && explicitAlias.trim().length > 0) return explicitAlias.trim();
|
|
667
|
+
const sourceWords = pascalWords(sourceModelName);
|
|
668
|
+
return `${sourceWords[sourceWords.length - 1] ?? sourceModelName}${targetModelName}`;
|
|
669
|
+
};
|
|
670
|
+
const deriveCollectionFieldName = (modelName) => {
|
|
671
|
+
if (!modelName) return "items";
|
|
672
|
+
const camel = `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}`;
|
|
673
|
+
if (camel.endsWith("s")) return `${camel}es`;
|
|
674
|
+
return `${camel}s`;
|
|
675
|
+
};
|
|
676
|
+
/**
|
|
677
|
+
* Format a SchemaForeignKeyAction value as a Prisma onDelete action string.
|
|
678
|
+
*
|
|
679
|
+
* @param action The foreign key action to format.
|
|
680
|
+
* @returns The corresponding Prisma onDelete action string.
|
|
681
|
+
*/
|
|
682
|
+
const formatRelationAction = (action) => {
|
|
683
|
+
if (action === "cascade") return "Cascade";
|
|
684
|
+
if (action === "restrict") return "Restrict";
|
|
685
|
+
if (action === "setNull") return "SetNull";
|
|
686
|
+
if (action === "setDefault") return "SetDefault";
|
|
687
|
+
return "NoAction";
|
|
688
|
+
};
|
|
689
|
+
/**
|
|
690
|
+
* Build a Prisma relation field line based on a SchemaForeignKey
|
|
691
|
+
* definition, including relation name and onDelete action.
|
|
692
|
+
*
|
|
693
|
+
* @param foreignKey The foreign key definition to convert to a relation line.
|
|
694
|
+
* @returns The corresponding Prisma schema line for the relation field.
|
|
695
|
+
*/
|
|
696
|
+
const buildRelationLine = (foreignKey) => {
|
|
697
|
+
if (!foreignKey.referencesTable.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced table.`);
|
|
698
|
+
if (!foreignKey.referencesColumn.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced column.`);
|
|
699
|
+
const fieldName = foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column);
|
|
700
|
+
const targetModel = toModelName(foreignKey.referencesTable);
|
|
701
|
+
const relationName = foreignKey.relationAlias?.trim();
|
|
702
|
+
const relationPrefix = relationName ? `@relation("${relationName.replace(/"/g, "\\\"")}", ` : "@relation(";
|
|
703
|
+
const onDelete = foreignKey.onDelete ? `, onDelete: ${formatRelationAction(foreignKey.onDelete)}` : "";
|
|
704
|
+
return ` ${fieldName} ${targetModel} ${relationPrefix}fields: [${foreignKey.column}], references: [${foreignKey.referencesColumn}]${onDelete})`;
|
|
705
|
+
};
|
|
706
|
+
/**
|
|
707
|
+
* Build a Prisma relation field line for the inverse side of a relation, based
|
|
708
|
+
* on the source and target model names and the foreign key definition, using
|
|
709
|
+
* naming conventions and any explicit inverse alias provided.
|
|
710
|
+
*
|
|
711
|
+
* @param sourceModelName The name of the source model in the relation.
|
|
712
|
+
* @param targetModelName The name of the target model in the relation.
|
|
713
|
+
* @param foreignKey The foreign key definition for the relation.
|
|
714
|
+
* @returns The Prisma schema line for the inverse relation field.
|
|
715
|
+
*/
|
|
716
|
+
const buildInverseRelationLine = (sourceModelName, targetModelName, foreignKey) => {
|
|
717
|
+
return ` ${deriveCollectionFieldName(sourceModelName)} ${sourceModelName}[] @relation("${deriveInverseRelationAlias(sourceModelName, targetModelName, foreignKey.inverseRelationAlias).replace(/"/g, "\\\"")}")`;
|
|
718
|
+
};
|
|
719
|
+
/**
|
|
720
|
+
* Inject a line into the body of a Prisma model block if it does not already
|
|
721
|
+
* exist, using a provided existence check function to determine if the line
|
|
722
|
+
* is already present.
|
|
723
|
+
*
|
|
724
|
+
* @param bodyLines The lines of the model block body to modify.
|
|
725
|
+
* @param line The line to inject if it does not already exist.
|
|
726
|
+
* @param exists A function that checks if a given line already exists in the body.
|
|
727
|
+
* @returns
|
|
728
|
+
*/
|
|
729
|
+
const injectLineIntoModelBody = (bodyLines, line, exists) => {
|
|
730
|
+
if (bodyLines.some(exists)) return bodyLines;
|
|
731
|
+
const insertIndex = Math.max(1, bodyLines.length - 1);
|
|
732
|
+
bodyLines.splice(insertIndex, 0, line);
|
|
733
|
+
return bodyLines;
|
|
734
|
+
};
|
|
735
|
+
/**
|
|
736
|
+
* Apply inverse relation definitions to a Prisma schema string based on the
|
|
737
|
+
* foreign keys defined in a create or alter table operation, ensuring that
|
|
738
|
+
* related models have corresponding relation fields for bi-directional navigation.
|
|
739
|
+
*
|
|
740
|
+
* @param schema The Prisma schema string to modify.
|
|
741
|
+
* @param sourceModelName The name of the source model in the relation.
|
|
742
|
+
* @param foreignKeys An array of foreign key definitions to process.
|
|
743
|
+
* @returns The updated Prisma schema string with inverse relations applied.
|
|
744
|
+
*/
|
|
745
|
+
const applyInverseRelations = (schema, sourceModelName, foreignKeys) => {
|
|
746
|
+
let nextSchema = schema;
|
|
747
|
+
for (const foreignKey of foreignKeys) {
|
|
748
|
+
const targetModel = findModelBlock(nextSchema, foreignKey.referencesTable);
|
|
749
|
+
if (!targetModel) continue;
|
|
750
|
+
const inverseLine = buildInverseRelationLine(sourceModelName, targetModel.modelName, foreignKey);
|
|
751
|
+
const targetBodyLines = targetModel.block.split("\n");
|
|
752
|
+
const fieldName = deriveCollectionFieldName(sourceModelName);
|
|
753
|
+
const fieldRegex = new RegExp(`^\\s*${escapeRegex(fieldName)}\\s+`);
|
|
754
|
+
injectLineIntoModelBody(targetBodyLines, inverseLine, (line) => fieldRegex.test(line));
|
|
755
|
+
const updatedTarget = targetBodyLines.join("\n");
|
|
756
|
+
nextSchema = `${nextSchema.slice(0, targetModel.start)}${updatedTarget}${nextSchema.slice(targetModel.end)}`;
|
|
757
|
+
}
|
|
758
|
+
return nextSchema;
|
|
759
|
+
};
|
|
760
|
+
/**
|
|
511
761
|
* Build a Prisma model block string based on a SchemaTableCreateOperation, including
|
|
512
762
|
* all fields and any necessary mapping.
|
|
513
763
|
*
|
|
@@ -518,12 +768,14 @@ const buildModelBlock = (operation) => {
|
|
|
518
768
|
const modelName = toModelName(operation.table);
|
|
519
769
|
const mapped = operation.table !== modelName.toLowerCase();
|
|
520
770
|
const fields = operation.columns.map(buildFieldLine);
|
|
771
|
+
const relations = (operation.foreignKeys ?? []).map(buildRelationLine);
|
|
521
772
|
const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${str(operation.table).snake()}")`] : []];
|
|
522
773
|
return `model ${modelName} {\n${(metadata.length > 0 ? [
|
|
523
774
|
...fields,
|
|
775
|
+
...relations,
|
|
524
776
|
"",
|
|
525
777
|
...metadata
|
|
526
|
-
] : fields).join("\n")}\n}`;
|
|
778
|
+
] : [...fields, ...relations]).join("\n")}\n}`;
|
|
527
779
|
};
|
|
528
780
|
/**
|
|
529
781
|
* Find the Prisma model block in a schema string that corresponds to a given
|
|
@@ -573,7 +825,7 @@ const findModelBlock = (schema, table) => {
|
|
|
573
825
|
const applyCreateTableOperation = (schema, operation) => {
|
|
574
826
|
if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
|
|
575
827
|
const block = buildModelBlock(operation);
|
|
576
|
-
return `${schema.trimEnd()}\n\n${block}\n
|
|
828
|
+
return applyInverseRelations(`${schema.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? []);
|
|
577
829
|
};
|
|
578
830
|
/**
|
|
579
831
|
* Apply an alter table operation to a Prisma schema string, modifying the model
|
|
@@ -610,8 +862,13 @@ const applyAlterTableOperation = (schema, operation) => {
|
|
|
610
862
|
const insertIndex = Math.max(1, bodyLines.length - 1);
|
|
611
863
|
bodyLines.splice(insertIndex, 0, indexLine);
|
|
612
864
|
});
|
|
865
|
+
for (const foreignKey of operation.addForeignKeys ?? []) {
|
|
866
|
+
const relationLine = buildRelationLine(foreignKey);
|
|
867
|
+
const relationRegex = new RegExp(`^\\s*${escapeRegex(foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column))}\\s+`);
|
|
868
|
+
injectLineIntoModelBody(bodyLines, relationLine, (line) => relationRegex.test(line));
|
|
869
|
+
}
|
|
613
870
|
block = bodyLines.join("\n");
|
|
614
|
-
return `${schema.slice(0, model.start)}${block}${schema.slice(model.end)}
|
|
871
|
+
return applyInverseRelations(`${schema.slice(0, model.start)}${block}${schema.slice(model.end)}`, model.modelName, operation.addForeignKeys ?? []);
|
|
615
872
|
};
|
|
616
873
|
/**
|
|
617
874
|
* Apply a drop table operation to a Prisma schema string, removing the model block
|