@3lineas/d1-orm 1.0.11 → 1.0.12

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/index.js CHANGED
@@ -134,7 +134,7 @@ import * as p3 from "@clack/prompts";
134
134
  import * as fs2 from "fs";
135
135
  import * as path2 from "path";
136
136
  import * as p2 from "@clack/prompts";
137
- async function makeMigration(name) {
137
+ async function makeMigration(name, fields) {
138
138
  const isStandalone = !name;
139
139
  if (isStandalone) {
140
140
  p2.intro("Creating a new migration...");
@@ -159,17 +159,38 @@ async function makeMigration(name) {
159
159
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").split(".")[0].replace("T", "_");
160
160
  const filename = `${timestamp}_${migrationName}.mts`;
161
161
  const targetPath = path2.join(process.cwd(), migrationsDir, filename);
162
+ const tableName = migrationName.replace("create_", "").replace("_table", "").replace("add_", "").replace("_to_", "").replace("_table", "");
163
+ const isAlter = migrationName.startsWith("add_");
164
+ let fieldsSql = "";
165
+ if (fields && fields.length > 0) {
166
+ fieldsSql = fields.map((f) => {
167
+ let line = "";
168
+ if (f.type === "enum") {
169
+ line = ` table.enum('${f.name}', ${JSON.stringify(f.values)})`;
170
+ } else {
171
+ line = ` table.${f.type}('${f.name}')`;
172
+ }
173
+ if (f.unique) line += ".unique()";
174
+ if (f.nullable) line += ".nullable()";
175
+ if (f.default !== void 0 && f.default !== "") {
176
+ const defVal = typeof f.default === "string" ? `'${f.default}'` : f.default;
177
+ line += `.default(${defVal})`;
178
+ }
179
+ return line + ";";
180
+ }).join("\n");
181
+ }
182
+ const method = isAlter ? "table" : "create";
183
+ const downSql = isAlter ? `// return Schema.table('${tableName}', (table: Blueprint) => { /* drop columns not supported in simple SQLite ALTER */ });` : `return Schema.dropIfExists('${tableName}');`;
162
184
  const template = `import { Blueprint, Schema } from '@3lineas/d1-orm';
163
185
 
164
186
  export const up = async () => {
165
- return Schema.create('${migrationName.replace("create_", "").replace("_table", "")}', (table: Blueprint) => {
166
- table.id();
167
- table.timestamps();
168
- });
187
+ return Schema.${method}('${tableName}', (table: Blueprint) => {
188
+ ${!isAlter ? " table.id();\n" : ""}${fieldsSql}
189
+ ${!isAlter ? " table.timestamps();\n" : ""} });
169
190
  };
170
191
 
171
192
  export const down = async () => {
172
- return Schema.dropIfExists('${migrationName.replace("create_", "").replace("_table", "")}');
193
+ ${downSql}
173
194
  };
174
195
  `;
175
196
  if (!fs2.existsSync(path2.join(process.cwd(), migrationsDir))) {
@@ -209,46 +230,245 @@ async function makeModel(name) {
209
230
  const modelPath = await findModelsPath() || "src/database/models";
210
231
  const filename = `${modelName}.mts`;
211
232
  const targetPath = path3.join(process.cwd(), modelPath, filename);
233
+ if (fs3.existsSync(targetPath)) {
234
+ p3.log.error(`Model ${filename} already exists at ${modelPath}.`);
235
+ p3.outro("Process aborted.");
236
+ return;
237
+ }
238
+ const fields = [];
239
+ const relations = [];
240
+ let addMore = true;
241
+ p3.log.step("Let's add some attributes to your model!");
242
+ while (addMore) {
243
+ const fieldName = await p3.text({
244
+ message: "New attribute or relation name (leave empty to stop):",
245
+ placeholder: "e.g. title or posts"
246
+ });
247
+ if (p3.isCancel(fieldName)) break;
248
+ if (!fieldName) break;
249
+ const fieldType = await p3.select({
250
+ message: `Type for ${fieldName}:`,
251
+ options: [
252
+ { value: "string", label: "String" },
253
+ { value: "integer", label: "Integer" },
254
+ { value: "boolean", label: "Boolean" },
255
+ { value: "text", label: "Text" },
256
+ { value: "float", label: "Float" },
257
+ { value: "json", label: "JSON" },
258
+ { value: "enum", label: "Enum" },
259
+ { value: "date", label: "Date" },
260
+ { value: "blob", label: "Blob" },
261
+ { value: "belongsToMany", label: "ManyToMany (Pivot Table)" },
262
+ { value: "relation", label: "Relation (1:1, 1:N)" }
263
+ ]
264
+ });
265
+ if (p3.isCancel(fieldType)) break;
266
+ if (fieldType === "relation" || fieldType === "belongsToMany") {
267
+ let relType = fieldType;
268
+ if (fieldType === "relation") {
269
+ relType = await p3.select({
270
+ message: `Relation type for ${fieldName}:`,
271
+ options: [
272
+ { value: "hasOne", label: "HasOne" },
273
+ { value: "hasMany", label: "HasMany" },
274
+ { value: "belongsTo", label: "BelongsTo" }
275
+ ]
276
+ });
277
+ }
278
+ if (p3.isCancel(relType)) break;
279
+ const modelsPath = await findModelsPath() || "src/database/models";
280
+ const availableModels = fs3.existsSync(
281
+ path3.join(process.cwd(), modelsPath)
282
+ ) ? fs3.readdirSync(path3.join(process.cwd(), modelPath)).map((f) => f.replace(".mts", "").replace(".ts", "")) : [];
283
+ const targetModel = await p3.text({
284
+ message: "Target model for this relation:",
285
+ placeholder: "e.g. Post",
286
+ validate: (v) => !v ? "Target model is required" : void 0
287
+ });
288
+ if (p3.isCancel(targetModel)) break;
289
+ let foreignKey = "";
290
+ let pivotTable = "";
291
+ if (relType === "belongsToMany") {
292
+ p3.log.info("Automatically creating a pivot table migration.");
293
+ pivotTable = await p3.text({
294
+ message: "Pivot table name:",
295
+ initialValue: [modelName.toLowerCase(), targetModel.toLowerCase()].sort().join("_")
296
+ });
297
+ if (p3.isCancel(pivotTable)) break;
298
+ } else if (relType === "belongsTo") {
299
+ foreignKey = await p3.text({
300
+ message: "Foreign key column name:",
301
+ placeholder: `e.g. ${targetModel.toLowerCase()}_id`,
302
+ initialValue: `${targetModel.toLowerCase()}_id`
303
+ });
304
+ if (p3.isCancel(foreignKey)) break;
305
+ fields.push({
306
+ name: foreignKey,
307
+ type: "foreign",
308
+ nullable: false,
309
+ unique: false
310
+ });
311
+ }
312
+ relations.push({
313
+ method: fieldName,
314
+ type: relType,
315
+ model: targetModel,
316
+ foreignKey,
317
+ pivotTable
318
+ });
319
+ const addInverse = await p3.confirm({
320
+ message: `Add the inverse relation in ${targetModel}.mts?`,
321
+ initialValue: true
322
+ });
323
+ if (addInverse && !p3.isCancel(addInverse)) {
324
+ let inverseType = "belongsTo";
325
+ if (relType === "belongsTo") inverseType = "hasMany";
326
+ if (relType === "hasOne") inverseType = "belongsTo";
327
+ if (relType === "belongsToMany") inverseType = "belongsToMany";
328
+ const inverseMethod = await p3.text({
329
+ message: "Inverse method name:",
330
+ initialValue: relType === "belongsTo" ? modelName.toLowerCase() + "s" : modelName.toLowerCase()
331
+ });
332
+ if (!p3.isCancel(inverseMethod)) {
333
+ await updateTargetModel(
334
+ targetModel,
335
+ inverseMethod,
336
+ inverseType,
337
+ modelName,
338
+ pivotTable
339
+ );
340
+ }
341
+ }
342
+ } else {
343
+ let enumValues = [];
344
+ if (fieldType === "enum") {
345
+ const valuesStr = await p3.text({
346
+ message: "Allowed values (comma separated):",
347
+ placeholder: "active, inactive, pending"
348
+ });
349
+ if (p3.isCancel(valuesStr)) break;
350
+ enumValues = valuesStr.split(",").map((v) => v.trim());
351
+ }
352
+ const isNullable = await p3.confirm({
353
+ message: "Is it nullable?",
354
+ initialValue: false
355
+ });
356
+ if (p3.isCancel(isNullable)) break;
357
+ const isUnique = await p3.confirm({
358
+ message: "Is it unique?",
359
+ initialValue: false
360
+ });
361
+ if (p3.isCancel(isUnique)) break;
362
+ fields.push({
363
+ name: fieldName,
364
+ type: fieldType,
365
+ nullable: isNullable,
366
+ unique: isUnique,
367
+ values: enumValues
368
+ });
369
+ }
370
+ }
371
+ const fieldProperties = fields.filter(
372
+ (f) => f.type !== "foreign" || !relations.some((r) => r.foreignKey === f.name)
373
+ ).map((f) => {
374
+ let tsType = "string";
375
+ if (f.type === "integer" || f.type === "float") tsType = "number";
376
+ else if (f.type === "boolean") tsType = "boolean";
377
+ else if (f.type === "json") tsType = "any";
378
+ else if (f.type === "blob") tsType = "Uint8Array";
379
+ return ` declare ${f.name}${f.nullable ? "?" : ""}: ${tsType};`;
380
+ }).join("\n");
381
+ const relationMethods = relations.map((r) => {
382
+ if (r.type === "belongsToMany") {
383
+ return ` ${r.method}() {
384
+ return this.belongsToMany(${r.model}, '${r.pivotTable}');
385
+ }`;
386
+ }
387
+ return ` ${r.method}() {
388
+ return this.${r.type}(${r.model});
389
+ }`;
390
+ }).join("\n\n");
212
391
  const template = `import { Model } from '@3lineas/d1-orm';
392
+ ${relations.map((r) => `import { ${r.model} } from './${r.model}.mts';`).join("\n")}
213
393
 
214
394
  export class ${modelName} extends Model {
215
395
  // protected static table = '${modelName.toLowerCase()}s';
216
396
 
217
- // declare id: number;
218
- // declare created_at: string;
219
- // declare updated_at: string;
397
+ declare id: number;
398
+ ${fieldProperties}
399
+ declare created_at: string;
400
+ declare updated_at: string;
401
+
402
+ ${relationMethods}
220
403
  }
221
404
  `;
222
- if (fs3.existsSync(targetPath)) {
223
- p3.log.error(`Model ${filename} already exists at ${modelPath}.`);
224
- p3.outro("Process aborted.");
225
- return;
226
- }
227
405
  if (!fs3.existsSync(path3.join(process.cwd(), modelPath))) {
228
406
  fs3.mkdirSync(path3.join(process.cwd(), modelPath), { recursive: true });
229
407
  }
230
408
  fs3.writeFileSync(targetPath, template);
231
409
  p3.log.success(`Created model: ${modelPath}/${filename}`);
232
- const options = await p3.multiselect({
233
- message: "Would you like to create any of the following?",
234
- options: [
235
- { value: "migration", label: "Migration" },
236
- { value: "seeder", label: "Database Seeder" }
237
- ],
238
- required: false
410
+ const migrationName = `create_${modelName.toLowerCase()}s_table`;
411
+ await makeMigration(migrationName, fields);
412
+ for (const rel of relations) {
413
+ if (rel.type === "belongsToMany") {
414
+ const pivotMigrationName = `create_${rel.pivotTable}_table`;
415
+ const pivotFields = [
416
+ {
417
+ name: `${modelName.toLowerCase()}_id`,
418
+ type: "integer",
419
+ nullable: false
420
+ },
421
+ {
422
+ name: `${rel.model.toLowerCase()}_id`,
423
+ type: "integer",
424
+ nullable: false
425
+ }
426
+ ];
427
+ await makeMigration(pivotMigrationName, pivotFields);
428
+ }
429
+ }
430
+ const wantSeeder = await p3.confirm({
431
+ message: "Would you like to create a seeder for this model?",
432
+ initialValue: true
239
433
  });
240
- if (p3.isCancel(options)) {
241
- p3.cancel("Operation cancelled.");
434
+ if (wantSeeder) {
435
+ await makeSeeder(modelName, modelPath, fields);
436
+ }
437
+ p3.outro("Model generation complete!");
438
+ }
439
+ async function updateTargetModel(targetModel, methodName, relType, sourceModel, pivotTable = "") {
440
+ const modelPath = await findModelsPath() || "src/database/models";
441
+ const filename = `${targetModel}.mts`;
442
+ const targetPath = path3.join(process.cwd(), modelPath, filename);
443
+ if (!fs3.existsSync(targetPath)) {
444
+ p3.log.warn(
445
+ `Model ${targetModel} not found at ${modelPath}. Skipping inverse relation.`
446
+ );
242
447
  return;
243
448
  }
244
- if (options.includes("migration")) {
245
- const migrationName = `create_${modelName.toLowerCase()}s_table`;
246
- await makeMigration(migrationName);
449
+ let content = fs3.readFileSync(targetPath, "utf-8");
450
+ if (!content.includes(`import { ${sourceModel} }`)) {
451
+ content = `import { ${sourceModel} } from './${sourceModel}.mts';
452
+ ` + content;
247
453
  }
248
- if (options.includes("seeder")) {
249
- await makeSeeder(modelName, modelPath);
454
+ let methodString = "";
455
+ if (relType === "belongsToMany") {
456
+ methodString = ` ${methodName}() {
457
+ return this.belongsToMany(${sourceModel}, '${pivotTable}');
458
+ }`;
459
+ } else {
460
+ methodString = ` ${methodName}() {
461
+ return this.${relType}(${sourceModel});
462
+ }`;
463
+ }
464
+ const lastBraceIndex = content.lastIndexOf("}");
465
+ if (lastBraceIndex !== -1) {
466
+ content = content.substring(0, lastBraceIndex) + "\n" + methodString + "\n" + content.substring(lastBraceIndex);
467
+ fs3.writeFileSync(targetPath, content);
468
+ p3.log.success(
469
+ `Updated ${targetModel}.mts with inverse relation: ${methodName}()`
470
+ );
250
471
  }
251
- p3.outro("Model generation complete!");
252
472
  }
253
473
  async function findModelsPath() {
254
474
  const commonPaths = [
@@ -258,12 +478,12 @@ async function findModelsPath() {
258
478
  "models",
259
479
  "app/Models"
260
480
  ];
261
- for (const p7 of commonPaths) {
262
- if (fs3.existsSync(path3.join(process.cwd(), p7))) return p7;
481
+ for (const p8 of commonPaths) {
482
+ if (fs3.existsSync(path3.join(process.cwd(), p8))) return p8;
263
483
  }
264
484
  return null;
265
485
  }
266
- async function makeSeeder(modelName, modelPath) {
486
+ async function makeSeeder(modelName, modelPath, fields = []) {
267
487
  const srcPath = path3.join(process.cwd(), "src");
268
488
  const useSrc = fs3.existsSync(srcPath) && fs3.lstatSync(srcPath).isDirectory();
269
489
  const seederDir = useSrc ? path3.join(process.cwd(), "src/database/seeders") : path3.join(process.cwd(), "database/seeders");
@@ -273,10 +493,25 @@ async function makeSeeder(modelName, modelPath) {
273
493
  fs3.mkdirSync(seederDir, { recursive: true });
274
494
  }
275
495
  const relativeModelPath = path3.relative(seederDir, path3.join(process.cwd(), modelPath, modelName)).replace(/\\/g, "/");
496
+ const dummyData = fields.map((f) => {
497
+ let val = "''";
498
+ if (f.type === "integer" || f.type === "foreign") val = "1";
499
+ if (f.type === "float") val = "1.5";
500
+ if (f.type === "boolean") val = "true";
501
+ if (f.type === "string" || f.type === "text") val = `'Sample ${f.name}'`;
502
+ if (f.type === "json") val = "'{}'";
503
+ if (f.type === "enum")
504
+ val = f.values && f.values.length > 0 ? `'${f.values[0]}'` : "''";
505
+ if (f.type === "date" || f.type === "datetime")
506
+ val = `'${(/* @__PURE__ */ new Date()).toISOString()}'`;
507
+ return ` ${f.name}: ${val},`;
508
+ }).join("\n");
276
509
  const template = `import { ${modelName} } from '${relativeModelPath}.mts';
277
510
 
278
511
  export const seed = async () => {
279
- // await ${modelName}.create({ ... });
512
+ await ${modelName}.create({
513
+ ${dummyData}
514
+ });
280
515
  };
281
516
  `;
282
517
  if (fs3.existsSync(targetPath)) {
@@ -284,18 +519,281 @@ export const seed = async () => {
284
519
  return;
285
520
  }
286
521
  fs3.writeFileSync(targetPath, template);
287
- p3.log.success(`Created seeder: database/seeders/${seederName}`);
522
+ p3.log.success(`Created seeder: ${seederDir}/${seederName}`);
523
+ }
524
+
525
+ // src/cli/commands/model-add.ts
526
+ import * as fs4 from "fs";
527
+ import * as path4 from "path";
528
+ import * as p4 from "@clack/prompts";
529
+ async function modelAdd() {
530
+ p4.intro("Adding attributes to an existing model...");
531
+ const modelPath = await findModelsPath2() || "src/database/models";
532
+ const fullModelPath = path4.join(process.cwd(), modelPath);
533
+ if (!fs4.existsSync(fullModelPath)) {
534
+ p4.log.error(`Models directory not found at ${modelPath}.`);
535
+ return;
536
+ }
537
+ const modelFiles = fs4.readdirSync(fullModelPath).filter((f) => f.endsWith(".mts") || f.endsWith(".ts"));
538
+ const options = modelFiles.map((f) => ({
539
+ value: f,
540
+ label: f
541
+ }));
542
+ options.unshift({ value: "new", label: "[ Create new model ]" });
543
+ const selectedModel = await p4.select({
544
+ message: "Select a model to add attributes to:",
545
+ options
546
+ });
547
+ if (p4.isCancel(selectedModel)) {
548
+ p4.cancel("Operation cancelled.");
549
+ return;
550
+ }
551
+ if (selectedModel === "new") {
552
+ await makeModel();
553
+ return;
554
+ }
555
+ const modelName = selectedModel.replace(".mts", "").replace(".ts", "");
556
+ const targetPath = path4.join(fullModelPath, selectedModel);
557
+ const fields = [];
558
+ const relations = [];
559
+ let addMore = true;
560
+ p4.log.step(`Adding attributes to ${modelName}...`);
561
+ while (addMore) {
562
+ const fieldName = await p4.text({
563
+ message: "New attribute or relation name (leave empty to stop):",
564
+ placeholder: "e.g. description or comments"
565
+ });
566
+ if (p4.isCancel(fieldName)) break;
567
+ if (!fieldName) break;
568
+ const fieldType = await p4.select({
569
+ message: `Type for ${fieldName}:`,
570
+ options: [
571
+ { value: "string", label: "String" },
572
+ { value: "integer", label: "Integer" },
573
+ { value: "boolean", label: "Boolean" },
574
+ { value: "text", label: "Text" },
575
+ { value: "float", label: "Float" },
576
+ { value: "json", label: "JSON" },
577
+ { value: "enum", label: "Enum" },
578
+ { value: "date", label: "Date" },
579
+ { value: "blob", label: "Blob" },
580
+ { value: "belongsToMany", label: "ManyToMany (Pivot Table)" },
581
+ { value: "relation", label: "Relation (1:1, 1:N)" }
582
+ ]
583
+ });
584
+ if (p4.isCancel(fieldType)) break;
585
+ if (fieldType === "relation" || fieldType === "belongsToMany") {
586
+ let relType = fieldType;
587
+ if (fieldType === "relation") {
588
+ relType = await p4.select({
589
+ message: `Relation type for ${fieldName}:`,
590
+ options: [
591
+ { value: "hasOne", label: "HasOne" },
592
+ { value: "hasMany", label: "HasMany" },
593
+ { value: "belongsTo", label: "BelongsTo" }
594
+ ]
595
+ });
596
+ }
597
+ if (p4.isCancel(relType)) break;
598
+ const targetModel = await p4.text({
599
+ message: "Target model for this relation:",
600
+ placeholder: "e.g. Comment",
601
+ validate: (v) => !v ? "Target model is required" : void 0
602
+ });
603
+ if (p4.isCancel(targetModel)) break;
604
+ let foreignKey = "";
605
+ let pivotTable = "";
606
+ if (relType === "belongsToMany") {
607
+ p4.log.info("Automatically creating a pivot table migration.");
608
+ pivotTable = await p4.text({
609
+ message: "Pivot table name:",
610
+ initialValue: [modelName.toLowerCase(), targetModel.toLowerCase()].sort().join("_")
611
+ });
612
+ if (p4.isCancel(pivotTable)) break;
613
+ } else if (relType === "belongsTo") {
614
+ foreignKey = await p4.text({
615
+ message: "Foreign key column name:",
616
+ placeholder: `e.g. ${targetModel.toLowerCase()}_id`,
617
+ initialValue: `${targetModel.toLowerCase()}_id`
618
+ });
619
+ if (p4.isCancel(foreignKey)) break;
620
+ fields.push({
621
+ name: foreignKey,
622
+ type: "foreign",
623
+ nullable: false,
624
+ unique: false
625
+ });
626
+ }
627
+ relations.push({
628
+ method: fieldName,
629
+ type: relType,
630
+ model: targetModel,
631
+ foreignKey,
632
+ pivotTable
633
+ });
634
+ const addInverse = await p4.confirm({
635
+ message: `Add the inverse relation in ${targetModel}.mts?`,
636
+ initialValue: true
637
+ });
638
+ if (addInverse && !p4.isCancel(addInverse)) {
639
+ let inverseType = "belongsTo";
640
+ if (relType === "belongsTo") inverseType = "hasMany";
641
+ if (relType === "hasOne") inverseType = "belongsTo";
642
+ if (relType === "belongsToMany") inverseType = "belongsToMany";
643
+ const inverseMethod = await p4.text({
644
+ message: "Inverse method name:",
645
+ initialValue: relType === "belongsTo" ? modelName.toLowerCase() + "s" : modelName.toLowerCase()
646
+ });
647
+ if (!p4.isCancel(inverseMethod)) {
648
+ await updateTargetModel2(
649
+ targetModel,
650
+ inverseMethod,
651
+ inverseType,
652
+ modelName,
653
+ pivotTable
654
+ );
655
+ }
656
+ }
657
+ } else {
658
+ let enumValues = [];
659
+ if (fieldType === "enum") {
660
+ const valuesStr = await p4.text({
661
+ message: "Allowed values (comma separated):",
662
+ placeholder: "active, inactive, pending"
663
+ });
664
+ if (p4.isCancel(valuesStr)) break;
665
+ enumValues = valuesStr.split(",").map((v) => v.trim());
666
+ }
667
+ const isNullable = await p4.confirm({
668
+ message: "Is it nullable?",
669
+ initialValue: false
670
+ });
671
+ if (p4.isCancel(isNullable)) break;
672
+ const isUnique = await p4.confirm({
673
+ message: "Is it unique?",
674
+ initialValue: false
675
+ });
676
+ if (p4.isCancel(isUnique)) break;
677
+ fields.push({
678
+ name: fieldName,
679
+ type: fieldType,
680
+ nullable: isNullable,
681
+ unique: isUnique,
682
+ values: enumValues
683
+ });
684
+ }
685
+ }
686
+ if (fields.length === 0 && relations.length === 0) {
687
+ p4.outro("No changes made.");
688
+ return;
689
+ }
690
+ let modelContent = fs4.readFileSync(targetPath, "utf-8");
691
+ const newImports = relations.filter((r) => !modelContent.includes(`import { ${r.model} }`)).map((r) => `import { ${r.model} } from './${r.model}.mts';`).join("\n");
692
+ if (newImports) {
693
+ modelContent = newImports + "\n" + modelContent;
694
+ }
695
+ const fieldProperties = fields.filter(
696
+ (f) => f.type !== "foreign" || !relations.some((r) => r.foreignKey === f.name)
697
+ ).map((f) => {
698
+ let tsType = "string";
699
+ if (f.type === "integer" || f.type === "float") tsType = "number";
700
+ else if (f.type === "boolean") tsType = "boolean";
701
+ else if (f.type === "json") tsType = "any";
702
+ else if (f.type === "blob") tsType = "Uint8Array";
703
+ return ` declare ${f.name}${f.nullable ? "?" : ""}: ${tsType};`;
704
+ }).join("\n");
705
+ const relationMethods = relations.map((r) => {
706
+ return ` ${r.method}() {
707
+ return this.${r.type}(${r.model});
708
+ }`;
709
+ }).join("\n\n");
710
+ const lastBraceIndex = modelContent.lastIndexOf("}");
711
+ if (lastBraceIndex !== -1) {
712
+ modelContent = modelContent.substring(0, lastBraceIndex) + fieldProperties + "\n" + relationMethods + "\n" + modelContent.substring(lastBraceIndex);
713
+ fs4.writeFileSync(targetPath, modelContent);
714
+ p4.log.success(`Updated model: ${modelPath}/${selectedModel}`);
715
+ }
716
+ if (fields.length > 0) {
717
+ const migrationName = `add_fields_to_${modelName.toLowerCase()}s_table`;
718
+ await makeMigration(migrationName, fields);
719
+ }
720
+ for (const rel of relations) {
721
+ if (rel.type === "belongsToMany") {
722
+ const pivotMigrationName = `create_${rel.pivotTable}_table`;
723
+ const pivotFields = [
724
+ {
725
+ name: `${modelName.toLowerCase()}_id`,
726
+ type: "integer",
727
+ nullable: false
728
+ },
729
+ {
730
+ name: `${rel.model.toLowerCase()}_id`,
731
+ type: "integer",
732
+ nullable: false
733
+ }
734
+ ];
735
+ await makeMigration(pivotMigrationName, pivotFields);
736
+ }
737
+ }
738
+ p4.outro("Model update complete!");
739
+ }
740
+ async function updateTargetModel2(targetModel, methodName, relType, sourceModel, pivotTable = "") {
741
+ const modelPath = await findModelsPath2() || "src/database/models";
742
+ const filename = `${targetModel}.mts`;
743
+ const targetPath = path4.join(process.cwd(), modelPath, filename);
744
+ if (!fs4.existsSync(targetPath)) {
745
+ p4.log.warn(
746
+ `Model ${targetModel} not found at ${modelPath}. Skipping inverse relation.`
747
+ );
748
+ return;
749
+ }
750
+ let content = fs4.readFileSync(targetPath, "utf-8");
751
+ if (!content.includes(`import { ${sourceModel} }`)) {
752
+ content = `import { ${sourceModel} } from './${sourceModel}.mts';
753
+ ` + content;
754
+ }
755
+ let methodString = "";
756
+ if (relType === "belongsToMany") {
757
+ methodString = ` ${methodName}() {
758
+ return this.belongsToMany(${sourceModel}, '${pivotTable}');
759
+ }`;
760
+ } else {
761
+ methodString = ` ${methodName}() {
762
+ return this.${relType}(${sourceModel});
763
+ }`;
764
+ }
765
+ const lastBraceIndex = content.lastIndexOf("}");
766
+ if (lastBraceIndex !== -1) {
767
+ content = content.substring(0, lastBraceIndex) + "\n" + methodString + "\n" + content.substring(lastBraceIndex);
768
+ fs4.writeFileSync(targetPath, content);
769
+ p4.log.success(
770
+ `Updated ${targetModel}.mts with inverse relation: ${methodName}()`
771
+ );
772
+ }
773
+ }
774
+ async function findModelsPath2() {
775
+ const commonPaths = [
776
+ "src/database/models",
777
+ "database/models",
778
+ "src/models",
779
+ "models",
780
+ "app/Models"
781
+ ];
782
+ for (const p8 of commonPaths) {
783
+ if (fs4.existsSync(path4.join(process.cwd(), p8))) return p8;
784
+ }
785
+ return null;
288
786
  }
289
787
 
290
788
  // src/cli/commands/migrate.ts
291
- import * as fs6 from "fs";
292
- import * as path6 from "path";
789
+ import * as fs7 from "fs";
790
+ import * as path7 from "path";
293
791
  import { execSync as execSync2 } from "child_process";
294
- import * as p5 from "@clack/prompts";
792
+ import * as p6 from "@clack/prompts";
295
793
 
296
794
  // src/cli/utils/config.ts
297
- import * as fs4 from "fs";
298
- import * as path4 from "path";
795
+ import * as fs5 from "fs";
796
+ import * as path5 from "path";
299
797
  var Config = class {
300
798
  /**
301
799
  * Detect the D1 database binding from Wrangler configuration or d1-orm config.
@@ -303,21 +801,21 @@ var Config = class {
303
801
  * @returns The binding name (e.g., "DB").
304
802
  */
305
803
  static getD1Binding() {
306
- const srcConfig = path4.join(process.cwd(), "src/database/config.mts");
307
- const srcConfigTs = path4.join(process.cwd(), "src/database/config.ts");
308
- const rootConfig = path4.join(process.cwd(), "database/config.mts");
309
- const rootConfigTs = path4.join(process.cwd(), "database/config.ts");
310
- const configPath = fs4.existsSync(srcConfig) ? srcConfig : fs4.existsSync(srcConfigTs) ? srcConfigTs : fs4.existsSync(rootConfig) ? rootConfig : fs4.existsSync(rootConfigTs) ? rootConfigTs : null;
804
+ const srcConfig = path5.join(process.cwd(), "src/database/config.mts");
805
+ const srcConfigTs = path5.join(process.cwd(), "src/database/config.ts");
806
+ const rootConfig = path5.join(process.cwd(), "database/config.mts");
807
+ const rootConfigTs = path5.join(process.cwd(), "database/config.ts");
808
+ const configPath = fs5.existsSync(srcConfig) ? srcConfig : fs5.existsSync(srcConfigTs) ? srcConfigTs : fs5.existsSync(rootConfig) ? rootConfig : fs5.existsSync(rootConfigTs) ? rootConfigTs : null;
311
809
  if (configPath) {
312
- const content = fs4.readFileSync(configPath, "utf-8");
810
+ const content = fs5.readFileSync(configPath, "utf-8");
313
811
  const match = content.match(/binding\s*:\s*["'](.+?)["']/);
314
812
  if (match) return match[1];
315
813
  }
316
814
  const wranglerPaths = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
317
815
  for (const configName of wranglerPaths) {
318
- const fullPath = path4.join(process.cwd(), configName);
319
- if (fs4.existsSync(fullPath)) {
320
- const content = fs4.readFileSync(fullPath, "utf-8");
816
+ const fullPath = path5.join(process.cwd(), configName);
817
+ if (fs5.existsSync(fullPath)) {
818
+ const content = fs5.readFileSync(fullPath, "utf-8");
321
819
  if (configName.endsWith(".json") || configName.endsWith(".jsonc")) {
322
820
  try {
323
821
  const jsonStr = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
@@ -355,10 +853,10 @@ var Config = class {
355
853
  * Detect if the project is ESM.
356
854
  */
357
855
  static isESM() {
358
- const pkgPath = path4.join(process.cwd(), "package.json");
359
- if (fs4.existsSync(pkgPath)) {
856
+ const pkgPath = path5.join(process.cwd(), "package.json");
857
+ if (fs5.existsSync(pkgPath)) {
360
858
  try {
361
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
859
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
362
860
  return pkg.type === "module";
363
861
  } catch (e) {
364
862
  return false;
@@ -369,8 +867,8 @@ var Config = class {
369
867
  };
370
868
 
371
869
  // src/cli/commands/seed.ts
372
- import * as fs5 from "fs";
373
- import * as path5 from "path";
870
+ import * as fs6 from "fs";
871
+ import * as path6 from "path";
374
872
 
375
873
  // src/cli/cli-connection.ts
376
874
  import { execSync } from "child_process";
@@ -481,38 +979,38 @@ var CLIPREparedStatement = class {
481
979
  };
482
980
 
483
981
  // src/cli/commands/seed.ts
484
- import * as p4 from "@clack/prompts";
982
+ import * as p5 from "@clack/prompts";
485
983
  async function seed(args2) {
486
- p4.intro("Seeding database...");
487
- const srcSeedersDir = path5.join(process.cwd(), "src/database/seeders");
488
- const rootSeedersDir = path5.join(process.cwd(), "database/seeders");
489
- const seedersDir = fs5.existsSync(srcSeedersDir) ? srcSeedersDir : rootSeedersDir;
490
- if (!fs5.existsSync(seedersDir)) {
491
- p4.log.warn(
984
+ p5.intro("Seeding database...");
985
+ const srcSeedersDir = path6.join(process.cwd(), "src/database/seeders");
986
+ const rootSeedersDir = path6.join(process.cwd(), "database/seeders");
987
+ const seedersDir = fs6.existsSync(srcSeedersDir) ? srcSeedersDir : rootSeedersDir;
988
+ if (!fs6.existsSync(seedersDir)) {
989
+ p5.log.warn(
492
990
  `No seeders directory found (checked ${srcSeedersDir} and ${rootSeedersDir}).`
493
991
  );
494
- p4.outro("Nothing to seed.");
992
+ p5.outro("Nothing to seed.");
495
993
  return;
496
994
  }
497
995
  const isRemote = args2.includes("--remote");
498
996
  const dbName = Config.getD1Binding();
499
- const s = p4.spinner();
997
+ const s = p5.spinner();
500
998
  s.start(`Initializing ORM (Binding: ${dbName})...`);
501
999
  try {
502
1000
  const connection = new CLIConnection(dbName, isRemote);
503
1001
  Database.setup(connection);
504
1002
  s.stop(`ORM initialized successfully with binding "${dbName}".`);
505
- const files = fs5.readdirSync(seedersDir).filter(
1003
+ const files = fs6.readdirSync(seedersDir).filter(
506
1004
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mts") || f.endsWith(".mjs")
507
1005
  ).sort();
508
1006
  if (files.length === 0) {
509
- p4.log.info("No seeder files found.");
510
- p4.outro("Seeding complete.");
1007
+ p5.log.info("No seeder files found.");
1008
+ p5.outro("Seeding complete.");
511
1009
  return;
512
1010
  }
513
1011
  for (const file of files) {
514
1012
  s.start(`Running seeder: ${file}`);
515
- const filePath = path5.join(seedersDir, file);
1013
+ const filePath = path6.join(seedersDir, file);
516
1014
  try {
517
1015
  const seeder = await import(filePath);
518
1016
  if (seeder.seed) {
@@ -523,59 +1021,62 @@ async function seed(args2) {
523
1021
  }
524
1022
  } catch (error) {
525
1023
  s.stop(`Failed: ${file}`, 1);
526
- p4.log.error(`Error running seeder ${file}: ${error}`);
1024
+ p5.log.error(`Error running seeder ${file}: ${error}`);
527
1025
  }
528
1026
  }
529
- p4.outro("Seeding completed successfully.");
1027
+ p5.outro("Seeding completed successfully.");
530
1028
  } catch (error) {
531
1029
  s.stop("Initialization failed.", 1);
532
- p4.log.error(`Seeding error: ${error}`);
1030
+ p5.log.error(`Seeding error: ${error}`);
533
1031
  }
534
1032
  }
535
1033
 
536
1034
  // src/cli/commands/migrate.ts
537
1035
  async function migrate(args2) {
538
- p5.intro("Running migrations...");
539
- const srcMigrationsDir = path6.join(process.cwd(), "src/database/migrations");
540
- const rootMigrationsDir = path6.join(process.cwd(), "database/migrations");
541
- const migrationsDir = fs6.existsSync(srcMigrationsDir) ? srcMigrationsDir : rootMigrationsDir;
542
- if (!fs6.existsSync(migrationsDir)) {
543
- p5.log.warn(
1036
+ p6.intro("Running migrations...");
1037
+ const srcMigrationsDir = path7.join(process.cwd(), "src/database/migrations");
1038
+ const rootMigrationsDir = path7.join(process.cwd(), "database/migrations");
1039
+ const migrationsDir = fs7.existsSync(srcMigrationsDir) ? srcMigrationsDir : rootMigrationsDir;
1040
+ if (!fs7.existsSync(migrationsDir)) {
1041
+ p6.log.warn(
544
1042
  `No migrations directory found (checked ${srcMigrationsDir} and ${rootMigrationsDir}).`
545
1043
  );
546
- p5.outro("Nothing to migrate.");
1044
+ p6.outro("Nothing to migrate.");
547
1045
  return;
548
1046
  }
549
- const files = fs6.readdirSync(migrationsDir).filter(
1047
+ const files = fs7.readdirSync(migrationsDir).filter(
550
1048
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mts") || f.endsWith(".mjs")
551
1049
  ).sort();
552
1050
  if (files.length === 0) {
553
- p5.log.info("No migration files found.");
554
- p5.outro("Migrations complete.");
1051
+ p6.log.info("No migration files found.");
1052
+ p6.outro("Migrations complete.");
555
1053
  return;
556
1054
  }
557
- const s = p5.spinner();
1055
+ const s = p6.spinner();
558
1056
  const dbName = Config.getD1Binding();
559
1057
  for (const file of files) {
560
1058
  s.start(`Processing migration: ${file} (Binding: ${dbName})`);
561
- const filePath = path6.join(migrationsDir, file);
1059
+ const filePath = path7.join(migrationsDir, file);
562
1060
  try {
563
1061
  const migration = await import(filePath);
564
1062
  if (migration.up) {
565
1063
  const sql = await migration.up();
566
1064
  if (sql) {
1065
+ const sqlStatements = Array.isArray(sql) ? sql : [sql];
567
1066
  const isRemote = args2.includes("--remote");
568
1067
  const command2 = isRemote ? "--remote" : "--local";
569
1068
  try {
570
- const execCmd = `npx wrangler d1 execute ${dbName} --command "${sql.replace(/"/g, '\\"')}" ${command2}`;
571
- execSync2(execCmd, {
572
- stdio: "pipe",
573
- env: Config.getCleanEnv()
574
- });
1069
+ for (const statement of sqlStatements) {
1070
+ const execCmd = `npx wrangler d1 execute ${dbName} --command "${statement.replace(/"/g, '\\"')}" ${command2}`;
1071
+ execSync2(execCmd, {
1072
+ stdio: "pipe",
1073
+ env: Config.getCleanEnv()
1074
+ });
1075
+ }
575
1076
  s.stop(`Migrated: ${file}`);
576
1077
  } catch (e) {
577
1078
  s.stop(`Failed: ${file}`, 1);
578
- p5.log.error(`Failed to execute migration: ${file}`);
1079
+ p6.log.error(`Failed to execute migration: ${file}`);
579
1080
  throw e;
580
1081
  }
581
1082
  } else {
@@ -586,10 +1087,10 @@ async function migrate(args2) {
586
1087
  }
587
1088
  } catch (error) {
588
1089
  s.stop(`Error: ${file}`, 1);
589
- p5.log.error(`Error processing migration ${file}: ${error}`);
1090
+ p6.log.error(`Error processing migration ${file}: ${error}`);
590
1091
  }
591
1092
  }
592
- p5.outro("All migrations executed.");
1093
+ p6.outro("All migrations executed.");
593
1094
  if (args2.includes("--seed")) {
594
1095
  await seed(args2);
595
1096
  }
@@ -597,13 +1098,13 @@ async function migrate(args2) {
597
1098
 
598
1099
  // src/cli/commands/migrate-fresh.ts
599
1100
  import { execSync as execSync3 } from "child_process";
600
- import * as p6 from "@clack/prompts";
1101
+ import * as p7 from "@clack/prompts";
601
1102
  async function migrateFresh(args2) {
602
- p6.intro("Resetting database...");
1103
+ p7.intro("Resetting database...");
603
1104
  const isRemote = args2.includes("--remote");
604
1105
  const dbName = Config.getD1Binding();
605
1106
  const flag = isRemote ? "--remote" : "--local";
606
- const s = p6.spinner();
1107
+ const s = p7.spinner();
607
1108
  s.start("Scanning for tables to drop...");
608
1109
  try {
609
1110
  const listTablesCmd = `npx wrangler d1 execute ${dbName} --command "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_cf_%'" ${flag} --json`;
@@ -634,7 +1135,7 @@ async function migrateFresh(args2) {
634
1135
  await migrate(args2);
635
1136
  } catch (error) {
636
1137
  s.stop("Process failed.", 1);
637
- p6.log.error(`Error during migrate:fresh: ${error}`);
1138
+ p7.log.error(`Error during migrate:fresh: ${error}`);
638
1139
  }
639
1140
  }
640
1141
 
@@ -652,6 +1153,9 @@ switch (command) {
652
1153
  case "make:migration":
653
1154
  makeMigration(param);
654
1155
  break;
1156
+ case "model:add":
1157
+ modelAdd();
1158
+ break;
655
1159
  case "migrate":
656
1160
  migrate(args);
657
1161
  break;
@@ -663,7 +1167,7 @@ switch (command) {
663
1167
  break;
664
1168
  default:
665
1169
  console.log(
666
- "Available commands: init, make:model, make:migration, migrate, migrate:fresh, db:seed"
1170
+ "Available commands: init, make:model, make:migration, model:add, migrate, migrate:fresh, db:seed"
667
1171
  );
668
1172
  break;
669
1173
  }