pecunia-cli 0.2.6 → 0.2.8

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/api.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { a as generateKyselySchema, n as generateSchema, o as generateDrizzleSchema, r as generatePrismaSchema, t as adapters } from "./generators-BVZYl7Wb.mjs";
1
+ import { a as generateKyselySchema, n as generateSchema, o as generateDrizzleSchema, r as generatePrismaSchema, t as adapters } from "./generators-BXA4y5O8.mjs";
2
2
 
3
3
  export { adapters, generateDrizzleSchema, generateKyselySchema, generatePrismaSchema, generateSchema };
@@ -325,29 +325,115 @@ async function getDrizzleMigrationsDir(projectRoot) {
325
325
  return drizzleDir;
326
326
  }
327
327
  /**
328
- * Find existing invariants migration file if it exists.
328
+ * Get the path to the Drizzle meta directory.
329
+ */
330
+ function getDrizzleMetaDir(projectRoot) {
331
+ return path.resolve(projectRoot, "drizzle", "meta");
332
+ }
333
+ /**
334
+ * Get the path to the Drizzle journal file.
335
+ */
336
+ function getJournalPath(projectRoot) {
337
+ return path.resolve(projectRoot, "drizzle", "meta", "_journal.json");
338
+ }
339
+ /**
340
+ * Read the Drizzle journal file, or return null if it doesn't exist.
341
+ */
342
+ async function readJournal(projectRoot) {
343
+ const journalPath = getJournalPath(projectRoot);
344
+ if (!existsSync(journalPath)) return null;
345
+ const content = await fs$1.readFile(journalPath, "utf-8");
346
+ return JSON.parse(content);
347
+ }
348
+ /**
349
+ * Write the Drizzle journal file with stable JSON formatting (2-space indent).
350
+ */
351
+ async function writeJournal(projectRoot, journal) {
352
+ const journalPath = getJournalPath(projectRoot);
353
+ await fs$1.writeFile(journalPath, JSON.stringify(journal, null, 2) + "\n", "utf-8");
354
+ }
355
+ /**
356
+ * Ensure the Drizzle journal exists. If it doesn't exist, create it with a baseline init migration.
329
357
  *
330
- * @param migrationsDir - Path to the drizzle migrations directory
331
- * @returns The migration number if found, or null
358
+ * @param projectRoot - The project root directory
359
+ * @returns true if journal was created, false if it already existed
360
+ */
361
+ async function ensureDrizzleJournal(projectRoot) {
362
+ if (existsSync(getJournalPath(projectRoot))) return false;
363
+ const metaDir = getDrizzleMetaDir(projectRoot);
364
+ await fs$1.mkdir(metaDir, { recursive: true });
365
+ await writeJournal(projectRoot, {
366
+ version: "7",
367
+ dialect: "postgresql",
368
+ entries: []
369
+ });
370
+ const migrationsDir = await getDrizzleMigrationsDir(projectRoot);
371
+ const initMigrationPath = path.join(migrationsDir, "0000_pecunia_init.sql");
372
+ await fs$1.writeFile(initMigrationPath, "-- Initial no-op migration created by pecunia-cli\n", "utf-8");
373
+ await upsertJournalEntry(projectRoot, "0000_pecunia_init");
374
+ return true;
375
+ }
376
+ /**
377
+ * Upsert a journal entry. If an entry with the same tag exists, do nothing.
378
+ * Otherwise, append a new entry with the next available idx.
379
+ *
380
+ * @param projectRoot - The project root directory
381
+ * @param tag - The migration tag (filename base without .sql)
332
382
  */
333
- async function findExistingInvariantsMigration(migrationsDir) {
334
- if (!existsSync(migrationsDir)) return null;
335
- const invariantsFile = (await fs$1.readdir(migrationsDir)).find((file) => file.endsWith("_pecunia_invariants.sql") && /^\d{4}_/.test(file));
336
- if (!invariantsFile) return null;
337
- const match = invariantsFile.match(/^(\d{4})_/);
338
- return match ? match[1] : null;
383
+ async function upsertJournalEntry(projectRoot, tag) {
384
+ const journal = await readJournal(projectRoot);
385
+ if (!journal) throw new Error("Journal does not exist. Call ensureDrizzleJournal() first.");
386
+ if (journal.entries.find((entry) => entry.tag === tag)) return;
387
+ const nextIdx = (journal.entries.length > 0 ? Math.max(...journal.entries.map((e) => e.idx)) : -1) + 1;
388
+ const hasBreakpoints = journal.entries.some((e) => e.breakpoints === true);
389
+ const newEntry = {
390
+ tag,
391
+ idx: nextIdx,
392
+ version: journal.version,
393
+ when: Date.now(),
394
+ ...hasBreakpoints && { breakpoints: true }
395
+ };
396
+ journal.entries.push(newEntry);
397
+ await writeJournal(projectRoot, journal);
339
398
  }
340
399
  /**
341
- * Determine the next migration number by scanning existing migration files.
342
- * Migration files are expected to follow the pattern: `NNNN_description.sql`
343
- * where NNNN is a 4-digit number.
400
+ * Find existing invariants migration tag in the journal.
344
401
  *
345
- * @param migrationsDir - Path to the drizzle migrations directory
402
+ * @param projectRoot - The project root directory
403
+ * @returns The tag if found, or null
404
+ */
405
+ async function findExistingInvariantsTag(projectRoot) {
406
+ const journal = await readJournal(projectRoot);
407
+ if (!journal) return null;
408
+ const invariantsEntry = journal.entries.find((entry) => entry.tag.endsWith("_pecunia_invariants"));
409
+ return invariantsEntry ? invariantsEntry.tag : null;
410
+ }
411
+ /**
412
+ * Determine the next migration number by checking journal entries and existing files.
413
+ * Prefers checking journal entries first, then falls back to scanning files.
414
+ *
415
+ * @param projectRoot - The project root directory
346
416
  * @returns The next migration number (4-digit string, e.g., "0001")
347
417
  */
348
- async function getNextMigrationNumber(migrationsDir) {
349
- const existingInvariantsNumber = await findExistingInvariantsMigration(migrationsDir);
350
- if (existingInvariantsNumber) return existingInvariantsNumber;
418
+ async function nextMigrationNumber(projectRoot) {
419
+ const existingInvariantsTag = await findExistingInvariantsTag(projectRoot);
420
+ if (existingInvariantsTag) {
421
+ const match = existingInvariantsTag.match(/^(\d{4})_/);
422
+ if (match) return match[1];
423
+ }
424
+ const journal = await readJournal(projectRoot);
425
+ if (journal && journal.entries.length > 0) {
426
+ const numbers$1 = journal.entries.map((entry) => {
427
+ const match = entry.tag.match(/^(\d{4})_/);
428
+ return match ? parseInt(match[1], 10) : 0;
429
+ }).filter((n) => n >= 0);
430
+ if (numbers$1.length > 0) {
431
+ const nextNumber$1 = Math.max(...numbers$1) + 1;
432
+ if (nextNumber$1 >= 9999) return "9999";
433
+ return String(nextNumber$1).padStart(4, "0");
434
+ }
435
+ }
436
+ const migrationsDir = await getDrizzleMigrationsDir(projectRoot);
351
437
  if (!existsSync(migrationsDir)) return "0001";
352
438
  const migrationFiles = (await fs$1.readdir(migrationsDir)).filter((file) => file.endsWith(".sql") && /^\d{4}_/.test(file));
353
439
  if (migrationFiles.length === 0) return "0001";
@@ -373,31 +459,57 @@ function getInvariantsMigrationPath(migrationsDir, migrationNumber) {
373
459
  return path.join(migrationsDir, `${migrationNumber}_pecunia_invariants.sql`);
374
460
  }
375
461
  /**
462
+ * Upsert a Drizzle migration file and register it in the journal.
463
+ *
464
+ * @param projectRoot - The project root directory
465
+ * @param tag - The migration tag (filename base without .sql)
466
+ * @param sql - The SQL content to write
467
+ */
468
+ async function upsertDrizzleMigration(projectRoot, tag, sql) {
469
+ const migrationsDir = await getDrizzleMigrationsDir(projectRoot);
470
+ const migrationPath = path.join(migrationsDir, `${tag}.sql`);
471
+ if (existsSync(migrationPath)) {
472
+ if (await fs$1.readFile(migrationPath, "utf-8") === sql) {
473
+ await upsertJournalEntry(projectRoot, tag);
474
+ return;
475
+ }
476
+ }
477
+ await fs$1.writeFile(migrationPath, sql, "utf-8");
478
+ await upsertJournalEntry(projectRoot, tag);
479
+ }
480
+ /**
376
481
  * Write the invariants SQL file to the drizzle migrations directory.
377
- * Handles idempotency by checking if the file exists and has the same content.
482
+ * Ensures journal exists, determines migration number, and registers in journal.
378
483
  *
379
484
  * @param projectRoot - The project root directory
380
485
  * @param sqlContent - The SQL content to write
381
- * @returns The path to the written file, or null if no changes were needed
486
+ * @returns The path to the written file and status flags
382
487
  */
383
488
  async function writeInvariantsMigration(projectRoot, sqlContent) {
384
- const migrationsDir = await getDrizzleMigrationsDir(projectRoot);
385
- const migrationPath = getInvariantsMigrationPath(migrationsDir, await getNextMigrationNumber(migrationsDir));
489
+ const journalCreated = await ensureDrizzleJournal(projectRoot);
490
+ const migrationNumber = await nextMigrationNumber(projectRoot);
491
+ const tag = `${migrationNumber}_pecunia_invariants`;
492
+ const migrationPath = getInvariantsMigrationPath(await getDrizzleMigrationsDir(projectRoot), migrationNumber);
386
493
  let created = false;
387
494
  let updated = false;
388
495
  if (existsSync(migrationPath)) {
389
- if (await fs$1.readFile(migrationPath, "utf-8") === sqlContent) return {
390
- path: migrationPath,
391
- created: false,
392
- updated: false
393
- };
496
+ if (await fs$1.readFile(migrationPath, "utf-8") === sqlContent) {
497
+ await upsertJournalEntry(projectRoot, tag);
498
+ return {
499
+ path: migrationPath,
500
+ created: false,
501
+ updated: false,
502
+ journalCreated
503
+ };
504
+ }
394
505
  updated = true;
395
506
  } else created = true;
396
- await fs$1.writeFile(migrationPath, sqlContent, "utf-8");
507
+ await upsertDrizzleMigration(projectRoot, tag, sqlContent);
397
508
  return {
398
509
  path: migrationPath,
399
510
  created,
400
- updated
511
+ updated,
512
+ journalCreated
401
513
  };
402
514
  }
403
515
 
@@ -633,61 +745,97 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
633
745
  const modelName = getModelName(tableKey);
634
746
  const oneRelations = [];
635
747
  const manyRelations = [];
636
- if (table.relations) for (const [relationName, relationDef] of Object.entries(table.relations)) {
637
- const referencedModelName = getModelName(relationDef.model);
638
- const foreignKeyField = table.fields[relationDef.foreignKey];
639
- if (relationDef.kind === "one") {
640
- if (foreignKeyField?.references) {
641
- const fieldRef = `${modelName}.${getFieldName({
642
- model: tableKey,
643
- field: relationDef.foreignKey
644
- })}`;
645
- const referenceRef = `${referencedModelName}.${getFieldName({
646
- model: relationDef.model,
647
- field: foreignKeyField.references.field || "id"
648
- })}`;
649
- oneRelations.push({
650
- key: relationName,
651
- model: referencedModelName,
652
- type: "one",
653
- reference: {
654
- field: fieldRef,
655
- references: referenceRef,
656
- fieldName: relationDef.foreignKey
657
- }
658
- });
659
- }
660
- } else if (relationDef.kind === "many") {
661
- const referencedTable = tables[relationDef.model];
662
- if (referencedTable) {
663
- const fkField = Object.entries(referencedTable.fields).find(([_, field]) => field.references && (field.references.model === tableKey || field.references.model === getModelName(tableKey)));
664
- if (fkField) {
665
- const [fkFieldName] = fkField;
666
- const fieldRef = `${referencedModelName}.${getFieldName({
667
- model: relationDef.model,
668
- field: fkFieldName
669
- })}`;
670
- const referenceRef = `${modelName}.${getFieldName({
748
+ if (table.relations) {
749
+ for (const [relationName, relationDef] of Object.entries(table.relations)) {
750
+ const referencedModelName = getModelName(relationDef.model);
751
+ const foreignKeyField = table.fields[relationDef.foreignKey];
752
+ const isSelfReferential = relationDef.model === tableKey || referencedModelName === modelName;
753
+ const generateRelationName = (fkName) => {
754
+ let cleaned = convertToSnakeCase(fkName, adapter.options?.camelCase).replace(/_by_id$/, "").replace(/_id$/, "");
755
+ const participleToNoun = {
756
+ "reversed": "reversal",
757
+ "created": "creation",
758
+ "updated": "update"
759
+ };
760
+ if (participleToNoun[cleaned]) cleaned = participleToNoun[cleaned];
761
+ return `${convertToSnakeCase(modelName, adapter.options?.camelCase)}_${cleaned}`;
762
+ };
763
+ if (relationDef.kind === "one") {
764
+ if (foreignKeyField?.references) {
765
+ const fieldRef = `${modelName}.${getFieldName({
671
766
  model: tableKey,
672
- field: "id"
767
+ field: relationDef.foreignKey
673
768
  })}`;
674
- manyRelations.push({
769
+ const referenceRef = `${referencedModelName}.${getFieldName({
770
+ model: relationDef.model,
771
+ field: foreignKeyField.references.field || "id"
772
+ })}`;
773
+ oneRelations.push({
675
774
  key: relationName,
676
775
  model: referencedModelName,
677
- type: "many",
776
+ type: "one",
678
777
  reference: {
679
778
  field: fieldRef,
680
779
  references: referenceRef,
681
- fieldName: fkFieldName
780
+ fieldName: relationDef.foreignKey
781
+ },
782
+ relationName: isSelfReferential ? generateRelationName(relationDef.foreignKey) : void 0
783
+ });
784
+ }
785
+ } else if (relationDef.kind === "many") {
786
+ const referencedTable = tables[relationDef.model];
787
+ if (referencedTable) {
788
+ const fkField = Object.entries(referencedTable.fields).find(([_, field]) => field.references && (field.references.model === tableKey || field.references.model === getModelName(tableKey)));
789
+ if (fkField) {
790
+ const [fkFieldName] = fkField;
791
+ const fieldRef = `${referencedModelName}.${getFieldName({
792
+ model: relationDef.model,
793
+ field: fkFieldName
794
+ })}`;
795
+ const referenceRef = `${modelName}.${getFieldName({
796
+ model: tableKey,
797
+ field: "id"
798
+ })}`;
799
+ let relationNameForMany;
800
+ if (isSelfReferential) {
801
+ const matchingOne = oneRelations.find((rel) => rel.reference?.fieldName === fkFieldName && rel.model === referencedModelName);
802
+ if (matchingOne?.relationName) relationNameForMany = matchingOne.relationName;
803
+ else {
804
+ let cleaned = convertToSnakeCase(fkFieldName, adapter.options?.camelCase).replace(/_by_id$/, "").replace(/_id$/, "");
805
+ const participleToNoun = {
806
+ "reversed": "reversal",
807
+ "created": "creation",
808
+ "updated": "update"
809
+ };
810
+ if (participleToNoun[cleaned]) cleaned = participleToNoun[cleaned];
811
+ relationNameForMany = `${convertToSnakeCase(modelName, adapter.options?.camelCase)}_${cleaned}`;
812
+ }
682
813
  }
814
+ manyRelations.push({
815
+ key: relationName,
816
+ model: referencedModelName,
817
+ type: "many",
818
+ reference: {
819
+ field: fieldRef,
820
+ references: referenceRef,
821
+ fieldName: fkFieldName
822
+ },
823
+ relationName: relationNameForMany
824
+ });
825
+ } else manyRelations.push({
826
+ key: relationName,
827
+ model: referencedModelName,
828
+ type: "many"
683
829
  });
684
- } else manyRelations.push({
685
- key: relationName,
686
- model: referencedModelName,
687
- type: "many"
688
- });
830
+ }
689
831
  }
690
832
  }
833
+ for (const oneRel of oneRelations) {
834
+ if (!oneRel.relationName || !oneRel.reference) continue;
835
+ const oneRelFieldName = oneRel.reference.fieldName;
836
+ const matchingMany = manyRelations.find((manyRel) => manyRel.model === oneRel.model && manyRel.reference?.fieldName === oneRelFieldName);
837
+ if (matchingMany) matchingMany.relationName = oneRel.relationName;
838
+ }
691
839
  }
692
840
  const relationsByModel = /* @__PURE__ */ new Map();
693
841
  for (const relation of oneRelations) {
@@ -703,10 +851,12 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
703
851
  for (const relation of duplicateRelations) {
704
852
  if (!relation.reference) continue;
705
853
  const fieldName = relation.reference.fieldName;
706
- const tableRelation = `export const ${`${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`} = relations(${modelName}, ({ one }) => ({
854
+ const relationExportName = `${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`;
855
+ const relationNameParam = relation.relationName ? `,\n relationName: "${relation.relationName}"` : "";
856
+ const tableRelation = `export const ${relationExportName} = relations(${modelName}, ({ one }) => ({
707
857
  ${relation.key}: one(${relation.model}, {
708
858
  fields: [${relation.reference.field}],
709
- references: [${relation.reference.references}],
859
+ references: [${relation.reference.references}]${relationNameParam}
710
860
  })
711
861
  }))`;
712
862
  relationsString += `\n${tableRelation}\n`;
@@ -715,24 +865,36 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
715
865
  const hasMany = manyRelations.length > 0;
716
866
  if (hasOne && hasMany) {
717
867
  const tableRelation = `export const ${modelName}Relations = relations(${modelName}, ({ one, many }) => ({
718
- ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
868
+ ${singleRelations.map((relation) => {
869
+ if (!relation.reference) return "";
870
+ const relationNameParam = relation.relationName ? `,\n relationName: "${relation.relationName}"` : "";
871
+ return ` ${relation.key}: one(${relation.model}, {
719
872
  fields: [${relation.reference.field}],
720
- references: [${relation.reference.references}],
721
- })` : "").filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""}
722
- ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
873
+ references: [${relation.reference.references}]${relationNameParam}
874
+ })`;
875
+ }).filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""}
876
+ ${manyRelations.map(({ key, model, relationName }) => {
877
+ return ` ${key}: many(${model}${relationName ? `, { relationName: "${relationName}" }` : ""})`;
878
+ }).join(",\n ")}
723
879
  }))`;
724
880
  relationsString += `\n${tableRelation}\n`;
725
881
  } else if (hasOne) {
726
882
  const tableRelation = `export const ${modelName}Relations = relations(${modelName}, ({ one }) => ({
727
- ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
883
+ ${singleRelations.map((relation) => {
884
+ if (!relation.reference) return "";
885
+ const relationNameParam = relation.relationName ? `,\n relationName: "${relation.relationName}"` : "";
886
+ return ` ${relation.key}: one(${relation.model}, {
728
887
  fields: [${relation.reference.field}],
729
- references: [${relation.reference.references}],
730
- })` : "").filter((x) => x !== "").join(",\n ")}
888
+ references: [${relation.reference.references}]${relationNameParam}
889
+ })`;
890
+ }).filter((x) => x !== "").join(",\n ")}
731
891
  }))`;
732
892
  relationsString += `\n${tableRelation}\n`;
733
893
  } else if (hasMany) {
734
894
  const tableRelation = `export const ${modelName}Relations = relations(${modelName}, ({ many }) => ({
735
- ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
895
+ ${manyRelations.map(({ key, model, relationName }) => {
896
+ return ` ${key}: many(${model}${relationName ? `, { relationName: "${relationName}" }` : ""})`;
897
+ }).join(",\n ")}
736
898
  }))`;
737
899
  relationsString += `\n${tableRelation}\n`;
738
900
  }
@@ -746,7 +908,8 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
746
908
  getModelName,
747
909
  getFieldName
748
910
  }), "public"));
749
- if (result.created) console.log(`Generated invariants migration: ${path.relative(projectRoot, result.path)}\nNote: Created ./drizzle directory. Invariants SQL is placed in Drizzle migrations.`);
911
+ if (result.journalCreated) console.log(`Created Drizzle migration journal and invariants migration; now run \`npx drizzle-kit migrate\`.`);
912
+ else if (result.created) console.log(`Generated invariants migration: ${path.relative(projectRoot, result.path)}`);
750
913
  else if (result.updated) console.log(`Updated invariants migration: ${path.relative(projectRoot, result.path)}`);
751
914
  else console.log(`Invariants migration up to date: ${path.relative(projectRoot, result.path)}`);
752
915
  }
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as getPackageInfo, n as generateSchema } from "./generators-BVZYl7Wb.mjs";
2
+ import { i as getPackageInfo, n as generateSchema } from "./generators-BXA4y5O8.mjs";
3
3
  import { Command } from "commander";
4
4
  import fs, { existsSync, readFileSync } from "node:fs";
5
5
  import fs$1 from "node:fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pecunia-cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "module": "dist/index.mjs",
6
6
  "main": "./dist/index.mjs",