arkormx 0.2.2 → 0.2.4

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
@@ -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.
@@ -281,6 +355,32 @@ var TableBuilder = class {
281
355
  return this;
282
356
  }
283
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
+ /**
284
384
  * Returns a deep copy of the defined columns for the table.
285
385
  *
286
386
  * @returns
@@ -308,6 +408,14 @@ var TableBuilder = class {
308
408
  }));
309
409
  }
310
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
+ /**
311
419
  * Defines a column in the table with the given name.
312
420
  *
313
421
  * @param name The name of the column.
@@ -370,7 +478,8 @@ var SchemaBuilder = class {
370
478
  type: "createTable",
371
479
  table,
372
480
  columns: builder.getColumns(),
373
- indexes: builder.getIndexes()
481
+ indexes: builder.getIndexes(),
482
+ foreignKeys: builder.getForeignKeys()
374
483
  });
375
484
  return this;
376
485
  }
@@ -389,7 +498,8 @@ var SchemaBuilder = class {
389
498
  table,
390
499
  addColumns: builder.getColumns(),
391
500
  dropColumns: builder.getDropColumns(),
392
- addIndexes: builder.getIndexes()
501
+ addIndexes: builder.getIndexes(),
502
+ addForeignKeys: builder.getForeignKeys()
393
503
  });
394
504
  return this;
395
505
  }
@@ -419,7 +529,8 @@ var SchemaBuilder = class {
419
529
  indexes: operation.indexes.map((index) => ({
420
530
  ...index,
421
531
  columns: [...index.columns]
422
- }))
532
+ })),
533
+ foreignKeys: operation.foreignKeys.map((foreignKey) => ({ ...foreignKey }))
423
534
  };
424
535
  if (operation.type === "alterTable") return {
425
536
  ...operation,
@@ -428,7 +539,8 @@ var SchemaBuilder = class {
428
539
  addIndexes: operation.addIndexes.map((index) => ({
429
540
  ...index,
430
541
  columns: [...index.columns]
431
- }))
542
+ })),
543
+ addForeignKeys: operation.addForeignKeys.map((foreignKey) => ({ ...foreignKey }))
432
544
  };
433
545
  return { ...operation };
434
546
  });
@@ -513,13 +625,139 @@ const buildFieldLine = (column) => {
513
625
  /**
514
626
  * Build a Prisma model-level @@index definition line.
515
627
  *
516
- * @param index
628
+ * @param index The schema index definition to convert to a Prisma \@\@index line.
517
629
  * @returns
518
630
  */
519
631
  const buildIndexLine = (index) => {
520
632
  return ` @@index([${index.columns.join(", ")}]${typeof index.name === "string" && index.name.trim().length > 0 ? `, name: "${index.name.replace(/"/g, "\\\"")}"` : ""})`;
521
633
  };
522
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
+ /**
523
761
  * Build a Prisma model block string based on a SchemaTableCreateOperation, including
524
762
  * all fields and any necessary mapping.
525
763
  *
@@ -530,12 +768,14 @@ const buildModelBlock = (operation) => {
530
768
  const modelName = toModelName(operation.table);
531
769
  const mapped = operation.table !== modelName.toLowerCase();
532
770
  const fields = operation.columns.map(buildFieldLine);
771
+ const relations = (operation.foreignKeys ?? []).map(buildRelationLine);
533
772
  const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${str(operation.table).snake()}")`] : []];
534
773
  return `model ${modelName} {\n${(metadata.length > 0 ? [
535
774
  ...fields,
775
+ ...relations,
536
776
  "",
537
777
  ...metadata
538
- ] : fields).join("\n")}\n}`;
778
+ ] : [...fields, ...relations]).join("\n")}\n}`;
539
779
  };
540
780
  /**
541
781
  * Find the Prisma model block in a schema string that corresponds to a given
@@ -585,7 +825,7 @@ const findModelBlock = (schema, table) => {
585
825
  const applyCreateTableOperation = (schema, operation) => {
586
826
  if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
587
827
  const block = buildModelBlock(operation);
588
- return `${schema.trimEnd()}\n\n${block}\n`;
828
+ return applyInverseRelations(`${schema.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? []);
589
829
  };
590
830
  /**
591
831
  * Apply an alter table operation to a Prisma schema string, modifying the model
@@ -622,8 +862,13 @@ const applyAlterTableOperation = (schema, operation) => {
622
862
  const insertIndex = Math.max(1, bodyLines.length - 1);
623
863
  bodyLines.splice(insertIndex, 0, indexLine);
624
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
+ }
625
870
  block = bodyLines.join("\n");
626
- 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 ?? []);
627
872
  };
628
873
  /**
629
874
  * Apply a drop table operation to a Prisma schema string, removing the model block
@@ -1628,7 +1873,7 @@ var MigrateCommand = class extends Command {
1628
1873
  const migrationsDir = this.app.resolveRuntimeDirectoryPath(configuredMigrationsDir);
1629
1874
  if (!existsSync$1(migrationsDir)) return void this.error(`Error: Migrations directory not found: ${this.app.formatPathForLog(configuredMigrationsDir)}`);
1630
1875
  const schemaPath = this.option("schema") ? resolve(String(this.option("schema"))) : join$1(process.cwd(), "prisma", "schema.prisma");
1631
- const classes = this.option("all") ? await this.loadAllMigrations(migrationsDir) : (await this.loadNamedMigration(migrationsDir, this.argument("name"))).filter(([cls]) => cls !== void 0);
1876
+ const classes = this.option("all") || !this.argument("name") ? await this.loadAllMigrations(migrationsDir) : (await this.loadNamedMigration(migrationsDir, this.argument("name"))).filter(([cls]) => cls !== void 0);
1632
1877
  if (classes.length === 0) return void this.error("Error: No migration classes found to run.");
1633
1878
  for (const [MigrationClassItem] of classes) await applyMigrationToPrismaSchema(MigrationClassItem, {
1634
1879
  schemaPath,