@apisr/drizzle-model 0.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 (47) hide show
  1. package/.turbo/turbo-check-types.log +2 -0
  2. package/ROADMAP.md +1 -0
  3. package/TODO.md +64 -0
  4. package/drizzle.config.ts +11 -0
  5. package/package.json +35 -0
  6. package/src/index.ts +1 -0
  7. package/src/model/builder.ts +46 -0
  8. package/src/model/config.ts +35 -0
  9. package/src/model/core/joins.ts +279 -0
  10. package/src/model/core/projection.ts +47 -0
  11. package/src/model/core/runtime.ts +249 -0
  12. package/src/model/core/thenable.ts +85 -0
  13. package/src/model/core/transform.ts +45 -0
  14. package/src/model/core/where.ts +183 -0
  15. package/src/model/core/with.ts +28 -0
  16. package/src/model/dialect.ts +11 -0
  17. package/src/model/foreigns.ts +31 -0
  18. package/src/model/format.ts +19 -0
  19. package/src/model/index.ts +1 -0
  20. package/src/model/methods/exclude.ts +32 -0
  21. package/src/model/methods/include.ts +3 -0
  22. package/src/model/methods/insert.ts +16 -0
  23. package/src/model/methods/levels.ts +2 -0
  24. package/src/model/methods/query/where.ts +48 -0
  25. package/src/model/methods/return.ts +39 -0
  26. package/src/model/methods/select.ts +38 -0
  27. package/src/model/methods/update.ts +4 -0
  28. package/src/model/methods/upsert.ts +54 -0
  29. package/src/model/methods/with.ts +40 -0
  30. package/src/model/model.ts +148 -0
  31. package/src/model/options.ts +64 -0
  32. package/src/model/query/operations.ts +170 -0
  33. package/src/model/relation.ts +121 -0
  34. package/src/model/result.ts +91 -0
  35. package/src/model/shape.ts +8 -0
  36. package/src/model/table.ts +127 -0
  37. package/src/types.ts +16 -0
  38. package/tests/builder-v2-mysql.type-test.ts +40 -0
  39. package/tests/builder-v2.type-test.ts +343 -0
  40. package/tests/builder.test.ts +63 -0
  41. package/tests/db.ts +25 -0
  42. package/tests/find.test.ts +155 -0
  43. package/tests/insert.test.ts +233 -0
  44. package/tests/relations.ts +38 -0
  45. package/tests/schema.ts +49 -0
  46. package/tsconfig.json +36 -0
  47. package/tsdown.config.ts +12 -0
@@ -0,0 +1,233 @@
1
+ import { modelBuilder } from "../src";
2
+ import * as schema from "./schema";
3
+ import { db } from "./db";
4
+ import { relations } from "./relations";
5
+ import { test, describe, expect } from "bun:test";
6
+ import { eq, inArray } from "drizzle-orm";
7
+ import { esc } from "@/model/query/operations";
8
+
9
+ const model = modelBuilder({
10
+ schema,
11
+ db,
12
+ relations,
13
+ dialect: "PostgreSQL",
14
+ });
15
+
16
+ const postsModel = model("userPosts", {});
17
+ const userModel = model("user", {});
18
+
19
+ function sortById<T extends { id: number }>(rows: T[]) {
20
+ return [...rows].sort((a, b) => a.id - b.id);
21
+ }
22
+
23
+ describe("Model Update/Delete/Upsert Test", () => {
24
+ test(".update | updates a single post", async () => {
25
+ const [seed] = (await postsModel.insert({
26
+ title: "Update seed",
27
+ featured: false,
28
+ userId: 1,
29
+ }).return()) as any[];
30
+
31
+ const updated = await postsModel
32
+ .where({ id: esc(seed.id) })
33
+ .update({
34
+ title: "Updated title",
35
+ featured: true,
36
+ })
37
+ .return();
38
+
39
+ const expected = await db
40
+ .select()
41
+ .from(schema.userPosts)
42
+ .where(eq(schema.userPosts.id, seed.id));
43
+
44
+ expect(updated).toBeArray();
45
+ expect((updated as any[])[0].id).toBe(seed.id);
46
+ expect((updated as any[])[0].title).toBe("Updated title");
47
+ expect((updated as any[])[0].featured).toBe(true);
48
+ expect((updated as any[])[0]).toEqual((expected as any[])[0]);
49
+ });
50
+
51
+ test(".delete | deletes a single post", async () => {
52
+ const [seed] = (await postsModel.insert({
53
+ title: "Delete seed",
54
+ featured: null,
55
+ userId: 1,
56
+ }).return()) as any[];
57
+
58
+ const deleted = await postsModel
59
+ .where({ id: esc(seed.id) })
60
+ .delete()
61
+ .return();
62
+
63
+ const expectedDeleted = await db
64
+ .select()
65
+ .from(schema.userPosts)
66
+ .where(eq(schema.userPosts.id, seed.id));
67
+
68
+ expect(deleted).toBeArray();
69
+ expect((deleted as any[])[0].id).toBe(seed.id);
70
+ expect(expectedDeleted).toHaveLength(0);
71
+ });
72
+
73
+ test(".upsert | inserts then updates by unique email", async () => {
74
+ const uniq = `${Date.now()}-${Math.random()}`;
75
+ const email = `upsert-${uniq}@example.com`;
76
+
77
+ const [created] = (await userModel.upsert({
78
+ insert: {
79
+ name: "Upsert user",
80
+ email,
81
+ age: 10,
82
+ isVerified: false,
83
+ },
84
+ update: {
85
+ name: "Upsert user updated",
86
+ },
87
+ target: schema.user.email as any,
88
+ }).return()) as any[];
89
+
90
+ const [updated] = (await userModel.upsert({
91
+ insert: {
92
+ name: "Upsert user (ignored)",
93
+ email,
94
+ age: 10,
95
+ isVerified: false,
96
+ },
97
+ update: {
98
+ name: "Upsert user updated",
99
+ },
100
+ target: schema.user.email as any,
101
+ }).return()) as any[];
102
+
103
+ const expected = await db
104
+ .select()
105
+ .from(schema.user)
106
+ .where(eq(schema.user.email, email));
107
+
108
+ expect(created).toBeDefined();
109
+ expect(updated).toBeDefined();
110
+ expect(updated.id).toBe(created.id);
111
+ expect(updated.name).toBe("Upsert user updated");
112
+ expect(updated).toEqual((expected as any[])[0]);
113
+ });
114
+ });
115
+
116
+ // Returns new model
117
+ // postsModel.extend({
118
+ // query: {
119
+ // userId: 123,
120
+ // },
121
+ // });
122
+
123
+ // const userModel = model({
124
+ // table: userTable,
125
+ // db,
126
+ // });
127
+
128
+ describe("Model Insert Test", () => {
129
+ test(".insert | one entry", async () => {
130
+ const inserted: any = await postsModel.insert({
131
+ title: "Hello world!",
132
+ featured: true,
133
+ userId: 1,
134
+ }).return();
135
+
136
+ const expected = await db
137
+ .select()
138
+ .from(schema.userPosts)
139
+ .where(eq(schema.userPosts.id, (inserted as any[])[0].id));
140
+
141
+ console.dir(inserted, {
142
+ depth: null,
143
+ });
144
+
145
+ expect(inserted).toBeDefined();
146
+ expect(Array.isArray(inserted)).toBe(true);
147
+ expect((inserted as any[])[0]).toBeDefined();
148
+ expect((inserted as any[])[0].title).toBe("Hello world!");
149
+ expect((inserted as any[])[0].featured).toBe(true);
150
+ expect((inserted as any[])[0].userId).toBe(1);
151
+ expect((inserted as any[])[0].likes).toBe(0);
152
+ expect((inserted as any[])[0].views).toBe(0);
153
+ expect((inserted as any[])[0]).toEqual((expected as any[])[0]);
154
+ });
155
+
156
+ test(".insert | one entry with defaults", async () => {
157
+ const inserted: any = await postsModel.insert({
158
+ title: "Post with defaults",
159
+ userId: 1,
160
+ }).return();
161
+
162
+ const expected = await db
163
+ .select()
164
+ .from(schema.userPosts)
165
+ .where(eq(schema.userPosts.id, (inserted as any[])[0].id));
166
+
167
+ console.dir(inserted, {
168
+ depth: null,
169
+ });
170
+
171
+ expect(inserted).toBeDefined();
172
+ expect(Array.isArray(inserted)).toBe(true);
173
+ expect((inserted as any[])[0]).toBeDefined();
174
+ expect((inserted as any[])[0].title).toBe("Post with defaults");
175
+ expect((inserted as any[])[0].userId).toBe(1);
176
+
177
+ // description and featured are optional
178
+ expect((inserted as any[])[0].description).toBeNull();
179
+ expect((inserted as any[])[0].featured).toBeNull();
180
+
181
+ // likes and views should use table defaults
182
+ expect((inserted as any[])[0].likes).toBe(0);
183
+ expect((inserted as any[])[0].views).toBe(0);
184
+ expect((inserted as any[])[0]).toEqual((expected as any[])[0]);
185
+ });
186
+
187
+ test(".insert | multiple entries", async () => {
188
+ const inserted = await postsModel.insert([
189
+ {
190
+ title: "First bulk post",
191
+ description: "First bulk description",
192
+ featured: false,
193
+ userId: 1,
194
+ },
195
+ {
196
+ title: "Second bulk post",
197
+ description: "Second bulk description",
198
+ featured: true,
199
+ userId: 1,
200
+ },
201
+ ]).return();
202
+
203
+ const ids = (inserted as any[]).map((x) => x.id);
204
+ const expected = await db
205
+ .select()
206
+ .from(schema.userPosts)
207
+ .where(inArray(schema.userPosts.id, ids));
208
+
209
+ console.dir(inserted, {
210
+ depth: null,
211
+ });
212
+
213
+ expect(inserted).toBeDefined();
214
+ expect(Array.isArray(inserted)).toBe(true);
215
+ expect(inserted as any[]).toHaveLength(2);
216
+
217
+ const [first, second] = inserted as any[];
218
+
219
+ expect(first.title).toBe("First bulk post");
220
+ expect(first.description).toBe("First bulk description");
221
+ expect(first.featured).toBe(false);
222
+ expect(first.likes).toBe(0);
223
+ expect(first.views).toBe(0);
224
+
225
+ expect(second.title).toBe("Second bulk post");
226
+ expect(second.description).toBe("Second bulk description");
227
+ expect(second.featured).toBe(true);
228
+ expect(second.likes).toBe(0);
229
+ expect(second.views).toBe(0);
230
+
231
+ expect(sortById(inserted as any)).toEqual(sortById(expected as any));
232
+ });
233
+ });
@@ -0,0 +1,38 @@
1
+ import * as schema from "./schema";
2
+ import { defineRelations } from "drizzle-orm";
3
+ // schema.
4
+ export const relations = defineRelations(schema, (r) => ({
5
+ user: {
6
+ posts: r.many.userPosts(),
7
+ invitee: r.one.user({
8
+ from: r.user.invitedBy,
9
+ to: r.user.id,
10
+ }),
11
+ },
12
+ userPosts: {
13
+ user: r.one.user({
14
+ from: r.userPosts.userId,
15
+ to: r.user.id,
16
+ }),
17
+ comments: r.many.postComments({
18
+ from: r.userPosts.id,
19
+ to: r.postComments.postId,
20
+ }),
21
+ },
22
+ postComments: {
23
+ author: r.one.user({
24
+ from: r.postComments.authorId,
25
+ to: r.user.id,
26
+ }),
27
+ post: r.one.userPosts({
28
+ from: r.postComments.postId,
29
+ to: r.userPosts.id,
30
+ }),
31
+ },
32
+ userIdeas: {
33
+ user: r.one.user({
34
+ from: r.userIdeas.userId,
35
+ to: r.user.id,
36
+ }),
37
+ }
38
+ }));
@@ -0,0 +1,49 @@
1
+ import { pgTable, integer, text, boolean } from "drizzle-orm/pg-core";
2
+
3
+ // --- TABLES ---
4
+
5
+ export const user = pgTable("user", {
6
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
7
+ name: text("name").notNull(),
8
+ email: text("email").unique().notNull(),
9
+ age: integer().notNull().default(0),
10
+ isVerified: boolean("is_verified"),
11
+ invitedBy: integer("invited_by"),
12
+ secretField: integer("secret_field").default(0),
13
+ });
14
+
15
+ export const userPosts = pgTable("user_posts", {
16
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
17
+ title: text("title").notNull(),
18
+ description: text("description"),
19
+ likes: integer().notNull().default(0),
20
+ views: integer().notNull().default(0),
21
+ featured: boolean("featured"),
22
+
23
+ // Foreign Key
24
+ userId: integer("user_id")
25
+ .notNull()
26
+ .references(() => user.id, { onDelete: "cascade" }),
27
+ });
28
+
29
+ export const postComments = pgTable("user_post_comments", {
30
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
31
+ content: text("content"),
32
+
33
+ // Foreign Keys
34
+ authorId: integer("author_id")
35
+ .notNull()
36
+ .references(() => user.id, { onDelete: "cascade" }),
37
+ postId: integer("post_id")
38
+ .notNull()
39
+ .references(() => userPosts.id, { onDelete: "cascade" }),
40
+ });
41
+
42
+
43
+ export const userIdeas = pgTable("user_ideas", {
44
+ id: integer().primaryKey().generatedAlwaysAsIdentity(),
45
+ content: text("content"),
46
+ userId: integer("user_id")
47
+ .notNull()
48
+ .references(() => user.id, { onDelete: "cascade" }),
49
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ "baseUrl": ".",
12
+ "paths": {
13
+ "@/*": ["./src/*"]
14
+ },
15
+
16
+ "preserveSymlinks": true,
17
+
18
+ // Bundler mode
19
+ "moduleResolution": "bundler",
20
+ "allowImportingTsExtensions": true,
21
+ "verbatimModuleSyntax": true,
22
+ "noEmit": true,
23
+
24
+ // Best practices
25
+ "strict": true,
26
+ "skipLibCheck": true,
27
+ "noFallthroughCasesInSwitch": true,
28
+ "noUncheckedIndexedAccess": true,
29
+ "noImplicitOverride": true,
30
+
31
+ // Some stricter flags (disabled by default)
32
+ "noUnusedLocals": false,
33
+ "noUnusedParameters": false,
34
+ "noPropertyAccessFromIndexSignature": false
35
+ }
36
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: "./src/index.ts",
5
+ dts: true,
6
+ format: "esm",
7
+ outDir: "./dist",
8
+ clean: true,
9
+ unbundle: true,
10
+ noExternal: [/@apisr\/.*/],
11
+ external: ["drizzle-kit", "drizzle-orm"],
12
+ });