@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,789 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import { KyselyPGlite } from "kysely-pglite";
|
|
3
|
+
import { assert, beforeAll, describe, expect, expectTypeOf, it } from "vitest";
|
|
4
|
+
import { KyselyAdapter } from "./kysely-adapter";
|
|
5
|
+
import {
|
|
6
|
+
column,
|
|
7
|
+
idColumn,
|
|
8
|
+
referenceColumn,
|
|
9
|
+
schema,
|
|
10
|
+
type FragnoId,
|
|
11
|
+
type FragnoReference,
|
|
12
|
+
} from "../../schema/create";
|
|
13
|
+
import { encodeCursor } from "../../query/cursor";
|
|
14
|
+
|
|
15
|
+
describe("KyselyAdapter PGLite", () => {
|
|
16
|
+
const testSchema = schema((s) => {
|
|
17
|
+
return s
|
|
18
|
+
.addTable("users", (t) => {
|
|
19
|
+
return t
|
|
20
|
+
.addColumn("id", idColumn())
|
|
21
|
+
.addColumn("name", column("string"))
|
|
22
|
+
.addColumn("age", column("integer").nullable())
|
|
23
|
+
.createIndex("name_idx", ["name"])
|
|
24
|
+
.createIndex("age_idx", ["age"]);
|
|
25
|
+
})
|
|
26
|
+
.addTable("emails", (t) => {
|
|
27
|
+
return t
|
|
28
|
+
.addColumn("id", idColumn())
|
|
29
|
+
.addColumn("user_id", referenceColumn())
|
|
30
|
+
.addColumn("email", column("string"))
|
|
31
|
+
.addColumn("is_primary", column("bool").defaultTo(false))
|
|
32
|
+
.createIndex("unique_email", ["email"], { unique: true })
|
|
33
|
+
.createIndex("user_emails", ["user_id"]);
|
|
34
|
+
})
|
|
35
|
+
.addTable("posts", (t) => {
|
|
36
|
+
return t
|
|
37
|
+
.addColumn("id", idColumn())
|
|
38
|
+
.addColumn("user_id", referenceColumn())
|
|
39
|
+
.addColumn("title", column("string"))
|
|
40
|
+
.addColumn("content", column("string"))
|
|
41
|
+
.createIndex("posts_user_idx", ["user_id"]);
|
|
42
|
+
})
|
|
43
|
+
.addTable("tags", (t) => {
|
|
44
|
+
return t
|
|
45
|
+
.addColumn("id", idColumn())
|
|
46
|
+
.addColumn("name", column("string"))
|
|
47
|
+
.createIndex("tag_name", ["name"]);
|
|
48
|
+
})
|
|
49
|
+
.addTable("post_tags", (t) => {
|
|
50
|
+
return t
|
|
51
|
+
.addColumn("id", idColumn())
|
|
52
|
+
.addColumn("post_id", referenceColumn())
|
|
53
|
+
.addColumn("tag_id", referenceColumn())
|
|
54
|
+
.createIndex("pt_post", ["post_id"])
|
|
55
|
+
.createIndex("pt_tag", ["tag_id"]);
|
|
56
|
+
})
|
|
57
|
+
.addTable("comments", (t) => {
|
|
58
|
+
return t
|
|
59
|
+
.addColumn("id", idColumn())
|
|
60
|
+
.addColumn("post_id", referenceColumn())
|
|
61
|
+
.addColumn("user_id", referenceColumn())
|
|
62
|
+
.addColumn("text", column("string"))
|
|
63
|
+
.createIndex("comments_post_idx", ["post_id"])
|
|
64
|
+
.createIndex("comments_user_idx", ["user_id"]);
|
|
65
|
+
})
|
|
66
|
+
.addReference("user", {
|
|
67
|
+
type: "one",
|
|
68
|
+
from: { table: "emails", column: "user_id" },
|
|
69
|
+
to: { table: "users", column: "id" },
|
|
70
|
+
})
|
|
71
|
+
.addReference("author", {
|
|
72
|
+
type: "one",
|
|
73
|
+
from: { table: "posts", column: "user_id" },
|
|
74
|
+
to: { table: "users", column: "id" },
|
|
75
|
+
})
|
|
76
|
+
.addReference("post", {
|
|
77
|
+
type: "one",
|
|
78
|
+
from: { table: "post_tags", column: "post_id" },
|
|
79
|
+
to: { table: "posts", column: "id" },
|
|
80
|
+
})
|
|
81
|
+
.addReference("tag", {
|
|
82
|
+
type: "one",
|
|
83
|
+
from: { table: "post_tags", column: "tag_id" },
|
|
84
|
+
to: { table: "tags", column: "id" },
|
|
85
|
+
})
|
|
86
|
+
.addReference("post", {
|
|
87
|
+
type: "one",
|
|
88
|
+
from: { table: "comments", column: "post_id" },
|
|
89
|
+
to: { table: "posts", column: "id" },
|
|
90
|
+
})
|
|
91
|
+
.addReference("commenter", {
|
|
92
|
+
type: "one",
|
|
93
|
+
from: { table: "comments", column: "user_id" },
|
|
94
|
+
to: { table: "users", column: "id" },
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
let kysely: Kysely<any>;
|
|
100
|
+
let adapter: KyselyAdapter;
|
|
101
|
+
|
|
102
|
+
beforeAll(async () => {
|
|
103
|
+
const { dialect } = await KyselyPGlite.create();
|
|
104
|
+
kysely = new Kysely({
|
|
105
|
+
dialect,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
adapter = new KyselyAdapter({
|
|
109
|
+
db: kysely,
|
|
110
|
+
provider: "postgresql",
|
|
111
|
+
});
|
|
112
|
+
}, 12000);
|
|
113
|
+
|
|
114
|
+
it("should run migrations and basic queries", async () => {
|
|
115
|
+
const schemaVersion = await adapter.getSchemaVersion("test");
|
|
116
|
+
expect(schemaVersion).toBeUndefined();
|
|
117
|
+
|
|
118
|
+
const migrator = adapter.createMigrationEngine(testSchema, "test");
|
|
119
|
+
const preparedMigration = await migrator.prepareMigration();
|
|
120
|
+
assert(preparedMigration.getSQL);
|
|
121
|
+
|
|
122
|
+
expect(preparedMigration.getSQL()).toMatchInlineSnapshot(`
|
|
123
|
+
"create table "fragno_db_settings" ("key" varchar(255) primary key, "value" text not null);
|
|
124
|
+
|
|
125
|
+
insert into "fragno_db_settings" ("key", "value") values ('test.schema_version', '12');
|
|
126
|
+
|
|
127
|
+
create table "users" ("id" varchar(30) not null unique, "name" text not null, "age" integer, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);
|
|
128
|
+
|
|
129
|
+
create index "name_idx" on "users" ("name");
|
|
130
|
+
|
|
131
|
+
create index "age_idx" on "users" ("age");
|
|
132
|
+
|
|
133
|
+
create table "emails" ("id" varchar(30) not null unique, "user_id" bigint not null, "email" text not null, "is_primary" boolean default false not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);
|
|
134
|
+
|
|
135
|
+
create unique index "unique_email" on "emails" ("email");
|
|
136
|
+
|
|
137
|
+
create index "user_emails" on "emails" ("user_id");
|
|
138
|
+
|
|
139
|
+
create table "posts" ("id" varchar(30) not null unique, "user_id" bigint not null, "title" text not null, "content" text not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);
|
|
140
|
+
|
|
141
|
+
create index "posts_user_idx" on "posts" ("user_id");
|
|
142
|
+
|
|
143
|
+
create table "tags" ("id" varchar(30) not null unique, "name" text not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);
|
|
144
|
+
|
|
145
|
+
create index "tag_name" on "tags" ("name");
|
|
146
|
+
|
|
147
|
+
create table "post_tags" ("id" varchar(30) not null unique, "post_id" bigint not null, "tag_id" bigint not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);
|
|
148
|
+
|
|
149
|
+
create index "pt_post" on "post_tags" ("post_id");
|
|
150
|
+
|
|
151
|
+
create index "pt_tag" on "post_tags" ("tag_id");
|
|
152
|
+
|
|
153
|
+
create table "comments" ("id" varchar(30) not null unique, "post_id" bigint not null, "user_id" bigint not null, "text" text not null, "_internalId" bigserial not null primary key, "_version" integer default 0 not null);
|
|
154
|
+
|
|
155
|
+
create index "comments_post_idx" on "comments" ("post_id");
|
|
156
|
+
|
|
157
|
+
create index "comments_user_idx" on "comments" ("user_id");
|
|
158
|
+
|
|
159
|
+
alter table "emails" add constraint "emails_users_user_fk" foreign key ("user_id") references "users" ("_internalId") on delete restrict on update restrict;
|
|
160
|
+
|
|
161
|
+
alter table "posts" add constraint "posts_users_author_fk" foreign key ("user_id") references "users" ("_internalId") on delete restrict on update restrict;
|
|
162
|
+
|
|
163
|
+
alter table "post_tags" add constraint "post_tags_posts_post_fk" foreign key ("post_id") references "posts" ("_internalId") on delete restrict on update restrict;
|
|
164
|
+
|
|
165
|
+
alter table "post_tags" add constraint "post_tags_tags_tag_fk" foreign key ("tag_id") references "tags" ("_internalId") on delete restrict on update restrict;
|
|
166
|
+
|
|
167
|
+
alter table "comments" add constraint "comments_posts_post_fk" foreign key ("post_id") references "posts" ("_internalId") on delete restrict on update restrict;
|
|
168
|
+
|
|
169
|
+
alter table "comments" add constraint "comments_users_commenter_fk" foreign key ("user_id") references "users" ("_internalId") on delete restrict on update restrict;"
|
|
170
|
+
`);
|
|
171
|
+
|
|
172
|
+
await preparedMigration.execute();
|
|
173
|
+
|
|
174
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
175
|
+
|
|
176
|
+
// Create a user
|
|
177
|
+
const userId = await queryEngine.create("users", {
|
|
178
|
+
name: "John Doe",
|
|
179
|
+
age: 30,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// create() now returns just the ID
|
|
183
|
+
expect(userId).toMatchObject({
|
|
184
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
185
|
+
internalId: expect.any(Number),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(userId.version).toBe(0);
|
|
189
|
+
|
|
190
|
+
const getUser = await queryEngine.findFirst("users", (b) =>
|
|
191
|
+
b.whereIndex("primary", (eb) => eb("id", "=", userId)).select(["id", "name"]),
|
|
192
|
+
);
|
|
193
|
+
expect(getUser).toMatchObject({
|
|
194
|
+
id: expect.objectContaining({
|
|
195
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
196
|
+
internalId: expect.any(Number),
|
|
197
|
+
}),
|
|
198
|
+
name: "John Doe",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Create 2 emails for the user
|
|
202
|
+
const email1Id = await queryEngine.create("emails", {
|
|
203
|
+
user_id: userId,
|
|
204
|
+
email: "john.doe@example.com",
|
|
205
|
+
is_primary: true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const email2Id = await queryEngine.create("emails", {
|
|
209
|
+
// Pass only the string (external ID) here, to make sure we generate the right sub-query.
|
|
210
|
+
user_id: userId.toString(),
|
|
211
|
+
email: "john.doe.work@company.com",
|
|
212
|
+
is_primary: false,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(email1Id).toMatchObject({
|
|
216
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
217
|
+
internalId: expect.any(Number),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(email2Id).toMatchObject({
|
|
221
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
222
|
+
internalId: expect.any(Number),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Update user name
|
|
226
|
+
await queryEngine.updateMany("users", (b) =>
|
|
227
|
+
b
|
|
228
|
+
.whereIndex("primary", (eb) => eb("id", "=", userId))
|
|
229
|
+
.set({
|
|
230
|
+
name: "Jane Doe",
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const updatedUser = await queryEngine.findFirst("users", (b) =>
|
|
235
|
+
b.whereIndex("primary", (eb) => eb("id", "=", userId)),
|
|
236
|
+
);
|
|
237
|
+
// Version has been incremented
|
|
238
|
+
expect(updatedUser!.id.version).toBe(1);
|
|
239
|
+
|
|
240
|
+
// Query emails with their users using join (since the relation is from emails to users)
|
|
241
|
+
const emailsWithUsers = await queryEngine.find("emails", (b) =>
|
|
242
|
+
b.whereIndex("primary").join((jb) => jb.user()),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(emailsWithUsers).toHaveLength(2); // One row per email
|
|
246
|
+
expect(emailsWithUsers[0]).toEqual({
|
|
247
|
+
id: expect.objectContaining({
|
|
248
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
249
|
+
internalId: expect.any(Number),
|
|
250
|
+
}),
|
|
251
|
+
user_id: expect.objectContaining({
|
|
252
|
+
internalId: expect.any(Number),
|
|
253
|
+
}),
|
|
254
|
+
email: expect.stringMatching(/\.com$/),
|
|
255
|
+
is_primary: expect.any(Boolean),
|
|
256
|
+
user: {
|
|
257
|
+
id: expect.objectContaining({
|
|
258
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
259
|
+
internalId: expect.any(Number),
|
|
260
|
+
}),
|
|
261
|
+
name: "Jane Doe",
|
|
262
|
+
age: 30,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Also test a more specific join query to get emails for a specific user
|
|
267
|
+
const userEmails = await queryEngine.find("emails", (b) =>
|
|
268
|
+
b.whereIndex("user_emails", (eb) => eb("user_id", "=", userId)).join((jb) => jb.user()),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(userEmails).toHaveLength(2);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should execute Unit of Work with version checking", async () => {
|
|
275
|
+
// Use the same namespace as the first test (migrations already ran)
|
|
276
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
277
|
+
|
|
278
|
+
// Create initial user
|
|
279
|
+
const initialUserId = await queryEngine.create("users", {
|
|
280
|
+
name: "Alice",
|
|
281
|
+
age: 25,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(initialUserId.version).toBe(0);
|
|
285
|
+
|
|
286
|
+
// Build a UOW to update the user with optimistic locking
|
|
287
|
+
const uow = queryEngine
|
|
288
|
+
.createUnitOfWork("update-user-age")
|
|
289
|
+
// Retrieval phase: find the user
|
|
290
|
+
.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", initialUserId)));
|
|
291
|
+
|
|
292
|
+
// Execute retrieval and transition to mutation phase
|
|
293
|
+
const [users] = await uow.executeRetrieve();
|
|
294
|
+
|
|
295
|
+
// Mutation phase: update with version check
|
|
296
|
+
uow.update("users", initialUserId, (b) => b.set({ age: 26 }).check());
|
|
297
|
+
|
|
298
|
+
// Execute mutations
|
|
299
|
+
const { success } = await uow.executeMutations();
|
|
300
|
+
|
|
301
|
+
// Should succeed
|
|
302
|
+
expect(success).toBe(true);
|
|
303
|
+
expect(users).toHaveLength(1);
|
|
304
|
+
|
|
305
|
+
// Verify the user was updated
|
|
306
|
+
const updatedUser = await queryEngine.findFirst("users", (b) =>
|
|
307
|
+
b.whereIndex("primary", (eb) => eb("id", "=", initialUserId)),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(updatedUser).toMatchObject({
|
|
311
|
+
id: expect.objectContaining({
|
|
312
|
+
externalId: initialUserId.externalId,
|
|
313
|
+
version: 1, // Version incremented
|
|
314
|
+
}),
|
|
315
|
+
name: "Alice",
|
|
316
|
+
age: 26,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Try to update again with stale version (should fail)
|
|
320
|
+
const uow2 = queryEngine.createUnitOfWork("update-user-stale");
|
|
321
|
+
|
|
322
|
+
// Use the old version (0) which is now stale
|
|
323
|
+
uow2.update("users", initialUserId, (b) => b.set({ age: 27 }).check());
|
|
324
|
+
|
|
325
|
+
const { success: success2 } = await uow2.executeMutations();
|
|
326
|
+
|
|
327
|
+
// Should fail due to version conflict
|
|
328
|
+
expect(success2).toBe(false);
|
|
329
|
+
|
|
330
|
+
// Verify the user was NOT updated
|
|
331
|
+
const [[unchangedUser]] = await queryEngine
|
|
332
|
+
.createUnitOfWork("verify-unchanged")
|
|
333
|
+
.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", initialUserId)))
|
|
334
|
+
.executeRetrieve();
|
|
335
|
+
|
|
336
|
+
expect(unchangedUser).toMatchObject({
|
|
337
|
+
id: expect.objectContaining({
|
|
338
|
+
version: 1, // Still version 1
|
|
339
|
+
}),
|
|
340
|
+
age: 26, // Still 26, not 27
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const uow3 = queryEngine
|
|
344
|
+
.createUnitOfWork("get-all-emails")
|
|
345
|
+
.find("emails", (b) => b.whereIndex("primary").orderByIndex("unique_email", "desc"));
|
|
346
|
+
const [allEmails] = await uow3.executeRetrieve();
|
|
347
|
+
const userNames = allEmails.map((email) => email.email);
|
|
348
|
+
expect(userNames).toEqual(["john.doe@example.com", "john.doe.work@company.com"]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("should support selectCount in UOW", async () => {
|
|
352
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
353
|
+
|
|
354
|
+
// Count all users
|
|
355
|
+
const uow = queryEngine
|
|
356
|
+
.createUnitOfWork("count-users")
|
|
357
|
+
.find("users", (b) => b.whereIndex("primary").selectCount());
|
|
358
|
+
|
|
359
|
+
const [count] = await uow.executeRetrieve();
|
|
360
|
+
|
|
361
|
+
// We created 2 users in previous tests (John Doe and Alice)
|
|
362
|
+
expect(count).toBeGreaterThanOrEqual(2);
|
|
363
|
+
expect(typeof count).toBe("number");
|
|
364
|
+
|
|
365
|
+
// Count with where clause
|
|
366
|
+
const uow2 = queryEngine
|
|
367
|
+
.createUnitOfWork("count-young-users")
|
|
368
|
+
.find("users", (b) => b.whereIndex("age_idx", (eb) => eb("age", "<", 28)).selectCount());
|
|
369
|
+
|
|
370
|
+
const [youngCount] = await uow2.executeRetrieve();
|
|
371
|
+
expect(youngCount).toBeGreaterThanOrEqual(1); // At least Alice (25)
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("should support cursor-based pagination in UOW", async () => {
|
|
375
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
376
|
+
|
|
377
|
+
// Create some test users for pagination
|
|
378
|
+
const users = [
|
|
379
|
+
{ name: "User A", age: 20 },
|
|
380
|
+
{ name: "User B", age: 21 },
|
|
381
|
+
{ name: "User C", age: 22 },
|
|
382
|
+
{ name: "User D", age: 23 },
|
|
383
|
+
{ name: "User E", age: 24 },
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
for (const user of users) {
|
|
387
|
+
await queryEngine.create("users", user);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Test forward pagination with after cursor, ordered by name
|
|
391
|
+
const page1 = queryEngine
|
|
392
|
+
.createUnitOfWork("page-1")
|
|
393
|
+
.find("users", (b) => b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(2));
|
|
394
|
+
|
|
395
|
+
const [page1Results] = await page1.executeRetrieve();
|
|
396
|
+
// Note: Previous tests created "Alice" and "Jane Doe"
|
|
397
|
+
expect(page1Results.map((u) => u.name)).toEqual(["Alice", "Jane Doe"]);
|
|
398
|
+
|
|
399
|
+
// Get cursor for pagination (using the last item from page 1)
|
|
400
|
+
const lastItem = page1Results[page1Results.length - 1]!;
|
|
401
|
+
const cursor = encodeCursor({
|
|
402
|
+
indexValues: { name: lastItem.name },
|
|
403
|
+
direction: "forward",
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Get page 2 using the cursor
|
|
407
|
+
const page2 = queryEngine
|
|
408
|
+
.createUnitOfWork("page-2")
|
|
409
|
+
.find("users", (b) =>
|
|
410
|
+
b.whereIndex("name_idx").orderByIndex("name_idx", "asc").after(cursor).pageSize(2),
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const [page2Results] = await page2.executeRetrieve();
|
|
414
|
+
expect(page2Results.map((u) => u.name)).toEqual(["User A", "User B"]);
|
|
415
|
+
|
|
416
|
+
// Ensure no overlap between pages
|
|
417
|
+
const page1Names = new Set(page1Results.map((u) => u.name));
|
|
418
|
+
for (const user of page2Results) {
|
|
419
|
+
expect(page1Names.has(user.name)).toBe(false);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("should support many-to-many queries through junction table", async () => {
|
|
424
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
425
|
+
|
|
426
|
+
// Create a user
|
|
427
|
+
const userId = await queryEngine.create("users", {
|
|
428
|
+
name: "Blog Author",
|
|
429
|
+
age: 28,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Create posts
|
|
433
|
+
const post1Id = await queryEngine.create("posts", {
|
|
434
|
+
title: "TypeScript Tips",
|
|
435
|
+
content: "Learn TypeScript",
|
|
436
|
+
user_id: userId,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const post2Id = await queryEngine.create("posts", {
|
|
440
|
+
title: "Database Design",
|
|
441
|
+
content: "Learn databases",
|
|
442
|
+
user_id: userId,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// Create tags
|
|
446
|
+
const tagTypeScriptId = await queryEngine.create("tags", {
|
|
447
|
+
name: "TypeScript",
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const tagDatabaseId = await queryEngine.create("tags", {
|
|
451
|
+
name: "Database",
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const tagTutorialId = await queryEngine.create("tags", {
|
|
455
|
+
name: "Tutorial",
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Link posts to tags via junction table
|
|
459
|
+
// Post 1 has tags: TypeScript, Tutorial
|
|
460
|
+
await queryEngine.create("post_tags", {
|
|
461
|
+
post_id: post1Id,
|
|
462
|
+
tag_id: tagTypeScriptId,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
await queryEngine.create("post_tags", {
|
|
466
|
+
post_id: post1Id,
|
|
467
|
+
tag_id: tagTutorialId,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Post 2 has tags: Database, Tutorial
|
|
471
|
+
await queryEngine.create("post_tags", {
|
|
472
|
+
post_id: post2Id,
|
|
473
|
+
tag_id: tagDatabaseId,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
await queryEngine.create("post_tags", {
|
|
477
|
+
post_id: post2Id,
|
|
478
|
+
tag_id: tagTutorialId,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Query post_tags with joined post and tag data using UOW
|
|
482
|
+
const uow = queryEngine
|
|
483
|
+
.createUnitOfWork("get-post-tags")
|
|
484
|
+
.find("post_tags", (b) =>
|
|
485
|
+
b
|
|
486
|
+
.whereIndex("primary")
|
|
487
|
+
.join((jb) => jb.post((pb) => pb.select(["title"])).tag((tb) => tb.select(["name"]))),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const [postTags] = await uow.executeRetrieve();
|
|
491
|
+
|
|
492
|
+
// Should have 4 post_tag entries
|
|
493
|
+
expect(postTags).toHaveLength(4);
|
|
494
|
+
|
|
495
|
+
// Verify the structure includes both post and tag data
|
|
496
|
+
expect(postTags[0]).toMatchObject({
|
|
497
|
+
id: expect.objectContaining({
|
|
498
|
+
externalId: expect.any(String),
|
|
499
|
+
}),
|
|
500
|
+
post_id: expect.objectContaining({
|
|
501
|
+
internalId: expect.any(Number),
|
|
502
|
+
}),
|
|
503
|
+
tag_id: expect.objectContaining({
|
|
504
|
+
internalId: expect.any(Number),
|
|
505
|
+
}),
|
|
506
|
+
post: {
|
|
507
|
+
title: expect.any(String),
|
|
508
|
+
},
|
|
509
|
+
tag: {
|
|
510
|
+
name: expect.any(String),
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
type InferArrayElement<T> = T extends (infer U)[] ? U : never;
|
|
515
|
+
type Prettify<T> = {
|
|
516
|
+
[K in keyof T]: T[K];
|
|
517
|
+
} & {};
|
|
518
|
+
type RemoveIndex<T> = {
|
|
519
|
+
[K in keyof T as string extends K
|
|
520
|
+
? never
|
|
521
|
+
: number extends K
|
|
522
|
+
? never
|
|
523
|
+
: symbol extends K
|
|
524
|
+
? never
|
|
525
|
+
: K]: T[K];
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
type PostTag = Prettify<InferArrayElement<typeof postTags>>;
|
|
529
|
+
type Tag = Prettify<RemoveIndex<PostTag["tag"]>>;
|
|
530
|
+
expectTypeOf<Tag>().toEqualTypeOf<{
|
|
531
|
+
id: FragnoId;
|
|
532
|
+
name: string;
|
|
533
|
+
} | null>();
|
|
534
|
+
|
|
535
|
+
// Verify we can find specific combinations
|
|
536
|
+
const typeScriptPosts = postTags.filter((pt) => pt.tag?.name === "TypeScript");
|
|
537
|
+
expect(typeScriptPosts).toHaveLength(1);
|
|
538
|
+
type Post = Prettify<(typeof typeScriptPosts)[number]["post"]>;
|
|
539
|
+
expectTypeOf<Post>().toEqualTypeOf<{
|
|
540
|
+
id: FragnoId;
|
|
541
|
+
user_id: FragnoReference;
|
|
542
|
+
title: string;
|
|
543
|
+
content: string;
|
|
544
|
+
} | null>();
|
|
545
|
+
expect(typeScriptPosts[0]!.post!.title).toBe("TypeScript Tips");
|
|
546
|
+
|
|
547
|
+
const tutorialPosts = postTags.filter((pt) => pt.tag!.name === "Tutorial");
|
|
548
|
+
expect(tutorialPosts).toHaveLength(2);
|
|
549
|
+
expect(tutorialPosts.map((pt) => pt.post!.title).sort()).toEqual([
|
|
550
|
+
"Database Design",
|
|
551
|
+
"TypeScript Tips",
|
|
552
|
+
]);
|
|
553
|
+
|
|
554
|
+
// Test nested many-to-many join: post_tags -> post -> author
|
|
555
|
+
const uow2 = queryEngine
|
|
556
|
+
.createUnitOfWork("get-post-tags-with-authors")
|
|
557
|
+
.find("post_tags", (b) =>
|
|
558
|
+
b
|
|
559
|
+
.whereIndex("pt_post", (eb) => eb("post_id", "=", post1Id))
|
|
560
|
+
.join((jb) =>
|
|
561
|
+
jb.post((pb) =>
|
|
562
|
+
pb.select(["title"]).join((jb2) => jb2["author"]((ab) => ab.select(["name"]))),
|
|
563
|
+
),
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const [postTagsWithAuthors] = await uow2.executeRetrieve();
|
|
568
|
+
|
|
569
|
+
// Should have 2 entries (TypeScript and Tutorial tags for post1)
|
|
570
|
+
expect(postTagsWithAuthors).toHaveLength(2);
|
|
571
|
+
|
|
572
|
+
// Verify nested structure
|
|
573
|
+
expect(postTagsWithAuthors[0]).toMatchObject({
|
|
574
|
+
post: {
|
|
575
|
+
title: "TypeScript Tips",
|
|
576
|
+
author: {
|
|
577
|
+
name: "Blog Author",
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Both should have the same author
|
|
583
|
+
for (const pt of postTagsWithAuthors) {
|
|
584
|
+
expect(pt.post!.author!.name).toBe("Blog Author");
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("should support complex nested joins (comments -> post -> author)", async () => {
|
|
589
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
590
|
+
|
|
591
|
+
// Create a user (author)
|
|
592
|
+
const authorId = await queryEngine.create("users", {
|
|
593
|
+
name: "Blog Author",
|
|
594
|
+
age: 30,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Create a post by the author
|
|
598
|
+
const postId = await queryEngine.create("posts", {
|
|
599
|
+
user_id: authorId,
|
|
600
|
+
title: "My First Post",
|
|
601
|
+
content: "This is the content of my first post",
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Create a commenter
|
|
605
|
+
const commenterId = await queryEngine.create("users", {
|
|
606
|
+
name: "Commenter User",
|
|
607
|
+
age: 25,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Create a comment on the post
|
|
611
|
+
await queryEngine.create("comments", {
|
|
612
|
+
post_id: postId,
|
|
613
|
+
user_id: commenterId,
|
|
614
|
+
text: "Great post!",
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Now perform a complex nested join: comments -> post -> author, and comments -> commenter
|
|
618
|
+
const uow = queryEngine.createUnitOfWork("test-complex-joins").find("comments", (b) =>
|
|
619
|
+
b.whereIndex("primary").join((jb) =>
|
|
620
|
+
jb
|
|
621
|
+
.post((postBuilder) =>
|
|
622
|
+
postBuilder
|
|
623
|
+
.select(["id", "title", "content"])
|
|
624
|
+
.orderByIndex("primary", "desc")
|
|
625
|
+
.pageSize(1)
|
|
626
|
+
.join((jb2) =>
|
|
627
|
+
// Nested join to the post's author
|
|
628
|
+
jb2.author((authorBuilder) =>
|
|
629
|
+
authorBuilder.select(["id", "name", "age"]).orderByIndex("name_idx", "asc"),
|
|
630
|
+
),
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
.commenter((commenterBuilder) => commenterBuilder.select(["id", "name"])),
|
|
634
|
+
),
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
const [[comment]] = await uow.executeRetrieve();
|
|
638
|
+
|
|
639
|
+
// Verify the result structure with nested joins
|
|
640
|
+
expect(comment).toMatchObject({
|
|
641
|
+
id: expect.objectContaining({
|
|
642
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
643
|
+
internalId: expect.any(Number),
|
|
644
|
+
}),
|
|
645
|
+
text: "Great post!",
|
|
646
|
+
// Post join (first level)
|
|
647
|
+
post: {
|
|
648
|
+
id: expect.objectContaining({
|
|
649
|
+
externalId: postId.externalId,
|
|
650
|
+
}),
|
|
651
|
+
title: "My First Post",
|
|
652
|
+
content: "This is the content of my first post",
|
|
653
|
+
// Nested author join (second level)
|
|
654
|
+
author: {
|
|
655
|
+
id: expect.objectContaining({
|
|
656
|
+
externalId: authorId.externalId,
|
|
657
|
+
}),
|
|
658
|
+
name: "Blog Author",
|
|
659
|
+
age: 30,
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
// Commenter join (first level)
|
|
663
|
+
commenter: {
|
|
664
|
+
id: expect.objectContaining({
|
|
665
|
+
externalId: commenterId.externalId,
|
|
666
|
+
}),
|
|
667
|
+
name: "Commenter User",
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it("should return created IDs from UOW create operations", async () => {
|
|
673
|
+
const queryEngine = adapter.createQueryEngine(testSchema, "test");
|
|
674
|
+
|
|
675
|
+
// Test 1: Create operations return IDs with both external and internal IDs
|
|
676
|
+
const uow1 = queryEngine.createUnitOfWork("create-multiple-users");
|
|
677
|
+
|
|
678
|
+
uow1.create("users", { name: "Test User 1", age: 30 });
|
|
679
|
+
uow1.create("users", { name: "Test User 2", age: 35 });
|
|
680
|
+
uow1.create("users", { name: "Test User 3", age: 40 });
|
|
681
|
+
|
|
682
|
+
const { success: success1 } = await uow1.executeMutations();
|
|
683
|
+
expect(success1).toBe(true);
|
|
684
|
+
|
|
685
|
+
const createdIds1 = uow1.getCreatedIds();
|
|
686
|
+
|
|
687
|
+
expect(createdIds1).toMatchObject([
|
|
688
|
+
expect.objectContaining({
|
|
689
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
690
|
+
internalId: expect.any(Number),
|
|
691
|
+
}),
|
|
692
|
+
expect.objectContaining({
|
|
693
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
694
|
+
internalId: expect.any(Number),
|
|
695
|
+
}),
|
|
696
|
+
expect.objectContaining({
|
|
697
|
+
externalId: expect.stringMatching(/^[a-z0-9]{20,}$/),
|
|
698
|
+
internalId: expect.any(Number),
|
|
699
|
+
}),
|
|
700
|
+
]);
|
|
701
|
+
|
|
702
|
+
// All external IDs should be unique
|
|
703
|
+
const externalIds = createdIds1.map((id) => id.externalId);
|
|
704
|
+
expect(new Set(externalIds).size).toBe(3);
|
|
705
|
+
|
|
706
|
+
// Verify we can use these IDs to query the created users
|
|
707
|
+
const user1 = await queryEngine.findFirst("users", (b) =>
|
|
708
|
+
b.whereIndex("primary", (eb) => eb("id", "=", createdIds1[0].externalId)),
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
const user2 = await queryEngine.findFirst("users", (b) =>
|
|
712
|
+
b.whereIndex("primary", (eb) => eb("id", "=", createdIds1[1].externalId)),
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
const user3 = await queryEngine.findFirst("users", (b) =>
|
|
716
|
+
b.whereIndex("primary", (eb) => eb("id", "=", createdIds1[2].externalId)),
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
expect(user1).toMatchObject({
|
|
720
|
+
id: expect.objectContaining({
|
|
721
|
+
externalId: createdIds1[0].externalId,
|
|
722
|
+
}),
|
|
723
|
+
name: "Test User 1",
|
|
724
|
+
age: 30,
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
expect(user2).toMatchObject({
|
|
728
|
+
id: expect.objectContaining({
|
|
729
|
+
externalId: createdIds1[1].externalId,
|
|
730
|
+
}),
|
|
731
|
+
name: "Test User 2",
|
|
732
|
+
age: 35,
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
expect(user3).toMatchObject({
|
|
736
|
+
id: expect.objectContaining({
|
|
737
|
+
externalId: createdIds1[2].externalId,
|
|
738
|
+
}),
|
|
739
|
+
name: "Test User 3",
|
|
740
|
+
age: 40,
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// Test 2: Mixed operations (creates, updates, deletes) - only creates return IDs
|
|
744
|
+
const uow2 = queryEngine.createUnitOfWork("mixed-operations");
|
|
745
|
+
|
|
746
|
+
uow2.create("users", { name: "New User", age: 50 });
|
|
747
|
+
uow2.update("users", createdIds1[0], (b) => b.set({ age: 31 }));
|
|
748
|
+
uow2.create("users", { name: "Another New User", age: 55 });
|
|
749
|
+
uow2.delete("users", createdIds1[2]);
|
|
750
|
+
|
|
751
|
+
const { success: success2 } = await uow2.executeMutations();
|
|
752
|
+
expect(success2).toBe(true);
|
|
753
|
+
|
|
754
|
+
const createdIds2 = uow2.getCreatedIds();
|
|
755
|
+
|
|
756
|
+
// Only 2 creates, so only 2 IDs
|
|
757
|
+
expect(createdIds2).toHaveLength(2);
|
|
758
|
+
expect(createdIds2[0].externalId).toBeDefined();
|
|
759
|
+
expect(createdIds2[1].externalId).toBeDefined();
|
|
760
|
+
|
|
761
|
+
// Test 3: User-provided IDs are preserved
|
|
762
|
+
const customId = "my-custom-user-id-12345";
|
|
763
|
+
const uow3 = queryEngine.createUnitOfWork("create-with-custom-id");
|
|
764
|
+
|
|
765
|
+
uow3.create("users", { id: customId, name: "Custom ID User", age: 60 });
|
|
766
|
+
|
|
767
|
+
const { success: success3 } = await uow3.executeMutations();
|
|
768
|
+
expect(success3).toBe(true);
|
|
769
|
+
|
|
770
|
+
const createdIds3 = uow3.getCreatedIds();
|
|
771
|
+
|
|
772
|
+
expect(createdIds3).toHaveLength(1);
|
|
773
|
+
expect(createdIds3[0].externalId).toBe(customId);
|
|
774
|
+
expect(createdIds3[0].internalId).toBeDefined();
|
|
775
|
+
|
|
776
|
+
// Verify the user was created with the custom ID
|
|
777
|
+
const customIdUser = await queryEngine.findFirst("users", (b) =>
|
|
778
|
+
b.whereIndex("primary", (eb) => eb("id", "=", customId)),
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
expect(customIdUser).toMatchObject({
|
|
782
|
+
id: expect.objectContaining({
|
|
783
|
+
externalId: customId,
|
|
784
|
+
}),
|
|
785
|
+
name: "Custom ID User",
|
|
786
|
+
age: 60,
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
});
|