@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,2 @@
1
+
2
+ $ tsc --noEmit
package/ROADMAP.md ADDED
@@ -0,0 +1 @@
1
+ - Support the (`Views`)[https://orm.drizzle.team/docs/views] on future.
package/TODO.md ADDED
@@ -0,0 +1,64 @@
1
+ - * Add all essential functions as `update`, `insert`, `delete`, `findMany`, `findFirst`
2
+ - Make `route` function (Just duplicate):
3
+ ```ts
4
+ const [val1, val2] = await userModel.name({
5
+ like: "A%"
6
+ }).route(
7
+ (userModel) => userModel.isVerified(true).findMany(),
8
+ (userModel) => userModel.isVerified(false).age({
9
+ lte: 18
10
+ }).findMany()
11
+ // ...args[]
12
+ )
13
+ ```
14
+ - Make built in `paginate` which is pagination function, used as:
15
+ ```ts
16
+ const userModel = model({
17
+ table: userTable,
18
+
19
+ // Optional
20
+ pagination: {
21
+ max: 10
22
+ }
23
+ })
24
+
25
+ const page = 1;
26
+
27
+ userModel.name({
28
+ like: "A%"
29
+ }).userId(userId).paginate(page)
30
+ ```
31
+ * Make built in `upsert`, as:
32
+ ```ts
33
+ userModel.id(123).upsert({
34
+ // create obj
35
+ }, {
36
+ // update obj
37
+ }, /* options */)
38
+ ```
39
+
40
+ * Transactions:
41
+ ```ts
42
+ userModel.transaction(tx => ...);
43
+
44
+ // With other models
45
+ userModel.transaction(tx => {
46
+ postsModel.db(tx).insert({
47
+ userId: 123,
48
+ content: ...
49
+ })
50
+
51
+ // or
52
+
53
+ const txPostsModel = postsModel.db(tx);
54
+
55
+ txPostsModel.insert({
56
+ userId: 123,
57
+ content: ...
58
+ })
59
+
60
+ txPostsModel.userId(123).delete()
61
+ });
62
+ ```
63
+ - / Soft delete?
64
+ - * Dialect based configuration `mysql`, `pgsql` and etc... like `returning()` on pgsql and `$returningId()` on mysql
@@ -0,0 +1,11 @@
1
+ import "dotenv/config";
2
+ import { defineConfig } from "drizzle-kit";
3
+
4
+ export default defineConfig({
5
+ out: "./drizzle",
6
+ schema: "./tests/schema.ts",
7
+ dialect: "postgresql",
8
+ dbCredentials: {
9
+ url: process.env.DATABASE_URL!,
10
+ },
11
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@apisr/drizzle-model",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "scripts": {
6
+ "lint": "eslint . --max-warnings 0",
7
+ "generate:component": "turbo gen react-component",
8
+ "check-types": "tsc --noEmit"
9
+ },
10
+ "peerDependencies": {
11
+ "drizzle-orm": "^1.0.0-beta.2-86f844e"
12
+ },
13
+ "devDependencies": {
14
+ "@repo/eslint-config": "*",
15
+ "@repo/typescript-config": "*",
16
+ "@types/bun": "latest",
17
+ "@types/node": "^22.15.3",
18
+ "@types/pg": "^8.15.6",
19
+ "dotenv": "^17.2.3",
20
+ "drizzle-kit": "^1.0.0-beta.2-86f844e",
21
+ "drizzle-orm": "^1.0.0-beta.2-86f844e",
22
+ "eslint": "^9.39.1",
23
+ "typescript": "5.9.2"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.mts",
28
+ "default": "./dist/index.mjs"
29
+ }
30
+ },
31
+ "type": "module",
32
+ "dependencies": {
33
+ "pg": "^8.16.3"
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./model/index.ts";
@@ -0,0 +1,46 @@
1
+ import type { AnyRelations, EmptyRelations } from "drizzle-orm";
2
+ import type { Model } from "./model.ts";
3
+ import type { ModelDialect } from "./dialect.ts";
4
+ import type { ModelOptions } from "./options.ts";
5
+ import type { ModelConfig } from "./config.ts";
6
+ import { makeModelRuntime } from "./core/runtime.ts";
7
+
8
+ export function modelBuilder<
9
+ TFullSchema extends Record<string, unknown> = Record<string, never>,
10
+ TRelations extends AnyRelations = EmptyRelations,
11
+ TDialect extends ModelDialect = ModelDialect,
12
+ >({
13
+ db,
14
+ relations,
15
+ schema,
16
+ dialect,
17
+ }: {
18
+ relations: TRelations;
19
+ db: any;
20
+ schema: TFullSchema;
21
+ dialect: TDialect;
22
+ }) {
23
+ return <
24
+ TTableName extends keyof TRelations,
25
+ TOptions extends ModelOptions<
26
+ TRelations,
27
+ TRelations[TTableName],
28
+ TDialect,
29
+ TOptions
30
+ >,
31
+ >(
32
+ table: TTableName,
33
+ options: TOptions,
34
+ ) => {
35
+ return makeModelRuntime({
36
+ db,
37
+ relations: relations as any,
38
+ schema: schema as any,
39
+ tableName: table as any,
40
+ dialect,
41
+ options: (options ?? {}) as any,
42
+ }) as Model<
43
+ ModelConfig<TRelations, TRelations[TTableName], TDialect, TOptions>
44
+ >;
45
+ };
46
+ }
@@ -0,0 +1,35 @@
1
+ import type { TableRelationalConfig, TablesRelationalConfig } from "drizzle-orm/relations";
2
+ import type { ModelDialect } from "./dialect.ts";
3
+ import type { ModelOptions, ResolveOptionsFormat } from "./options.ts";
4
+ import type { IsTable, TableOutput } from "./table.ts";
5
+ import type { GetPrimarySerialOrDefaultKeys } from "./methods/return.ts";
6
+ import type { MethodWhereValue } from "./methods/query/where.ts";
7
+ import type { DrizzleColumnDataType } from "./query/operations.ts";
8
+
9
+ export type ModelConfig<
10
+ TSchema extends TablesRelationalConfig = TablesRelationalConfig,
11
+ TTable extends TableRelationalConfig = TableRelationalConfig,
12
+ TDialect extends ModelDialect = ModelDialect,
13
+ TOptions extends ModelOptions<any> = ModelOptions<any>
14
+ > = {
15
+ schema: TSchema;
16
+ table: TTable;
17
+ dialect: TDialect;
18
+ options: TOptions;
19
+
20
+ // Aliases:
21
+ tableOutput: TableOutput<TTable>;
22
+ tableColumns: IsTable<TTable["table"]>["_"]["columns"];
23
+
24
+ optionsFormat: ResolveOptionsFormat<TOptions["format"]>;
25
+
26
+ primaryKeys: keyof GetPrimarySerialOrDefaultKeys<
27
+ IsTable<TTable["table"]>["_"]["columns"]
28
+ >
29
+ primaryKeysWithDataType: {
30
+ [TKey in keyof GetPrimarySerialOrDefaultKeys<
31
+ IsTable<TTable["table"]>["_"]["columns"]
32
+ >]: DrizzleColumnDataType<IsTable<TTable["table"]>["_"]["columns"][TKey]>
33
+ }
34
+ whereValue: MethodWhereValue<TSchema, TTable>;
35
+ };
@@ -0,0 +1,279 @@
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 !!value && typeof value === "object" && typeof value.getSQL === "function";
24
+ }
25
+
26
+ function getPrimaryKeyField(table: AnyObj): string {
27
+ for (const [k, v] of Object.entries(table)) {
28
+ if (!isDrizzleColumn(v)) continue;
29
+ if ((v as any).primary === true) return k;
30
+ if ((v as any).config?.primaryKey === true) return k;
31
+ }
32
+ if ("id" in table) return "id";
33
+ return Object.keys(table).find((k) => isDrizzleColumn((table as any)[k])) ?? "id";
34
+ }
35
+
36
+ function isAllNullRow(obj: AnyObj | null | undefined): boolean {
37
+ if (!obj || typeof obj !== "object") return true;
38
+ for (const v of Object.values(obj)) {
39
+ if (v !== null && v !== undefined) return false;
40
+ }
41
+ return true;
42
+ }
43
+
44
+ async function aliasTable(table: AnyObj, aliasName: string, dialect: string): Promise<AnyObj> {
45
+ // Drizzle exports `alias()` from dialect-specific core modules.
46
+ // We keep this dynamic to avoid hard dependency on a single dialect.
47
+ if (dialect === "PostgreSQL") {
48
+ const mod: any = await import("drizzle-orm/pg-core");
49
+ if (typeof mod.alias === "function") return mod.alias(table, aliasName);
50
+ }
51
+ if (dialect === "MySQL") {
52
+ const mod: any = await import("drizzle-orm/mysql-core");
53
+ if (typeof mod.alias === "function") return mod.alias(table, aliasName);
54
+ }
55
+ if (dialect === "SQLite") {
56
+ const mod: any = await import("drizzle-orm/sqlite-core");
57
+ if (typeof mod.alias === "function") return mod.alias(table, aliasName);
58
+ }
59
+
60
+ return table;
61
+ }
62
+
63
+ function buildJoinOn(node: JoinNode): any {
64
+ const parts = node.sourceColumns.map((src, i) => {
65
+ const tgt = node.targetColumns[i];
66
+ // tgt is a column bound to the *original* target table; we need the one from alias table.
67
+ const tgtKey = Object.entries(node.targetTable).find(([, v]) => v === tgt)?.[0];
68
+ const tgtCol = tgtKey ? (node.targetAliasTable as any)[tgtKey] : tgt;
69
+ return eq(tgtCol, src);
70
+ });
71
+ return parts.length === 1 ? parts[0] : and(...parts);
72
+ }
73
+
74
+ function buildSelectMapForTable(table: AnyObj): AnyObj {
75
+ const out: AnyObj = {};
76
+ for (const [k, v] of Object.entries(table)) {
77
+ if (isDrizzleColumn(v)) out[k] = v;
78
+ }
79
+ return out;
80
+ }
81
+
82
+ export async function executeWithJoins(args: {
83
+ db: any;
84
+ schema: Record<string, any>;
85
+ relations: Record<string, any>;
86
+ baseTableName: string;
87
+ baseTable: AnyObj;
88
+ dialect: string;
89
+ whereSql?: any;
90
+ withValue: AnyObj;
91
+ limitOne?: boolean;
92
+ }): Promise<any> {
93
+ const { db, schema, relations, baseTableName, baseTable, dialect, whereSql, withValue, limitOne } = args;
94
+
95
+ const usedAliasKeys = new Set<string>();
96
+
97
+ const buildNode = async (
98
+ parent: JoinNode | undefined,
99
+ currentTableName: string,
100
+ currentTable: AnyObj,
101
+ key: string,
102
+ value: any,
103
+ path: string[],
104
+ ): Promise<JoinNode> => {
105
+ const relMeta = (relations as any)[currentTableName]?.relations?.[key];
106
+ if (!relMeta) throw new Error(`Unknown relation '${key}' on table '${currentTableName}'.`);
107
+
108
+ const targetTableName: string = relMeta.targetTableName;
109
+ const targetTable: AnyObj = (schema as any)[targetTableName];
110
+ const aliasKeyBase = [...path, key].join("__");
111
+ let aliasKey = aliasKeyBase;
112
+ let idx = 1;
113
+ while (usedAliasKeys.has(aliasKey)) {
114
+ aliasKey = `${aliasKeyBase}_${idx++}`;
115
+ }
116
+ usedAliasKeys.add(aliasKey);
117
+
118
+ const needsAlias = targetTableName === currentTableName || usedAliasKeys.has(`table:${targetTableName}`);
119
+ usedAliasKeys.add(`table:${targetTableName}`);
120
+
121
+ const targetAliasTable = needsAlias ? await aliasTable(targetTable, aliasKey, dialect) : targetTable;
122
+
123
+ const node: JoinNode = {
124
+ path: [...path, key],
125
+ key,
126
+ relationType: relMeta.relationType,
127
+ sourceTableName: currentTableName,
128
+ targetTableName,
129
+ sourceTable: currentTable,
130
+ targetTable,
131
+ targetAliasTable,
132
+ aliasKey,
133
+ sourceColumns: relMeta.sourceColumns ?? [],
134
+ targetColumns: relMeta.targetColumns ?? [],
135
+ pkField: getPrimaryKeyField(targetAliasTable),
136
+ parent,
137
+ children: [],
138
+ };
139
+
140
+ if (value && typeof value === "object" && value !== true) {
141
+ for (const [childKey, childVal] of Object.entries(value)) {
142
+ if (childVal !== true && (typeof childVal !== "object" || childVal == null)) continue;
143
+ const child = await buildNode(node, targetTableName, targetAliasTable, childKey, childVal, [...path, key]);
144
+ node.children.push(child);
145
+ }
146
+ }
147
+
148
+ return node;
149
+ };
150
+
151
+ const root: JoinNode = {
152
+ path: [],
153
+ key: "$root",
154
+ relationType: "one",
155
+ sourceTableName: baseTableName,
156
+ targetTableName: baseTableName,
157
+ sourceTable: baseTable,
158
+ targetTable: baseTable,
159
+ targetAliasTable: baseTable,
160
+ aliasKey: "$base",
161
+ sourceColumns: [],
162
+ targetColumns: [],
163
+ pkField: getPrimaryKeyField(baseTable),
164
+ children: [],
165
+ };
166
+
167
+ for (const [key, value] of Object.entries(withValue)) {
168
+ if (value !== true && (typeof value !== "object" || value == null)) continue;
169
+ const child = await buildNode(undefined, baseTableName, baseTable, key, value, []);
170
+ root.children.push(child);
171
+ }
172
+
173
+ // Flatten nodes in join order (preorder)
174
+ const nodes: JoinNode[] = [];
175
+ const walk = (n: JoinNode) => {
176
+ for (const c of n.children) {
177
+ nodes.push(c);
178
+ walk(c);
179
+ }
180
+ };
181
+ walk(root);
182
+
183
+ // Build select map: base + each joined alias
184
+ const selectMap: AnyObj = {
185
+ base: buildSelectMapForTable(baseTable),
186
+ };
187
+ for (const n of nodes) {
188
+ selectMap[n.aliasKey] = buildSelectMapForTable(n.targetAliasTable);
189
+ }
190
+
191
+ let q = db.select(selectMap).from(baseTable);
192
+ if (whereSql) q = q.where(whereSql);
193
+
194
+ // Apply joins
195
+ for (const n of nodes) {
196
+ const on = buildJoinOn(n);
197
+ q = q.leftJoin(n.targetAliasTable, on);
198
+ }
199
+
200
+ if (limitOne) q = q.limit(1);
201
+
202
+ const rows = await q;
203
+
204
+ // Group rows into nested objects.
205
+ const basePk = root.pkField;
206
+ const baseMap = new Map<any, AnyObj>();
207
+
208
+ const ensureManyContainer = (obj: AnyObj, key: string) => {
209
+ if (!Array.isArray(obj[key])) obj[key] = [];
210
+ };
211
+
212
+ const ensureOneContainer = (obj: AnyObj, key: string) => {
213
+ if (!(key in obj)) obj[key] = null;
214
+ };
215
+
216
+ const manyIndexByPath = new Map<string, Map<any, AnyObj>>();
217
+
218
+ for (const row of rows) {
219
+ const baseRow = (row as any).base;
220
+ const baseId = (baseRow as any)[basePk];
221
+ if (baseId === undefined) continue;
222
+
223
+ const baseObj = (() => {
224
+ const existing = baseMap.get(baseId);
225
+ if (existing) return existing;
226
+ const created = { ...baseRow };
227
+ baseMap.set(baseId, created);
228
+ return created;
229
+ })();
230
+
231
+ // Walk nodes, attach to parent objects.
232
+ for (const n of nodes) {
233
+ const data = (row as any)[n.aliasKey];
234
+ const relPath = n.path.join(".");
235
+
236
+ // Resolve parent container
237
+ const parentPath = n.parent ? n.parent.path.join(".") : "";
238
+ let parentObj: AnyObj = baseObj;
239
+ if (parentPath) {
240
+ // parent might be many; we attach to the last inserted parent instance.
241
+ const parentIndex = manyIndexByPath.get(parentPath);
242
+ if (parentIndex && parentIndex.size) {
243
+ // pick last inserted (Map preserves insertion order)
244
+ parentObj = Array.from(parentIndex.values()).at(-1) as AnyObj;
245
+ } else {
246
+ const parentKey = n.parent?.key;
247
+ parentObj = parentKey ? ((baseObj as any)[parentKey] ?? baseObj) : baseObj;
248
+ }
249
+ }
250
+
251
+ if (isAllNullRow(data)) {
252
+ if (n.relationType === "one") {
253
+ ensureOneContainer(parentObj, n.key);
254
+ } else {
255
+ ensureManyContainer(parentObj, n.key);
256
+ }
257
+ continue;
258
+ }
259
+
260
+ const pk = (data as any)[n.pkField];
261
+ if (n.relationType === "one") {
262
+ parentObj[n.key] = { ...(data as any) };
263
+ } else {
264
+ ensureManyContainer(parentObj, n.key);
265
+ const indexKey = relPath;
266
+ if (!manyIndexByPath.has(indexKey)) manyIndexByPath.set(indexKey, new Map());
267
+ const idxMap = manyIndexByPath.get(indexKey)!;
268
+ if (!idxMap.has(pk)) {
269
+ const obj = { ...(data as any) };
270
+ idxMap.set(pk, obj);
271
+ (parentObj[n.key] as any[]).push(obj);
272
+ }
273
+ }
274
+ }
275
+ }
276
+
277
+ const out = Array.from(baseMap.values());
278
+ return limitOne ? out[0] : out;
279
+ }
@@ -0,0 +1,47 @@
1
+ type AnyObj = Record<string, any>;
2
+
3
+ type ProjectionResult = {
4
+ selectMap?: AnyObj;
5
+ };
6
+
7
+ function isDrizzleColumn(value: any): boolean {
8
+ return !!value && typeof value === "object" && typeof value.getSQL === "function";
9
+ }
10
+
11
+ function getTableColumnsMap(table: AnyObj): AnyObj {
12
+ const out: AnyObj = {};
13
+ for (const [key, value] of Object.entries(table)) {
14
+ if (isDrizzleColumn(value)) out[key] = value;
15
+ }
16
+ return out;
17
+ }
18
+
19
+ export function buildSelectProjection(table: AnyObj, select?: AnyObj, exclude?: AnyObj): ProjectionResult {
20
+ const all = getTableColumnsMap(table);
21
+
22
+ if (select && typeof select === "object") {
23
+ const picked: AnyObj = {};
24
+ for (const [key, value] of Object.entries(select)) {
25
+ if (value === true && key in all) {
26
+ picked[key] = all[key];
27
+ }
28
+ }
29
+
30
+ // If nothing was picked, fall back to selecting all columns.
31
+ if (Object.keys(picked).length) return { selectMap: picked };
32
+ return { selectMap: all };
33
+ }
34
+
35
+ if (exclude && typeof exclude === "object") {
36
+ const omitted: AnyObj = { ...all };
37
+ for (const [key, value] of Object.entries(exclude)) {
38
+ if (value === true) delete omitted[key];
39
+ }
40
+
41
+ // If everything was excluded (edge case), fall back to selecting all columns.
42
+ if (Object.keys(omitted).length) return { selectMap: omitted };
43
+ return { selectMap: all };
44
+ }
45
+
46
+ return { selectMap: all };
47
+ }