pecunia-cli 0.3.8 → 0.4.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.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-CL-OPJxb.mjs";
1
+ import { a as generateKyselySchema, n as generateSchema, o as generateDrizzleSchema, r as generatePrismaSchema, t as adapters } from "./generators-EoVt1CPk.mjs";
2
2
 
3
3
  export { adapters, generateDrizzleSchema, generateKyselySchema, generatePrismaSchema, generateSchema };
@@ -30,12 +30,70 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
30
30
  schema: tables,
31
31
  usePlural: adapter.options?.adapterConfig?.usePlural
32
32
  });
33
- for (const tableKey in tables) {
33
+ const circularReferenceFields = new Set(["defaultPaymentMethodId", "defaultPriceId"]);
34
+ /**
35
+ * Build a safe table order to avoid forward-reference runtime errors.
36
+ * We do a simple topological sort by references, while ignoring references for
37
+ * known circular reference fields.
38
+ *
39
+ * If there is still a cycle (due to schema), we fall back to stable ordering
40
+ * by `order` and name.
41
+ */
42
+ function getSafeTableOrder(tableKeys) {
43
+ const deps = /* @__PURE__ */ new Map();
44
+ for (const key of tableKeys) {
45
+ deps.set(key, /* @__PURE__ */ new Set());
46
+ const table = tables[key];
47
+ for (const [fieldKey, field] of Object.entries(table.fields)) {
48
+ if (circularReferenceFields.has(fieldKey)) continue;
49
+ if (!field.references?.model) continue;
50
+ const refModel = field.references.model;
51
+ if (tables[refModel]) {
52
+ deps.get(key).add(refModel);
53
+ continue;
54
+ }
55
+ const found = tableKeys.find((k) => getModelName(k) === refModel);
56
+ if (found) deps.get(key).add(found);
57
+ }
58
+ }
59
+ const orderByMeta = (a, b) => {
60
+ const oa = tables[a]?.order ?? 0;
61
+ const ob = tables[b]?.order ?? 0;
62
+ if (oa !== ob) return oa - ob;
63
+ return a.localeCompare(b);
64
+ };
65
+ const result = [];
66
+ const perm = /* @__PURE__ */ new Set();
67
+ const temp = /* @__PURE__ */ new Set();
68
+ let hasCycle = false;
69
+ const visit = (n) => {
70
+ if (perm.has(n)) return;
71
+ if (temp.has(n)) {
72
+ hasCycle = true;
73
+ return;
74
+ }
75
+ temp.add(n);
76
+ const children = Array.from(deps.get(n) ?? []).sort(orderByMeta);
77
+ for (const m of children) visit(m);
78
+ temp.delete(n);
79
+ perm.add(n);
80
+ result.push(n);
81
+ };
82
+ for (const k of tableKeys.sort(orderByMeta)) visit(k);
83
+ if (hasCycle) return tableKeys.sort(orderByMeta);
84
+ return result;
85
+ }
86
+ const sortedTableKeys = getSafeTableOrder(Object.keys(tables));
87
+ function getIdColumn() {
88
+ if (databaseType === "pg") return `uuid("id").default(sql\`gen_random_uuid()\`).primaryKey()`;
89
+ if (databaseType === "mysql") return `varchar("id", { length: 36 }).primaryKey()`;
90
+ return `text("id").primaryKey()`;
91
+ }
92
+ for (const tableKey of sortedTableKeys) {
34
93
  const table = tables[tableKey];
35
94
  const modelName = getModelName(tableKey);
36
95
  const fields = table.fields;
37
96
  function getType(name, field) {
38
- if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`);
39
97
  name = convertToSnakeCase(name, adapter.options?.camelCase);
40
98
  if (field.references?.field === "id") {
41
99
  if (databaseType === "pg") return `uuid('${name}')`;
@@ -43,17 +101,19 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
43
101
  return `text('${name}')`;
44
102
  }
45
103
  const type = field.type;
46
- if (typeof type !== "string") if (Array.isArray(type) && type.every((x) => typeof x === "string")) return {
47
- sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
48
- pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
49
- mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])`
50
- }[databaseType];
51
- else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`);
104
+ if (typeof type !== "string") {
105
+ if (Array.isArray(type) && type.every((x) => typeof x === "string")) return {
106
+ sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
107
+ pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
108
+ mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])`
109
+ }[databaseType];
110
+ throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`);
111
+ }
52
112
  const dbTypeMap = {
53
113
  string: {
54
114
  sqlite: `text('${name}')`,
55
115
  pg: `text('${name}')`,
56
- mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : field.sortable ? `varchar('${name}', { length: 255 })` : field.index ? `varchar('${name}', { length: 255 })` : `text('${name}')`
116
+ mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : field.sortable || field.index ? `varchar('${name}', { length: 255 })` : `text('${name}')`
57
117
  },
58
118
  boolean: {
59
119
  sqlite: `integer('${name}', { mode: 'boolean' })`,
@@ -89,151 +149,124 @@ const generateDrizzleSchema = async ({ options, file, adapter }) => {
89
149
  if (!dbTypeMap) throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`);
90
150
  return dbTypeMap[databaseType];
91
151
  }
92
- let id = "";
93
- if (databaseType === "pg") id = `uuid("id").default(sql\`gen_random_uuid()\`).primaryKey()`;
94
- else if (databaseType === "mysql") id = `varchar("id", { length: 36 }).primaryKey()`;
95
- else id = `text("id").primaryKey()`;
96
- let indexes = [];
97
- const assignIndexes = (indexes$1) => {
98
- if (!indexes$1.length) return "";
99
- let code$1 = [`, (table) => [`];
100
- for (const index of indexes$1) code$1.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
101
- code$1.push(`]`);
102
- return code$1.join("\n");
152
+ const indexes = [];
153
+ const assignIndexes = (indexesToAssign) => {
154
+ if (!indexesToAssign.length) return "";
155
+ const lines = [`, (table) => [`];
156
+ for (const idx of indexesToAssign) lines.push(` ${idx.type}("${idx.name}").on(table.${idx.on}),`);
157
+ lines.push(`]`);
158
+ return lines.join("\n");
103
159
  };
104
- const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
105
- id: ${id},
106
- ${Object.keys(fields).filter((field) => field !== "id").map((field) => {
107
- const attr = fields[field];
108
- const fieldName = attr.fieldName || field;
160
+ const fieldLines = Object.keys(fields).filter((field) => field !== "id").map((fieldKey) => {
161
+ const attr = fields[fieldKey];
162
+ const fieldName = attr.fieldName || fieldKey;
109
163
  let type = getType(fieldName, attr);
110
- if (attr.index && !attr.unique) indexes.push({
111
- type: "index",
112
- name: `${modelName}_${fieldName}_idx`,
113
- on: fieldName
114
- });
115
- else if (attr.index && attr.unique) indexes.push({
116
- type: "uniqueIndex",
117
- name: `${modelName}_${fieldName}_uidx`,
118
- on: fieldName
119
- });
120
164
  if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
121
165
  if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
122
166
  else type += `.defaultNow()`;
123
167
  } else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`;
124
168
  else type += `.default(${attr.defaultValue})`;
125
- if (attr.onUpdate && attr.type === "date") {
126
- if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
127
- }
128
- return `${fieldName}: ${type}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
169
+ if (attr.onUpdate && attr.type === "date" && typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
170
+ if (attr.index && !attr.unique) indexes.push({
171
+ type: "index",
172
+ name: `${modelName}_${fieldName}_idx`,
173
+ on: fieldName
174
+ });
175
+ const isCircularReference = circularReferenceFields.has(fieldKey);
176
+ const referenceCode = !!attr.references && !isCircularReference ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
129
177
  model: attr.references.model,
130
178
  field: attr.references.field
131
- })}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
132
- }).join(",\n ")}
179
+ })}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : "";
180
+ return `${fieldName}: ${type}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${referenceCode}`;
181
+ });
182
+ const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
183
+ id: ${getIdColumn()},
184
+ ${fieldLines.join(",\n ")}
133
185
  }${assignIndexes(indexes)});`;
134
186
  code += `\n${schema}\n`;
135
187
  }
136
188
  let relationsString = "";
137
- for (const tableKey in tables) {
189
+ const normalizeManyKey = (key) => {
190
+ if (!adapter.options?.adapterConfig?.usePlural && !key.endsWith("s")) return `${key}s`;
191
+ return key;
192
+ };
193
+ for (const tableKey of sortedTableKeys) {
138
194
  const table = tables[tableKey];
139
195
  const modelName = getModelName(tableKey);
140
196
  const oneRelations = [];
141
197
  const manyRelations = [];
142
- const manyRelationsSet = /* @__PURE__ */ new Set();
143
- const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
144
- for (const [fieldName, field] of foreignFields) {
198
+ const manyKeys = /* @__PURE__ */ new Set();
199
+ for (const [fieldKey, field] of Object.entries(table.fields)) {
200
+ if (!field.references) continue;
145
201
  const referencedModel = field.references.model;
146
- const relationKey = getModelName(referencedModel);
147
- const fieldRef = `${getModelName(tableKey)}.${getFieldName({
148
- model: tableKey,
149
- field: fieldName
150
- })}`;
151
- const referenceRef = `${getModelName(referencedModel)}.${getFieldName({
152
- model: referencedModel,
153
- field: field.references.field || "id"
154
- })}`;
202
+ const referencedField = field.references.field || "id";
203
+ const key = getModelName(referencedModel);
155
204
  oneRelations.push({
156
- key: relationKey,
205
+ key,
157
206
  model: getModelName(referencedModel),
158
207
  type: "one",
159
208
  reference: {
160
- field: fieldRef,
161
- references: referenceRef,
162
- fieldName
209
+ field: `${getModelName(tableKey)}.${getFieldName({
210
+ model: tableKey,
211
+ field: fieldKey
212
+ })}`,
213
+ references: `${getModelName(referencedModel)}.${getFieldName({
214
+ model: referencedModel,
215
+ field: referencedField
216
+ })}`
163
217
  }
164
218
  });
165
219
  }
166
- const otherModels = Object.entries(tables).filter(([modelName$1]) => modelName$1 !== tableKey);
167
- const modelRelationsMap = /* @__PURE__ */ new Map();
168
- for (const [modelName$1, otherTable] of otherModels) {
169
- const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey));
170
- if (foreignKeysPointingHere.length === 0) continue;
171
- const hasUnique = foreignKeysPointingHere.some(([_, field]) => !!field.unique);
172
- const hasMany$1 = foreignKeysPointingHere.some(([_, field]) => !field.unique);
173
- modelRelationsMap.set(modelName$1, {
174
- modelName: modelName$1,
175
- hasUnique,
176
- hasMany: hasMany$1
220
+ for (const circularField of circularReferenceFields) {
221
+ if (!table.fields[circularField]) continue;
222
+ let referencedModel = null;
223
+ if (circularField === "defaultPaymentMethodId") referencedModel = "payment_method";
224
+ if (circularField === "defaultPriceId") referencedModel = "prices";
225
+ if (!referencedModel) continue;
226
+ const relationKey = circularField.replace(/Id$/, "");
227
+ oneRelations.push({
228
+ key: relationKey,
229
+ model: getModelName(referencedModel),
230
+ type: "one",
231
+ reference: {
232
+ field: `${getModelName(tableKey)}.${getFieldName({
233
+ model: tableKey,
234
+ field: circularField
235
+ })}`,
236
+ references: `${getModelName(referencedModel)}.${getFieldName({
237
+ model: referencedModel,
238
+ field: "id"
239
+ })}`
240
+ }
177
241
  });
178
242
  }
179
- for (const { modelName: modelName$1, hasMany: hasMany$1 } of modelRelationsMap.values()) {
180
- const relationType = hasMany$1 ? "many" : "one";
181
- let relationKey = getModelName(modelName$1);
182
- if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
183
- if (!manyRelationsSet.has(relationKey)) {
184
- manyRelationsSet.add(relationKey);
185
- manyRelations.push({
186
- key: relationKey,
187
- model: getModelName(modelName$1),
188
- type: relationType
189
- });
190
- }
191
- }
192
- const relationsByModel = /* @__PURE__ */ new Map();
193
- for (const relation of oneRelations) if (relation.reference) {
194
- const modelKey = relation.key;
195
- if (!relationsByModel.has(modelKey)) relationsByModel.set(modelKey, []);
196
- relationsByModel.get(modelKey).push(relation);
197
- }
198
- const duplicateRelations = [];
199
- const singleRelations = [];
200
- for (const [_modelKey, relations] of relationsByModel.entries()) if (relations.length > 1) duplicateRelations.push(...relations);
201
- else singleRelations.push(relations[0]);
202
- for (const relation of duplicateRelations) if (relation.reference) {
203
- const fieldName = relation.reference.fieldName;
204
- const tableRelation = `export const ${`${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`} = relations(${getModelName(table.modelName)}, ({ one }) => ({
205
- ${relation.key}: one(${relation.model}, {
206
- fields: [${relation.reference.field}],
207
- references: [${relation.reference.references}],
208
- })
209
- }))`;
210
- relationsString += `\n${tableRelation}\n`;
211
- }
212
- const hasOne = singleRelations.length > 0;
213
- const hasMany = manyRelations.length > 0;
214
- if (hasOne && hasMany) {
215
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one, many }) => ({
216
- ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
217
- fields: [${relation.reference.field}],
218
- references: [${relation.reference.references}],
219
- })` : "").filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""}
220
- ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
221
- }))`;
222
- relationsString += `\n${tableRelation}\n`;
223
- } else if (hasOne) {
224
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one }) => ({
225
- ${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
226
- fields: [${relation.reference.field}],
227
- references: [${relation.reference.references}],
228
- })` : "").filter((x) => x !== "").join(",\n ")}
229
- }))`;
230
- relationsString += `\n${tableRelation}\n`;
231
- } else if (hasMany) {
232
- const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ many }) => ({
233
- ${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
234
- }))`;
235
- relationsString += `\n${tableRelation}\n`;
243
+ for (const otherKey of sortedTableKeys) {
244
+ if (otherKey === tableKey) continue;
245
+ const otherTable = tables[otherKey];
246
+ if (!Object.entries(otherTable.fields).filter(([, f]) => f.references?.model === tableKey).length) continue;
247
+ let key = normalizeManyKey(getModelName(otherKey));
248
+ if (manyKeys.has(key)) continue;
249
+ manyKeys.add(key);
250
+ manyRelations.push({
251
+ key,
252
+ model: getModelName(otherKey),
253
+ type: "many"
254
+ });
236
255
  }
256
+ const oneLines = oneRelations.map((r) => {
257
+ if (!r.reference) return "";
258
+ return ` ${r.key}: one(${r.model}, {
259
+ fields: [${r.reference.field}],
260
+ references: [${r.reference.references}],
261
+ })`;
262
+ }).filter(Boolean);
263
+ const manyLines = manyRelations.map((r) => ` ${r.key}: many(${r.model})`);
264
+ if (!oneLines.length && !manyLines.length) continue;
265
+ const args = oneLines.length && manyLines.length ? "({ one, many })" : oneLines.length ? "({ one })" : "({ many })";
266
+ const body = [...oneLines, ...manyLines].join(",\n");
267
+ relationsString += `\nexport const ${modelName}Relations = relations(${getModelName(table.modelName)}, ${args} => ({
268
+ ${body}
269
+ }));\n`;
237
270
  }
238
271
  code += `\n${relationsString}`;
239
272
  return {
@@ -255,27 +288,29 @@ function generateImport({ databaseType, tables }) {
255
288
  if (hasJson && hasBigint) break;
256
289
  }
257
290
  coreImports.push(`${databaseType}Table`);
258
- coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
259
- coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
260
- coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
291
+ if (databaseType === "mysql") coreImports.push("varchar, text");
292
+ else coreImports.push("text");
293
+ if (databaseType === "pg") coreImports.push("uuid");
294
+ if (databaseType === "sqlite") coreImports.push("integer");
295
+ coreImports.push(hasBigint && databaseType !== "sqlite" ? "bigint" : "");
296
+ if (databaseType !== "sqlite") coreImports.push("timestamp, boolean");
261
297
  if (databaseType === "mysql") {
262
298
  if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint))) coreImports.push("int");
263
299
  if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => typeof field.type !== "string" && Array.isArray(field.type) && field.type.every((x) => typeof x === "string")))) coreImports.push("mysqlEnum");
264
300
  } else if (databaseType === "pg") {
265
301
  rootImports.push("sql");
266
- coreImports.push("uuid");
267
302
  if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint))) coreImports.push("integer");
268
- } else coreImports.push("integer");
303
+ }
269
304
  if (hasJson) {
270
305
  if (databaseType === "pg") coreImports.push("jsonb");
271
306
  if (databaseType === "mysql") coreImports.push("json");
272
307
  }
273
308
  if (databaseType === "sqlite" && Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.type === "date" && field.defaultValue && typeof field.defaultValue === "function" && field.defaultValue.toString().includes("new Date()")))) rootImports.push("sql");
274
309
  const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique));
275
- const hasUniqueIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique && field.index));
310
+ const hasUniqueOnColumn = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique));
276
311
  if (hasIndexes) coreImports.push("index");
277
- if (hasUniqueIndexes) coreImports.push("uniqueIndex");
278
- return `${rootImports.length > 0 ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";\n`;
312
+ if (hasUniqueOnColumn) {}
313
+ return `${rootImports.length ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports.map((x) => x.trim()).filter(Boolean).join(", ")} } from "drizzle-orm/${databaseType}-core";\n`;
279
314
  }
280
315
 
281
316
  //#endregion
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-CL-OPJxb.mjs";
2
+ import { i as getPackageInfo, n as generateSchema } from "./generators-EoVt1CPk.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.3.8",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "module": "dist/index.mjs",
6
6
  "main": "./dist/index.mjs",