@holoyan/adonisjs-polymorphic 0.1.0
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 +549 -0
- package/build/configure.d.ts +10 -0
- package/build/configure.d.ts.map +1 -0
- package/build/configure.js +27 -0
- package/build/configure.js.map +1 -0
- package/build/providers/plugin_provider.d.ts +15 -0
- package/build/providers/plugin_provider.d.ts.map +1 -0
- package/build/providers/plugin_provider.js +21 -0
- package/build/providers/plugin_provider.js.map +1 -0
- package/build/src/decorators.d.ts +41 -0
- package/build/src/decorators.d.ts.map +1 -0
- package/build/src/decorators.js +64 -0
- package/build/src/decorators.js.map +1 -0
- package/build/src/define_config.d.ts +20 -0
- package/build/src/define_config.d.ts.map +1 -0
- package/build/src/define_config.js +8 -0
- package/build/src/define_config.js.map +1 -0
- package/build/src/index.d.ts +16 -0
- package/build/src/index.d.ts.map +1 -0
- package/build/src/index.js +15 -0
- package/build/src/index.js.map +1 -0
- package/build/src/relations/morph_many/index.d.ts +39 -0
- package/build/src/relations/morph_many/index.d.ts.map +1 -0
- package/build/src/relations/morph_many/index.js +110 -0
- package/build/src/relations/morph_many/index.js.map +1 -0
- package/build/src/relations/morph_many/query_builder.d.ts +22 -0
- package/build/src/relations/morph_many/query_builder.d.ts.map +1 -0
- package/build/src/relations/morph_many/query_builder.js +80 -0
- package/build/src/relations/morph_many/query_builder.js.map +1 -0
- package/build/src/relations/morph_many/query_client.d.ts +40 -0
- package/build/src/relations/morph_many/query_client.d.ts.map +1 -0
- package/build/src/relations/morph_many/query_client.js +115 -0
- package/build/src/relations/morph_many/query_client.js.map +1 -0
- package/build/src/relations/morph_one/index.d.ts +68 -0
- package/build/src/relations/morph_one/index.d.ts.map +1 -0
- package/build/src/relations/morph_one/index.js +147 -0
- package/build/src/relations/morph_one/index.js.map +1 -0
- package/build/src/relations/morph_one/query_builder.d.ts +22 -0
- package/build/src/relations/morph_one/query_builder.d.ts.map +1 -0
- package/build/src/relations/morph_one/query_builder.js +79 -0
- package/build/src/relations/morph_one/query_builder.js.map +1 -0
- package/build/src/relations/morph_one/query_client.d.ts +41 -0
- package/build/src/relations/morph_one/query_client.d.ts.map +1 -0
- package/build/src/relations/morph_one/query_client.js +94 -0
- package/build/src/relations/morph_one/query_client.js.map +1 -0
- package/build/src/relations/morph_to/eager_loader.d.ts +29 -0
- package/build/src/relations/morph_to/eager_loader.d.ts.map +1 -0
- package/build/src/relations/morph_to/eager_loader.js +78 -0
- package/build/src/relations/morph_to/eager_loader.js.map +1 -0
- package/build/src/relations/morph_to/index.d.ts +76 -0
- package/build/src/relations/morph_to/index.d.ts.map +1 -0
- package/build/src/relations/morph_to/index.js +172 -0
- package/build/src/relations/morph_to/index.js.map +1 -0
- package/build/src/relations/morph_to/query_client.d.ts +27 -0
- package/build/src/relations/morph_to/query_client.d.ts.map +1 -0
- package/build/src/relations/morph_to/query_client.js +64 -0
- package/build/src/relations/morph_to/query_client.js.map +1 -0
- package/build/src/relations/morph_to/registry.d.ts +2 -0
- package/build/src/relations/morph_to/registry.d.ts.map +1 -0
- package/build/src/relations/morph_to/registry.js +13 -0
- package/build/src/relations/morph_to/registry.js.map +1 -0
- package/build/src/relations/shared/query_builder.d.ts +55 -0
- package/build/src/relations/shared/query_builder.d.ts.map +1 -0
- package/build/src/relations/shared/query_builder.js +70 -0
- package/build/src/relations/shared/query_builder.js.map +1 -0
- package/build/src/types.d.ts +84 -0
- package/build/src/types.d.ts.map +1 -0
- package/build/src/types.js +2 -0
- package/build/src/types.js.map +1 -0
- package/build/stubs/config/polymorphic.stub +19 -0
- package/build/stubs/main.d.ts +6 -0
- package/build/stubs/main.d.ts.map +1 -0
- package/build/stubs/main.js +6 -0
- package/build/stubs/main.js.map +1 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# @holoyan/adonisjs-polymorphic
|
|
2
|
+
|
|
3
|
+
Polymorphic relations for [AdonisJS Lucid ORM](https://lucid.adonisjs.com) — `morphOne`, `morphMany`, and `morphTo`.
|
|
4
|
+
|
|
5
|
+
| Package version | AdonisJS version |
|
|
6
|
+
|---|---|
|
|
7
|
+
| v0.x | v6 + v7 |
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## How can you support me?
|
|
12
|
+
|
|
13
|
+
It's simple — just star this repository. That is enough to keep me motivated to maintain this package.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Related packages
|
|
18
|
+
|
|
19
|
+
- [@holoyan/adonisjs-permissions](https://github.com/holoyan/adonisjs-permissions) — Role & permission system for AdonisJS. Supports multi-model ACL, resource-level permissions, scopes (multi-tenancy), and events.
|
|
20
|
+
- [@holoyan/morph-map-js](https://github.com/holoyan/morph-map-js) — The framework-agnostic morph map registry that powers the `@MorphMap` decorator used by this package.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [What are polymorphic relations?](#what-are-polymorphic-relations)
|
|
28
|
+
- [morphOne](#morphone)
|
|
29
|
+
- [morphMany](#morphmany)
|
|
30
|
+
- [morphTo](#morphto)
|
|
31
|
+
- [Global morph map with @MorphMap](#global-morph-map-with-morphmap)
|
|
32
|
+
- [Options reference](#options-reference)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @holoyan/adonisjs-polymorphic
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Register the service provider by running the configure command:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node ace configure @holoyan/adonisjs-polymorphic
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This automatically adds the provider to your `adonisrc.ts`.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## What are polymorphic relations?
|
|
53
|
+
|
|
54
|
+
A polymorphic relation lets a single child model belong to more than one parent model using a shared pair of columns — a **type** column and an **id** column.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
images
|
|
58
|
+
id
|
|
59
|
+
url
|
|
60
|
+
imageable_type ← 'posts' | 'videos'
|
|
61
|
+
imageable_id ← id of the parent row
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This means a single `images` table can store thumbnails for both posts and videos without needing separate `post_images` and `video_images` tables.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## morphOne
|
|
69
|
+
|
|
70
|
+
A parent model **has one** polymorphic child.
|
|
71
|
+
|
|
72
|
+
### Database migration
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
await schema.createTable('images', (table) => {
|
|
76
|
+
table.increments('id')
|
|
77
|
+
table.string('url').notNullable()
|
|
78
|
+
table.string('imageable_type').notNullable()
|
|
79
|
+
table.integer('imageable_id').notNullable()
|
|
80
|
+
table.index(['imageable_type', 'imageable_id'])
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Model setup
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// app/models/image.ts
|
|
88
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
|
89
|
+
import { morphTo } from '@holoyan/adonisjs-polymorphic'
|
|
90
|
+
import Post from '#models/post'
|
|
91
|
+
import Video from '#models/video'
|
|
92
|
+
|
|
93
|
+
export default class Image extends BaseModel {
|
|
94
|
+
@column({ isPrimary: true })
|
|
95
|
+
declare id: number
|
|
96
|
+
|
|
97
|
+
@column()
|
|
98
|
+
declare url: string
|
|
99
|
+
|
|
100
|
+
@column()
|
|
101
|
+
declare imageableType: string
|
|
102
|
+
|
|
103
|
+
@column()
|
|
104
|
+
declare imageableId: number
|
|
105
|
+
|
|
106
|
+
@morphTo({ name: 'imageable', morphMap: { posts: () => Post, videos: () => Video } })
|
|
107
|
+
declare imageable: Post | Video | null
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// app/models/post.ts
|
|
113
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
|
114
|
+
import { morphOne } from '@holoyan/adonisjs-polymorphic'
|
|
115
|
+
import Image from '#models/image'
|
|
116
|
+
|
|
117
|
+
export default class Post extends BaseModel {
|
|
118
|
+
@column({ isPrimary: true })
|
|
119
|
+
declare id: number
|
|
120
|
+
|
|
121
|
+
@column()
|
|
122
|
+
declare title: string
|
|
123
|
+
|
|
124
|
+
@morphOne(() => Image, { name: 'imageable' })
|
|
125
|
+
declare image: Image | null
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// app/models/video.ts
|
|
131
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
|
132
|
+
import { morphOne } from '@holoyan/adonisjs-polymorphic'
|
|
133
|
+
import Image from '#models/image'
|
|
134
|
+
|
|
135
|
+
export default class Video extends BaseModel {
|
|
136
|
+
@column({ isPrimary: true })
|
|
137
|
+
declare id: number
|
|
138
|
+
|
|
139
|
+
@column()
|
|
140
|
+
declare title: string
|
|
141
|
+
|
|
142
|
+
@morphOne(() => Image, { name: 'imageable' })
|
|
143
|
+
declare image: Image | null
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Querying
|
|
148
|
+
|
|
149
|
+
**Eager load (preload):**
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const post = await (Post.query() as any)
|
|
153
|
+
.preload('image')
|
|
154
|
+
.firstOrFail()
|
|
155
|
+
|
|
156
|
+
console.log(post.image) // Image | null
|
|
157
|
+
console.log(post.image?.url) // 'photo.jpg'
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Preload multiple parents at once:**
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const posts = await (Post.query() as any)
|
|
164
|
+
.preload('image') as Post[]
|
|
165
|
+
|
|
166
|
+
// One SQL query — no N+1
|
|
167
|
+
// SELECT * FROM images WHERE imageable_type = 'posts' AND imageable_id IN (1, 2, 3)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Ad-hoc query:**
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const image = await (post as any)
|
|
174
|
+
.related('image')
|
|
175
|
+
.query()
|
|
176
|
+
.firstOrFail()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Writing
|
|
180
|
+
|
|
181
|
+
**Create a related image:**
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// imageableType and imageableId are set automatically
|
|
185
|
+
const image = await (post as any)
|
|
186
|
+
.related('image')
|
|
187
|
+
.create({ url: 'photo.jpg' })
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Save an existing image instance:**
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const image = new Image()
|
|
194
|
+
image.url = 'photo.jpg'
|
|
195
|
+
|
|
196
|
+
await (post as any).related('image').save(image)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Find or create:**
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const image = await (post as any)
|
|
203
|
+
.related('image')
|
|
204
|
+
.firstOrCreate({ url: 'photo.jpg' })
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Update or create:**
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const image = await (post as any)
|
|
211
|
+
.related('image')
|
|
212
|
+
.updateOrCreate({ imageableId: post.id }, { url: 'new-photo.jpg' })
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## morphMany
|
|
218
|
+
|
|
219
|
+
A parent model **has many** polymorphic children. Works exactly like `morphOne` but returns an array.
|
|
220
|
+
|
|
221
|
+
### Database migration
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
await schema.createTable('comments', (table) => {
|
|
225
|
+
table.increments('id')
|
|
226
|
+
table.text('body').notNullable()
|
|
227
|
+
table.string('commentable_type').nullable()
|
|
228
|
+
table.integer('commentable_id').nullable()
|
|
229
|
+
table.index(['commentable_type', 'commentable_id'])
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Model setup
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// app/models/comment.ts
|
|
237
|
+
import { BaseModel, column } from '@adonisjs/lucid/orm'
|
|
238
|
+
import { morphTo } from '@holoyan/adonisjs-polymorphic'
|
|
239
|
+
import Post from '#models/post'
|
|
240
|
+
import Video from '#models/video'
|
|
241
|
+
|
|
242
|
+
export default class Comment extends BaseModel {
|
|
243
|
+
@column({ isPrimary: true })
|
|
244
|
+
declare id: number
|
|
245
|
+
|
|
246
|
+
@column()
|
|
247
|
+
declare body: string
|
|
248
|
+
|
|
249
|
+
@column()
|
|
250
|
+
declare commentableType: string
|
|
251
|
+
|
|
252
|
+
@column()
|
|
253
|
+
declare commentableId: number
|
|
254
|
+
|
|
255
|
+
@morphTo({ name: 'commentable', morphMap: { posts: () => Post, videos: () => Video } })
|
|
256
|
+
declare commentable: Post | Video | null
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// app/models/post.ts
|
|
262
|
+
import { morphOne, morphMany } from '@holoyan/adonisjs-polymorphic'
|
|
263
|
+
import Image from '#models/image'
|
|
264
|
+
import Comment from '#models/comment'
|
|
265
|
+
|
|
266
|
+
export default class Post extends BaseModel {
|
|
267
|
+
// ...
|
|
268
|
+
|
|
269
|
+
@morphOne(() => Image, { name: 'imageable' })
|
|
270
|
+
declare image: Image | null
|
|
271
|
+
|
|
272
|
+
@morphMany(() => Comment, { name: 'commentable' })
|
|
273
|
+
declare comments: Comment[]
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Querying
|
|
278
|
+
|
|
279
|
+
**Eager load:**
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
const post = await (Post.query() as any)
|
|
283
|
+
.preload('comments')
|
|
284
|
+
.firstOrFail()
|
|
285
|
+
|
|
286
|
+
console.log(post.comments) // Comment[]
|
|
287
|
+
console.log(post.comments.length) // 3
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Comments are isolated by type — a post only gets its own comments, not a video's:**
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const post = await (Post.query() as any).preload('comments').firstOrFail()
|
|
294
|
+
const video = await (Video.query() as any).preload('comments').firstOrFail()
|
|
295
|
+
|
|
296
|
+
// Each only sees their own comments
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Ad-hoc query with additional constraints:**
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const recentComments = await (post as any)
|
|
303
|
+
.related('comments')
|
|
304
|
+
.query()
|
|
305
|
+
.orderBy('created_at', 'desc')
|
|
306
|
+
.limit(5)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Writing
|
|
310
|
+
|
|
311
|
+
**Create one:**
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const comment = await (post as any)
|
|
315
|
+
.related('comments')
|
|
316
|
+
.create({ body: 'Great post!' })
|
|
317
|
+
|
|
318
|
+
console.log(comment.commentableType) // 'posts'
|
|
319
|
+
console.log(comment.commentableId) // post.id
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Create many:**
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
await (post as any).related('comments').createMany([
|
|
326
|
+
{ body: 'First comment' },
|
|
327
|
+
{ body: 'Second comment' },
|
|
328
|
+
])
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Save an existing instance:**
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
const comment = new Comment()
|
|
335
|
+
comment.body = 'Hello'
|
|
336
|
+
|
|
337
|
+
await (post as any).related('comments').save(comment)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Save many:**
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
await (post as any).related('comments').saveMany([comment1, comment2])
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## morphTo
|
|
349
|
+
|
|
350
|
+
The child side of a polymorphic relation. A comment **belongs to** either a `Post` or a `Video`.
|
|
351
|
+
|
|
352
|
+
### Querying
|
|
353
|
+
|
|
354
|
+
**Preload the parent:**
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
const comment = await (Comment.query() as any)
|
|
358
|
+
.preload('commentable')
|
|
359
|
+
.firstOrFail()
|
|
360
|
+
|
|
361
|
+
if (comment.commentable instanceof Post) {
|
|
362
|
+
console.log('belongs to a post:', comment.commentable.title)
|
|
363
|
+
} else if (comment.commentable instanceof Video) {
|
|
364
|
+
console.log('belongs to a video:', comment.commentable.title)
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Preload mixed parent types in one query:**
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
// All comments in one query, parents resolved in two queries (posts + videos)
|
|
372
|
+
const comments = await (Comment.query() as any)
|
|
373
|
+
.preload('commentable') as Comment[]
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Ad-hoc query:**
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
const parent = await (comment as any)
|
|
380
|
+
.related('commentable')
|
|
381
|
+
.query()
|
|
382
|
+
.firstOrFail()
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Writing
|
|
386
|
+
|
|
387
|
+
**Associate with a parent:**
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const post = await Post.findOrFail(1)
|
|
391
|
+
await (comment as any).related('commentable').associate(post)
|
|
392
|
+
|
|
393
|
+
// comment.commentableType is now 'posts'
|
|
394
|
+
// comment.commentableId is now post.id
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Dissociate from parent:**
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
await (comment as any).related('commentable').dissociate()
|
|
401
|
+
|
|
402
|
+
// comment.commentableType is now null
|
|
403
|
+
// comment.commentableId is now null
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Global morph map with @MorphMap
|
|
409
|
+
|
|
410
|
+
When you have many `morphTo` relations, repeating `morphMap: { posts: () => Post, videos: () => Video }` on each one gets tedious. Use the `@MorphMap` decorator from `@holoyan/morph-map-js` (bundled as a dependency) to register each model once globally.
|
|
411
|
+
|
|
412
|
+
### Setup
|
|
413
|
+
|
|
414
|
+
Decorate each parent model with its alias:
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
// app/models/post.ts
|
|
418
|
+
import { MorphMap } from '@holoyan/morph-map-js'
|
|
419
|
+
|
|
420
|
+
@MorphMap('posts')
|
|
421
|
+
export default class Post extends BaseModel {
|
|
422
|
+
// ...
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
// app/models/video.ts
|
|
428
|
+
import { MorphMap } from '@holoyan/morph-map-js'
|
|
429
|
+
|
|
430
|
+
@MorphMap('videos')
|
|
431
|
+
export default class Video extends BaseModel {
|
|
432
|
+
// ...
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Now `morphTo` relations can omit the `morphMap` option entirely:
|
|
437
|
+
|
|
438
|
+
```ts
|
|
439
|
+
// app/models/comment.ts
|
|
440
|
+
import { morphTo } from '@holoyan/adonisjs-polymorphic'
|
|
441
|
+
|
|
442
|
+
export default class Comment extends BaseModel {
|
|
443
|
+
@column()
|
|
444
|
+
declare commentableType: string
|
|
445
|
+
|
|
446
|
+
@column()
|
|
447
|
+
declare commentableId: number
|
|
448
|
+
|
|
449
|
+
// No morphMap needed — resolved from global registry at query time
|
|
450
|
+
@morphTo({ name: 'commentable' })
|
|
451
|
+
declare commentable: Post | Video | null
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Adding a new parent type (e.g. `Podcast`) only requires one change:
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
@MorphMap('podcasts')
|
|
459
|
+
export default class Podcast extends BaseModel {}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
All existing `morphTo` relations pick it up automatically.
|
|
463
|
+
|
|
464
|
+
### Alias vs table name
|
|
465
|
+
|
|
466
|
+
The `@MorphMap` alias is also used as the `morphValue` stored in the type column. This lets you decouple the alias from the table name:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
@MorphMap('post') // alias stored in type column
|
|
470
|
+
export default class Post extends BaseModel {
|
|
471
|
+
static table = 'posts' // actual DB table
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
@morphOne(() => Image, { name: 'imageable' })
|
|
477
|
+
// morphValue will be 'post' (from @MorphMap), not 'posts' (from table)
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Ensuring models are registered at boot time
|
|
481
|
+
|
|
482
|
+
The global registry is populated when a model file is **imported**. To guarantee the registry is fully populated before any request, seeder, or test query runs, register your parent models in `config/polymorphic.ts` (published automatically by `node ace configure`):
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
// config/polymorphic.ts
|
|
486
|
+
import { defineConfig } from '@holoyan/adonisjs-polymorphic'
|
|
487
|
+
|
|
488
|
+
export default defineConfig({
|
|
489
|
+
morphModels: [
|
|
490
|
+
() => import('#models/post'),
|
|
491
|
+
() => import('#models/video'),
|
|
492
|
+
() => import('#models/podcast'), // add new parent models here
|
|
493
|
+
],
|
|
494
|
+
})
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The service provider imports all listed models during `boot()` — before the app serves any request, before seeders run, before tests execute. This completely eliminates any load order concerns.
|
|
498
|
+
|
|
499
|
+
Every time you add a new model decorated with `@MorphMap`, add it to this list.
|
|
500
|
+
|
|
501
|
+
### Explicit morphMap always wins
|
|
502
|
+
|
|
503
|
+
You can always override the registry on a per-relation basis:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
@morphTo({
|
|
507
|
+
name: 'commentable',
|
|
508
|
+
morphMap: { posts: () => Post }, // only posts, ignores registry
|
|
509
|
+
})
|
|
510
|
+
declare commentable: Post | null
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Priority order
|
|
514
|
+
|
|
515
|
+
| What's set | morphTo resolution | morphOne/morphMany morphValue |
|
|
516
|
+
|---|---|---|
|
|
517
|
+
| Explicit `morphMap` option | Used directly | — |
|
|
518
|
+
| `morphValue` option | — | Used directly |
|
|
519
|
+
| `@MorphMap` on model | Registry fallback | Registry alias |
|
|
520
|
+
| Nothing | Error at query time | `model.table` |
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Options reference
|
|
525
|
+
|
|
526
|
+
### `@morphOne(relatedModel, options)`
|
|
527
|
+
|
|
528
|
+
| Option | Type | Default | Description |
|
|
529
|
+
|---|---|---|---|
|
|
530
|
+
| `name` | `string` | **required** | Prefix for the type/id columns on the related model. `'imageable'` → `imageableType` + `imageableId` |
|
|
531
|
+
| `localKey` | `string` | primary key | Attribute on the parent used to match against the id column |
|
|
532
|
+
| `morphValue` | `string` | `@MorphMap` alias or `model.table` | Value stored in the type column to identify this parent |
|
|
533
|
+
| `serializeAs` | `string \| null` | relation name | Key used during serialization. `null` excludes it |
|
|
534
|
+
| `onQuery` | `(query) => void` | — | Hook to add default constraints to every query on this relation |
|
|
535
|
+
|
|
536
|
+
### `@morphMany(relatedModel, options)`
|
|
537
|
+
|
|
538
|
+
Same options as `@morphOne`.
|
|
539
|
+
|
|
540
|
+
### `@morphTo(options)`
|
|
541
|
+
|
|
542
|
+
| Option | Type | Default | Description |
|
|
543
|
+
|---|---|---|---|
|
|
544
|
+
| `name` | `string` | relation name | Prefix used to derive the type/id attribute names on this model |
|
|
545
|
+
| `morphMap` | `Record<string, () => Model>` | global registry | Maps type strings to model factories. Optional when `@MorphMap` is used |
|
|
546
|
+
| `typeKey` | `string` | `${name}Type` | Explicit attribute name for the type column if it doesn't follow the naming convention |
|
|
547
|
+
| `idKey` | `string` | `${name}Id` | Explicit attribute name for the id column if it doesn't follow the naming convention |
|
|
548
|
+
| `serializeAs` | `string \| null` | relation name | Key used during serialization. `null` excludes it |
|
|
549
|
+
| `onQuery` | `(query) => void` | — | Hook to add default constraints |
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type Configure from '@adonisjs/core/commands/configure';
|
|
2
|
+
/**
|
|
3
|
+
* Configure hook — runs when the user executes:
|
|
4
|
+
* node ace configure @holoyan/adonisjs-polymorphic
|
|
5
|
+
*
|
|
6
|
+
* - Registers the service provider in adonisrc.ts
|
|
7
|
+
* - Publishes config/polymorphic.ts
|
|
8
|
+
*/
|
|
9
|
+
export declare function configure(command: Configure): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=configure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../configure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,mCAAmC,CAAA;AAG9D;;;;;;GAMG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,SAAS,iBAoBjD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { stubsRoot } from './stubs/main.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configure hook — runs when the user executes:
|
|
4
|
+
* node ace configure @holoyan/adonisjs-polymorphic
|
|
5
|
+
*
|
|
6
|
+
* - Registers the service provider in adonisrc.ts
|
|
7
|
+
* - Publishes config/polymorphic.ts
|
|
8
|
+
*/
|
|
9
|
+
export async function configure(command) {
|
|
10
|
+
const codemods = await command.createCodemods();
|
|
11
|
+
await codemods.updateRcFile((rcFile) => {
|
|
12
|
+
rcFile.addProvider('@holoyan/adonisjs-polymorphic/provider');
|
|
13
|
+
});
|
|
14
|
+
await codemods.makeUsingStub(stubsRoot, 'config/polymorphic.stub', {});
|
|
15
|
+
command.logger.log('');
|
|
16
|
+
command.logger.log(' Install complete. Next steps:');
|
|
17
|
+
command.logger.log('');
|
|
18
|
+
command.logger.log(' 1. Decorate your parent models with @MorphMap:');
|
|
19
|
+
command.logger.log(' @MorphMap(\'posts\') export default class Post extends BaseModel {}');
|
|
20
|
+
command.logger.log('');
|
|
21
|
+
command.logger.log(' 2. Register them in config/polymorphic.ts:');
|
|
22
|
+
command.logger.log(' morphModels: [() => import(\'#models/post\')]');
|
|
23
|
+
command.logger.log('');
|
|
24
|
+
command.logger.log(' 3. Use @morphOne, @morphMany, @morphTo in your models.');
|
|
25
|
+
command.logger.log('');
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=configure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configure.js","sourceRoot":"","sources":["../configure.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAkB;IAChD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,cAAc,EAAE,CAAA;IAE/C,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,WAAW,CAAC,wCAAwC,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,MAAM,QAAQ,CAAC,aAAa,CAAC,SAAS,EAAE,yBAAyB,EAAE,EAAE,CAAC,CAAA;IAEtE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACtB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAA;IACrD,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACtB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,kDAAkD,CAAC,CAAA;IACtE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,4EAA4E,CAAC,CAAA;IAChG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACtB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAA;IAClE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,sDAAsD,CAAC,CAAA;IAC1E,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACtB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,0DAA0D,CAAC,CAAA;IAC9E,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AACxB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Service provider for @holoyan/adonisjs-polymorphic.
|
|
4
|
+
*
|
|
5
|
+
* During boot, imports every model listed in config/polymorphic.ts so that
|
|
6
|
+
* @MorphMap decorators run and the global registry is fully populated before
|
|
7
|
+
* any request, command, or test query executes.
|
|
8
|
+
*/
|
|
9
|
+
export default class PolymorphicProvider {
|
|
10
|
+
protected app: ApplicationService;
|
|
11
|
+
constructor(app: ApplicationService);
|
|
12
|
+
register(): void;
|
|
13
|
+
boot(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=plugin_provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin_provider.d.ts","sourceRoot":"","sources":["../../providers/plugin_provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAG9D;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAC1B,SAAS,CAAC,GAAG,EAAE,kBAAkB;gBAAvB,GAAG,EAAE,kBAAkB;IAE7C,QAAQ;IAEF,IAAI;CAOX"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service provider for @holoyan/adonisjs-polymorphic.
|
|
3
|
+
*
|
|
4
|
+
* During boot, imports every model listed in config/polymorphic.ts so that
|
|
5
|
+
* @MorphMap decorators run and the global registry is fully populated before
|
|
6
|
+
* any request, command, or test query executes.
|
|
7
|
+
*/
|
|
8
|
+
export default class PolymorphicProvider {
|
|
9
|
+
app;
|
|
10
|
+
constructor(app) {
|
|
11
|
+
this.app = app;
|
|
12
|
+
}
|
|
13
|
+
register() { }
|
|
14
|
+
async boot() {
|
|
15
|
+
const config = this.app.config.get('polymorphic', {});
|
|
16
|
+
if (config.morphModels?.length) {
|
|
17
|
+
await Promise.all(config.morphModels.map((factory) => factory()));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=plugin_provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin_provider.js","sourceRoot":"","sources":["../../providers/plugin_provider.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,mBAAmB;IAChB;IAAtB,YAAsB,GAAuB;QAAvB,QAAG,GAAH,GAAG,CAAoB;IAAG,CAAC;IAEjD,QAAQ,KAAI,CAAC;IAEb,KAAK,CAAC,IAAI;QACR,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,aAAa,EAAE,EAAE,CAAC,CAAA;QAExE,IAAI,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC;YAC/B,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QACnE,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MorphOneOptions, MorphManyOptions, MorphToOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decorator to define a morphOne (has-one polymorphic) relation on a parent model.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* class Post extends BaseModel {
|
|
8
|
+
* @morphOne(() => Image, { name: 'imageable' })
|
|
9
|
+
* declare image: Image | null
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function morphOne(relatedModelFactory: () => any, options: MorphOneOptions): (target: any, relationName: string) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Decorator to define a morphMany (has-many polymorphic) relation on a parent model.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* class Post extends BaseModel {
|
|
20
|
+
* @morphMany(() => Comment, { name: 'commentable' })
|
|
21
|
+
* declare comments: Comment[]
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function morphMany(relatedModelFactory: () => any, options: MorphManyOptions): (target: any, relationName: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Decorator to define a morphTo (belongs-to polymorphic) relation on a child model.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* class Comment extends BaseModel {
|
|
32
|
+
* @column() declare commentableType: string
|
|
33
|
+
* @column() declare commentableId: number
|
|
34
|
+
*
|
|
35
|
+
* @morphTo({ name: 'commentable', morphMap: { posts: () => Post, videos: () => Video } })
|
|
36
|
+
* declare commentable: Post | Video | null
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function morphTo(options: MorphToOptions): (target: any, relationName: string) => void;
|
|
41
|
+
//# sourceMappingURL=decorators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../../src/decorators.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEnF;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,mBAAmB,EAAE,MAAM,GAAG,EAAE,OAAO,EAAE,eAAe,IAC5C,QAAQ,GAAG,EAAE,cAAc,MAAM,UAMrE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,mBAAmB,EAAE,MAAM,GAAG,EAAE,OAAO,EAAE,gBAAgB,IAC7C,QAAQ,GAAG,EAAE,cAAc,MAAM,UAMtE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,cAAc,IACX,QAAQ,GAAG,EAAE,cAAc,MAAM,UAMpE"}
|