@apisr/drizzle-model 2.0.2 → 2.0.3
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 +4 -1
- package/README.md +280 -37
- package/ROADMAP.md +3 -0
- package/dist/core/dialect.mjs +56 -0
- package/dist/core/query/joins.mjs +334 -0
- package/dist/core/query/projection.mjs +78 -0
- package/dist/core/query/where.mjs +265 -0
- package/dist/core/result.mjs +215 -0
- package/dist/core/runtime.mjs +393 -0
- package/dist/core/transform.mjs +83 -0
- package/dist/index.d.mts +4 -4
- package/dist/model/builder.mjs +22 -2
- package/dist/model/config.d.mts +6 -6
- package/dist/model/format.d.mts +6 -2
- package/dist/model/index.d.mts +2 -2
- package/dist/model/methods/exclude.d.mts +1 -1
- package/dist/model/methods/return.d.mts +2 -2
- package/dist/model/methods/select.d.mts +1 -1
- package/dist/model/model.d.mts +10 -12
- package/dist/model/query/error.d.mts +4 -0
- package/dist/model/query/operations.d.mts +89 -39
- package/dist/model/query/operations.mjs +12 -0
- package/dist/model/result.d.mts +26 -10
- package/dist/types.d.mts +16 -1
- package/package.json +1 -1
- package/tests/snippets/x-2.ts +2 -0
- package/dist/model/core/joins.mjs +0 -184
- package/dist/model/core/projection.mjs +0 -28
- package/dist/model/core/runtime.mjs +0 -198
- package/dist/model/core/thenable.mjs +0 -64
- package/dist/model/core/transform.mjs +0 -39
- package/dist/model/core/where.mjs +0 -130
- package/dist/model/core/with.mjs +0 -19
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# @apisr/drizzle-model
|
|
2
2
|
|
|
3
|
-
> **⚠️
|
|
3
|
+
> **⚠️ Development Status**
|
|
4
|
+
> This package is in active development and **not recommended for production use yet**.
|
|
5
|
+
> APIs may change between minor versions. Semantic versioning will be enforced after v1.0.0 stable release.
|
|
4
6
|
|
|
5
7
|
> **⚠️ Requires `drizzle-orm@beta`**
|
|
6
8
|
> This package is built for Drizzle ORM beta versions (`^1.0.0-beta.2-86f844e`).
|
|
9
|
+
> See [Compatibility](#compatibility) for details.
|
|
7
10
|
|
|
8
11
|
Type-safe, chainable model runtime for **Drizzle ORM**.
|
|
9
12
|
|
|
@@ -16,6 +19,58 @@ Build reusable models for tables and relations with a progressive flow:
|
|
|
16
19
|
|
|
17
20
|
---
|
|
18
21
|
|
|
22
|
+
## Philosophy
|
|
23
|
+
|
|
24
|
+
Drizzle ORM gives you low-level composable primitives.
|
|
25
|
+
|
|
26
|
+
`@apisr/drizzle-model` adds:
|
|
27
|
+
|
|
28
|
+
- **A model abstraction per table** — encapsulate table logic in reusable models
|
|
29
|
+
- **A progressive query pipeline** — build queries step-by-step with clear intent
|
|
30
|
+
- **A unified result-shaping layer** — consistent formatting and transformation
|
|
31
|
+
- **Safe execution flows** — error handling without try-catch boilerplate
|
|
32
|
+
- **Reusable business logic extensions** — custom methods and model composition
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Why not just use Drizzle directly?
|
|
37
|
+
|
|
38
|
+
Drizzle ORM is already type-safe and powerful.
|
|
39
|
+
|
|
40
|
+
`@apisr/drizzle-model` adds value when you need:
|
|
41
|
+
|
|
42
|
+
- **Reusable model abstraction per table** — define once, use everywhere
|
|
43
|
+
- **Chainable intent → execution flow** — progressive, readable query building
|
|
44
|
+
- **Built-in result shaping** — consistent formatting and field selection
|
|
45
|
+
- **Centralized formatting layer** — transform data in one place
|
|
46
|
+
- **Safer error pipelines** — `.safe()` for error-as-value patterns
|
|
47
|
+
- **Composable model extensions** — custom methods and model inheritance
|
|
48
|
+
|
|
49
|
+
### Quick Comparison
|
|
50
|
+
|
|
51
|
+
**Without drizzle-model:**
|
|
52
|
+
```ts
|
|
53
|
+
import { eq } from "drizzle-orm";
|
|
54
|
+
|
|
55
|
+
await db
|
|
56
|
+
.select()
|
|
57
|
+
.from(schema.user)
|
|
58
|
+
.where(eq(schema.user.id, 1));
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**With drizzle-model:**
|
|
62
|
+
```ts
|
|
63
|
+
await userModel.where({ id: esc(1) }).findFirst();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The difference becomes more apparent with:
|
|
67
|
+
- Consistent formatting across queries
|
|
68
|
+
- Reusable where conditions
|
|
69
|
+
- Nested relation loading
|
|
70
|
+
- Custom business logic methods
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
19
74
|
## Learning Path (easy → advanced)
|
|
20
75
|
|
|
21
76
|
1. [Install and create your first model](#install-and-first-model)
|
|
@@ -23,8 +78,12 @@ Build reusable models for tables and relations with a progressive flow:
|
|
|
23
78
|
3. [Basic writes](#basic-writes)
|
|
24
79
|
4. [Result refinement](#result-refinement)
|
|
25
80
|
5. [Error-safe execution](#error-safe-execution)
|
|
26
|
-
6. [
|
|
27
|
-
7. [
|
|
81
|
+
6. [Transactions](#transactions)
|
|
82
|
+
7. [Advanced: model options and extension](#advanced-model-options-and-extension)
|
|
83
|
+
8. [Performance considerations](#performance-considerations)
|
|
84
|
+
9. [Limitations](#limitations)
|
|
85
|
+
10. [Full API reference](#full-api-reference)
|
|
86
|
+
11. [Compatibility](#compatibility)
|
|
28
87
|
|
|
29
88
|
---
|
|
30
89
|
|
|
@@ -51,6 +110,17 @@ const model = modelBuilder({
|
|
|
51
110
|
});
|
|
52
111
|
|
|
53
112
|
const userModel = model("user", {});
|
|
113
|
+
|
|
114
|
+
// Real-world example with formatting
|
|
115
|
+
const postModel = model("post", {
|
|
116
|
+
format(row) {
|
|
117
|
+
return {
|
|
118
|
+
...row,
|
|
119
|
+
createdAt: new Date(row.createdAt),
|
|
120
|
+
updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
|
54
124
|
```
|
|
55
125
|
|
|
56
126
|
> `drizzle-orm` is a peer dependency.
|
|
@@ -68,9 +138,15 @@ const user = await userModel.findFirst();
|
|
|
68
138
|
### Find many with filter
|
|
69
139
|
|
|
70
140
|
```ts
|
|
141
|
+
// ✅ Correct: use esc() for literal values
|
|
71
142
|
const users = await userModel
|
|
72
143
|
.where({ name: esc("Alex") })
|
|
73
144
|
.findMany();
|
|
145
|
+
|
|
146
|
+
// ❌ Wrong: plain values are not allowed
|
|
147
|
+
// const users = await userModel
|
|
148
|
+
// .where({ name: "Alex" }) // Type error!
|
|
149
|
+
// .findMany();
|
|
74
150
|
```
|
|
75
151
|
|
|
76
152
|
### Count
|
|
@@ -166,10 +242,15 @@ const users = await userModel
|
|
|
166
242
|
|
|
167
243
|
#### Using `.include()` for type-safe relation values
|
|
168
244
|
|
|
169
|
-
`.include()` is a helper that
|
|
245
|
+
`.include()` is a helper that allows you to specify nested relations when using a model instance in `.with()`.
|
|
246
|
+
|
|
247
|
+
**Why it exists:**
|
|
248
|
+
- When you pass a model with `.where()` to `.with()`, you need a way to also load nested relations
|
|
249
|
+
- `.include()` preserves the model's where clause while adding relation loading
|
|
250
|
+
- It's purely type-level — it doesn't affect SQL directly, but enables type-safe nested relation selection
|
|
170
251
|
|
|
171
252
|
```ts
|
|
172
|
-
//
|
|
253
|
+
// Load posts with a filter AND their comments
|
|
173
254
|
const users = await userModel.findMany().with({
|
|
174
255
|
posts: postModel.where({
|
|
175
256
|
title: {
|
|
@@ -179,6 +260,11 @@ const users = await userModel.findMany().with({
|
|
|
179
260
|
comments: true
|
|
180
261
|
})
|
|
181
262
|
});
|
|
263
|
+
|
|
264
|
+
// Without .include(), you can only filter posts:
|
|
265
|
+
const users = await userModel.findMany().with({
|
|
266
|
+
posts: postModel.where({ published: esc(true) })
|
|
267
|
+
});
|
|
182
268
|
```
|
|
183
269
|
|
|
184
270
|
#### SQL column selection with `.select()` and `.exclude()`
|
|
@@ -253,6 +339,32 @@ Available mutation refiners:
|
|
|
253
339
|
- `.omit(fields)` — remove fields from result after query (programmatic, not SQL)
|
|
254
340
|
- `.safe()` — wrap in `{ data, error }`
|
|
255
341
|
|
|
342
|
+
### Edge case behavior
|
|
343
|
+
|
|
344
|
+
**What happens when `.findFirst()` returns nothing?**
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
const user = await userModel.where({ id: esc(999) }).findFirst();
|
|
348
|
+
// user is `undefined` if no row matches
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**What does `.returnFirst()` return if no row was affected?**
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
const updated = await userModel
|
|
355
|
+
.where({ id: esc(999) })
|
|
356
|
+
.update({ name: "New" })
|
|
357
|
+
.returnFirst();
|
|
358
|
+
// updated is `undefined` if no row was updated
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Return nullability:**
|
|
362
|
+
- `.findFirst()` → `T | undefined`
|
|
363
|
+
- `.findMany()` → `T[]` (empty array if no matches)
|
|
364
|
+
- `.returnFirst()` → `T | undefined`
|
|
365
|
+
- `.return()` → `T[]` (empty array if no rows affected)
|
|
366
|
+
- `.count()` → `number` (0 if no matches)
|
|
367
|
+
|
|
256
368
|
---
|
|
257
369
|
|
|
258
370
|
## Error-safe execution
|
|
@@ -279,10 +391,39 @@ type SafeResult<T> =
|
|
|
279
391
|
|
|
280
392
|
---
|
|
281
393
|
|
|
394
|
+
## Transactions
|
|
395
|
+
|
|
396
|
+
Use `.db()` to bind a model to a transaction instance:
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
await db.transaction(async (tx) => {
|
|
400
|
+
const txUser = userModel.db(tx);
|
|
401
|
+
const txPost = postModel.db(tx);
|
|
402
|
+
|
|
403
|
+
const user = await txUser.insert({
|
|
404
|
+
name: "Alice",
|
|
405
|
+
email: "alice@example.com",
|
|
406
|
+
age: 25,
|
|
407
|
+
}).returnFirst();
|
|
408
|
+
|
|
409
|
+
await txPost.insert({
|
|
410
|
+
title: "First Post",
|
|
411
|
+
content: "Hello world",
|
|
412
|
+
authorId: user.id,
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The transaction-bound model uses the same API as the regular model.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
282
421
|
## Advanced: model options and extension
|
|
283
422
|
|
|
284
423
|
### `format`
|
|
285
424
|
|
|
425
|
+
Transform every row returned from queries:
|
|
426
|
+
|
|
286
427
|
```ts
|
|
287
428
|
const userModel = model("user", {
|
|
288
429
|
format(row) {
|
|
@@ -293,9 +434,27 @@ const userModel = model("user", {
|
|
|
293
434
|
};
|
|
294
435
|
},
|
|
295
436
|
});
|
|
437
|
+
|
|
438
|
+
// Real-world example: date parsing and sanitization
|
|
439
|
+
const postModel = model("post", {
|
|
440
|
+
format(row) {
|
|
441
|
+
return {
|
|
442
|
+
...row,
|
|
443
|
+
createdAt: new Date(row.createdAt),
|
|
444
|
+
updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,
|
|
445
|
+
// Remove internal fields
|
|
446
|
+
internalStatus: undefined,
|
|
447
|
+
};
|
|
448
|
+
},
|
|
449
|
+
});
|
|
296
450
|
```
|
|
297
451
|
|
|
298
|
-
Use `.raw()` to bypass format
|
|
452
|
+
Use `.raw()` to bypass format when needed:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
const rawUser = await userModel.findFirst().raw();
|
|
456
|
+
// secretField is present, isVerified is original type
|
|
457
|
+
```
|
|
299
458
|
|
|
300
459
|
### Default `where`
|
|
301
460
|
|
|
@@ -335,39 +494,123 @@ Note: when method names conflict during `extend`, existing runtime methods take
|
|
|
335
494
|
|
|
336
495
|
---
|
|
337
496
|
|
|
497
|
+
## Performance considerations
|
|
498
|
+
|
|
499
|
+
### Relation loading with `.with()`
|
|
500
|
+
|
|
501
|
+
- **No N+1 queries** — `.with()` uses JOIN-based loading, not separate queries per row
|
|
502
|
+
- **Deep nesting** may generate larger queries — use `.select()` to reduce payload size
|
|
503
|
+
- **Selective loading** — only load relations you need
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
// ✅ Good: selective loading
|
|
507
|
+
const users = await userModel
|
|
508
|
+
.findMany()
|
|
509
|
+
.with({ posts: true })
|
|
510
|
+
.select({ id: true, name: true });
|
|
511
|
+
|
|
512
|
+
// ⚠️ Careful: deep nesting + all columns
|
|
513
|
+
const users = await userModel
|
|
514
|
+
.findMany()
|
|
515
|
+
.with({
|
|
516
|
+
posts: {
|
|
517
|
+
comments: {
|
|
518
|
+
author: true
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
// This works, but generates a large query with many JOINs
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Query optimization tips
|
|
526
|
+
|
|
527
|
+
- Use `.select()` to fetch only needed columns
|
|
528
|
+
- Use `.count()` instead of `.findMany()` when you only need the count
|
|
529
|
+
- Add indexes on columns used in `.where()` conditions
|
|
530
|
+
- Use `.raw()` to skip formatting when performance is critical
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Limitations
|
|
535
|
+
|
|
536
|
+
Current limitations you should be aware of:
|
|
537
|
+
|
|
538
|
+
- **Requires Drizzle ORM relations v2** — v1 relations are not supported
|
|
539
|
+
- **Explicit `esc()` required** — plain values in `.where()` are not allowed (by design for type safety)
|
|
540
|
+
- **No lazy loading** — relations must be loaded eagerly with `.with()`
|
|
541
|
+
- **No middleware system** — use `format()` for transformations
|
|
542
|
+
- **No query caching** — implement caching at application level if needed
|
|
543
|
+
- **No automatic soft deletes** — implement via default `where` conditions
|
|
544
|
+
- **No polymorphic relations** — standard Drizzle relation limitations apply
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
338
548
|
## Full API reference
|
|
339
549
|
|
|
340
|
-
###
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
- `.
|
|
361
|
-
- `.
|
|
362
|
-
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
- `.
|
|
369
|
-
- `.
|
|
370
|
-
- `.
|
|
550
|
+
### Intent Stage
|
|
551
|
+
|
|
552
|
+
Declare what you want to do:
|
|
553
|
+
|
|
554
|
+
- `where(value)` — filter conditions
|
|
555
|
+
- `insert(value)` — insert new rows
|
|
556
|
+
- `update(value)` — update existing rows
|
|
557
|
+
- `delete()` — delete rows
|
|
558
|
+
- `upsert(value)` — insert or update
|
|
559
|
+
|
|
560
|
+
### Execution Stage
|
|
561
|
+
|
|
562
|
+
Choose how to execute:
|
|
563
|
+
|
|
564
|
+
**Queries:**
|
|
565
|
+
- `findMany()` — fetch multiple rows
|
|
566
|
+
- `findFirst()` — fetch first matching row
|
|
567
|
+
- `count()` — count matching rows
|
|
568
|
+
|
|
569
|
+
**Mutations:**
|
|
570
|
+
- `.return()` — return all affected rows
|
|
571
|
+
- `.returnFirst()` — return first affected row
|
|
572
|
+
- (no return chain) — execute without returning rows
|
|
573
|
+
|
|
574
|
+
### Refinement Stage
|
|
575
|
+
|
|
576
|
+
Shape the SQL query:
|
|
577
|
+
|
|
578
|
+
- `.with(relations)` — load related entities via JOINs
|
|
579
|
+
- `.select(fields)` — SQL SELECT whitelist
|
|
580
|
+
- `.exclude(fields)` — SQL SELECT blacklist
|
|
581
|
+
|
|
582
|
+
### Programmatic Stage
|
|
583
|
+
|
|
584
|
+
Post-process the result:
|
|
585
|
+
|
|
586
|
+
- `.omit(fields)` — remove fields from result after query
|
|
587
|
+
- `.raw()` — skip format function
|
|
588
|
+
- `.safe()` — wrap in `{ data, error }`
|
|
589
|
+
- `.debug()` — inspect query state
|
|
590
|
+
|
|
591
|
+
### Model-level utilities
|
|
592
|
+
|
|
593
|
+
- `include(value)` — specify nested relations for model instances in `.with()`
|
|
594
|
+
- `extend(options)` — create extended model with additional methods
|
|
595
|
+
- `db(dbInstance)` — bind model to different db/transaction instance
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Compatibility
|
|
600
|
+
|
|
601
|
+
| Drizzle Version | Supported | Notes |
|
|
602
|
+
|------------------|-----------|-------|
|
|
603
|
+
| v1 beta (≥ 1.0.0-beta.2) | ✅ Yes | Requires relations v2 |
|
|
604
|
+
| v0.x stable | ❌ No | Relations v1 not supported |
|
|
605
|
+
|
|
606
|
+
**Supported dialects:**
|
|
607
|
+
- PostgreSQL
|
|
608
|
+
- MySQL
|
|
609
|
+
- SQLite
|
|
610
|
+
|
|
611
|
+
**Node.js version:**
|
|
612
|
+
- Node.js 18+ recommended
|
|
613
|
+
- Bun 1.0+ supported
|
|
371
614
|
|
|
372
615
|
---
|
|
373
616
|
|
package/ROADMAP.md
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/core/dialect.ts
|
|
2
|
+
/**
|
|
3
|
+
* Encapsulates dialect-specific logic for different SQL databases.
|
|
4
|
+
*
|
|
5
|
+
* Provides helpers for determining returning-id behaviour and
|
|
6
|
+
* lazily importing the correct Drizzle `alias()` function based on the dialect.
|
|
7
|
+
*/
|
|
8
|
+
var DialectHelper = class {
|
|
9
|
+
/** The dialect string identifying the target database engine. */
|
|
10
|
+
dialect;
|
|
11
|
+
constructor(dialect) {
|
|
12
|
+
this.dialect = dialect;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns `true` when the dialect only supports `$returningId()`
|
|
16
|
+
* instead of the standard `.returning()` method.
|
|
17
|
+
*
|
|
18
|
+
* Applies to MySQL, SingleStore and CockroachDB.
|
|
19
|
+
*/
|
|
20
|
+
isReturningIdOnly() {
|
|
21
|
+
return this.dialect === "MySQL" || this.dialect === "SingleStore" || this.dialect === "CockroachDB";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates a table alias using the dialect-specific Drizzle module.
|
|
25
|
+
*
|
|
26
|
+
* Dynamically imports the correct `alias` utility so the core
|
|
27
|
+
* does not carry a hard dependency on every dialect.
|
|
28
|
+
*
|
|
29
|
+
* @param table - The Drizzle table object to alias.
|
|
30
|
+
* @param aliasName - The SQL alias name.
|
|
31
|
+
* @returns The aliased table, or the original table if aliasing is unavailable.
|
|
32
|
+
*/
|
|
33
|
+
async createTableAlias(table, aliasName) {
|
|
34
|
+
const modulePath = this.getDialectModulePath();
|
|
35
|
+
if (!modulePath) return table;
|
|
36
|
+
const mod = await import(modulePath);
|
|
37
|
+
if (typeof mod.alias === "function") return mod.alias(table, aliasName);
|
|
38
|
+
return table;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the Drizzle ORM module path for the current dialect.
|
|
42
|
+
*
|
|
43
|
+
* @returns The import specifier, or `undefined` for unsupported dialects.
|
|
44
|
+
*/
|
|
45
|
+
getDialectModulePath() {
|
|
46
|
+
switch (this.dialect) {
|
|
47
|
+
case "PostgreSQL": return "drizzle-orm/pg-core";
|
|
48
|
+
case "MySQL": return "drizzle-orm/mysql-core";
|
|
49
|
+
case "SQLite": return "drizzle-orm/sqlite-core";
|
|
50
|
+
default: return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { DialectHelper };
|