pecunia-cli 0.1.9 → 0.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/api.d.mts CHANGED
@@ -20,6 +20,7 @@ declare const adapters: {
20
20
  prisma: SchemaGenerator;
21
21
  drizzle: SchemaGenerator;
22
22
  kysely: SchemaGenerator;
23
+ mongodb: SchemaGenerator;
23
24
  };
24
25
  declare const generateSchema: (opts: {
25
26
  adapter: DBAdapter$1;
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-D5zLfxo1.mjs";
1
+ import { a as generateKyselySchema, n as generateSchema, o as generateDrizzleSchema, r as generatePrismaSchema, t as adapters } from "./generators-DjXYs9VN.mjs";
2
2
 
3
3
  export { adapters, generateDrizzleSchema, generateKyselySchema, generatePrismaSchema, generateSchema };
@@ -1,18 +1,319 @@
1
1
  import fs, { existsSync } from "node:fs";
2
2
  import fs$1 from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { getMigrations } from "pecunia-root";
5
- import { capitalizeFirstLetter, getPaymentTables, initGetFieldName, initGetModelName } from "pecunia-core";
4
+ import { getMigrations, getPaymentTables } from "pecunia-root";
5
+ import { capitalizeFirstLetter, getPaymentTables as getPaymentTables$1, initGetFieldName, initGetModelName } from "pecunia-core";
6
6
  import prettier from "prettier";
7
7
  import { produceSchema } from "@mrleebo/prisma-ast";
8
8
 
9
+ //#region src/generators/invariants.ts
10
+ /**
11
+ * Normalize invariants from schema into IR format.
12
+ * This parses invariant descriptions and extracts structured logic.
13
+ */
14
+ function normalizeInvariants(schema, options) {
15
+ const invariants = [];
16
+ for (const [tableKey, table] of Object.entries(schema)) {
17
+ if (!table.invariants) continue;
18
+ const modelName = options.getModelName(tableKey);
19
+ const tableName = table.modelName;
20
+ for (const invariant of table.invariants) {
21
+ const logic = parseInvariantLogic(invariant.description, tableKey, table, schema, options);
22
+ invariants.push({
23
+ id: invariant.id,
24
+ description: invariant.description,
25
+ modelName,
26
+ tableName,
27
+ appliesTo: invariant.appliesTo,
28
+ enforcement: invariant.enforcement || {},
29
+ logic
30
+ });
31
+ }
32
+ }
33
+ return invariants;
34
+ }
35
+ /**
36
+ * Parse invariant description into structured logic.
37
+ * This is a heuristic parser that extracts common patterns.
38
+ */
39
+ function parseInvariantLogic(description, tableKey, table, schema, options) {
40
+ const desc = description.toLowerCase();
41
+ if (desc.includes("determines") && desc.includes("presence")) {
42
+ const modeMatch = description.match(/(\w+)\.(\w+)\s+determines/);
43
+ const subMatch = description.match(/(\w+)\s+must be (present|null\/absent)/);
44
+ const enumMatches = description.matchAll(/(\w+)\s*=>\s*(\w+)\s+must be (present|null\/absent)/g);
45
+ if (modeMatch && subMatch) {
46
+ const [, modelName, fieldName] = modeMatch;
47
+ const [, conditionalField] = subMatch;
48
+ const field = Object.keys(table.fields).find((k) => k.toLowerCase() === fieldName.toLowerCase());
49
+ const conditional = Object.keys(table.fields).find((k) => k.toLowerCase() === conditionalField.toLowerCase());
50
+ if (field && conditional) {
51
+ const fieldAttr = table.fields[field];
52
+ let allowedValues = [];
53
+ if (typeof fieldAttr.type !== "string" && Array.isArray(fieldAttr.type) && fieldAttr.type.every((x) => typeof x === "string")) allowedValues = fieldAttr.type;
54
+ else {
55
+ const enumValues = /* @__PURE__ */ new Set();
56
+ for (const match of enumMatches) {
57
+ const val = match[1]?.trim();
58
+ if (val) enumValues.add(val);
59
+ }
60
+ if (enumValues.size > 0) allowedValues = Array.from(enumValues);
61
+ else allowedValues = ["PAYMENT", "SUBSCRIPTION"].filter((e) => desc.includes(e.toLowerCase()));
62
+ }
63
+ let whenPresent = false;
64
+ for (const match of enumMatches) {
65
+ const enumVal = match[1];
66
+ const requirement = match[3];
67
+ if (enumVal && requirement === "present") {
68
+ whenPresent = true;
69
+ break;
70
+ }
71
+ }
72
+ if (!whenPresent && desc.includes("subscription") && desc.includes("must be present")) whenPresent = true;
73
+ return {
74
+ type: "field_enum_constraint",
75
+ field,
76
+ fieldName: options.getFieldName({
77
+ model: tableKey,
78
+ field
79
+ }),
80
+ allowedValues: allowedValues.length > 0 ? allowedValues : ["PAYMENT", "SUBSCRIPTION"],
81
+ conditionalField: {
82
+ field: conditional,
83
+ fieldName: options.getFieldName({
84
+ model: tableKey,
85
+ field: conditional
86
+ }),
87
+ whenPresent
88
+ }
89
+ };
90
+ }
91
+ }
92
+ }
93
+ if (desc.includes("must belong to same") || desc.includes("must equal") && desc.includes("when")) {
94
+ const fieldMatch = description.match(/(\w+)\.(\w+)\s+must/);
95
+ const refMatch = description.match(/(\w+)\.(\w+)\s+must equal\s+(\w+)\.(\w+)/);
96
+ description.match(/(\w+)\.(\w+)\s+must equal/);
97
+ if (fieldMatch && refMatch) {
98
+ const [, modelName, fieldName] = fieldMatch;
99
+ const [, refTable, refField, ownerTable, ownerField] = refMatch;
100
+ const field = Object.keys(table.fields).find((k) => k.toLowerCase() === fieldName.toLowerCase());
101
+ if (field && table.fields[field]?.references) {
102
+ table.fields[field].references;
103
+ const refTableKey = Object.keys(schema).find((k) => schema[k]?.modelName === refTable || k === refTable);
104
+ const ownerTableKey = Object.keys(schema).find((k) => schema[k]?.modelName === ownerTable || k === ownerTable);
105
+ if (refTableKey && ownerTableKey) {
106
+ const ownershipField = Object.keys(table.fields).find((k) => k.toLowerCase() === ownerField.toLowerCase());
107
+ if (ownershipField) return {
108
+ type: "cross_table_ownership",
109
+ field,
110
+ fieldName: options.getFieldName({
111
+ model: tableKey,
112
+ field
113
+ }),
114
+ referencedTable: options.getModelName(refTableKey),
115
+ referencedField: options.getFieldName({
116
+ model: refTableKey,
117
+ field: refField
118
+ }),
119
+ ownershipField,
120
+ ownershipFieldName: options.getFieldName({
121
+ model: tableKey,
122
+ field: ownershipField
123
+ })
124
+ };
125
+ }
126
+ }
127
+ }
128
+ }
129
+ if (desc.includes("must match") && desc.includes("of its")) {
130
+ const fieldMatch = description.match(/(\w+)\.(\w+)\s+must match/);
131
+ const refMatch = description.match(/of its\s+(\w+)/);
132
+ const equalityMatch = description.match(/(\w+)\.(\w+)\s+must equal\s+(\w+)\.(\w+)/);
133
+ if (fieldMatch && refMatch && equalityMatch) {
134
+ const [, , equalityFieldName] = fieldMatch;
135
+ const [, refFieldName] = refMatch;
136
+ const [, refTable, refEqualityField, , sourceEqualityField] = equalityMatch;
137
+ const equalityField = Object.keys(table.fields).find((k) => k.toLowerCase() === equalityFieldName.toLowerCase());
138
+ const refField = Object.keys(table.fields).find((k) => k.toLowerCase() === refFieldName.toLowerCase());
139
+ if (equalityField && refField && table.fields[refField]?.references) {
140
+ const ref = table.fields[refField].references;
141
+ const refTableKey = Object.keys(schema).find((k) => schema[k]?.modelName === refTable || k === refTable);
142
+ if (refTableKey) {
143
+ const refEquality = Object.keys(schema[refTableKey].fields).find((k) => k.toLowerCase() === refEqualityField.toLowerCase());
144
+ if (refEquality) return {
145
+ type: "cross_table_equality",
146
+ field: refField,
147
+ fieldName: options.getFieldName({
148
+ model: tableKey,
149
+ field: refField
150
+ }),
151
+ referencedTable: options.getModelName(refTableKey),
152
+ referencedField: options.getFieldName({
153
+ model: refTableKey,
154
+ field: ref.field
155
+ }),
156
+ equalityField,
157
+ equalityFieldName: options.getFieldName({
158
+ model: tableKey,
159
+ field: equalityField
160
+ }),
161
+ referencedEqualityField: refEquality,
162
+ referencedEqualityFieldName: options.getFieldName({
163
+ model: refTableKey,
164
+ field: refEquality
165
+ })
166
+ };
167
+ }
168
+ }
169
+ }
170
+ }
171
+ return {
172
+ type: "raw",
173
+ description
174
+ };
175
+ }
176
+
177
+ //#endregion
178
+ //#region src/generators/invariants-sql.ts
179
+ /**
180
+ * Generate SQL for enforcing invariants in Postgres (and compatible databases).
181
+ * Returns SQL statements for CHECK constraints and triggers.
182
+ */
183
+ function emitPostgresInvariantSql(invariants, schemaName = "public") {
184
+ const statements = [];
185
+ const checkInvariants = invariants.filter((inv) => inv.enforcement.postgres === "check" && inv.appliesTo.includes("postgres"));
186
+ const triggerInvariants = invariants.filter((inv) => inv.enforcement.postgres === "trigger" && inv.appliesTo.includes("postgres"));
187
+ for (const inv of checkInvariants) {
188
+ const sql = generateCheckConstraint(inv, schemaName);
189
+ if (sql) statements.push(sql);
190
+ }
191
+ for (const inv of triggerInvariants) {
192
+ const sql = generateTrigger(inv, schemaName);
193
+ if (sql) statements.push(...sql);
194
+ }
195
+ if (statements.length === 0) return "-- No invariant enforcement SQL generated\n";
196
+ return `-- Invariant enforcement SQL
197
+ -- Generated from schema invariants
198
+ -- DO NOT EDIT MANUALLY - This file is auto-generated
199
+
200
+ ${statements.join("\n\n")}
201
+ `;
202
+ }
203
+ /**
204
+ * Generate CHECK constraint SQL for an invariant.
205
+ */
206
+ function generateCheckConstraint(inv, schemaName) {
207
+ const { logic, tableName, id } = inv;
208
+ switch (logic.type) {
209
+ case "field_enum_constraint": {
210
+ const { fieldName, allowedValues, conditionalField } = logic;
211
+ if (conditionalField) {
212
+ const constraintName$1 = `${tableName}_${id}_check`;
213
+ const enumCheck$1 = allowedValues.map((val) => `'${val}'`).join(", ");
214
+ const subscriptionValue = allowedValues.find((v) => v === "SUBSCRIPTION") || allowedValues.find((v) => v !== "PAYMENT") || allowedValues[0];
215
+ const paymentValue = allowedValues.find((v) => v === "PAYMENT") || allowedValues[0];
216
+ const conditionalCheck = `(
217
+ (${fieldName} = '${subscriptionValue}') = (${conditionalField.fieldName} IS NOT NULL) AND
218
+ (${fieldName} = '${paymentValue}') = (${conditionalField.fieldName} IS NULL)
219
+ )`;
220
+ return `-- ${inv.description}
221
+ ALTER TABLE ${schemaName}.${tableName}
222
+ ADD CONSTRAINT ${constraintName$1}
223
+ CHECK (
224
+ ${fieldName} IN (${enumCheck$1}) AND
225
+ ${conditionalCheck}
226
+ );`;
227
+ }
228
+ const constraintName = `${tableName}_${id}_check`;
229
+ const enumCheck = allowedValues.map((val) => `'${val}'`).join(", ");
230
+ return `-- ${inv.description}
231
+ ALTER TABLE ${schemaName}.${tableName}
232
+ ADD CONSTRAINT ${constraintName}
233
+ CHECK (${fieldName} IN (${enumCheck}));`;
234
+ }
235
+ default: return null;
236
+ }
237
+ }
238
+ /**
239
+ * Generate trigger SQL for an invariant.
240
+ * Returns array of SQL statements (function + trigger).
241
+ */
242
+ function generateTrigger(inv, schemaName) {
243
+ const { logic, tableName, id } = inv;
244
+ switch (logic.type) {
245
+ case "cross_table_ownership": {
246
+ const { fieldName, referencedTable, referencedField, ownershipFieldName } = logic;
247
+ const functionName = `${tableName}_${id}_fn`;
248
+ const triggerName = `${tableName}_${id}_trigger`;
249
+ return [`-- Function to enforce: ${inv.description}
250
+ CREATE OR REPLACE FUNCTION ${schemaName}.${functionName}()
251
+ RETURNS TRIGGER AS $$
252
+ BEGIN
253
+ IF NEW.${fieldName} IS NOT NULL THEN
254
+ IF NOT EXISTS (
255
+ SELECT 1
256
+ FROM ${schemaName}.${referencedTable}
257
+ WHERE ${referencedTable}.${referencedField} = NEW.${fieldName}
258
+ AND ${referencedTable}.${ownershipFieldName} = NEW.${ownershipFieldName}
259
+ ) THEN
260
+ RAISE EXCEPTION 'Invariant violation: % must belong to the same % as the record', '${fieldName}', '${ownershipFieldName}';
261
+ END IF;
262
+ END IF;
263
+ RETURN NEW;
264
+ END;
265
+ $$ LANGUAGE plpgsql;`, `-- Trigger to enforce: ${inv.description}
266
+ DROP TRIGGER IF EXISTS ${triggerName} ON ${schemaName}.${tableName};
267
+ CREATE TRIGGER ${triggerName}
268
+ BEFORE INSERT OR UPDATE ON ${schemaName}.${tableName}
269
+ FOR EACH ROW
270
+ EXECUTE FUNCTION ${schemaName}.${functionName}();`];
271
+ }
272
+ case "cross_table_equality": {
273
+ const { fieldName, referencedTable, referencedField, equalityFieldName, referencedEqualityFieldName } = logic;
274
+ const functionName = `${tableName}_${id}_fn`;
275
+ const triggerName = `${tableName}_${id}_trigger`;
276
+ return [`-- Function to enforce: ${inv.description}
277
+ CREATE OR REPLACE FUNCTION ${schemaName}.${functionName}()
278
+ RETURNS TRIGGER AS $$
279
+ DECLARE
280
+ ref_${referencedEqualityFieldName} TEXT;
281
+ BEGIN
282
+ IF NEW.${fieldName} IS NOT NULL THEN
283
+ SELECT ${referencedEqualityFieldName} INTO ref_${referencedEqualityFieldName}
284
+ FROM ${schemaName}.${referencedTable}
285
+ WHERE ${referencedTable}.${referencedField} = NEW.${fieldName}
286
+ LIMIT 1;
287
+
288
+ IF ref_${referencedEqualityFieldName} IS NULL THEN
289
+ RAISE EXCEPTION 'Invariant violation: Referenced record not found in ${referencedTable}';
290
+ END IF;
291
+
292
+ IF NEW.${equalityFieldName} != ref_${referencedEqualityFieldName} THEN
293
+ RAISE EXCEPTION 'Invariant violation: % must equal %.%', '${equalityFieldName}', '${referencedTable}', '${referencedEqualityFieldName}';
294
+ END IF;
295
+ END IF;
296
+ RETURN NEW;
297
+ END;
298
+ $$ LANGUAGE plpgsql;`, `-- Trigger to enforce: ${inv.description}
299
+ DROP TRIGGER IF EXISTS ${triggerName} ON ${schemaName}.${tableName};
300
+ CREATE TRIGGER ${triggerName}
301
+ BEFORE INSERT OR UPDATE ON ${schemaName}.${tableName}
302
+ FOR EACH ROW
303
+ EXECUTE FUNCTION ${schemaName}.${functionName}();`];
304
+ }
305
+ default: return null;
306
+ }
307
+ }
308
+
309
+ //#endregion
9
310
  //#region src/generators/drizzle.ts
10
311
  function convertToSnakeCase(str, camelCase) {
11
312
  if (camelCase) return str;
12
313
  return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase();
13
314
  }
14
315
  const generateDrizzleSchema = async ({ options, file, adapter }) => {
15
- const tables = getPaymentTables(options);
316
+ const tables = getPaymentTables$1(options);
16
317
  const filePath = file || "./payment-schema.ts";
17
318
  const databaseType = adapter.options?.provider;
18
319
  if (!databaseType) throw new Error("Database provider type is undefined during Drizzle schema generation. Please define a `provider` in the Drizzle adapter config.");
@@ -325,12 +626,55 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
325
626
  }
326
627
  }
327
628
  code += `\n${relationsString}`;
629
+ const typeHints = generateInvariantTypeHints(tables, getModelName, getFieldName);
630
+ if (typeHints) code += `\n\n${typeHints}`;
631
+ const formattedCode = await prettier.format(code, { parser: "typescript" });
632
+ if (databaseType === "pg") {
633
+ const sql = emitPostgresInvariantSql(normalizeInvariants(tables, {
634
+ getModelName,
635
+ getFieldName
636
+ }), "public");
637
+ const sqlFilePath = filePath.replace(/\.ts$/, "-invariants.sql");
638
+ const sqlDir = path.dirname(path.resolve(process.cwd(), sqlFilePath));
639
+ await fs$1.mkdir(sqlDir, { recursive: true });
640
+ await fs$1.writeFile(path.resolve(process.cwd(), sqlFilePath), sql);
641
+ console.log(`📝 Generated invariant SQL: ${sqlFilePath}`);
642
+ }
328
643
  return {
329
- code: await prettier.format(code, { parser: "typescript" }),
644
+ code: formattedCode,
330
645
  fileName: filePath,
331
646
  overwrite: fileExist
332
647
  };
333
648
  };
649
+ /**
650
+ * Generate TypeScript type hints for invariants (e.g., enum unions).
651
+ */
652
+ function generateInvariantTypeHints(tables, getModelName, getFieldName) {
653
+ const hints = [];
654
+ for (const [tableKey, table] of Object.entries(tables)) {
655
+ if (!table.invariants) continue;
656
+ for (const invariant of table.invariants) {
657
+ const desc = invariant.description.toLowerCase();
658
+ if (desc.includes("determines") && desc.includes("presence")) {
659
+ const modeMatch = invariant.description.match(/(\w+)\.(\w+)\s+determines/);
660
+ if (modeMatch) {
661
+ const [, , fieldName] = modeMatch;
662
+ const field = Object.keys(table.fields).find((k) => k.toLowerCase() === fieldName.toLowerCase());
663
+ if (field) {
664
+ const fieldAttr = table.fields[field];
665
+ if (typeof fieldAttr.type !== "string" && Array.isArray(fieldAttr.type) && fieldAttr.type.every((x) => typeof x === "string")) {
666
+ const enumName = `${getModelName(tableKey)}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Mode`;
667
+ const enumValues = fieldAttr.type.map((v) => `"${v}"`).join(" | ");
668
+ hints.push(`// Type hint for invariant: ${invariant.id}`);
669
+ hints.push(`export type ${enumName} = ${enumValues};`);
670
+ }
671
+ }
672
+ }
673
+ }
674
+ }
675
+ }
676
+ return hints.length > 0 ? `\n// Invariant type hints\n${hints.join("\n")}` : "";
677
+ }
334
678
  function generateImport({ databaseType, tables }) {
335
679
  const rootImports = ["relations"];
336
680
  const coreImports = [];
@@ -370,12 +714,31 @@ function generateImport({ databaseType, tables }) {
370
714
 
371
715
  //#endregion
372
716
  //#region src/generators/kysely.ts
373
- const generateKyselySchema = async ({ options, file }) => {
717
+ const generateKyselySchema = async ({ options, file, adapter }) => {
374
718
  const { compileMigrations } = await getMigrations(options);
375
719
  const migrations = await compileMigrations();
720
+ const migrationFile = file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`;
721
+ if ((adapter?.options?.type || "postgres") === "postgres") {
722
+ const tables = getPaymentTables(options);
723
+ const sql = emitPostgresInvariantSql(normalizeInvariants(tables, {
724
+ getModelName: initGetModelName({
725
+ schema: tables,
726
+ usePlural: adapter?.options?.adapterConfig?.usePlural
727
+ }),
728
+ getFieldName: initGetFieldName({
729
+ schema: tables,
730
+ usePlural: false
731
+ })
732
+ }), "public");
733
+ const sqlFilePath = path.join(path.dirname(migrationFile), "invariants.sql");
734
+ const sqlDir = path.dirname(path.resolve(process.cwd(), sqlFilePath));
735
+ await fs$1.mkdir(sqlDir, { recursive: true });
736
+ await fs$1.writeFile(path.resolve(process.cwd(), sqlFilePath), sql);
737
+ console.log(`📝 Generated invariant SQL: ${sqlFilePath}`);
738
+ }
376
739
  return {
377
740
  code: migrations.trim() === ";" ? "" : migrations,
378
- fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
741
+ fileName: migrationFile
379
742
  };
380
743
  };
381
744
 
@@ -401,15 +764,15 @@ function getPrismaVersion(cwd) {
401
764
  //#region src/generators/prisma.ts
402
765
  const generatePrismaSchema = async ({ adapter, options, file }) => {
403
766
  const provider = adapter.options?.provider || "postgresql";
404
- const tables = getPaymentTables(options);
767
+ const tables = getPaymentTables$1(options);
405
768
  const filePath = file || "./prisma/schema.prisma";
406
769
  const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
407
770
  const getModelName = initGetModelName({
408
- schema: getPaymentTables(options),
771
+ schema: getPaymentTables$1(options),
409
772
  usePlural: adapter.options?.adapterConfig?.usePlural
410
773
  });
411
774
  const getFieldName = initGetFieldName({
412
- schema: getPaymentTables(options),
775
+ schema: getPaymentTables$1(options),
413
776
  usePlural: false
414
777
  });
415
778
  let schemaPrisma = "";
@@ -580,6 +943,24 @@ const generatePrismaSchema = async ({ adapter, options, file }) => {
580
943
  }
581
944
  });
582
945
  const schemaChanged = schema.trim() !== schemaPrisma.trim();
946
+ if (provider === "postgresql") {
947
+ const tables$1 = getPaymentTables$1(options);
948
+ const sql = emitPostgresInvariantSql(normalizeInvariants(tables$1, {
949
+ getModelName: initGetModelName({
950
+ schema: tables$1,
951
+ usePlural: adapter.options?.adapterConfig?.usePlural
952
+ }),
953
+ getFieldName: initGetFieldName({
954
+ schema: tables$1,
955
+ usePlural: false
956
+ })
957
+ }), "public");
958
+ const sqlFilePath = path.join(path.dirname(filePath), "invariants.sql");
959
+ const sqlDir = path.dirname(path.resolve(process.cwd(), sqlFilePath));
960
+ await fs$1.mkdir(sqlDir, { recursive: true });
961
+ await fs$1.writeFile(path.resolve(process.cwd(), sqlFilePath), sql);
962
+ console.log(`📝 Generated invariant SQL: ${sqlFilePath}`);
963
+ }
583
964
  return {
584
965
  code: schemaChanged ? schema : "",
585
966
  fileName: filePath,
@@ -598,12 +979,278 @@ const getNewPrisma = (provider, cwd) => {
598
979
  }`;
599
980
  };
600
981
 
982
+ //#endregion
983
+ //#region src/generators/invariants-mongo.ts
984
+ /**
985
+ * Generate MongoDB collection validator JSON schema for invariants.
986
+ */
987
+ function emitMongoValidators(invariants) {
988
+ const validators = {};
989
+ for (const inv of invariants) {
990
+ if (inv.enforcement.mongo !== "validator" || !inv.appliesTo.includes("mongo")) continue;
991
+ const collectionName = inv.tableName;
992
+ if (!validators[collectionName]) validators[collectionName] = { $jsonSchema: {
993
+ bsonType: "object",
994
+ required: [],
995
+ properties: {},
996
+ additionalProperties: true
997
+ } };
998
+ const validator = generateMongoValidator(inv);
999
+ if (validator) {
1000
+ Object.assign(validators[collectionName].$jsonSchema.properties, validator.properties || {});
1001
+ if (validator.required) validators[collectionName].$jsonSchema.required = [...validators[collectionName].$jsonSchema.required || [], ...validator.required];
1002
+ if (validator.anyOf) {
1003
+ if (!validators[collectionName].$jsonSchema.anyOf) validators[collectionName].$jsonSchema.anyOf = [];
1004
+ validators[collectionName].$jsonSchema.anyOf.push(...validator.anyOf);
1005
+ }
1006
+ }
1007
+ }
1008
+ return validators;
1009
+ }
1010
+ /**
1011
+ * Generate MongoDB validator JSON schema for a single invariant.
1012
+ */
1013
+ function generateMongoValidator(inv) {
1014
+ const { logic } = inv;
1015
+ switch (logic.type) {
1016
+ case "field_enum_constraint": {
1017
+ const { fieldName, allowedValues, conditionalField } = logic;
1018
+ if (conditionalField) {
1019
+ const anyOf = [];
1020
+ if (!conditionalField.whenPresent) anyOf.push({ properties: {
1021
+ [fieldName]: { enum: ["PAYMENT"] },
1022
+ [conditionalField.fieldName]: { bsonType: "null" }
1023
+ } });
1024
+ if (conditionalField.whenPresent) anyOf.push({
1025
+ properties: {
1026
+ [fieldName]: { enum: ["SUBSCRIPTION"] },
1027
+ [conditionalField.fieldName]: { bsonType: "string" }
1028
+ },
1029
+ required: [conditionalField.fieldName]
1030
+ });
1031
+ return {
1032
+ properties: { [fieldName]: { enum: allowedValues } },
1033
+ anyOf
1034
+ };
1035
+ }
1036
+ return { properties: { [fieldName]: { enum: allowedValues } } };
1037
+ }
1038
+ default: return null;
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Generate TypeScript guard module for MongoDB "app" enforcement invariants.
1043
+ */
1044
+ function emitMongoGuards(invariants) {
1045
+ const guardInvariants = invariants.filter((inv) => inv.enforcement.mongo === "app" && inv.appliesTo.includes("mongo"));
1046
+ if (guardInvariants.length === 0) return `// No MongoDB app-level invariant guards generated
1047
+ // This file is auto-generated - DO NOT EDIT MANUALLY
1048
+
1049
+ export {};
1050
+ `;
1051
+ const guards = [];
1052
+ const imports = [];
1053
+ for (const inv of guardInvariants) {
1054
+ const guard = generateMongoGuard(inv);
1055
+ if (guard) {
1056
+ guards.push(guard);
1057
+ if (guard.imports) imports.push(...guard.imports);
1058
+ }
1059
+ }
1060
+ const uniqueImports = Array.from(new Set(imports));
1061
+ return `// MongoDB invariant guards
1062
+ // Generated from schema invariants
1063
+ // DO NOT EDIT MANUALLY - This file is auto-generated
1064
+ //
1065
+ // These guards should be called before write operations to enforce invariants
1066
+ // at the application level.
1067
+
1068
+ ${uniqueImports.length > 0 ? uniqueImports.join("\n") + "\n" : ""}
1069
+
1070
+ ${guards.join("\n\n")}
1071
+ `;
1072
+ }
1073
+ /**
1074
+ * Generate TypeScript guard function for a single invariant.
1075
+ */
1076
+ function generateMongoGuard(inv) {
1077
+ const { logic, tableName, id, description } = inv;
1078
+ switch (logic.type) {
1079
+ case "cross_table_ownership": {
1080
+ const { fieldName, referencedTable, referencedField, ownershipFieldName } = logic;
1081
+ return { code: `/**
1082
+ * Guard: ${description}
1083
+ *
1084
+ * @param data - The record being created/updated (use camelCase field names)
1085
+ * @param db - MongoDB database instance
1086
+ * @returns true if invariant is satisfied, throws error otherwise
1087
+ */
1088
+ export async function ${id}Guard(
1089
+ data: { ${fieldName}?: string | null; ${ownershipFieldName}: string },
1090
+ db: { collection(name: string): { findOne(filter: any): Promise<any | null> } }
1091
+ ): Promise<boolean> {
1092
+ if (!data.${fieldName}) {
1093
+ return true; // Field is optional/nullable
1094
+ }
1095
+
1096
+ const referenced = await db.collection("${referencedTable}").findOne({
1097
+ ${referencedField}: data.${fieldName}
1098
+ });
1099
+
1100
+ if (!referenced) {
1101
+ throw new Error(\`Invariant violation: \${data.${fieldName}} not found in ${referencedTable}\`);
1102
+ }
1103
+
1104
+ // Compare ownership field (use database field name for referenced record)
1105
+ const dataOwnerValue = data.${ownershipFieldName};
1106
+ const refOwnerValue = referenced.${ownershipFieldName};
1107
+
1108
+ if (refOwnerValue !== dataOwnerValue) {
1109
+ throw new Error(
1110
+ \`Invariant violation: ${fieldName} must belong to the same ${ownershipFieldName} as the record. \` +
1111
+ \`Expected \${dataOwnerValue}, got \${refOwnerValue}\`
1112
+ );
1113
+ }
1114
+
1115
+ return true;
1116
+ }` };
1117
+ }
1118
+ case "cross_table_equality": {
1119
+ const { fieldName, referencedTable, referencedField, equalityFieldName, referencedEqualityFieldName } = logic;
1120
+ return { code: `/**
1121
+ * Guard: ${description}
1122
+ *
1123
+ * @param data - The record being created/updated
1124
+ * @param db - MongoDB database instance
1125
+ * @returns true if invariant is satisfied, throws error otherwise
1126
+ */
1127
+ export async function ${id}Guard(
1128
+ data: { ${fieldName}: string; ${equalityFieldName}: string },
1129
+ db: { collection(name: string): { findOne(filter: any): Promise<any | null> } }
1130
+ ): Promise<boolean> {
1131
+ if (!data.${fieldName}) {
1132
+ throw new Error(\`Invariant violation: ${fieldName} is required\`);
1133
+ }
1134
+
1135
+ const referenced = await db.collection("${referencedTable}").findOne({
1136
+ ${referencedField}: data.${fieldName}
1137
+ });
1138
+
1139
+ if (!referenced) {
1140
+ throw new Error(\`Invariant violation: Referenced record not found in ${referencedTable}\`);
1141
+ }
1142
+
1143
+ if (referenced.${referencedEqualityFieldName} !== data.${equalityFieldName}) {
1144
+ throw new Error(
1145
+ \`Invariant violation: ${equalityFieldName} must equal ${referencedTable}.${referencedEqualityFieldName}. \` +
1146
+ \`Expected \${referenced.${referencedEqualityFieldName}}, got \${data.${equalityFieldName}}\`
1147
+ );
1148
+ }
1149
+
1150
+ return true;
1151
+ }` };
1152
+ }
1153
+ case "raw": return { code: `/**
1154
+ * Guard: ${description}
1155
+ *
1156
+ * TODO: Implement this guard based on the invariant description.
1157
+ * This is a placeholder - you must implement the actual validation logic.
1158
+ *
1159
+ * @param data - The record being created/updated
1160
+ * @param db - MongoDB database instance
1161
+ * @returns true if invariant is satisfied, throws error otherwise
1162
+ */
1163
+ export async function ${id}Guard(
1164
+ data: any,
1165
+ db: { collection(name: string): { findOne(filter: any): Promise<any | null> } }
1166
+ ): Promise<boolean> {
1167
+ // TODO: Implement invariant: ${description}
1168
+ console.warn("Guard ${id}Guard is not yet implemented");
1169
+ return true;
1170
+ }` };
1171
+ default: return null;
1172
+ }
1173
+ }
1174
+
1175
+ //#endregion
1176
+ //#region src/generators/mongodb.ts
1177
+ const generateMongoDBSchema = async ({ options, file, adapter }) => {
1178
+ const tables = getPaymentTables$1(options);
1179
+ const filePath = file || "./mongodb-schema.ts";
1180
+ const invariants = normalizeInvariants(tables, {
1181
+ getModelName: initGetModelName({
1182
+ schema: tables,
1183
+ usePlural: adapter?.options?.adapterConfig?.usePlural
1184
+ }),
1185
+ getFieldName: initGetFieldName({
1186
+ schema: tables,
1187
+ usePlural: false
1188
+ })
1189
+ });
1190
+ const validators = emitMongoValidators(invariants);
1191
+ const validatorsJson = JSON.stringify(validators, null, 2);
1192
+ const guards = emitMongoGuards(invariants);
1193
+ const validatorsPath = filePath.replace(/\.ts$/, "-validators.json");
1194
+ const validatorsDir = path.dirname(path.resolve(process.cwd(), validatorsPath));
1195
+ await fs$1.mkdir(validatorsDir, { recursive: true });
1196
+ await fs$1.writeFile(path.resolve(process.cwd(), validatorsPath), validatorsJson);
1197
+ console.log(`📝 Generated MongoDB validators: ${validatorsPath}`);
1198
+ const guardsPath = filePath.replace(/\.ts$/, "-guards.ts");
1199
+ const guardsDir = path.dirname(path.resolve(process.cwd(), guardsPath));
1200
+ await fs$1.mkdir(guardsDir, { recursive: true });
1201
+ const formattedGuards = await prettier.format(guards, { parser: "typescript" });
1202
+ await fs$1.writeFile(path.resolve(process.cwd(), guardsPath), formattedGuards);
1203
+ console.log(`📝 Generated MongoDB guards: ${guardsPath}`);
1204
+ const validatorsBaseName = path.basename(validatorsPath, ".json");
1205
+ const guardsBaseName = path.basename(guardsPath, ".ts");
1206
+ const schemaCode = `// MongoDB schema setup
1207
+ // Generated from schema definitions
1208
+ // DO NOT EDIT MANUALLY - This file is auto-generated
1209
+
1210
+ import validators from "./${validatorsBaseName}.json";
1211
+ import * as guards from "./${guardsBaseName}";
1212
+
1213
+ /**
1214
+ * MongoDB collection validators.
1215
+ * Apply these using db.createCollection() or db.command({ collMod: ... })
1216
+ *
1217
+ * Example:
1218
+ * \`\`\`
1219
+ * await db.createCollection("checkout_session", {
1220
+ * validator: validators.checkout_session
1221
+ * });
1222
+ * \`\`\`
1223
+ */
1224
+ export { validators };
1225
+
1226
+ /**
1227
+ * MongoDB invariant guards.
1228
+ * Call these before write operations to enforce invariants at the application level.
1229
+ *
1230
+ * Example:
1231
+ * \`\`\`
1232
+ * import { customer_payment_method_ownershipGuard } from "./${guardsBaseName}";
1233
+ *
1234
+ * await customer_payment_method_ownershipGuard(data, db);
1235
+ * await db.collection("customer").insertOne(data);
1236
+ * \`\`\`
1237
+ */
1238
+ export { guards };
1239
+ `;
1240
+ return {
1241
+ code: await prettier.format(schemaCode, { parser: "typescript" }),
1242
+ fileName: filePath,
1243
+ overwrite: true
1244
+ };
1245
+ };
1246
+
601
1247
  //#endregion
602
1248
  //#region src/generators/index.ts
603
1249
  const adapters = {
604
1250
  prisma: generatePrismaSchema,
605
1251
  drizzle: generateDrizzleSchema,
606
- kysely: generateKyselySchema
1252
+ kysely: generateKyselySchema,
1253
+ mongodb: generateMongoDBSchema
607
1254
  };
608
1255
  const generateSchema = async (opts) => {
609
1256
  const adapter = opts.adapter;
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-D5zLfxo1.mjs";
2
+ import { i as getPackageInfo, n as generateSchema } from "./generators-DjXYs9VN.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.1.9",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "module": "dist/index.mjs",
6
6
  "main": "./dist/index.mjs",