@apisr/drizzle-model 2.0.2 → 2.0.4

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 CHANGED
@@ -1,6 +1,13 @@
1
1
  # Change log
2
2
 
3
- ## 2.0.2 | 01-0-2026
3
+ ## 2.0.4 | 02-03-2026
4
+ - Fix custom methods execution error
5
+ - Add custom methods tests
6
+
7
+ ## 2.0.3 | 02-03-2026
8
+ - I forgot to rebuild package in `2.0.2 to 2.0.0`, damn
9
+
10
+ ## 2.0.2 | 01-03-2026
4
11
  - add `esc.*()` operations
5
12
  - update README.md with new `esc.*()`
6
13
 
package/README.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # @apisr/drizzle-model
2
2
 
3
- > **⚠️ This package is on high development stage! May have bugs**
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. [Advanced: model options and extension](#advanced-model-options-and-extension)
27
- 7. [Full API reference](#full-api-reference)
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 returns the relation value as-is, used for type-level relation selection:
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
- // Pass to .with()
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
- ### 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()`
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
@@ -1 +1,4 @@
1
+ # Roadmap
2
+
1
3
  - Support the (`Views`)[https://orm.drizzle.team/docs/views] on future.
4
+ - Support `format` in relations (joins). Not sure, as I dont know how to make right API for this
@@ -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 };