@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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# @rudderjs/orm
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Eloquent-inspired ORM for RudderJS. Provides a `Model` base class with static query methods, attribute casting, accessors/mutators, soft deletes, observers, scopes, API resources (`JsonResource`), model collections, and factories. The ORM is adapter-based -- the actual database driver (e.g. Prisma) is registered via `ModelRegistry.set(adapter)` in a service provider.
|
|
6
|
+
|
|
7
|
+
## Key Patterns
|
|
8
|
+
|
|
9
|
+
### Defining Models
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Model, Attribute } from '@rudderjs/orm'
|
|
13
|
+
|
|
14
|
+
export class Post extends Model {
|
|
15
|
+
static table = 'posts'
|
|
16
|
+
static fillable = ['title', 'body', 'userId']
|
|
17
|
+
static hidden = ['deletedAt']
|
|
18
|
+
static softDeletes = true
|
|
19
|
+
|
|
20
|
+
static casts = {
|
|
21
|
+
isPublished: 'boolean',
|
|
22
|
+
metadata: 'json',
|
|
23
|
+
createdAt: 'datetime',
|
|
24
|
+
} as const satisfies Record<string, CastDefinition>
|
|
25
|
+
|
|
26
|
+
id!: number
|
|
27
|
+
title!: string
|
|
28
|
+
body!: string
|
|
29
|
+
userId!: number
|
|
30
|
+
isPublished!: boolean
|
|
31
|
+
metadata!: Record<string, unknown>
|
|
32
|
+
createdAt!: Date
|
|
33
|
+
updatedAt!: Date
|
|
34
|
+
deletedAt!: Date | null
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Table name defaults to lowercase class name + `s` if `static table` is omitted.
|
|
39
|
+
|
|
40
|
+
### Relationships
|
|
41
|
+
|
|
42
|
+
Relationships are loaded via the query builder's `with()` method. The adapter resolves relation names to the underlying DB joins/includes.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const user = await User.with('posts', 'profile').find(1)
|
|
46
|
+
const posts = await Post.with('author', 'comments').where('isPublished', true).get()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Querying
|
|
50
|
+
|
|
51
|
+
All query methods are static on the Model class and return a chainable `QueryBuilder`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const user = await User.find(1) // by primary key
|
|
55
|
+
const users = await User.all() // all records
|
|
56
|
+
const admin = await User.where('role', 'admin').first() // first match
|
|
57
|
+
|
|
58
|
+
// Chained queries
|
|
59
|
+
const recent = await Post
|
|
60
|
+
.where('isPublished', true)
|
|
61
|
+
.orderBy('createdAt', 'DESC')
|
|
62
|
+
.limit(10)
|
|
63
|
+
.get()
|
|
64
|
+
|
|
65
|
+
// Pagination
|
|
66
|
+
const page = await User.query().paginate(1, 15)
|
|
67
|
+
// => { data: [...], total, page, perPage, lastPage }
|
|
68
|
+
|
|
69
|
+
// Operators
|
|
70
|
+
const expensive = await Product.where('price', '>', 100).get()
|
|
71
|
+
|
|
72
|
+
// Soft deletes (when static softDeletes = true)
|
|
73
|
+
const withDeleted = await Post.query().withTrashed().get()
|
|
74
|
+
const onlyDeleted = await Post.query().onlyTrashed().get()
|
|
75
|
+
await Post.restore(id)
|
|
76
|
+
await Post.forceDelete(id)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Scopes
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
class Post extends Model {
|
|
83
|
+
static scopes = {
|
|
84
|
+
published: (query) => query.where('isPublished', true),
|
|
85
|
+
byAuthor: (query, userId: number) => query.where('userId', userId),
|
|
86
|
+
}
|
|
87
|
+
static globalScopes = {
|
|
88
|
+
active: (query) => query.where('deletedAt', null),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Usage
|
|
93
|
+
const posts = await Post.query().scope('published').scope('byAuthor', 42).get()
|
|
94
|
+
const all = await Post.query().withoutGlobalScope('active').get()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Accessors & Casts
|
|
98
|
+
|
|
99
|
+
Built-in casts: `string`, `integer`, `float`, `boolean`, `date`, `datetime`, `json`, `array`, `encrypted`.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { Attribute } from '@rudderjs/orm'
|
|
103
|
+
|
|
104
|
+
class User extends Model {
|
|
105
|
+
static casts = { isAdmin: 'boolean', settings: 'json' }
|
|
106
|
+
|
|
107
|
+
static attributes = {
|
|
108
|
+
firstName: Attribute.make({
|
|
109
|
+
get: (v) => String(v).charAt(0).toUpperCase() + String(v).slice(1),
|
|
110
|
+
}),
|
|
111
|
+
fullName: Attribute.make({
|
|
112
|
+
get: (_, attrs) => `${attrs['firstName']} ${attrs['lastName']}`,
|
|
113
|
+
}),
|
|
114
|
+
password: Attribute.make({
|
|
115
|
+
set: async (v) => await bcrypt.hash(String(v), 10),
|
|
116
|
+
}),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static appends = ['fullName']
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Decorators are also available: `@Hidden`, `@Visible`, `@Appends`, `@Cast('boolean')`.
|
|
124
|
+
|
|
125
|
+
### API Resources
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { JsonResource } from '@rudderjs/orm'
|
|
129
|
+
|
|
130
|
+
class UserResource extends JsonResource<User> {
|
|
131
|
+
toArray() {
|
|
132
|
+
return {
|
|
133
|
+
id: this.resource.id,
|
|
134
|
+
name: this.resource.name,
|
|
135
|
+
email: this.resource.email,
|
|
136
|
+
admin: this.when(this.resource.role === 'admin', true),
|
|
137
|
+
posts: this.whenLoaded('posts'),
|
|
138
|
+
...this.mergeWhen(this.resource.role === 'admin', {
|
|
139
|
+
permissions: this.resource.permissions,
|
|
140
|
+
}),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Single resource
|
|
146
|
+
res.json(new UserResource(user).toArray())
|
|
147
|
+
|
|
148
|
+
// Collection with pagination meta
|
|
149
|
+
res.json(await UserResource.collection(users, { total: 100, page: 1 }).toResponse())
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Factories
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { ModelFactory, sequence } from '@rudderjs/orm'
|
|
156
|
+
|
|
157
|
+
class UserFactory extends ModelFactory<{ name: string; email: string; role: string }> {
|
|
158
|
+
protected modelClass = User
|
|
159
|
+
|
|
160
|
+
definition() {
|
|
161
|
+
return {
|
|
162
|
+
name: 'Alice',
|
|
163
|
+
email: sequence(i => `user${i}@example.com`)(),
|
|
164
|
+
role: 'user',
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected states() {
|
|
169
|
+
return {
|
|
170
|
+
admin: () => ({ role: 'admin' }),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const user = await UserFactory.new().create()
|
|
176
|
+
const admin = await UserFactory.new().state('admin').create()
|
|
177
|
+
const users = await UserFactory.new().create(5)
|
|
178
|
+
const dto = await UserFactory.new().make() // no DB write
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Observers
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
class UserObserver {
|
|
185
|
+
creating(data: Record<string, unknown>) {
|
|
186
|
+
data['slug'] = slugify(data['name'] as string)
|
|
187
|
+
return data
|
|
188
|
+
}
|
|
189
|
+
created(record: Record<string, unknown>) {
|
|
190
|
+
console.log('User created:', record['id'])
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
User.observe(UserObserver)
|
|
195
|
+
|
|
196
|
+
// Or inline event listeners
|
|
197
|
+
User.on('updating', (id, data) => { data['updatedAt'] = new Date() ; return data })
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Common Pitfalls
|
|
201
|
+
|
|
202
|
+
- **No adapter registered**: You must call `ModelRegistry.set(adapter)` in a service provider before using any Model. The `DatabaseServiceProvider` must come before providers that query models.
|
|
203
|
+
- **Forgotten `static table`**: Without it, table name is auto-derived as `lowercase(ClassName) + 's'` (e.g. `User` -> `users`). Set it explicitly if your table name differs.
|
|
204
|
+
- **Casts not applied on write**: Casts transform values in both directions. If you bypass `Model.create()`/`Model.update()` and write directly via the adapter, casts and mutators are skipped.
|
|
205
|
+
- **Appends without accessor**: Adding a field to `static appends` has no effect unless it also has a `get` function in `static attributes`.
|
|
206
|
+
- **`encrypted` casts need `@rudderjs/crypt`**: The `encrypted`, `encrypted:array`, and `encrypted:object` casts require `@rudderjs/crypt` as a peer dependency.
|
|
207
|
+
|
|
208
|
+
## Key Imports
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import {
|
|
212
|
+
Model,
|
|
213
|
+
Attribute,
|
|
214
|
+
JsonResource,
|
|
215
|
+
ResourceCollection,
|
|
216
|
+
ModelCollection,
|
|
217
|
+
ModelFactory,
|
|
218
|
+
sequence,
|
|
219
|
+
ModelRegistry,
|
|
220
|
+
Hidden,
|
|
221
|
+
Visible,
|
|
222
|
+
Appends,
|
|
223
|
+
Cast,
|
|
224
|
+
} from '@rudderjs/orm'
|
|
225
|
+
|
|
226
|
+
import type {
|
|
227
|
+
QueryBuilder,
|
|
228
|
+
PaginatedResult,
|
|
229
|
+
CastDefinition,
|
|
230
|
+
CastUsing,
|
|
231
|
+
ModelEvent,
|
|
232
|
+
ModelObserver,
|
|
233
|
+
} from '@rudderjs/orm'
|
|
234
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface AttributeConfig<TGet = unknown, TSet = unknown> {
|
|
2
|
+
/** Transform the raw stored value when reading (accessor). */
|
|
3
|
+
get?: (value: unknown, attributes: Record<string, unknown>) => TGet;
|
|
4
|
+
/** Transform the value before writing to the database (mutator). */
|
|
5
|
+
set?: (value: TSet, attributes: Record<string, unknown>) => unknown;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Defines a model accessor and/or mutator for a property.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* class User extends Model {
|
|
12
|
+
* static attributes = {
|
|
13
|
+
* // Accessor only — capitalises first name on read
|
|
14
|
+
* firstName: Attribute.make({
|
|
15
|
+
* get: (v) => String(v).charAt(0).toUpperCase() + String(v).slice(1),
|
|
16
|
+
* }),
|
|
17
|
+
*
|
|
18
|
+
* // Mutator only — hash password on write
|
|
19
|
+
* password: Attribute.make({
|
|
20
|
+
* set: async (v) => await bcrypt.hash(String(v), 10),
|
|
21
|
+
* }),
|
|
22
|
+
*
|
|
23
|
+
* // Computed property from multiple columns (accessor only)
|
|
24
|
+
* fullName: Attribute.make({
|
|
25
|
+
* get: (_, attrs) => `${attrs['firstName']} ${attrs['lastName']}`,
|
|
26
|
+
* }),
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
export declare class Attribute<TGet = unknown, TSet = unknown> {
|
|
31
|
+
readonly getter: ((value: unknown, attributes: Record<string, unknown>) => TGet) | undefined;
|
|
32
|
+
readonly setter: ((value: TSet, attributes: Record<string, unknown>) => unknown) | undefined;
|
|
33
|
+
private constructor();
|
|
34
|
+
static make<TGet = unknown, TSet = unknown>(config: AttributeConfig<TGet, TSet>): Attribute<TGet, TSet>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=attribute.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribute.d.ts","sourceRoot":"","sources":["../src/attribute.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,GAAG,OAAO;IAC7D,8DAA8D;IAC9D,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;IACnE,oEAAoE;IACpE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAA;CACpE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,SAAS,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,GAAG,OAAO;IAEjD,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,GAAG,SAAS;IAC5F,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,GAAG,SAAS;IAF9F,OAAO;IAKP,MAAM,CAAC,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,GAAG,OAAO,EACxC,MAAM,EAAE,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,GAClC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC;CAGzB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ─── Attribute (Accessor / Mutator) ────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* Defines a model accessor and/or mutator for a property.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* class User extends Model {
|
|
7
|
+
* static attributes = {
|
|
8
|
+
* // Accessor only — capitalises first name on read
|
|
9
|
+
* firstName: Attribute.make({
|
|
10
|
+
* get: (v) => String(v).charAt(0).toUpperCase() + String(v).slice(1),
|
|
11
|
+
* }),
|
|
12
|
+
*
|
|
13
|
+
* // Mutator only — hash password on write
|
|
14
|
+
* password: Attribute.make({
|
|
15
|
+
* set: async (v) => await bcrypt.hash(String(v), 10),
|
|
16
|
+
* }),
|
|
17
|
+
*
|
|
18
|
+
* // Computed property from multiple columns (accessor only)
|
|
19
|
+
* fullName: Attribute.make({
|
|
20
|
+
* get: (_, attrs) => `${attrs['firstName']} ${attrs['lastName']}`,
|
|
21
|
+
* }),
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
export class Attribute {
|
|
26
|
+
getter;
|
|
27
|
+
setter;
|
|
28
|
+
constructor(getter, setter) {
|
|
29
|
+
this.getter = getter;
|
|
30
|
+
this.setter = setter;
|
|
31
|
+
}
|
|
32
|
+
static make(config) {
|
|
33
|
+
return new Attribute(config.get, config.set);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=attribute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attribute.js","sourceRoot":"","sources":["../src/attribute.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAS9D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,SAAS;IAET;IACA;IAFX,YACW,MAAmF,EACnF,MAAmF;QADnF,WAAM,GAAN,MAAM,CAA6E;QACnF,WAAM,GAAN,MAAM,CAA6E;IAC3F,CAAC;IAEJ,MAAM,CAAC,IAAI,CACT,MAAmC;QAEnC,OAAO,IAAI,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IAC9C,CAAC;CACF"}
|
package/dist/cast.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type BuiltInCast = 'string' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'json' | 'array' | 'collection' | 'encrypted' | 'encrypted:array' | 'encrypted:object';
|
|
2
|
+
/** Interface for a custom cast class. */
|
|
3
|
+
export interface CastUsing {
|
|
4
|
+
/** Transform a raw DB value to the application type (on read). */
|
|
5
|
+
get(key: string, value: unknown, attributes: Record<string, unknown>): unknown;
|
|
6
|
+
/** Transform an application value to a DB-storable type (on write). */
|
|
7
|
+
set(key: string, value: unknown, attributes: Record<string, unknown>): unknown;
|
|
8
|
+
}
|
|
9
|
+
export type CastDefinition = BuiltInCast | (new () => CastUsing);
|
|
10
|
+
/** Apply a cast when reading from DB (get side). */
|
|
11
|
+
export declare function castGet(type: string, key: string, value: unknown, attributes: Record<string, unknown>): unknown;
|
|
12
|
+
/** Apply a cast when writing to DB (set side). */
|
|
13
|
+
export declare function castSet(type: string, key: string, value: unknown, attributes: Record<string, unknown>): unknown;
|
|
14
|
+
//# sourceMappingURL=cast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cast.d.ts","sourceRoot":"","sources":["../src/cast.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,SAAS,GACT,OAAO,GACP,SAAS,GACT,MAAM,GACN,UAAU,GACV,MAAM,GACN,OAAO,GACP,YAAY,GACZ,WAAW,GACX,iBAAiB,GACjB,kBAAkB,CAAA;AAEtB,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAA;IAC9E,uEAAuE;IACvE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAA;CAC/E;AAED,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,CAAC,UAAU,SAAS,CAAC,CAAA;AAIhE,oDAAoD;AACpD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CA2B/G;AAED,kDAAkD;AAClD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAyB/G"}
|
package/dist/cast.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// ─── Cast Types ────────────────────────────────────────────
|
|
2
|
+
// ─── Built-in cast helpers ──────────────────────────────────
|
|
3
|
+
/** Apply a cast when reading from DB (get side). */
|
|
4
|
+
export function castGet(type, key, value, attributes) {
|
|
5
|
+
if (value === null || value === undefined)
|
|
6
|
+
return value;
|
|
7
|
+
if (typeof type !== 'string') {
|
|
8
|
+
// custom cast class — type is a constructor
|
|
9
|
+
const instance = new type();
|
|
10
|
+
return instance.get(key, value, attributes);
|
|
11
|
+
}
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'string': return String(value);
|
|
14
|
+
case 'integer': return parseInt(String(value), 10);
|
|
15
|
+
case 'float': return parseFloat(String(value));
|
|
16
|
+
case 'boolean': return value === 1 || value === '1' || value === true || value === 'true';
|
|
17
|
+
case 'date': return new Date(String(value));
|
|
18
|
+
case 'datetime': return new Date(String(value));
|
|
19
|
+
case 'json':
|
|
20
|
+
case 'array': return typeof value === 'string' ? JSON.parse(value) : value;
|
|
21
|
+
case 'collection':
|
|
22
|
+
// Returns plain array — ModelCollection wrapping done by caller if needed
|
|
23
|
+
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
24
|
+
case 'encrypted':
|
|
25
|
+
case 'encrypted:array':
|
|
26
|
+
case 'encrypted:object':
|
|
27
|
+
return _decrypt(type, value);
|
|
28
|
+
default: return value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Apply a cast when writing to DB (set side). */
|
|
32
|
+
export function castSet(type, key, value, attributes) {
|
|
33
|
+
if (value === null || value === undefined)
|
|
34
|
+
return value;
|
|
35
|
+
if (typeof type !== 'string') {
|
|
36
|
+
const instance = new type();
|
|
37
|
+
return instance.set(key, value, attributes);
|
|
38
|
+
}
|
|
39
|
+
switch (type) {
|
|
40
|
+
case 'string': return String(value);
|
|
41
|
+
case 'integer': return parseInt(String(value), 10);
|
|
42
|
+
case 'float': return parseFloat(String(value));
|
|
43
|
+
case 'boolean': return value === true || value === 'true' || value === 1 || value === '1' ? 1 : 0;
|
|
44
|
+
case 'date': return value instanceof Date ? value.toISOString().slice(0, 10) : String(value);
|
|
45
|
+
case 'datetime': return value instanceof Date ? value.toISOString() : String(value);
|
|
46
|
+
case 'json':
|
|
47
|
+
case 'array':
|
|
48
|
+
case 'collection':
|
|
49
|
+
return typeof value === 'object' ? JSON.stringify(value) : value;
|
|
50
|
+
case 'encrypted':
|
|
51
|
+
case 'encrypted:array':
|
|
52
|
+
case 'encrypted:object':
|
|
53
|
+
return _encrypt(type, value);
|
|
54
|
+
default: return value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// ─── Encryption stubs ───────────────────────────────────────
|
|
58
|
+
// Uses @rudderjs/crypt if available, otherwise throws clearly.
|
|
59
|
+
function _getCrypt() {
|
|
60
|
+
try {
|
|
61
|
+
// Dynamic require — only works if @rudderjs/crypt is installed
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
63
|
+
return require('@rudderjs/crypt');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function _encrypt(castType, value) {
|
|
70
|
+
const crypt = _getCrypt();
|
|
71
|
+
if (!crypt) {
|
|
72
|
+
throw new Error(`[RudderJS ORM] Cast type "${castType}" requires @rudderjs/crypt. Run: pnpm add @rudderjs/crypt`);
|
|
73
|
+
}
|
|
74
|
+
const serialized = castType === 'encrypted' ? String(value) : JSON.stringify(value);
|
|
75
|
+
return crypt.encrypt(serialized);
|
|
76
|
+
}
|
|
77
|
+
function _decrypt(castType, value) {
|
|
78
|
+
const crypt = _getCrypt();
|
|
79
|
+
if (!crypt) {
|
|
80
|
+
throw new Error(`[RudderJS ORM] Cast type "${castType}" requires @rudderjs/crypt. Run: pnpm add @rudderjs/crypt`);
|
|
81
|
+
}
|
|
82
|
+
const decrypted = crypt.decrypt(String(value));
|
|
83
|
+
return castType === 'encrypted' ? decrypted : JSON.parse(decrypted);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=cast.js.map
|
package/dist/cast.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cast.js","sourceRoot":"","sources":["../src/cast.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Model } from './index.js';
|
|
2
|
+
import type { QueryBuilder } from '@rudderjs/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* A typed array wrapper for Eloquent-style model collection operations.
|
|
5
|
+
* Returned by ORM queries when using `ModelCollection.wrap()`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const users = ModelCollection.wrap(await User.all())
|
|
9
|
+
*
|
|
10
|
+
* users.modelKeys() // [1, 2, 3]
|
|
11
|
+
* users.find(2) // { id: 2, ... }
|
|
12
|
+
* users.contains(2) // true
|
|
13
|
+
* users.except([1, 3]) // [{ id: 2, ... }]
|
|
14
|
+
* users.only([1, 2]) // [{ id: 1, ... }, { id: 2, ... }]
|
|
15
|
+
* users.unique('email') // deduplicated by email
|
|
16
|
+
* users.makeHidden(['email']) // each item's email hidden from JSON
|
|
17
|
+
*/
|
|
18
|
+
export declare class ModelCollection<T extends Record<string, unknown>> {
|
|
19
|
+
private readonly _items;
|
|
20
|
+
private readonly _primaryKey;
|
|
21
|
+
private constructor();
|
|
22
|
+
static wrap<T extends Record<string, unknown>>(items: T[], primaryKey?: string): ModelCollection<T>;
|
|
23
|
+
all(): T[];
|
|
24
|
+
count(): number;
|
|
25
|
+
isEmpty(): boolean;
|
|
26
|
+
isNotEmpty(): boolean;
|
|
27
|
+
toArray(): T[];
|
|
28
|
+
/** Returns an array of primary key values. */
|
|
29
|
+
modelKeys(): Array<string | number>;
|
|
30
|
+
/** Find item by primary key, or `undefined` if not found. */
|
|
31
|
+
find(id: string | number): T | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Returns true if any item matches. Accepts a primary key value or a predicate.
|
|
34
|
+
*/
|
|
35
|
+
contains(idOrFn: string | number | ((item: T) => boolean)): boolean;
|
|
36
|
+
/** Items whose primary key is NOT in `ids`. */
|
|
37
|
+
except(ids: Array<string | number>): ModelCollection<T>;
|
|
38
|
+
/** Items whose primary key IS in `ids`. */
|
|
39
|
+
only(ids: Array<string | number>): ModelCollection<T>;
|
|
40
|
+
/** Items not present in `other` (diff by primary key). */
|
|
41
|
+
diff(other: ModelCollection<T> | T[]): ModelCollection<T>;
|
|
42
|
+
/**
|
|
43
|
+
* Items unique by the given key (or primary key if omitted).
|
|
44
|
+
* First occurrence wins.
|
|
45
|
+
*/
|
|
46
|
+
unique(key?: string): ModelCollection<T>;
|
|
47
|
+
/** Call `makeVisible(keys)` on each model instance and return a new collection. */
|
|
48
|
+
makeVisible(keys: string[]): ModelCollection<T>;
|
|
49
|
+
/** Call `makeHidden(keys)` on each model instance and return a new collection. */
|
|
50
|
+
makeHidden(keys: string[]): ModelCollection<T>;
|
|
51
|
+
/**
|
|
52
|
+
* Reload each item from the database. Returns a new collection with fresh data.
|
|
53
|
+
* Items are loaded via `Model.find(id)` so they must have their primary key set.
|
|
54
|
+
*/
|
|
55
|
+
fresh(modelClass: typeof Model): Promise<ModelCollection<Record<string, unknown>>>;
|
|
56
|
+
/**
|
|
57
|
+
* Eager-load relations for all items.
|
|
58
|
+
* Returns a new collection with relations loaded.
|
|
59
|
+
*/
|
|
60
|
+
load(modelClass: typeof Model, ...relations: string[]): Promise<ModelCollection<Record<string, unknown>>>;
|
|
61
|
+
/**
|
|
62
|
+
* Eager-load relations that are not already present on the items.
|
|
63
|
+
* Only loads relations where `item[relation]` is undefined.
|
|
64
|
+
*/
|
|
65
|
+
loadMissing(modelClass: typeof Model, ...relations: string[]): Promise<ModelCollection<Record<string, unknown>>>;
|
|
66
|
+
/**
|
|
67
|
+
* Return a query builder scoped to this collection's primary keys.
|
|
68
|
+
* Useful for building additional queries on the same set of records.
|
|
69
|
+
*/
|
|
70
|
+
toQuery(modelClass: typeof Model): QueryBuilder<T>;
|
|
71
|
+
toJSON(): T[];
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=collection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAIvD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAE1D,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAF9B,OAAO;IAKP,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3C,KAAK,EAAE,CAAC,EAAE,EACV,UAAU,SAAO,GAChB,eAAe,CAAC,CAAC,CAAC;IAMrB,GAAG,IAAI,CAAC,EAAE;IACV,KAAK,IAAI,MAAM;IACf,OAAO,IAAI,OAAO;IAClB,UAAU,IAAI,OAAO;IACrB,OAAO,IAAI,CAAC,EAAE;IAEd,8CAA8C;IAC9C,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;IAMnC,6DAA6D;IAC7D,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,GAAG,SAAS;IAIxC;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,GAAG,OAAO;IAOnE,+CAA+C;IAC/C,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC;IAQvD,2CAA2C;IAC3C,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC;IAQrD,0DAA0D;IAC1D,IAAI,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC;IASzD;;;OAGG;IACH,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC,CAAC,CAAC;IAgBxC,mFAAmF;IACnF,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC;IAa/C,kFAAkF;IAClF,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CAAC,CAAC,CAAC;IAe9C;;;OAGG;IACG,KAAK,CAAC,UAAU,EAAE,OAAO,KAAK,GAAG,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAUxF;;;OAGG;IACG,IAAI,CACR,UAAU,EAAE,OAAO,KAAK,EACxB,GAAG,SAAS,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAOpD;;;OAGG;IACG,WAAW,CACf,UAAU,EAAE,OAAO,KAAK,EACxB,GAAG,SAAS,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAWpD;;;OAGG;IACH,OAAO,CAAC,UAAU,EAAE,OAAO,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC;IAMlD,MAAM,IAAI,CAAC,EAAE;CAGd"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// ─── ModelCollection ────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* A typed array wrapper for Eloquent-style model collection operations.
|
|
4
|
+
* Returned by ORM queries when using `ModelCollection.wrap()`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const users = ModelCollection.wrap(await User.all())
|
|
8
|
+
*
|
|
9
|
+
* users.modelKeys() // [1, 2, 3]
|
|
10
|
+
* users.find(2) // { id: 2, ... }
|
|
11
|
+
* users.contains(2) // true
|
|
12
|
+
* users.except([1, 3]) // [{ id: 2, ... }]
|
|
13
|
+
* users.only([1, 2]) // [{ id: 1, ... }, { id: 2, ... }]
|
|
14
|
+
* users.unique('email') // deduplicated by email
|
|
15
|
+
* users.makeHidden(['email']) // each item's email hidden from JSON
|
|
16
|
+
*/
|
|
17
|
+
export class ModelCollection {
|
|
18
|
+
_items;
|
|
19
|
+
_primaryKey;
|
|
20
|
+
constructor(_items, _primaryKey = 'id') {
|
|
21
|
+
this._items = _items;
|
|
22
|
+
this._primaryKey = _primaryKey;
|
|
23
|
+
}
|
|
24
|
+
static wrap(items, primaryKey = 'id') {
|
|
25
|
+
return new ModelCollection(items, primaryKey);
|
|
26
|
+
}
|
|
27
|
+
// ── Core ──────────────────────────────────────────────
|
|
28
|
+
all() { return this._items; }
|
|
29
|
+
count() { return this._items.length; }
|
|
30
|
+
isEmpty() { return this._items.length === 0; }
|
|
31
|
+
isNotEmpty() { return this._items.length > 0; }
|
|
32
|
+
toArray() { return [...this._items]; }
|
|
33
|
+
/** Returns an array of primary key values. */
|
|
34
|
+
modelKeys() {
|
|
35
|
+
return this._items.map(item => item[this._primaryKey]);
|
|
36
|
+
}
|
|
37
|
+
// ── Search ────────────────────────────────────────────
|
|
38
|
+
/** Find item by primary key, or `undefined` if not found. */
|
|
39
|
+
find(id) {
|
|
40
|
+
return this._items.find(item => item[this._primaryKey] === id);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if any item matches. Accepts a primary key value or a predicate.
|
|
44
|
+
*/
|
|
45
|
+
contains(idOrFn) {
|
|
46
|
+
if (typeof idOrFn === 'function')
|
|
47
|
+
return this._items.some(idOrFn);
|
|
48
|
+
return this._items.some(item => item[this._primaryKey] === idOrFn);
|
|
49
|
+
}
|
|
50
|
+
// ── Filtering ─────────────────────────────────────────
|
|
51
|
+
/** Items whose primary key is NOT in `ids`. */
|
|
52
|
+
except(ids) {
|
|
53
|
+
const set = new Set(ids);
|
|
54
|
+
return ModelCollection.wrap(this._items.filter(item => !set.has(item[this._primaryKey])), this._primaryKey);
|
|
55
|
+
}
|
|
56
|
+
/** Items whose primary key IS in `ids`. */
|
|
57
|
+
only(ids) {
|
|
58
|
+
const set = new Set(ids);
|
|
59
|
+
return ModelCollection.wrap(this._items.filter(item => set.has(item[this._primaryKey])), this._primaryKey);
|
|
60
|
+
}
|
|
61
|
+
/** Items not present in `other` (diff by primary key). */
|
|
62
|
+
diff(other) {
|
|
63
|
+
const otherItems = other instanceof ModelCollection ? other.all() : other;
|
|
64
|
+
const otherKeys = new Set(otherItems.map(i => i[this._primaryKey]));
|
|
65
|
+
return ModelCollection.wrap(this._items.filter(item => !otherKeys.has(item[this._primaryKey])), this._primaryKey);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Items unique by the given key (or primary key if omitted).
|
|
69
|
+
* First occurrence wins.
|
|
70
|
+
*/
|
|
71
|
+
unique(key) {
|
|
72
|
+
const k = key ?? this._primaryKey;
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
return ModelCollection.wrap(this._items.filter(item => {
|
|
75
|
+
const val = item[k];
|
|
76
|
+
if (seen.has(val))
|
|
77
|
+
return false;
|
|
78
|
+
seen.add(val);
|
|
79
|
+
return true;
|
|
80
|
+
}), this._primaryKey);
|
|
81
|
+
}
|
|
82
|
+
// ── Serialization controls ────────────────────────────
|
|
83
|
+
/** Call `makeVisible(keys)` on each model instance and return a new collection. */
|
|
84
|
+
makeVisible(keys) {
|
|
85
|
+
return ModelCollection.wrap(this._items.map(item => {
|
|
86
|
+
const m = item;
|
|
87
|
+
if (typeof m['makeVisible'] === 'function') {
|
|
88
|
+
return m['makeVisible'](keys);
|
|
89
|
+
}
|
|
90
|
+
return item;
|
|
91
|
+
}), this._primaryKey);
|
|
92
|
+
}
|
|
93
|
+
/** Call `makeHidden(keys)` on each model instance and return a new collection. */
|
|
94
|
+
makeHidden(keys) {
|
|
95
|
+
return ModelCollection.wrap(this._items.map(item => {
|
|
96
|
+
const m = item;
|
|
97
|
+
if (typeof m['makeHidden'] === 'function') {
|
|
98
|
+
return m['makeHidden'](keys);
|
|
99
|
+
}
|
|
100
|
+
return item;
|
|
101
|
+
}), this._primaryKey);
|
|
102
|
+
}
|
|
103
|
+
// ── Async ORM operations ──────────────────────────────
|
|
104
|
+
/**
|
|
105
|
+
* Reload each item from the database. Returns a new collection with fresh data.
|
|
106
|
+
* Items are loaded via `Model.find(id)` so they must have their primary key set.
|
|
107
|
+
*/
|
|
108
|
+
async fresh(modelClass) {
|
|
109
|
+
const ids = this.modelKeys();
|
|
110
|
+
const results = await Promise.all(ids.map(id => modelClass.find(id)));
|
|
111
|
+
return ModelCollection.wrap(
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
results.filter((r) => r !== null), this._primaryKey);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Eager-load relations for all items.
|
|
117
|
+
* Returns a new collection with relations loaded.
|
|
118
|
+
*/
|
|
119
|
+
async load(modelClass, ...relations) {
|
|
120
|
+
const ids = this.modelKeys();
|
|
121
|
+
const items = await modelClass.with(...relations).where(this._primaryKey, ids).get();
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
return ModelCollection.wrap(items, this._primaryKey);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Eager-load relations that are not already present on the items.
|
|
127
|
+
* Only loads relations where `item[relation]` is undefined.
|
|
128
|
+
*/
|
|
129
|
+
async loadMissing(modelClass, ...relations) {
|
|
130
|
+
const first = this._items[0];
|
|
131
|
+
if (!first)
|
|
132
|
+
return ModelCollection.wrap([], this._primaryKey);
|
|
133
|
+
const missing = relations.filter(rel => !(rel in first) || first[rel] === undefined);
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
135
|
+
if (missing.length === 0)
|
|
136
|
+
return ModelCollection.wrap(this._items, this._primaryKey);
|
|
137
|
+
return this.load(modelClass, ...missing);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Return a query builder scoped to this collection's primary keys.
|
|
141
|
+
* Useful for building additional queries on the same set of records.
|
|
142
|
+
*/
|
|
143
|
+
toQuery(modelClass) {
|
|
144
|
+
const ids = this.modelKeys();
|
|
145
|
+
// Use where with array to produce a WHERE IN equivalent
|
|
146
|
+
return modelClass.where(this._primaryKey, ids);
|
|
147
|
+
}
|
|
148
|
+
toJSON() {
|
|
149
|
+
return this.toArray();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=collection.js.map
|