@3lineas/d1-orm 1.0.10 → 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,24 +801,24 @@ 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
- const jsonStr = content.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, "");
821
+ const jsonStr = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
324
822
  const config = JSON.parse(jsonStr);
325
823
  const d1Databases = config.d1_databases;
326
824
  if (Array.isArray(d1Databases) && d1Databases.length > 0) {
@@ -337,14 +835,28 @@ var Config = class {
337
835
  }
338
836
  return "DB";
339
837
  }
838
+ /**
839
+ * Get a filtered environment object to avoid system warnings.
840
+ * Filters out npm_config_* variables and sets NODE_NO_WARNINGS.
841
+ */
842
+ static getCleanEnv() {
843
+ const cleanEnv = { ...process.env };
844
+ Object.keys(cleanEnv).forEach((key) => {
845
+ if (key.startsWith("npm_config_")) {
846
+ delete cleanEnv[key];
847
+ }
848
+ });
849
+ cleanEnv.NODE_NO_WARNINGS = "1";
850
+ return cleanEnv;
851
+ }
340
852
  /**
341
853
  * Detect if the project is ESM.
342
854
  */
343
855
  static isESM() {
344
- const pkgPath = path4.join(process.cwd(), "package.json");
345
- if (fs4.existsSync(pkgPath)) {
856
+ const pkgPath = path5.join(process.cwd(), "package.json");
857
+ if (fs5.existsSync(pkgPath)) {
346
858
  try {
347
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
859
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
348
860
  return pkg.type === "module";
349
861
  } catch (e) {
350
862
  return false;
@@ -355,8 +867,8 @@ var Config = class {
355
867
  };
356
868
 
357
869
  // src/cli/commands/seed.ts
358
- import * as fs5 from "fs";
359
- import * as path5 from "path";
870
+ import * as fs6 from "fs";
871
+ import * as path6 from "path";
360
872
 
361
873
  // src/cli/cli-connection.ts
362
874
  import { execSync } from "child_process";
@@ -467,38 +979,38 @@ var CLIPREparedStatement = class {
467
979
  };
468
980
 
469
981
  // src/cli/commands/seed.ts
470
- import * as p4 from "@clack/prompts";
982
+ import * as p5 from "@clack/prompts";
471
983
  async function seed(args2) {
472
- p4.intro("Seeding database...");
473
- const srcSeedersDir = path5.join(process.cwd(), "src/database/seeders");
474
- const rootSeedersDir = path5.join(process.cwd(), "database/seeders");
475
- const seedersDir = fs5.existsSync(srcSeedersDir) ? srcSeedersDir : rootSeedersDir;
476
- if (!fs5.existsSync(seedersDir)) {
477
- 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(
478
990
  `No seeders directory found (checked ${srcSeedersDir} and ${rootSeedersDir}).`
479
991
  );
480
- p4.outro("Nothing to seed.");
992
+ p5.outro("Nothing to seed.");
481
993
  return;
482
994
  }
483
995
  const isRemote = args2.includes("--remote");
484
996
  const dbName = Config.getD1Binding();
485
- const s = p4.spinner();
997
+ const s = p5.spinner();
486
998
  s.start(`Initializing ORM (Binding: ${dbName})...`);
487
999
  try {
488
1000
  const connection = new CLIConnection(dbName, isRemote);
489
1001
  Database.setup(connection);
490
1002
  s.stop(`ORM initialized successfully with binding "${dbName}".`);
491
- const files = fs5.readdirSync(seedersDir).filter(
1003
+ const files = fs6.readdirSync(seedersDir).filter(
492
1004
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mts") || f.endsWith(".mjs")
493
1005
  ).sort();
494
1006
  if (files.length === 0) {
495
- p4.log.info("No seeder files found.");
496
- p4.outro("Seeding complete.");
1007
+ p5.log.info("No seeder files found.");
1008
+ p5.outro("Seeding complete.");
497
1009
  return;
498
1010
  }
499
1011
  for (const file of files) {
500
1012
  s.start(`Running seeder: ${file}`);
501
- const filePath = path5.join(seedersDir, file);
1013
+ const filePath = path6.join(seedersDir, file);
502
1014
  try {
503
1015
  const seeder = await import(filePath);
504
1016
  if (seeder.seed) {
@@ -509,59 +1021,62 @@ async function seed(args2) {
509
1021
  }
510
1022
  } catch (error) {
511
1023
  s.stop(`Failed: ${file}`, 1);
512
- p4.log.error(`Error running seeder ${file}: ${error}`);
1024
+ p5.log.error(`Error running seeder ${file}: ${error}`);
513
1025
  }
514
1026
  }
515
- p4.outro("Seeding completed successfully.");
1027
+ p5.outro("Seeding completed successfully.");
516
1028
  } catch (error) {
517
1029
  s.stop("Initialization failed.", 1);
518
- p4.log.error(`Seeding error: ${error}`);
1030
+ p5.log.error(`Seeding error: ${error}`);
519
1031
  }
520
1032
  }
521
1033
 
522
1034
  // src/cli/commands/migrate.ts
523
1035
  async function migrate(args2) {
524
- p5.intro("Running migrations...");
525
- const srcMigrationsDir = path6.join(process.cwd(), "src/database/migrations");
526
- const rootMigrationsDir = path6.join(process.cwd(), "database/migrations");
527
- const migrationsDir = fs6.existsSync(srcMigrationsDir) ? srcMigrationsDir : rootMigrationsDir;
528
- if (!fs6.existsSync(migrationsDir)) {
529
- 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(
530
1042
  `No migrations directory found (checked ${srcMigrationsDir} and ${rootMigrationsDir}).`
531
1043
  );
532
- p5.outro("Nothing to migrate.");
1044
+ p6.outro("Nothing to migrate.");
533
1045
  return;
534
1046
  }
535
- const files = fs6.readdirSync(migrationsDir).filter(
1047
+ const files = fs7.readdirSync(migrationsDir).filter(
536
1048
  (f) => f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".mts") || f.endsWith(".mjs")
537
1049
  ).sort();
538
1050
  if (files.length === 0) {
539
- p5.log.info("No migration files found.");
540
- p5.outro("Migrations complete.");
1051
+ p6.log.info("No migration files found.");
1052
+ p6.outro("Migrations complete.");
541
1053
  return;
542
1054
  }
543
- const s = p5.spinner();
1055
+ const s = p6.spinner();
544
1056
  const dbName = Config.getD1Binding();
545
1057
  for (const file of files) {
546
1058
  s.start(`Processing migration: ${file} (Binding: ${dbName})`);
547
- const filePath = path6.join(migrationsDir, file);
1059
+ const filePath = path7.join(migrationsDir, file);
548
1060
  try {
549
1061
  const migration = await import(filePath);
550
1062
  if (migration.up) {
551
1063
  const sql = await migration.up();
552
1064
  if (sql) {
1065
+ const sqlStatements = Array.isArray(sql) ? sql : [sql];
553
1066
  const isRemote = args2.includes("--remote");
554
1067
  const command2 = isRemote ? "--remote" : "--local";
555
1068
  try {
556
- const execCmd = `npx wrangler d1 execute ${dbName} --command "${sql.replace(/"/g, '\\"')}" ${command2}`;
557
- execSync2(execCmd, {
558
- stdio: "pipe",
559
- env: { ...process.env, NODE_NO_WARNINGS: "1" }
560
- });
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
+ }
561
1076
  s.stop(`Migrated: ${file}`);
562
1077
  } catch (e) {
563
1078
  s.stop(`Failed: ${file}`, 1);
564
- p5.log.error(`Failed to execute migration: ${file}`);
1079
+ p6.log.error(`Failed to execute migration: ${file}`);
565
1080
  throw e;
566
1081
  }
567
1082
  } else {
@@ -572,10 +1087,10 @@ async function migrate(args2) {
572
1087
  }
573
1088
  } catch (error) {
574
1089
  s.stop(`Error: ${file}`, 1);
575
- p5.log.error(`Error processing migration ${file}: ${error}`);
1090
+ p6.log.error(`Error processing migration ${file}: ${error}`);
576
1091
  }
577
1092
  }
578
- p5.outro("All migrations executed.");
1093
+ p6.outro("All migrations executed.");
579
1094
  if (args2.includes("--seed")) {
580
1095
  await seed(args2);
581
1096
  }
@@ -583,19 +1098,20 @@ async function migrate(args2) {
583
1098
 
584
1099
  // src/cli/commands/migrate-fresh.ts
585
1100
  import { execSync as execSync3 } from "child_process";
586
- import * as p6 from "@clack/prompts";
1101
+ import * as p7 from "@clack/prompts";
587
1102
  async function migrateFresh(args2) {
588
- p6.intro("Resetting database...");
1103
+ p7.intro("Resetting database...");
589
1104
  const isRemote = args2.includes("--remote");
590
1105
  const dbName = Config.getD1Binding();
591
1106
  const flag = isRemote ? "--remote" : "--local";
592
- const s = p6.spinner();
1107
+ const s = p7.spinner();
593
1108
  s.start("Scanning for tables to drop...");
594
1109
  try {
595
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`;
596
1111
  const output = execSync3(listTablesCmd, {
597
1112
  encoding: "utf-8",
598
- stdio: ["ignore", "pipe", "inherit"]
1113
+ stdio: ["ignore", "pipe", "pipe"],
1114
+ env: Config.getCleanEnv()
599
1115
  });
600
1116
  const jsonStart = output.indexOf("[");
601
1117
  if (jsonStart !== -1) {
@@ -605,7 +1121,10 @@ async function migrateFresh(args2) {
605
1121
  s.message(`Dropping ${tables.length} tables...`);
606
1122
  const dropCommands = tables.map((t) => `DROP TABLE IF EXISTS ${t.name};`).join(" ");
607
1123
  const dropCmd = `npx wrangler d1 execute ${dbName} --command "${dropCommands}" ${flag}`;
608
- execSync3(dropCmd, { stdio: "pipe" });
1124
+ execSync3(dropCmd, {
1125
+ stdio: "pipe",
1126
+ env: Config.getCleanEnv()
1127
+ });
609
1128
  s.stop("All tables dropped successfully.");
610
1129
  } else {
611
1130
  s.stop("No tables found to drop.");
@@ -616,7 +1135,7 @@ async function migrateFresh(args2) {
616
1135
  await migrate(args2);
617
1136
  } catch (error) {
618
1137
  s.stop("Process failed.", 1);
619
- p6.log.error(`Error during migrate:fresh: ${error}`);
1138
+ p7.log.error(`Error during migrate:fresh: ${error}`);
620
1139
  }
621
1140
  }
622
1141
 
@@ -634,6 +1153,9 @@ switch (command) {
634
1153
  case "make:migration":
635
1154
  makeMigration(param);
636
1155
  break;
1156
+ case "model:add":
1157
+ modelAdd();
1158
+ break;
637
1159
  case "migrate":
638
1160
  migrate(args);
639
1161
  break;
@@ -645,7 +1167,7 @@ switch (command) {
645
1167
  break;
646
1168
  default:
647
1169
  console.log(
648
- "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"
649
1171
  );
650
1172
  break;
651
1173
  }