@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.
- package/.turbo/turbo-check-types.log +2 -0
- package/ROADMAP.md +1 -0
- package/TODO.md +64 -0
- package/drizzle.config.ts +11 -0
- package/package.json +35 -0
- package/src/index.ts +1 -0
- package/src/model/builder.ts +46 -0
- package/src/model/config.ts +35 -0
- package/src/model/core/joins.ts +279 -0
- package/src/model/core/projection.ts +47 -0
- package/src/model/core/runtime.ts +249 -0
- package/src/model/core/thenable.ts +85 -0
- package/src/model/core/transform.ts +45 -0
- package/src/model/core/where.ts +183 -0
- package/src/model/core/with.ts +28 -0
- package/src/model/dialect.ts +11 -0
- package/src/model/foreigns.ts +31 -0
- package/src/model/format.ts +19 -0
- package/src/model/index.ts +1 -0
- package/src/model/methods/exclude.ts +32 -0
- package/src/model/methods/include.ts +3 -0
- package/src/model/methods/insert.ts +16 -0
- package/src/model/methods/levels.ts +2 -0
- package/src/model/methods/query/where.ts +48 -0
- package/src/model/methods/return.ts +39 -0
- package/src/model/methods/select.ts +38 -0
- package/src/model/methods/update.ts +4 -0
- package/src/model/methods/upsert.ts +54 -0
- package/src/model/methods/with.ts +40 -0
- package/src/model/model.ts +148 -0
- package/src/model/options.ts +64 -0
- package/src/model/query/operations.ts +170 -0
- package/src/model/relation.ts +121 -0
- package/src/model/result.ts +91 -0
- package/src/model/shape.ts +8 -0
- package/src/model/table.ts +127 -0
- package/src/types.ts +16 -0
- package/tests/builder-v2-mysql.type-test.ts +40 -0
- package/tests/builder-v2.type-test.ts +343 -0
- package/tests/builder.test.ts +63 -0
- package/tests/db.ts +25 -0
- package/tests/find.test.ts +155 -0
- package/tests/insert.test.ts +233 -0
- package/tests/relations.ts +38 -0
- package/tests/schema.ts +49 -0
- package/tsconfig.json +36 -0
- 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
|
+
}));
|
package/tests/schema.ts
ADDED
|
@@ -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
|
+
}
|
package/tsdown.config.ts
ADDED
|
@@ -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
|
+
});
|