prisma-laravel-migrate 0.0.4 → 0.0.6
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/README.md +329 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/models.index.js +2 -1
- package/dist/generator/migrator/index.js +17 -3
- package/dist/generator/migrator/sort.js +75 -0
- package/dist/generator/modeler/index.js +18 -2
- package/dist/generator/utils.js +28 -73
- package/dist/index.js +220 -0
- package/dist/printer/migrations.js +47 -19
- package/dist/printer/models.js +64 -32
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -3
- package/schema.prisma +1630 -7
- package/stubs/model.stub +3 -73
- package/dist/migrations.js +0 -3
- package/dist/models.js +0 -3
- package/stubs/simple-model.stub +0 -14
package/README.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Prisma Laravel Migrate
|
|
2
|
+
|
|
3
|
+
A generator toolkit that translates your **Prisma schema** into Laravel-compatible
|
|
4
|
+
**Database Migrations**, **Eloquent Models**, and **Enum classes**.
|
|
5
|
+
Everything is written in strict TypeScript and fully customizable through stubs, grouping, and marker-based injection.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📦 Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install prisma-laravel-migrate --save-dev
|
|
13
|
+
|
|
14
|
+
(Requires prisma in the same project.)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
🛠️ Prisma Generator Setup
|
|
20
|
+
|
|
21
|
+
Insert two generator blocks in schema.prisma:
|
|
22
|
+
|
|
23
|
+
generator migrate {
|
|
24
|
+
provider = "prisma-laravel-migrate"
|
|
25
|
+
stubDir = "./prisma/stubs"
|
|
26
|
+
output = "database/migrations" // fallback
|
|
27
|
+
outputDir = "database/migrations" // takes precedence
|
|
28
|
+
startMarker = "// <prisma-laravel:start>"
|
|
29
|
+
endMarker = "// <prisma-laravel:end>"
|
|
30
|
+
noEmit = false // set true to skip writing files
|
|
31
|
+
groups = "./prisma/group-stubs.js" // optional group mapping
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
generator modeler {
|
|
35
|
+
provider = "prisma-laravel-models"
|
|
36
|
+
stubDir = "./prisma/stubs"
|
|
37
|
+
output = "app/Models"
|
|
38
|
+
outputDir = "app/Models" // takes precedence
|
|
39
|
+
outputEnumDir = "app/Enums" // enums folder (optional)
|
|
40
|
+
startMarker = "// <prisma-laravel:start>"
|
|
41
|
+
endMarker = "// <prisma-laravel:end>"
|
|
42
|
+
noEmit = false
|
|
43
|
+
groups = "./prisma/group-stubs.js"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Field Reference
|
|
47
|
+
|
|
48
|
+
Key Notes
|
|
49
|
+
|
|
50
|
+
output / outputDir Destination folder; outputDir overrides output.
|
|
51
|
+
outputEnumDir (modeler) folder for PHP enum classes.
|
|
52
|
+
stubDir Root stubs folder (migration/, model/, enum/).
|
|
53
|
+
startMarker / endMarker Region markers the generator will update.
|
|
54
|
+
groups Path to JS module exporting stub-group mappings.
|
|
55
|
+
noEmit If true, generator parses but writes no files.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
📁 Stub Folder Layout
|
|
62
|
+
|
|
63
|
+
Running
|
|
64
|
+
|
|
65
|
+
npx prisma-laravel-cli init --schema=prisma/schema.prisma
|
|
66
|
+
|
|
67
|
+
creates:
|
|
68
|
+
|
|
69
|
+
prisma/stubs/
|
|
70
|
+
├── migration/index.stub
|
|
71
|
+
├── model/index.stub
|
|
72
|
+
├── model/simple-model.stub
|
|
73
|
+
└── enum/index.stub
|
|
74
|
+
|
|
75
|
+
Copy log (example):
|
|
76
|
+
|
|
77
|
+
➡️ Copied migration.stub → stubs/migration/index.stub
|
|
78
|
+
➡️ Copied model.stub → stubs/model/index.stub
|
|
79
|
+
➡️ Copied enums.stub → stubs/enum/index.stub
|
|
80
|
+
➡️ Copied simple-model.stub → stubs/model/simple-model.stub
|
|
81
|
+
|
|
82
|
+
Override a single table / enum by adding
|
|
83
|
+
stubs/<type>/<name>.stub (e.g. stubs/model/users.stub).
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
🔧 CLI Commands
|
|
89
|
+
|
|
90
|
+
Command Purpose
|
|
91
|
+
|
|
92
|
+
init Injects generator blocks & scaffold stub folders.
|
|
93
|
+
customize Create per-table stub overrides.
|
|
94
|
+
gen Run prisma generate and then Laravel generators.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
init
|
|
98
|
+
|
|
99
|
+
npx prisma-laravel-cli init --schema=prisma/schema.prisma
|
|
100
|
+
|
|
101
|
+
customize
|
|
102
|
+
|
|
103
|
+
# create migration+model overrides for users & accounts
|
|
104
|
+
npx prisma-laravel-cli customize -t migration,model -n users,accounts
|
|
105
|
+
|
|
106
|
+
# create enum overrides
|
|
107
|
+
npx prisma-laravel-cli customize -t enum -n UserStatus,RoleType
|
|
108
|
+
|
|
109
|
+
migration & model may be combined; enum must be separate.
|
|
110
|
+
|
|
111
|
+
gen
|
|
112
|
+
|
|
113
|
+
# run prisma generate then Laravel generation
|
|
114
|
+
npx prisma-laravel-cli gen --config=prisma/laravel.config.js
|
|
115
|
+
|
|
116
|
+
# skip prisma generate step
|
|
117
|
+
npx prisma-laravel-cli gen --config=prisma/laravel.config.js --skipGenerate
|
|
118
|
+
|
|
119
|
+
prisma/laravel.config.js example:
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
migrator: {
|
|
123
|
+
outputDir: 'database/migrations',
|
|
124
|
+
stubDir: 'prisma/stubs',
|
|
125
|
+
groups: './prisma/group-stubs.js',
|
|
126
|
+
},
|
|
127
|
+
modeler: {
|
|
128
|
+
outputDir: 'app/Models',
|
|
129
|
+
outputEnumDir: 'app/Enums',
|
|
130
|
+
stubDir: 'prisma/stubs',
|
|
131
|
+
groups: './prisma/group-stubs.js',
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
🧩 Grouping Stubs
|
|
139
|
+
|
|
140
|
+
prisma/group-stubs.js
|
|
141
|
+
|
|
142
|
+
module.exports = [
|
|
143
|
+
{ stubFile: 'auth.stub', tables: ['users','accounts','password_resets'] },
|
|
144
|
+
{ stubFile: 'billing.stub', tables: ['invoices','transactions'] },
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
Resolution order
|
|
148
|
+
|
|
149
|
+
1. stubs/<type>/<table>.stub
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
2. Matching group stub (stubFile)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
3. stubs/<type>/index.stub
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
✨ Stub Customization Notes
|
|
163
|
+
|
|
164
|
+
Stubs are JavaScript template literals. Escape \` and \${ } if you need them literally.
|
|
165
|
+
|
|
166
|
+
Keep the ${content} placeholder inside the marker block if you want the generator to keep injecting its dynamic chunk.
|
|
167
|
+
|
|
168
|
+
> Full Custom Model Stubs
|
|
169
|
+
If you plan to hand-craft a model stub completely—removing both the ${content} placeholder and the // <prisma-laravel:start> / // <prisma-laravel:end> markers—set
|
|
170
|
+
noEmit = true for the modeler generator (or exclude that table via custom rules).
|
|
171
|
+
Otherwise, the generator has nowhere to inject its code and will skip the file.
|
|
172
|
+
Leaving the markers + ${content} lets you customise everything around the generated region while the tool continues to maintain fillable lists, casts, and relations automatically.
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
📑 Default Stub Templates
|
|
180
|
+
|
|
181
|
+
Enum
|
|
182
|
+
|
|
183
|
+
<?php
|
|
184
|
+
|
|
185
|
+
namespace App\\Enums;
|
|
186
|
+
|
|
187
|
+
enum ${enumDef.name}: string
|
|
188
|
+
{
|
|
189
|
+
// <prisma-laravel:start>
|
|
190
|
+
${enumDef.values.map(v => ` case ${v} = '${v}';`).join('\\n')}
|
|
191
|
+
// <prisma-laravel:end>
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
Migration
|
|
195
|
+
|
|
196
|
+
<?php
|
|
197
|
+
|
|
198
|
+
use Illuminate\\Database\\Migrations\\Migration;
|
|
199
|
+
use Illuminate\\Database\\Schema\\Blueprint;
|
|
200
|
+
use Illuminate\\Support\\Facades\\Schema;
|
|
201
|
+
|
|
202
|
+
return new class extends Migration
|
|
203
|
+
{
|
|
204
|
+
public function up(): void
|
|
205
|
+
{
|
|
206
|
+
Schema::create('${tableName}', function (Blueprint $table) {
|
|
207
|
+
// <prisma-laravel:start>
|
|
208
|
+
${columns}
|
|
209
|
+
// <prisma-laravel:end>
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public function down(): void
|
|
214
|
+
{
|
|
215
|
+
Schema::dropIfExists('${tableName}');
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
🏗️ Complex Model Stub Example
|
|
223
|
+
|
|
224
|
+
<?php
|
|
225
|
+
|
|
226
|
+
namespace App\\Models;
|
|
227
|
+
|
|
228
|
+
${model.imports}
|
|
229
|
+
use Illuminate\\Database\\Eloquent\\Model;
|
|
230
|
+
use Illuminate\\Database\\Eloquent\\Relations\\{ BelongsTo, HasMany, BelongsToMany };
|
|
231
|
+
|
|
232
|
+
class ${model.className} extends Model
|
|
233
|
+
{
|
|
234
|
+
protected $table = '${model.tableName}';
|
|
235
|
+
|
|
236
|
+
/* ---------- Mass Assignment ---------- */
|
|
237
|
+
protected $fillable = [
|
|
238
|
+
${model.properties.filter(p => p.fillable).map(p => ` '${p.name}',`).join('\\n')}
|
|
239
|
+
];
|
|
240
|
+
protected $guarded = [
|
|
241
|
+
${(model.guarded ?? []).map(n => ` '${n}',`).join('\\n')}
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
/* ---------- Hidden / Casts ---------- */
|
|
245
|
+
protected $hidden = [
|
|
246
|
+
${model.properties.filter(p => p.hidden).map(p => ` '${p.name}',`).join('\\n')}
|
|
247
|
+
];
|
|
248
|
+
protected $casts = [
|
|
249
|
+
${model.properties.filter(p => p.cast).map(p => ` '${p.name}' => '${p.cast}',`).join('\\n')}
|
|
250
|
+
${model.properties.filter(p => p.enumRef).map(p => ` '${p.name}' => ${p.enumRef}::class,`).join('\\n')}
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
/* ---------- Eager Loading ---------- */
|
|
254
|
+
protected $with = [
|
|
255
|
+
${(model.with ?? []).map(r => ` '${r}',`).join('\\n')}
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
/* ---------- Interfaces ---------- */
|
|
259
|
+
public array $interfaces = [
|
|
260
|
+
${Object.entries(model.interfaces).map(([k,i]) => ` '${k}' => {${i.import ? ` import: '${i.import}',` : ''} type: '${i.type}' },`).join('\\n')}
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
/* ---------- Relationships ---------- */
|
|
264
|
+
${model.relations.map(r => {
|
|
265
|
+
const args = [r.modelClass, r.foreignKey ? `'${r.foreignKey}'` : '', r.localKey ? `'${r.localKey}'` : ''].filter(Boolean).join(', ');
|
|
266
|
+
return ` public function ${r.name}(): ${r.type.charAt(0).toUpperCase() + r.type.slice(1)}\\n {\\n return $this->${r.type}(${args});\\n }`;
|
|
267
|
+
}).join('\\n\\n')}
|
|
268
|
+
|
|
269
|
+
// <prisma-laravel:start>
|
|
270
|
+
${content}
|
|
271
|
+
// <prisma-laravel:end>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
📚 Interfaces Metadata Example
|
|
278
|
+
|
|
279
|
+
Useful for fumeapp/modeltyper integration:
|
|
280
|
+
|
|
281
|
+
public array $interfaces = [
|
|
282
|
+
'props' => [
|
|
283
|
+
'import' => \"@typings/service-forms\",
|
|
284
|
+
'type' => 'ServiceProps',
|
|
285
|
+
],
|
|
286
|
+
'services' => [
|
|
287
|
+
'type' => 'Array<SMMService>',
|
|
288
|
+
],
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
Type definition:
|
|
292
|
+
Record<string, { import?: string; type: string }>.
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
🚀 Enum Casting
|
|
298
|
+
|
|
299
|
+
The generator automatically casts Prisma enums:
|
|
300
|
+
|
|
301
|
+
protected $casts = [
|
|
302
|
+
'status' => StatusEnum::class,
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
Enums are saved to outputEnumDir if configured.
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
💡 Tips
|
|
311
|
+
|
|
312
|
+
Combine migration & model in the same customize command when table names align.
|
|
313
|
+
|
|
314
|
+
Escape template characters in stub files.
|
|
315
|
+
|
|
316
|
+
Use noEmit: true for dry-runs or CI validation.
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
📜 License
|
|
323
|
+
|
|
324
|
+
MIT — Happy scaffolding! 🎉
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
Copy that entire block into **README.md** and you’ll have the corrected, full documentation—including the note about fully custom model stubs.
|
|
329
|
+
|
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { generatorHandler } from "@prisma/generator-helper";
|
|
3
2
|
import { generateLaravelSchema } from "../generator/migrator/index.js";
|
|
3
|
+
import helperPkg from '@prisma/generator-helper';
|
|
4
|
+
const { generatorHandler } = helperPkg;
|
|
4
5
|
generatorHandler({
|
|
5
6
|
onGenerate: generateLaravelSchema,
|
|
6
7
|
onManifest: () => ({
|
package/dist/cli/models.index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { generatorHandler } from "@prisma/generator-helper";
|
|
3
2
|
import { generateLaravelModels } from "../generator/modeler/index.js";
|
|
3
|
+
import helperPkg from '@prisma/generator-helper';
|
|
4
|
+
const { generatorHandler } = helperPkg;
|
|
4
5
|
generatorHandler({
|
|
5
6
|
onGenerate: generateLaravelModels,
|
|
6
7
|
onManifest: () => ({
|
|
@@ -2,13 +2,25 @@ import { existsSync, mkdirSync, readdirSync } from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { PrismaToLaravelMigrationGenerator } from "./PrismaToLaravelMigrationGenerator.js";
|
|
4
4
|
import { StubMigrationPrinter } from "../../printer/migrations.js";
|
|
5
|
-
import {
|
|
5
|
+
import { writeWithMarkers } from "../../generator/utils.js";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
+
import { sortMigrations } from "./sort.js";
|
|
7
8
|
export async function generateLaravelSchema(options) {
|
|
8
9
|
const { dmmf, generator } = options;
|
|
9
10
|
// 0) Pull config values (all come in as strings)
|
|
10
11
|
// Inside generateLaravelSchema()
|
|
11
12
|
const raw = (generator.config ?? {});
|
|
13
|
+
// 0.a) Load groups from a JS file if provided
|
|
14
|
+
let groups = [];
|
|
15
|
+
if (raw["groups"]) {
|
|
16
|
+
const groupsModulePath = path.resolve(process.cwd(), raw["groups"]);
|
|
17
|
+
const imported = await import(groupsModulePath);
|
|
18
|
+
const exported = imported.default ?? imported;
|
|
19
|
+
if (!Array.isArray(exported)) {
|
|
20
|
+
throw new Error(`Custom groups module must export an array, but got ${typeof exported}`);
|
|
21
|
+
}
|
|
22
|
+
groups = exported;
|
|
23
|
+
}
|
|
12
24
|
const cfg = {
|
|
13
25
|
stubPath: raw["stubPath"],
|
|
14
26
|
overwriteExisting: raw["overwriteExisting"] === "true",
|
|
@@ -16,6 +28,8 @@ export async function generateLaravelSchema(options) {
|
|
|
16
28
|
outputDir: raw["outputDir"],
|
|
17
29
|
startMarker: raw["startMarker"] ?? "// <prisma-laravel:start>",
|
|
18
30
|
endMarker: raw["endMarker"] ?? "// <prisma-laravel:end>",
|
|
31
|
+
stubDir: raw["stubDir"],
|
|
32
|
+
groups,
|
|
19
33
|
};
|
|
20
34
|
// 1) Determine and ensure output directory exists
|
|
21
35
|
const baseOut = cfg.outputDir
|
|
@@ -46,10 +60,10 @@ export async function generateLaravelSchema(options) {
|
|
|
46
60
|
const __filename = fileURLToPath(import.meta.url);
|
|
47
61
|
const __dirname = path.dirname(__filename);
|
|
48
62
|
// 4) Prepare the stub printer
|
|
49
|
-
const
|
|
63
|
+
const fallbackStubFile = cfg.stubPath
|
|
50
64
|
? path.resolve(process.cwd(), cfg.stubPath)
|
|
51
65
|
: path.resolve(__dirname, "../../../stubs/migration.stub");
|
|
52
|
-
|
|
66
|
+
let printer = new StubMigrationPrinter(cfg, fallbackStubFile);
|
|
53
67
|
// 5) Write each migration file
|
|
54
68
|
migrations.forEach((mig, idx) => {
|
|
55
69
|
const timestamp = formatLaravelTimestamp(new Date(), idx + 1);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reorders migrations so that any table with foreign‐key dependencies
|
|
3
|
+
* is always migrated *after* the tables it references.
|
|
4
|
+
*
|
|
5
|
+
* @param migrations Array of Migration objects (with tableName & definitions[])
|
|
6
|
+
* @returns New array sorted in dependency order
|
|
7
|
+
* @throws If there’s a cycle in the relationships
|
|
8
|
+
*/
|
|
9
|
+
export function sortMigrations(migrations) {
|
|
10
|
+
// 1) Build a map: tableName → Migration
|
|
11
|
+
const migMap = new Map(migrations.map(m => [m.tableName, m]));
|
|
12
|
+
// 2) Collect “true” FKs only (skip back‐relation object fields)
|
|
13
|
+
const rawDeps = new Map();
|
|
14
|
+
for (const { tableName } of migrations) {
|
|
15
|
+
rawDeps.set(tableName, new Set());
|
|
16
|
+
}
|
|
17
|
+
for (const m of migrations) {
|
|
18
|
+
for (const def of m.definitions) {
|
|
19
|
+
// only consider a relationship if:
|
|
20
|
+
// - it exists (def.relationship)
|
|
21
|
+
// - this field is a scalar FK column (def.kind === 'scalar')
|
|
22
|
+
// - it's the owning side (relationFromFields non-empty)
|
|
23
|
+
if (!def.relationship ||
|
|
24
|
+
!def.relationFromFields ||
|
|
25
|
+
def.relationFromFields.length === 0) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const parent = def.relationship.on;
|
|
29
|
+
if (!migMap.has(parent) || m.tableName == parent)
|
|
30
|
+
continue; // skip external tables
|
|
31
|
+
rawDeps.get(m.tableName).add(parent);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// 3) Build adjacency (parent → dependents) and in-degree (table → count)
|
|
35
|
+
const adj = new Map();
|
|
36
|
+
const inDegree = new Map();
|
|
37
|
+
for (const tbl of migMap.keys()) {
|
|
38
|
+
adj.set(tbl, []);
|
|
39
|
+
inDegree.set(tbl, 0);
|
|
40
|
+
}
|
|
41
|
+
for (const [child, parents] of rawDeps) {
|
|
42
|
+
for (const parent of parents) {
|
|
43
|
+
if (parent !== child)
|
|
44
|
+
adj.get(parent).push(child);
|
|
45
|
+
inDegree.set(child, inDegree.get(child) + 1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 4) Kahn’s algorithm: enqueue all zero in-degree tables
|
|
49
|
+
const queue = [];
|
|
50
|
+
for (const [tbl, deg] of inDegree) {
|
|
51
|
+
if (deg === 0)
|
|
52
|
+
queue.push(tbl);
|
|
53
|
+
}
|
|
54
|
+
const sorted = [];
|
|
55
|
+
while (queue.length) {
|
|
56
|
+
const tbl = queue.shift();
|
|
57
|
+
sorted.push(migMap.get(tbl));
|
|
58
|
+
for (const child of adj.get(tbl)) {
|
|
59
|
+
const nd = inDegree.get(child) - 1;
|
|
60
|
+
inDegree.set(child, nd);
|
|
61
|
+
if (nd === 0)
|
|
62
|
+
queue.push(child);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// 5) If not all processed, there is a genuine cycle
|
|
66
|
+
if (sorted.length !== migrations.length) {
|
|
67
|
+
const cycle = migrations
|
|
68
|
+
.map(m => m.tableName)
|
|
69
|
+
.filter(t => !sorted.some(s => s.tableName === t));
|
|
70
|
+
throw new Error(`Cycle detected in migration dependencies: ${cycle.join(' → ')}`);
|
|
71
|
+
}
|
|
72
|
+
console.log('\n📦 Sorted Migration Tables:\n' + sorted.map((item, i) => ` ${i + 1}. ${item.tableName}`).join('\n') + '\n');
|
|
73
|
+
return sorted;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=sort.js.map
|
|
@@ -9,13 +9,26 @@ export async function generateLaravelModels(options) {
|
|
|
9
9
|
// 0) Pull config values
|
|
10
10
|
// Inside generateLaravelModels()
|
|
11
11
|
const raw = (generator.config ?? {});
|
|
12
|
+
let groups = [];
|
|
13
|
+
if (raw["groups"]) {
|
|
14
|
+
const groupsModulePath = path.resolve(process.cwd(), raw["groups"]);
|
|
15
|
+
const imported = await import(groupsModulePath);
|
|
16
|
+
const exported = imported.default ?? imported;
|
|
17
|
+
if (!Array.isArray(exported)) {
|
|
18
|
+
throw new Error(`Custom groups module must export an array, but got ${typeof exported}`);
|
|
19
|
+
}
|
|
20
|
+
groups = exported;
|
|
21
|
+
}
|
|
12
22
|
const cfg = {
|
|
13
23
|
modelStubPath: raw["modelStubPath"],
|
|
14
24
|
enumStubPath: raw["enumStubPath"],
|
|
15
25
|
overwriteExisting: raw["overwriteExisting"] === "true",
|
|
16
26
|
outputDir: raw["outputDir"],
|
|
27
|
+
outputEnumDir: raw["outputEnumDir"],
|
|
17
28
|
startMarker: raw["startMarker"] ?? "// <prisma-laravel:start>",
|
|
18
29
|
endMarker: raw["endMarker"] ?? "// <prisma-laravel:end>",
|
|
30
|
+
stubDir: raw["stubDir"],
|
|
31
|
+
groups,
|
|
19
32
|
};
|
|
20
33
|
// 1) Determine and ensure output directories
|
|
21
34
|
const modelsDir = cfg.outputDir
|
|
@@ -24,7 +37,10 @@ export async function generateLaravelModels(options) {
|
|
|
24
37
|
if (!existsSync(modelsDir)) {
|
|
25
38
|
mkdirSync(modelsDir, { recursive: true });
|
|
26
39
|
}
|
|
27
|
-
const enumsDir =
|
|
40
|
+
const enumsDir = cfg.outputEnumDir
|
|
41
|
+
? path.resolve(process.cwd(), cfg.outputEnumDir)
|
|
42
|
+
: path.resolve(process.cwd(), 'app/Enums');
|
|
43
|
+
console.log(enumsDir, cfg, process.cwd());
|
|
28
44
|
if (!existsSync(enumsDir)) {
|
|
29
45
|
mkdirSync(enumsDir, { recursive: true });
|
|
30
46
|
}
|
|
@@ -39,7 +55,7 @@ export async function generateLaravelModels(options) {
|
|
|
39
55
|
const enumStub = cfg.enumStubPath
|
|
40
56
|
? path.resolve(process.cwd(), cfg.enumStubPath)
|
|
41
57
|
: path.resolve(__dirname, "../../../stubs/enums.stub");
|
|
42
|
-
const printer = new StubModelPrinter(modelStub, enumStub);
|
|
58
|
+
const printer = new StubModelPrinter(cfg, modelStub, enumStub);
|
|
43
59
|
// 3) Generate definitions
|
|
44
60
|
const schemaGen = new PrismaToLaravelModelGenerator(dmmf);
|
|
45
61
|
const { models, enums } = schemaGen.generateAll();
|
package/dist/generator/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NativeToMigrationTypeMap } from "./migrator/column-maps.js";
|
|
2
2
|
import { MigrationTypes } from "./migrator/migrationTypes.js";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import path from "path";
|
|
4
5
|
/**
|
|
5
6
|
* Given a Prisma field default, return the PHP code fragment
|
|
6
7
|
* to append to your migration column definition.
|
|
@@ -77,79 +78,6 @@ export function getType(field) {
|
|
|
77
78
|
// @ts-ignore
|
|
78
79
|
return NativeToMigrationTypeMap[key] ?? MigrationTypes.string;
|
|
79
80
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Reorders migrations so that any table with foreign‐key dependencies
|
|
82
|
-
* is always migrated *after* the tables it references.
|
|
83
|
-
*
|
|
84
|
-
* @param migrations Array of Migration objects (with tableName & definitions[])
|
|
85
|
-
* @returns New array sorted in dependency order
|
|
86
|
-
* @throws If there’s a cycle in the relationships
|
|
87
|
-
*/
|
|
88
|
-
export function sortMigrations(migrations) {
|
|
89
|
-
// 1) Build a map: tableName → Migration
|
|
90
|
-
const migMap = new Map(migrations.map(m => [m.tableName, m]));
|
|
91
|
-
// 2) Collect “true” FKs only (skip back‐relation object fields)
|
|
92
|
-
const rawDeps = new Map();
|
|
93
|
-
for (const { tableName } of migrations) {
|
|
94
|
-
rawDeps.set(tableName, new Set());
|
|
95
|
-
}
|
|
96
|
-
for (const m of migrations) {
|
|
97
|
-
for (const def of m.definitions) {
|
|
98
|
-
// only consider a relationship if:
|
|
99
|
-
// - it exists (def.relationship)
|
|
100
|
-
// - this field is a scalar FK column (def.kind === 'scalar')
|
|
101
|
-
// - it's the owning side (relationFromFields non-empty)
|
|
102
|
-
if (!def.relationship ||
|
|
103
|
-
!def.relationFromFields ||
|
|
104
|
-
def.relationFromFields.length === 0) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const parent = def.relationship.on;
|
|
108
|
-
if (!migMap.has(parent))
|
|
109
|
-
continue; // skip external tables
|
|
110
|
-
rawDeps.get(m.tableName).add(parent);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// 3) Build adjacency (parent → dependents) and in-degree (table → count)
|
|
114
|
-
const adj = new Map();
|
|
115
|
-
const inDegree = new Map();
|
|
116
|
-
for (const tbl of migMap.keys()) {
|
|
117
|
-
adj.set(tbl, []);
|
|
118
|
-
inDegree.set(tbl, 0);
|
|
119
|
-
}
|
|
120
|
-
for (const [child, parents] of rawDeps) {
|
|
121
|
-
for (const parent of parents) {
|
|
122
|
-
adj.get(parent).push(child);
|
|
123
|
-
inDegree.set(child, inDegree.get(child) + 1);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// 4) Kahn’s algorithm: enqueue all zero in-degree tables
|
|
127
|
-
const queue = [];
|
|
128
|
-
for (const [tbl, deg] of inDegree) {
|
|
129
|
-
if (deg === 0)
|
|
130
|
-
queue.push(tbl);
|
|
131
|
-
}
|
|
132
|
-
const sorted = [];
|
|
133
|
-
while (queue.length) {
|
|
134
|
-
const tbl = queue.shift();
|
|
135
|
-
sorted.push(migMap.get(tbl));
|
|
136
|
-
for (const child of adj.get(tbl)) {
|
|
137
|
-
const nd = inDegree.get(child) - 1;
|
|
138
|
-
inDegree.set(child, nd);
|
|
139
|
-
if (nd === 0)
|
|
140
|
-
queue.push(child);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// 5) If not all processed, there is a genuine cycle
|
|
144
|
-
if (sorted.length !== migrations.length) {
|
|
145
|
-
const cycle = migrations
|
|
146
|
-
.map(m => m.tableName)
|
|
147
|
-
.filter(t => !sorted.some(s => s.tableName === t));
|
|
148
|
-
throw new Error(`Cycle detected in migration dependencies: ${cycle.join(' → ')}`);
|
|
149
|
-
}
|
|
150
|
-
console.log(sorted.map(item => item.tableName));
|
|
151
|
-
return sorted;
|
|
152
|
-
}
|
|
153
81
|
export function buildModelContent(model) {
|
|
154
82
|
const lines = [];
|
|
155
83
|
// 1) If @guarded is used, emit $guarded instead of $fillable
|
|
@@ -236,4 +164,31 @@ export function writeWithMarkers(filePath, fullContent, generated, startMarker,
|
|
|
236
164
|
// Otherwise write the full content (which itself can include the markers)
|
|
237
165
|
writeFileSync(filePath, fullContent, 'utf-8');
|
|
238
166
|
}
|
|
167
|
+
export function resolveStub(cfg, type, tableName) {
|
|
168
|
+
if (!cfg.stubDir)
|
|
169
|
+
return;
|
|
170
|
+
//---
|
|
171
|
+
const dir = path.resolve(process.cwd(), cfg.stubDir, type);
|
|
172
|
+
// A) 1st: file‐based override: <tableName>.stub
|
|
173
|
+
const fileOverride = path.join(dir, `${tableName}.stub`);
|
|
174
|
+
if (existsSync(fileOverride)) {
|
|
175
|
+
return fileOverride;
|
|
176
|
+
}
|
|
177
|
+
// B) 2nd: group‐based override
|
|
178
|
+
if (cfg.groups) {
|
|
179
|
+
for (const { stubFile, tables } of cfg.groups) {
|
|
180
|
+
if (tables.includes(tableName)) {
|
|
181
|
+
const groupPath = path.join(dir, stubFile);
|
|
182
|
+
if (existsSync(groupPath)) {
|
|
183
|
+
return groupPath;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// C) Fallback to index.stub
|
|
189
|
+
const defaultPath = path.join(dir, "index.stub");
|
|
190
|
+
if (!existsSync(defaultPath))
|
|
191
|
+
return;
|
|
192
|
+
return defaultPath;
|
|
193
|
+
}
|
|
239
194
|
//# sourceMappingURL=utils.js.map
|