@rudderjs/orm 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +604 -0
- package/boost/guidelines.md +234 -0
- package/dist/attribute.d.ts +36 -0
- package/dist/attribute.d.ts.map +1 -0
- package/dist/attribute.js +36 -0
- package/dist/attribute.js.map +1 -0
- package/dist/cast.d.ts +14 -0
- package/dist/cast.d.ts.map +1 -0
- package/dist/cast.js +85 -0
- package/dist/cast.js.map +1 -0
- package/dist/collection.d.ts +73 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +152 -0
- package/dist/collection.js.map +1 -0
- package/dist/factory.d.ts +80 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +129 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +195 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +450 -0
- package/dist/index.js.map +1 -0
- package/dist/resource.d.ts +95 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +115 -0
- package/dist/resource.js.map +1 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Suleiman Shahbari
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
# @rudderjs/orm
|
|
2
|
+
|
|
3
|
+
ORM contract, `Model` base class, and `ModelRegistry` for RudderJS applications.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @rudderjs/orm
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
This package provides the shared abstractions. For a working database connection use an adapter:
|
|
10
|
+
|
|
11
|
+
- `@rudderjs/orm-prisma` — Prisma adapter (SQLite, PostgreSQL, MySQL)
|
|
12
|
+
- `@rudderjs/orm-drizzle` — Drizzle adapter (SQLite, PostgreSQL, LibSQL)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
Register a database provider in `bootstrap/providers.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { database } from '@rudderjs/orm-prisma'
|
|
22
|
+
import configs from '../config/index.js'
|
|
23
|
+
|
|
24
|
+
export default [
|
|
25
|
+
database(configs.database),
|
|
26
|
+
// ...other providers
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The provider calls `ModelRegistry.set(adapter)` during boot — no manual wiring needed.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Defining a Model
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Model } from '@rudderjs/orm'
|
|
38
|
+
|
|
39
|
+
export class User extends Model {
|
|
40
|
+
static override table = 'users' // optional — defaults to lowercase class name + 's'
|
|
41
|
+
static override fillable = ['name', 'email', 'role']
|
|
42
|
+
static override hidden = ['password']
|
|
43
|
+
|
|
44
|
+
declare id: number
|
|
45
|
+
declare name: string
|
|
46
|
+
declare email: string
|
|
47
|
+
declare password: string
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Querying
|
|
54
|
+
|
|
55
|
+
All static query methods delegate to the registered adapter's `QueryBuilder`.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// Find by primary key — returns null if not found
|
|
59
|
+
const user = await User.find(1)
|
|
60
|
+
|
|
61
|
+
// Fetch all rows
|
|
62
|
+
const users = await User.all()
|
|
63
|
+
|
|
64
|
+
// Conditional query — returns a chainable QueryBuilder
|
|
65
|
+
const admins = await User.where('role', 'admin').get()
|
|
66
|
+
|
|
67
|
+
// Eager-load relations
|
|
68
|
+
const posts = await Post.with('author', 'tags').get()
|
|
69
|
+
|
|
70
|
+
// Raw query builder
|
|
71
|
+
const recent = await User.query()
|
|
72
|
+
.orderBy('createdAt', 'desc')
|
|
73
|
+
.limit(10)
|
|
74
|
+
.get()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### QueryBuilder methods
|
|
78
|
+
|
|
79
|
+
| Method | Returns | Description |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `where(col, val)` | `QueryBuilder` | Add a WHERE clause |
|
|
82
|
+
| `orWhere(col, val)` | `QueryBuilder` | Add an OR WHERE clause |
|
|
83
|
+
| `orderBy(col, dir)` | `QueryBuilder` | Add ORDER BY |
|
|
84
|
+
| `limit(n)` | `QueryBuilder` | Limit result count |
|
|
85
|
+
| `offset(n)` | `QueryBuilder` | Skip n rows |
|
|
86
|
+
| `with(...rels)` | `QueryBuilder` | Eager-load relations |
|
|
87
|
+
| `scope(name, ...args)` | `QueryBuilder` | Apply a local scope defined in `static scopes` |
|
|
88
|
+
| `withoutGlobalScope(name)` | `QueryBuilder` | Rebuild the query excluding a named global scope |
|
|
89
|
+
| `first()` | `Promise<T \| null>` | First matching row |
|
|
90
|
+
| `find(id)` | `Promise<T \| null>` | Find by primary key |
|
|
91
|
+
| `get()` | `Promise<T[]>` | All matching rows |
|
|
92
|
+
| `all()` | `Promise<T[]>` | All rows (no conditions) |
|
|
93
|
+
| `count()` | `Promise<number>` | Row count |
|
|
94
|
+
| `create(data)` | `Promise<T>` | Insert a new row |
|
|
95
|
+
| `update(id, data)` | `Promise<T>` | Update a row by primary key |
|
|
96
|
+
| `delete(id)` | `Promise<void>` | Delete a row by primary key |
|
|
97
|
+
| `paginate(page, perPage)` | `Promise<PaginatedResult<T>>` | Paginated results |
|
|
98
|
+
|
|
99
|
+
### Creating records
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const user = await User.create({ name: 'Alice', email: 'alice@example.com' })
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Attribute Casts
|
|
108
|
+
|
|
109
|
+
Casts automatically transform attribute values when reading from and writing to the database.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { Model } from '@rudderjs/orm'
|
|
113
|
+
|
|
114
|
+
class Post extends Model {
|
|
115
|
+
static override casts = {
|
|
116
|
+
isPublished: 'boolean',
|
|
117
|
+
publishedAt: 'date',
|
|
118
|
+
metadata: 'json',
|
|
119
|
+
viewCount: 'integer',
|
|
120
|
+
rating: 'float',
|
|
121
|
+
tags: 'array',
|
|
122
|
+
} as const
|
|
123
|
+
|
|
124
|
+
declare isPublished: boolean
|
|
125
|
+
declare publishedAt: Date
|
|
126
|
+
declare metadata: Record<string, unknown>
|
|
127
|
+
declare viewCount: number
|
|
128
|
+
declare rating: number
|
|
129
|
+
declare tags: string[]
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Built-in cast types
|
|
134
|
+
|
|
135
|
+
| Cast | Get (read) | Set (write) |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `'string'` | `String(v)` | `String(v)` |
|
|
138
|
+
| `'integer'` | `parseInt(v)` | `parseInt(v)` |
|
|
139
|
+
| `'float'` | `parseFloat(v)` | `parseFloat(v)` |
|
|
140
|
+
| `'boolean'` | `true/false` from truthy values | `1` / `0` |
|
|
141
|
+
| `'date'` | `new Date(v)` | `toISOString().slice(0,10)` |
|
|
142
|
+
| `'datetime'` | `new Date(v)` | `toISOString()` |
|
|
143
|
+
| `'json'` | `JSON.parse(v)` | `JSON.stringify(v)` |
|
|
144
|
+
| `'array'` | `JSON.parse(v)` | `JSON.stringify(v)` |
|
|
145
|
+
| `'collection'` | `JSON.parse(v)` (as array) | `JSON.stringify(v)` |
|
|
146
|
+
| `'encrypted'` | Decrypts string | Encrypts string |
|
|
147
|
+
| `'encrypted:array'` | Decrypts + parses JSON | Encrypts JSON |
|
|
148
|
+
| `'encrypted:object'` | Decrypts + parses JSON | Encrypts JSON |
|
|
149
|
+
|
|
150
|
+
Encrypted casts require `@rudderjs/crypt` to be installed.
|
|
151
|
+
|
|
152
|
+
### Custom cast classes
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import type { CastUsing } from '@rudderjs/orm'
|
|
156
|
+
|
|
157
|
+
class MoneyCast implements CastUsing {
|
|
158
|
+
get(key: string, value: unknown) {
|
|
159
|
+
return Number(value) / 100 // cents → dollars
|
|
160
|
+
}
|
|
161
|
+
set(key: string, value: unknown) {
|
|
162
|
+
return Math.round(Number(value) * 100) // dollars → cents
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class Product extends Model {
|
|
167
|
+
static override casts = { price: MoneyCast }
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### `@Cast` decorator
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { Model, Cast } from '@rudderjs/orm'
|
|
175
|
+
|
|
176
|
+
class User extends Model {
|
|
177
|
+
@Cast('boolean') isAdmin = false
|
|
178
|
+
@Cast('date') createdAt = new Date()
|
|
179
|
+
@Cast(MoneyCast) balance = 0
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Accessors & Mutators
|
|
186
|
+
|
|
187
|
+
Define computed getters and write transformations using `Attribute.make()`.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { Model, Attribute } from '@rudderjs/orm'
|
|
191
|
+
|
|
192
|
+
class User extends Model {
|
|
193
|
+
static override attributes = {
|
|
194
|
+
// Accessor — transform on read
|
|
195
|
+
firstName: Attribute.make({
|
|
196
|
+
get: (value) => String(value).charAt(0).toUpperCase() + String(value).slice(1),
|
|
197
|
+
}),
|
|
198
|
+
|
|
199
|
+
// Computed from multiple columns
|
|
200
|
+
fullName: Attribute.make({
|
|
201
|
+
get: (_, attrs) => `${attrs['firstName']} ${attrs['lastName']}`,
|
|
202
|
+
}),
|
|
203
|
+
|
|
204
|
+
// Mutator — transform on write (create/update)
|
|
205
|
+
password: Attribute.make({
|
|
206
|
+
set: (value) => hashSync(String(value)),
|
|
207
|
+
}),
|
|
208
|
+
|
|
209
|
+
// Both accessor and mutator
|
|
210
|
+
email: Attribute.make({
|
|
211
|
+
get: (v) => String(v).toLowerCase(),
|
|
212
|
+
set: (v) => String(v).toLowerCase().trim(),
|
|
213
|
+
}),
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- **Accessors** run in `toJSON()` and transform the raw stored value.
|
|
219
|
+
- **Mutators** run in `Model.create()` and `Model.update()` before data hits the database.
|
|
220
|
+
- Attribute accessors take priority over casts for the same key.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Serialization Controls
|
|
225
|
+
|
|
226
|
+
### `static hidden` / `static visible`
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
class User extends Model {
|
|
230
|
+
static override hidden = ['password', 'rememberToken'] // denylist
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
class PublicUser extends Model {
|
|
234
|
+
static override visible = ['id', 'name', 'avatar'] // allowlist (takes precedence)
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### `static appends`
|
|
239
|
+
|
|
240
|
+
Always include computed accessor values in JSON output:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
class User extends Model {
|
|
244
|
+
static override appends = ['fullName']
|
|
245
|
+
|
|
246
|
+
static override attributes = {
|
|
247
|
+
fullName: Attribute.make({
|
|
248
|
+
get: (_, attrs) => `${attrs['firstName']} ${attrs['lastName']}`,
|
|
249
|
+
}),
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
JSON.stringify(user) // includes "fullName" even though it's not a stored column
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Decorators
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { Model, Hidden, Visible, Appends } from '@rudderjs/orm'
|
|
260
|
+
|
|
261
|
+
class User extends Model {
|
|
262
|
+
@Hidden password = '' // added to static hidden
|
|
263
|
+
@Visible id = 0 // added to static visible
|
|
264
|
+
@Visible name = ''
|
|
265
|
+
@Appends fullName = '' // added to static appends
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Instance-level overrides
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const user = await User.find(1)
|
|
273
|
+
|
|
274
|
+
// Temporarily show hidden fields
|
|
275
|
+
user.makeVisible(['password'])
|
|
276
|
+
|
|
277
|
+
// Temporarily hide fields
|
|
278
|
+
user.makeHidden(['email'])
|
|
279
|
+
|
|
280
|
+
// Replace the lists entirely
|
|
281
|
+
user.setVisible(['id', 'name'])
|
|
282
|
+
user.setHidden(['password', 'token'])
|
|
283
|
+
|
|
284
|
+
// Merge into existing lists
|
|
285
|
+
user.mergeVisible(['avatar'])
|
|
286
|
+
user.mergeHidden(['ssn'])
|
|
287
|
+
|
|
288
|
+
// All return `this` for chaining
|
|
289
|
+
user.makeVisible(['email']).makeHidden(['phone']).toJSON()
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## API Resources
|
|
295
|
+
|
|
296
|
+
Transform model data for API responses with conditional fields and nested resources.
|
|
297
|
+
|
|
298
|
+
### `JsonResource`
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { JsonResource } from '@rudderjs/orm'
|
|
302
|
+
|
|
303
|
+
class UserResource extends JsonResource<User> {
|
|
304
|
+
toArray() {
|
|
305
|
+
return {
|
|
306
|
+
id: this.resource.id,
|
|
307
|
+
name: this.resource.name,
|
|
308
|
+
email: this.resource.email,
|
|
309
|
+
|
|
310
|
+
// Only include when condition is true
|
|
311
|
+
admin: this.when(this.resource.role === 'admin', true),
|
|
312
|
+
|
|
313
|
+
// Only include when value is not null
|
|
314
|
+
bio: this.whenNotNull(this.resource.bio, (b) => b.trim()),
|
|
315
|
+
|
|
316
|
+
// Only include when relation is loaded
|
|
317
|
+
posts: this.whenLoaded('posts'),
|
|
318
|
+
|
|
319
|
+
// Merge multiple fields conditionally
|
|
320
|
+
...this.mergeWhen(this.resource.isAdmin, {
|
|
321
|
+
permissions: this.resource.permissions,
|
|
322
|
+
lastLogin: this.resource.lastLogin,
|
|
323
|
+
}),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Single resource
|
|
329
|
+
const json = new UserResource(user).toArray()
|
|
330
|
+
|
|
331
|
+
// Collection
|
|
332
|
+
const collection = UserResource.collection(users)
|
|
333
|
+
const response = await collection.toResponse()
|
|
334
|
+
// → { data: [...] }
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### `ResourceCollection`
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { ResourceCollection } from '@rudderjs/orm'
|
|
341
|
+
|
|
342
|
+
// With pagination metadata
|
|
343
|
+
const collection = UserResource.collection(users, {
|
|
344
|
+
total: 100, page: 1, perPage: 15,
|
|
345
|
+
})
|
|
346
|
+
const response = await collection.toResponse()
|
|
347
|
+
// → { data: [...], meta: { total: 100, page: 1, perPage: 15 } }
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## ModelCollection
|
|
353
|
+
|
|
354
|
+
Typed array wrapper with ORM-specific operations:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { ModelCollection } from '@rudderjs/orm'
|
|
358
|
+
|
|
359
|
+
const users = ModelCollection.wrap(await User.all())
|
|
360
|
+
|
|
361
|
+
users.modelKeys() // [1, 2, 3]
|
|
362
|
+
users.find(2) // item with id 2
|
|
363
|
+
users.contains(2) // true
|
|
364
|
+
users.contains(u => u.name === 'Alice') // predicate
|
|
365
|
+
users.except([1, 3]) // items not in list
|
|
366
|
+
users.only([1, 2]) // items in list
|
|
367
|
+
users.diff(otherUsers) // items not in other
|
|
368
|
+
users.unique('email') // deduplicated by key
|
|
369
|
+
users.isEmpty() // false
|
|
370
|
+
users.isNotEmpty() // true
|
|
371
|
+
users.count() // 3
|
|
372
|
+
|
|
373
|
+
// Serialization controls on each item
|
|
374
|
+
users.makeVisible(['password'])
|
|
375
|
+
users.makeHidden(['email'])
|
|
376
|
+
|
|
377
|
+
// Async ORM operations
|
|
378
|
+
const fresh = await users.fresh(User) // reload from DB
|
|
379
|
+
const loaded = await users.load(User, 'posts') // eager-load
|
|
380
|
+
const loaded2 = await users.loadMissing(User, 'posts') // load if missing
|
|
381
|
+
const query = users.toQuery(User) // query builder scoped to IDs
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Model Factories
|
|
387
|
+
|
|
388
|
+
Create model instances for testing with named states and sequences.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import { ModelFactory, sequence } from '@rudderjs/orm'
|
|
392
|
+
|
|
393
|
+
class UserFactory extends ModelFactory<{ name: string; email: string; role: string }> {
|
|
394
|
+
protected modelClass = User
|
|
395
|
+
|
|
396
|
+
definition() {
|
|
397
|
+
return {
|
|
398
|
+
name: 'Alice',
|
|
399
|
+
email: sequence(i => `user${i}@example.com`)(),
|
|
400
|
+
role: 'user',
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
protected states() {
|
|
405
|
+
return {
|
|
406
|
+
admin: () => ({ role: 'admin' }),
|
|
407
|
+
banned: () => ({ role: 'banned' }),
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Single record
|
|
413
|
+
const user = await UserFactory.new().create()
|
|
414
|
+
|
|
415
|
+
// With named state
|
|
416
|
+
const admin = await UserFactory.new().state('admin').create()
|
|
417
|
+
|
|
418
|
+
// Multiple records
|
|
419
|
+
const users = await UserFactory.new().create(5)
|
|
420
|
+
|
|
421
|
+
// Without saving to DB
|
|
422
|
+
const dto = await UserFactory.new().make()
|
|
423
|
+
const dtos = await UserFactory.new().make(3)
|
|
424
|
+
|
|
425
|
+
// With overrides
|
|
426
|
+
const custom = await UserFactory.new().create({ name: 'Bob' })
|
|
427
|
+
|
|
428
|
+
// Inline state
|
|
429
|
+
const mod = await UserFactory.new().with(() => ({ role: 'moderator' })).create()
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### `sequence()`
|
|
433
|
+
|
|
434
|
+
Generates cycling or index-based values:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
// Array cycling
|
|
438
|
+
sequence(['Alice', 'Bob', 'Carol']) // returns a function: Alice → Bob → Carol → Alice → ...
|
|
439
|
+
|
|
440
|
+
// Index-based
|
|
441
|
+
sequence(i => `user${i}@example.com`) // user0@... → user1@... → user2@...
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Scopes
|
|
447
|
+
|
|
448
|
+
### Global Scopes
|
|
449
|
+
|
|
450
|
+
Applied automatically to every query on the model:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
export class Article extends Model {
|
|
454
|
+
static globalScopes = {
|
|
455
|
+
ordered: (q) => q.orderBy('createdAt', 'DESC'),
|
|
456
|
+
active: (q) => q.where('active', true),
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
await Article.query().get() // ordered + active
|
|
461
|
+
await Article.query().withoutGlobalScope('active').get() // ordered only
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Local Scopes
|
|
465
|
+
|
|
466
|
+
Reusable query fragments, opt-in via `.scope('name')`:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
export class Article extends Model {
|
|
470
|
+
static scopes = {
|
|
471
|
+
published: (q) => q.where('draftStatus', 'published'),
|
|
472
|
+
recent: (q) => q.where('createdAt', '>', new Date(Date.now() - 30 * 86400000).toISOString()),
|
|
473
|
+
byAuthor: (q, authorId: string) => q.where('authorId', authorId),
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await Article.query().scope('published').scope('recent').get()
|
|
478
|
+
await Article.query().scope('byAuthor', userId).get()
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Soft Deletes
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
class Post extends Model {
|
|
487
|
+
static override softDeletes = true
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
await Post.delete(1) // sets deletedAt
|
|
491
|
+
await Post.restore(1) // clears deletedAt
|
|
492
|
+
await Post.forceDelete(1) // permanent delete
|
|
493
|
+
|
|
494
|
+
// Query helpers
|
|
495
|
+
Post.query().withTrashed().get() // include soft-deleted
|
|
496
|
+
Post.query().onlyTrashed().get() // only soft-deleted
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Observers
|
|
502
|
+
|
|
503
|
+
Register lifecycle hooks on a model to transform data, log events, or cancel operations.
|
|
504
|
+
|
|
505
|
+
### Observer Class
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
class ArticleObserver {
|
|
509
|
+
creating(data) {
|
|
510
|
+
data.slug = slugify(data.title)
|
|
511
|
+
return data // return transformed data
|
|
512
|
+
}
|
|
513
|
+
created(record) { console.log('Article created:', record.id) }
|
|
514
|
+
updating(id, data) { return { ...data, updatedAt: new Date() } }
|
|
515
|
+
deleting(id) { /* return false to cancel */ }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
Article.observe(ArticleObserver)
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Inline Listeners
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
Article.on('creating', (data) => { data.slug = slugify(data.title); return data })
|
|
525
|
+
Article.on('deleting', (id) => { if (id === protectedId) return false })
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Events
|
|
529
|
+
|
|
530
|
+
| Event | Arguments | Can cancel? | Can transform? |
|
|
531
|
+
|---|---|---|---|
|
|
532
|
+
| `creating` | `data` | Yes | Yes |
|
|
533
|
+
| `created` | `record` | No | No |
|
|
534
|
+
| `updating` | `id, data` | Yes | Yes |
|
|
535
|
+
| `updated` | `record` | No | No |
|
|
536
|
+
| `deleting` | `id` | Yes | No |
|
|
537
|
+
| `deleted` | `id` | No | No |
|
|
538
|
+
| `restoring` | `id` | Yes | No |
|
|
539
|
+
| `restored` | `record` | No | No |
|
|
540
|
+
|
|
541
|
+
> Use `Model.create()`/`Model.update()`/`Model.delete()` to trigger events.
|
|
542
|
+
> `Model.query().create()` does NOT fire events.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## toJSON()
|
|
547
|
+
|
|
548
|
+
`toJSON()` applies casts, accessors, visible/hidden filtering, and appends:
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
class User extends Model {
|
|
552
|
+
static override hidden = ['password']
|
|
553
|
+
static override casts = { isAdmin: 'boolean' }
|
|
554
|
+
static override appends = ['fullName']
|
|
555
|
+
static override attributes = {
|
|
556
|
+
fullName: Attribute.make({ get: (_, a) => `${a['firstName']} ${a['lastName']}` }),
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
JSON.stringify(user)
|
|
561
|
+
// { "name": "Alice", "isAdmin": true, "fullName": "Alice Smith" }
|
|
562
|
+
// password excluded, isAdmin cast to boolean, fullName computed
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## ModelRegistry
|
|
568
|
+
|
|
569
|
+
Low-level registry used by adapters and the ORM itself.
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
import { ModelRegistry } from '@rudderjs/orm'
|
|
573
|
+
|
|
574
|
+
ModelRegistry.set(adapter) // called by provider packages
|
|
575
|
+
ModelRegistry.get() // current adapter (null if none)
|
|
576
|
+
ModelRegistry.getAdapter() // adapter or throw
|
|
577
|
+
ModelRegistry.reset() // clear (for tests)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## API Reference
|
|
583
|
+
|
|
584
|
+
| Export | Kind | Description |
|
|
585
|
+
|---|---|---|
|
|
586
|
+
| `Model` | Abstract class | Base class for all models |
|
|
587
|
+
| `ModelRegistry` | Class | Global ORM adapter registry |
|
|
588
|
+
| `Attribute` | Class | Accessor/mutator definition |
|
|
589
|
+
| `JsonResource` | Abstract class | API resource transformation |
|
|
590
|
+
| `ResourceCollection` | Class | Collection of resources with pagination |
|
|
591
|
+
| `ModelCollection` | Class | Typed array wrapper with ORM operations |
|
|
592
|
+
| `ModelFactory` | Abstract class | Factory for testing |
|
|
593
|
+
| `sequence` | Function | Cycling/indexed value generator |
|
|
594
|
+
| `Hidden` | Decorator | Mark property as hidden |
|
|
595
|
+
| `Visible` | Decorator | Mark property as visible |
|
|
596
|
+
| `Appends` | Decorator | Append accessor to JSON output |
|
|
597
|
+
| `Cast` | Decorator | Apply a cast type to a property |
|
|
598
|
+
| `CastUsing` | Interface | Custom cast class contract |
|
|
599
|
+
| `CastDefinition` | Type | Built-in cast name or custom cast class |
|
|
600
|
+
| `QueryBuilder<T>` | Interface | Fluent query builder contract |
|
|
601
|
+
| `OrmAdapter` | Interface | Adapter contract |
|
|
602
|
+
| `PaginatedResult<T>` | Interface | Paginated result shape |
|
|
603
|
+
| `ModelEvent` | Type | Observer event names |
|
|
604
|
+
| `ModelObserver` | Interface | Observer class contract |
|