@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.
@@ -154,7 +154,7 @@ var p3 = __toESM(require("@clack/prompts"), 1);
154
154
  var fs2 = __toESM(require("fs"), 1);
155
155
  var path2 = __toESM(require("path"), 1);
156
156
  var p2 = __toESM(require("@clack/prompts"), 1);
157
- async function makeMigration(name) {
157
+ async function makeMigration(name, fields) {
158
158
  const isStandalone = !name;
159
159
  if (isStandalone) {
160
160
  p2.intro("Creating a new migration...");
@@ -179,17 +179,38 @@ async function makeMigration(name) {
179
179
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").split(".")[0].replace("T", "_");
180
180
  const filename = `${timestamp}_${migrationName}.mts`;
181
181
  const targetPath = path2.join(process.cwd(), migrationsDir, filename);
182
+ const tableName = migrationName.replace("create_", "").replace("_table", "").replace("add_", "").replace("_to_", "").replace("_table", "");
183
+ const isAlter = migrationName.startsWith("add_");
184
+ let fieldsSql = "";
185
+ if (fields && fields.length > 0) {
186
+ fieldsSql = fields.map((f) => {
187
+ let line = "";
188
+ if (f.type === "enum") {
189
+ line = ` table.enum('${f.name}', ${JSON.stringify(f.values)})`;
190
+ } else {
191
+ line = ` table.${f.type}('${f.name}')`;
192
+ }
193
+ if (f.unique) line += ".unique()";
194
+ if (f.nullable) line += ".nullable()";
195
+ if (f.default !== void 0 && f.default !== "") {
196
+ const defVal = typeof f.default === "string" ? `'${f.default}'` : f.default;
197
+ line += `.default(${defVal})`;
198
+ }
199
+ return line + ";";
200
+ }).join("\n");
201
+ }
202
+ const method = isAlter ? "table" : "create";
203
+ const downSql = isAlter ? `// return Schema.table('${tableName}', (table: Blueprint) => { /* drop columns not supported in simple SQLite ALTER */ });` : `return Schema.dropIfExists('${tableName}');`;
182
204
  const template = `import { Blueprint, Schema } from '@3lineas/d1-orm';
183
205
 
184
206
  export const up = async () => {
185
- return Schema.create('${migrationName.replace("create_", "").replace("_table", "")}', (table: Blueprint) => {
186
- table.id();
187
- table.timestamps();
188
- });
207
+ return Schema.${method}('${tableName}', (table: Blueprint) => {
208
+ ${!isAlter ? " table.id();\n" : ""}${fieldsSql}
209
+ ${!isAlter ? " table.timestamps();\n" : ""} });
189
210
  };
190
211
 
191
212
  export const down = async () => {
192
- return Schema.dropIfExists('${migrationName.replace("create_", "").replace("_table", "")}');
213
+ ${downSql}
193
214
  };
194
215
  `;
195
216
  if (!fs2.existsSync(path2.join(process.cwd(), migrationsDir))) {
@@ -229,46 +250,245 @@ async function makeModel(name) {
229
250
  const modelPath = await findModelsPath() || "src/database/models";
230
251
  const filename = `${modelName}.mts`;
231
252
  const targetPath = path3.join(process.cwd(), modelPath, filename);
253
+ if (fs3.existsSync(targetPath)) {
254
+ p3.log.error(`Model ${filename} already exists at ${modelPath}.`);
255
+ p3.outro("Process aborted.");
256
+ return;
257
+ }
258
+ const fields = [];
259
+ const relations = [];
260
+ let addMore = true;
261
+ p3.log.step("Let's add some attributes to your model!");
262
+ while (addMore) {
263
+ const fieldName = await p3.text({
264
+ message: "New attribute or relation name (leave empty to stop):",
265
+ placeholder: "e.g. title or posts"
266
+ });
267
+ if (p3.isCancel(fieldName)) break;
268
+ if (!fieldName) break;
269
+ const fieldType = await p3.select({
270
+ message: `Type for ${fieldName}:`,
271
+ options: [
272
+ { value: "string", label: "String" },
273
+ { value: "integer", label: "Integer" },
274
+ { value: "boolean", label: "Boolean" },
275
+ { value: "text", label: "Text" },
276
+ { value: "float", label: "Float" },
277
+ { value: "json", label: "JSON" },
278
+ { value: "enum", label: "Enum" },
279
+ { value: "date", label: "Date" },
280
+ { value: "blob", label: "Blob" },
281
+ { value: "belongsToMany", label: "ManyToMany (Pivot Table)" },
282
+ { value: "relation", label: "Relation (1:1, 1:N)" }
283
+ ]
284
+ });
285
+ if (p3.isCancel(fieldType)) break;
286
+ if (fieldType === "relation" || fieldType === "belongsToMany") {
287
+ let relType = fieldType;
288
+ if (fieldType === "relation") {
289
+ relType = await p3.select({
290
+ message: `Relation type for ${fieldName}:`,
291
+ options: [
292
+ { value: "hasOne", label: "HasOne" },
293
+ { value: "hasMany", label: "HasMany" },
294
+ { value: "belongsTo", label: "BelongsTo" }
295
+ ]
296
+ });
297
+ }
298
+ if (p3.isCancel(relType)) break;
299
+ const modelsPath = await findModelsPath() || "src/database/models";
300
+ const availableModels = fs3.existsSync(
301
+ path3.join(process.cwd(), modelsPath)
302
+ ) ? fs3.readdirSync(path3.join(process.cwd(), modelPath)).map((f) => f.replace(".mts", "").replace(".ts", "")) : [];
303
+ const targetModel = await p3.text({
304
+ message: "Target model for this relation:",
305
+ placeholder: "e.g. Post",
306
+ validate: (v) => !v ? "Target model is required" : void 0
307
+ });
308
+ if (p3.isCancel(targetModel)) break;
309
+ let foreignKey = "";
310
+ let pivotTable = "";
311
+ if (relType === "belongsToMany") {
312
+ p3.log.info("Automatically creating a pivot table migration.");
313
+ pivotTable = await p3.text({
314
+ message: "Pivot table name:",
315
+ initialValue: [modelName.toLowerCase(), targetModel.toLowerCase()].sort().join("_")
316
+ });
317
+ if (p3.isCancel(pivotTable)) break;
318
+ } else if (relType === "belongsTo") {
319
+ foreignKey = await p3.text({
320
+ message: "Foreign key column name:",
321
+ placeholder: `e.g. ${targetModel.toLowerCase()}_id`,
322
+ initialValue: `${targetModel.toLowerCase()}_id`
323
+ });
324
+ if (p3.isCancel(foreignKey)) break;
325
+ fields.push({
326
+ name: foreignKey,
327
+ type: "foreign",
328
+ nullable: false,
329
+ unique: false
330
+ });
331
+ }
332
+ relations.push({
333
+ method: fieldName,
334
+ type: relType,
335
+ model: targetModel,
336
+ foreignKey,
337
+ pivotTable
338
+ });
339
+ const addInverse = await p3.confirm({
340
+ message: `Add the inverse relation in ${targetModel}.mts?`,
341
+ initialValue: true
342
+ });
343
+ if (addInverse && !p3.isCancel(addInverse)) {
344
+ let inverseType = "belongsTo";
345
+ if (relType === "belongsTo") inverseType = "hasMany";
346
+ if (relType === "hasOne") inverseType = "belongsTo";
347
+ if (relType === "belongsToMany") inverseType = "belongsToMany";
348
+ const inverseMethod = await p3.text({
349
+ message: "Inverse method name:",
350
+ initialValue: relType === "belongsTo" ? modelName.toLowerCase() + "s" : modelName.toLowerCase()
351
+ });
352
+ if (!p3.isCancel(inverseMethod)) {
353
+ await updateTargetModel(
354
+ targetModel,
355
+ inverseMethod,
356
+ inverseType,
357
+ modelName,
358
+ pivotTable
359
+ );
360
+ }
361
+ }
362
+ } else {
363
+ let enumValues = [];
364
+ if (fieldType === "enum") {
365
+ const valuesStr = await p3.text({
366
+ message: "Allowed values (comma separated):",
367
+ placeholder: "active, inactive, pending"
368
+ });
369
+ if (p3.isCancel(valuesStr)) break;
370
+ enumValues = valuesStr.split(",").map((v) => v.trim());
371
+ }
372
+ const isNullable = await p3.confirm({
373
+ message: "Is it nullable?",
374
+ initialValue: false
375
+ });
376
+ if (p3.isCancel(isNullable)) break;
377
+ const isUnique = await p3.confirm({
378
+ message: "Is it unique?",
379
+ initialValue: false
380
+ });
381
+ if (p3.isCancel(isUnique)) break;
382
+ fields.push({
383
+ name: fieldName,
384
+ type: fieldType,
385
+ nullable: isNullable,
386
+ unique: isUnique,
387
+ values: enumValues
388
+ });
389
+ }
390
+ }
391
+ const fieldProperties = fields.filter(
392
+ (f) => f.type !== "foreign" || !relations.some((r) => r.foreignKey === f.name)
393
+ ).map((f) => {
394
+ let tsType = "string";
395
+ if (f.type === "integer" || f.type === "float") tsType = "number";
396
+ else if (f.type === "boolean") tsType = "boolean";
397
+ else if (f.type === "json") tsType = "any";
398
+ else if (f.type === "blob") tsType = "Uint8Array";
399
+ return ` declare ${f.name}${f.nullable ? "?" : ""}: ${tsType};`;
400
+ }).join("\n");
401
+ const relationMethods = relations.map((r) => {
402
+ if (r.type === "belongsToMany") {
403
+ return ` ${r.method}() {
404
+ return this.belongsToMany(${r.model}, '${r.pivotTable}');
405
+ }`;
406
+ }
407
+ return ` ${r.method}() {
408
+ return this.${r.type}(${r.model});
409
+ }`;
410
+ }).join("\n\n");
232
411
  const template = `import { Model } from '@3lineas/d1-orm';
412
+ ${relations.map((r) => `import { ${r.model} } from './${r.model}.mts';`).join("\n")}
233
413
 
234
414
  export class ${modelName} extends Model {
235
415
  // protected static table = '${modelName.toLowerCase()}s';
236
416
 
237
- // declare id: number;
238
- // declare created_at: string;
239
- // declare updated_at: string;
417
+ declare id: number;
418
+ ${fieldProperties}
419
+ declare created_at: string;
420
+ declare updated_at: string;
421
+
422
+ ${relationMethods}
240
423
  }
241
424
  `;
242
- if (fs3.existsSync(targetPath)) {
243
- p3.log.error(`Model ${filename} already exists at ${modelPath}.`);
244
- p3.outro("Process aborted.");
245
- return;
246
- }
247
425
  if (!fs3.existsSync(path3.join(process.cwd(), modelPath))) {
248
426
  fs3.mkdirSync(path3.join(process.cwd(), modelPath), { recursive: true });
249
427
  }
250
428
  fs3.writeFileSync(targetPath, template);
251
429
  p3.log.success(`Created model: ${modelPath}/${filename}`);
252
- const options = await p3.multiselect({
253
- message: "Would you like to create any of the following?",
254
- options: [
255
- { value: "migration", label: "Migration" },
256
- { value: "seeder", label: "Database Seeder" }
257
- ],
258
- required: false
430
+ const migrationName = `create_${modelName.toLowerCase()}s_table`;
431
+ await makeMigration(migrationName, fields);
432
+ for (const rel of relations) {
433
+ if (rel.type === "belongsToMany") {
434
+ const pivotMigrationName = `create_${rel.pivotTable}_table`;
435
+ const pivotFields = [
436
+ {
437
+ name: `${modelName.toLowerCase()}_id`,
438
+ type: "integer",
439
+ nullable: false
440
+ },
441
+ {
442
+ name: `${rel.model.toLowerCase()}_id`,
443
+ type: "integer",
444
+ nullable: false
445
+ }
446
+ ];
447
+ await makeMigration(pivotMigrationName, pivotFields);
448
+ }
449
+ }
450
+ const wantSeeder = await p3.confirm({
451
+ message: "Would you like to create a seeder for this model?",
452
+ initialValue: true
259
453
  });
260
- if (p3.isCancel(options)) {
261
- p3.cancel("Operation cancelled.");
454
+ if (wantSeeder) {
455
+ await makeSeeder(modelName, modelPath, fields);
456
+ }
457
+ p3.outro("Model generation complete!");
458
+ }
459
+ async function updateTargetModel(targetModel, methodName, relType, sourceModel, pivotTable = "") {
460
+ const modelPath = await findModelsPath() || "src/database/models";
461
+ const filename = `${targetModel}.mts`;
462
+ const targetPath = path3.join(process.cwd(), modelPath, filename);
463
+ if (!fs3.existsSync(targetPath)) {
464
+ p3.log.warn(
465
+ `Model ${targetModel} not found at ${modelPath}. Skipping inverse relation.`
466
+ );
262
467
  return;
263
468
  }
264
- if (options.includes("migration")) {
265
- const migrationName = `create_${modelName.toLowerCase()}s_table`;
266
- await makeMigration(migrationName);
469
+ let content = fs3.readFileSync(targetPath, "utf-8");
470
+ if (!content.includes(`import { ${sourceModel} }`)) {
471
+ content = `import { ${sourceModel} } from './${sourceModel}.mts';
472
+ ` + content;
267
473
  }
268
- if (options.includes("seeder")) {
269
- await makeSeeder(modelName, modelPath);
474
+ let methodString = "";
475
+ if (relType === "belongsToMany") {
476
+ methodString = ` ${methodName}() {
477
+ return this.belongsToMany(${sourceModel}, '${pivotTable}');
478
+ }`;
479
+ } else {
480
+ methodString = ` ${methodName}() {
481
+ return this.${relType}(${sourceModel});
482
+ }`;
483
+ }
484
+ const lastBraceIndex = content.lastIndexOf("}");
485
+ if (lastBraceIndex !== -1) {
486
+ content = content.substring(0, lastBraceIndex) + "\n" + methodString + "\n" + content.substring(lastBraceIndex);
487
+ fs3.writeFileSync(targetPath, content);
488
+ p3.log.success(
489
+ `Updated ${targetModel}.mts with inverse relation: ${methodName}()`
490
+ );
270
491
  }
271
- p3.outro("Model generation complete!");
272
492
  }
273
493
  async function findModelsPath() {
274
494
  const commonPaths = [
@@ -278,12 +498,12 @@ async function findModelsPath() {
278
498
  "models",
279
499
  "app/Models"
280
500
  ];
281
- for (const p7 of commonPaths) {
282
- if (fs3.existsSync(path3.join(process.cwd(), p7))) return p7;
501
+ for (const p8 of commonPaths) {
502
+ if (fs3.existsSync(path3.join(process.cwd(), p8))) return p8;
283
503
  }
284
504
  return null;
285
505
  }
286
- async function makeSeeder(modelName, modelPath) {
506
+ async function makeSeeder(modelName, modelPath, fields = []) {
287
507
  const srcPath = path3.join(process.cwd(), "src");
288
508
  const useSrc = fs3.existsSync(srcPath) && fs3.lstatSync(srcPath).isDirectory();
289
509
  const seederDir = useSrc ? path3.join(process.cwd(), "src/database/seeders") : path3.join(process.cwd(), "database/seeders");
@@ -293,10 +513,25 @@ async function makeSeeder(modelName, modelPath) {
293
513
  fs3.mkdirSync(seederDir, { recursive: true });
294
514
  }
295
515
  const relativeModelPath = path3.relative(seederDir, path3.join(process.cwd(), modelPath, modelName)).replace(/\\/g, "/");
516
+ const dummyData = fields.map((f) => {
517
+ let val = "''";
518
+ if (f.type === "integer" || f.type === "foreign") val = "1";
519
+ if (f.type === "float") val = "1.5";
520
+ if (f.type === "boolean") val = "true";
521
+ if (f.type === "string" || f.type === "text") val = `'Sample ${f.name}'`;
522
+ if (f.type === "json") val = "'{}'";
523
+ if (f.type === "enum")
524
+ val = f.values && f.values.length > 0 ? `'${f.values[0]}'` : "''";
525
+ if (f.type === "date" || f.type === "datetime")
526
+ val = `'${(/* @__PURE__ */ new Date()).toISOString()}'`;
527
+ return ` ${f.name}: ${val},`;
528
+ }).join("\n");
296
529
  const template = `import { ${modelName} } from '${relativeModelPath}.mts';
297
530
 
298
531
  export const seed = async () => {
299
- // await ${modelName}.create({ ... });
532
+ await ${modelName}.create({
533
+ ${dummyData}
534
+ });
300
535
  };
301
536
  `;
302
537
  if (fs3.existsSync(targetPath)) {
@@ -304,18 +539,281 @@ export const seed = async () => {
304
539
  return;
305
540
  }
306
541
  fs3.writeFileSync(targetPath, template);
307
- p3.log.success(`Created seeder: database/seeders/${seederName}`);
542
+ p3.log.success(`Created seeder: ${seederDir}/${seederName}`);
543
+ }
544
+
545
+ // src/cli/commands/model-add.ts
546
+ var fs4 = __toESM(require("fs"), 1);
547
+ var path4 = __toESM(require("path"), 1);
548
+ var p4 = __toESM(require("@clack/prompts"), 1);
549
+ async function modelAdd() {
550
+ p4.intro("Adding attributes to an existing model...");
551
+ const modelPath = await findModelsPath2() || "src/database/models";
552
+ const fullModelPath = path4.join(process.cwd(), modelPath);
553
+ if (!fs4.existsSync(fullModelPath)) {
554
+ p4.log.error(`Models directory not found at ${modelPath}.`);
555
+ return;
556
+ }
557
+ const modelFiles = fs4.readdirSync(fullModelPath).filter((f) => f.endsWith(".mts") || f.endsWith(".ts"));
558
+ const options = modelFiles.map((f) => ({
559
+ value: f,
560
+ label: f
561
+ }));
562
+ options.unshift({ value: "new", label: "[ Create new model ]" });
563
+ const selectedModel = await p4.select({
564
+ message: "Select a model to add attributes to:",
565
+ options
566
+ });
567
+ if (p4.isCancel(selectedModel)) {
568
+ p4.cancel("Operation cancelled.");
569
+ return;
570
+ }
571
+ if (selectedModel === "new") {
572
+ await makeModel();
573
+ return;
574
+ }
575
+ const modelName = selectedModel.replace(".mts", "").replace(".ts", "");
576
+ const targetPath = path4.join(fullModelPath, selectedModel);
577
+ const fields = [];
578
+ const relations = [];
579
+ let addMore = true;
580
+ p4.log.step(`Adding attributes to ${modelName}...`);
581
+ while (addMore) {
582
+ const fieldName = await p4.text({
583
+ message: "New attribute or relation name (leave empty to stop):",
584
+ placeholder: "e.g. description or comments"
585
+ });
586
+ if (p4.isCancel(fieldName)) break;
587
+ if (!fieldName) break;
588
+ const fieldType = await p4.select({
589
+ message: `Type for ${fieldName}:`,
590
+ options: [
591
+ { value: "string", label: "String" },
592
+ { value: "integer", label: "Integer" },
593
+ { value: "boolean", label: "Boolean" },
594
+ { value: "text", label: "Text" },
595
+ { value: "float", label: "Float" },
596
+ { value: "json", label: "JSON" },
597
+ { value: "enum", label: "Enum" },
598
+ { value: "date", label: "Date" },
599
+ { value: "blob", label: "Blob" },
600
+ { value: "belongsToMany", label: "ManyToMany (Pivot Table)" },
601
+ { value: "relation", label: "Relation (1:1, 1:N)" }
602
+ ]
603
+ });
604
+ if (p4.isCancel(fieldType)) break;
605
+ if (fieldType === "relation" || fieldType === "belongsToMany") {
606
+ let relType = fieldType;
607
+ if (fieldType === "relation") {
608
+ relType = await p4.select({
609
+ message: `Relation type for ${fieldName}:`,
610
+ options: [
611
+ { value: "hasOne", label: "HasOne" },
612
+ { value: "hasMany", label: "HasMany" },
613
+ { value: "belongsTo", label: "BelongsTo" }
614
+ ]
615
+ });
616
+ }
617
+ if (p4.isCancel(relType)) break;
618
+ const targetModel = await p4.text({
619
+ message: "Target model for this relation:",
620
+ placeholder: "e.g. Comment",
621
+ validate: (v) => !v ? "Target model is required" : void 0
622
+ });
623
+ if (p4.isCancel(targetModel)) break;
624
+ let foreignKey = "";
625
+ let pivotTable = "";
626
+ if (relType === "belongsToMany") {
627
+ p4.log.info("Automatically creating a pivot table migration.");
628
+ pivotTable = await p4.text({
629
+ message: "Pivot table name:",
630
+ initialValue: [modelName.toLowerCase(), targetModel.toLowerCase()].sort().join("_")
631
+ });
632
+ if (p4.isCancel(pivotTable)) break;
633
+ } else if (relType === "belongsTo") {
634
+ foreignKey = await p4.text({
635
+ message: "Foreign key column name:",
636
+ placeholder: `e.g. ${targetModel.toLowerCase()}_id`,
637
+ initialValue: `${targetModel.toLowerCase()}_id`
638
+ });
639
+ if (p4.isCancel(foreignKey)) break;
640
+ fields.push({
641
+ name: foreignKey,
642
+ type: "foreign",
643
+ nullable: false,
644
+ unique: false
645
+ });
646
+ }
647
+ relations.push({
648
+ method: fieldName,
649
+ type: relType,
650
+ model: targetModel,
651
+ foreignKey,
652
+ pivotTable
653
+ });
654
+ const addInverse = await p4.confirm({
655
+ message: `Add the inverse relation in ${targetModel}.mts?`,
656
+ initialValue: true
657
+ });
658
+ if (addInverse && !p4.isCancel(addInverse)) {
659
+ let inverseType = "belongsTo";
660
+ if (relType === "belongsTo") inverseType = "hasMany";
661
+ if (relType === "hasOne") inverseType = "belongsTo";
662
+ if (relType === "belongsToMany") inverseType = "belongsToMany";
663
+ const inverseMethod = await p4.text({
664
+ message: "Inverse method name:",
665
+ initialValue: relType === "belongsTo" ? modelName.toLowerCase() + "s" : modelName.toLowerCase()
666
+ });
667
+ if (!p4.isCancel(inverseMethod)) {
668
+ await updateTargetModel2(
669
+ targetModel,
670
+ inverseMethod,
671
+ inverseType,
672
+ modelName,
673
+ pivotTable
674
+ );
675
+ }
676
+ }
677
+ } else {
678
+ let enumValues = [];
679
+ if (fieldType === "enum") {
680
+ const valuesStr = await p4.text({
681
+ message: "Allowed values (comma separated):",
682
+ placeholder: "active, inactive, pending"
683
+ });
684
+ if (p4.isCancel(valuesStr)) break;
685
+ enumValues = valuesStr.split(",").map((v) => v.trim());
686
+ }
687
+ const isNullable = await p4.confirm({
688
+ message: "Is it nullable?",
689
+ initialValue: false
690
+ });
691
+ if (p4.isCancel(isNullable)) break;
692
+ const isUnique = await p4.confirm({
693
+ message: "Is it unique?",
694
+ initialValue: false
695
+ });
696
+ if (p4.isCancel(isUnique)) break;
697
+ fields.push({
698
+ name: fieldName,
699
+ type: fieldType,
700
+ nullable: isNullable,
701
+ unique: isUnique,
702
+ values: enumValues
703
+ });
704
+ }
705
+ }
706
+ if (fields.length === 0 && relations.length === 0) {
707
+ p4.outro("No changes made.");
708
+ return;
709
+ }
710
+ let modelContent = fs4.readFileSync(targetPath, "utf-8");
711
+ const newImports = relations.filter((r) => !modelContent.includes(`import { ${r.model} }`)).map((r) => `import { ${r.model} } from './${r.model}.mts';`).join("\n");
712
+ if (newImports) {
713
+ modelContent = newImports + "\n" + modelContent;
714
+ }
715
+ const fieldProperties = fields.filter(
716
+ (f) => f.type !== "foreign" || !relations.some((r) => r.foreignKey === f.name)
717
+ ).map((f) => {
718
+ let tsType = "string";
719
+ if (f.type === "integer" || f.type === "float") tsType = "number";
720
+ else if (f.type === "boolean") tsType = "boolean";
721
+ else if (f.type === "json") tsType = "any";
722
+ else if (f.type === "blob") tsType = "Uint8Array";
723
+ return ` declare ${f.name}${f.nullable ? "?" : ""}: ${tsType};`;
724
+ }).join("\n");
725
+ const relationMethods = relations.map((r) => {
726
+ return ` ${r.method}() {
727
+ return this.${r.type}(${r.model});
728
+ }`;
729
+ }).join("\n\n");
730
+ const lastBraceIndex = modelContent.lastIndexOf("}");
731
+ if (lastBraceIndex !== -1) {
732
+ modelContent = modelContent.substring(0, lastBraceIndex) + fieldProperties + "\n" + relationMethods + "\n" + modelContent.substring(lastBraceIndex);
733
+ fs4.writeFileSync(targetPath, modelContent);
734
+ p4.log.success(`Updated model: ${modelPath}/${selectedModel}`);
735
+ }
736
+ if (fields.length > 0) {
737
+ const migrationName = `add_fields_to_${modelName.toLowerCase()}s_table`;
738
+ await makeMigration(migrationName, fields);
739
+ }
740
+ for (const rel of relations) {
741
+ if (rel.type === "belongsToMany") {
742
+ const pivotMigrationName = `create_${rel.pivotTable}_table`;
743
+ const pivotFields = [
744
+ {
745
+ name: `${modelName.toLowerCase()}_id`,
746
+ type: "integer",
747
+ nullable: false
748
+ },
749
+ {
750
+ name: `${rel.model.toLowerCase()}_id`,
751
+ type: "integer",
752
+ nullable: false
753
+ }
754
+ ];
755
+ await makeMigration(pivotMigrationName, pivotFields);
756
+ }
757
+ }
758
+ p4.outro("Model update complete!");
759
+ }
760
+ async function updateTargetModel2(targetModel, methodName, relType, sourceModel, pivotTable = "") {
761
+ const modelPath = await findModelsPath2() || "src/database/models";
762
+ const filename = `${targetModel}.mts`;
763
+ const targetPath = path4.join(process.cwd(), modelPath, filename);
764
+ if (!fs4.existsSync(targetPath)) {
765
+ p4.log.warn(
766
+ `Model ${targetModel} not found at ${modelPath}. Skipping inverse relation.`
767
+ );
768
+ return;
769
+ }
770
+ let content = fs4.readFileSync(targetPath, "utf-8");
771
+ if (!content.includes(`import { ${sourceModel} }`)) {
772
+ content = `import { ${sourceModel} } from './${sourceModel}.mts';
773
+ ` + content;
774
+ }
775
+ let methodString = "";
776
+ if (relType === "belongsToMany") {
777
+ methodString = ` ${methodName}() {
778
+ return this.belongsToMany(${sourceModel}, '${pivotTable}');
779
+ }`;
780
+ } else {
781
+ methodString = ` ${methodName}() {
782
+ return this.${relType}(${sourceModel});
783
+ }`;
784
+ }
785
+ const lastBraceIndex = content.lastIndexOf("}");
786
+ if (lastBraceIndex !== -1) {
787
+ content = content.substring(0, lastBraceIndex) + "\n" + methodString + "\n" + content.substring(lastBraceIndex);
788
+ fs4.writeFileSync(targetPath, content);
789
+ p4.log.success(
790
+ `Updated ${targetModel}.mts with inverse relation: ${methodName}()`
791
+ );
792
+ }
793
+ }
794
+ async function findModelsPath2() {
795
+ const commonPaths = [
796
+ "src/database/models",
797
+ "database/models",
798
+ "src/models",
799
+ "models",
800
+ "app/Models"
801
+ ];
802
+ for (const p8 of commonPaths) {
803
+ if (fs4.existsSync(path4.join(process.cwd(), p8))) return p8;
804
+ }
805
+ return null;
308
806
  }
309
807
 
310
808
  // src/cli/commands/migrate.ts
311
- var fs6 = __toESM(require("fs"), 1);
312
- var path6 = __toESM(require("path"), 1);
809
+ var fs7 = __toESM(require("fs"), 1);
810
+ var path7 = __toESM(require("path"), 1);
313
811
  var import_child_process2 = require("child_process");
314
- var p5 = __toESM(require("@clack/prompts"), 1);
812
+ var p6 = __toESM(require("@clack/prompts"), 1);
315
813
 
316
814
  // src/cli/utils/config.ts
317
- var fs4 = __toESM(require("fs"), 1);
318
- var path4 = __toESM(require("path"), 1);
815
+ var fs5 = __toESM(require("fs"), 1);
816
+ var path5 = __toESM(require("path"), 1);
319
817
  var Config = class {
320
818
  /**
321
819
  * Detect the D1 database binding from Wrangler configuration or d1-orm config.
@@ -323,21 +821,21 @@ var Config = class {
323
821
  * @returns The binding name (e.g., "DB").
324
822
  */
325
823
  static getD1Binding() {
326
- const srcConfig = path4.join(process.cwd(), "src/database/config.mts");
327
- const srcConfigTs = path4.join(process.cwd(), "src/database/config.ts");
328
- const rootConfig = path4.join(process.cwd(), "database/config.mts");
329
- const rootConfigTs = path4.join(process.cwd(), "database/config.ts");
330
- const configPath = fs4.existsSync(srcConfig) ? srcConfig : fs4.existsSync(srcConfigTs) ? srcConfigTs : fs4.existsSync(rootConfig) ? rootConfig : fs4.existsSync(rootConfigTs) ? rootConfigTs : null;
824
+ const srcConfig = path5.join(process.cwd(), "src/database/config.mts");
825
+ const srcConfigTs = path5.join(process.cwd(), "src/database/config.ts");
826
+ const rootConfig = path5.join(process.cwd(), "database/config.mts");
827
+ const rootConfigTs = path5.join(process.cwd(), "database/config.ts");
828
+ const configPath = fs5.existsSync(srcConfig) ? srcConfig : fs5.existsSync(srcConfigTs) ? srcConfigTs : fs5.existsSync(rootConfig) ? rootConfig : fs5.existsSync(rootConfigTs) ? rootConfigTs : null;
331
829
  if (configPath) {
332
- const content = fs4.readFileSync(configPath, "utf-8");
830
+ const content = fs5.readFileSync(configPath, "utf-8");
333
831
  const match = content.match(/binding\s*:\s*["'](.+?)["']/);
334
832
  if (match) return match[1];
335
833
  }
336
834
  const wranglerPaths = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
337
835
  for (const configName of wranglerPaths) {
338
- const fullPath = path4.join(process.cwd(), configName);
339
- if (fs4.existsSync(fullPath)) {
340
- const content = fs4.readFileSync(fullPath, "utf-8");
836
+ const fullPath = path5.join(process.cwd(), configName);
837
+ if (fs5.existsSync(fullPath)) {
838
+ const content = fs5.readFileSync(fullPath, "utf-8");
341
839
  if (configName.endsWith(".json") || configName.endsWith(".jsonc")) {
342
840
  try {
343
841
  const jsonStr = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
@@ -375,10 +873,10 @@ var Config = class {
375
873
  * Detect if the project is ESM.
376
874
  */
377
875
  static isESM() {
378
- const pkgPath = path4.join(process.cwd(), "package.json");
379
- if (fs4.existsSync(pkgPath)) {
876
+ const pkgPath = path5.join(process.cwd(), "package.json");
877
+ if (fs5.existsSync(pkgPath)) {
380
878
  try {
381
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
879
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
382
880
  return pkg.type === "module";
383
881
  } catch (e) {
384
882
  return false;
@@ -389,8 +887,8 @@ var Config = class {
389
887
  };
390
888
 
391
889
  // src/cli/commands/seed.ts
392
- var fs5 = __toESM(require("fs"), 1);
393
- var path5 = __toESM(require("path"), 1);
890
+ var fs6 = __toESM(require("fs"), 1);
891
+ var path6 = __toESM(require("path"), 1);
394
892
 
395
893
  // src/core/connection.ts
396
894
  var Connection = class {
@@ -624,38 +1122,38 @@ var CLIPREparedStatement = class {
624
1122
  };
625
1123
 
626
1124
  // src/cli/commands/seed.ts
627
- var p4 = __toESM(require("@clack/prompts"), 1);
1125
+ var p5 = __toESM(require("@clack/prompts"), 1);
628
1126
  async function seed(args2) {
629
- p4.intro("Seeding database...");
630
- const srcSeedersDir = path5.join(process.cwd(), "src/database/seeders");
631
- const rootSeedersDir = path5.join(process.cwd(), "database/seeders");
632
- const seedersDir = fs5.existsSync(srcSeedersDir) ? srcSeedersDir : rootSeedersDir;
633
- if (!fs5.existsSync(seedersDir)) {
634
- p4.log.warn(
1127
+ p5.intro("Seeding database...");
1128
+ const srcSeedersDir = path6.join(process.cwd(), "src/database/seeders");
1129
+ const rootSeedersDir = path6.join(process.cwd(), "database/seeders");
1130
+ const seedersDir = fs6.existsSync(srcSeedersDir) ? srcSeedersDir : rootSeedersDir;
1131
+ if (!fs6.existsSync(seedersDir)) {
1132
+ p5.log.warn(
635
1133
  `No seeders directory found (checked ${srcSeedersDir} and ${rootSeedersDir}).`
636
1134
  );
637
- p4.outro("Nothing to seed.");
1135
+ p5.outro("Nothing to seed.");
638
1136
  return;
639
1137
  }
640
1138
  const isRemote = args2.includes("--remote");
641
1139
  const dbName = Config.getD1Binding();
642
- const s = p4.spinner();
1140
+ const s = p5.spinner();
643
1141
  s.start(`Initializing ORM (Binding: ${dbName})...`);
644
1142
  try {
645
1143
  const connection = new CLIConnection(dbName, isRemote);
646
1144
  Database.setup(connection);
647
1145
  s.stop(`ORM initialized successfully with binding "${dbName}".`);
648
- const files = fs5.readdirSync(seedersDir).filter(
1146
+ const files = fs6.readdirSync(seedersDir).filter(
649
1147
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mts") || f.endsWith(".mjs")
650
1148
  ).sort();
651
1149
  if (files.length === 0) {
652
- p4.log.info("No seeder files found.");
653
- p4.outro("Seeding complete.");
1150
+ p5.log.info("No seeder files found.");
1151
+ p5.outro("Seeding complete.");
654
1152
  return;
655
1153
  }
656
1154
  for (const file of files) {
657
1155
  s.start(`Running seeder: ${file}`);
658
- const filePath = path5.join(seedersDir, file);
1156
+ const filePath = path6.join(seedersDir, file);
659
1157
  try {
660
1158
  const seeder = await import(filePath);
661
1159
  if (seeder.seed) {
@@ -666,59 +1164,62 @@ async function seed(args2) {
666
1164
  }
667
1165
  } catch (error) {
668
1166
  s.stop(`Failed: ${file}`, 1);
669
- p4.log.error(`Error running seeder ${file}: ${error}`);
1167
+ p5.log.error(`Error running seeder ${file}: ${error}`);
670
1168
  }
671
1169
  }
672
- p4.outro("Seeding completed successfully.");
1170
+ p5.outro("Seeding completed successfully.");
673
1171
  } catch (error) {
674
1172
  s.stop("Initialization failed.", 1);
675
- p4.log.error(`Seeding error: ${error}`);
1173
+ p5.log.error(`Seeding error: ${error}`);
676
1174
  }
677
1175
  }
678
1176
 
679
1177
  // src/cli/commands/migrate.ts
680
1178
  async function migrate(args2) {
681
- p5.intro("Running migrations...");
682
- const srcMigrationsDir = path6.join(process.cwd(), "src/database/migrations");
683
- const rootMigrationsDir = path6.join(process.cwd(), "database/migrations");
684
- const migrationsDir = fs6.existsSync(srcMigrationsDir) ? srcMigrationsDir : rootMigrationsDir;
685
- if (!fs6.existsSync(migrationsDir)) {
686
- p5.log.warn(
1179
+ p6.intro("Running migrations...");
1180
+ const srcMigrationsDir = path7.join(process.cwd(), "src/database/migrations");
1181
+ const rootMigrationsDir = path7.join(process.cwd(), "database/migrations");
1182
+ const migrationsDir = fs7.existsSync(srcMigrationsDir) ? srcMigrationsDir : rootMigrationsDir;
1183
+ if (!fs7.existsSync(migrationsDir)) {
1184
+ p6.log.warn(
687
1185
  `No migrations directory found (checked ${srcMigrationsDir} and ${rootMigrationsDir}).`
688
1186
  );
689
- p5.outro("Nothing to migrate.");
1187
+ p6.outro("Nothing to migrate.");
690
1188
  return;
691
1189
  }
692
- const files = fs6.readdirSync(migrationsDir).filter(
1190
+ const files = fs7.readdirSync(migrationsDir).filter(
693
1191
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mts") || f.endsWith(".mjs")
694
1192
  ).sort();
695
1193
  if (files.length === 0) {
696
- p5.log.info("No migration files found.");
697
- p5.outro("Migrations complete.");
1194
+ p6.log.info("No migration files found.");
1195
+ p6.outro("Migrations complete.");
698
1196
  return;
699
1197
  }
700
- const s = p5.spinner();
1198
+ const s = p6.spinner();
701
1199
  const dbName = Config.getD1Binding();
702
1200
  for (const file of files) {
703
1201
  s.start(`Processing migration: ${file} (Binding: ${dbName})`);
704
- const filePath = path6.join(migrationsDir, file);
1202
+ const filePath = path7.join(migrationsDir, file);
705
1203
  try {
706
1204
  const migration = await import(filePath);
707
1205
  if (migration.up) {
708
1206
  const sql = await migration.up();
709
1207
  if (sql) {
1208
+ const sqlStatements = Array.isArray(sql) ? sql : [sql];
710
1209
  const isRemote = args2.includes("--remote");
711
1210
  const command2 = isRemote ? "--remote" : "--local";
712
1211
  try {
713
- const execCmd = `npx wrangler d1 execute ${dbName} --command "${sql.replace(/"/g, '\\"')}" ${command2}`;
714
- (0, import_child_process2.execSync)(execCmd, {
715
- stdio: "pipe",
716
- env: Config.getCleanEnv()
717
- });
1212
+ for (const statement of sqlStatements) {
1213
+ const execCmd = `npx wrangler d1 execute ${dbName} --command "${statement.replace(/"/g, '\\"')}" ${command2}`;
1214
+ (0, import_child_process2.execSync)(execCmd, {
1215
+ stdio: "pipe",
1216
+ env: Config.getCleanEnv()
1217
+ });
1218
+ }
718
1219
  s.stop(`Migrated: ${file}`);
719
1220
  } catch (e) {
720
1221
  s.stop(`Failed: ${file}`, 1);
721
- p5.log.error(`Failed to execute migration: ${file}`);
1222
+ p6.log.error(`Failed to execute migration: ${file}`);
722
1223
  throw e;
723
1224
  }
724
1225
  } else {
@@ -729,10 +1230,10 @@ async function migrate(args2) {
729
1230
  }
730
1231
  } catch (error) {
731
1232
  s.stop(`Error: ${file}`, 1);
732
- p5.log.error(`Error processing migration ${file}: ${error}`);
1233
+ p6.log.error(`Error processing migration ${file}: ${error}`);
733
1234
  }
734
1235
  }
735
- p5.outro("All migrations executed.");
1236
+ p6.outro("All migrations executed.");
736
1237
  if (args2.includes("--seed")) {
737
1238
  await seed(args2);
738
1239
  }
@@ -740,13 +1241,13 @@ async function migrate(args2) {
740
1241
 
741
1242
  // src/cli/commands/migrate-fresh.ts
742
1243
  var import_child_process3 = require("child_process");
743
- var p6 = __toESM(require("@clack/prompts"), 1);
1244
+ var p7 = __toESM(require("@clack/prompts"), 1);
744
1245
  async function migrateFresh(args2) {
745
- p6.intro("Resetting database...");
1246
+ p7.intro("Resetting database...");
746
1247
  const isRemote = args2.includes("--remote");
747
1248
  const dbName = Config.getD1Binding();
748
1249
  const flag = isRemote ? "--remote" : "--local";
749
- const s = p6.spinner();
1250
+ const s = p7.spinner();
750
1251
  s.start("Scanning for tables to drop...");
751
1252
  try {
752
1253
  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`;
@@ -777,7 +1278,7 @@ async function migrateFresh(args2) {
777
1278
  await migrate(args2);
778
1279
  } catch (error) {
779
1280
  s.stop("Process failed.", 1);
780
- p6.log.error(`Error during migrate:fresh: ${error}`);
1281
+ p7.log.error(`Error during migrate:fresh: ${error}`);
781
1282
  }
782
1283
  }
783
1284
 
@@ -795,6 +1296,9 @@ switch (command) {
795
1296
  case "make:migration":
796
1297
  makeMigration(param);
797
1298
  break;
1299
+ case "model:add":
1300
+ modelAdd();
1301
+ break;
798
1302
  case "migrate":
799
1303
  migrate(args);
800
1304
  break;
@@ -806,7 +1310,7 @@ switch (command) {
806
1310
  break;
807
1311
  default:
808
1312
  console.log(
809
- "Available commands: init, make:model, make:migration, migrate, migrate:fresh, db:seed"
1313
+ "Available commands: init, make:model, make:migration, model:add, migrate, migrate:fresh, db:seed"
810
1314
  );
811
1315
  break;
812
1316
  }