@famgia/omnify-laravel 0.0.13 → 0.0.15

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.
@@ -0,0 +1,2239 @@
1
+ // src/plugin.ts
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ // src/migration/schema-builder.ts
6
+ var TYPE_METHOD_MAP = {
7
+ String: "string",
8
+ Int: "integer",
9
+ BigInt: "bigInteger",
10
+ Float: "double",
11
+ Decimal: "decimal",
12
+ Boolean: "boolean",
13
+ Text: "text",
14
+ LongText: "longText",
15
+ Date: "date",
16
+ Time: "time",
17
+ Timestamp: "timestamp",
18
+ Json: "json",
19
+ Email: "string",
20
+ Password: "string",
21
+ Enum: "enum"
22
+ // Note: File type is now polymorphic - no column generated, uses files table
23
+ };
24
+ var PK_METHOD_MAP = {
25
+ Int: "increments",
26
+ BigInt: "bigIncrements",
27
+ Uuid: "uuid",
28
+ String: "string"
29
+ };
30
+ function getIdType(schema) {
31
+ return schema.options?.idType ?? "BigInt";
32
+ }
33
+ function hasAutoId(schema) {
34
+ return schema.options?.id !== false;
35
+ }
36
+ function toColumnName(propertyName) {
37
+ return propertyName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
38
+ }
39
+ function toTableName(schemaName) {
40
+ const snakeCase = schemaName.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
41
+ if (snakeCase.endsWith("y")) {
42
+ return snakeCase.slice(0, -1) + "ies";
43
+ } else if (snakeCase.endsWith("s") || snakeCase.endsWith("x") || snakeCase.endsWith("ch") || snakeCase.endsWith("sh")) {
44
+ return snakeCase + "es";
45
+ } else {
46
+ return snakeCase + "s";
47
+ }
48
+ }
49
+ function propertyToColumnMethod(propertyName, property) {
50
+ if (property.type === "Association") {
51
+ return null;
52
+ }
53
+ if (property.type === "File") {
54
+ return null;
55
+ }
56
+ const columnName = toColumnName(propertyName);
57
+ const method = TYPE_METHOD_MAP[property.type] ?? "string";
58
+ const args = [columnName];
59
+ const modifiers = [];
60
+ const propWithLength = property;
61
+ if (method === "string" && propWithLength.length) {
62
+ args.push(propWithLength.length);
63
+ }
64
+ if (property.type === "Decimal") {
65
+ const decimalProp = property;
66
+ const precision = decimalProp.precision ?? 8;
67
+ const scale = decimalProp.scale ?? 2;
68
+ args.push(precision, scale);
69
+ }
70
+ if (property.type === "Enum") {
71
+ const enumProp = property;
72
+ if (enumProp.enum && enumProp.enum.length > 0) {
73
+ args.push(enumProp.enum);
74
+ }
75
+ }
76
+ const baseProp = property;
77
+ if (baseProp.nullable) {
78
+ modifiers.push({ method: "nullable" });
79
+ }
80
+ if (baseProp.unique) {
81
+ modifiers.push({ method: "unique" });
82
+ }
83
+ if (baseProp.default !== void 0 && baseProp.default !== null) {
84
+ modifiers.push({ method: "default", args: [baseProp.default] });
85
+ }
86
+ if (baseProp.unsigned && (method === "integer" || method === "bigInteger")) {
87
+ modifiers.push({ method: "unsigned" });
88
+ }
89
+ return {
90
+ name: columnName,
91
+ method,
92
+ args,
93
+ modifiers
94
+ };
95
+ }
96
+ function generatePrimaryKeyColumn(pkType = "BigInt") {
97
+ const method = PK_METHOD_MAP[pkType] ?? "bigIncrements";
98
+ if (pkType === "Uuid") {
99
+ return {
100
+ name: "id",
101
+ method: "uuid",
102
+ args: ["id"],
103
+ modifiers: [{ method: "primary" }]
104
+ };
105
+ }
106
+ if (pkType === "String") {
107
+ return {
108
+ name: "id",
109
+ method: "string",
110
+ args: ["id", 255],
111
+ modifiers: [{ method: "primary" }]
112
+ };
113
+ }
114
+ return {
115
+ name: "id",
116
+ method,
117
+ args: ["id"],
118
+ modifiers: []
119
+ };
120
+ }
121
+ function generateTimestampColumns() {
122
+ return [
123
+ {
124
+ name: "created_at",
125
+ method: "timestamp",
126
+ args: ["created_at"],
127
+ modifiers: [{ method: "nullable" }]
128
+ },
129
+ {
130
+ name: "updated_at",
131
+ method: "timestamp",
132
+ args: ["updated_at"],
133
+ modifiers: [{ method: "nullable" }]
134
+ }
135
+ ];
136
+ }
137
+ function generateSoftDeleteColumn() {
138
+ return {
139
+ name: "deleted_at",
140
+ method: "timestamp",
141
+ args: ["deleted_at"],
142
+ modifiers: [{ method: "nullable" }]
143
+ };
144
+ }
145
+ function generatePolymorphicColumns(propertyName, property, allSchemas) {
146
+ if (property.type !== "Association") {
147
+ return null;
148
+ }
149
+ const assocProp = property;
150
+ if (assocProp.relation !== "MorphTo") {
151
+ return null;
152
+ }
153
+ const targets = assocProp.targets;
154
+ if (!targets || targets.length === 0) {
155
+ return null;
156
+ }
157
+ const columnBaseName = toColumnName(propertyName);
158
+ const typeColumnName = `${columnBaseName}_type`;
159
+ const idColumnName = `${columnBaseName}_id`;
160
+ const typeColumn = {
161
+ name: typeColumnName,
162
+ method: "enum",
163
+ args: [typeColumnName, targets],
164
+ modifiers: [{ method: "nullable" }]
165
+ };
166
+ let idMethod = "unsignedBigInteger";
167
+ for (const targetName of targets) {
168
+ const targetSchema = allSchemas[targetName];
169
+ if (targetSchema) {
170
+ const targetIdType = targetSchema.options?.idType ?? "BigInt";
171
+ if (targetIdType === "Uuid") {
172
+ idMethod = "uuid";
173
+ break;
174
+ } else if (targetIdType === "String") {
175
+ idMethod = "string";
176
+ }
177
+ }
178
+ }
179
+ const idColumn = {
180
+ name: idColumnName,
181
+ method: idMethod,
182
+ args: idMethod === "string" ? [idColumnName, 255] : [idColumnName],
183
+ modifiers: [{ method: "nullable" }]
184
+ };
185
+ const indexes = [
186
+ {
187
+ columns: [typeColumnName, idColumnName],
188
+ unique: false
189
+ }
190
+ ];
191
+ return { typeColumn, idColumn, indexes };
192
+ }
193
+ function generateForeignKey(propertyName, property, allSchemas) {
194
+ if (property.type !== "Association") {
195
+ return null;
196
+ }
197
+ const assocProp = property;
198
+ if (assocProp.relation !== "ManyToOne" && assocProp.relation !== "OneToOne") {
199
+ return null;
200
+ }
201
+ if (assocProp.mappedBy) {
202
+ return null;
203
+ }
204
+ const columnName = toColumnName(propertyName) + "_id";
205
+ const targetSchema = assocProp.target ? allSchemas[assocProp.target] : void 0;
206
+ const targetTable = assocProp.target ? toTableName(assocProp.target) : "unknown";
207
+ const targetPkType = targetSchema ? getIdType(targetSchema) : "BigInt";
208
+ let method = "unsignedBigInteger";
209
+ if (targetPkType === "Int") {
210
+ method = "unsignedInteger";
211
+ } else if (targetPkType === "Uuid") {
212
+ method = "uuid";
213
+ } else if (targetPkType === "String") {
214
+ method = "string";
215
+ }
216
+ const column = {
217
+ name: columnName,
218
+ method,
219
+ args: [columnName],
220
+ modifiers: assocProp.relation === "ManyToOne" ? [{ method: "nullable" }] : []
221
+ };
222
+ const foreignKey = {
223
+ columns: [columnName],
224
+ references: "id",
225
+ on: [targetTable],
226
+ onDelete: assocProp.onDelete ?? "restrict",
227
+ onUpdate: assocProp.onUpdate ?? "cascade"
228
+ };
229
+ const index = {
230
+ columns: [columnName],
231
+ unique: false
232
+ };
233
+ return { column, foreignKey, index };
234
+ }
235
+ function schemaToBlueprint(schema, allSchemas) {
236
+ const tableName = toTableName(schema.name);
237
+ const columns = [];
238
+ const foreignKeys = [];
239
+ const indexes = [];
240
+ if (hasAutoId(schema)) {
241
+ const pkType = getIdType(schema);
242
+ columns.push(generatePrimaryKeyColumn(pkType));
243
+ }
244
+ if (schema.properties) {
245
+ for (const [propName, property] of Object.entries(schema.properties)) {
246
+ const columnMethod = propertyToColumnMethod(propName, property);
247
+ if (columnMethod) {
248
+ columns.push(columnMethod);
249
+ }
250
+ const fkResult = generateForeignKey(propName, property, allSchemas);
251
+ if (fkResult) {
252
+ columns.push(fkResult.column);
253
+ foreignKeys.push(fkResult.foreignKey);
254
+ indexes.push(fkResult.index);
255
+ }
256
+ const polyResult = generatePolymorphicColumns(propName, property, allSchemas);
257
+ if (polyResult) {
258
+ columns.push(polyResult.typeColumn);
259
+ columns.push(polyResult.idColumn);
260
+ indexes.push(...polyResult.indexes);
261
+ }
262
+ }
263
+ }
264
+ if (schema.options?.timestamps !== false) {
265
+ columns.push(...generateTimestampColumns());
266
+ }
267
+ if (schema.options?.softDelete) {
268
+ columns.push(generateSoftDeleteColumn());
269
+ }
270
+ if (schema.options?.indexes) {
271
+ for (const index of schema.options.indexes) {
272
+ if (typeof index === "string") {
273
+ indexes.push({
274
+ columns: [toColumnName(index)],
275
+ unique: false
276
+ });
277
+ } else {
278
+ indexes.push({
279
+ name: index.name,
280
+ columns: index.columns.map(toColumnName),
281
+ unique: index.unique ?? false
282
+ });
283
+ }
284
+ }
285
+ }
286
+ if (schema.options?.unique) {
287
+ const uniqueConstraints = Array.isArray(schema.options.unique[0]) ? schema.options.unique : [schema.options.unique];
288
+ for (const constraint of uniqueConstraints) {
289
+ indexes.push({
290
+ columns: constraint.map(toColumnName),
291
+ unique: true
292
+ });
293
+ }
294
+ }
295
+ return {
296
+ tableName,
297
+ columns,
298
+ primaryKey: ["id"],
299
+ foreignKeys,
300
+ indexes
301
+ };
302
+ }
303
+ function formatColumnMethod(column) {
304
+ const args = column.args.map((arg) => {
305
+ if (typeof arg === "string") {
306
+ return `'${arg}'`;
307
+ }
308
+ if (Array.isArray(arg)) {
309
+ return `[${arg.map((v) => `'${v}'`).join(", ")}]`;
310
+ }
311
+ return String(arg);
312
+ }).join(", ");
313
+ let code = `$table->${column.method}(${args})`;
314
+ for (const modifier of column.modifiers) {
315
+ if (modifier.args && modifier.args.length > 0) {
316
+ const modArgs = modifier.args.map((arg) => {
317
+ if (typeof arg === "string") {
318
+ return `'${arg}'`;
319
+ }
320
+ if (typeof arg === "boolean") {
321
+ return arg ? "true" : "false";
322
+ }
323
+ if (typeof arg === "number") {
324
+ return String(arg);
325
+ }
326
+ return String(arg);
327
+ }).join(", ");
328
+ code += `->${modifier.method}(${modArgs})`;
329
+ } else {
330
+ code += `->${modifier.method}()`;
331
+ }
332
+ }
333
+ return code + ";";
334
+ }
335
+ function formatForeignKey(fk) {
336
+ const column = fk.columns[0];
337
+ const table = fk.on[0];
338
+ let code = `$table->foreign('${column}')->references('${fk.references}')->on('${table}')`;
339
+ if (fk.onDelete) {
340
+ code += `->onDelete('${fk.onDelete}')`;
341
+ }
342
+ if (fk.onUpdate) {
343
+ code += `->onUpdate('${fk.onUpdate}')`;
344
+ }
345
+ return code + ";";
346
+ }
347
+ function formatIndex(index) {
348
+ const columns = index.columns.length === 1 ? `'${index.columns[0]}'` : `[${index.columns.map((c) => `'${c}'`).join(", ")}]`;
349
+ const method = index.unique ? "unique" : "index";
350
+ const name = index.name ? `, '${index.name}'` : "";
351
+ return `$table->${method}(${columns}${name});`;
352
+ }
353
+ function generatePivotTableName(sourceTable, targetTable, customName) {
354
+ if (customName) {
355
+ return customName;
356
+ }
357
+ const tables = [sourceTable, targetTable].sort();
358
+ const singular1 = tables[0].replace(/ies$/, "y").replace(/s$/, "");
359
+ const singular2 = tables[1].replace(/ies$/, "y").replace(/s$/, "");
360
+ return `${singular1}_${singular2}`;
361
+ }
362
+ function extractManyToManyRelations(schema, allSchemas) {
363
+ const pivotTables = [];
364
+ if (!schema.properties) {
365
+ return pivotTables;
366
+ }
367
+ const sourceTable = toTableName(schema.name);
368
+ const sourcePkType = getIdType(schema);
369
+ for (const [, property] of Object.entries(schema.properties)) {
370
+ if (property.type !== "Association") {
371
+ continue;
372
+ }
373
+ const assocProp = property;
374
+ if (assocProp.relation !== "ManyToMany") {
375
+ continue;
376
+ }
377
+ const targetName = assocProp.target;
378
+ if (!targetName) {
379
+ continue;
380
+ }
381
+ const targetSchema = allSchemas[targetName];
382
+ const targetTable = toTableName(targetName);
383
+ const targetPkType = targetSchema ? getIdType(targetSchema) : "BigInt";
384
+ const isOwningSide = assocProp.owning ?? schema.name < targetName;
385
+ if (!isOwningSide) {
386
+ continue;
387
+ }
388
+ const pivotTableName = generatePivotTableName(sourceTable, targetTable, assocProp.joinTable);
389
+ const sourceColumn = sourceTable.replace(/ies$/, "y").replace(/s$/, "") + "_id";
390
+ const targetColumn = targetTable.replace(/ies$/, "y").replace(/s$/, "") + "_id";
391
+ pivotTables.push({
392
+ tableName: pivotTableName,
393
+ sourceTable,
394
+ targetTable,
395
+ sourceColumn,
396
+ targetColumn,
397
+ sourcePkType,
398
+ targetPkType,
399
+ onDelete: assocProp.onDelete,
400
+ onUpdate: assocProp.onUpdate
401
+ });
402
+ }
403
+ return pivotTables;
404
+ }
405
+ function generatePivotTableBlueprint(pivot) {
406
+ const columns = [];
407
+ const foreignKeys = [];
408
+ const indexes = [];
409
+ const getMethodForPkType = (pkType) => {
410
+ switch (pkType) {
411
+ case "Int":
412
+ return "unsignedInteger";
413
+ case "Uuid":
414
+ return "uuid";
415
+ case "String":
416
+ return "string";
417
+ default:
418
+ return "unsignedBigInteger";
419
+ }
420
+ };
421
+ columns.push({
422
+ name: pivot.sourceColumn,
423
+ method: getMethodForPkType(pivot.sourcePkType),
424
+ args: [pivot.sourceColumn],
425
+ modifiers: []
426
+ });
427
+ columns.push({
428
+ name: pivot.targetColumn,
429
+ method: getMethodForPkType(pivot.targetPkType),
430
+ args: [pivot.targetColumn],
431
+ modifiers: []
432
+ });
433
+ columns.push(...generateTimestampColumns());
434
+ foreignKeys.push({
435
+ columns: [pivot.sourceColumn],
436
+ references: "id",
437
+ on: [pivot.sourceTable],
438
+ onDelete: pivot.onDelete ?? "cascade",
439
+ onUpdate: pivot.onUpdate ?? "cascade"
440
+ });
441
+ foreignKeys.push({
442
+ columns: [pivot.targetColumn],
443
+ references: "id",
444
+ on: [pivot.targetTable],
445
+ onDelete: pivot.onDelete ?? "cascade",
446
+ onUpdate: pivot.onUpdate ?? "cascade"
447
+ });
448
+ indexes.push({
449
+ columns: [pivot.sourceColumn, pivot.targetColumn],
450
+ unique: true
451
+ });
452
+ indexes.push({
453
+ columns: [pivot.sourceColumn],
454
+ unique: false
455
+ });
456
+ indexes.push({
457
+ columns: [pivot.targetColumn],
458
+ unique: false
459
+ });
460
+ return {
461
+ tableName: pivot.tableName,
462
+ columns,
463
+ primaryKey: [pivot.sourceColumn, pivot.targetColumn],
464
+ foreignKeys,
465
+ indexes
466
+ };
467
+ }
468
+
469
+ // src/migration/generator.ts
470
+ function generateTimestamp() {
471
+ const now = /* @__PURE__ */ new Date();
472
+ const year = now.getFullYear();
473
+ const month = String(now.getMonth() + 1).padStart(2, "0");
474
+ const day = String(now.getDate()).padStart(2, "0");
475
+ const hours = String(now.getHours()).padStart(2, "0");
476
+ const minutes = String(now.getMinutes()).padStart(2, "0");
477
+ const seconds = String(now.getSeconds()).padStart(2, "0");
478
+ return `${year}_${month}_${day}_${hours}${minutes}${seconds}`;
479
+ }
480
+ function toClassName(tableName, type) {
481
+ const pascalCase = tableName.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
482
+ switch (type) {
483
+ case "create":
484
+ return `Create${pascalCase}Table`;
485
+ case "alter":
486
+ return `Alter${pascalCase}Table`;
487
+ case "drop":
488
+ return `Drop${pascalCase}Table`;
489
+ }
490
+ }
491
+ function generateFileName(tableName, type, timestamp) {
492
+ const ts = timestamp ?? generateTimestamp();
493
+ const action = type === "create" ? "create" : type === "drop" ? "drop" : "update";
494
+ return `${ts}_${action}_${tableName}_table.php`;
495
+ }
496
+ function renderCreateTableUp(blueprint) {
497
+ const lines = [];
498
+ for (const column of blueprint.columns) {
499
+ lines.push(` ${formatColumnMethod(column)}`);
500
+ }
501
+ return lines.join("\n");
502
+ }
503
+ function renderForeignKeys(blueprint) {
504
+ if (blueprint.foreignKeys.length === 0) {
505
+ return "";
506
+ }
507
+ const lines = blueprint.foreignKeys.map((fk) => ` ${formatForeignKey(fk)}`);
508
+ return "\n" + lines.join("\n");
509
+ }
510
+ function renderIndexes(blueprint) {
511
+ const customIndexes = blueprint.indexes.filter((idx) => {
512
+ if (idx.unique && idx.columns.length === 1) {
513
+ return false;
514
+ }
515
+ return true;
516
+ });
517
+ if (customIndexes.length === 0) {
518
+ return "";
519
+ }
520
+ const lines = customIndexes.map((idx) => ` ${formatIndex(idx)}`);
521
+ return "\n" + lines.join("\n");
522
+ }
523
+ function generateCreateMigration(blueprint, options = {}) {
524
+ const className = toClassName(blueprint.tableName, "create");
525
+ const fileName = generateFileName(blueprint.tableName, "create", options.timestamp);
526
+ const connection = options.connection ? `
527
+ protected $connection = '${options.connection}';
528
+ ` : "";
529
+ const upContent = renderCreateTableUp(blueprint);
530
+ const foreignKeyContent = renderForeignKeys(blueprint);
531
+ const indexContent = renderIndexes(blueprint);
532
+ const content = `<?php
533
+
534
+ use Illuminate\\Database\\Migrations\\Migration;
535
+ use Illuminate\\Database\\Schema\\Blueprint;
536
+ use Illuminate\\Support\\Facades\\Schema;
537
+
538
+ return new class extends Migration
539
+ {${connection}
540
+ /**
541
+ * Run the migrations.
542
+ */
543
+ public function up(): void
544
+ {
545
+ Schema::create('${blueprint.tableName}', function (Blueprint $table) {
546
+ ${upContent}${foreignKeyContent}${indexContent}
547
+ });
548
+ }
549
+
550
+ /**
551
+ * Reverse the migrations.
552
+ */
553
+ public function down(): void
554
+ {
555
+ Schema::dropIfExists('${blueprint.tableName}');
556
+ }
557
+ };
558
+ `;
559
+ return {
560
+ fileName,
561
+ className,
562
+ content,
563
+ tables: [blueprint.tableName],
564
+ type: "create"
565
+ };
566
+ }
567
+ function generateDropMigration(tableName, options = {}) {
568
+ const className = toClassName(tableName, "drop");
569
+ const fileName = generateFileName(tableName, "drop", options.timestamp);
570
+ const connection = options.connection ? `
571
+ protected $connection = '${options.connection}';
572
+ ` : "";
573
+ const content = `<?php
574
+
575
+ use Illuminate\\Database\\Migrations\\Migration;
576
+ use Illuminate\\Database\\Schema\\Blueprint;
577
+ use Illuminate\\Support\\Facades\\Schema;
578
+
579
+ return new class extends Migration
580
+ {${connection}
581
+ /**
582
+ * Run the migrations.
583
+ */
584
+ public function up(): void
585
+ {
586
+ Schema::dropIfExists('${tableName}');
587
+ }
588
+
589
+ /**
590
+ * Reverse the migrations.
591
+ */
592
+ public function down(): void
593
+ {
594
+ // Cannot recreate table without schema information
595
+ // This is a one-way migration
596
+ }
597
+ };
598
+ `;
599
+ return {
600
+ fileName,
601
+ className,
602
+ content,
603
+ tables: [tableName],
604
+ type: "drop"
605
+ };
606
+ }
607
+ function extractDependencies(schema) {
608
+ const deps = [];
609
+ if (!schema.properties) {
610
+ return deps;
611
+ }
612
+ for (const property of Object.values(schema.properties)) {
613
+ if (property.type !== "Association") {
614
+ continue;
615
+ }
616
+ const assocProp = property;
617
+ if ((assocProp.relation === "ManyToOne" || assocProp.relation === "OneToOne") && !assocProp.mappedBy && assocProp.target) {
618
+ deps.push(assocProp.target);
619
+ }
620
+ }
621
+ return deps;
622
+ }
623
+ function topologicalSort(schemas) {
624
+ const schemaList = Object.values(schemas).filter((s) => s.kind !== "enum");
625
+ const sorted = [];
626
+ const visited = /* @__PURE__ */ new Set();
627
+ const visiting = /* @__PURE__ */ new Set();
628
+ function visit(schema) {
629
+ if (visited.has(schema.name)) {
630
+ return;
631
+ }
632
+ if (visiting.has(schema.name)) {
633
+ return;
634
+ }
635
+ visiting.add(schema.name);
636
+ const deps = extractDependencies(schema);
637
+ for (const depName of deps) {
638
+ const depSchema = schemas[depName];
639
+ if (depSchema && depSchema.kind !== "enum") {
640
+ visit(depSchema);
641
+ }
642
+ }
643
+ visiting.delete(schema.name);
644
+ visited.add(schema.name);
645
+ sorted.push(schema);
646
+ }
647
+ for (const schema of schemaList) {
648
+ visit(schema);
649
+ }
650
+ return sorted;
651
+ }
652
+ function generateMigrations(schemas, options = {}) {
653
+ const migrations = [];
654
+ const pivotTablesGenerated = /* @__PURE__ */ new Set();
655
+ let timestampOffset = 0;
656
+ const sortedSchemas = topologicalSort(schemas);
657
+ for (const schema of sortedSchemas) {
658
+ const timestamp = options.timestamp ?? generateTimestamp();
659
+ const offsetTimestamp = incrementTimestamp(timestamp, timestampOffset);
660
+ timestampOffset++;
661
+ const blueprint = schemaToBlueprint(schema, schemas);
662
+ const migration = generateCreateMigration(blueprint, {
663
+ ...options,
664
+ timestamp: offsetTimestamp
665
+ });
666
+ migrations.push(migration);
667
+ }
668
+ for (const schema of sortedSchemas) {
669
+ const pivotTables = extractManyToManyRelations(schema, schemas);
670
+ for (const pivot of pivotTables) {
671
+ if (pivotTablesGenerated.has(pivot.tableName)) {
672
+ continue;
673
+ }
674
+ pivotTablesGenerated.add(pivot.tableName);
675
+ const timestamp = options.timestamp ?? generateTimestamp();
676
+ const offsetTimestamp = incrementTimestamp(timestamp, timestampOffset);
677
+ timestampOffset++;
678
+ const blueprint = generatePivotTableBlueprint(pivot);
679
+ const migration = generateCreateMigration(blueprint, {
680
+ ...options,
681
+ timestamp: offsetTimestamp
682
+ });
683
+ migrations.push(migration);
684
+ }
685
+ }
686
+ return migrations;
687
+ }
688
+ function incrementTimestamp(timestamp, seconds) {
689
+ if (seconds === 0) return timestamp;
690
+ const parts = timestamp.split("_");
691
+ if (parts.length < 4) {
692
+ return timestamp;
693
+ }
694
+ const yearPart = parts[0] ?? "2024";
695
+ const monthPart = parts[1] ?? "01";
696
+ const dayPart = parts[2] ?? "01";
697
+ const timePart = parts[3] ?? "000000";
698
+ const year = parseInt(yearPart, 10);
699
+ const month = parseInt(monthPart, 10) - 1;
700
+ const day = parseInt(dayPart, 10);
701
+ const hours = parseInt(timePart.substring(0, 2), 10);
702
+ const minutes = parseInt(timePart.substring(2, 4), 10);
703
+ const secs = parseInt(timePart.substring(4, 6), 10);
704
+ const date = new Date(year, month, day, hours, minutes, secs + seconds);
705
+ const newYear = date.getFullYear();
706
+ const newMonth = String(date.getMonth() + 1).padStart(2, "0");
707
+ const newDay = String(date.getDate()).padStart(2, "0");
708
+ const newHours = String(date.getHours()).padStart(2, "0");
709
+ const newMinutes = String(date.getMinutes()).padStart(2, "0");
710
+ const newSecs = String(date.getSeconds()).padStart(2, "0");
711
+ return `${newYear}_${newMonth}_${newDay}_${newHours}${newMinutes}${newSecs}`;
712
+ }
713
+ function generateMigrationFromSchema(schema, allSchemas, options = {}) {
714
+ const blueprint = schemaToBlueprint(schema, allSchemas);
715
+ return generateCreateMigration(blueprint, options);
716
+ }
717
+ function generateDropMigrationForTable(tableName, options = {}) {
718
+ return generateDropMigration(tableName, options);
719
+ }
720
+ function formatMigrationFile(migration) {
721
+ return migration.content;
722
+ }
723
+ function getMigrationPath(migration, outputDir = "database/migrations") {
724
+ return `${outputDir}/${migration.fileName}`;
725
+ }
726
+
727
+ // src/migration/alter-generator.ts
728
+ var TYPE_METHOD_MAP2 = {
729
+ String: "string",
730
+ Int: "integer",
731
+ BigInt: "bigInteger",
732
+ Float: "double",
733
+ Decimal: "decimal",
734
+ Boolean: "boolean",
735
+ Text: "text",
736
+ LongText: "longText",
737
+ Date: "date",
738
+ Time: "time",
739
+ Timestamp: "timestamp",
740
+ Json: "json",
741
+ Email: "string",
742
+ Password: "string",
743
+ File: "string",
744
+ MultiFile: "json",
745
+ Enum: "enum",
746
+ Select: "string",
747
+ Lookup: "unsignedBigInteger"
748
+ };
749
+ function generateTimestamp2() {
750
+ const now = /* @__PURE__ */ new Date();
751
+ const year = now.getFullYear();
752
+ const month = String(now.getMonth() + 1).padStart(2, "0");
753
+ const day = String(now.getDate()).padStart(2, "0");
754
+ const hours = String(now.getHours()).padStart(2, "0");
755
+ const minutes = String(now.getMinutes()).padStart(2, "0");
756
+ const seconds = String(now.getSeconds()).padStart(2, "0");
757
+ return `${year}_${month}_${day}_${hours}${minutes}${seconds}`;
758
+ }
759
+ function formatAddColumn(columnName, prop) {
760
+ const snakeColumn = toColumnName(columnName);
761
+ const method = TYPE_METHOD_MAP2[prop.type] ?? "string";
762
+ let code;
763
+ if (prop.type === "Decimal") {
764
+ const precision = prop.precision ?? 8;
765
+ const scale = prop.scale ?? 2;
766
+ code = `$table->${method}('${snakeColumn}', ${precision}, ${scale})`;
767
+ } else {
768
+ code = `$table->${method}('${snakeColumn}')`;
769
+ }
770
+ if (prop.nullable) code += "->nullable()";
771
+ if (prop.unique) code += "->unique()";
772
+ if (prop.default !== void 0) {
773
+ const defaultValue = typeof prop.default === "string" ? `'${prop.default}'` : JSON.stringify(prop.default);
774
+ code += `->default(${defaultValue})`;
775
+ }
776
+ return code + ";";
777
+ }
778
+ function formatDropColumn(columnName) {
779
+ const snakeColumn = toColumnName(columnName);
780
+ return `$table->dropColumn('${snakeColumn}');`;
781
+ }
782
+ function formatRenameColumn(oldName, newName) {
783
+ const oldSnake = toColumnName(oldName);
784
+ const newSnake = toColumnName(newName);
785
+ return `$table->renameColumn('${oldSnake}', '${newSnake}');`;
786
+ }
787
+ function formatModifyColumn(columnName, _prevProp, currProp) {
788
+ const snakeColumn = toColumnName(columnName);
789
+ const method = TYPE_METHOD_MAP2[currProp.type] ?? "string";
790
+ let code;
791
+ if (currProp.type === "Decimal") {
792
+ const precision = currProp.precision ?? 8;
793
+ const scale = currProp.scale ?? 2;
794
+ code = `$table->${method}('${snakeColumn}', ${precision}, ${scale})`;
795
+ } else {
796
+ code = `$table->${method}('${snakeColumn}')`;
797
+ }
798
+ if (currProp.nullable) code += "->nullable()";
799
+ if (currProp.unique) code += "->unique()";
800
+ if (currProp.default !== void 0) {
801
+ const defaultValue = typeof currProp.default === "string" ? `'${currProp.default}'` : JSON.stringify(currProp.default);
802
+ code += `->default(${defaultValue})`;
803
+ }
804
+ return code + "->change();";
805
+ }
806
+ function formatAddIndex(columns, unique) {
807
+ const snakeColumns = columns.map(toColumnName);
808
+ const method = unique ? "unique" : "index";
809
+ const colsArg = snakeColumns.length === 1 ? `'${snakeColumns[0]}'` : `[${snakeColumns.map((c) => `'${c}'`).join(", ")}]`;
810
+ return `$table->${method}(${colsArg});`;
811
+ }
812
+ function formatDropIndex(tableName, columns, unique) {
813
+ const snakeColumns = columns.map(toColumnName);
814
+ const method = unique ? "dropUnique" : "dropIndex";
815
+ const suffix = unique ? "unique" : "index";
816
+ const indexName = `${tableName}_${snakeColumns.join("_")}_${suffix}`;
817
+ return `$table->${method}('${indexName}');`;
818
+ }
819
+ function generateAlterMigrationContent(tableName, change, options = {}) {
820
+ const upLines = [];
821
+ const downLines = [];
822
+ if (change.columnChanges) {
823
+ for (const col of change.columnChanges) {
824
+ if (col.changeType === "added" && col.currentDef) {
825
+ upLines.push(` ${formatAddColumn(col.column, col.currentDef)}`);
826
+ downLines.push(` ${formatDropColumn(col.column)}`);
827
+ } else if (col.changeType === "removed" && col.previousDef) {
828
+ upLines.push(` ${formatDropColumn(col.column)}`);
829
+ downLines.push(` ${formatAddColumn(col.column, col.previousDef)}`);
830
+ } else if (col.changeType === "modified" && col.previousDef && col.currentDef) {
831
+ upLines.push(` ${formatModifyColumn(col.column, col.previousDef, col.currentDef)}`);
832
+ downLines.push(` ${formatModifyColumn(col.column, col.currentDef, col.previousDef)}`);
833
+ } else if (col.changeType === "renamed" && col.previousColumn) {
834
+ upLines.push(` ${formatRenameColumn(col.previousColumn, col.column)}`);
835
+ downLines.push(` ${formatRenameColumn(col.column, col.previousColumn)}`);
836
+ if (col.modifications && col.modifications.length > 0 && col.previousDef && col.currentDef) {
837
+ upLines.push(` ${formatModifyColumn(col.column, col.previousDef, col.currentDef)}`);
838
+ downLines.push(` ${formatModifyColumn(col.column, col.currentDef, col.previousDef)}`);
839
+ }
840
+ }
841
+ }
842
+ }
843
+ if (change.indexChanges) {
844
+ for (const idx of change.indexChanges) {
845
+ if (idx.changeType === "added") {
846
+ upLines.push(` ${formatAddIndex(idx.index.columns, idx.index.unique)}`);
847
+ downLines.push(` ${formatDropIndex(tableName, idx.index.columns, idx.index.unique)}`);
848
+ } else {
849
+ upLines.push(` ${formatDropIndex(tableName, idx.index.columns, idx.index.unique)}`);
850
+ downLines.push(` ${formatAddIndex(idx.index.columns, idx.index.unique)}`);
851
+ }
852
+ }
853
+ }
854
+ if (change.optionChanges) {
855
+ if (change.optionChanges.timestamps) {
856
+ const { from, to } = change.optionChanges.timestamps;
857
+ if (to && !from) {
858
+ upLines.push(` $table->timestamps();`);
859
+ downLines.push(` $table->dropTimestamps();`);
860
+ } else if (from && !to) {
861
+ upLines.push(` $table->dropTimestamps();`);
862
+ downLines.push(` $table->timestamps();`);
863
+ }
864
+ }
865
+ if (change.optionChanges.softDelete) {
866
+ const { from, to } = change.optionChanges.softDelete;
867
+ if (to && !from) {
868
+ upLines.push(` $table->softDeletes();`);
869
+ downLines.push(` $table->dropSoftDeletes();`);
870
+ } else if (from && !to) {
871
+ upLines.push(` $table->dropSoftDeletes();`);
872
+ downLines.push(` $table->softDeletes();`);
873
+ }
874
+ }
875
+ }
876
+ const connection = options.connection ? `
877
+ protected $connection = '${options.connection}';
878
+ ` : "";
879
+ return `<?php
880
+
881
+ use Illuminate\\Database\\Migrations\\Migration;
882
+ use Illuminate\\Database\\Schema\\Blueprint;
883
+ use Illuminate\\Support\\Facades\\Schema;
884
+
885
+ return new class extends Migration
886
+ {${connection}
887
+ /**
888
+ * Run the migrations.
889
+ */
890
+ public function up(): void
891
+ {
892
+ Schema::table('${tableName}', function (Blueprint $table) {
893
+ ${upLines.join("\n")}
894
+ });
895
+ }
896
+
897
+ /**
898
+ * Reverse the migrations.
899
+ */
900
+ public function down(): void
901
+ {
902
+ Schema::table('${tableName}', function (Blueprint $table) {
903
+ ${downLines.join("\n")}
904
+ });
905
+ }
906
+ };
907
+ `;
908
+ }
909
+ function generateAlterMigration(change, options = {}) {
910
+ if (change.changeType !== "modified") {
911
+ return null;
912
+ }
913
+ const hasChanges = change.columnChanges && change.columnChanges.length > 0 || change.indexChanges && change.indexChanges.length > 0 || change.optionChanges && (change.optionChanges.timestamps || change.optionChanges.softDelete);
914
+ if (!hasChanges) {
915
+ return null;
916
+ }
917
+ const tableName = toTableName(change.schemaName);
918
+ const timestamp = options.timestamp ?? generateTimestamp2();
919
+ const fileName = `${timestamp}_update_${tableName}_table.php`;
920
+ const content = generateAlterMigrationContent(tableName, change, options);
921
+ return {
922
+ fileName,
923
+ className: `Update${change.schemaName}Table`,
924
+ content,
925
+ tables: [tableName],
926
+ type: "alter"
927
+ };
928
+ }
929
+ function generateDropTableMigration(schemaName, options = {}) {
930
+ const tableName = toTableName(schemaName);
931
+ const timestamp = options.timestamp ?? generateTimestamp2();
932
+ const fileName = `${timestamp}_drop_${tableName}_table.php`;
933
+ const connection = options.connection ? `
934
+ protected $connection = '${options.connection}';
935
+ ` : "";
936
+ const content = `<?php
937
+
938
+ use Illuminate\\Database\\Migrations\\Migration;
939
+ use Illuminate\\Database\\Schema\\Blueprint;
940
+ use Illuminate\\Support\\Facades\\Schema;
941
+
942
+ return new class extends Migration
943
+ {${connection}
944
+ /**
945
+ * Run the migrations.
946
+ */
947
+ public function up(): void
948
+ {
949
+ Schema::dropIfExists('${tableName}');
950
+ }
951
+
952
+ /**
953
+ * Reverse the migrations.
954
+ */
955
+ public function down(): void
956
+ {
957
+ // Cannot recreate table without full schema
958
+ // Consider restoring from backup if needed
959
+ }
960
+ };
961
+ `;
962
+ return {
963
+ fileName,
964
+ className: `Drop${schemaName}Table`,
965
+ content,
966
+ tables: [tableName],
967
+ type: "drop"
968
+ };
969
+ }
970
+ function generateMigrationsFromChanges(changes, options = {}) {
971
+ const migrations = [];
972
+ let timestampOffset = 0;
973
+ const getNextTimestamp = () => {
974
+ const ts = options.timestamp ?? generateTimestamp2();
975
+ const offset = timestampOffset++;
976
+ if (offset === 0) return ts;
977
+ const parts = ts.split("_");
978
+ if (parts.length >= 4) {
979
+ const timePart = parts[3] ?? "000000";
980
+ const secs = parseInt(timePart.substring(4, 6), 10) + offset;
981
+ const newSecs = String(secs % 60).padStart(2, "0");
982
+ parts[3] = timePart.substring(0, 4) + newSecs;
983
+ return parts.join("_");
984
+ }
985
+ return ts;
986
+ };
987
+ for (const change of changes) {
988
+ if (change.changeType === "modified") {
989
+ const migration = generateAlterMigration(change, {
990
+ ...options,
991
+ timestamp: getNextTimestamp()
992
+ });
993
+ if (migration) {
994
+ migrations.push(migration);
995
+ }
996
+ } else if (change.changeType === "removed") {
997
+ migrations.push(
998
+ generateDropTableMigration(change.schemaName, {
999
+ ...options,
1000
+ timestamp: getNextTimestamp()
1001
+ })
1002
+ );
1003
+ }
1004
+ }
1005
+ return migrations;
1006
+ }
1007
+
1008
+ // src/utils.ts
1009
+ function toSnakeCase(str) {
1010
+ return str.replace(/([A-Z])/g, "_$1").replace(/^_/, "").toLowerCase();
1011
+ }
1012
+ function toPascalCase(str) {
1013
+ return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
1014
+ }
1015
+ function toCamelCase(str) {
1016
+ const pascal = toPascalCase(str);
1017
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
1018
+ }
1019
+ function pluralize(word) {
1020
+ if (word.endsWith("y") && !["ay", "ey", "iy", "oy", "uy"].some((v) => word.endsWith(v))) {
1021
+ return word.slice(0, -1) + "ies";
1022
+ }
1023
+ if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z") || word.endsWith("ch") || word.endsWith("sh")) {
1024
+ return word + "es";
1025
+ }
1026
+ return word + "s";
1027
+ }
1028
+
1029
+ // src/model/generator.ts
1030
+ var DEFAULT_OPTIONS = {
1031
+ baseModelNamespace: "App\\Models\\OmnifyBase",
1032
+ modelNamespace: "App\\Models",
1033
+ baseModelClassName: "BaseModel",
1034
+ baseModelPath: "app/Models/OmnifyBase",
1035
+ modelPath: "app/Models"
1036
+ };
1037
+ function resolveOptions(options) {
1038
+ return {
1039
+ baseModelNamespace: options?.baseModelNamespace ?? DEFAULT_OPTIONS.baseModelNamespace,
1040
+ modelNamespace: options?.modelNamespace ?? DEFAULT_OPTIONS.modelNamespace,
1041
+ baseModelClassName: options?.baseModelClassName ?? DEFAULT_OPTIONS.baseModelClassName,
1042
+ baseModelPath: options?.baseModelPath ?? DEFAULT_OPTIONS.baseModelPath,
1043
+ modelPath: options?.modelPath ?? DEFAULT_OPTIONS.modelPath
1044
+ };
1045
+ }
1046
+ function getCastType(propDef) {
1047
+ switch (propDef.type) {
1048
+ case "Boolean":
1049
+ return "boolean";
1050
+ case "Int":
1051
+ case "BigInt":
1052
+ return "integer";
1053
+ case "Float":
1054
+ return "float";
1055
+ case "Decimal":
1056
+ return "decimal:" + (propDef.scale ?? 2);
1057
+ case "Json":
1058
+ return "array";
1059
+ case "Date":
1060
+ return "date";
1061
+ case "Timestamp":
1062
+ return "datetime";
1063
+ case "Password":
1064
+ return "hashed";
1065
+ default:
1066
+ return null;
1067
+ }
1068
+ }
1069
+ function isNullable(propDef) {
1070
+ return "nullable" in propDef && propDef.nullable === true;
1071
+ }
1072
+ function getPhpDocType(propDef, schemas) {
1073
+ const nullable = isNullable(propDef);
1074
+ switch (propDef.type) {
1075
+ case "String":
1076
+ case "Text":
1077
+ case "LongText":
1078
+ case "Email":
1079
+ case "Password":
1080
+ return "string" + (nullable ? "|null" : "");
1081
+ case "Int":
1082
+ case "BigInt":
1083
+ return "int" + (nullable ? "|null" : "");
1084
+ case "Float":
1085
+ case "Decimal":
1086
+ return "float" + (nullable ? "|null" : "");
1087
+ case "Boolean":
1088
+ return "bool" + (nullable ? "|null" : "");
1089
+ case "Date":
1090
+ case "Time":
1091
+ case "Timestamp":
1092
+ return "\\Carbon\\Carbon" + (nullable ? "|null" : "");
1093
+ case "Json":
1094
+ return "array" + (nullable ? "|null" : "");
1095
+ case "Enum":
1096
+ case "EnumRef":
1097
+ return "string" + (nullable ? "|null" : "");
1098
+ case "Association": {
1099
+ const assoc = propDef;
1100
+ if (assoc.target) {
1101
+ const className = toPascalCase(assoc.target);
1102
+ switch (assoc.relation) {
1103
+ case "OneToMany":
1104
+ case "ManyToMany":
1105
+ case "MorphMany":
1106
+ case "MorphToMany":
1107
+ case "MorphedByMany":
1108
+ return `\\Illuminate\\Database\\Eloquent\\Collection<${className}>`;
1109
+ default:
1110
+ return className + "|null";
1111
+ }
1112
+ }
1113
+ return "mixed";
1114
+ }
1115
+ default:
1116
+ return "mixed";
1117
+ }
1118
+ }
1119
+ function generateBaseModel(schemas, options, stubContent) {
1120
+ const modelMap = Object.values(schemas).filter((s) => s.kind !== "enum").map((s) => {
1121
+ const className = toPascalCase(s.name);
1122
+ return ` '${s.name}' => \\${options.modelNamespace}\\${className}::class,`;
1123
+ }).join("\n");
1124
+ const content = stubContent.replace(/\{\{BASE_MODEL_NAMESPACE\}\}/g, options.baseModelNamespace).replace(/\{\{BASE_MODEL_CLASS\}\}/g, options.baseModelClassName).replace(/\{\{MODEL_MAP\}\}/g, modelMap);
1125
+ return {
1126
+ path: `${options.baseModelPath}/${options.baseModelClassName}.php`,
1127
+ content,
1128
+ type: "base-model",
1129
+ overwrite: true,
1130
+ schemaName: "__base__"
1131
+ };
1132
+ }
1133
+ function generateEntityBaseModel(schema, schemas, options, stubContent, authStubContent) {
1134
+ const className = toPascalCase(schema.name);
1135
+ const tableName = schema.options?.tableName ?? pluralize(toSnakeCase(schema.name));
1136
+ const isAuth = schema.options?.authenticatable ?? false;
1137
+ const primaryKey = "id";
1138
+ const idType = schema.options?.idType ?? "BigInt";
1139
+ const isUuid = idType === "Uuid";
1140
+ const isStringKey = idType === "Uuid" || idType === "String";
1141
+ const imports = [];
1142
+ const traits = [];
1143
+ const fillable = [];
1144
+ const hidden = [];
1145
+ const appends = [];
1146
+ const casts = [];
1147
+ const relations = [];
1148
+ const docProperties = [];
1149
+ if (schema.options?.softDelete) {
1150
+ imports.push("use Illuminate\\Database\\Eloquent\\SoftDeletes;");
1151
+ traits.push(" use SoftDeletes;");
1152
+ }
1153
+ const properties = schema.properties ?? {};
1154
+ for (const [propName, propDef] of Object.entries(properties)) {
1155
+ const snakeName = toSnakeCase(propName);
1156
+ const phpType = getPhpDocType(propDef, schemas);
1157
+ docProperties.push(` * @property ${phpType} $${snakeName}`);
1158
+ if (propDef.type === "Association") {
1159
+ const assoc = propDef;
1160
+ if (assoc.target) {
1161
+ imports.push(`use ${options.modelNamespace}\\${toPascalCase(assoc.target)};`);
1162
+ }
1163
+ relations.push(generateRelation(propName, assoc, schema, schemas, options));
1164
+ if (assoc.relation === "ManyToOne" || assoc.relation === "OneToOne") {
1165
+ if (!assoc.mappedBy) {
1166
+ const fkName = toSnakeCase(propName) + "_id";
1167
+ fillable.push(` '${fkName}',`);
1168
+ docProperties.push(` * @property int|null $${fkName}`);
1169
+ }
1170
+ }
1171
+ } else if (propDef.type === "Password") {
1172
+ fillable.push(` '${snakeName}',`);
1173
+ hidden.push(` '${snakeName}',`);
1174
+ const cast = getCastType(propDef);
1175
+ if (cast) {
1176
+ casts.push(` '${snakeName}' => '${cast}',`);
1177
+ }
1178
+ } else if (propDef.type === "File") {
1179
+ const relMethod = generateFileRelation(propName, propDef);
1180
+ relations.push(relMethod);
1181
+ } else {
1182
+ fillable.push(` '${snakeName}',`);
1183
+ const cast = getCastType(propDef);
1184
+ if (cast) {
1185
+ casts.push(` '${snakeName}' => '${cast}',`);
1186
+ }
1187
+ }
1188
+ }
1189
+ const docComment = `/**
1190
+ * ${className}BaseModel
1191
+ *
1192
+ ${docProperties.join("\n")}
1193
+ */`;
1194
+ const stub = isAuth ? authStubContent : stubContent;
1195
+ const keyType = isStringKey ? ` /**
1196
+ * The "type" of the primary key ID.
1197
+ */
1198
+ protected $keyType = 'string';
1199
+
1200
+ ` : "";
1201
+ const incrementing = isUuid ? ` /**
1202
+ * Indicates if the IDs are auto-incrementing.
1203
+ */
1204
+ public $incrementing = false;
1205
+
1206
+ ` : "";
1207
+ if (isUuid) {
1208
+ imports.push("use Illuminate\\Database\\Eloquent\\Concerns\\HasUuids;");
1209
+ traits.push(" use HasUuids;");
1210
+ }
1211
+ const content = stub.replace(/\{\{BASE_MODEL_NAMESPACE\}\}/g, options.baseModelNamespace).replace(/\{\{BASE_MODEL_CLASS\}\}/g, options.baseModelClassName).replace(/\{\{CLASS_NAME\}\}/g, className).replace(/\{\{TABLE_NAME\}\}/g, tableName).replace(/\{\{PRIMARY_KEY\}\}/g, primaryKey).replace(/\{\{KEY_TYPE\}\}/g, keyType).replace(/\{\{INCREMENTING\}\}/g, incrementing).replace(/\{\{TIMESTAMPS\}\}/g, schema.options?.timestamps !== false ? "true" : "false").replace(/\{\{IMPORTS\}\}/g, [...new Set(imports)].sort().join("\n")).replace(/\{\{TRAITS\}\}/g, traits.join("\n")).replace(/\{\{DOC_COMMENT\}\}/g, docComment).replace(/\{\{FILLABLE\}\}/g, fillable.join("\n")).replace(/\{\{HIDDEN\}\}/g, hidden.join("\n")).replace(/\{\{APPENDS\}\}/g, appends.join("\n")).replace(/\{\{CASTS\}\}/g, casts.join("\n")).replace(/\{\{RELATIONS\}\}/g, relations.join("\n\n"));
1212
+ return {
1213
+ path: `${options.baseModelPath}/${className}BaseModel.php`,
1214
+ content,
1215
+ type: "entity-base",
1216
+ overwrite: true,
1217
+ schemaName: schema.name
1218
+ };
1219
+ }
1220
+ function findInverseRelation(currentSchemaName, targetSchemaName, schemas) {
1221
+ const targetSchema = schemas[targetSchemaName];
1222
+ if (!targetSchema || !targetSchema.properties) {
1223
+ return null;
1224
+ }
1225
+ for (const [propName, propDef] of Object.entries(targetSchema.properties)) {
1226
+ if (propDef.type === "Association") {
1227
+ const assoc = propDef;
1228
+ if (assoc.relation === "ManyToOne" && assoc.target === currentSchemaName) {
1229
+ return propName;
1230
+ }
1231
+ }
1232
+ }
1233
+ return null;
1234
+ }
1235
+ function generateRelation(propName, assoc, schema, schemas, options) {
1236
+ const methodName = toCamelCase(propName);
1237
+ const targetClass = assoc.target ? toPascalCase(assoc.target) : "";
1238
+ const fkName = toSnakeCase(propName) + "_id";
1239
+ switch (assoc.relation) {
1240
+ case "ManyToOne":
1241
+ return ` /**
1242
+ * Get the ${propName} that owns this model.
1243
+ */
1244
+ public function ${methodName}(): BelongsTo
1245
+ {
1246
+ return $this->belongsTo(${targetClass}::class, '${fkName}');
1247
+ }`;
1248
+ case "OneToOne":
1249
+ if (assoc.mappedBy) {
1250
+ return ` /**
1251
+ * Get the ${propName} for this model.
1252
+ */
1253
+ public function ${methodName}(): HasOne
1254
+ {
1255
+ return $this->hasOne(${targetClass}::class, '${toSnakeCase(assoc.mappedBy)}_id');
1256
+ }`;
1257
+ }
1258
+ return ` /**
1259
+ * Get the ${propName} that owns this model.
1260
+ */
1261
+ public function ${methodName}(): BelongsTo
1262
+ {
1263
+ return $this->belongsTo(${targetClass}::class, '${fkName}');
1264
+ }`;
1265
+ case "OneToMany": {
1266
+ let foreignKey;
1267
+ if (assoc.inversedBy) {
1268
+ foreignKey = toSnakeCase(assoc.inversedBy) + "_id";
1269
+ } else if (assoc.target) {
1270
+ const inverseRelation = findInverseRelation(schema.name, assoc.target, schemas);
1271
+ if (inverseRelation) {
1272
+ foreignKey = toSnakeCase(inverseRelation) + "_id";
1273
+ } else {
1274
+ foreignKey = toSnakeCase(schema.name) + "_id";
1275
+ }
1276
+ } else {
1277
+ foreignKey = toSnakeCase(propName) + "_id";
1278
+ }
1279
+ return ` /**
1280
+ * Get the ${propName} for this model.
1281
+ */
1282
+ public function ${methodName}(): HasMany
1283
+ {
1284
+ return $this->hasMany(${targetClass}::class, '${foreignKey}');
1285
+ }`;
1286
+ }
1287
+ case "ManyToMany": {
1288
+ const pivotTable = assoc.joinTable ?? `${toSnakeCase(propName)}_pivot`;
1289
+ return ` /**
1290
+ * The ${propName} that belong to this model.
1291
+ */
1292
+ public function ${methodName}(): BelongsToMany
1293
+ {
1294
+ return $this->belongsToMany(${targetClass}::class, '${pivotTable}')
1295
+ ->withTimestamps();
1296
+ }`;
1297
+ }
1298
+ case "MorphTo":
1299
+ return ` /**
1300
+ * Get the parent ${propName} model.
1301
+ */
1302
+ public function ${methodName}(): MorphTo
1303
+ {
1304
+ return $this->morphTo('${methodName}');
1305
+ }`;
1306
+ case "MorphOne":
1307
+ return ` /**
1308
+ * Get the ${propName} for this model.
1309
+ */
1310
+ public function ${methodName}(): MorphOne
1311
+ {
1312
+ return $this->morphOne(${targetClass}::class, '${assoc.morphName ?? propName}');
1313
+ }`;
1314
+ case "MorphMany":
1315
+ return ` /**
1316
+ * Get the ${propName} for this model.
1317
+ */
1318
+ public function ${methodName}(): MorphMany
1319
+ {
1320
+ return $this->morphMany(${targetClass}::class, '${assoc.morphName ?? propName}');
1321
+ }`;
1322
+ default:
1323
+ return ` // TODO: Implement ${assoc.relation} relation for ${propName}`;
1324
+ }
1325
+ }
1326
+ function generateFileRelation(propName, propDef) {
1327
+ const methodName = toCamelCase(propName);
1328
+ const relationType = propDef.multiple ? "MorphMany" : "MorphOne";
1329
+ const relationMethod = propDef.multiple ? "morphMany" : "morphOne";
1330
+ return ` /**
1331
+ * Get the ${propName} file(s) for this model.
1332
+ */
1333
+ public function ${methodName}(): ${relationType}
1334
+ {
1335
+ return $this->${relationMethod}(FileUpload::class, 'uploadable')
1336
+ ->where('attribute_name', '${propName}');
1337
+ }`;
1338
+ }
1339
+ function generateEntityModel(schema, options, stubContent) {
1340
+ const className = toPascalCase(schema.name);
1341
+ const content = stubContent.replace(/\{\{BASE_MODEL_NAMESPACE\}\}/g, options.baseModelNamespace).replace(/\{\{MODEL_NAMESPACE\}\}/g, options.modelNamespace).replace(/\{\{CLASS_NAME\}\}/g, className);
1342
+ return {
1343
+ path: `${options.modelPath}/${className}.php`,
1344
+ content,
1345
+ type: "entity",
1346
+ overwrite: false,
1347
+ // Never overwrite user models
1348
+ schemaName: schema.name
1349
+ };
1350
+ }
1351
+ function getStubContent(stubName) {
1352
+ const stubs = {
1353
+ "base-model": `<?php
1354
+
1355
+ namespace {{BASE_MODEL_NAMESPACE}};
1356
+
1357
+ /**
1358
+ * Base model class for all Omnify-generated models.
1359
+ * Contains model mapping for polymorphic relations.
1360
+ *
1361
+ * DO NOT EDIT - This file is auto-generated by Omnify.
1362
+ * Any changes will be overwritten on next generation.
1363
+ *
1364
+ * @generated by @famgia/omnify-laravel
1365
+ */
1366
+
1367
+ use Illuminate\\Database\\Eloquent\\Model;
1368
+ use Illuminate\\Database\\Eloquent\\Relations\\Relation;
1369
+
1370
+ abstract class {{BASE_MODEL_CLASS}} extends Model
1371
+ {
1372
+ /**
1373
+ * Model class map for polymorphic relations.
1374
+ */
1375
+ protected static array $modelMap = [
1376
+ {{MODEL_MAP}}
1377
+ ];
1378
+
1379
+ /**
1380
+ * Boot the model and register morph map.
1381
+ */
1382
+ protected static function boot(): void
1383
+ {
1384
+ parent::boot();
1385
+
1386
+ // Register morph map for polymorphic relations
1387
+ Relation::enforceMorphMap(static::$modelMap);
1388
+ }
1389
+
1390
+ /**
1391
+ * Get the model class for a given morph type.
1392
+ */
1393
+ public static function getModelClass(string $morphType): ?string
1394
+ {
1395
+ return static::$modelMap[$morphType] ?? null;
1396
+ }
1397
+ }
1398
+ `,
1399
+ "entity-base": `<?php
1400
+
1401
+ namespace {{BASE_MODEL_NAMESPACE}};
1402
+
1403
+ /**
1404
+ * DO NOT EDIT - This file is auto-generated by Omnify.
1405
+ * Any changes will be overwritten on next generation.
1406
+ *
1407
+ * @generated by @famgia/omnify-laravel
1408
+ */
1409
+
1410
+ use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
1411
+ use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
1412
+ use Illuminate\\Database\\Eloquent\\Relations\\HasOne;
1413
+ use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
1414
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphTo;
1415
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphOne;
1416
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphMany;
1417
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphToMany;
1418
+ use Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;
1419
+ {{IMPORTS}}
1420
+
1421
+ {{DOC_COMMENT}}
1422
+ class {{CLASS_NAME}}BaseModel extends {{BASE_MODEL_CLASS}}
1423
+ {
1424
+ {{TRAITS}}
1425
+ /**
1426
+ * The table associated with the model.
1427
+ */
1428
+ protected $table = '{{TABLE_NAME}}';
1429
+
1430
+ /**
1431
+ * The primary key for the model.
1432
+ */
1433
+ protected $primaryKey = '{{PRIMARY_KEY}}';
1434
+
1435
+ {{KEY_TYPE}}
1436
+ {{INCREMENTING}}
1437
+ /**
1438
+ * Indicates if the model should be timestamped.
1439
+ */
1440
+ public $timestamps = {{TIMESTAMPS}};
1441
+
1442
+ /**
1443
+ * The attributes that are mass assignable.
1444
+ */
1445
+ protected $fillable = [
1446
+ {{FILLABLE}}
1447
+ ];
1448
+
1449
+ /**
1450
+ * The attributes that should be hidden for serialization.
1451
+ */
1452
+ protected $hidden = [
1453
+ {{HIDDEN}}
1454
+ ];
1455
+
1456
+ /**
1457
+ * The accessors to append to the model's array form.
1458
+ */
1459
+ protected $appends = [
1460
+ {{APPENDS}}
1461
+ ];
1462
+
1463
+ /**
1464
+ * Get the attributes that should be cast.
1465
+ */
1466
+ protected function casts(): array
1467
+ {
1468
+ return [
1469
+ {{CASTS}}
1470
+ ];
1471
+ }
1472
+
1473
+ {{RELATIONS}}
1474
+ }
1475
+ `,
1476
+ "entity-base-auth": `<?php
1477
+
1478
+ namespace {{BASE_MODEL_NAMESPACE}};
1479
+
1480
+ /**
1481
+ * DO NOT EDIT - This file is auto-generated by Omnify.
1482
+ * Any changes will be overwritten on next generation.
1483
+ *
1484
+ * @generated by @famgia/omnify-laravel
1485
+ */
1486
+
1487
+ use Illuminate\\Foundation\\Auth\\User as Authenticatable;
1488
+ use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
1489
+ use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
1490
+ use Illuminate\\Database\\Eloquent\\Relations\\HasOne;
1491
+ use Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;
1492
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphTo;
1493
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphOne;
1494
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphMany;
1495
+ use Illuminate\\Database\\Eloquent\\Relations\\MorphToMany;
1496
+ use Illuminate\\Database\\Eloquent\\Collection as EloquentCollection;
1497
+ use Illuminate\\Notifications\\Notifiable;
1498
+ {{IMPORTS}}
1499
+
1500
+ {{DOC_COMMENT}}
1501
+ class {{CLASS_NAME}}BaseModel extends Authenticatable
1502
+ {
1503
+ use Notifiable;
1504
+ {{TRAITS}}
1505
+ /**
1506
+ * The table associated with the model.
1507
+ */
1508
+ protected $table = '{{TABLE_NAME}}';
1509
+
1510
+ /**
1511
+ * The primary key for the model.
1512
+ */
1513
+ protected $primaryKey = '{{PRIMARY_KEY}}';
1514
+
1515
+ {{KEY_TYPE}}
1516
+ {{INCREMENTING}}
1517
+ /**
1518
+ * Indicates if the model should be timestamped.
1519
+ */
1520
+ public $timestamps = {{TIMESTAMPS}};
1521
+
1522
+ /**
1523
+ * The attributes that are mass assignable.
1524
+ */
1525
+ protected $fillable = [
1526
+ {{FILLABLE}}
1527
+ ];
1528
+
1529
+ /**
1530
+ * The attributes that should be hidden for serialization.
1531
+ */
1532
+ protected $hidden = [
1533
+ {{HIDDEN}}
1534
+ ];
1535
+
1536
+ /**
1537
+ * The accessors to append to the model's array form.
1538
+ */
1539
+ protected $appends = [
1540
+ {{APPENDS}}
1541
+ ];
1542
+
1543
+ /**
1544
+ * Get the attributes that should be cast.
1545
+ */
1546
+ protected function casts(): array
1547
+ {
1548
+ return [
1549
+ {{CASTS}}
1550
+ ];
1551
+ }
1552
+
1553
+ {{RELATIONS}}
1554
+ }
1555
+ `,
1556
+ "entity": `<?php
1557
+
1558
+ namespace {{MODEL_NAMESPACE}};
1559
+
1560
+ use {{BASE_MODEL_NAMESPACE}}\\{{CLASS_NAME}}BaseModel;
1561
+ use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
1562
+
1563
+ /**
1564
+ * {{CLASS_NAME}} Model
1565
+ *
1566
+ * This file is generated once and can be customized.
1567
+ * Add your custom methods and logic here.
1568
+ */
1569
+ class {{CLASS_NAME}} extends {{CLASS_NAME}}BaseModel
1570
+ {
1571
+ use HasFactory;
1572
+
1573
+ /**
1574
+ * Create a new model instance.
1575
+ */
1576
+ public function __construct(array $attributes = [])
1577
+ {
1578
+ parent::__construct($attributes);
1579
+ }
1580
+
1581
+ // Add your custom methods here
1582
+ }
1583
+ `,
1584
+ "service-provider": `<?php
1585
+
1586
+ namespace App\\Providers;
1587
+
1588
+ use Illuminate\\Database\\Eloquent\\Relations\\Relation;
1589
+ use Illuminate\\Support\\ServiceProvider;
1590
+
1591
+ /**
1592
+ * Omnify Service Provider
1593
+ *
1594
+ * DO NOT EDIT - This file is auto-generated by Omnify.
1595
+ * Any changes will be overwritten on next generation.
1596
+ *
1597
+ * - Loads Omnify migrations from database/migrations/omnify
1598
+ * - Registers morph map for polymorphic relationships
1599
+ *
1600
+ * @generated by @famgia/omnify-laravel
1601
+ */
1602
+ class OmnifyServiceProvider extends ServiceProvider
1603
+ {
1604
+ /**
1605
+ * Register any application services.
1606
+ */
1607
+ public function register(): void
1608
+ {
1609
+ //
1610
+ }
1611
+
1612
+ /**
1613
+ * Bootstrap any application services.
1614
+ */
1615
+ public function boot(): void
1616
+ {
1617
+ // Load Omnify migrations from custom directory
1618
+ $this->loadMigrationsFrom(database_path('migrations/omnify'));
1619
+
1620
+ // Register morph map for polymorphic relationships
1621
+ Relation::enforceMorphMap([
1622
+ {{MORPH_MAP}}
1623
+ ]);
1624
+ }
1625
+ }
1626
+ `
1627
+ };
1628
+ return stubs[stubName] ?? "";
1629
+ }
1630
+ function generateServiceProvider(schemas, options, stubContent) {
1631
+ const morphMap = Object.values(schemas).filter((s) => s.kind !== "enum").map((s) => {
1632
+ const className = toPascalCase(s.name);
1633
+ return ` '${s.name}' => \\${options.modelNamespace}\\${className}::class,`;
1634
+ }).join("\n");
1635
+ const content = stubContent.replace(/\{\{MORPH_MAP\}\}/g, morphMap);
1636
+ return {
1637
+ path: "app/Providers/OmnifyServiceProvider.php",
1638
+ content,
1639
+ type: "service-provider",
1640
+ overwrite: true,
1641
+ // Always overwrite to keep morph map in sync
1642
+ schemaName: "__service_provider__"
1643
+ };
1644
+ }
1645
+ function generateModels(schemas, options) {
1646
+ const resolved = resolveOptions(options);
1647
+ const models = [];
1648
+ models.push(generateBaseModel(schemas, resolved, getStubContent("base-model")));
1649
+ models.push(generateServiceProvider(schemas, resolved, getStubContent("service-provider")));
1650
+ for (const schema of Object.values(schemas)) {
1651
+ if (schema.kind === "enum") {
1652
+ continue;
1653
+ }
1654
+ models.push(generateEntityBaseModel(
1655
+ schema,
1656
+ schemas,
1657
+ resolved,
1658
+ getStubContent("entity-base"),
1659
+ getStubContent("entity-base-auth")
1660
+ ));
1661
+ models.push(generateEntityModel(schema, resolved, getStubContent("entity")));
1662
+ }
1663
+ return models;
1664
+ }
1665
+ function getModelPath(model) {
1666
+ return model.path;
1667
+ }
1668
+ function generateProviderRegistration(existingContent, laravelVersion) {
1669
+ const providerClass = "App\\Providers\\OmnifyServiceProvider::class";
1670
+ const providerLine = ` ${providerClass},`;
1671
+ if (existingContent && existingContent.includes("OmnifyServiceProvider")) {
1672
+ return {
1673
+ path: laravelVersion === "laravel11+" ? "bootstrap/providers.php" : "config/app.php",
1674
+ content: existingContent,
1675
+ laravelVersion,
1676
+ alreadyRegistered: true
1677
+ };
1678
+ }
1679
+ if (laravelVersion === "laravel11+") {
1680
+ if (existingContent) {
1681
+ const lines = existingContent.split("\n");
1682
+ const result = [];
1683
+ let inserted = false;
1684
+ for (let i = 0; i < lines.length; i++) {
1685
+ const line = lines[i];
1686
+ if (!inserted && line.trim() === "];") {
1687
+ result.push(providerLine);
1688
+ inserted = true;
1689
+ }
1690
+ result.push(line);
1691
+ }
1692
+ return {
1693
+ path: "bootstrap/providers.php",
1694
+ content: result.join("\n"),
1695
+ laravelVersion,
1696
+ alreadyRegistered: false
1697
+ };
1698
+ } else {
1699
+ return {
1700
+ path: "bootstrap/providers.php",
1701
+ content: `<?php
1702
+
1703
+ return [
1704
+ App\\Providers\\AppServiceProvider::class,
1705
+ ${providerLine}
1706
+ ];
1707
+ `,
1708
+ laravelVersion,
1709
+ alreadyRegistered: false
1710
+ };
1711
+ }
1712
+ } else {
1713
+ if (existingContent) {
1714
+ const providersSectionRegex = /'providers'\s*=>\s*\[[\s\S]*?\n(\s*)\]/m;
1715
+ const match = existingContent.match(providersSectionRegex);
1716
+ if (match) {
1717
+ const providersStart = existingContent.indexOf("'providers'");
1718
+ if (providersStart === -1) {
1719
+ return null;
1720
+ }
1721
+ let depth = 0;
1722
+ let foundStart = false;
1723
+ let insertPos = -1;
1724
+ for (let i = providersStart; i < existingContent.length; i++) {
1725
+ const char = existingContent[i];
1726
+ if (char === "[") {
1727
+ foundStart = true;
1728
+ depth++;
1729
+ } else if (char === "]") {
1730
+ depth--;
1731
+ if (foundStart && depth === 0) {
1732
+ insertPos = i;
1733
+ break;
1734
+ }
1735
+ }
1736
+ }
1737
+ if (insertPos !== -1) {
1738
+ const beforeClose = existingContent.substring(0, insertPos);
1739
+ const lastNewline = beforeClose.lastIndexOf("\n");
1740
+ const content = existingContent.substring(0, lastNewline + 1) + providerLine + "\n" + existingContent.substring(lastNewline + 1);
1741
+ return {
1742
+ path: "config/app.php",
1743
+ content,
1744
+ laravelVersion,
1745
+ alreadyRegistered: false
1746
+ };
1747
+ }
1748
+ }
1749
+ return null;
1750
+ } else {
1751
+ return null;
1752
+ }
1753
+ }
1754
+ }
1755
+
1756
+ // src/factory/generator.ts
1757
+ function resolveOptions2(options) {
1758
+ return {
1759
+ modelNamespace: options?.modelNamespace ?? "App\\Models",
1760
+ factoryPath: options?.factoryPath ?? "database/factories",
1761
+ fakerLocale: options?.fakerLocale ?? "en_US"
1762
+ };
1763
+ }
1764
+ function getStubContent2() {
1765
+ return `<?php
1766
+
1767
+ namespace Database\\Factories;
1768
+
1769
+ use {{MODEL_NAMESPACE}}\\{{MODEL_NAME}};
1770
+ use Illuminate\\Database\\Eloquent\\Factories\\Factory;
1771
+ {{IMPORTS}}
1772
+
1773
+ /**
1774
+ * @extends Factory<{{MODEL_NAME}}>
1775
+ */
1776
+ class {{MODEL_NAME}}Factory extends Factory
1777
+ {
1778
+ protected $model = {{MODEL_NAME}}::class;
1779
+
1780
+ /**
1781
+ * Define the model's default state.
1782
+ *
1783
+ * @return array<string, mixed>
1784
+ */
1785
+ public function definition(): array
1786
+ {
1787
+ return [
1788
+ {{ATTRIBUTES}}
1789
+ ];
1790
+ }
1791
+ }
1792
+ `;
1793
+ }
1794
+ function generateFakeData(propertyName, property, schema, schemas) {
1795
+ const type = property.type;
1796
+ if (["deleted_at", "created_at", "updated_at"].includes(propertyName)) {
1797
+ return null;
1798
+ }
1799
+ if (type === "Association") {
1800
+ return null;
1801
+ }
1802
+ switch (type) {
1803
+ case "String":
1804
+ return generateStringFake(propertyName, property);
1805
+ case "Email":
1806
+ return `'${propertyName}' => fake()->unique()->safeEmail(),`;
1807
+ case "Password":
1808
+ return `'${propertyName}' => bcrypt('password'),`;
1809
+ case "Int":
1810
+ case "BigInt":
1811
+ return generateIntFake(propertyName, property);
1812
+ case "Float":
1813
+ case "Decimal":
1814
+ return `'${propertyName}' => fake()->randomFloat(2, 1, 10000),`;
1815
+ case "Boolean":
1816
+ return `'${propertyName}' => fake()->boolean(),`;
1817
+ case "Text":
1818
+ return `'${propertyName}' => fake()->paragraphs(3, true),`;
1819
+ case "LongText":
1820
+ return `'${propertyName}' => fake()->paragraphs(5, true),`;
1821
+ case "Date":
1822
+ return `'${propertyName}' => fake()->date(),`;
1823
+ case "Time":
1824
+ return `'${propertyName}' => fake()->time(),`;
1825
+ case "Timestamp":
1826
+ case "DateTime":
1827
+ return `'${propertyName}' => fake()->dateTime(),`;
1828
+ case "Json":
1829
+ return `'${propertyName}' => [],`;
1830
+ case "Enum":
1831
+ return generateEnumFake(propertyName, property);
1832
+ case "EnumRef":
1833
+ return generateEnumRefFake(propertyName, property, schemas);
1834
+ default:
1835
+ return `'${propertyName}' => fake()->sentence(),`;
1836
+ }
1837
+ }
1838
+ function generateStringFake(propertyName, property) {
1839
+ if (propertyName === "slug") {
1840
+ return `'${propertyName}' => fake()->unique()->slug(),`;
1841
+ }
1842
+ if (propertyName === "uuid" || propertyName === "uid") {
1843
+ return `'${propertyName}' => (string) \\Illuminate\\Support\\Str::uuid(),`;
1844
+ }
1845
+ if (propertyName.includes("email")) {
1846
+ return `'${propertyName}' => fake()->unique()->safeEmail(),`;
1847
+ }
1848
+ if (propertyName.includes("phone")) {
1849
+ return `'${propertyName}' => fake()->phoneNumber(),`;
1850
+ }
1851
+ if (propertyName.includes("image") || propertyName.includes("photo") || propertyName.includes("avatar")) {
1852
+ return `'${propertyName}' => fake()->imageUrl(),`;
1853
+ }
1854
+ if (propertyName.includes("url") || propertyName.includes("website")) {
1855
+ return `'${propertyName}' => fake()->url(),`;
1856
+ }
1857
+ if (propertyName.includes("path") || propertyName.includes("file")) {
1858
+ return `'${propertyName}' => 'uploads/' . fake()->uuid() . '.jpg',`;
1859
+ }
1860
+ if (propertyName === "name" || propertyName === "title") {
1861
+ return `'${propertyName}' => fake()->sentence(3),`;
1862
+ }
1863
+ if (propertyName.includes("name")) {
1864
+ return `'${propertyName}' => fake()->name(),`;
1865
+ }
1866
+ if (propertyName.includes("address")) {
1867
+ return `'${propertyName}' => fake()->address(),`;
1868
+ }
1869
+ if (propertyName.includes("city")) {
1870
+ return `'${propertyName}' => fake()->city(),`;
1871
+ }
1872
+ if (propertyName.includes("country")) {
1873
+ return `'${propertyName}' => fake()->country(),`;
1874
+ }
1875
+ if (propertyName.includes("zip") || propertyName.includes("postal")) {
1876
+ return `'${propertyName}' => fake()->postcode(),`;
1877
+ }
1878
+ if (propertyName.includes("color")) {
1879
+ return `'${propertyName}' => fake()->hexColor(),`;
1880
+ }
1881
+ if (propertyName.includes("token") || propertyName.includes("secret") || propertyName.includes("key")) {
1882
+ return `'${propertyName}' => \\Illuminate\\Support\\Str::random(32),`;
1883
+ }
1884
+ if (propertyName.includes("code")) {
1885
+ return `'${propertyName}' => fake()->unique()->regexify('[A-Z0-9]{8}'),`;
1886
+ }
1887
+ const length = property.length;
1888
+ if (length && length <= 50) {
1889
+ return `'${propertyName}' => fake()->words(3, true),`;
1890
+ }
1891
+ return `'${propertyName}' => fake()->sentence(),`;
1892
+ }
1893
+ function generateIntFake(propertyName, property) {
1894
+ if (propertyName.includes("count") || propertyName.includes("quantity")) {
1895
+ return `'${propertyName}' => fake()->numberBetween(0, 100),`;
1896
+ }
1897
+ if (propertyName.includes("price") || propertyName.includes("amount") || propertyName.includes("cost")) {
1898
+ return `'${propertyName}' => fake()->numberBetween(100, 10000),`;
1899
+ }
1900
+ if (propertyName.includes("order") || propertyName.includes("sort") || propertyName.includes("position")) {
1901
+ return `'${propertyName}' => fake()->numberBetween(1, 100),`;
1902
+ }
1903
+ if (propertyName.includes("age")) {
1904
+ return `'${propertyName}' => fake()->numberBetween(18, 80),`;
1905
+ }
1906
+ if (propertyName.includes("year")) {
1907
+ return `'${propertyName}' => fake()->year(),`;
1908
+ }
1909
+ return `'${propertyName}' => fake()->numberBetween(1, 1000),`;
1910
+ }
1911
+ function generateEnumFake(propertyName, property) {
1912
+ const enumValues = property.enum;
1913
+ if (!enumValues || enumValues.length === 0) {
1914
+ return `'${propertyName}' => null,`;
1915
+ }
1916
+ const values = enumValues.map((v) => typeof v === "string" ? v : v.value);
1917
+ const valuesStr = values.map((v) => `'${v}'`).join(", ");
1918
+ return `'${propertyName}' => fake()->randomElement([${valuesStr}]),`;
1919
+ }
1920
+ function generateEnumRefFake(propertyName, property, schemas) {
1921
+ const enumName = property.enum;
1922
+ if (!enumName) {
1923
+ return `'${propertyName}' => null,`;
1924
+ }
1925
+ const enumSchema = schemas[enumName];
1926
+ if (!enumSchema || enumSchema.kind !== "enum" || !enumSchema.values) {
1927
+ return `'${propertyName}' => null,`;
1928
+ }
1929
+ const valuesStr = enumSchema.values.map((v) => `'${v}'`).join(", ");
1930
+ return `'${propertyName}' => fake()->randomElement([${valuesStr}]),`;
1931
+ }
1932
+ function generateAssociationFake(propertyName, property, schema, schemas, modelNamespace) {
1933
+ if (property.type !== "Association") {
1934
+ return null;
1935
+ }
1936
+ const relation = property.relation;
1937
+ const target = property.target;
1938
+ if (relation !== "ManyToOne" || !target) {
1939
+ return null;
1940
+ }
1941
+ const foreignKey = `${toSnakeCase(propertyName)}_id`;
1942
+ const isNullable2 = property.nullable ?? false;
1943
+ const targetSchema = schemas[target];
1944
+ if (!targetSchema) {
1945
+ return null;
1946
+ }
1947
+ let fake;
1948
+ if (isNullable2) {
1949
+ fake = `'${foreignKey}' => ${target}::query()->inRandomOrder()->first()?->id,`;
1950
+ } else {
1951
+ fake = `'${foreignKey}' => ${target}::query()->inRandomOrder()->first()?->id ?? ${target}::factory()->create()->id,`;
1952
+ }
1953
+ let importStatement;
1954
+ if (target !== schema.name) {
1955
+ importStatement = `use ${modelNamespace}\\${target};`;
1956
+ }
1957
+ return { fake, import: importStatement };
1958
+ }
1959
+ function generateFactory(schema, schemas, options, stubContent) {
1960
+ if (schema.kind === "enum") {
1961
+ return null;
1962
+ }
1963
+ const modelName = toPascalCase(schema.name);
1964
+ const factoryName = `${modelName}Factory`;
1965
+ const attributes = [];
1966
+ const imports = [];
1967
+ if (schema.properties) {
1968
+ for (const [propName, prop] of Object.entries(schema.properties)) {
1969
+ if (prop.type === "Association") {
1970
+ const assocResult = generateAssociationFake(propName, prop, schema, schemas, options.modelNamespace);
1971
+ if (assocResult) {
1972
+ attributes.push(assocResult.fake);
1973
+ if (assocResult.import) {
1974
+ imports.push(assocResult.import);
1975
+ }
1976
+ }
1977
+ continue;
1978
+ }
1979
+ const fake = generateFakeData(propName, prop, schema, schemas);
1980
+ if (fake) {
1981
+ attributes.push(fake);
1982
+ }
1983
+ }
1984
+ }
1985
+ let content = stubContent;
1986
+ content = content.replace(/\{\{MODEL_NAMESPACE\}\}/g, options.modelNamespace);
1987
+ content = content.replace(/\{\{MODEL_NAME\}\}/g, modelName);
1988
+ const uniqueImports = [...new Set(imports)];
1989
+ const importsStr = uniqueImports.length > 0 ? "\n" + uniqueImports.join("\n") : "";
1990
+ content = content.replace(/\{\{IMPORTS\}\}/g, importsStr);
1991
+ const attributesStr = attributes.length > 0 ? attributes.map((a) => ` ${a}`).join("\n") : "";
1992
+ content = content.replace(/\{\{ATTRIBUTES\}\}/g, attributesStr);
1993
+ return {
1994
+ name: factoryName,
1995
+ schemaName: schema.name,
1996
+ path: `${options.factoryPath}/${factoryName}.php`,
1997
+ content,
1998
+ overwrite: false
1999
+ // Factories should not overwrite existing files
2000
+ };
2001
+ }
2002
+ function generateFactories(schemas, options) {
2003
+ const resolved = resolveOptions2(options);
2004
+ const stubContent = getStubContent2();
2005
+ const factories = [];
2006
+ for (const schema of Object.values(schemas)) {
2007
+ const factory = generateFactory(schema, schemas, resolved, stubContent);
2008
+ if (factory) {
2009
+ factories.push(factory);
2010
+ }
2011
+ }
2012
+ return factories;
2013
+ }
2014
+ function getFactoryPath(factory) {
2015
+ return factory.path;
2016
+ }
2017
+
2018
+ // src/plugin.ts
2019
+ var LARAVEL_CONFIG_SCHEMA = {
2020
+ fields: [
2021
+ {
2022
+ key: "migrationsPath",
2023
+ type: "path",
2024
+ label: "Migrations Path",
2025
+ description: "Directory for Laravel migration files (loaded via OmnifyServiceProvider)",
2026
+ default: "database/migrations/omnify",
2027
+ group: "output"
2028
+ },
2029
+ {
2030
+ key: "modelsPath",
2031
+ type: "path",
2032
+ label: "Models Path",
2033
+ description: "Directory for user-editable model files",
2034
+ default: "app/Models",
2035
+ group: "output"
2036
+ },
2037
+ {
2038
+ key: "baseModelsPath",
2039
+ type: "path",
2040
+ label: "Base Models Path",
2041
+ description: "Directory for auto-generated base model files",
2042
+ default: "app/Models/OmnifyBase",
2043
+ group: "output"
2044
+ },
2045
+ {
2046
+ key: "generateModels",
2047
+ type: "boolean",
2048
+ label: "Generate Models",
2049
+ description: "Generate Eloquent model classes",
2050
+ default: true,
2051
+ group: "options"
2052
+ },
2053
+ {
2054
+ key: "factoriesPath",
2055
+ type: "path",
2056
+ label: "Factories Path",
2057
+ description: "Directory for Laravel factory files",
2058
+ default: "database/factories",
2059
+ group: "output"
2060
+ },
2061
+ {
2062
+ key: "generateFactories",
2063
+ type: "boolean",
2064
+ label: "Generate Factories",
2065
+ description: "Generate Laravel factory classes for testing",
2066
+ default: true,
2067
+ group: "options"
2068
+ },
2069
+ {
2070
+ key: "connection",
2071
+ type: "string",
2072
+ label: "Database Connection",
2073
+ description: "Laravel database connection name (optional)",
2074
+ placeholder: "mysql",
2075
+ group: "options"
2076
+ }
2077
+ ]
2078
+ };
2079
+ function resolveOptions3(options) {
2080
+ return {
2081
+ migrationsPath: options?.migrationsPath ?? "database/migrations/omnify",
2082
+ modelsPath: options?.modelsPath ?? "app/Models",
2083
+ baseModelsPath: options?.baseModelsPath ?? "app/Models/OmnifyBase",
2084
+ modelNamespace: options?.modelNamespace ?? "App\\Models",
2085
+ baseModelNamespace: options?.baseModelNamespace ?? "App\\Models\\OmnifyBase",
2086
+ generateModels: options?.generateModels ?? true,
2087
+ factoriesPath: options?.factoriesPath ?? "database/factories",
2088
+ generateFactories: options?.generateFactories ?? true,
2089
+ fakerLocale: options?.fakerLocale ?? "en_US",
2090
+ connection: options?.connection,
2091
+ timestamp: options?.timestamp
2092
+ };
2093
+ }
2094
+ function laravelPlugin(options) {
2095
+ const resolved = resolveOptions3(options);
2096
+ const migrationGenerator = {
2097
+ name: "laravel-migrations",
2098
+ description: "Generate Laravel migration files",
2099
+ generate: async (ctx) => {
2100
+ const migrationOptions = {
2101
+ connection: resolved.connection,
2102
+ timestamp: resolved.timestamp
2103
+ };
2104
+ const migrations = generateMigrations(ctx.schemas, migrationOptions);
2105
+ return migrations.map((migration) => ({
2106
+ path: getMigrationPath(migration, resolved.migrationsPath),
2107
+ content: migration.content,
2108
+ type: "migration",
2109
+ metadata: {
2110
+ tableName: migration.tables[0],
2111
+ migrationType: migration.type
2112
+ }
2113
+ }));
2114
+ }
2115
+ };
2116
+ const modelGenerator = {
2117
+ name: "laravel-models",
2118
+ description: "Generate Eloquent model classes",
2119
+ generate: async (ctx) => {
2120
+ const modelOptions = {
2121
+ modelNamespace: resolved.modelNamespace,
2122
+ baseModelNamespace: resolved.baseModelNamespace,
2123
+ modelPath: resolved.modelsPath,
2124
+ baseModelPath: resolved.baseModelsPath
2125
+ };
2126
+ const models = generateModels(ctx.schemas, modelOptions);
2127
+ const outputs = models.map((model) => ({
2128
+ path: getModelPath(model),
2129
+ content: model.content,
2130
+ type: "model",
2131
+ // Skip writing user models if they already exist
2132
+ skipIfExists: !model.overwrite,
2133
+ metadata: {
2134
+ modelType: model.type,
2135
+ schemaName: model.schemaName
2136
+ }
2137
+ }));
2138
+ const bootstrapProvidersPath = join(ctx.cwd, "bootstrap/providers.php");
2139
+ const configAppPath = join(ctx.cwd, "config/app.php");
2140
+ let existingContent = null;
2141
+ let laravelVersion;
2142
+ if (existsSync(bootstrapProvidersPath)) {
2143
+ laravelVersion = "laravel11+";
2144
+ try {
2145
+ existingContent = readFileSync(bootstrapProvidersPath, "utf-8");
2146
+ } catch {
2147
+ existingContent = null;
2148
+ }
2149
+ } else if (existsSync(configAppPath)) {
2150
+ laravelVersion = "laravel10-";
2151
+ try {
2152
+ existingContent = readFileSync(configAppPath, "utf-8");
2153
+ } catch {
2154
+ existingContent = null;
2155
+ }
2156
+ } else {
2157
+ laravelVersion = "laravel11+";
2158
+ }
2159
+ const registration = generateProviderRegistration(existingContent, laravelVersion);
2160
+ if (registration && !registration.alreadyRegistered) {
2161
+ outputs.push({
2162
+ path: registration.path,
2163
+ content: registration.content,
2164
+ type: "other",
2165
+ skipIfExists: false,
2166
+ // We want to modify the file
2167
+ metadata: {
2168
+ registrationType: "provider-registration",
2169
+ laravelVersion: registration.laravelVersion
2170
+ }
2171
+ });
2172
+ ctx.logger.info(`OmnifyServiceProvider will be registered in ${registration.path}`);
2173
+ } else if (registration?.alreadyRegistered) {
2174
+ ctx.logger.info("OmnifyServiceProvider is already registered");
2175
+ }
2176
+ return outputs;
2177
+ }
2178
+ };
2179
+ const factoryGenerator = {
2180
+ name: "laravel-factories",
2181
+ description: "Generate Laravel factory classes for testing",
2182
+ generate: async (ctx) => {
2183
+ const factoryOptions = {
2184
+ modelNamespace: resolved.modelNamespace,
2185
+ factoryPath: resolved.factoriesPath,
2186
+ fakerLocale: resolved.fakerLocale
2187
+ };
2188
+ const factories = generateFactories(ctx.schemas, factoryOptions);
2189
+ return factories.map((factory) => ({
2190
+ path: getFactoryPath(factory),
2191
+ content: factory.content,
2192
+ type: "factory",
2193
+ // Skip writing factories if they already exist (allow customization)
2194
+ skipIfExists: !factory.overwrite,
2195
+ metadata: {
2196
+ factoryName: factory.name,
2197
+ schemaName: factory.schemaName
2198
+ }
2199
+ }));
2200
+ }
2201
+ };
2202
+ const generators = [migrationGenerator];
2203
+ if (resolved.generateModels) {
2204
+ generators.push(modelGenerator);
2205
+ }
2206
+ if (resolved.generateFactories) {
2207
+ generators.push(factoryGenerator);
2208
+ }
2209
+ return {
2210
+ name: "@famgia/omnify-laravel",
2211
+ version: "0.0.14",
2212
+ configSchema: LARAVEL_CONFIG_SCHEMA,
2213
+ generators
2214
+ };
2215
+ }
2216
+
2217
+ export {
2218
+ toColumnName,
2219
+ toTableName,
2220
+ propertyToColumnMethod,
2221
+ generatePrimaryKeyColumn,
2222
+ generateTimestampColumns,
2223
+ generateSoftDeleteColumn,
2224
+ generateForeignKey,
2225
+ schemaToBlueprint,
2226
+ formatColumnMethod,
2227
+ formatForeignKey,
2228
+ formatIndex,
2229
+ generateMigrations,
2230
+ generateMigrationFromSchema,
2231
+ generateDropMigrationForTable,
2232
+ formatMigrationFile,
2233
+ getMigrationPath,
2234
+ generateAlterMigration,
2235
+ generateDropTableMigration,
2236
+ generateMigrationsFromChanges,
2237
+ laravelPlugin
2238
+ };
2239
+ //# sourceMappingURL=chunk-REDFZUQY.js.map