@3lineas/d1-orm 1.0.11 → 1.0.13

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