@famgia/omnify 1.0.10 → 1.0.11
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 +1 -1
- package/docs/associations.md +760 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -217,7 +217,7 @@ properties:
|
|
|
217
217
|
|
|
218
218
|
### Associations (Relationships)
|
|
219
219
|
|
|
220
|
-
Define relationships between schemas. See [full documentation](https://
|
|
220
|
+
Define relationships between schemas. See [full documentation](https://cdn.jsdelivr.net/npm/@famgia/omnify/docs/associations.md) for details.
|
|
221
221
|
|
|
222
222
|
```yaml
|
|
223
223
|
# schemas/post.yaml
|
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
# Associations (Relationships)
|
|
2
|
+
|
|
3
|
+
Omnify supports full relational database associations with bidirectional mappings, cascading operations, and automatic foreign key generation.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Relationship Types](#relationship-types)
|
|
9
|
+
- [ManyToOne (BelongsTo)](#manytoone-belongsto)
|
|
10
|
+
- [OneToMany (HasMany)](#onetomany-hasmany)
|
|
11
|
+
- [OneToOne](#onetoone)
|
|
12
|
+
- [ManyToMany (BelongsToMany)](#manytomany-belongstomany)
|
|
13
|
+
- [Association Properties](#association-properties)
|
|
14
|
+
- [Bidirectional Relationships](#bidirectional-relationships)
|
|
15
|
+
- [Cascading Operations](#cascading-operations)
|
|
16
|
+
- [Self-Referencing Associations](#self-referencing-associations)
|
|
17
|
+
- [Polymorphic Associations](#polymorphic-associations)
|
|
18
|
+
- [Pivot Tables](#pivot-tables)
|
|
19
|
+
- [Generated Output](#generated-output)
|
|
20
|
+
- [Best Practices](#best-practices)
|
|
21
|
+
- [Common Patterns](#common-patterns)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
Associations are defined within the `properties` section using `type: Association`:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
properties:
|
|
31
|
+
author:
|
|
32
|
+
type: Association
|
|
33
|
+
relation: ManyToOne
|
|
34
|
+
target: User
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This generates:
|
|
38
|
+
- **Laravel Migration**: Foreign key column (`author_id`) with constraint
|
|
39
|
+
- **TypeScript**: Type with foreign key field
|
|
40
|
+
- **Validation**: Checks that target schema exists
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Relationship Types
|
|
45
|
+
|
|
46
|
+
### ManyToOne (BelongsTo)
|
|
47
|
+
|
|
48
|
+
The most common relationship. Creates a foreign key column on this table.
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
# schemas/Post.yaml
|
|
52
|
+
name: Post
|
|
53
|
+
properties:
|
|
54
|
+
title:
|
|
55
|
+
type: String
|
|
56
|
+
|
|
57
|
+
# A post belongs to one author
|
|
58
|
+
author:
|
|
59
|
+
type: Association
|
|
60
|
+
relation: ManyToOne
|
|
61
|
+
target: User
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Generated Migration:**
|
|
65
|
+
```php
|
|
66
|
+
Schema::create('posts', function (Blueprint $table) {
|
|
67
|
+
$table->id();
|
|
68
|
+
$table->string('title');
|
|
69
|
+
$table->unsignedBigInteger('author_id');
|
|
70
|
+
|
|
71
|
+
$table->foreign('author_id')
|
|
72
|
+
->references('id')
|
|
73
|
+
->on('users');
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Generated TypeScript:**
|
|
78
|
+
```typescript
|
|
79
|
+
interface Post {
|
|
80
|
+
id: number;
|
|
81
|
+
title: string;
|
|
82
|
+
author_id: number;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### OneToMany (HasMany)
|
|
87
|
+
|
|
88
|
+
Inverse side of ManyToOne. Does NOT create a column (the FK is on the other table).
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
# schemas/User.yaml
|
|
92
|
+
name: User
|
|
93
|
+
properties:
|
|
94
|
+
name:
|
|
95
|
+
type: String
|
|
96
|
+
|
|
97
|
+
# A user has many posts
|
|
98
|
+
posts:
|
|
99
|
+
type: Association
|
|
100
|
+
relation: OneToMany
|
|
101
|
+
target: Post
|
|
102
|
+
inversedBy: author # Links to Post.author
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Generated Migration:** No column added (FK is on `posts` table)
|
|
106
|
+
|
|
107
|
+
**Generated TypeScript:**
|
|
108
|
+
```typescript
|
|
109
|
+
interface User {
|
|
110
|
+
id: number;
|
|
111
|
+
name: string;
|
|
112
|
+
// posts relationship accessed via ORM, not as direct field
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### OneToOne
|
|
117
|
+
|
|
118
|
+
Creates a unique foreign key. Use `owningSide: true` on the table that should have the FK column.
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
# schemas/User.yaml
|
|
122
|
+
name: User
|
|
123
|
+
properties:
|
|
124
|
+
name:
|
|
125
|
+
type: String
|
|
126
|
+
|
|
127
|
+
profile:
|
|
128
|
+
type: Association
|
|
129
|
+
relation: OneToOne
|
|
130
|
+
target: Profile
|
|
131
|
+
owningSide: true # User table has profile_id
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```yaml
|
|
135
|
+
# schemas/Profile.yaml
|
|
136
|
+
name: Profile
|
|
137
|
+
properties:
|
|
138
|
+
bio:
|
|
139
|
+
type: Text
|
|
140
|
+
|
|
141
|
+
user:
|
|
142
|
+
type: Association
|
|
143
|
+
relation: OneToOne
|
|
144
|
+
target: User
|
|
145
|
+
mappedBy: profile # Inverse side
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Generated Migration (users table):**
|
|
149
|
+
```php
|
|
150
|
+
$table->unsignedBigInteger('profile_id')->unique();
|
|
151
|
+
$table->foreign('profile_id')->references('id')->on('profiles');
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### ManyToMany (BelongsToMany)
|
|
155
|
+
|
|
156
|
+
Creates a pivot table. Define on both sides with `inversedBy`.
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
# schemas/Post.yaml
|
|
160
|
+
name: Post
|
|
161
|
+
properties:
|
|
162
|
+
title:
|
|
163
|
+
type: String
|
|
164
|
+
|
|
165
|
+
tags:
|
|
166
|
+
type: Association
|
|
167
|
+
relation: ManyToMany
|
|
168
|
+
target: Tag
|
|
169
|
+
inversedBy: posts
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
# schemas/Tag.yaml
|
|
174
|
+
name: Tag
|
|
175
|
+
properties:
|
|
176
|
+
name:
|
|
177
|
+
type: String
|
|
178
|
+
|
|
179
|
+
posts:
|
|
180
|
+
type: Association
|
|
181
|
+
relation: ManyToMany
|
|
182
|
+
target: Post
|
|
183
|
+
inversedBy: tags
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Generated Pivot Migration:**
|
|
187
|
+
```php
|
|
188
|
+
Schema::create('post_tag', function (Blueprint $table) {
|
|
189
|
+
$table->unsignedBigInteger('post_id');
|
|
190
|
+
$table->unsignedBigInteger('tag_id');
|
|
191
|
+
|
|
192
|
+
$table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
|
|
193
|
+
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
|
|
194
|
+
|
|
195
|
+
$table->primary(['post_id', 'tag_id']);
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Association Properties
|
|
202
|
+
|
|
203
|
+
| Property | Type | Required | Description |
|
|
204
|
+
|----------|------|----------|-------------|
|
|
205
|
+
| `type` | `'Association'` | Yes | Must be `'Association'` |
|
|
206
|
+
| `relation` | `string` | Yes | `OneToOne`, `OneToMany`, `ManyToOne`, `ManyToMany` |
|
|
207
|
+
| `target` | `string` | Yes | Target schema name (PascalCase) |
|
|
208
|
+
| `inversedBy` | `string` | No | Property name on target that maps back (owner defines) |
|
|
209
|
+
| `mappedBy` | `string` | No | Property name on target that owns the relationship |
|
|
210
|
+
| `onDelete` | `string` | No | `CASCADE`, `SET NULL`, `RESTRICT`, `NO ACTION` |
|
|
211
|
+
| `onUpdate` | `string` | No | `CASCADE`, `SET NULL`, `RESTRICT`, `NO ACTION` |
|
|
212
|
+
| `owningSide` | `boolean` | No | For OneToOne, marks which side has the FK |
|
|
213
|
+
| `nullable` | `boolean` | No | Allow NULL for optional relationships |
|
|
214
|
+
| `joinTable` | `string` | No | Custom pivot table name for ManyToMany |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Bidirectional Relationships
|
|
219
|
+
|
|
220
|
+
For bidirectional relationships, use `inversedBy` on the owning side and `mappedBy` on the inverse side:
|
|
221
|
+
|
|
222
|
+
```yaml
|
|
223
|
+
# Post (owning side - has the FK)
|
|
224
|
+
author:
|
|
225
|
+
type: Association
|
|
226
|
+
relation: ManyToOne
|
|
227
|
+
target: User
|
|
228
|
+
inversedBy: posts # "User.posts points back to me"
|
|
229
|
+
|
|
230
|
+
# User (inverse side)
|
|
231
|
+
posts:
|
|
232
|
+
type: Association
|
|
233
|
+
relation: OneToMany
|
|
234
|
+
target: Post
|
|
235
|
+
mappedBy: author # "Post.author is the owning side"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Rules:**
|
|
239
|
+
- `ManyToOne` is always the owning side (has FK column)
|
|
240
|
+
- `OneToMany` is always the inverse side (no FK column)
|
|
241
|
+
- For `OneToOne`, specify `owningSide: true` on one side
|
|
242
|
+
- For `ManyToMany`, either side can have `inversedBy`
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Cascading Operations
|
|
247
|
+
|
|
248
|
+
Control what happens when referenced records are deleted or updated:
|
|
249
|
+
|
|
250
|
+
### onDelete Options
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
author:
|
|
254
|
+
type: Association
|
|
255
|
+
relation: ManyToOne
|
|
256
|
+
target: User
|
|
257
|
+
onDelete: CASCADE # Delete post when user is deleted
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
| Value | Behavior |
|
|
261
|
+
|-------|----------|
|
|
262
|
+
| `CASCADE` | Delete related records automatically |
|
|
263
|
+
| `SET NULL` | Set FK to NULL (requires `nullable: true`) |
|
|
264
|
+
| `RESTRICT` | Prevent deletion if references exist |
|
|
265
|
+
| `NO ACTION` | Database default (usually same as RESTRICT) |
|
|
266
|
+
|
|
267
|
+
### onUpdate Options
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
author:
|
|
271
|
+
type: Association
|
|
272
|
+
relation: ManyToOne
|
|
273
|
+
target: User
|
|
274
|
+
onUpdate: CASCADE # Update FK when user ID changes
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Common Patterns
|
|
278
|
+
|
|
279
|
+
```yaml
|
|
280
|
+
# Required relationship - prevent orphaned records
|
|
281
|
+
author:
|
|
282
|
+
type: Association
|
|
283
|
+
relation: ManyToOne
|
|
284
|
+
target: User
|
|
285
|
+
onDelete: RESTRICT
|
|
286
|
+
|
|
287
|
+
# Optional relationship - allow NULL
|
|
288
|
+
reviewer:
|
|
289
|
+
type: Association
|
|
290
|
+
relation: ManyToOne
|
|
291
|
+
target: User
|
|
292
|
+
nullable: true
|
|
293
|
+
onDelete: SET NULL
|
|
294
|
+
|
|
295
|
+
# Cascade delete - clean up related data
|
|
296
|
+
comments:
|
|
297
|
+
type: Association
|
|
298
|
+
relation: OneToMany
|
|
299
|
+
target: Comment
|
|
300
|
+
inversedBy: post
|
|
301
|
+
# Note: onDelete is set on the owning side (Comment.post)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Self-Referencing Associations
|
|
307
|
+
|
|
308
|
+
Schemas can reference themselves for hierarchical data:
|
|
309
|
+
|
|
310
|
+
```yaml
|
|
311
|
+
# schemas/Category.yaml
|
|
312
|
+
name: Category
|
|
313
|
+
properties:
|
|
314
|
+
name:
|
|
315
|
+
type: String
|
|
316
|
+
|
|
317
|
+
# Parent category (nullable for root categories)
|
|
318
|
+
parent:
|
|
319
|
+
type: Association
|
|
320
|
+
relation: ManyToOne
|
|
321
|
+
target: Category
|
|
322
|
+
nullable: true
|
|
323
|
+
onDelete: SET NULL
|
|
324
|
+
|
|
325
|
+
# Child categories
|
|
326
|
+
children:
|
|
327
|
+
type: Association
|
|
328
|
+
relation: OneToMany
|
|
329
|
+
target: Category
|
|
330
|
+
inversedBy: parent
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Generated Migration:**
|
|
334
|
+
```php
|
|
335
|
+
Schema::create('categories', function (Blueprint $table) {
|
|
336
|
+
$table->id();
|
|
337
|
+
$table->string('name');
|
|
338
|
+
$table->unsignedBigInteger('parent_id')->nullable();
|
|
339
|
+
|
|
340
|
+
$table->foreign('parent_id')
|
|
341
|
+
->references('id')
|
|
342
|
+
->on('categories')
|
|
343
|
+
->onDelete('set null');
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Polymorphic Associations
|
|
350
|
+
|
|
351
|
+
For polymorphic relationships (one field pointing to multiple tables):
|
|
352
|
+
|
|
353
|
+
```yaml
|
|
354
|
+
# schemas/Comment.yaml
|
|
355
|
+
name: Comment
|
|
356
|
+
properties:
|
|
357
|
+
body:
|
|
358
|
+
type: Text
|
|
359
|
+
|
|
360
|
+
# Can comment on Posts, Videos, or Photos
|
|
361
|
+
commentable:
|
|
362
|
+
type: Polymorphic
|
|
363
|
+
targets:
|
|
364
|
+
- Post
|
|
365
|
+
- Video
|
|
366
|
+
- Photo
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Generated Migration:**
|
|
370
|
+
```php
|
|
371
|
+
$table->string('commentable_type');
|
|
372
|
+
$table->unsignedBigInteger('commentable_id');
|
|
373
|
+
$table->index(['commentable_type', 'commentable_id']);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Pivot Tables
|
|
379
|
+
|
|
380
|
+
### Auto-generated Pivot Tables
|
|
381
|
+
|
|
382
|
+
ManyToMany relationships automatically generate pivot tables:
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
# Post has many Tags
|
|
386
|
+
tags:
|
|
387
|
+
type: Association
|
|
388
|
+
relation: ManyToMany
|
|
389
|
+
target: Tag
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Default pivot table name: `post_tag` (alphabetical order, snake_case)
|
|
393
|
+
|
|
394
|
+
### Custom Pivot Table Name
|
|
395
|
+
|
|
396
|
+
```yaml
|
|
397
|
+
tags:
|
|
398
|
+
type: Association
|
|
399
|
+
relation: ManyToMany
|
|
400
|
+
target: Tag
|
|
401
|
+
joinTable: article_tags # Custom name
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Pivot Table with Extra Columns
|
|
405
|
+
|
|
406
|
+
Create a dedicated schema with `id: false`:
|
|
407
|
+
|
|
408
|
+
```yaml
|
|
409
|
+
# schemas/PostTag.yaml
|
|
410
|
+
name: PostTag
|
|
411
|
+
options:
|
|
412
|
+
id: false # No auto-increment ID
|
|
413
|
+
timestamps: true # Add created_at, updated_at
|
|
414
|
+
|
|
415
|
+
properties:
|
|
416
|
+
post:
|
|
417
|
+
type: Association
|
|
418
|
+
relation: ManyToOne
|
|
419
|
+
target: Post
|
|
420
|
+
onDelete: CASCADE
|
|
421
|
+
|
|
422
|
+
tag:
|
|
423
|
+
type: Association
|
|
424
|
+
relation: ManyToOne
|
|
425
|
+
target: Tag
|
|
426
|
+
onDelete: CASCADE
|
|
427
|
+
|
|
428
|
+
order:
|
|
429
|
+
type: Int
|
|
430
|
+
default: 0
|
|
431
|
+
|
|
432
|
+
featured:
|
|
433
|
+
type: Boolean
|
|
434
|
+
default: false
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Generated Migration:**
|
|
438
|
+
```php
|
|
439
|
+
Schema::create('post_tags', function (Blueprint $table) {
|
|
440
|
+
$table->unsignedBigInteger('post_id');
|
|
441
|
+
$table->unsignedBigInteger('tag_id');
|
|
442
|
+
$table->integer('order')->default(0);
|
|
443
|
+
$table->boolean('featured')->default(false);
|
|
444
|
+
$table->timestamps();
|
|
445
|
+
|
|
446
|
+
$table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
|
|
447
|
+
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
|
|
448
|
+
|
|
449
|
+
$table->primary(['post_id', 'tag_id']);
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Generated Output
|
|
456
|
+
|
|
457
|
+
### Laravel Migration Example
|
|
458
|
+
|
|
459
|
+
```yaml
|
|
460
|
+
# schemas/Comment.yaml
|
|
461
|
+
name: Comment
|
|
462
|
+
properties:
|
|
463
|
+
body:
|
|
464
|
+
type: Text
|
|
465
|
+
|
|
466
|
+
post:
|
|
467
|
+
type: Association
|
|
468
|
+
relation: ManyToOne
|
|
469
|
+
target: Post
|
|
470
|
+
onDelete: CASCADE
|
|
471
|
+
|
|
472
|
+
author:
|
|
473
|
+
type: Association
|
|
474
|
+
relation: ManyToOne
|
|
475
|
+
target: User
|
|
476
|
+
nullable: true
|
|
477
|
+
onDelete: SET NULL
|
|
478
|
+
|
|
479
|
+
options:
|
|
480
|
+
timestamps: true
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Generated:**
|
|
484
|
+
```php
|
|
485
|
+
Schema::create('comments', function (Blueprint $table) {
|
|
486
|
+
$table->id();
|
|
487
|
+
$table->text('body');
|
|
488
|
+
$table->unsignedBigInteger('post_id');
|
|
489
|
+
$table->unsignedBigInteger('author_id')->nullable();
|
|
490
|
+
$table->timestamps();
|
|
491
|
+
|
|
492
|
+
$table->foreign('post_id')
|
|
493
|
+
->references('id')
|
|
494
|
+
->on('posts')
|
|
495
|
+
->onDelete('cascade');
|
|
496
|
+
|
|
497
|
+
$table->foreign('author_id')
|
|
498
|
+
->references('id')
|
|
499
|
+
->on('users')
|
|
500
|
+
->onDelete('set null');
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### TypeScript Types Example
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
interface Comment {
|
|
508
|
+
id: number;
|
|
509
|
+
body: string;
|
|
510
|
+
post_id: number;
|
|
511
|
+
author_id: number | null;
|
|
512
|
+
created_at: string;
|
|
513
|
+
updated_at: string;
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Best Practices
|
|
520
|
+
|
|
521
|
+
### 1. Always Define Both Sides
|
|
522
|
+
|
|
523
|
+
Define relationships on both schemas for clarity and tooling support:
|
|
524
|
+
|
|
525
|
+
```yaml
|
|
526
|
+
# Post.yaml
|
|
527
|
+
author:
|
|
528
|
+
type: Association
|
|
529
|
+
relation: ManyToOne
|
|
530
|
+
target: User
|
|
531
|
+
inversedBy: posts
|
|
532
|
+
|
|
533
|
+
# User.yaml
|
|
534
|
+
posts:
|
|
535
|
+
type: Association
|
|
536
|
+
relation: OneToMany
|
|
537
|
+
target: Post
|
|
538
|
+
mappedBy: author
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### 2. Use Appropriate Cascade Actions
|
|
542
|
+
|
|
543
|
+
| Scenario | onDelete | Why |
|
|
544
|
+
|----------|----------|-----|
|
|
545
|
+
| Comments on a Post | `CASCADE` | Delete comments when post is deleted |
|
|
546
|
+
| Posts by a User | `RESTRICT` | Prevent user deletion if they have posts |
|
|
547
|
+
| Optional reviewer | `SET NULL` | Keep record, just remove the reference |
|
|
548
|
+
|
|
549
|
+
### 3. Name Associations Clearly
|
|
550
|
+
|
|
551
|
+
```yaml
|
|
552
|
+
# Good - clear intent
|
|
553
|
+
author:
|
|
554
|
+
type: Association
|
|
555
|
+
relation: ManyToOne
|
|
556
|
+
target: User
|
|
557
|
+
|
|
558
|
+
createdBy:
|
|
559
|
+
type: Association
|
|
560
|
+
relation: ManyToOne
|
|
561
|
+
target: User
|
|
562
|
+
|
|
563
|
+
assignedTo:
|
|
564
|
+
type: Association
|
|
565
|
+
relation: ManyToOne
|
|
566
|
+
target: User
|
|
567
|
+
nullable: true
|
|
568
|
+
|
|
569
|
+
# Bad - ambiguous
|
|
570
|
+
user1:
|
|
571
|
+
type: Association
|
|
572
|
+
relation: ManyToOne
|
|
573
|
+
target: User
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### 4. Consider Query Patterns
|
|
577
|
+
|
|
578
|
+
Place FK on the table you'll query most often:
|
|
579
|
+
|
|
580
|
+
```yaml
|
|
581
|
+
# If you often query "posts by user", this is correct:
|
|
582
|
+
# Post.author -> User (FK on posts table)
|
|
583
|
+
|
|
584
|
+
# If you often query "user's current session", consider:
|
|
585
|
+
# User.currentSession -> Session (FK on users table)
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
## Common Patterns
|
|
591
|
+
|
|
592
|
+
### Blog System
|
|
593
|
+
|
|
594
|
+
```yaml
|
|
595
|
+
# User.yaml
|
|
596
|
+
name: User
|
|
597
|
+
properties:
|
|
598
|
+
name:
|
|
599
|
+
type: String
|
|
600
|
+
posts:
|
|
601
|
+
type: Association
|
|
602
|
+
relation: OneToMany
|
|
603
|
+
target: Post
|
|
604
|
+
inversedBy: author
|
|
605
|
+
comments:
|
|
606
|
+
type: Association
|
|
607
|
+
relation: OneToMany
|
|
608
|
+
target: Comment
|
|
609
|
+
inversedBy: author
|
|
610
|
+
|
|
611
|
+
# Post.yaml
|
|
612
|
+
name: Post
|
|
613
|
+
properties:
|
|
614
|
+
title:
|
|
615
|
+
type: String
|
|
616
|
+
content:
|
|
617
|
+
type: Text
|
|
618
|
+
author:
|
|
619
|
+
type: Association
|
|
620
|
+
relation: ManyToOne
|
|
621
|
+
target: User
|
|
622
|
+
mappedBy: posts
|
|
623
|
+
onDelete: RESTRICT
|
|
624
|
+
comments:
|
|
625
|
+
type: Association
|
|
626
|
+
relation: OneToMany
|
|
627
|
+
target: Comment
|
|
628
|
+
inversedBy: post
|
|
629
|
+
tags:
|
|
630
|
+
type: Association
|
|
631
|
+
relation: ManyToMany
|
|
632
|
+
target: Tag
|
|
633
|
+
inversedBy: posts
|
|
634
|
+
|
|
635
|
+
# Comment.yaml
|
|
636
|
+
name: Comment
|
|
637
|
+
properties:
|
|
638
|
+
body:
|
|
639
|
+
type: Text
|
|
640
|
+
post:
|
|
641
|
+
type: Association
|
|
642
|
+
relation: ManyToOne
|
|
643
|
+
target: Post
|
|
644
|
+
onDelete: CASCADE
|
|
645
|
+
author:
|
|
646
|
+
type: Association
|
|
647
|
+
relation: ManyToOne
|
|
648
|
+
target: User
|
|
649
|
+
nullable: true
|
|
650
|
+
onDelete: SET NULL
|
|
651
|
+
|
|
652
|
+
# Tag.yaml
|
|
653
|
+
name: Tag
|
|
654
|
+
properties:
|
|
655
|
+
name:
|
|
656
|
+
type: String
|
|
657
|
+
unique: true
|
|
658
|
+
posts:
|
|
659
|
+
type: Association
|
|
660
|
+
relation: ManyToMany
|
|
661
|
+
target: Post
|
|
662
|
+
inversedBy: tags
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### E-commerce Order System
|
|
666
|
+
|
|
667
|
+
```yaml
|
|
668
|
+
# Order.yaml
|
|
669
|
+
name: Order
|
|
670
|
+
properties:
|
|
671
|
+
orderNumber:
|
|
672
|
+
type: String
|
|
673
|
+
unique: true
|
|
674
|
+
customer:
|
|
675
|
+
type: Association
|
|
676
|
+
relation: ManyToOne
|
|
677
|
+
target: User
|
|
678
|
+
onDelete: RESTRICT
|
|
679
|
+
items:
|
|
680
|
+
type: Association
|
|
681
|
+
relation: OneToMany
|
|
682
|
+
target: OrderItem
|
|
683
|
+
inversedBy: order
|
|
684
|
+
|
|
685
|
+
# OrderItem.yaml
|
|
686
|
+
name: OrderItem
|
|
687
|
+
properties:
|
|
688
|
+
quantity:
|
|
689
|
+
type: Int
|
|
690
|
+
price:
|
|
691
|
+
type: Decimal
|
|
692
|
+
precision: 10
|
|
693
|
+
scale: 2
|
|
694
|
+
order:
|
|
695
|
+
type: Association
|
|
696
|
+
relation: ManyToOne
|
|
697
|
+
target: Order
|
|
698
|
+
onDelete: CASCADE
|
|
699
|
+
product:
|
|
700
|
+
type: Association
|
|
701
|
+
relation: ManyToOne
|
|
702
|
+
target: Product
|
|
703
|
+
onDelete: RESTRICT
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Multi-tenant System
|
|
707
|
+
|
|
708
|
+
```yaml
|
|
709
|
+
# Tenant.yaml
|
|
710
|
+
name: Tenant
|
|
711
|
+
properties:
|
|
712
|
+
name:
|
|
713
|
+
type: String
|
|
714
|
+
users:
|
|
715
|
+
type: Association
|
|
716
|
+
relation: OneToMany
|
|
717
|
+
target: User
|
|
718
|
+
inversedBy: tenant
|
|
719
|
+
projects:
|
|
720
|
+
type: Association
|
|
721
|
+
relation: OneToMany
|
|
722
|
+
target: Project
|
|
723
|
+
inversedBy: tenant
|
|
724
|
+
|
|
725
|
+
# User.yaml
|
|
726
|
+
name: User
|
|
727
|
+
properties:
|
|
728
|
+
name:
|
|
729
|
+
type: String
|
|
730
|
+
tenant:
|
|
731
|
+
type: Association
|
|
732
|
+
relation: ManyToOne
|
|
733
|
+
target: Tenant
|
|
734
|
+
onDelete: CASCADE
|
|
735
|
+
|
|
736
|
+
# Project.yaml
|
|
737
|
+
name: Project
|
|
738
|
+
properties:
|
|
739
|
+
name:
|
|
740
|
+
type: String
|
|
741
|
+
tenant:
|
|
742
|
+
type: Association
|
|
743
|
+
relation: ManyToOne
|
|
744
|
+
target: Tenant
|
|
745
|
+
onDelete: CASCADE
|
|
746
|
+
members:
|
|
747
|
+
type: Association
|
|
748
|
+
relation: ManyToMany
|
|
749
|
+
target: User
|
|
750
|
+
inversedBy: projects
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
## See Also
|
|
756
|
+
|
|
757
|
+
- [Schema Format](./schema-format.md)
|
|
758
|
+
- [Property Types](./property-types.md)
|
|
759
|
+
- [Migration Generation](./migrations.md)
|
|
760
|
+
- [TypeScript Generation](./typescript.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@famgia/omnify",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Schema-driven database migration system with TypeScript types and Laravel migrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
21
|
"bin",
|
|
22
|
+
"docs",
|
|
22
23
|
"README.md"
|
|
23
24
|
],
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@famgia/omnify-core": "0.0.7",
|
|
26
26
|
"@famgia/omnify-cli": "0.0.9",
|
|
27
|
+
"@famgia/omnify-core": "0.0.7",
|
|
27
28
|
"@famgia/omnify-types": "0.0.7",
|
|
28
29
|
"@famgia/omnify-laravel": "0.0.9",
|
|
29
30
|
"@famgia/omnify-atlas": "0.0.7"
|