@apisr/drizzle-model 0.0.4 → 2.0.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/CHANGELOG.md +22 -0
- package/DISCLAIMER.md +5 -0
- package/README.md +433 -0
- package/TODO.md +8 -61
- package/package.json +5 -3
- package/src/core/dialect.ts +81 -0
- package/src/core/index.ts +24 -0
- package/src/core/query/error.ts +15 -0
- package/src/core/query/joins.ts +663 -0
- package/src/core/query/projection.ts +136 -0
- package/src/core/query/where.ts +449 -0
- package/src/core/result.ts +303 -0
- package/src/core/runtime.ts +636 -0
- package/src/core/transform.ts +119 -0
- package/src/model/builder.ts +40 -6
- package/src/model/config.ts +9 -9
- package/src/model/format.ts +20 -8
- package/src/model/methods/exclude.ts +1 -7
- package/src/model/methods/return.ts +11 -11
- package/src/model/methods/select.ts +2 -8
- package/src/model/model.ts +10 -16
- package/src/model/query/error.ts +1 -0
- package/src/model/result.ts +137 -21
- package/src/types.ts +38 -0
- package/tests/base/count.test.ts +47 -0
- package/tests/base/delete.test.ts +90 -0
- package/tests/base/find.test.ts +209 -0
- package/tests/base/insert.test.ts +152 -0
- package/tests/base/relations.test.ts +593 -0
- package/tests/base/safe.test.ts +91 -0
- package/tests/base/update.test.ts +88 -0
- package/tests/base/upsert.test.ts +121 -0
- package/tests/base.ts +21 -0
- package/src/model/core/joins.ts +0 -364
- package/src/model/core/projection.ts +0 -61
- package/src/model/core/runtime.ts +0 -334
- package/src/model/core/thenable.ts +0 -94
- package/src/model/core/transform.ts +0 -65
- package/src/model/core/where.ts +0 -249
- package/src/model/core/with.ts +0 -28
- package/tests/builder-v2-mysql.type-test.ts +0 -51
- package/tests/builder-v2.type-test.ts +0 -336
- package/tests/builder.test.ts +0 -63
- package/tests/find.test.ts +0 -166
- package/tests/insert.test.ts +0 -247
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Change log
|
|
2
|
+
|
|
3
|
+
## 2.0.1 | 01-03-2026
|
|
4
|
+
- mark `pg` as peerDep
|
|
5
|
+
- add `CHANGELOG.md`
|
|
6
|
+
- add `relations.test.ts` to test relations
|
|
7
|
+
- add `SimplifyDeep<>` from `type-fest` in queries, for better DX of using relations
|
|
8
|
+
- fix relations `include()` function
|
|
9
|
+
|
|
10
|
+
## 2.0.0 | 28-02-2026
|
|
11
|
+
- add `REAMDE.md`
|
|
12
|
+
- add `count()` function
|
|
13
|
+
- add `Simplify<>` to all queries
|
|
14
|
+
- add `returnFirst()` and make `return()` to return array of rows
|
|
15
|
+
- add `omit()` as transformator after `return()/returnFirst()`
|
|
16
|
+
- remake entire core, better JSDoc, OOP over functional, and much more...
|
|
17
|
+
- remake all tests
|
|
18
|
+
- add `safe()` function
|
|
19
|
+
```ts
|
|
20
|
+
const { data: user, error } = userModel.findFirst().safe();
|
|
21
|
+
```
|
|
22
|
+
- and in overall just make better DX
|
package/DISCLAIMER.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# @apisr/drizzle-model
|
|
2
|
+
|
|
3
|
+
> **⚠️ This package is on high development stage! May have bugs**
|
|
4
|
+
|
|
5
|
+
> **⚠️ Requires `drizzle-orm@beta`**
|
|
6
|
+
> This package is built for Drizzle ORM beta versions (`^1.0.0-beta.2-86f844e`).
|
|
7
|
+
|
|
8
|
+
Type-safe, chainable model runtime for **Drizzle ORM**.
|
|
9
|
+
|
|
10
|
+
Build reusable models for tables and relations with a progressive flow:
|
|
11
|
+
|
|
12
|
+
1. **Intent Stage** — declare what you want (`where`, `insert`, `update`, ...)
|
|
13
|
+
2. **Execution Stage** — choose execution (`findMany`, `findFirst`, `return`, `returnFirst`)
|
|
14
|
+
3. **Refinement Stage** — shape the SQL query (`select`, `exclude`, `with`)
|
|
15
|
+
4. **Programmatic Polishing** — post-process the result (`omit`, `raw`, `safe`)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Learning Path (easy → advanced)
|
|
20
|
+
|
|
21
|
+
1. [Install and create your first model](#install-and-first-model)
|
|
22
|
+
2. [Basic reads](#basic-reads)
|
|
23
|
+
3. [Basic writes](#basic-writes)
|
|
24
|
+
4. [Result refinement](#result-refinement)
|
|
25
|
+
5. [Error-safe execution](#error-safe-execution)
|
|
26
|
+
6. [Advanced: model options and extension](#advanced-model-options-and-extension)
|
|
27
|
+
7. [Full API reference](#full-api-reference)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Install and first model
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bun add @apisr/drizzle-model drizzle-orm@beta
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { modelBuilder, esc } from "@apisr/drizzle-model";
|
|
39
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
40
|
+
import * as schema from "./schema";
|
|
41
|
+
import { relations } from "./relations";
|
|
42
|
+
|
|
43
|
+
const db = drizzle(process.env.DATABASE_URL!, { schema, relations });
|
|
44
|
+
|
|
45
|
+
const model = modelBuilder({
|
|
46
|
+
db,
|
|
47
|
+
schema,
|
|
48
|
+
// requires DrizzleORM relations v2. See: https://orm.drizzle.team/docs/relations-v1-v2
|
|
49
|
+
relations,
|
|
50
|
+
dialect: "PostgreSQL",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const userModel = model("user", {});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> `drizzle-orm` is a peer dependency.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Basic reads
|
|
61
|
+
|
|
62
|
+
### Find one
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const user = await userModel.findFirst();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Find many with filter
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const users = await userModel
|
|
72
|
+
.where({ name: esc("Alex") })
|
|
73
|
+
.findMany();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Count
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const total = await userModel.count();
|
|
80
|
+
const verified = await userModel.where({ isVerified: esc(true) }).count();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Basic writes
|
|
86
|
+
|
|
87
|
+
### Insert
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await userModel.insert({
|
|
91
|
+
name: "New User",
|
|
92
|
+
email: "new@example.com",
|
|
93
|
+
age: 18,
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Update
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const updated = await userModel
|
|
101
|
+
.where({ id: esc(1) })
|
|
102
|
+
.update({ name: "Updated" })
|
|
103
|
+
.returnFirst();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Delete
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
await userModel.where({ id: esc(2) }).delete();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Upsert
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const row = await userModel
|
|
116
|
+
.upsert({
|
|
117
|
+
insert: { name: "Alex", email: "alex@ex.com", age: 20 },
|
|
118
|
+
update: { name: "Alex Updated" },
|
|
119
|
+
target: schema.user.email,
|
|
120
|
+
})
|
|
121
|
+
.returnFirst();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Result refinement
|
|
127
|
+
|
|
128
|
+
### Query-side refinement (`findMany` / `findFirst` result)
|
|
129
|
+
|
|
130
|
+
#### Loading relations with `.with()`
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// Load related posts for each user
|
|
134
|
+
const users = await userModel
|
|
135
|
+
.findMany()
|
|
136
|
+
.with({ posts: true });
|
|
137
|
+
|
|
138
|
+
// Nested relations
|
|
139
|
+
const users = await userModel
|
|
140
|
+
.findMany()
|
|
141
|
+
.with({
|
|
142
|
+
posts: {
|
|
143
|
+
comments: true,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Multiple relations
|
|
148
|
+
const users = await userModel
|
|
149
|
+
.findMany()
|
|
150
|
+
.with({
|
|
151
|
+
posts: true,
|
|
152
|
+
invitee: true,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Query `where` relations
|
|
156
|
+
const users = await userModel
|
|
157
|
+
.findMany()
|
|
158
|
+
.with({
|
|
159
|
+
posts: postModel.where({
|
|
160
|
+
title: {
|
|
161
|
+
like: "New%"
|
|
162
|
+
}
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### Using `.include()` for type-safe relation values
|
|
168
|
+
|
|
169
|
+
`.include()` is a helper that returns the relation value as-is, used for type-level relation selection:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Pass to .with()
|
|
173
|
+
const users = await userModel.findMany().with({
|
|
174
|
+
posts: postModel.where({
|
|
175
|
+
title: {
|
|
176
|
+
like: "New%"
|
|
177
|
+
}
|
|
178
|
+
}).include({
|
|
179
|
+
comments: true
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### SQL column selection with `.select()` and `.exclude()`
|
|
185
|
+
|
|
186
|
+
`.select()` and `.exclude()` control which columns appear in the SQL `SELECT` clause — they affect the query itself, not just the result.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// Only fetch id and name columns
|
|
190
|
+
const users = await userModel
|
|
191
|
+
.findMany()
|
|
192
|
+
.select({ id: true, name: true });
|
|
193
|
+
|
|
194
|
+
// Fetch all columns except email
|
|
195
|
+
const users = await userModel
|
|
196
|
+
.findMany()
|
|
197
|
+
.exclude({ email: true });
|
|
198
|
+
|
|
199
|
+
// Combine: start with a whitelist, then drop a field
|
|
200
|
+
const users = await userModel
|
|
201
|
+
.findMany()
|
|
202
|
+
.select({ id: true, name: true, email: true })
|
|
203
|
+
.exclude({ email: true });
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
This is equivalent to:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
db.select({ id: schema.user.id, name: schema.user.name }).from(schema.user);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### Combining query refiners
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const users = await userModel
|
|
216
|
+
.findMany()
|
|
217
|
+
.with({ posts: true })
|
|
218
|
+
.select({ id: true, name: true });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Available query refiners:
|
|
222
|
+
|
|
223
|
+
- `.select(fields)` — SQL SELECT whitelist
|
|
224
|
+
- `.exclude(fields)` — SQL SELECT blacklist
|
|
225
|
+
- `.with(relations)` — load related entities via JOINs
|
|
226
|
+
- `.raw()` — skip format function
|
|
227
|
+
- `.safe()` — wrap in `{ data, error }`
|
|
228
|
+
- `.debug()` — inspect query state
|
|
229
|
+
|
|
230
|
+
### Mutation-side refinement (`insert` / `update` / `delete` / `upsert` result)
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const rows = await userModel
|
|
234
|
+
.insert({ email: "a@b.com", name: "Alex", age: 20 })
|
|
235
|
+
.return();
|
|
236
|
+
|
|
237
|
+
const first = await userModel
|
|
238
|
+
.insert({ email: "b@b.com", name: "Anna", age: 21 })
|
|
239
|
+
.returnFirst();
|
|
240
|
+
|
|
241
|
+
// .omit() removes fields from the result AFTER the query runs (programmatic, not SQL)
|
|
242
|
+
const sanitized = await userModel
|
|
243
|
+
.where({ id: esc(1) })
|
|
244
|
+
.update({ secretField: 999 })
|
|
245
|
+
.returnFirst()
|
|
246
|
+
.omit({ secretField: true });
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Available mutation refiners:
|
|
250
|
+
|
|
251
|
+
- `.return(fields?)` — return all rows
|
|
252
|
+
- `.returnFirst(fields?)` — return first row
|
|
253
|
+
- `.omit(fields)` — remove fields from result after query (programmatic, not SQL)
|
|
254
|
+
- `.safe()` — wrap in `{ data, error }`
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Error-safe execution
|
|
259
|
+
|
|
260
|
+
Use `.safe()` when you prefer a result object instead of throw/reject behavior.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const result = await userModel.findMany().safe();
|
|
264
|
+
|
|
265
|
+
if (result.error) {
|
|
266
|
+
console.error(result.error);
|
|
267
|
+
} else {
|
|
268
|
+
console.log(result.data);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Shape:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
type SafeResult<T> =
|
|
276
|
+
| { data: T; error: undefined }
|
|
277
|
+
| { data: undefined; error: unknown };
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Advanced: model options and extension
|
|
283
|
+
|
|
284
|
+
### `format`
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const userModel = model("user", {
|
|
288
|
+
format(row) {
|
|
289
|
+
const { secretField, ...rest } = row;
|
|
290
|
+
return {
|
|
291
|
+
...rest,
|
|
292
|
+
isVerified: Boolean(rest.isVerified),
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Use `.raw()` to bypass format.
|
|
299
|
+
|
|
300
|
+
### Default `where`
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
const activeUsers = model("user", {
|
|
304
|
+
where: { isVerified: esc(true) },
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Custom `methods`
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
const userModel = model("user", {
|
|
312
|
+
methods: {
|
|
313
|
+
async byEmail(email: string) {
|
|
314
|
+
return await userModel.where({ email: esc(email) }).findFirst();
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### `extend()` and `db()`
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const extended = userModel.extend({
|
|
324
|
+
methods: {
|
|
325
|
+
async adults() {
|
|
326
|
+
return await userModel.where({ age: { gte: esc(18) } }).findMany();
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const txUserModel = userModel.db(db);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Note: when method names conflict during `extend`, existing runtime methods take precedence over newly passed ones.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Full API reference
|
|
339
|
+
|
|
340
|
+
### Model-level methods
|
|
341
|
+
|
|
342
|
+
- Query/lifecycle:
|
|
343
|
+
- `where(value)`
|
|
344
|
+
- `findMany()`
|
|
345
|
+
- `findFirst()`
|
|
346
|
+
- `count()`
|
|
347
|
+
- `include(value)`
|
|
348
|
+
- `extend(options)`
|
|
349
|
+
- `db(dbInstance)`
|
|
350
|
+
- Mutations:
|
|
351
|
+
- `insert(value)`
|
|
352
|
+
- `update(value)`
|
|
353
|
+
- `delete()`
|
|
354
|
+
- `upsert(value)`
|
|
355
|
+
|
|
356
|
+
### Query result methods
|
|
357
|
+
|
|
358
|
+
- `.with(...)`
|
|
359
|
+
- `.select(...)`
|
|
360
|
+
- `.exclude(...)`
|
|
361
|
+
- `.raw()`
|
|
362
|
+
- `.safe()`
|
|
363
|
+
- `.debug()`
|
|
364
|
+
|
|
365
|
+
### Mutation result methods
|
|
366
|
+
|
|
367
|
+
- `.return(...)`
|
|
368
|
+
- `.returnFirst(...)`
|
|
369
|
+
- `.omit(...)`
|
|
370
|
+
- `.safe()`
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Dialect notes
|
|
375
|
+
|
|
376
|
+
- Dialects with native `.returning()` use it for mutation return pipelines.
|
|
377
|
+
- Dialects with ID-only return paths may use dialect-specific fallback behavior.
|
|
378
|
+
- Upsert uses `onConflictDoUpdate` when supported.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Type safety notes
|
|
383
|
+
|
|
384
|
+
- Prefer `esc(...)` for explicit where value/operator expressions.
|
|
385
|
+
- `.select()` and `.exclude()` control SQL SELECT columns and refine result types.
|
|
386
|
+
- `.omit()` removes fields from the result programmatically after the query.
|
|
387
|
+
- `.safe()` wraps result types into `{ data, error }`.
|
|
388
|
+
- `.return()` returns array shape; `.returnFirst()` returns single-row shape.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Testing
|
|
393
|
+
|
|
394
|
+
Comprehensive tests are available in `tests/base`:
|
|
395
|
+
|
|
396
|
+
- `find.test.ts`
|
|
397
|
+
- `insert.test.ts`
|
|
398
|
+
- `update.test.ts`
|
|
399
|
+
- `delete.test.ts`
|
|
400
|
+
- `upsert.test.ts`
|
|
401
|
+
- `count.test.ts`
|
|
402
|
+
- `safe.test.ts`
|
|
403
|
+
- `relations.test.ts`
|
|
404
|
+
|
|
405
|
+
Run all base tests:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
bun test base
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Troubleshooting
|
|
414
|
+
|
|
415
|
+
### `safe()` returns `{ data: undefined, error }`
|
|
416
|
+
|
|
417
|
+
The underlying operation throws. Re-run without `.safe()` to inspect the raw stack.
|
|
418
|
+
|
|
419
|
+
### `.return()` result shape surprises
|
|
420
|
+
|
|
421
|
+
- `.return()` => array
|
|
422
|
+
- `.returnFirst()` => single object
|
|
423
|
+
- no return chain => dialect/default execution behavior
|
|
424
|
+
|
|
425
|
+
### Relation loading with `.with(...)`
|
|
426
|
+
|
|
427
|
+
Ensure relation metadata is defined with Drizzle `defineRelations` and passed to `modelBuilder({ relations })`.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## License
|
|
432
|
+
|
|
433
|
+
MIT (follow repository root license if different).
|
package/TODO.md
CHANGED
|
@@ -1,64 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
- Make `route` function (Just duplicate):
|
|
3
|
-
```ts
|
|
4
|
-
const [val1, val2] = await userModel.name({
|
|
5
|
-
like: "A%"
|
|
6
|
-
}).route(
|
|
7
|
-
(userModel) => userModel.isVerified(true).findMany(),
|
|
8
|
-
(userModel) => userModel.isVerified(false).age({
|
|
9
|
-
lte: 18
|
|
10
|
-
}).findMany()
|
|
11
|
-
// ...args[]
|
|
12
|
-
)
|
|
13
|
-
```
|
|
14
|
-
- Make built in `paginate` which is pagination function, used as:
|
|
15
|
-
```ts
|
|
16
|
-
const userModel = model({
|
|
17
|
-
table: userTable,
|
|
18
|
-
|
|
19
|
-
// Optional
|
|
20
|
-
pagination: {
|
|
21
|
-
max: 10
|
|
22
|
-
}
|
|
23
|
-
})
|
|
1
|
+
# TODO:
|
|
24
2
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* Make built in `upsert`, as:
|
|
3
|
+
- DONE => `count()` function to count rows
|
|
4
|
+
- DONE => Fix types. On insert, and add Simplify<> = for more readable queries.
|
|
5
|
+
- DONE => add `returnFirst()` to return first of `return()`
|
|
6
|
+
- DONE => add `omit()` as progammic `exclude()`. The main difference is `omit()` is applied after query. `exclude()` is applied on the query.
|
|
7
|
+
- DONE JUST remake a entire core. Manually write code with a few AI changes.
|
|
8
|
+
- DONE => Add `safe()`:
|
|
32
9
|
```ts
|
|
33
|
-
userModel.
|
|
34
|
-
// create obj
|
|
35
|
-
}, {
|
|
36
|
-
// update obj
|
|
37
|
-
}, /* options */)
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
* Transactions:
|
|
41
|
-
```ts
|
|
42
|
-
userModel.transaction(tx => ...);
|
|
43
|
-
|
|
44
|
-
// With other models
|
|
45
|
-
userModel.transaction(tx => {
|
|
46
|
-
postsModel.db(tx).insert({
|
|
47
|
-
userId: 123,
|
|
48
|
-
content: ...
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
// or
|
|
52
|
-
|
|
53
|
-
const txPostsModel = postsModel.db(tx);
|
|
54
|
-
|
|
55
|
-
txPostsModel.insert({
|
|
56
|
-
userId: 123,
|
|
57
|
-
content: ...
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
txPostsModel.userId(123).delete()
|
|
61
|
-
});
|
|
10
|
+
const { data: user, error } = userModel.findFirst().safe();
|
|
62
11
|
```
|
|
63
|
-
- / Soft delete?
|
|
64
|
-
- * Dialect based configuration `mysql`, `pgsql` and etc... like `returning()` on pgsql and `$returningId()` on mysql
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apisr/drizzle-model",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"lint": "eslint . --max-warnings 0",
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
"check-types": "tsc --noEmit"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"drizzle-orm": "^1.0.0-beta.2-86f844e"
|
|
11
|
+
"drizzle-orm": "^1.0.0-beta.2-86f844e",
|
|
12
|
+
"pg": "^8.16.3"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"@repo/eslint-config": "*",
|
|
15
16
|
"@repo/typescript-config": "*",
|
|
17
|
+
"@apisr/logger": "^0.0.3",
|
|
16
18
|
"@types/bun": "latest",
|
|
17
19
|
"@types/node": "^22.15.3",
|
|
18
20
|
"@types/pg": "^8.15.6",
|
|
@@ -30,6 +32,6 @@
|
|
|
30
32
|
},
|
|
31
33
|
"type": "module",
|
|
32
34
|
"dependencies": {
|
|
33
|
-
"
|
|
35
|
+
"type-fest": "^5.4.4"
|
|
34
36
|
}
|
|
35
37
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ModelDialect } from "../model/dialect.ts";
|
|
2
|
+
|
|
3
|
+
/** Alias for a generic record with string keys. */
|
|
4
|
+
type AnyRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Encapsulates dialect-specific logic for different SQL databases.
|
|
8
|
+
*
|
|
9
|
+
* Provides helpers for determining returning-id behaviour and
|
|
10
|
+
* lazily importing the correct Drizzle `alias()` function based on the dialect.
|
|
11
|
+
*/
|
|
12
|
+
export class DialectHelper {
|
|
13
|
+
/** The dialect string identifying the target database engine. */
|
|
14
|
+
readonly dialect: ModelDialect;
|
|
15
|
+
|
|
16
|
+
constructor(dialect: ModelDialect) {
|
|
17
|
+
this.dialect = dialect;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns `true` when the dialect only supports `$returningId()`
|
|
22
|
+
* instead of the standard `.returning()` method.
|
|
23
|
+
*
|
|
24
|
+
* Applies to MySQL, SingleStore and CockroachDB.
|
|
25
|
+
*/
|
|
26
|
+
isReturningIdOnly(): boolean {
|
|
27
|
+
return (
|
|
28
|
+
this.dialect === "MySQL" ||
|
|
29
|
+
this.dialect === "SingleStore" ||
|
|
30
|
+
this.dialect === "CockroachDB"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a table alias using the dialect-specific Drizzle module.
|
|
36
|
+
*
|
|
37
|
+
* Dynamically imports the correct `alias` utility so the core
|
|
38
|
+
* does not carry a hard dependency on every dialect.
|
|
39
|
+
*
|
|
40
|
+
* @param table - The Drizzle table object to alias.
|
|
41
|
+
* @param aliasName - The SQL alias name.
|
|
42
|
+
* @returns The aliased table, or the original table if aliasing is unavailable.
|
|
43
|
+
*/
|
|
44
|
+
async createTableAlias(
|
|
45
|
+
table: AnyRecord,
|
|
46
|
+
aliasName: string
|
|
47
|
+
): Promise<AnyRecord> {
|
|
48
|
+
const modulePath = this.getDialectModulePath();
|
|
49
|
+
if (!modulePath) {
|
|
50
|
+
return table;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const mod: AnyRecord = await import(modulePath);
|
|
54
|
+
if (typeof mod.alias === "function") {
|
|
55
|
+
return (mod.alias as (t: AnyRecord, name: string) => AnyRecord)(
|
|
56
|
+
table,
|
|
57
|
+
aliasName
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return table;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolves the Drizzle ORM module path for the current dialect.
|
|
66
|
+
*
|
|
67
|
+
* @returns The import specifier, or `undefined` for unsupported dialects.
|
|
68
|
+
*/
|
|
69
|
+
private getDialectModulePath(): string | undefined {
|
|
70
|
+
switch (this.dialect) {
|
|
71
|
+
case "PostgreSQL":
|
|
72
|
+
return "drizzle-orm/pg-core";
|
|
73
|
+
case "MySQL":
|
|
74
|
+
return "drizzle-orm/mysql-core";
|
|
75
|
+
case "SQLite":
|
|
76
|
+
return "drizzle-orm/sqlite-core";
|
|
77
|
+
default:
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Core v2 — public API
|
|
2
|
+
export { DialectHelper } from "./dialect.ts";
|
|
3
|
+
export { QueryError } from "./query/error.ts";
|
|
4
|
+
export {
|
|
5
|
+
JoinExecutor,
|
|
6
|
+
type JoinExecutorConfig,
|
|
7
|
+
type JoinNode,
|
|
8
|
+
} from "./query/joins.ts";
|
|
9
|
+
export {
|
|
10
|
+
ProjectionBuilder,
|
|
11
|
+
type ProjectionResult,
|
|
12
|
+
} from "./query/projection.ts";
|
|
13
|
+
export { WhereCompiler } from "./query/where.ts";
|
|
14
|
+
export {
|
|
15
|
+
type MutateKind,
|
|
16
|
+
MutateResult,
|
|
17
|
+
type MutateState,
|
|
18
|
+
QueryResult,
|
|
19
|
+
type QueryState,
|
|
20
|
+
type SafeResult,
|
|
21
|
+
ThenableResult,
|
|
22
|
+
} from "./result.ts";
|
|
23
|
+
export { ModelRuntime, type ModelRuntimeConfig } from "./runtime.ts";
|
|
24
|
+
export { ResultTransformer } from "./transform.ts";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents an error that occurred during query compilation or execution.
|
|
3
|
+
*
|
|
4
|
+
* Extends the native `Error` class so it can be caught independently
|
|
5
|
+
* from generic runtime errors.
|
|
6
|
+
*/
|
|
7
|
+
export class QueryError extends Error {
|
|
8
|
+
/** Discriminator for runtime type checks. */
|
|
9
|
+
readonly kind = "QueryError" as const;
|
|
10
|
+
|
|
11
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.name = "QueryError";
|
|
14
|
+
}
|
|
15
|
+
}
|