@rudderjs/orm 1.8.1 → 1.9.1
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 +53 -0
- package/boost/skills/orm-models/SKILL.md +24 -251
- package/boost/skills/orm-models/rules/crud-and-observers.md +130 -0
- package/boost/skills/orm-models/rules/defining-models.md +137 -0
- package/boost/skills/orm-models/rules/factories.md +73 -0
- package/boost/skills/orm-models/rules/querying.md +117 -0
- package/boost/skills/orm-models/rules/resources.md +111 -0
- package/dist/cast.d.ts +39 -0
- package/dist/cast.d.ts.map +1 -1
- package/dist/cast.js +91 -1
- package/dist/cast.js.map +1 -1
- package/dist/commands/migrate.d.ts +54 -0
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +156 -2
- package/dist/commands/migrate.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/vector-errors.d.ts +71 -0
- package/dist/vector-errors.d.ts.map +1 -0
- package/dist/vector-errors.js +88 -0
- package/dist/vector-errors.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Model Factories
|
|
2
|
+
|
|
3
|
+
## Basic shape
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { ModelFactory, sequence } from '@rudderjs/orm'
|
|
7
|
+
import { User } from '../app/Models/User.js'
|
|
8
|
+
|
|
9
|
+
class UserFactory extends ModelFactory<{ name: string; email: string; role: string }> {
|
|
10
|
+
protected modelClass = User
|
|
11
|
+
|
|
12
|
+
definition() {
|
|
13
|
+
return {
|
|
14
|
+
name: 'Alice',
|
|
15
|
+
email: sequence(i => `user${i}@example.com`),
|
|
16
|
+
role: 'user',
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected states() {
|
|
21
|
+
return {
|
|
22
|
+
admin: () => ({ role: 'admin' }),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const one = await UserFactory.new().create() // 1 row, persisted
|
|
32
|
+
const five = await UserFactory.new().create(5) // 5 rows
|
|
33
|
+
const dtos = await UserFactory.new().make(3) // 3 in-memory only
|
|
34
|
+
const admin = await UserFactory.new().state('admin').create()
|
|
35
|
+
const custom = await UserFactory.new().with(() => ({ name: 'Bob' })).create()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`.make()` does not write to the DB — useful for testing serialization, validation, or non-persisting code paths.
|
|
39
|
+
`.create()` writes via `Model.create()`, so observers / mutators / mass assignment all apply.
|
|
40
|
+
|
|
41
|
+
## sequence()
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
email: sequence(i => `user${i}@example.com`)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Inside `definition()`, return the call directly — the factory resolves callables for you. Each generated row gets the next index.
|
|
48
|
+
|
|
49
|
+
## Pitfalls
|
|
50
|
+
|
|
51
|
+
❌ **Don't** call `sequence(...)` outside `definition()`:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const s = sequence(i => i)
|
|
55
|
+
class UserFactory extends ModelFactory<{ n: number }> {
|
|
56
|
+
definition() { return { n: s() } } // shared sequence across factory instances
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
✅ **Do** return the sequence callable from `definition()`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
definition() { return { n: sequence(i => i) } } // fresh per factory instance
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
❌ **Don't** assume `.make()` ran mutators:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const draft = await UserFactory.new().make()
|
|
70
|
+
// password mutator did NOT run because there's no save() — but accessors DID run on serialization
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
✅ **Do** call `.create()` when you need mutator side effects (password hashing, slug generation, etc.).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Querying
|
|
2
|
+
|
|
3
|
+
## Single-row reads
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const user = await User.find(1) // by primary key
|
|
7
|
+
const first = await User.first() // first row
|
|
8
|
+
const total = await User.count() // SELECT count(*)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Filtered reads
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const admins = await User.where('role', 'admin').all()
|
|
15
|
+
const recent = await User.where('createdAt', '>', oneWeekAgo).orderBy('createdAt', 'desc').limit(10).all()
|
|
16
|
+
const page = await User.paginate(1, 15)
|
|
17
|
+
// { data, total, page, perPage, lastPage }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`where` accepts `(column, value)` (defaults to `=`), `(column, op, value)`, or a callback for grouped predicates.
|
|
21
|
+
|
|
22
|
+
## Eager loading
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const posts = await Post.with('author', 'comments').all()
|
|
26
|
+
const user = await User.with({ posts: q => q.where('isPublished', true) }).find(1)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Whole-row eager loading is handled natively by the adapter (Prisma `include`, Drizzle `with`).
|
|
30
|
+
|
|
31
|
+
## Aggregate eager loading
|
|
32
|
+
|
|
33
|
+
Stays portable across adapters:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const users = await User.withCount('posts').all() // posts_count column
|
|
37
|
+
const authors = await Post.withSum('viewCount', 'views').all() // posts_sum_views
|
|
38
|
+
const post = await Post.find(1).then(p => p.loadCount('comments')) // per-instance
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`withCount` on `belongsTo` and `morphTo` throws — you can't count something there's exactly one of (or whose target table is dynamic).
|
|
42
|
+
|
|
43
|
+
## Scopes
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export class Post extends Model {
|
|
47
|
+
static globalScopes = {
|
|
48
|
+
published: (q) => q.where('isPublished', true), // ALWAYS applied
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static scopes = {
|
|
52
|
+
byAuthor: (q, authorId: number) => q.where('authorId', authorId),
|
|
53
|
+
recent: (q) => q.orderBy('createdAt', 'desc').limit(10),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const posts = await Post.query().scope('byAuthor', 1).scope('recent').all()
|
|
58
|
+
const allPosts = await Post.query().withoutGlobalScope('published').all()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Relation predicates (`whereHas`)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// Users who have at least one published post
|
|
65
|
+
const authors = await User.query()
|
|
66
|
+
.whereHas('posts', q => q.where('isPublished', true))
|
|
67
|
+
.all()
|
|
68
|
+
|
|
69
|
+
// Users who have no comments
|
|
70
|
+
const lurkers = await User.query().whereDoesntHave('comments').all()
|
|
71
|
+
|
|
72
|
+
// Eager-load with the same constraint
|
|
73
|
+
const data = await User.query()
|
|
74
|
+
.withWhereHas('posts', q => q.where('isPublished', true))
|
|
75
|
+
.all()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Pitfalls
|
|
79
|
+
|
|
80
|
+
❌ **Don't** use `eq(col, null)` for null checks:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// drizzle-orm
|
|
84
|
+
qb.where(eq(users.deletedAt, null)) // never matches anything
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
✅ **Do** use `isNull` / `isNotNull`:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
qb.where(isNull(users.deletedAt))
|
|
91
|
+
qb.where(isNotNull(users.deletedAt))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
❌ **Don't** assume `assert.deepStrictEqual(result, plainObject)` holds since hydration shipped:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const user = await User.find(1)
|
|
98
|
+
assert.deepStrictEqual(user, { id: 1, name: 'Alice' }) // ❌ prototype mismatch
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
✅ **Do** compare via spread or assert `instanceof`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
assert.deepStrictEqual({ ...user }, { id: 1, name: 'Alice' })
|
|
105
|
+
assert.ok(user instanceof User)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
❌ **Don't** use `morphTo` with `whereHas` — the related table is dynamic.
|
|
109
|
+
|
|
110
|
+
✅ **Do** filter on the morph columns directly:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
await Comment.query()
|
|
114
|
+
.where('commentableType', 'Post')
|
|
115
|
+
.where('commentableId', 1)
|
|
116
|
+
.all()
|
|
117
|
+
```
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# API Resources
|
|
2
|
+
|
|
3
|
+
`JsonResource` is the controller-friendly way to shape model output for an HTTP response — without leaking columns that shouldn't reach the client.
|
|
4
|
+
|
|
5
|
+
## Single resource
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { JsonResource } from '@rudderjs/orm'
|
|
9
|
+
|
|
10
|
+
class UserResource extends JsonResource<User> {
|
|
11
|
+
toArray() {
|
|
12
|
+
return {
|
|
13
|
+
id: this.resource.id,
|
|
14
|
+
name: this.resource.name,
|
|
15
|
+
email: this.resource.email,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// In a route handler
|
|
21
|
+
res.json(new UserResource(user).toArray())
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Conditional fields
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
class UserResource extends JsonResource<User> {
|
|
28
|
+
toArray() {
|
|
29
|
+
return {
|
|
30
|
+
id: this.resource.id,
|
|
31
|
+
name: this.resource.name,
|
|
32
|
+
|
|
33
|
+
// Include only when condition is true
|
|
34
|
+
admin: this.when(this.resource.role === 'admin', true),
|
|
35
|
+
|
|
36
|
+
// Include only when the relation was eager-loaded
|
|
37
|
+
posts: this.whenLoaded('posts', PostResource.collection(this.resource.posts)),
|
|
38
|
+
|
|
39
|
+
// Merge multiple fields conditionally
|
|
40
|
+
...this.mergeWhen(this.resource.isAdmin, {
|
|
41
|
+
permissions: this.resource.permissions,
|
|
42
|
+
lastLogin: this.resource.lastLoginAt,
|
|
43
|
+
}),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`whenLoaded` is the canonical guard against N+1 — the field stays absent if the caller didn't `.with('posts')`.
|
|
50
|
+
|
|
51
|
+
## Collections
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const users = await User.with('posts').all()
|
|
55
|
+
|
|
56
|
+
const collection = UserResource.collection(users, {
|
|
57
|
+
total: 100,
|
|
58
|
+
page: 1,
|
|
59
|
+
perPage: 15,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
res.json(await collection.toResponse())
|
|
63
|
+
// {
|
|
64
|
+
// data: [...],
|
|
65
|
+
// meta: { total: 100, page: 1, perPage: 15 }
|
|
66
|
+
// }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For a paginated query:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const page = await User.paginate(1, 15)
|
|
73
|
+
const collection = UserResource.collection(page.data, {
|
|
74
|
+
total: page.total,
|
|
75
|
+
page: page.page,
|
|
76
|
+
perPage: page.perPage,
|
|
77
|
+
lastPage: page.lastPage,
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Pitfalls
|
|
82
|
+
|
|
83
|
+
❌ **Don't** assume `whenLoaded` works without `.with()`:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const user = await User.find(1) // posts NOT loaded
|
|
87
|
+
const res = new UserResource(user).toArray()
|
|
88
|
+
// res.posts is omitted — that's correct, but the caller may have expected it
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
✅ **Do** eager-load when the resource needs the relation:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const user = await User.with('posts').find(1)
|
|
95
|
+
const res = new UserResource(user).toArray()
|
|
96
|
+
// res.posts is present
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
❌ **Don't** use `when` for relations:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
posts: this.when(this.resource.posts !== undefined, PostResource.collection(this.resource.posts))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
✅ **Do** use `whenLoaded` — it's the relation-aware variant:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
posts: this.whenLoaded('posts', PostResource.collection(this.resource.posts))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
❌ **Don't** mutate `this.resource` inside `toArray()` — observers / mutators won't fire and you risk a stale state on the next access. Compute derived values and return them; don't write to the model.
|
package/dist/cast.d.ts
CHANGED
|
@@ -7,6 +7,45 @@ export interface CastUsing {
|
|
|
7
7
|
set(key: string, value: unknown, attributes: Record<string, unknown>): unknown;
|
|
8
8
|
}
|
|
9
9
|
export type CastDefinition = BuiltInCast | (new () => CastUsing);
|
|
10
|
+
/**
|
|
11
|
+
* Build a cast for a pgvector column. The returned class implements
|
|
12
|
+
* {@link CastUsing}: on write, validates dimension count + element
|
|
13
|
+
* finiteness and serializes `number[]` → pgvector text format
|
|
14
|
+
* (`'[0.1,0.2,...]'`); on read, parses the text format back to
|
|
15
|
+
* `number[]`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { Model, vector, type CastDefinition } from '@rudderjs/orm'
|
|
20
|
+
*
|
|
21
|
+
* class Document extends Model {
|
|
22
|
+
* static casts = {
|
|
23
|
+
* embedding: vector({ dimensions: 1536 }),
|
|
24
|
+
* } as const satisfies Record<string, CastDefinition>
|
|
25
|
+
*
|
|
26
|
+
* embedding!: number[]
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* # Why a factory + class (not a string-keyed built-in cast)
|
|
31
|
+
*
|
|
32
|
+
* The built-in cast string union (`'integer'`, `'json'`, …) can't
|
|
33
|
+
* carry parameters. `vector` needs `dimensions` for write-time
|
|
34
|
+
* validation. A class with the dim baked into its closure is the
|
|
35
|
+
* cleanest fit for the existing `CastDefinition` shape.
|
|
36
|
+
*
|
|
37
|
+
* # Postgres-only
|
|
38
|
+
*
|
|
39
|
+
* The serialization format (`'[1,2,3]'`) is pgvector's. SQLite +
|
|
40
|
+
* MySQL don't have an equivalent; storing the same string in a TEXT
|
|
41
|
+
* column would compile but no vector ops would work. The cast
|
|
42
|
+
* doesn't enforce the adapter — that check lives at query time
|
|
43
|
+
* ({@link VectorStorageUnsupportedError}, raised by the adapter when
|
|
44
|
+
* pgvector isn't installed).
|
|
45
|
+
*/
|
|
46
|
+
export declare function vector(opts: {
|
|
47
|
+
dimensions: number;
|
|
48
|
+
}): new () => CastUsing;
|
|
10
49
|
/** Apply a cast when reading from DB (get side). */
|
|
11
50
|
export declare function castGet(type: string, key: string, value: unknown, attributes: Record<string, unknown>): unknown;
|
|
12
51
|
/** Apply a cast when writing to DB (set side). */
|
package/dist/cast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cast.d.ts","sourceRoot":"","sources":["../src/cast.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cast.d.ts","sourceRoot":"","sources":["../src/cast.ts"],"names":[],"mappings":"AAIA,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,SAAS,CAyDxE;AAID,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
CHANGED
|
@@ -1,4 +1,94 @@
|
|
|
1
|
-
|
|
1
|
+
import { VectorDimensionMismatchError } from './vector-errors.js';
|
|
2
|
+
// ─── Vector cast (#B7 Phase 1) ──────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* Build a cast for a pgvector column. The returned class implements
|
|
5
|
+
* {@link CastUsing}: on write, validates dimension count + element
|
|
6
|
+
* finiteness and serializes `number[]` → pgvector text format
|
|
7
|
+
* (`'[0.1,0.2,...]'`); on read, parses the text format back to
|
|
8
|
+
* `number[]`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { Model, vector, type CastDefinition } from '@rudderjs/orm'
|
|
13
|
+
*
|
|
14
|
+
* class Document extends Model {
|
|
15
|
+
* static casts = {
|
|
16
|
+
* embedding: vector({ dimensions: 1536 }),
|
|
17
|
+
* } as const satisfies Record<string, CastDefinition>
|
|
18
|
+
*
|
|
19
|
+
* embedding!: number[]
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* # Why a factory + class (not a string-keyed built-in cast)
|
|
24
|
+
*
|
|
25
|
+
* The built-in cast string union (`'integer'`, `'json'`, …) can't
|
|
26
|
+
* carry parameters. `vector` needs `dimensions` for write-time
|
|
27
|
+
* validation. A class with the dim baked into its closure is the
|
|
28
|
+
* cleanest fit for the existing `CastDefinition` shape.
|
|
29
|
+
*
|
|
30
|
+
* # Postgres-only
|
|
31
|
+
*
|
|
32
|
+
* The serialization format (`'[1,2,3]'`) is pgvector's. SQLite +
|
|
33
|
+
* MySQL don't have an equivalent; storing the same string in a TEXT
|
|
34
|
+
* column would compile but no vector ops would work. The cast
|
|
35
|
+
* doesn't enforce the adapter — that check lives at query time
|
|
36
|
+
* ({@link VectorStorageUnsupportedError}, raised by the adapter when
|
|
37
|
+
* pgvector isn't installed).
|
|
38
|
+
*/
|
|
39
|
+
export function vector(opts) {
|
|
40
|
+
const dimensions = opts.dimensions;
|
|
41
|
+
if (!Number.isInteger(dimensions) || dimensions < 1) {
|
|
42
|
+
throw new Error(`[RudderJS ORM] vector({ dimensions }) requires a positive integer; got ${String(dimensions)}`);
|
|
43
|
+
}
|
|
44
|
+
return class VectorCast {
|
|
45
|
+
get(_key, value) {
|
|
46
|
+
if (value === null || value === undefined)
|
|
47
|
+
return value;
|
|
48
|
+
// Already an array (e.g. roundtrip from cache) — passthrough.
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return value;
|
|
51
|
+
// pgvector text format: '[0.1,0.2,0.3]'. JSON.parse handles it
|
|
52
|
+
// since pgvector emits numbers without quotes — same shape as JSON.
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(value);
|
|
56
|
+
if (!Array.isArray(parsed)) {
|
|
57
|
+
throw new Error(`expected array, got ${typeof parsed}`);
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
63
|
+
throw new Error(`[RudderJS ORM] Vector cast failed to parse value (${msg}): ${value.slice(0, 80)}`, { cause: err });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
set(key, value) {
|
|
69
|
+
if (value === null || value === undefined)
|
|
70
|
+
return value;
|
|
71
|
+
if (!Array.isArray(value)) {
|
|
72
|
+
throw new Error(`[RudderJS ORM] Vector column "${key}" expected number[], got ${typeof value}`);
|
|
73
|
+
}
|
|
74
|
+
if (value.length !== dimensions) {
|
|
75
|
+
throw new VectorDimensionMismatchError(key, dimensions, value.length);
|
|
76
|
+
}
|
|
77
|
+
// pgvector rejects NaN / ±Infinity — pre-validate so the throw
|
|
78
|
+
// surfaces the column name + element index instead of a Prisma
|
|
79
|
+
// error 1000 layers deep.
|
|
80
|
+
for (let i = 0; i < value.length; i++) {
|
|
81
|
+
const n = value[i];
|
|
82
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) {
|
|
83
|
+
throw new Error(`[RudderJS ORM] Vector column "${key}" element ${i} must be a finite number, got ${String(n)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// pgvector accepts the same syntax JSON arrays use — comma-separated
|
|
87
|
+
// numbers in square brackets.
|
|
88
|
+
return `[${value.join(',')}]`;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
2
92
|
// ─── Built-in cast helpers ──────────────────────────────────
|
|
3
93
|
/** Apply a cast when reading from DB (get side). */
|
|
4
94
|
export function castGet(type, key, value, attributes) {
|
package/dist/cast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cast.js","sourceRoot":"","sources":["../src/cast.ts"],"names":[],"mappings":"AAAA,8DAA8D;
|
|
1
|
+
{"version":3,"file":"cast.js","sourceRoot":"","sources":["../src/cast.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,60 @@ export declare function buildArgs(orm: ORM, command: 'migrate' | 'migrate:fresh'
|
|
|
16
16
|
name?: string;
|
|
17
17
|
env?: string;
|
|
18
18
|
}): string[];
|
|
19
|
+
export interface VectorMigrationOptions {
|
|
20
|
+
table: string;
|
|
21
|
+
column: string;
|
|
22
|
+
dimensions: number;
|
|
23
|
+
/** ORM target — affects the migration filename layout. Auto-detected from
|
|
24
|
+
* package.json when omitted; falls back to 'drizzle' if no ORM is detected. */
|
|
25
|
+
orm?: 'prisma' | 'drizzle';
|
|
26
|
+
/** Distance metric the HNSW index will be optimized for. Default `'cosine'`. */
|
|
27
|
+
metric?: 'cosine' | 'l2' | 'inner-product';
|
|
28
|
+
}
|
|
29
|
+
export interface VectorMigrationResult {
|
|
30
|
+
filePath: string;
|
|
31
|
+
sql: string;
|
|
32
|
+
/** Schema.prisma snippet apps using Prisma should add to their model. */
|
|
33
|
+
prismaSchemaSnippet?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build the raw SQL for adding a pgvector column + HNSW index. Pure;
|
|
37
|
+
* no I/O. Exported for testing and so apps can compose the snippet
|
|
38
|
+
* into a hand-rolled migration if their layout differs from the
|
|
39
|
+
* convention {@link writeVectorMigration} uses.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildVectorMigrationSql(opts: VectorMigrationOptions): string;
|
|
42
|
+
/**
|
|
43
|
+
* Build the Prisma `schema.prisma` snippet that mirrors the SQL
|
|
44
|
+
* column. Prisma can't natively type pgvector columns; users declare
|
|
45
|
+
* `Unsupported("vector(N)")` and the cosine HNSW index alongside.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildPrismaSchemaSnippet(opts: VectorMigrationOptions): string;
|
|
48
|
+
/**
|
|
49
|
+
* Write a pgvector migration file to a sensible default location for
|
|
50
|
+
* the detected ORM. Prisma migrations land under
|
|
51
|
+
* `prisma/migrations/<ts>_add_<col>_vector_to_<table>/migration.sql`
|
|
52
|
+
* (Prisma's standard layout). Drizzle migrations land under
|
|
53
|
+
* `drizzle/<ts>_add_<col>_vector_to_<table>.sql`.
|
|
54
|
+
*
|
|
55
|
+
* If the layout differs from the default, use {@link buildVectorMigrationSql}
|
|
56
|
+
* directly and write the SQL wherever your migration tooling expects.
|
|
57
|
+
*/
|
|
58
|
+
export declare function writeVectorMigration(opts: VectorMigrationOptions, cwd?: string, now?: Date): Promise<VectorMigrationResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Parse `--vector <table> <column> <dimensions>` (with optional
|
|
61
|
+
* `--metric <cosine|l2|inner-product>`) out of the `make:migration`
|
|
62
|
+
* CLI args. Returns `null` if `--vector` isn't present so the standard
|
|
63
|
+
* delegation to prisma/drizzle-kit can run.
|
|
64
|
+
*
|
|
65
|
+
* Exported for testing.
|
|
66
|
+
*/
|
|
67
|
+
export declare function parseVectorFlag(args: readonly string[]): {
|
|
68
|
+
table: string;
|
|
69
|
+
column: string;
|
|
70
|
+
dimensions: number;
|
|
71
|
+
metric?: 'cosine' | 'l2' | 'inner-product';
|
|
72
|
+
} | null;
|
|
19
73
|
/**
|
|
20
74
|
* Register all migrate/db commands with the rudder CLI.
|
|
21
75
|
* Called by the CLI's eager-load mechanism (no provider boot needed).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,GAAG,GAAG,QAAQ,GAAG,SAAS,CAAA;AAItC,2EAA2E;AAC3E,wBAAgB,SAAS,CAAC,GAAG,GAAE,MAAsB,GAAG,GAAG,GAAG,IAAI,CAUjE;AAWD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAMzE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAOxE;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CACvB,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,SAAS,GAAG,eAAe,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,SAAS,GAAG,aAAa,EACtG,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5C,MAAM,EAAE,CAqCV;AAWD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAO,MAAM,CAAA;IAClB,MAAM,EAAM,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB;oFACgF;IAChF,GAAG,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IAC1B,gFAAgF;IAChF,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,eAAe,CAAA;CAC3C;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAO,MAAM,CAAA;IAChB,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,CA0B5E;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,CAqB7E;AAmBD;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,sBAAsB,EAC5B,GAAG,GAAE,MAAsB,EAC3B,GAAG,GAAE,IAAiB,GACrB,OAAO,CAAC,qBAAqB,CAAC,CAkBhC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,eAAe,CAAA;CAAE,GAAG,IAAI,CAyBjK;AAID;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG;QAAE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAAE,GAC3H,IAAI,CA6FN;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0C1E"}
|