@fragno-dev/db 0.0.1 → 0.1.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.
- package/.turbo/turbo-build.log +137 -13
- package/.turbo/turbo-test.log +36 -0
- package/CHANGELOG.md +7 -0
- package/dist/adapters/adapters.d.ts +18 -0
- package/dist/adapters/adapters.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +21 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-adapter.js +62 -0
- package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts +17 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-query.js +139 -0
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +9 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +300 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +82 -0
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -0
- package/dist/adapters/drizzle/drizzle-uow-executor.js +125 -0
- package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -0
- package/dist/adapters/drizzle/generate.js +273 -0
- package/dist/adapters/drizzle/generate.js.map +1 -0
- package/dist/adapters/drizzle/join-column-utils.js +28 -0
- package/dist/adapters/drizzle/join-column-utils.js.map +1 -0
- package/dist/adapters/drizzle/shared.js +11 -0
- package/dist/adapters/drizzle/shared.js.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +23 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.js +119 -0
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -0
- package/dist/adapters/kysely/kysely-query-builder.js +306 -0
- package/dist/adapters/kysely/kysely-query-builder.js.map +1 -0
- package/dist/adapters/kysely/kysely-query-compiler.js +67 -0
- package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -0
- package/dist/adapters/kysely/kysely-query.js +158 -0
- package/dist/adapters/kysely/kysely-query.js.map +1 -0
- package/dist/adapters/kysely/kysely-uow-compiler.js +139 -0
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -0
- package/dist/adapters/kysely/kysely-uow-executor.js +89 -0
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -0
- package/dist/adapters/kysely/migration/execute.js +176 -0
- package/dist/adapters/kysely/migration/execute.js.map +1 -0
- package/dist/fragment.d.ts +54 -0
- package/dist/fragment.d.ts.map +1 -0
- package/dist/fragment.js +92 -0
- package/dist/fragment.js.map +1 -0
- package/dist/id.d.ts +2 -0
- package/dist/migration-engine/auto-from-schema.js +116 -0
- package/dist/migration-engine/auto-from-schema.js.map +1 -0
- package/dist/migration-engine/create.d.ts +41 -0
- package/dist/migration-engine/create.d.ts.map +1 -0
- package/dist/migration-engine/create.js +58 -0
- package/dist/migration-engine/create.js.map +1 -0
- package/dist/migration-engine/shared.d.ts +90 -0
- package/dist/migration-engine/shared.d.ts.map +1 -0
- package/dist/migration-engine/shared.js +8 -0
- package/dist/migration-engine/shared.js.map +1 -0
- package/dist/mod.d.ts +55 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +111 -2
- package/dist/mod.js.map +1 -1
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js +108 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js +55 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js +18 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js +183 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js +58 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js +68 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js +56 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js +65 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js +81 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js +13 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js +10 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js +372 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js +23 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js +62 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js +6 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js +8 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js +8 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js.map +1 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js +6 -0
- package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js.map +1 -0
- package/dist/query/condition-builder.d.ts +41 -0
- package/dist/query/condition-builder.d.ts.map +1 -0
- package/dist/query/condition-builder.js +93 -0
- package/dist/query/condition-builder.js.map +1 -0
- package/dist/query/cursor.d.ts +88 -0
- package/dist/query/cursor.d.ts.map +1 -0
- package/dist/query/cursor.js +103 -0
- package/dist/query/cursor.js.map +1 -0
- package/dist/query/orm/orm.d.ts +18 -0
- package/dist/query/orm/orm.d.ts.map +1 -0
- package/dist/query/orm/orm.js +48 -0
- package/dist/query/orm/orm.js.map +1 -0
- package/dist/query/query.d.ts +79 -0
- package/dist/query/query.d.ts.map +1 -0
- package/dist/query/query.js +1 -0
- package/dist/query/result-transform.js +155 -0
- package/dist/query/result-transform.js.map +1 -0
- package/dist/query/unit-of-work.d.ts +435 -0
- package/dist/query/unit-of-work.d.ts.map +1 -0
- package/dist/query/unit-of-work.js +549 -0
- package/dist/query/unit-of-work.js.map +1 -0
- package/dist/schema/create.d.ts +273 -116
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +410 -222
- package/dist/schema/create.js.map +1 -1
- package/dist/schema/serialize.js +101 -0
- package/dist/schema/serialize.js.map +1 -0
- package/dist/schema-generator/schema-generator.d.ts +15 -0
- package/dist/schema-generator/schema-generator.d.ts.map +1 -0
- package/dist/shared/providers.d.ts +6 -0
- package/dist/shared/providers.d.ts.map +1 -0
- package/dist/util/import-generator.js +26 -0
- package/dist/util/import-generator.js.map +1 -0
- package/dist/util/parse.js +15 -0
- package/dist/util/parse.js.map +1 -0
- package/dist/util/types.d.ts +8 -0
- package/dist/util/types.d.ts.map +1 -0
- package/package.json +63 -2
- package/src/adapters/adapters.ts +22 -0
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +433 -0
- package/src/adapters/drizzle/drizzle-adapter.test.ts +122 -0
- package/src/adapters/drizzle/drizzle-adapter.ts +118 -0
- package/src/adapters/drizzle/drizzle-query.ts +234 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +1084 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +546 -0
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +165 -0
- package/src/adapters/drizzle/drizzle-uow-executor.ts +213 -0
- package/src/adapters/drizzle/generate.test.ts +643 -0
- package/src/adapters/drizzle/generate.ts +481 -0
- package/src/adapters/drizzle/join-column-utils.test.ts +79 -0
- package/src/adapters/drizzle/join-column-utils.ts +39 -0
- package/src/adapters/drizzle/migrate-drizzle.test.ts +226 -0
- package/src/adapters/drizzle/shared.ts +22 -0
- package/src/adapters/drizzle/test-utils.ts +56 -0
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +789 -0
- package/src/adapters/kysely/kysely-adapter.ts +196 -0
- package/src/adapters/kysely/kysely-query-builder.test.ts +1344 -0
- package/src/adapters/kysely/kysely-query-builder.ts +611 -0
- package/src/adapters/kysely/kysely-query-compiler.ts +124 -0
- package/src/adapters/kysely/kysely-query.ts +254 -0
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +916 -0
- package/src/adapters/kysely/kysely-uow-compiler.ts +271 -0
- package/src/adapters/kysely/kysely-uow-executor.ts +149 -0
- package/src/adapters/kysely/kysely-uow-joins.test.ts +811 -0
- package/src/adapters/kysely/migration/execute-mysql.test.ts +1173 -0
- package/src/adapters/kysely/migration/execute-postgres.test.ts +2657 -0
- package/src/adapters/kysely/migration/execute.ts +382 -0
- package/src/adapters/kysely/migration/kysely-migrator.test.ts +197 -0
- package/src/fragment.test.ts +287 -0
- package/src/fragment.ts +198 -0
- package/src/migration-engine/auto-from-schema.test.ts +118 -58
- package/src/migration-engine/auto-from-schema.ts +103 -32
- package/src/migration-engine/create.test.ts +34 -46
- package/src/migration-engine/create.ts +41 -26
- package/src/migration-engine/shared.ts +26 -6
- package/src/mod.ts +197 -1
- package/src/query/condition-builder.test.ts +379 -0
- package/src/query/condition-builder.ts +294 -0
- package/src/query/cursor.test.ts +296 -0
- package/src/query/cursor.ts +147 -0
- package/src/query/orm/orm.ts +92 -0
- package/src/query/query-type.test.ts +429 -0
- package/src/query/query.ts +200 -0
- package/src/query/result-transform.test.ts +795 -0
- package/src/query/result-transform.ts +247 -0
- package/src/query/unit-of-work-types.test.ts +192 -0
- package/src/query/unit-of-work.test.ts +947 -0
- package/src/query/unit-of-work.ts +1199 -0
- package/src/schema/create.test.ts +653 -110
- package/src/schema/create.ts +708 -337
- package/src/schema/serialize.test.ts +559 -0
- package/src/schema/serialize.ts +359 -0
- package/src/schema-generator/schema-generator.ts +12 -0
- package/src/shared/config.ts +0 -8
- package/src/util/import-generator.ts +28 -0
- package/src/util/parse.ts +16 -0
- package/src/util/types.ts +4 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +11 -1
- package/vitest.config.ts +3 -0
- /package/dist/{cuid.js → id.js} +0 -0
- /package/src/{cuid.ts → id.ts} +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { column, idColumn, schema } from "../schema/create";
|
|
3
|
+
import { createBuilder, createIndexedBuilder } from "./condition-builder";
|
|
4
|
+
|
|
5
|
+
describe("ConditionBuilder", () => {
|
|
6
|
+
const testSchema = schema((s) =>
|
|
7
|
+
s
|
|
8
|
+
.addTable("users", (t) =>
|
|
9
|
+
t
|
|
10
|
+
.addColumn("id", idColumn())
|
|
11
|
+
.addColumn("email", column("string"))
|
|
12
|
+
.addColumn("name", column("string"))
|
|
13
|
+
.addColumn("age", column("integer").nullable())
|
|
14
|
+
.createIndex("_primary", ["id"], { unique: true })
|
|
15
|
+
.createIndex("idx_email", ["email"], { unique: true })
|
|
16
|
+
.createIndex("idx_name_age", ["name", "age"]),
|
|
17
|
+
)
|
|
18
|
+
.addTable("posts", (t) =>
|
|
19
|
+
t
|
|
20
|
+
.addColumn("id", idColumn())
|
|
21
|
+
.addColumn("title", column("string"))
|
|
22
|
+
.addColumn("content", column("string"))
|
|
23
|
+
.addColumn("published", column("bool"))
|
|
24
|
+
.createIndex("_primary", ["id"], { unique: true })
|
|
25
|
+
.createIndex("idx_published", ["published"]),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const usersTable = testSchema.tables.users;
|
|
30
|
+
|
|
31
|
+
describe("createBuilder", () => {
|
|
32
|
+
it("should create conditions with all columns", () => {
|
|
33
|
+
const builder = createBuilder(usersTable.columns);
|
|
34
|
+
|
|
35
|
+
const condition = builder("email", "=", "test@example.com");
|
|
36
|
+
expect(condition).toEqual({
|
|
37
|
+
type: "compare",
|
|
38
|
+
a: usersTable.columns.email,
|
|
39
|
+
operator: "=",
|
|
40
|
+
b: "test@example.com",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should support comparison operators", () => {
|
|
45
|
+
const builder = createBuilder(usersTable.columns);
|
|
46
|
+
|
|
47
|
+
expect(builder("age", ">", 18)).toEqual({
|
|
48
|
+
type: "compare",
|
|
49
|
+
a: usersTable.columns.age,
|
|
50
|
+
operator: ">",
|
|
51
|
+
b: 18,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(builder("age", "<=", 65)).toEqual({
|
|
55
|
+
type: "compare",
|
|
56
|
+
a: usersTable.columns.age,
|
|
57
|
+
operator: "<=",
|
|
58
|
+
b: 65,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should support array operators", () => {
|
|
63
|
+
const builder = createBuilder(usersTable.columns);
|
|
64
|
+
|
|
65
|
+
const condition = builder("name", "in", ["Alice", "Bob"]);
|
|
66
|
+
expect(condition).toEqual({
|
|
67
|
+
type: "compare",
|
|
68
|
+
a: usersTable.columns.name,
|
|
69
|
+
operator: "in",
|
|
70
|
+
b: ["Alice", "Bob"],
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should support string operators", () => {
|
|
75
|
+
const builder = createBuilder(usersTable.columns);
|
|
76
|
+
|
|
77
|
+
const condition = builder("email", "contains", "example");
|
|
78
|
+
expect(condition).toEqual({
|
|
79
|
+
type: "compare",
|
|
80
|
+
a: usersTable.columns.email,
|
|
81
|
+
operator: "contains",
|
|
82
|
+
b: "example",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should support null checks", () => {
|
|
87
|
+
const builder = createBuilder(usersTable.columns);
|
|
88
|
+
|
|
89
|
+
expect(builder.isNull("age")).toEqual({
|
|
90
|
+
type: "compare",
|
|
91
|
+
a: usersTable.columns.age,
|
|
92
|
+
operator: "is",
|
|
93
|
+
b: null,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(builder.isNotNull("age")).toEqual({
|
|
97
|
+
type: "compare",
|
|
98
|
+
a: usersTable.columns.age,
|
|
99
|
+
operator: "is not",
|
|
100
|
+
b: null,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should support boolean columns", () => {
|
|
105
|
+
const postsTable = testSchema.tables.posts;
|
|
106
|
+
const builder = createBuilder(postsTable.columns);
|
|
107
|
+
|
|
108
|
+
expect(builder("published")).toEqual({
|
|
109
|
+
type: "compare",
|
|
110
|
+
a: postsTable.columns.published,
|
|
111
|
+
operator: "=",
|
|
112
|
+
b: true,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should support AND conditions", () => {
|
|
117
|
+
const builder = createBuilder(usersTable.columns);
|
|
118
|
+
|
|
119
|
+
const condition = builder.and(builder("age", ">", 18), builder("age", "<", 65));
|
|
120
|
+
|
|
121
|
+
expect(condition).toEqual({
|
|
122
|
+
type: "and",
|
|
123
|
+
items: [
|
|
124
|
+
{
|
|
125
|
+
type: "compare",
|
|
126
|
+
a: usersTable.columns.age,
|
|
127
|
+
operator: ">",
|
|
128
|
+
b: 18,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: "compare",
|
|
132
|
+
a: usersTable.columns.age,
|
|
133
|
+
operator: "<",
|
|
134
|
+
b: 65,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should support OR conditions", () => {
|
|
141
|
+
const builder = createBuilder(usersTable.columns);
|
|
142
|
+
|
|
143
|
+
const condition = builder.or(
|
|
144
|
+
builder("email", "contains", "gmail"),
|
|
145
|
+
builder("email", "contains", "yahoo"),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(condition).toEqual({
|
|
149
|
+
type: "or",
|
|
150
|
+
items: [
|
|
151
|
+
{
|
|
152
|
+
type: "compare",
|
|
153
|
+
a: usersTable.columns.email,
|
|
154
|
+
operator: "contains",
|
|
155
|
+
b: "gmail",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: "compare",
|
|
159
|
+
a: usersTable.columns.email,
|
|
160
|
+
operator: "contains",
|
|
161
|
+
b: "yahoo",
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should support NOT conditions", () => {
|
|
168
|
+
const builder = createBuilder(usersTable.columns);
|
|
169
|
+
|
|
170
|
+
const condition = builder.not(builder("age", ">", 65));
|
|
171
|
+
|
|
172
|
+
expect(condition).toEqual({
|
|
173
|
+
type: "not",
|
|
174
|
+
item: {
|
|
175
|
+
type: "compare",
|
|
176
|
+
a: usersTable.columns.age,
|
|
177
|
+
operator: ">",
|
|
178
|
+
b: 65,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should handle boolean shortcuts in AND", () => {
|
|
184
|
+
const builder = createBuilder(usersTable.columns);
|
|
185
|
+
|
|
186
|
+
// true in AND should be ignored
|
|
187
|
+
expect(builder.and(true, builder("age", ">", 18))).toEqual({
|
|
188
|
+
type: "and",
|
|
189
|
+
items: [
|
|
190
|
+
{
|
|
191
|
+
type: "compare",
|
|
192
|
+
a: usersTable.columns.age,
|
|
193
|
+
operator: ">",
|
|
194
|
+
b: 18,
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// false in AND should short-circuit
|
|
200
|
+
expect(builder.and(false, builder("age", ">", 18))).toBe(false);
|
|
201
|
+
|
|
202
|
+
// all true should return true
|
|
203
|
+
expect(builder.and(true, true)).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should handle boolean shortcuts in OR", () => {
|
|
207
|
+
const builder = createBuilder(usersTable.columns);
|
|
208
|
+
|
|
209
|
+
// true in OR should short-circuit
|
|
210
|
+
expect(builder.or(true, builder("age", ">", 18))).toBe(true);
|
|
211
|
+
|
|
212
|
+
// false in OR should be ignored
|
|
213
|
+
expect(builder.or(false, builder("age", ">", 18))).toEqual({
|
|
214
|
+
type: "or",
|
|
215
|
+
items: [
|
|
216
|
+
{
|
|
217
|
+
type: "compare",
|
|
218
|
+
a: usersTable.columns.age,
|
|
219
|
+
operator: ">",
|
|
220
|
+
b: 18,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// all false should return false
|
|
226
|
+
expect(builder.or(false, false)).toBe(false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should throw on invalid column name", () => {
|
|
230
|
+
const builder = createBuilder(usersTable.columns);
|
|
231
|
+
|
|
232
|
+
expect(() => builder("nonexistent" as "email", "=", "test")).toThrow(
|
|
233
|
+
"Invalid column name nonexistent",
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should throw on unsupported operator", () => {
|
|
238
|
+
const builder = createBuilder(usersTable.columns);
|
|
239
|
+
|
|
240
|
+
expect(() => builder("email", "invalid" as "=", "test")).toThrow(
|
|
241
|
+
"Unsupported operator: invalid",
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("createIndexedBuilder", () => {
|
|
247
|
+
it("should allow comparisons on indexed columns", () => {
|
|
248
|
+
const indexedColumns = new Set(["id", "email", "name", "age"]);
|
|
249
|
+
const builder = createIndexedBuilder(usersTable.columns, indexedColumns);
|
|
250
|
+
|
|
251
|
+
const condition = builder("email", "=", "test@example.com");
|
|
252
|
+
expect(condition).toEqual({
|
|
253
|
+
type: "compare",
|
|
254
|
+
a: usersTable.columns.email,
|
|
255
|
+
operator: "=",
|
|
256
|
+
b: "test@example.com",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("should allow comparisons on multiple indexed columns", () => {
|
|
261
|
+
const indexedColumns = new Set(["id", "email", "name", "age"]);
|
|
262
|
+
const builder = createIndexedBuilder(usersTable.columns, indexedColumns);
|
|
263
|
+
|
|
264
|
+
const condition = builder.and(builder("name", "=", "Alice"), builder("age", ">", 18));
|
|
265
|
+
|
|
266
|
+
expect(condition).toEqual({
|
|
267
|
+
type: "and",
|
|
268
|
+
items: [
|
|
269
|
+
{
|
|
270
|
+
type: "compare",
|
|
271
|
+
a: usersTable.columns.name,
|
|
272
|
+
operator: "=",
|
|
273
|
+
b: "Alice",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
type: "compare",
|
|
277
|
+
a: usersTable.columns.age,
|
|
278
|
+
operator: ">",
|
|
279
|
+
b: 18,
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should throw error when using non-indexed column", () => {
|
|
286
|
+
// Only email is indexed
|
|
287
|
+
const indexedColumns = new Set(["email"]);
|
|
288
|
+
const builder = createIndexedBuilder(usersTable.columns, indexedColumns);
|
|
289
|
+
|
|
290
|
+
expect(() => builder("name", "=", "Alice")).toThrow(
|
|
291
|
+
'Column "name" is not indexed. Only indexed columns can be used in Unit of Work queries. Available indexed columns: email',
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should throw error with helpful message listing available columns", () => {
|
|
296
|
+
const indexedColumns = new Set(["id", "email"]);
|
|
297
|
+
const builder = createIndexedBuilder(usersTable.columns, indexedColumns);
|
|
298
|
+
|
|
299
|
+
expect(() => builder("age", ">", 18)).toThrow(
|
|
300
|
+
'Column "age" is not indexed. Only indexed columns can be used in Unit of Work queries. Available indexed columns: id, email',
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should support all builder methods with indexed columns", () => {
|
|
305
|
+
const indexedColumns = new Set(["id", "email", "name"]);
|
|
306
|
+
const builder = createIndexedBuilder(usersTable.columns, indexedColumns);
|
|
307
|
+
|
|
308
|
+
// Test various builder methods
|
|
309
|
+
expect(builder.isNull("email")).toEqual({
|
|
310
|
+
type: "compare",
|
|
311
|
+
a: usersTable.columns.email,
|
|
312
|
+
operator: "is",
|
|
313
|
+
b: null,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(builder.isNotNull("name")).toEqual({
|
|
317
|
+
type: "compare",
|
|
318
|
+
a: usersTable.columns.name,
|
|
319
|
+
operator: "is not",
|
|
320
|
+
b: null,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const orCondition = builder.or(
|
|
324
|
+
builder("email", "contains", "gmail"),
|
|
325
|
+
builder("email", "contains", "yahoo"),
|
|
326
|
+
);
|
|
327
|
+
expect(orCondition).toEqual({
|
|
328
|
+
type: "or",
|
|
329
|
+
items: [
|
|
330
|
+
{
|
|
331
|
+
type: "compare",
|
|
332
|
+
a: usersTable.columns.email,
|
|
333
|
+
operator: "contains",
|
|
334
|
+
b: "gmail",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
type: "compare",
|
|
338
|
+
a: usersTable.columns.email,
|
|
339
|
+
operator: "contains",
|
|
340
|
+
b: "yahoo",
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const notCondition = builder.not(builder("name", "=", "Bob"));
|
|
346
|
+
expect(notCondition).toEqual({
|
|
347
|
+
type: "not",
|
|
348
|
+
item: {
|
|
349
|
+
type: "compare",
|
|
350
|
+
a: usersTable.columns.name,
|
|
351
|
+
operator: "=",
|
|
352
|
+
b: "Bob",
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should support array operators on indexed columns", () => {
|
|
358
|
+
const indexedColumns = new Set(["name"]);
|
|
359
|
+
const builder = createIndexedBuilder(usersTable.columns, indexedColumns);
|
|
360
|
+
|
|
361
|
+
const condition = builder("name", "in", ["Alice", "Bob", "Charlie"]);
|
|
362
|
+
expect(condition).toEqual({
|
|
363
|
+
type: "compare",
|
|
364
|
+
a: usersTable.columns.name,
|
|
365
|
+
operator: "in",
|
|
366
|
+
b: ["Alice", "Bob", "Charlie"],
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("should allow empty indexed columns set to reject all comparisons", () => {
|
|
371
|
+
const indexedColumns = new Set<string>();
|
|
372
|
+
const builder = createIndexedBuilder(usersTable.columns, indexedColumns);
|
|
373
|
+
|
|
374
|
+
expect(() => builder("id", "=", "123")).toThrow(
|
|
375
|
+
'Column "id" is not indexed. Only indexed columns can be used in Unit of Work queries. Available indexed columns: ',
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { AnyColumn, FragnoId, IdColumn } from "../schema/create";
|
|
2
|
+
|
|
3
|
+
export type ConditionType = "compare" | "and" | "or" | "not";
|
|
4
|
+
|
|
5
|
+
export type Condition =
|
|
6
|
+
| {
|
|
7
|
+
type: "compare";
|
|
8
|
+
a: AnyColumn;
|
|
9
|
+
operator: Operator;
|
|
10
|
+
b: AnyColumn | unknown | null;
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
type: "or" | "and";
|
|
14
|
+
items: Condition[];
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
type: "not";
|
|
18
|
+
item: Condition;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// TODO: we temporarily dropped support for comparing against another column, because Prisma ORM still have problems with it.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Helper type that allows FragnoId for ID columns and reference columns (bigint).
|
|
25
|
+
* Used in ConditionBuilder to accept FragnoId values in where conditions.
|
|
26
|
+
*/
|
|
27
|
+
type AcceptsFragnoId<T extends AnyColumn> = T extends IdColumn
|
|
28
|
+
? T["$in"] | FragnoId
|
|
29
|
+
: T["$in"] extends bigint
|
|
30
|
+
? T["$in"] | FragnoId
|
|
31
|
+
: T["$in"];
|
|
32
|
+
|
|
33
|
+
export type ConditionBuilder<Columns extends Record<string, AnyColumn>> = {
|
|
34
|
+
<ColName extends keyof Columns>(
|
|
35
|
+
a: ColName,
|
|
36
|
+
operator: (typeof valueOperators)[number] | (typeof stringOperators)[number],
|
|
37
|
+
b: AcceptsFragnoId<Columns[ColName]> | null,
|
|
38
|
+
): Condition;
|
|
39
|
+
|
|
40
|
+
<ColName extends keyof Columns>(
|
|
41
|
+
a: ColName,
|
|
42
|
+
operator: (typeof arrayOperators)[number],
|
|
43
|
+
b: AcceptsFragnoId<Columns[ColName]>[],
|
|
44
|
+
): Condition;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Boolean values
|
|
48
|
+
*/
|
|
49
|
+
<ColName extends keyof Columns>(a: ColName): Condition;
|
|
50
|
+
|
|
51
|
+
and: (...v: (Condition | boolean)[]) => Condition | boolean;
|
|
52
|
+
or: (...v: (Condition | boolean)[]) => Condition | boolean;
|
|
53
|
+
not: (v: Condition | boolean) => Condition | boolean;
|
|
54
|
+
|
|
55
|
+
isNull: (a: keyof Columns) => Condition;
|
|
56
|
+
isNotNull: (a: keyof Columns) => Condition;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// replacement for `like` (Prisma doesn't support `like`)
|
|
60
|
+
const stringOperators = [
|
|
61
|
+
"contains",
|
|
62
|
+
"starts with",
|
|
63
|
+
"ends with",
|
|
64
|
+
|
|
65
|
+
"not contains",
|
|
66
|
+
"not starts with",
|
|
67
|
+
"not ends with",
|
|
68
|
+
// excluded `regexp` since MSSQL doesn't support it, may re-consider
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
const arrayOperators = ["in", "not in"] as const;
|
|
72
|
+
|
|
73
|
+
const valueOperators = ["=", "!=", ">", ">=", "<", "<=", "is", "is not"] as const;
|
|
74
|
+
|
|
75
|
+
// JSON specific operators are not included, some databases don't support them
|
|
76
|
+
// `match` requires additional extensions & configurations on SQLite and PostgreSQL
|
|
77
|
+
// MySQL & SQLite requires workarounds to support `ilike`
|
|
78
|
+
export const operators = [...valueOperators, ...arrayOperators, ...stringOperators] as const;
|
|
79
|
+
|
|
80
|
+
export type Operator = (typeof operators)[number];
|
|
81
|
+
|
|
82
|
+
export function createBuilder<Columns extends Record<string, AnyColumn>>(
|
|
83
|
+
columns: Columns,
|
|
84
|
+
): ConditionBuilder<Columns> {
|
|
85
|
+
function col(name: keyof Columns) {
|
|
86
|
+
const out = columns[name];
|
|
87
|
+
if (!out) {
|
|
88
|
+
throw new Error(`Invalid column name ${String(name)}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const builder: ConditionBuilder<Columns> = (...args: [string, Operator, unknown] | [string]) => {
|
|
95
|
+
if (args.length === 3) {
|
|
96
|
+
const [a, operator, b] = args;
|
|
97
|
+
|
|
98
|
+
if (!operators.includes(operator)) throw new Error(`Unsupported operator: ${operator}`);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
type: "compare",
|
|
102
|
+
a: col(a),
|
|
103
|
+
b,
|
|
104
|
+
operator,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
type: "compare",
|
|
110
|
+
a: col(args[0]),
|
|
111
|
+
operator: "=",
|
|
112
|
+
b: true,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
builder.isNull = (a) => builder(a, "is", null);
|
|
117
|
+
builder.isNotNull = (a) => builder(a, "is not", null);
|
|
118
|
+
builder.not = (condition) => {
|
|
119
|
+
if (typeof condition === "boolean") return !condition;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
type: "not",
|
|
123
|
+
item: condition,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
builder.or = (...conditions) => {
|
|
128
|
+
const out = {
|
|
129
|
+
type: "or",
|
|
130
|
+
items: [] as Condition[],
|
|
131
|
+
} as const;
|
|
132
|
+
|
|
133
|
+
for (const item of conditions) {
|
|
134
|
+
if (item === true) return true;
|
|
135
|
+
if (item === false) continue;
|
|
136
|
+
|
|
137
|
+
out.items.push(item);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (out.items.length === 0) return false;
|
|
141
|
+
return out;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
builder.and = (...conditions) => {
|
|
145
|
+
const out = {
|
|
146
|
+
type: "and",
|
|
147
|
+
items: [] as Condition[],
|
|
148
|
+
} as const;
|
|
149
|
+
|
|
150
|
+
for (const item of conditions) {
|
|
151
|
+
if (item === true) continue;
|
|
152
|
+
if (item === false) return false;
|
|
153
|
+
|
|
154
|
+
out.items.push(item);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (out.items.length === 0) return true;
|
|
158
|
+
return out;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return builder;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function buildCondition<T, Columns extends Record<string, AnyColumn>>(
|
|
165
|
+
columns: Columns,
|
|
166
|
+
input: (builder: ConditionBuilder<Columns>) => T,
|
|
167
|
+
): T {
|
|
168
|
+
return input(createBuilder(columns));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create a ConditionBuilder that only allows comparisons on indexed columns.
|
|
173
|
+
* Used in Unit of Work to ensure queries can leverage indexes for optimal performance.
|
|
174
|
+
*
|
|
175
|
+
* @param columns - The full set of columns from the table
|
|
176
|
+
* @param indexedColumnNames - Set of column names that are part of indexes
|
|
177
|
+
* @returns A ConditionBuilder restricted to indexed columns only
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* const builder = createIndexedBuilder(
|
|
182
|
+
* table.columns,
|
|
183
|
+
* new Set(["id", "userId", "createdAt"])
|
|
184
|
+
* );
|
|
185
|
+
* const condition = builder("userId", "=", "123");
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export function createIndexedBuilder<Columns extends Record<string, AnyColumn>>(
|
|
189
|
+
columns: Columns,
|
|
190
|
+
indexedColumnNames: Set<string>,
|
|
191
|
+
): ConditionBuilder<Columns> {
|
|
192
|
+
function col(name: keyof Columns) {
|
|
193
|
+
const columnName = String(name);
|
|
194
|
+
|
|
195
|
+
if (!indexedColumnNames.has(columnName)) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Column "${columnName}" is not indexed. Only indexed columns can be used in Unit of Work queries. ` +
|
|
198
|
+
`Available indexed columns: ${Array.from(indexedColumnNames).join(", ")}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const out = columns[name];
|
|
203
|
+
if (!out) {
|
|
204
|
+
throw new Error(`Invalid column name ${columnName}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const builder: ConditionBuilder<Columns> = (...args: [string, Operator, unknown] | [string]) => {
|
|
211
|
+
if (args.length === 3) {
|
|
212
|
+
const [a, operator, b] = args;
|
|
213
|
+
|
|
214
|
+
if (!operators.includes(operator)) {
|
|
215
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
type: "compare",
|
|
220
|
+
a: col(a),
|
|
221
|
+
b,
|
|
222
|
+
operator,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
type: "compare",
|
|
228
|
+
a: col(args[0]),
|
|
229
|
+
operator: "=",
|
|
230
|
+
b: true,
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
builder.isNull = (a) => builder(a, "is", null);
|
|
235
|
+
builder.isNotNull = (a) => builder(a, "is not", null);
|
|
236
|
+
builder.not = (condition) => {
|
|
237
|
+
if (typeof condition === "boolean") {
|
|
238
|
+
return !condition;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
type: "not",
|
|
243
|
+
item: condition,
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
builder.or = (...conditions) => {
|
|
248
|
+
const out = {
|
|
249
|
+
type: "or",
|
|
250
|
+
items: [] as Condition[],
|
|
251
|
+
} as const;
|
|
252
|
+
|
|
253
|
+
for (const item of conditions) {
|
|
254
|
+
if (item === true) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
if (item === false) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
out.items.push(item);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (out.items.length === 0) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
builder.and = (...conditions) => {
|
|
271
|
+
const out = {
|
|
272
|
+
type: "and",
|
|
273
|
+
items: [] as Condition[],
|
|
274
|
+
} as const;
|
|
275
|
+
|
|
276
|
+
for (const item of conditions) {
|
|
277
|
+
if (item === true) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (item === false) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
out.items.push(item);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (out.items.length === 0) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
return builder;
|
|
294
|
+
}
|