@apisr/drizzle-model 0.0.3 → 2.0.0

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 (43) hide show
  1. package/DISCLAIMER.md +5 -0
  2. package/TODO.md +8 -61
  3. package/package.json +2 -1
  4. package/src/core/dialect.ts +81 -0
  5. package/src/core/index.ts +24 -0
  6. package/src/core/query/error.ts +15 -0
  7. package/src/core/query/joins.ts +596 -0
  8. package/src/core/query/projection.ts +136 -0
  9. package/src/core/query/where.ts +449 -0
  10. package/src/core/result.ts +297 -0
  11. package/src/core/runtime.ts +612 -0
  12. package/src/core/transform.ts +119 -0
  13. package/src/model/builder.ts +40 -6
  14. package/src/model/config.ts +9 -9
  15. package/src/model/format.ts +20 -8
  16. package/src/model/methods/exclude.ts +1 -7
  17. package/src/model/methods/return.ts +11 -11
  18. package/src/model/methods/select.ts +2 -8
  19. package/src/model/model.ts +10 -16
  20. package/src/model/query/error.ts +1 -0
  21. package/src/model/result.ts +134 -21
  22. package/src/types.ts +38 -0
  23. package/tests/base/count.test.ts +47 -0
  24. package/tests/base/delete.test.ts +90 -0
  25. package/tests/base/find.test.ts +209 -0
  26. package/tests/base/insert.test.ts +152 -0
  27. package/tests/base/safe.test.ts +91 -0
  28. package/tests/base/update.test.ts +88 -0
  29. package/tests/base/upsert.test.ts +121 -0
  30. package/tests/base.ts +21 -0
  31. package/tests/snippets/x-1.ts +22 -0
  32. package/src/model/core/joins.ts +0 -364
  33. package/src/model/core/projection.ts +0 -61
  34. package/src/model/core/runtime.ts +0 -330
  35. package/src/model/core/thenable.ts +0 -94
  36. package/src/model/core/transform.ts +0 -65
  37. package/src/model/core/where.ts +0 -249
  38. package/src/model/core/with.ts +0 -28
  39. package/tests/builder-v2-mysql.type-test.ts +0 -51
  40. package/tests/builder-v2.type-test.ts +0 -336
  41. package/tests/builder.test.ts +0 -63
  42. package/tests/find.test.ts +0 -166
  43. package/tests/insert.test.ts +0 -247
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { model } from "tests/base";
3
+ import { db } from "tests/db";
4
+ import * as schema from "tests/schema";
5
+ import { esc } from "@/model";
6
+
7
+ const userModel = model("user", {});
8
+ const postsModel = model("userPosts", {});
9
+
10
+ describe("count", () => {
11
+ test("returns total row count", async () => {
12
+ const count = await userModel.count();
13
+ const rows = await db.select().from(schema.user);
14
+
15
+ expect(count).toBeNumber();
16
+ expect(count).toBe((rows as any[]).length);
17
+ });
18
+
19
+ test("respects where clause", async () => {
20
+ const count = await userModel.where({ name: esc("Alex") }).count();
21
+
22
+ expect(count).toBeNumber();
23
+ expect(count).toBeGreaterThanOrEqual(0);
24
+ });
25
+
26
+ test("returns 0 for no matches", async () => {
27
+ const count = await userModel
28
+ .where({ name: esc("__nonexistent_user_name__") })
29
+ .count();
30
+
31
+ expect(count).toBe(0);
32
+ });
33
+
34
+ test("reflects inserted rows", async () => {
35
+ const before = await postsModel.count();
36
+
37
+ await postsModel.insert({
38
+ title: "Count check",
39
+ featured: false,
40
+ userId: 1,
41
+ });
42
+
43
+ const after = await postsModel.count();
44
+
45
+ expect(after).toBe(before + 1);
46
+ });
47
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { eq } from "drizzle-orm";
3
+ import { model } from "tests/base";
4
+ import { db } from "tests/db";
5
+ import * as schema from "tests/schema";
6
+ import { esc } from "@/model";
7
+
8
+ const postsModel = model("userPosts", {});
9
+
10
+ async function seedPost(title: string) {
11
+ const [row] = (await postsModel
12
+ .insert({ title, featured: false, userId: 1 })
13
+ .return()) as any[];
14
+ return row;
15
+ }
16
+
17
+ describe("delete", () => {
18
+ test("deletes matching rows — return", async () => {
19
+ const seed = await seedPost("Delete seed");
20
+
21
+ const rows = (await postsModel
22
+ .where({ id: esc(seed.id) })
23
+ .delete()
24
+ .return()) as any[];
25
+
26
+ expect(rows).toBeArray();
27
+ expect(rows[0].id).toBe(seed.id);
28
+
29
+ const remaining = await db
30
+ .select()
31
+ .from(schema.userPosts)
32
+ .where(eq(schema.userPosts.id, seed.id));
33
+
34
+ expect(remaining).toHaveLength(0);
35
+ });
36
+
37
+ test("returnFirst — returns the deleted row", async () => {
38
+ const seed = await seedPost("Delete returnFirst");
39
+
40
+ const row = (await postsModel
41
+ .where({ id: esc(seed.id) })
42
+ .delete()
43
+ .returnFirst()) as any;
44
+
45
+ expect(row).toBeDefined();
46
+ expect(row.id).toBe(seed.id);
47
+ expect(row.title).toBe("Delete returnFirst");
48
+ });
49
+
50
+ test("return > omit — removes specified fields", async () => {
51
+ const seed = await seedPost("Delete omit");
52
+
53
+ const rows = (await postsModel
54
+ .where({ id: esc(seed.id) })
55
+ .delete()
56
+ .return()
57
+ .omit({ likes: true, views: true })) as any[];
58
+
59
+ expect(rows[0].likes).toBeUndefined();
60
+ expect(rows[0].views).toBeUndefined();
61
+ expect(rows[0].title).toBe("Delete omit");
62
+ });
63
+
64
+ test("returnFirst > safe — success", async () => {
65
+ const seed = await seedPost("Delete safe");
66
+
67
+ const result = await postsModel
68
+ .where({ id: esc(seed.id) })
69
+ .delete()
70
+ .returnFirst()
71
+ .safe();
72
+
73
+ expect(result.error).toBeUndefined();
74
+ expect(result.data).toBeDefined();
75
+ expect((result.data as any).id).toBe(seed.id);
76
+ });
77
+
78
+ test("row is gone after delete", async () => {
79
+ const seed = await seedPost("Delete verify gone");
80
+
81
+ await postsModel.where({ id: esc(seed.id) }).delete();
82
+
83
+ const remaining = await db
84
+ .select()
85
+ .from(schema.userPosts)
86
+ .where(eq(schema.userPosts.id, seed.id));
87
+
88
+ expect(remaining).toHaveLength(0);
89
+ });
90
+ });
@@ -0,0 +1,209 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { eq } from "drizzle-orm";
3
+ import { model } from "tests/base";
4
+ import { db } from "tests/db";
5
+ import * as schema from "tests/schema";
6
+ import { esc } from "@/model";
7
+
8
+ const userModel = model("user", {});
9
+ const userModelFormat = model("user", {
10
+ format({ secretField, isVerified, ...rest }) {
11
+ return {
12
+ ...rest,
13
+ isVerified: !!isVerified,
14
+ customField: "Hello World" as const,
15
+ };
16
+ },
17
+ });
18
+
19
+ describe("find", () => {
20
+ // -----------------------------------------------------------------------
21
+ // findFirst
22
+ // -----------------------------------------------------------------------
23
+
24
+ describe("findFirst", () => {
25
+ test("returns a single row", async () => {
26
+ const user = await userModel.findFirst();
27
+
28
+ expect(user).toBeDefined();
29
+ expect(user.id).toBeNumber();
30
+ });
31
+
32
+ test("where — equality", async () => {
33
+ const user = await userModel.where({ name: esc("Alex") }).findFirst();
34
+
35
+ expect(user).toBeDefined();
36
+ expect(user.name).toBe("Alex");
37
+ });
38
+
39
+ test("where — like operator", async () => {
40
+ const user = await userModel.where({ name: { like: "An%" } }).findFirst();
41
+
42
+ expect(user).toBeDefined();
43
+ expect(user.name.startsWith("An")).toBe(true);
44
+ });
45
+
46
+ test("where — or combinator", async () => {
47
+ const user = await userModel
48
+ .where({ name: { or: [esc("Alex"), esc("Anna")] } })
49
+ .findFirst();
50
+
51
+ expect(user).toBeDefined();
52
+ expect(["Alex", "Anna"]).toContain(user.name);
53
+ });
54
+
55
+ test("select — picks only specified fields", async () => {
56
+ const user = await userModel.findFirst().select({
57
+ name: true,
58
+ age: true,
59
+ });
60
+
61
+ expect(user.name).toBeDefined();
62
+ expect(user.age).toBeDefined();
63
+ // @ts-expect-error
64
+ expect(user.email).toBeUndefined();
65
+ });
66
+
67
+ test("exclude — removes specified fields", async () => {
68
+ const user = await userModel.findFirst().exclude({
69
+ name: true,
70
+ });
71
+
72
+ // @ts-expect-error
73
+ expect(user.name).toBeUndefined();
74
+ expect(user.id).toBeDefined();
75
+ });
76
+
77
+ test("format — applies format function", async () => {
78
+ const user = await userModelFormat.findFirst();
79
+
80
+ expect(user.customField).toBe("Hello World");
81
+ // @ts-expect-error
82
+ expect(user.secretField).toBeUndefined();
83
+ expect(typeof user.isVerified).toBe("boolean");
84
+ });
85
+
86
+ test("raw — skips format", async () => {
87
+ const user = await userModelFormat.findFirst().raw();
88
+
89
+ expect(user.secretField).toBeDefined();
90
+ });
91
+
92
+ test("matches raw drizzle query", async () => {
93
+ const user = await userModel.where({ name: esc("Alex") }).findFirst();
94
+
95
+ const [expected] = await db
96
+ .select()
97
+ .from(schema.user)
98
+ .where(eq(schema.user.name, "Alex"))
99
+ .limit(1);
100
+
101
+ expect(user).toEqual(expected as any);
102
+ });
103
+
104
+ test("safe — wraps result in { data, error }", async () => {
105
+ const result = await userModel.findFirst().safe();
106
+
107
+ expect(result.error).toBeUndefined();
108
+ expect(result.data).toBeDefined();
109
+ expect((result.data as any).id).toBeNumber();
110
+ });
111
+ });
112
+
113
+ // -----------------------------------------------------------------------
114
+ // findMany
115
+ // -----------------------------------------------------------------------
116
+
117
+ describe("findMany", () => {
118
+ test("returns all rows", async () => {
119
+ const users = await userModel.findMany();
120
+ const expected = await db.select().from(schema.user);
121
+
122
+ expect(users).toBeArray();
123
+ expect(users).toHaveLength((expected as any[]).length);
124
+ });
125
+
126
+ test("where — equality", async () => {
127
+ const users = await userModel.where({ name: esc("Alex") }).findMany();
128
+
129
+ expect(users).toBeArray();
130
+ for (const user of users) {
131
+ expect(user.name).toBe("Alex");
132
+ }
133
+ });
134
+
135
+ test("where — multiple filters (AND)", async () => {
136
+ const users = await userModel
137
+ .where({
138
+ name: esc("Alex"),
139
+ isVerified: esc(false),
140
+ })
141
+ .findMany();
142
+
143
+ for (const user of users) {
144
+ expect(user.name).toBe("Alex");
145
+ expect(user.isVerified).toBe(false);
146
+ }
147
+ });
148
+
149
+ test("where — gte operator", async () => {
150
+ const users = await userModel.where({ age: { gte: esc(18) } }).findMany();
151
+
152
+ for (const user of users) {
153
+ expect(user.age).toBeGreaterThanOrEqual(18);
154
+ }
155
+ });
156
+
157
+ test("select — picks only specified fields", async () => {
158
+ const users = await userModel.findMany().select({
159
+ name: true,
160
+ age: true,
161
+ });
162
+
163
+ for (const user of users) {
164
+ expect(user.name).toBeDefined();
165
+ expect(user.age).toBeDefined();
166
+ // @ts-expect-error
167
+ expect(user.email).toBeUndefined();
168
+ }
169
+ });
170
+
171
+ test("exclude — removes specified fields", async () => {
172
+ const users = await userModel.findMany().exclude({
173
+ name: true,
174
+ });
175
+
176
+ for (const user of users) {
177
+ // @ts-expect-error
178
+ expect(user.name).toBeUndefined();
179
+ expect(user.id).toBeDefined();
180
+ }
181
+ });
182
+
183
+ test("format — applies format function", async () => {
184
+ const users = await userModelFormat.findMany();
185
+
186
+ for (const user of users) {
187
+ expect(user.customField).toBe("Hello World");
188
+ // @ts-expect-error
189
+ expect(user.secretField).toBeUndefined();
190
+ }
191
+ });
192
+
193
+ test("raw — skips format", async () => {
194
+ const users = await userModelFormat.findMany().raw();
195
+
196
+ for (const user of users) {
197
+ expect(user.secretField).toBeDefined();
198
+ }
199
+ });
200
+
201
+ test("safe — wraps result in { data, error }", async () => {
202
+ const result = await userModel.findMany().safe();
203
+
204
+ expect(result.error).toBeUndefined();
205
+ expect(result.data).toBeDefined();
206
+ expect(result.data).toBeArray();
207
+ });
208
+ });
209
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { eq, inArray } from "drizzle-orm";
3
+ import { model } from "tests/base";
4
+ import { db } from "tests/db";
5
+ import * as schema from "tests/schema";
6
+
7
+ const postsModel = model("userPosts", {});
8
+ const userModel = model("user", {});
9
+
10
+ function uid(): string {
11
+ return `${Date.now()}-${Math.random()}`;
12
+ }
13
+
14
+ describe("insert", () => {
15
+ test("single row — no return", async () => {
16
+ await postsModel.insert({
17
+ title: "Insert no-return",
18
+ featured: false,
19
+ userId: 1,
20
+ });
21
+ });
22
+
23
+ test("single row — return", async () => {
24
+ const rows = await postsModel
25
+ .insert({
26
+ title: "Insert return",
27
+ featured: true,
28
+ userId: 1,
29
+ })
30
+ .return();
31
+
32
+ expect(rows).toBeArray();
33
+ expect((rows as any[])[0].title).toBe("Insert return");
34
+ expect((rows as any[])[0].featured).toBe(true);
35
+ });
36
+
37
+ test("single row — returnFirst", async () => {
38
+ const row = await postsModel
39
+ .insert({
40
+ title: "Insert returnFirst",
41
+ featured: false,
42
+ userId: 1,
43
+ })
44
+ .returnFirst();
45
+
46
+ expect(row).toBeDefined();
47
+ expect((row as any).id).toBeNumber();
48
+ expect((row as any).title).toBe("Insert returnFirst");
49
+ });
50
+
51
+ test("single row — matches raw drizzle", async () => {
52
+ const [inserted] = (await postsModel
53
+ .insert({
54
+ title: "Insert drizzle check",
55
+ featured: null,
56
+ userId: 1,
57
+ })
58
+ .return()) as any[];
59
+
60
+ const [expected] = await db
61
+ .select()
62
+ .from(schema.userPosts)
63
+ .where(eq(schema.userPosts.id, inserted.id));
64
+
65
+ expect(inserted).toEqual(expected as any);
66
+ });
67
+
68
+ test("single row with defaults", async () => {
69
+ const row = (await postsModel
70
+ .insert({
71
+ title: "Insert defaults",
72
+ userId: 1,
73
+ })
74
+ .returnFirst()) as any;
75
+
76
+ expect(row.likes).toBe(0);
77
+ expect(row.views).toBe(0);
78
+ expect(row.description).toBeNull();
79
+ expect(row.featured).toBeNull();
80
+ });
81
+
82
+ test("multiple rows — return", async () => {
83
+ const rows = (await postsModel
84
+ .insert([
85
+ { title: "Bulk 1", featured: false, userId: 1 },
86
+ { title: "Bulk 2", featured: true, userId: 1 },
87
+ ])
88
+ .return()) as any[];
89
+
90
+ const ids = rows.map((r) => r.id);
91
+ const expected = await db
92
+ .select()
93
+ .from(schema.userPosts)
94
+ .where(inArray(schema.userPosts.id, ids));
95
+
96
+ expect(rows).toHaveLength(2);
97
+ expect(rows[0].title).toBe("Bulk 1");
98
+ expect(rows[1].title).toBe("Bulk 2");
99
+ expect(rows.sort((a, b) => a.id - b.id)).toEqual(
100
+ (expected as any[]).sort((a, b) => a.id - b.id)
101
+ );
102
+ });
103
+
104
+ test("return > omit — removes specified fields", async () => {
105
+ const rows = (await userModel
106
+ .insert({
107
+ email: `${uid()}@test.com`,
108
+ name: "Omit test",
109
+ age: 99,
110
+ secretField: 42,
111
+ })
112
+ .return()
113
+ .omit({ age: true, secretField: true })) as any[];
114
+
115
+ for (const row of rows) {
116
+ expect(row.age).toBeUndefined();
117
+ expect(row.secretField).toBeUndefined();
118
+ expect(row.name).toBe("Omit test");
119
+ }
120
+ });
121
+
122
+ test("returnFirst > omit — removes specified fields", async () => {
123
+ const row = (await userModel
124
+ .insert({
125
+ email: `${uid()}@test.com`,
126
+ name: "Omit first test",
127
+ age: 55,
128
+ secretField: 7,
129
+ })
130
+ .returnFirst()
131
+ .omit({ age: true, secretField: true })) as any;
132
+
133
+ expect(row.age).toBeUndefined();
134
+ expect(row.secretField).toBeUndefined();
135
+ expect(row.name).toBe("Omit first test");
136
+ });
137
+
138
+ test("returnFirst > safe — success", async () => {
139
+ const result = await userModel
140
+ .insert({
141
+ email: `${uid()}@test.com`,
142
+ name: "Safe test",
143
+ age: 20,
144
+ })
145
+ .returnFirst()
146
+ .safe();
147
+
148
+ expect(result.error).toBeUndefined();
149
+ expect(result.data).toBeDefined();
150
+ expect((result.data as any).name).toBe("Safe test");
151
+ });
152
+ });
@@ -0,0 +1,91 @@
1
+ import { 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("safe", () => {
12
+ describe("query — safe()", () => {
13
+ test("findFirst — success returns { data, error: undefined }", async () => {
14
+ const result = await userModel.findFirst().safe();
15
+
16
+ expect(result.error).toBeUndefined();
17
+ expect(result.data).toBeDefined();
18
+ expect((result.data as any).id).toBeNumber();
19
+ });
20
+
21
+ test("findMany — success returns { data, error: undefined }", async () => {
22
+ const result = await userModel.findMany().safe();
23
+
24
+ expect(result.error).toBeUndefined();
25
+ expect(result.data).toBeArray();
26
+ });
27
+
28
+ test("findFirst with where — success", async () => {
29
+ const result = await userModel
30
+ .where({ name: esc("Alex") })
31
+ .findFirst()
32
+ .safe();
33
+
34
+ expect(result.error).toBeUndefined();
35
+ expect(result.data).toBeDefined();
36
+ });
37
+ });
38
+
39
+ describe("mutation — safe()", () => {
40
+ test("insert > returnFirst > safe — success", async () => {
41
+ const result = await userModel
42
+ .insert({
43
+ email: `${uid()}@safe.com`,
44
+ name: "Safe insert",
45
+ age: 25,
46
+ })
47
+ .returnFirst()
48
+ .safe();
49
+
50
+ expect(result.error).toBeUndefined();
51
+ expect(result.data).toBeDefined();
52
+ expect((result.data as any).name).toBe("Safe insert");
53
+ });
54
+
55
+ test("insert > return > safe — success", async () => {
56
+ const result = await userModel
57
+ .insert({
58
+ email: `${uid()}@safe.com`,
59
+ name: "Safe array insert",
60
+ age: 30,
61
+ })
62
+ .return()
63
+ .safe();
64
+
65
+ expect(result.error).toBeUndefined();
66
+ expect(result.data).toBeDefined();
67
+ });
68
+
69
+ test("insert > safe — error on duplicate email", async () => {
70
+ const email = `${uid()}@safe-dup.com`;
71
+
72
+ await userModel.insert({
73
+ email,
74
+ name: "First",
75
+ age: 20,
76
+ });
77
+
78
+ const result = await userModel
79
+ .insert({
80
+ email,
81
+ name: "Duplicate",
82
+ age: 20,
83
+ })
84
+ .returnFirst()
85
+ .safe();
86
+
87
+ expect(result.error).toBeDefined();
88
+ expect(result.data).toBeUndefined();
89
+ });
90
+ });
91
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { eq } from "drizzle-orm";
3
+ import { model } from "tests/base";
4
+ import { db } from "tests/db";
5
+ import * as schema from "tests/schema";
6
+ import { esc } from "@/model";
7
+
8
+ const postsModel = model("userPosts", {});
9
+
10
+ async function seedPost(title: string) {
11
+ const [row] = (await postsModel
12
+ .insert({ title, featured: false, userId: 1 })
13
+ .return()) as any[];
14
+ return row;
15
+ }
16
+
17
+ describe("update", () => {
18
+ test("updates matching rows — return", async () => {
19
+ const seed = await seedPost("Update seed");
20
+
21
+ const rows = (await postsModel
22
+ .where({ id: esc(seed.id) })
23
+ .update({ title: "Updated title", featured: true })
24
+ .return()) as any[];
25
+
26
+ expect(rows).toBeArray();
27
+ expect(rows[0].id).toBe(seed.id);
28
+ expect(rows[0].title).toBe("Updated title");
29
+ expect(rows[0].featured).toBe(true);
30
+ });
31
+
32
+ test("matches raw drizzle after update", async () => {
33
+ const seed = await seedPost("Update drizzle check");
34
+
35
+ await postsModel
36
+ .where({ id: esc(seed.id) })
37
+ .update({ title: "Verified update" })
38
+ .return();
39
+
40
+ const [expected] = await db
41
+ .select()
42
+ .from(schema.userPosts)
43
+ .where(eq(schema.userPosts.id, seed.id));
44
+
45
+ expect((expected as any).title).toBe("Verified update");
46
+ });
47
+
48
+ test("returnFirst — returns single object", async () => {
49
+ const seed = await seedPost("Update returnFirst");
50
+
51
+ const row = (await postsModel
52
+ .where({ id: esc(seed.id) })
53
+ .update({ title: "First updated" })
54
+ .returnFirst()) as any;
55
+
56
+ expect(row).toBeDefined();
57
+ expect(row.id).toBe(seed.id);
58
+ expect(row.title).toBe("First updated");
59
+ });
60
+
61
+ test("return > omit — removes specified fields", async () => {
62
+ const seed = await seedPost("Update omit");
63
+
64
+ const rows = (await postsModel
65
+ .where({ id: esc(seed.id) })
66
+ .update({ title: "Omitted update" })
67
+ .return()
68
+ .omit({ likes: true, views: true })) as any[];
69
+
70
+ expect(rows[0].likes).toBeUndefined();
71
+ expect(rows[0].views).toBeUndefined();
72
+ expect(rows[0].title).toBe("Omitted update");
73
+ });
74
+
75
+ test("returnFirst > safe — success", async () => {
76
+ const seed = await seedPost("Update safe");
77
+
78
+ const result = await postsModel
79
+ .where({ id: esc(seed.id) })
80
+ .update({ title: "Safe updated" })
81
+ .returnFirst()
82
+ .safe();
83
+
84
+ expect(result.error).toBeUndefined();
85
+ expect(result.data).toBeDefined();
86
+ expect((result.data as any).title).toBe("Safe updated");
87
+ });
88
+ });