@apisr/drizzle-model 2.0.1 → 2.0.2

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,5 +1,9 @@
1
1
  # Change log
2
2
 
3
+ ## 2.0.2 | 01-0-2026
4
+ - add `esc.*()` operations
5
+ - update README.md with new `esc.*()`
6
+
3
7
  ## 2.0.1 | 01-03-2026
4
8
  - mark `pg` as peerDep
5
9
  - add `CHANGELOG.md`
package/README.md CHANGED
@@ -381,7 +381,45 @@ Note: when method names conflict during `extend`, existing runtime methods take
381
381
 
382
382
  ## Type safety notes
383
383
 
384
- - Prefer `esc(...)` for explicit where value/operator expressions.
384
+ ### Using `esc()` for explicit where expressions
385
+
386
+ The `esc()` function provides three ways to specify comparison operators:
387
+
388
+ **1. Implicit equality (simplest):**
389
+ ```ts
390
+ where({ name: esc("Alex") })
391
+ ```
392
+
393
+ **2. Explicit operator (Drizzle-style):**
394
+ ```ts
395
+ import { gte } from "drizzle-orm";
396
+ where({ age: esc(gte, 18) })
397
+ ```
398
+
399
+ **3. Chainable methods (recommended):**
400
+ ```ts
401
+ where({ name: esc.like("%Alex%") })
402
+ where({ age: esc.gte(18) })
403
+ where({ status: esc.in(["active", "pending"]) })
404
+ where({ price: esc.between(10, 100) })
405
+ ```
406
+
407
+ **Available chainable methods:**
408
+ - `esc.eq(value)` — equality
409
+ - `esc.not(value)` — inequality
410
+ - `esc.gt(value)` — greater than
411
+ - `esc.gte(value)` — greater than or equal
412
+ - `esc.lt(value)` — less than
413
+ - `esc.lte(value)` — less than or equal
414
+ - `esc.like(pattern)` — SQL LIKE pattern matching
415
+ - `esc.ilike(pattern)` — case-insensitive LIKE
416
+ - `esc.in(values)` — value in array
417
+ - `esc.nin(values)` — value not in array
418
+ - `esc.between(min, max)` — value between range
419
+ - `esc.notBetween(min, max)` — value not between range
420
+
421
+ ### Other type safety features
422
+
385
423
  - `.select()` and `.exclude()` control SQL SELECT columns and refine result types.
386
424
  - `.omit()` removes fields from the result programmatically after the query.
387
425
  - `.safe()` wraps result types into `{ data, error }`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apisr/drizzle-model",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "lint": "eslint . --max-warnings 0",
@@ -17,59 +17,59 @@ export type EscapedValue<T> =
17
17
 
18
18
  type OpValue<T> = T | SQL | EscapedValue<T>;
19
19
 
20
- export type ColumnOpsBase<T> = {
20
+ export interface ColumnOpsBase<T> {
21
21
  eq?: OpValue<T>;
22
22
  equal?: OpValue<T>;
23
- not?: OpValue<T>;
24
23
  in?: OpValue<T>[];
25
- nin?: OpValue<T>[];
26
24
  isNull?: boolean;
27
- };
25
+ nin?: OpValue<T>[];
26
+ not?: OpValue<T>;
27
+ }
28
28
 
29
- export type NumberOps = {
29
+ export interface NumberOps {
30
+ between?: [OpValue<number>, OpValue<number>];
30
31
  gt?: OpValue<number>;
31
32
  gte?: OpValue<number>;
32
33
  lt?: OpValue<number>;
33
34
  lte?: OpValue<number>;
34
- between?: [OpValue<number>, OpValue<number>];
35
35
  notBetween?: [OpValue<number>, OpValue<number>];
36
- };
36
+ }
37
37
 
38
- export type StringOps = {
39
- like?: OpValue<string>;
40
- ilike?: OpValue<string>;
41
- startsWith?: OpValue<string>;
42
- endsWith?: OpValue<string>;
38
+ export interface StringOps {
43
39
  contains?: OpValue<string>;
44
- regex?: OpValue<string>;
45
- notRegex?: OpValue<string>;
40
+ endsWith?: OpValue<string>;
41
+ ilike?: OpValue<string>;
46
42
  length?: NumberOps;
47
- };
43
+ like?: OpValue<string>;
44
+ notRegex?: OpValue<string>;
45
+ regex?: OpValue<string>;
46
+ startsWith?: OpValue<string>;
47
+ }
48
48
 
49
- export type BoolOps = {
50
- isTrue?: boolean;
49
+ export interface BoolOps {
51
50
  isFalse?: boolean;
52
- };
51
+ isTrue?: boolean;
52
+ }
53
53
 
54
- export type DateOps = {
55
- before?: OpValue<Date | string>;
54
+ export interface DateOps {
56
55
  after?: OpValue<Date | string>;
57
- on?: OpValue<Date | string>;
58
- notOn?: OpValue<Date | string>;
56
+ before?: OpValue<Date | string>;
59
57
  between?: [OpValue<Date | string>, OpValue<Date | string>];
60
- };
58
+ notOn?: OpValue<Date | string>;
59
+ on?: OpValue<Date | string>;
60
+ }
61
61
 
62
- export type JsonOps<T> = {
62
+ export interface JsonOps<T> {
63
63
  has?: T;
64
- hasAny?: T[];
65
64
  hasAll?: T[];
65
+ hasAny?: T[];
66
66
  len?: NumberOps;
67
- };
67
+ }
68
68
 
69
- export type LogicalOps<TColumn extends Column> = {
70
- or?: ColumnValue<TColumn>[];
69
+ export interface LogicalOps<TColumn extends Column> {
71
70
  and?: ColumnValue<TColumn>[];
72
- };
71
+ or?: ColumnValue<TColumn>[];
72
+ }
73
73
 
74
74
  export type TypeOps<T> = T extends number
75
75
  ? NumberOps
@@ -103,29 +103,40 @@ export type ColumnValue<
103
103
  * - Drizzle ORM operators should be used directly
104
104
  * - complex types (e.g. Date, objects, custom classes) need safe handling
105
105
  *
106
- * There are two supported forms:
106
+ * There are three supported forms:
107
107
  *
108
108
  * 1) Implicit equality (default behavior):
109
109
  * ```ts
110
110
  * where({ name: esc("Alex") })
111
111
  * ```
112
- * Compiles to:
113
- * ```ts
114
- * {
115
- * eq: "Alex"
116
- * }
117
- * // In drizzle: eq(column, "Alex")
118
- * ```
119
112
  *
120
113
  * 2) Explicit operator (Drizzle-style):
121
114
  * ```ts
122
115
  * where({ age: esc(gte, 18) })
123
116
  * ```
124
- * Compiles to:
117
+ *
118
+ * 3) Chainable operator methods (recommended):
125
119
  * ```ts
126
- * gte(column, 18)
120
+ * where({ name: esc.like("%Alex%") })
121
+ * where({ age: esc.gte(18) })
122
+ * where({ status: esc.in(["active", "pending"]) })
123
+ * where({ price: esc.between(10, 100) })
127
124
  * ```
128
125
  *
126
+ * Available chainable methods:
127
+ * - `esc.eq(value)` — equality
128
+ * - `esc.not(value)` — inequality
129
+ * - `esc.gt(value)` — greater than
130
+ * - `esc.gte(value)` — greater than or equal
131
+ * - `esc.lt(value)` — less than
132
+ * - `esc.lte(value)` — less than or equal
133
+ * - `esc.like(pattern)` — SQL LIKE pattern matching
134
+ * - `esc.ilike(pattern)` — case-insensitive LIKE
135
+ * - `esc.in(values)` — value in array
136
+ * - `esc.nin(values)` — value not in array
137
+ * - `esc.between(min, max)` — value between range
138
+ * - `esc.notBetween(min, max)` — value not between range
139
+ *
129
140
  * The column is injected later during query compilation.
130
141
  * `esc` does NOT execute the operator immediately.
131
142
  *
@@ -169,3 +180,30 @@ export function esc<T>(arg1: any, arg2?: any): EscapedValue<T> {
169
180
  equal: arg1,
170
181
  };
171
182
  }
183
+
184
+ // Chainable operator methods - return DSL objects
185
+ esc.eq = <T>(value: T) => ({ eq: value });
186
+
187
+ esc.not = <T>(value: T) => ({ not: value });
188
+
189
+ esc.gt = <T>(value: T) => ({ gt: value });
190
+
191
+ esc.gte = <T>(value: T) => ({ gte: value });
192
+
193
+ esc.lt = <T>(value: T) => ({ lt: value });
194
+
195
+ esc.lte = <T>(value: T) => ({ lte: value });
196
+
197
+ esc.like = (pattern: string) => ({ like: pattern });
198
+
199
+ esc.ilike = (pattern: string) => ({ ilike: pattern });
200
+
201
+ esc.in = <T>(values: T[]) => ({ in: values });
202
+
203
+ esc.nin = <T>(values: T[]) => ({ nin: values });
204
+
205
+ esc.between = <T>(min: T, max: T) => ({ between: [min, max] as [T, T] });
206
+
207
+ esc.notBetween = <T>(min: T, max: T) => ({
208
+ notBetween: [min, max] as [T, T],
209
+ });
@@ -0,0 +1,159 @@
1
+ import { beforeAll, describe, expect, test } from "bun:test";
2
+ import { model } from "tests/base";
3
+ import { esc } from "@/model";
4
+
5
+ const userModel = model("user", {});
6
+
7
+ function uid(): string {
8
+ return `${Date.now()}-${Math.random()}`;
9
+ }
10
+
11
+ describe("esc chainable methods", () => {
12
+ let testUserId: number;
13
+
14
+ beforeAll(async () => {
15
+ const user = await userModel
16
+ .insert({
17
+ name: "Esc Test User",
18
+ email: `${uid()}@esc.com`,
19
+ age: 25,
20
+ })
21
+ .returnFirst();
22
+ testUserId = user.id;
23
+
24
+ await userModel.insert({
25
+ name: "Alice",
26
+ email: `${uid()}@esc.com`,
27
+ age: 30,
28
+ });
29
+
30
+ await userModel.insert({
31
+ name: "Bob",
32
+ email: `${uid()}@esc.com`,
33
+ age: 20,
34
+ });
35
+ });
36
+
37
+ test("esc.eq() - equality", async () => {
38
+ const users = await userModel.where({ id: esc.eq(testUserId) }).findMany();
39
+
40
+ expect(users).toBeArray();
41
+ expect(users.length).toBe(1);
42
+ expect(users[0]?.id).toBe(testUserId);
43
+ });
44
+
45
+ test("esc.not() - inequality", async () => {
46
+ const users = await userModel
47
+ .where({ id: esc.not(testUserId) })
48
+ .findMany();
49
+
50
+ expect(users).toBeArray();
51
+ for (const user of users) {
52
+ expect(user.id).not.toBe(testUserId);
53
+ }
54
+ });
55
+
56
+ test("esc.gt() - greater than", async () => {
57
+ const users = await userModel.where({ age: esc.gt(25) }).findMany();
58
+
59
+ expect(users).toBeArray();
60
+ for (const user of users) {
61
+ expect(user.age).toBeGreaterThan(25);
62
+ }
63
+ });
64
+
65
+ test("esc.gte() - greater than or equal", async () => {
66
+ const users = await userModel.where({ age: esc.gte(25) }).findMany();
67
+
68
+ expect(users).toBeArray();
69
+ for (const user of users) {
70
+ expect(user.age).toBeGreaterThanOrEqual(25);
71
+ }
72
+ });
73
+
74
+ test("esc.lt() - less than", async () => {
75
+ const users = await userModel.where({ age: esc.lt(25) }).findMany();
76
+
77
+ expect(users).toBeArray();
78
+ for (const user of users) {
79
+ expect(user.age).toBeLessThan(25);
80
+ }
81
+ });
82
+
83
+ test("esc.lte() - less than or equal", async () => {
84
+ const users = await userModel.where({ age: esc.lte(25) }).findMany();
85
+
86
+ expect(users).toBeArray();
87
+ for (const user of users) {
88
+ expect(user.age).toBeLessThanOrEqual(25);
89
+ }
90
+ });
91
+
92
+ test("esc.like() - pattern matching", async () => {
93
+ const users = await userModel.where({ name: esc.like("Esc%") }).findMany();
94
+
95
+ expect(users).toBeArray();
96
+ expect(users.length).toBeGreaterThan(0);
97
+ for (const user of users) {
98
+ expect(user.name.startsWith("Esc")).toBe(true);
99
+ }
100
+ });
101
+
102
+ test("esc.ilike() - case-insensitive pattern matching", async () => {
103
+ const users = await userModel
104
+ .where({ name: esc.ilike("%alice%") })
105
+ .findMany();
106
+
107
+ expect(users).toBeArray();
108
+ expect(users.length).toBeGreaterThan(0);
109
+ for (const user of users) {
110
+ expect(user.name.toLowerCase()).toContain("alice");
111
+ }
112
+ });
113
+
114
+ test("esc.in() - value in array", async () => {
115
+ const users = await userModel
116
+ .where({ name: esc.in(["Alice", "Bob"]) })
117
+ .findMany();
118
+
119
+ expect(users).toBeArray();
120
+ expect(users.length).toBeGreaterThan(0);
121
+ for (const user of users) {
122
+ expect(["Alice", "Bob"]).toContain(user.name);
123
+ }
124
+ });
125
+
126
+ test("esc.nin() - value not in array", async () => {
127
+ const users = await userModel
128
+ .where({ name: esc.nin(["Alice", "Bob"]) })
129
+ .findMany();
130
+
131
+ expect(users).toBeArray();
132
+ for (const user of users) {
133
+ expect(["Alice", "Bob"]).not.toContain(user.name);
134
+ }
135
+ });
136
+
137
+ test("esc.between() - value in range", async () => {
138
+ const users = await userModel
139
+ .where({ age: esc.between(20, 30) })
140
+ .findMany();
141
+
142
+ expect(users).toBeArray();
143
+ for (const user of users) {
144
+ expect(user.age).toBeGreaterThanOrEqual(20);
145
+ expect(user.age).toBeLessThanOrEqual(30);
146
+ }
147
+ });
148
+
149
+ test("esc.notBetween() - value outside range", async () => {
150
+ const users = await userModel
151
+ .where({ age: esc.notBetween(21, 29) })
152
+ .findMany();
153
+
154
+ expect(users).toBeArray();
155
+ for (const user of users) {
156
+ expect(user.age < 21 || user.age > 29).toBe(true);
157
+ }
158
+ });
159
+ });
@@ -0,0 +1,28 @@
1
+ import { esc, modelBuilder } from "src/model";
2
+ import { db } from "../db";
3
+ import { relations } from "../relations";
4
+ import * as schema from "../schema";
5
+
6
+ const model = modelBuilder({
7
+ schema,
8
+ db,
9
+ relations,
10
+ dialect: "PostgreSQL",
11
+ });
12
+
13
+ // create model
14
+ const userModel = model("user", {});
15
+
16
+ await userModel
17
+ .where({
18
+ name: {
19
+ like: "A%",
20
+ },
21
+ })
22
+ .findFirst();
23
+
24
+ await userModel
25
+ .where({
26
+ name: esc.like("A%"),
27
+ })
28
+ .findFirst();