@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/DISCLAIMER.md +5 -0
  3. package/README.md +433 -0
  4. package/TODO.md +8 -61
  5. package/package.json +5 -3
  6. package/src/core/dialect.ts +81 -0
  7. package/src/core/index.ts +24 -0
  8. package/src/core/query/error.ts +15 -0
  9. package/src/core/query/joins.ts +663 -0
  10. package/src/core/query/projection.ts +136 -0
  11. package/src/core/query/where.ts +449 -0
  12. package/src/core/result.ts +303 -0
  13. package/src/core/runtime.ts +636 -0
  14. package/src/core/transform.ts +119 -0
  15. package/src/model/builder.ts +40 -6
  16. package/src/model/config.ts +9 -9
  17. package/src/model/format.ts +20 -8
  18. package/src/model/methods/exclude.ts +1 -7
  19. package/src/model/methods/return.ts +11 -11
  20. package/src/model/methods/select.ts +2 -8
  21. package/src/model/model.ts +10 -16
  22. package/src/model/query/error.ts +1 -0
  23. package/src/model/result.ts +137 -21
  24. package/src/types.ts +38 -0
  25. package/tests/base/count.test.ts +47 -0
  26. package/tests/base/delete.test.ts +90 -0
  27. package/tests/base/find.test.ts +209 -0
  28. package/tests/base/insert.test.ts +152 -0
  29. package/tests/base/relations.test.ts +593 -0
  30. package/tests/base/safe.test.ts +91 -0
  31. package/tests/base/update.test.ts +88 -0
  32. package/tests/base/upsert.test.ts +121 -0
  33. package/tests/base.ts +21 -0
  34. package/src/model/core/joins.ts +0 -364
  35. package/src/model/core/projection.ts +0 -61
  36. package/src/model/core/runtime.ts +0 -334
  37. package/src/model/core/thenable.ts +0 -94
  38. package/src/model/core/transform.ts +0 -65
  39. package/src/model/core/where.ts +0 -249
  40. package/src/model/core/with.ts +0 -28
  41. package/tests/builder-v2-mysql.type-test.ts +0 -51
  42. package/tests/builder-v2.type-test.ts +0 -336
  43. package/tests/builder.test.ts +0 -63
  44. package/tests/find.test.ts +0 -166
  45. 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
@@ -0,0 +1,5 @@
1
+ # DISCLAIMER
2
+
3
+ ### What is still not in package
4
+ - Custom columns (Still not planned)
5
+ - Views (Still not planned)
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
- - * Add all essential functions as `update`, `insert`, `delete`, `findMany`, `findFirst`
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
- const page = 1;
26
-
27
- userModel.name({
28
- like: "A%"
29
- }).userId(userId).paginate(page)
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.id(123).upsert({
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": "0.0.4",
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
- "pg": "^8.16.3"
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
+ }