@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,121 @@
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
+
7
+ const userModel = model("user", {});
8
+
9
+ function uid(): string {
10
+ return `${Date.now()}-${Math.random()}`;
11
+ }
12
+
13
+ describe("upsert", () => {
14
+ test("inserts when no conflict", async () => {
15
+ const email = `upsert-new-${uid()}@test.com`;
16
+
17
+ const [row] = (await userModel
18
+ .upsert({
19
+ insert: { name: "Upsert new", email, age: 25 },
20
+ update: { name: "Should not apply" },
21
+ target: schema.user.email as any,
22
+ })
23
+ .return()) as any[];
24
+
25
+ expect(row).toBeDefined();
26
+ expect(row.name).toBe("Upsert new");
27
+ expect(row.email).toBe(email);
28
+ });
29
+
30
+ test("updates on conflict", async () => {
31
+ const email = `upsert-conflict-${uid()}@test.com`;
32
+
33
+ const [created] = (await userModel
34
+ .upsert({
35
+ insert: { name: "Original", email, age: 20 },
36
+ update: { name: "Overwritten" },
37
+ target: schema.user.email as any,
38
+ })
39
+ .return()) as any[];
40
+
41
+ const [updated] = (await userModel
42
+ .upsert({
43
+ insert: { name: "Ignored insert", email, age: 20 },
44
+ update: { name: "Overwritten" },
45
+ target: schema.user.email as any,
46
+ })
47
+ .return()) as any[];
48
+
49
+ expect(updated.id).toBe(created.id);
50
+ expect(updated.name).toBe("Overwritten");
51
+ });
52
+
53
+ test("matches raw drizzle after upsert", async () => {
54
+ const email = `upsert-verify-${uid()}@test.com`;
55
+
56
+ await userModel
57
+ .upsert({
58
+ insert: { name: "Verify", email, age: 30 },
59
+ update: { name: "Verified" },
60
+ target: schema.user.email as any,
61
+ })
62
+ .return();
63
+
64
+ const [expected] = await db
65
+ .select()
66
+ .from(schema.user)
67
+ .where(eq(schema.user.email, email));
68
+
69
+ expect((expected as any).name).toBe("Verify");
70
+ });
71
+
72
+ test("returnFirst — returns single object", async () => {
73
+ const email = `upsert-first-${uid()}@test.com`;
74
+
75
+ const row = (await userModel
76
+ .upsert({
77
+ insert: { name: "First upsert", email, age: 18 },
78
+ update: { name: "Updated" },
79
+ target: schema.user.email as any,
80
+ })
81
+ .returnFirst()) as any;
82
+
83
+ expect(row).toBeDefined();
84
+ expect(row.id).toBeNumber();
85
+ expect(row.name).toBe("First upsert");
86
+ });
87
+
88
+ test("return > omit — removes specified fields", async () => {
89
+ const email = `upsert-omit-${uid()}@test.com`;
90
+
91
+ const [row] = (await userModel
92
+ .upsert({
93
+ insert: { name: "Omit upsert", email, age: 40, secretField: 99 },
94
+ update: { name: "Omit updated" },
95
+ target: schema.user.email as any,
96
+ })
97
+ .return()
98
+ .omit({ age: true, secretField: true })) as any[];
99
+
100
+ expect(row.age).toBeUndefined();
101
+ expect(row.secretField).toBeUndefined();
102
+ expect(row.name).toBe("Omit upsert");
103
+ });
104
+
105
+ test("returnFirst > safe — success", async () => {
106
+ const email = `upsert-safe-${uid()}@test.com`;
107
+
108
+ const result = await userModel
109
+ .upsert({
110
+ insert: { name: "Safe upsert", email, age: 22 },
111
+ update: { name: "Safe updated" },
112
+ target: schema.user.email as any,
113
+ })
114
+ .returnFirst()
115
+ .safe();
116
+
117
+ expect(result.error).toBeUndefined();
118
+ expect(result.data).toBeDefined();
119
+ expect((result.data as any).name).toBe("Safe upsert");
120
+ });
121
+ });
package/tests/base.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { createLogger } from "@apisr/logger";
2
+ import { createConsole } from "@apisr/logger/console";
3
+ import { modelBuilder } from "../src";
4
+ import { db } from "./db";
5
+ import { relations } from "./relations";
6
+ import * as schema from "./schema";
7
+
8
+ export const model = modelBuilder({
9
+ schema,
10
+ db,
11
+ relations,
12
+ dialect: "PostgreSQL",
13
+ });
14
+
15
+ export const logger = createLogger({
16
+ transports: {
17
+ console: createConsole({
18
+ mode: "pretty",
19
+ }),
20
+ },
21
+ });
@@ -0,0 +1,22 @@
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
+ const user = await userModel
17
+ .where({
18
+ age: esc(12),
19
+ })
20
+ .findFirst();
21
+
22
+ console.log(user);
@@ -1,364 +0,0 @@
1
- import { and, eq } from "drizzle-orm";
2
-
3
- type AnyObj = Record<string, any>;
4
-
5
- type JoinNode = {
6
- path: string[];
7
- key: string;
8
- relationType: "one" | "many";
9
- sourceTableName: string;
10
- targetTableName: string;
11
- sourceTable: AnyObj;
12
- targetTable: AnyObj;
13
- targetAliasTable: AnyObj;
14
- aliasKey: string;
15
- sourceColumns: any[];
16
- targetColumns: any[];
17
- pkField: string;
18
- parent?: JoinNode;
19
- children: JoinNode[];
20
- };
21
-
22
- function isDrizzleColumn(value: any): boolean {
23
- return (
24
- !!value && typeof value === "object" && typeof value.getSQL === "function"
25
- );
26
- }
27
-
28
- function getPrimaryKeyField(table: AnyObj): string {
29
- for (const [k, v] of Object.entries(table)) {
30
- if (!isDrizzleColumn(v)) {
31
- continue;
32
- }
33
- if ((v as any).primary === true) {
34
- return k;
35
- }
36
- if ((v as any).config?.primaryKey === true) {
37
- return k;
38
- }
39
- }
40
- if ("id" in table) {
41
- return "id";
42
- }
43
- return (
44
- Object.keys(table).find((k) => isDrizzleColumn((table as any)[k])) ?? "id"
45
- );
46
- }
47
-
48
- function isAllNullRow(obj: AnyObj | null | undefined): boolean {
49
- if (!obj || typeof obj !== "object") {
50
- return true;
51
- }
52
- for (const v of Object.values(obj)) {
53
- if (v !== null && v !== undefined) {
54
- return false;
55
- }
56
- }
57
- return true;
58
- }
59
-
60
- async function aliasTable(
61
- table: AnyObj,
62
- aliasName: string,
63
- dialect: string
64
- ): Promise<AnyObj> {
65
- // Drizzle exports `alias()` from dialect-specific core modules.
66
- // We keep this dynamic to avoid hard dependency on a single dialect.
67
- if (dialect === "PostgreSQL") {
68
- const mod: any = await import("drizzle-orm/pg-core");
69
- if (typeof mod.alias === "function") {
70
- return mod.alias(table, aliasName);
71
- }
72
- }
73
- if (dialect === "MySQL") {
74
- const mod: any = await import("drizzle-orm/mysql-core");
75
- if (typeof mod.alias === "function") {
76
- return mod.alias(table, aliasName);
77
- }
78
- }
79
- if (dialect === "SQLite") {
80
- const mod: any = await import("drizzle-orm/sqlite-core");
81
- if (typeof mod.alias === "function") {
82
- return mod.alias(table, aliasName);
83
- }
84
- }
85
-
86
- return table;
87
- }
88
-
89
- function buildJoinOn(node: JoinNode): any {
90
- const parts = node.sourceColumns.map((src, i) => {
91
- const tgt = node.targetColumns[i];
92
- // tgt is a column bound to the *original* target table; we need the one from alias table.
93
- const tgtKey = Object.entries(node.targetTable).find(
94
- ([, v]) => v === tgt
95
- )?.[0];
96
- const tgtCol = tgtKey ? (node.targetAliasTable as any)[tgtKey] : tgt;
97
- return eq(tgtCol, src);
98
- });
99
- return parts.length === 1 ? parts[0] : and(...parts);
100
- }
101
-
102
- function buildSelectMapForTable(table: AnyObj): AnyObj {
103
- const out: AnyObj = {};
104
- for (const [k, v] of Object.entries(table)) {
105
- if (isDrizzleColumn(v)) {
106
- out[k] = v;
107
- }
108
- }
109
- return out;
110
- }
111
-
112
- export async function executeWithJoins(args: {
113
- db: any;
114
- schema: Record<string, any>;
115
- relations: Record<string, any>;
116
- baseTableName: string;
117
- baseTable: AnyObj;
118
- dialect: string;
119
- whereSql?: any;
120
- withValue: AnyObj;
121
- limitOne?: boolean;
122
- }): Promise<any> {
123
- const {
124
- db,
125
- schema,
126
- relations,
127
- baseTableName,
128
- baseTable,
129
- dialect,
130
- whereSql,
131
- withValue,
132
- limitOne,
133
- } = args;
134
-
135
- const usedAliasKeys = new Set<string>();
136
-
137
- const buildNode = async (
138
- parent: JoinNode | undefined,
139
- currentTableName: string,
140
- currentTable: AnyObj,
141
- key: string,
142
- value: any,
143
- path: string[]
144
- ): Promise<JoinNode> => {
145
- const relMeta = (relations as any)[currentTableName]?.relations?.[key];
146
- if (!relMeta) {
147
- throw new Error(
148
- `Unknown relation '${key}' on table '${currentTableName}'.`
149
- );
150
- }
151
-
152
- const targetTableName: string = relMeta.targetTableName;
153
- const targetTable: AnyObj = (schema as any)[targetTableName];
154
- const aliasKeyBase = [...path, key].join("__");
155
- let aliasKey = aliasKeyBase;
156
- let idx = 1;
157
- while (usedAliasKeys.has(aliasKey)) {
158
- aliasKey = `${aliasKeyBase}_${idx++}`;
159
- }
160
- usedAliasKeys.add(aliasKey);
161
-
162
- const needsAlias =
163
- targetTableName === currentTableName ||
164
- usedAliasKeys.has(`table:${targetTableName}`);
165
- usedAliasKeys.add(`table:${targetTableName}`);
166
-
167
- const targetAliasTable = needsAlias
168
- ? await aliasTable(targetTable, aliasKey, dialect)
169
- : targetTable;
170
-
171
- const node: JoinNode = {
172
- path: [...path, key],
173
- key,
174
- relationType: relMeta.relationType,
175
- sourceTableName: currentTableName,
176
- targetTableName,
177
- sourceTable: currentTable,
178
- targetTable,
179
- targetAliasTable,
180
- aliasKey,
181
- sourceColumns: relMeta.sourceColumns ?? [],
182
- targetColumns: relMeta.targetColumns ?? [],
183
- pkField: getPrimaryKeyField(targetAliasTable),
184
- parent,
185
- children: [],
186
- };
187
-
188
- if (value && typeof value === "object" && value !== true) {
189
- for (const [childKey, childVal] of Object.entries(value)) {
190
- if (
191
- childVal !== true &&
192
- (typeof childVal !== "object" || childVal == null)
193
- ) {
194
- continue;
195
- }
196
- const child = await buildNode(
197
- node,
198
- targetTableName,
199
- targetAliasTable,
200
- childKey,
201
- childVal,
202
- [...path, key]
203
- );
204
- node.children.push(child);
205
- }
206
- }
207
-
208
- return node;
209
- };
210
-
211
- const root: JoinNode = {
212
- path: [],
213
- key: "$root",
214
- relationType: "one",
215
- sourceTableName: baseTableName,
216
- targetTableName: baseTableName,
217
- sourceTable: baseTable,
218
- targetTable: baseTable,
219
- targetAliasTable: baseTable,
220
- aliasKey: "$base",
221
- sourceColumns: [],
222
- targetColumns: [],
223
- pkField: getPrimaryKeyField(baseTable),
224
- children: [],
225
- };
226
-
227
- for (const [key, value] of Object.entries(withValue)) {
228
- if (value !== true && (typeof value !== "object" || value == null)) {
229
- continue;
230
- }
231
- const child = await buildNode(
232
- undefined,
233
- baseTableName,
234
- baseTable,
235
- key,
236
- value,
237
- []
238
- );
239
- root.children.push(child);
240
- }
241
-
242
- // Flatten nodes in join order (preorder)
243
- const nodes: JoinNode[] = [];
244
- const walk = (n: JoinNode) => {
245
- for (const c of n.children) {
246
- nodes.push(c);
247
- walk(c);
248
- }
249
- };
250
- walk(root);
251
-
252
- // Build select map: base + each joined alias
253
- const selectMap: AnyObj = {
254
- base: buildSelectMapForTable(baseTable),
255
- };
256
- for (const n of nodes) {
257
- selectMap[n.aliasKey] = buildSelectMapForTable(n.targetAliasTable);
258
- }
259
-
260
- let q = db.select(selectMap).from(baseTable);
261
- if (whereSql) {
262
- q = q.where(whereSql);
263
- }
264
-
265
- // Apply joins
266
- for (const n of nodes) {
267
- const on = buildJoinOn(n);
268
- q = q.leftJoin(n.targetAliasTable, on);
269
- }
270
-
271
- if (limitOne) {
272
- q = q.limit(1);
273
- }
274
-
275
- const rows = await q;
276
-
277
- // Group rows into nested objects.
278
- const basePk = root.pkField;
279
- const baseMap = new Map<any, AnyObj>();
280
-
281
- const ensureManyContainer = (obj: AnyObj, key: string) => {
282
- if (!Array.isArray(obj[key])) {
283
- obj[key] = [];
284
- }
285
- };
286
-
287
- const ensureOneContainer = (obj: AnyObj, key: string) => {
288
- if (!(key in obj)) {
289
- obj[key] = null;
290
- }
291
- };
292
-
293
- const manyIndexByPath = new Map<string, Map<any, AnyObj>>();
294
-
295
- for (const row of rows) {
296
- const baseRow = (row as any).base;
297
- const baseId = (baseRow as any)[basePk];
298
- if (baseId === undefined) {
299
- continue;
300
- }
301
-
302
- const baseObj = (() => {
303
- const existing = baseMap.get(baseId);
304
- if (existing) {
305
- return existing;
306
- }
307
- const created = { ...baseRow };
308
- baseMap.set(baseId, created);
309
- return created;
310
- })();
311
-
312
- // Walk nodes, attach to parent objects.
313
- for (const n of nodes) {
314
- const data = (row as any)[n.aliasKey];
315
- const relPath = n.path.join(".");
316
-
317
- // Resolve parent container
318
- const parentPath = n.parent ? n.parent.path.join(".") : "";
319
- let parentObj: AnyObj = baseObj;
320
- if (parentPath) {
321
- // parent might be many; we attach to the last inserted parent instance.
322
- const parentIndex = manyIndexByPath.get(parentPath);
323
- if (parentIndex && parentIndex.size) {
324
- // pick last inserted (Map preserves insertion order)
325
- parentObj = Array.from(parentIndex.values()).at(-1) as AnyObj;
326
- } else {
327
- const parentKey = n.parent?.key;
328
- parentObj = parentKey
329
- ? ((baseObj as any)[parentKey] ?? baseObj)
330
- : baseObj;
331
- }
332
- }
333
-
334
- if (isAllNullRow(data)) {
335
- if (n.relationType === "one") {
336
- ensureOneContainer(parentObj, n.key);
337
- } else {
338
- ensureManyContainer(parentObj, n.key);
339
- }
340
- continue;
341
- }
342
-
343
- const pk = (data as any)[n.pkField];
344
- if (n.relationType === "one") {
345
- parentObj[n.key] = { ...(data as any) };
346
- } else {
347
- ensureManyContainer(parentObj, n.key);
348
- const indexKey = relPath;
349
- if (!manyIndexByPath.has(indexKey)) {
350
- manyIndexByPath.set(indexKey, new Map());
351
- }
352
- const idxMap = manyIndexByPath.get(indexKey)!;
353
- if (!idxMap.has(pk)) {
354
- const obj = { ...(data as any) };
355
- idxMap.set(pk, obj);
356
- (parentObj[n.key] as any[]).push(obj);
357
- }
358
- }
359
- }
360
- }
361
-
362
- const out = Array.from(baseMap.values());
363
- return limitOne ? out[0] : out;
364
- }
@@ -1,61 +0,0 @@
1
- type AnyObj = Record<string, any>;
2
-
3
- type ProjectionResult = {
4
- selectMap?: AnyObj;
5
- };
6
-
7
- function isDrizzleColumn(value: any): boolean {
8
- return (
9
- !!value && typeof value === "object" && typeof value.getSQL === "function"
10
- );
11
- }
12
-
13
- function getTableColumnsMap(table: AnyObj): AnyObj {
14
- const out: AnyObj = {};
15
- for (const [key, value] of Object.entries(table)) {
16
- if (isDrizzleColumn(value)) {
17
- out[key] = value;
18
- }
19
- }
20
- return out;
21
- }
22
-
23
- export function buildSelectProjection(
24
- table: AnyObj,
25
- select?: AnyObj,
26
- exclude?: AnyObj
27
- ): ProjectionResult {
28
- const all = getTableColumnsMap(table);
29
-
30
- if (select && typeof select === "object") {
31
- const picked: AnyObj = {};
32
- for (const [key, value] of Object.entries(select)) {
33
- if (value === true && key in all) {
34
- picked[key] = all[key];
35
- }
36
- }
37
-
38
- // If nothing was picked, fall back to selecting all columns.
39
- if (Object.keys(picked).length) {
40
- return { selectMap: picked };
41
- }
42
- return { selectMap: all };
43
- }
44
-
45
- if (exclude && typeof exclude === "object") {
46
- const omitted: AnyObj = { ...all };
47
- for (const [key, value] of Object.entries(exclude)) {
48
- if (value === true) {
49
- delete omitted[key];
50
- }
51
- }
52
-
53
- // If everything was excluded (edge case), fall back to selecting all columns.
54
- if (Object.keys(omitted).length) {
55
- return { selectMap: omitted };
56
- }
57
- return { selectMap: all };
58
- }
59
-
60
- return { selectMap: all };
61
- }