prisma-laravel-migrate 0.0.7 → 0.0.9

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 CHANGED
@@ -1,299 +1,482 @@
1
- # Prisma Laravel Migrate
2
-
3
- A generator plugin that translates your **Prisma schema** into Laravel‑ready
4
- **Database Migrations**, **Eloquent Models**, and **Enum classes**.
5
- Built in strict TypeScript with fully‑customisable stubs, grouping, and marker‑based updates.
6
-
7
- ---
8
-
9
- ## 📦 Installation
10
-
11
- ```bash
12
- npm install prisma-laravel-migrate --save-dev
13
- # requires the Prisma CLI in your project
14
- ```
15
-
16
- ---
17
-
18
- ## 🛠️ Prisma Generator Setup
19
-
20
- Add both generator blocks to **`schema.prisma`**:
21
-
22
- ```prisma
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
31
- groups = "./prisma/group-stubs.js"
32
- }
33
-
34
- generator modeler {
35
- provider = "prisma-laravel-models"
36
- stubDir = "./prisma/stubs"
37
- output = "app/Models"
38
- outputDir = "app/Models" // overrides output
39
- outputEnumDir = "app/Enums"
40
- startMarker = "// <prisma-laravel:start>"
41
- endMarker = "// <prisma-laravel:end>"
42
- noEmit = false
43
- groups = "./prisma/group-stubs.js"
44
- }
45
- ```
46
-
47
- ### Field Reference
48
-
49
- | Key | Notes |
50
- | --- | --- |
51
- | `outputDir / output` | Destination folder (`outputDir` takes precedence). |
52
- | `outputEnumDir` | (modeler) directory for PHP enum classes. |
53
- | `stubDir` | Root stubs folder (`migration/`, `model/`, `enum/`). |
54
- | `startMarker/endMarker` | Region markers the generator will update. |
55
- | `groups` | JS module exporting stubgroup mappings. |
56
- | `noEmit` | If `true`, generator parses but **writes no** files. |
57
-
58
- ---
59
-
60
- ## 📁 Stub Folder Layout
61
-
62
- ```
63
- prisma/stubs/
64
- ├── migration/index.stub
65
- ├── model/index.stub
66
- ├── model/simple-model.stub
67
- └── enum/index.stub
68
- ```
69
-
70
- Create a table‑specific override with
71
- `stubs/<type>/<table>.stub` (e.g. `stubs/model/users.stub`).
72
-
73
- ---
74
-
75
- ## 🔧 CLI Commands
76
-
77
- | Command | What it does |
78
- | --- | --- |
79
- | `init` | Inject generator blocks & scaffold stub folders |
80
- | `customize` | Create per‑table stub overrides |
81
- | `gen` | Run `prisma generate` then Laravel generators |
82
-
83
- ### init
84
-
85
- ```bash
86
- npx prisma-laravel-cli init --schema=prisma/schema.prisma
87
- ```
88
-
89
- ### customize
90
-
91
- Generate override stubs.
92
-
93
- ```bash
94
- npx prisma-laravel-cli customize \
95
- -t <types> \
96
- -n <names> \
97
- [--force] \
98
- [--config <path>]
99
- ```
100
-
101
- | Flag | Description |
102
- | --- | --- |
103
- | `-t, --type` | **Required.** Comma‑separated list of stub types.<br>Valid values: `migration`, `model`, `enum`. You may combine `migration,model`; `enum` must be used alone. |
104
- | `-n, --names` | **Required.** Comma‑separated table or enum names (e.g. `users,accounts`). |
105
- | `--force` | Overwrite existing stub files. |
106
- | `--config` | Path to an alternate CLI config file (custom stubDir/groups). |
107
-
108
- **Behaviour**
109
-
110
- 1. Checks `<stubDir>/<type>/<name>.stub`.
111
- 2. If missing, copies from `index.stub` that path and logs:
112
- `➡️ Created stubs/<type>/<name>.stub from index.stub`
113
- 3. If present and `--force` **not** set:
114
- `⏭️ Skipped existing stubs/<type>/<name>.stub`
115
- 4. With `--force`, overwrites and logs creation.
116
-
117
- **Example**
118
-
119
- ```bash
120
- # migration + model overrides for two tables
121
- npx prisma-laravel-cli customize -t migration,model -n users,accounts
122
- ```
123
-
124
- ### gen
125
-
126
- ```bash
127
- # run prisma generate then Laravel generators
128
- npx prisma-laravel-cli gen --config=prisma/laravel.config.js
129
-
130
- # skip prisma generate step
131
- npx prisma-laravel-cli gen --config=prisma/laravel.config.js --skipGenerate
132
- ```
133
-
134
- `prisma/laravel.config.js` example:
135
-
136
- ```js
137
- module.exports = {
138
- migrator: {
139
- outputDir: "database/migrations",
140
- stubDir: "prisma/stubs",
141
- groups: "./prisma/group-stubs.js",
142
- },
143
- modeler: {
144
- outputDir: "app/Models",
145
- outputEnumDir: "app/Enums",
146
- stubDir: "prisma/stubs",
147
- groups: "./prisma/group-stubs.js",
148
- },
149
- };
150
- ```
151
-
152
- ---
153
-
154
- ## ✨ Stub Customization Notes
155
-
156
- Stubs are **JS template literals**. Escape \\` and \\${ } if you want them literally.
157
-
158
- > **Full custom model stubs**
159
- > If you plan to hand-craft **all** the internal sections yourself
160
- > (fillable, hidden, casts, etc.), remove the `${content}` placeholder
161
- > but **keep** the `// <prisma-laravel:start>` and
162
- > `// <prisma-laravel:end>` markers so the generator still knows where
163
- > to inject future updates.
164
- > Remove the markers only if you never want the file touched again
165
-
166
- ---
167
-
168
- ## 📑 Default Stub Templates
169
-
170
- <details>
171
- <summary>Enum <code>index.stub</code></summary>
172
-
173
- ```php
174
- <?php
175
-
176
- namespace App\\Enums;
177
-
178
- enum ${enumDef.name}: string
179
- {
180
- // <prisma-laravel:start>
181
- ${enumDef.values.map(v => ` case ${v} = '${v}';`).join('\\n')}
182
- // <prisma-laravel:end>
183
- }
184
- ```
185
-
186
- </details>
187
-
188
- <details>
189
- <summary>Migration <code>index.stub</code></summary>
190
-
191
- ```php
192
- <?php
193
-
194
- use Illuminate\\Database\\Migrations\\Migration;
195
- use Illuminate\\Database\\Schema\\Blueprint;
196
- use Illuminate\\Support\\Facades\\Schema;
197
-
198
- return new class extends Migration
199
- {
200
- public function up(): void
201
- {
202
- Schema::create('${tableName}', function (Blueprint $table) {
203
- // <prisma-laravel:start>
204
- ${columns}
205
- // <prisma-laravel:end>
206
- });
207
- }
208
-
209
- public function down(): void
210
- {
211
- Schema::dropIfExists('${tableName}');
212
- }
213
- };
214
- ```
215
-
216
- </details>
217
-
218
- ---
219
-
220
- ## 🏗️ Complex Model Stub Example
221
-
222
- <details>
223
- <summary>Expand stub</summary>
224
-
225
- ```php
226
- <?php
227
-
228
- namespace App\\Models;
229
-
230
- ${model.imports}
231
- use Illuminate\\Database\\Eloquent\\Model;
232
- use Illuminate\\Database\\Eloquent\\Relations\\{ BelongsTo, HasMany, BelongsToMany };
233
-
234
- class ${model.className} extends Model
235
- {
236
- protected $table = '${model.tableName}';
237
-
238
- /* Mass Assignment */
239
- protected $fillable = [
240
- ${model.properties.filter(p => p.fillable).map(p => ` '${p.name}',`).join('\\n')}
241
- ];
242
- protected $guarded = [
243
- ${(model.guarded ?? []).map(n => ` '${n}',`).join('\\n')}
244
- ];
245
-
246
- /* Hidden & Casts */
247
- protected $hidden = [
248
- ${model.properties.filter(p => p.hidden).map(p => ` '${p.name}',`).join('\\n')}
249
- ];
250
- protected $casts = [
251
- ${model.properties.filter(p => p.cast).map(p => ` '${p.name}' => '${p.cast}',`).join('\\n')}
252
- ${model.properties.filter(p => p.enumRef).map(p => ` '${p.name}' => ${p.enumRef}::class,`).join('\\n')}
253
- ];
254
-
255
- /* Interfaces metadata */
256
- public array $interfaces = [
257
- ${Object.entries(model.interfaces).map(([k,i]) =>
258
- ` '${k}' => {${i.import ? ` import: '${i.import}',` : ''} type: '${i.type}' },`).join('\\n')}
259
- ];
260
- // This structure is useful for packages like fumeapp/modeltyper which
261
- // read interface metadata to build TypeScript helpers.
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
- </details>
276
-
277
- ---
278
-
279
- ## 🚀 Enum Casting
280
-
281
- ```php
282
- protected $casts = [
283
- 'status' => StatusEnum::class,
284
- ];
285
- ```
286
-
287
- ---
288
-
289
- ## 💡 Tips
290
-
291
- - Combine `migration` & `model` in one customize command when table names align.
292
- - Use `noEmit: true` for dry‑runs or CI validation.
293
- - Escape template chars in stub files.
294
-
295
- ---
296
-
297
- ## 📜 License
298
-
1
+ # Prisma Laravel Migrate
2
+
3
+ A generator plugin that translates your **Prisma schema** into Laravel‑ready
4
+ **Database Migrations**, **Eloquent Models**, and **Enum classes**.
5
+ Built in strict TypeScript with fully‑customisable stubs, grouping, and smart merge updates.
6
+
7
+ ---
8
+
9
+ ## 📦 Installation
10
+
11
+ ```bash
12
+ npm install prisma-laravel-migrate --save-dev
13
+ # requires the Prisma CLI in your project
14
+ ```
15
+
16
+ ---
17
+
18
+ ## 🛠️ Prisma Generator Setup
19
+
20
+ Add both generator blocks to **`schema.prisma`**:
21
+
22
+ ```prisma
23
+ generator migrate {
24
+ provider = "prisma-laravel-migrate"
25
+ stubDir = "./prisma/stubs"
26
+
27
+ output = "database/migrations" // fallback
28
+ outputDir = "database/migrations" // takes precedence
29
+
30
+ noEmit = false // skip writing if true
31
+ groups = "./prisma/group-stubs.js"
32
+ }
33
+
34
+ generator modeler {
35
+ provider = "prisma-laravel-models"
36
+ stubDir = "./prisma/stubs"
37
+
38
+ output = "app/Models"
39
+ outputDir = "app/Models" // overrides output
40
+ outputEnumDir = "app/Enums"
41
+
42
+ noEmit = false
43
+ groups = "./prisma/group-stubs.js"
44
+ }
45
+ ```
46
+
47
+ ### Field Reference
48
+
49
+ | Key | Notes |
50
+ | --- | --- |
51
+ | `outputDir / output` | Destination folder (`outputDir` overrides `output`). |
52
+ | `outputEnumDir` | (modeler) directory for generated enum classes. |
53
+ | `stubDir` | Root stub folder (`migration/`, `model/`, `enum/`). |
54
+ | `groups` | JS module that maps stub files to table groups. |
55
+ | `noEmit` | If `true`, generator parses but **writes no** files (dryrun). |
56
+
57
+ ---
58
+
59
+ ## 🔀 `groups` – Stub Grouping
60
+
61
+ ```prisma
62
+ generator migrate {
63
+ provider = "prisma-laravel-migrate"
64
+ stubDir = "./prisma/stubs"
65
+ groups = "./prisma/group-stubs.js"
66
+ }
67
+ ```
68
+
69
+ `prisma/group-stubs.js`
70
+
71
+ ```js
72
+ module.exports = [
73
+ {
74
+ stubFile: "auth.stub", // stubs/migration/auth.stub
75
+ tables: ["users","accounts","password_resets"]
76
+ },
77
+ {
78
+ stubFile: "billing.stub", // stubs/migration/billing.stub
79
+ tables: ["invoices","transactions"]
80
+ }
81
+ ];
82
+ ```
83
+
84
+ **Resolution order**
85
+
86
+ 1. `stubs/<type>/<table>.stub` (table‑specific)
87
+ 2. Matching group stub (`stubFile`)
88
+ 3. `stubs/<type>/index.stub` (default)
89
+
90
+ ---
91
+
92
+ ## 📁 Stub Folder Layout
93
+
94
+ ```text
95
+ prisma/stubs/
96
+ ├── migration/index.stub
97
+ ├── model/index.stub
98
+ ├── model/simple-model.stub
99
+ └── enum/index.stub
100
+ ```
101
+
102
+ Add table‑specific overrides at
103
+ `stubs/<type>/<table>.stub` (e.g. `stubs/model/users.stub`).
104
+
105
+ ---
106
+
107
+ ## 🔧 CLI Commands
108
+
109
+ | Command | Purpose |
110
+ | --- | --- |
111
+ | `init` | Inject generator blocks & scaffold stub folders |
112
+ | `customize` | Create per-table stub overrides |
113
+ | `gen` | Run `prisma generate` then Laravel generators |
114
+
115
+ ### init
116
+
117
+ ```bash
118
+ npx prisma-laravel-cli init --schema=prisma/schema.prisma
119
+ ```
120
+
121
+ ### customize
122
+
123
+ ```bash
124
+ npx prisma-laravel-cli customize -t migration,model -n users,accounts --force
125
+ ```
126
+
127
+ | Flag | Description |
128
+ | --- | --- |
129
+ | `-t, --type` | **Required.** Stub types (`migration`, `model`, `enum`). `enum` may not mix. |
130
+ | `-n, --names` | **Required.** Table or enum names (`users,accounts`). |
131
+ | `--force` | Overwrite existing stub files. |
132
+ | `--config` | Alternate CLI config path. |
133
+
134
+ ### gen
135
+
136
+ ```bash
137
+ npx prisma-laravel-cli gen --config=prisma/laravel.config.js
138
+ # skip prisma generate step
139
+ npx prisma-laravel-cli gen --config=prisma/laravel.config.js --skipGenerate
140
+ ```
141
+
142
+ `prisma/laravel.config.js`
143
+
144
+ ```js
145
+ module.exports = {
146
+ migrator: {
147
+ outputDir: "database/migrations",
148
+ stubDir: "prisma/stubs",
149
+ groups: "./prisma/group-stubs.js"
150
+ },
151
+ modeler: {
152
+ outputDir: "app/Models",
153
+ outputEnumDir: "app/Enums",
154
+ stubDir: "prisma/stubs",
155
+ groups: "./prisma/group-stubs.js"
156
+ }
157
+ };
158
+ ```
159
+
160
+ ---
161
+
162
+ ## 🔄 How updates are applied
163
+
164
+ 1. Generator builds a **full new file** from your schema & stubs.
165
+ 2. Performs a **git‑style 3‑way merge** (using `node-diff3`):
166
+ - **base** = last generator output (`.prisma-laravel/backups/...`)
167
+ - **ours** = file on disk (user edits)
168
+ - **theirs** = freshly generated file
169
+ 3. Non‑conflicting changes merge automatically; conflicts are wrapped with
170
+ `<<<<<<<`, `=======`, `>>>>>>>`.
171
+ 4. New `use …;` imports are merged, duplicates skipped.
172
+ 5. Baseline copy is updated in the backups folder.
173
+
174
+ Delete the marker block **and** set `noEmit = true` to stop updates for a file.
175
+
176
+ ---
177
+
178
+ ## Stub Customisation Notes
179
+
180
+ Stubs are **JavaScript template literals**. Escape \` and \${ } if you want them literally.
181
+
182
+ > **Fully custom model stubs**
183
+ > If you remove the `${content}` placeholder **and** the marker block, the
184
+ > generator leaves the file untouched.
185
+ > Keep the markers if you want automated updates but customised surroundings.
186
+
187
+ ---
188
+
189
+ ## 📑 Default Stub Templates
190
+
191
+ <details>
192
+ <summary>Enum <code>index.stub</code></summary>
193
+
194
+ ```php
195
+ <?php
196
+
197
+ namespace App\\Enums;
198
+
199
+ enum ${enumDef.name}: string
200
+ {
201
+ // <prisma-laravel:start>
202
+ ${enumDef.values.map(v => ` case ${v} = '${v}';`).join('\\n')}
203
+ // <prisma-laravel:end>
204
+ }
205
+ ```
206
+
207
+ </details>
208
+
209
+ <details>
210
+ <summary>Migration <code>index.stub</code></summary>
211
+
212
+ ```php
213
+ <?php
214
+
215
+ use Illuminate\\Database\\Migrations\\Migration;
216
+ use Illuminate\\Database\\Schema\\Blueprint;
217
+ use Illuminate\\Support\\Facades\\Schema;
218
+
219
+ return new class extends Migration
220
+ {
221
+ public function up(): void
222
+ {
223
+ Schema::create('${tableName}', function (Blueprint $table) {
224
+ // <prisma-laravel:start>
225
+ ${columns}
226
+ // <prisma-laravel:end>
227
+ });
228
+ }
229
+
230
+ public function down(): void
231
+ {
232
+ Schema::dropIfExists('${tableName}');
233
+ }
234
+ };
235
+ ```
236
+
237
+ </details>
238
+
239
+ ---
240
+
241
+ ## 🏗️ Complex Model Stub Example
242
+
243
+ <details>
244
+ <summary>Expand stub</summary>
245
+
246
+ ```php
247
+ <?php
248
+
249
+ namespace App\\Models;
250
+
251
+ ${model.imports}
252
+ use Illuminate\\Database\\Eloquent\\Model;
253
+ use Illuminate\\Database\\Eloquent\\Relations\\{ BelongsTo, HasMany, BelongsToMany };
254
+
255
+ class ${model.className} extends Model
256
+ {
257
+ protected $table = '${model.tableName}';
258
+
259
+ /* Mass Assignment */
260
+ protected $fillable = [
261
+ ${model.properties.filter(p => p.fillable).map(p => ` '${p.name}',`).join('\\n')}
262
+ ];
263
+ protected $guarded = [
264
+ ${(model.guarded ?? []).map(n => ` '${n}',`).join('\\n')}
265
+ ];
266
+
267
+ /* Hidden & Casts */
268
+ protected $hidden = [
269
+ ${model.properties.filter(p => p.hidden).map(p => ` '${p.name}',`).join('\\n')}
270
+ ];
271
+ protected $casts = [
272
+ ${model.properties.filter(p => p.cast).map(p => ` '${p.name}' => '${p.cast}',`).join('\\n')}
273
+ ${model.properties.filter(p => p.enumRef).map(p => ` '${p.name}' => ${p.enumRef}::class,`).join('\\n')}
274
+ ];
275
+
276
+ /* Interfaces metadata */
277
+ public array $interfaces = [
278
+ ${Object.entries(model.interfaces).map(([k,i]) =>
279
+ ` '${k}' => {${i.import ? ` import: '${i.import}',` : ''} type: '${i.type}' },`).join('\\n')}
280
+ ];
281
+ // This structure is useful for packages like fumeapp/modeltyper which
282
+ // read interface metadata to build TypeScript helpers.
283
+
284
+ /* Relationships */
285
+ ${model.relations.map(r => {
286
+ const args = [r.modelClass, r.foreignKey ? `'${r.foreignKey}'` : '', r.localKey ? `'${r.localKey}'` : ''].filter(Boolean).join(', ');
287
+ return ` public function ${r.name}(): ${r.type.charAt(0).toUpperCase()+r.type.slice(1)}\\n {\\n return $this->${r.type}(${args});\\n }`;
288
+ }).join('\\n\\n')}
289
+
290
+ // <prisma-laravel:start>
291
+ ${content}
292
+ // <prisma-laravel:end>
293
+ }
294
+ ```
295
+
296
+ </details>
297
+
298
+ ---
299
+
300
+ ## 🚀 Enum Casting
301
+
302
+ ```php
303
+ protected $casts = [
304
+ 'status' => StatusEnum::class,
305
+ ];
306
+ ```
307
+
308
+ ---
309
+
310
+
311
+ ## 🧩 Custom Migration Rules
312
+
313
+ Point the generator’s `rules` field to a JS file exporting an **array** of
314
+ objects that implement the `Rule` interface:
315
+
316
+ ```prisma
317
+ generator migrate {
318
+ provider = "prisma-laravel-migration"
319
+ stubDir = "./prisma/stubs"
320
+ rules = "./prisma/custom-rules.js"
321
+ }
322
+ ```
323
+
324
+ `prisma/custom-rules.js`
325
+
326
+ ```js
327
+ /** @type {import('prisma-laravel-migrate').Rule[]} */
328
+ module.exports = [
329
+ {
330
+ // Always add an `archived` boolean column defaulting to false
331
+ test(def) {
332
+ return def.name === "archived" && def.migrationType === "boolean";
333
+ },
334
+ render() {
335
+ return {
336
+ column: "archived",
337
+ snippet: ["$table->boolean('archived')->default(false);"],
338
+ };
339
+ },
340
+ },
341
+ // add more Rule objects...
342
+ ];
343
+ ```
344
+
345
+ **Rule execution order**
346
+
347
+ 1. Built‑in rules
348
+ 2. Custom rules (executed in array order)
349
+
350
+ ---
351
+
352
+ ### ColumnDefinition quick reference
353
+
354
+ `ColumnDefinition` extends Prisma’s `DMMF.Field`, so all raw Prisma
355
+ properties remain accessible.
356
+
357
+ ```ts
358
+ import { DMMF } from "@prisma/generator-helper";
359
+
360
+ export interface ColumnDefinition extends DMMF.Field {
361
+ migrationType: MigrationType; // e.g. "unsignedBigInteger"
362
+ args?: string[];
363
+ nullable?: boolean;
364
+ unsigned?: boolean;
365
+
366
+ hasDefaultValue: boolean;
367
+ default?: string | number | boolean | null;
368
+
369
+ relationship?: {
370
+ on: string;
371
+ references?: string;
372
+ onDelete?: string;
373
+ onUpdate?: string;
374
+ };
375
+
376
+ ignore?: boolean;
377
+ }
378
+ ```
379
+
380
+ Common checks inside a rule:
381
+
382
+ ```ts
383
+ def.kind // "scalar" | "enum" | "object"
384
+ def.type // original Prisma scalar
385
+ def.migrationType // mapped Laravel builder name
386
+ def.isId
387
+ ```
388
+
389
+ 📝 **Prisma DMMF docs:**
390
+ https://github.com/prisma/prisma/blob/main/packages/prisma-schema-wasm/src/__tests__/snapshot/dmmf.md
391
+
392
+
393
+ ---
394
+
395
+ ### 📝 Comment-Directives in `schema.prisma`
396
+
397
+ Attach these `@` directives either to a **field** (inline or `///` above) **or**
398
+ to the **model** (curly‑brace syntax) to control what the generator writes into
399
+ your Eloquent model.
400
+
401
+ | Directive | Where you can put it | Effect in generated PHP |
402
+ | --- | --- | --- |
403
+ | `@fillable` | Field **or** `@fillable{...}` on model | Adds column(s) to `$fillable` |
404
+ | `@hidden` | Field **or** `@hidden{...}` on model | Adds column(s) to `$hidden` |
405
+ | `@guarded` | Field **or** `@guarded{...}` on model | Adds column(s) to `$guarded` |
406
+ | `@cast{...}` | Field only | Adds custom entry to `$casts` |
407
+ | `@type{ import:'…', type:'…' }` | Field only | Adds entry to `$interfaces` metadata |
408
+ | `@ignore` | Relation field | Skips generating the relationship method |
409
+ | `@with` (no args) | Relation field | Adds that single relation to `$with` |
410
+ | `@with(rel1,rel2,…)` | Model only | Adds listed relations to `$with` |
411
+
412
+ > **Syntax options**
413
+ > • Inline:
414
+ > `balance Decimal /// @fillable @cast{decimal:2}`
415
+ > • Block above field:
416
+ > `/// @hidden`
417
+ > • Model list:
418
+ > `/// @fillable{name,balance}`
419
+ > • Model eager‑load:
420
+ > `/// @with(posts,roles)`
421
+
422
+ ---
423
+
424
+ #### Example
425
+
426
+ ```prisma
427
+ /// @fillable{name,balance}
428
+ /// @hidden{secretToken}
429
+ model Account {
430
+ id Int @id @default(autoincrement())
431
+
432
+ balance Decimal @default(0.0) /// @cast{decimal:2}
433
+
434
+ nickname String /// @fillable @hidden
435
+
436
+ profile Json? /// @type{ import:'@types/forms', type:'ProfileDTO' }
437
+
438
+ company Company? @relation(fields:[companyId], references:[id]) /// @ignore
439
+ companyId Int?
440
+
441
+ posts Post[] /// @with
442
+ }
443
+
444
+ /// @with(posts,comments)
445
+ model User {
446
+ id Int @id @default(autoincrement())
447
+ email String
448
+ posts Post[]
449
+ comments Comment[]
450
+ }
451
+ ```
452
+
453
+ **Generated output**
454
+
455
+ ```php
456
+ protected $fillable = ['name','balance','nickname'];
457
+ protected $hidden = ['secretToken','nickname'];
458
+ protected $casts = ['balance' => 'decimal:2'];
459
+
460
+ public array $interfaces = [
461
+ 'profile' => { import: '@types/forms', type: 'ProfileDTO' },
462
+ ];
463
+
464
+ protected $with = ['posts','comments'];
465
+ ```
466
+
467
+ `@ignore` prevents the `company()` relation method.
468
+ Combine multiple inline directives; they’re processed left‑to‑right.
469
+
470
+ ---
471
+
472
+ ## 💡 Tips
473
+
474
+ - Combine `migration` & `model` in one customize command when table names align.
475
+ - Use `noEmit: true` for dry‑runs or CI validation.
476
+ - Escape template chars in stub files.
477
+
478
+ ---
479
+
480
+ ## 📜 License
481
+
299
482
  MIT — Happy scaffolding! 🎉
@@ -0,0 +1,14 @@
1
+ import path from "path";
2
+ import { mkdirSync, existsSync } from "fs";
3
+ /** project-level hidden folder */
4
+ const BACKUP_ROOT = path.resolve(process.cwd(), ".prisma-laravel", "backups");
5
+ /** Returns `<root>/.prisma-laravel/backups/<relative-to-cwd>.bak` */
6
+ export function backupPathFor(targetFile) {
7
+ const rel = path.relative(process.cwd(), targetFile);
8
+ const full = path.join(BACKUP_ROOT, rel + ".bak");
9
+ const dir = path.dirname(full);
10
+ if (!existsSync(dir))
11
+ mkdirSync(dir, { recursive: true });
12
+ return full;
13
+ }
14
+ //# sourceMappingURL=backupPath.js.map
@@ -0,0 +1,37 @@
1
+ // writeWithMerge.ts
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import * as diff3 from "node-diff3";
4
+ import { backupPathFor } from "./backupPath.js";
5
+ /**
6
+ * Git-style 3-way merge writer.
7
+ * @param filePath Destination file
8
+ * @param newContent Freshly generated FULL text
9
+ * @param overwrite Skip write if false and file exists
10
+ */
11
+ export function writeWithMerge(filePath, newContent, overwrite = true) {
12
+ if (!overwrite && existsSync(filePath))
13
+ return;
14
+ const bakPath = backupPathFor(filePath);
15
+ const base = existsSync(bakPath) ? readFileSync(bakPath, "utf-8") : null;
16
+ /* initial write */
17
+ if (!existsSync(filePath)) {
18
+ writeFileSync(filePath, newContent, "utf-8");
19
+ writeFileSync(bakPath, newContent, "utf-8");
20
+ return;
21
+ }
22
+ const mine = readFileSync(filePath, "utf-8");
23
+ if (mine === newContent)
24
+ return; // already up-to-date
25
+ /* no baseline yet → save current as baseline, overwrite */
26
+ if (!base) {
27
+ writeFileSync(bakPath, mine, "utf-8");
28
+ writeFileSync(filePath, newContent, "utf-8");
29
+ return;
30
+ }
31
+ /* diff3 merge */
32
+ const merged = diff3
33
+ .merge(mine.split(/\r?\n/), base.split(/\r?\n/), newContent.split(/\r?\n/), { stringSeparator: "\n" }).result.join("\n");
34
+ writeFileSync(filePath, merged, "utf-8");
35
+ writeFileSync(bakPath, newContent, "utf-8"); // update baseline
36
+ }
37
+ //# sourceMappingURL=writer.js.map
@@ -2,9 +2,9 @@ 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 { writeWithMarkers } from "../../generator/utils.js";
6
5
  import { fileURLToPath } from "url";
7
6
  import { sortMigrations } from "./sort.js";
7
+ import { writeWithMerge } from "diff-writer/writer.js";
8
8
  export async function generateLaravelSchema(options) {
9
9
  const { dmmf, generator } = options;
10
10
  // 0) Pull config values (all come in as strings)
@@ -75,9 +75,9 @@ export async function generateLaravelSchema(options) {
75
75
  : `${timestamp}_create_${mig.tableName}_table.php`;
76
76
  const filePath = path.join(baseOut, fileName);
77
77
  // 3) Extract full & generated parts from your printer
78
- const { fullContent: content, columns: generated } = printer.printMigration(mig);
78
+ const { fullContent: content } = printer.printMigration(mig);
79
79
  // 4) Write with markers as before
80
- writeWithMarkers(filePath, content, generated, cfg.startMarker, cfg.endMarker, cfg.overwriteExisting ?? false);
80
+ writeWithMerge(filePath, content, cfg.overwriteExisting ?? false);
81
81
  });
82
82
  return migrations;
83
83
  }
@@ -13,58 +13,80 @@ export class PrismaToLaravelModelGenerator {
13
13
  values: e.values.map((v) => v.name),
14
14
  }));
15
15
  // 2) Build each ModelDefinition
16
- const models = this.dmmf.datamodel.models.map((model) => {
17
- const tableName = model.dbName ?? model.name;
18
- const className = model.name;
16
+ // 2) Build each ModelDefinition
17
+ const models = this.dmmf.datamodel.models.map(model => {
18
+ /* ── 2.1 Model-level directives ──────────────────────────────── */
19
19
  const modelDoc = model.documentation ?? "";
20
- const guardedMatch = modelDoc.match(/@guarded\{([^}]+)\}/);
21
- const guarded = guardedMatch
22
- ? guardedMatch[1]
23
- .split(",")
24
- .map((s) => s.trim())
25
- .filter(Boolean)
26
- : undefined;
27
- const withList = [];
28
- // 2a) Properties (scalars + enums + @fillable/@hidden/@ignore/@cast/@type)
29
- const properties = model.fields.map((field) => {
20
+ // helper get list from @tag{a,b,c}
21
+ const listFrom = (doc, tag) => {
22
+ const m = doc.match(new RegExp(`@${tag}\\{([^}]+)\\}`));
23
+ return m ? m[1].split(",").map(s => s.trim()).filter(Boolean) : [];
24
+ };
25
+ const modelFillable = listFrom(modelDoc, "fillable");
26
+ const modelHidden = listFrom(modelDoc, "hidden");
27
+ const modelGuarded = listFrom(modelDoc, "guarded");
28
+ // model-level eager-loads @with(rel1,rel2)
29
+ const modelWith = (() => {
30
+ const m = modelDoc.match(/@with([^)]+)/);
31
+ return m ? m[1].split(",").map(s => s.trim()).filter(Boolean) : [];
32
+ })();
33
+ /* ── 2.2 Field processing ────────────────────────────────────── */
34
+ const withList = [...modelWith]; // eager-load bucket
35
+ const guardedSet = new Set(modelGuarded);
36
+ const fillableSet = new Set(modelFillable);
37
+ const hiddenSet = new Set(modelHidden);
38
+ const properties = model.fields.map(field => {
30
39
  const doc = field.documentation ?? "";
31
- const fillable = /@fillable\b/.test(doc);
32
- const hidden = /@hidden\b/.test(doc);
33
- const ignore = /@ignore\b/.test(doc);
34
- // parse a single @cast{...}
40
+ // quick helper for boolean flags
41
+ const flag = (tag) => new RegExp(`@${tag}\\b`).test(doc);
42
+ const fillable = flag("fillable") || fillableSet.has(field.name);
43
+ const hidden = flag("hidden") || hiddenSet.has(field.name);
44
+ const guarded = flag("guarded") || guardedSet.has(field.name);
45
+ const ignore = flag("ignore");
46
+ // eager-load relation field
47
+ if (flag("with") && !withList.includes(field.name)) {
48
+ withList.push(field.name);
49
+ }
50
+ // custom cast
35
51
  const castMatch = doc.match(/@cast\{([^}]+)\}/);
36
- let cast = castMatch ? castMatch[1].trim() : undefined;
37
- const typeMatch = doc.match(/@type\s*(?:import\s*=\s*"([^"]+)")?\s*,?\s*type\s*=\s*"([^"]+)"/);
52
+ const cast = castMatch ? castMatch[1].trim() : undefined;
53
+ // @type{ import:'x', type:'Y' }
54
+ const typeMatch = doc.match(/@type\{\s*(?:import\s*:\s*'([^']+)')?\s*,?\s*type\s*:\s*'([^']+)'\s*\}/);
38
55
  const typeAnnotation = typeMatch
39
56
  ? { import: typeMatch[1], type: typeMatch[2] }
40
57
  : undefined;
41
- const enumMeta = enums.find((e) => e.name === field.type);
58
+ const enumMeta = enums.find(e => e.name === field.type);
42
59
  const phpType = enumMeta
43
- ? `${field.type}`
60
+ ? enumMeta.name
44
61
  : this.mapPrismaToPhpType(field.type);
45
- // with checking
46
- if (doc.includes("@with"))
47
- withList.push(field.name);
48
62
  return {
49
63
  name: field.name,
50
64
  phpType,
51
65
  fillable,
52
66
  hidden,
53
67
  ignore,
68
+ guarded,
54
69
  cast,
55
70
  enumRef: enumMeta?.name,
56
71
  typeAnnotation,
57
72
  };
58
73
  });
59
- // 2b) Relations: skip any field marked @ignore
74
+ /* ── 2.3 Laravel $guarded array (union model + field) ─────────── */
75
+ const guarded = guardedSet.size || properties.some(p => p.guarded)
76
+ ? [
77
+ ...guardedSet,
78
+ ...properties.filter(p => p.guarded).map(p => p.name),
79
+ ]
80
+ : undefined;
81
+ /* ── 2.4 Relations (unchanged except @ignore honoured) ────────── */
60
82
  const relations = model.fields
61
- .filter((f) => f.kind === "object" &&
83
+ .filter(f => f.kind === "object" &&
62
84
  f.relationName &&
63
85
  !/@ignore\b/.test(f.documentation ?? ""))
64
- .map((f) => {
65
- const relatedModel = this.dmmf.datamodel.models.find((m) => m.name === f.type);
86
+ .map(f => {
87
+ const relatedModel = this.dmmf.datamodel.models.find(m => m.name === f.type);
66
88
  const relatedTable = relatedModel.dbName ?? f.type;
67
- const thisTable = tableName;
89
+ const thisTable = model.dbName ?? model.name;
68
90
  const isImplicitM2M = f.isList && (f.relationFromFields?.length ?? 0) === 0;
69
91
  const relType = isImplicitM2M
70
92
  ? "belongsToMany"
@@ -74,7 +96,7 @@ export class PrismaToLaravelModelGenerator {
74
96
  let pivotTable;
75
97
  if (relType === "belongsToMany") {
76
98
  pivotTable = [thisTable, relatedTable]
77
- .map((t) => t.toLowerCase())
99
+ .map(t => t.toLowerCase())
78
100
  .sort()
79
101
  .join("_");
80
102
  }
@@ -87,22 +109,23 @@ export class PrismaToLaravelModelGenerator {
87
109
  pivotTable,
88
110
  };
89
111
  });
90
- // 2c) Interfaces from @type annotations
112
+ /* ── 2.5 Interfaces from @type annotations ────────────────────── */
91
113
  const interfaces = {};
92
114
  for (const prop of properties) {
93
115
  if (prop.typeAnnotation) {
94
116
  interfaces[prop.name] = { ...prop.typeAnnotation };
95
117
  }
96
118
  }
119
+ /* ── 2.6 Final ModelDefinition ────────────────────────────────── */
97
120
  return {
121
+ className: model.name,
122
+ tableName: model.dbName ?? model.name,
98
123
  guarded,
99
- className,
100
- tableName,
101
124
  properties,
102
125
  relations,
103
126
  enums,
104
127
  interfaces,
105
- with: withList.length ? withList : undefined
128
+ with: withList.length ? withList : undefined,
106
129
  };
107
130
  });
108
131
  return { models, enums };
@@ -1,9 +1,10 @@
1
1
  import { existsSync, mkdirSync } from "fs";
2
2
  import path from "path";
3
- import { buildModelContent, writeWithMarkers } from "../utils.js";
3
+ import { buildModelContent } from "../utils.js";
4
4
  import { StubModelPrinter } from "../../printer/models.js";
5
5
  import { PrismaToLaravelModelGenerator } from "./generator.js";
6
6
  import { fileURLToPath } from "url";
7
+ import { writeWithMerge } from "diff-writer/writer.js";
7
8
  export async function generateLaravelModels(options) {
8
9
  const { dmmf, generator } = options;
9
10
  // 0) Pull config values
@@ -51,7 +52,7 @@ export async function generateLaravelModels(options) {
51
52
  // 2) Load stubs (allow overrides)
52
53
  const modelStub = cfg.modelStubPath
53
54
  ? path.resolve(process.cwd(), cfg.modelStubPath)
54
- : path.resolve(__dirname, "../../../stubs/simple-model.stub");
55
+ : path.resolve(__dirname, "../../../stubs/model.stub");
55
56
  const enumStub = cfg.enumStubPath
56
57
  ? path.resolve(process.cwd(), cfg.enumStubPath)
57
58
  : path.resolve(__dirname, "../../../stubs/enums.stub");
@@ -63,14 +64,16 @@ export async function generateLaravelModels(options) {
63
64
  for (const enumDef of enums) {
64
65
  const enumPhp = printer.printEnum(enumDef);
65
66
  const enumFile = path.join(enumsDir, `${enumDef.name}.php`);
66
- writeWithMarkers(enumFile, enumPhp, enumDef.values.map(v => ` case ${v} = '${v}';`).join('\n'), cfg.startMarker, cfg.endMarker, cfg.overwriteExisting ?? false);
67
+ writeWithMerge(enumFile, enumPhp, cfg.overwriteExisting ?? false);
67
68
  }
68
69
  // 5) Write model files
69
70
  for (const model of models) {
71
+ model.imports = model.properties.filter(item => item.enumRef).map(item => `use App\\Enums\\${item.enumRef};`);
72
+ //----
70
73
  const content = buildModelContent(model);
71
74
  const modelPhp = printer.printModel(model, enums, content);
72
75
  const modelFile = path.join(modelsDir, `${model.className}.php`);
73
- writeWithMarkers(modelFile, modelPhp, content, cfg.startMarker, cfg.endMarker, cfg.overwriteExisting ?? false);
76
+ writeWithMerge(modelFile, modelPhp, cfg.overwriteExisting ?? false);
74
77
  }
75
78
  return { models, enums };
76
79
  }
@@ -1,6 +1,6 @@
1
1
  import { NativeToMigrationTypeMap } from "./migrator/column-maps.js";
2
2
  import { MigrationTypes } from "./migrator/migrationTypes.js";
3
- import { existsSync, readFileSync, writeFileSync } from 'fs';
3
+ import { existsSync } from 'fs';
4
4
  import path from "path";
5
5
  /**
6
6
  * Given a Prisma field default, return the PHP code fragment
@@ -105,9 +105,21 @@ export function buildModelContent(model) {
105
105
  if (model.properties.some(p => p.cast || p.enumRef)) {
106
106
  lines.push(`protected $casts = [\n${model.properties
107
107
  .filter(p => p.cast || p.enumRef)
108
- .map(p => ` '${p.name}' => ${p.enumRef ? `\\App\\Enums\\${p.enumRef}::class` : `'${p.cast}'`}`)
108
+ .map(p => ` '${p.name}' => ${p.enumRef ? `${p.enumRef}::class` : `'${p.cast}'`}`)
109
109
  .join(",\n")}\n ];`);
110
110
  }
111
+ // — Interfaces metadata slot —
112
+ if (model.interfaces && Object.keys(model.interfaces).length) {
113
+ lines.push(` public array $interfaces = [`);
114
+ for (const [key, info] of Object.entries(model.interfaces)) {
115
+ const parts = [];
116
+ if (info.import)
117
+ parts.push(`import: '${info.import}'`);
118
+ parts.push(`type: '${info.type}'`);
119
+ lines.push(` '${key}' => { ${parts.join(', ')} },`);
120
+ }
121
+ lines.push(` ];`);
122
+ }
111
123
  // 4) Relations (unchanged)
112
124
  for (const rel of model.relations) {
113
125
  const args = [
@@ -131,38 +143,6 @@ export function buildModelContent(model) {
131
143
  */
132
144
  export function formatStub(stub) {
133
145
  return stub;
134
- // escape backslashes first
135
- // .replace(/\\/g, '\\\\')
136
- // then escape any backticks
137
- // .replace(/`/g, '\\`');
138
- }
139
- /**
140
- * Safely write or update a file by replacing the region between
141
- * startMarker and endMarker if both exist, otherwise overwrite the whole file.
142
- *
143
- * @param filePath Path to the target file
144
- * @param fullContent The full text to write if markers are missing
145
- * @param generated The text to inject between the markers
146
- * @param startMarker Literal string marking the region start
147
- * @param endMarker Literal string marking the region end
148
- * @param overwrite If false and file exists, do nothing
149
- */
150
- export function writeWithMarkers(filePath, fullContent, generated, startMarker, endMarker, overwrite) {
151
- // If the file exists but we're *not* overwriting, skip entirely
152
- if (existsSync(filePath) && !overwrite)
153
- return;
154
- if (existsSync(filePath)) {
155
- const existing = readFileSync(filePath, 'utf-8');
156
- // If both markers are present, do an in‐place replace
157
- if (existing.includes(startMarker) && existing.includes(endMarker)) {
158
- const escaped = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
159
- const re = new RegExp(`${escaped(startMarker)}[\\s\\S]*?${escaped(endMarker)}`, 'm');
160
- const updated = existing.replace(re, `${startMarker}\n${generated}\n${endMarker}`);
161
- return writeFileSync(filePath, updated, 'utf-8');
162
- }
163
- }
164
- // Otherwise write the full content (which itself can include the markers)
165
- writeFileSync(filePath, fullContent, 'utf-8');
166
146
  }
167
147
  export function resolveStub(cfg, type, tableName) {
168
148
  if (!cfg.stubDir)
@@ -1 +1 @@
1
- {"root":["../src/global.ts","../src/index.ts","../src/test.ts","../src/cli/index.ts","../src/cli/models.index.ts","../src/generator/utils.ts","../src/generator/migrator/prismatolaravelmigrationgenerator.ts","../src/generator/migrator/actions-map.ts","../src/generator/migrator/column-definition-types.ts","../src/generator/migrator/column-definition.ts","../src/generator/migrator/column-maps.ts","../src/generator/migrator/index.ts","../src/generator/migrator/migrationtypes.ts","../src/generator/migrator/rule-definition.ts","../src/generator/migrator/rules.ts","../src/generator/migrator/sort.ts","../src/generator/modeler/generator.ts","../src/generator/modeler/index.ts","../src/generator/modeler/types.ts","../src/printer/migrations.ts","../src/printer/models.ts"],"version":"5.7.2"}
1
+ {"root":["../src/global.ts","../src/index.ts","../src/test.ts","../src/cli/index.ts","../src/cli/models.index.ts","../src/diff-writer/backuppath.ts","../src/diff-writer/writer.ts","../src/generator/utils.ts","../src/generator/migrator/prismatolaravelmigrationgenerator.ts","../src/generator/migrator/actions-map.ts","../src/generator/migrator/column-definition-types.ts","../src/generator/migrator/column-definition.ts","../src/generator/migrator/column-maps.ts","../src/generator/migrator/index.ts","../src/generator/migrator/migrationtypes.ts","../src/generator/migrator/rule-definition.ts","../src/generator/migrator/rules.ts","../src/generator/migrator/sort.ts","../src/generator/modeler/generator.ts","../src/generator/modeler/index.ts","../src/generator/modeler/types.ts","../src/printer/migrations.ts","../src/printer/models.ts"],"version":"5.7.2"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prisma-laravel-migrate",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Generate laravel migrations and/or models using prisma files",
5
5
  "bin": {
6
6
  "prisma-laravel-migrations": "./dist/cli/index.js",
@@ -43,7 +43,9 @@
43
43
  "@types/node": "^24.0.3",
44
44
  "change-case": "^5.4.4",
45
45
  "dayjs": "^1.11.13",
46
+ "diff3": "^0.0.4",
46
47
  "jest": "^30.0.2",
48
+ "node-diff3": "^3.1.2",
47
49
  "pluralize": "^8.0.0",
48
50
  "prettier": "^3.5.3",
49
51
  "ts-node": "^10.9.2",