@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.
Files changed (200) hide show
  1. package/.turbo/turbo-build.log +137 -13
  2. package/.turbo/turbo-test.log +36 -0
  3. package/CHANGELOG.md +7 -0
  4. package/dist/adapters/adapters.d.ts +18 -0
  5. package/dist/adapters/adapters.d.ts.map +1 -0
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +21 -0
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -0
  8. package/dist/adapters/drizzle/drizzle-adapter.js +62 -0
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -0
  10. package/dist/adapters/drizzle/drizzle-query.d.ts +17 -0
  11. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -0
  12. package/dist/adapters/drizzle/drizzle-query.js +139 -0
  13. package/dist/adapters/drizzle/drizzle-query.js.map +1 -0
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +9 -0
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -0
  16. package/dist/adapters/drizzle/drizzle-uow-compiler.js +300 -0
  17. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -0
  18. package/dist/adapters/drizzle/drizzle-uow-decoder.js +82 -0
  19. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -0
  20. package/dist/adapters/drizzle/drizzle-uow-executor.js +125 -0
  21. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -0
  22. package/dist/adapters/drizzle/generate.js +273 -0
  23. package/dist/adapters/drizzle/generate.js.map +1 -0
  24. package/dist/adapters/drizzle/join-column-utils.js +28 -0
  25. package/dist/adapters/drizzle/join-column-utils.js.map +1 -0
  26. package/dist/adapters/drizzle/shared.js +11 -0
  27. package/dist/adapters/drizzle/shared.js.map +1 -0
  28. package/dist/adapters/kysely/kysely-adapter.d.ts +23 -0
  29. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -0
  30. package/dist/adapters/kysely/kysely-adapter.js +119 -0
  31. package/dist/adapters/kysely/kysely-adapter.js.map +1 -0
  32. package/dist/adapters/kysely/kysely-query-builder.js +306 -0
  33. package/dist/adapters/kysely/kysely-query-builder.js.map +1 -0
  34. package/dist/adapters/kysely/kysely-query-compiler.js +67 -0
  35. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -0
  36. package/dist/adapters/kysely/kysely-query.js +158 -0
  37. package/dist/adapters/kysely/kysely-query.js.map +1 -0
  38. package/dist/adapters/kysely/kysely-uow-compiler.js +139 -0
  39. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -0
  40. package/dist/adapters/kysely/kysely-uow-executor.js +89 -0
  41. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -0
  42. package/dist/adapters/kysely/migration/execute.js +176 -0
  43. package/dist/adapters/kysely/migration/execute.js.map +1 -0
  44. package/dist/fragment.d.ts +54 -0
  45. package/dist/fragment.d.ts.map +1 -0
  46. package/dist/fragment.js +92 -0
  47. package/dist/fragment.js.map +1 -0
  48. package/dist/id.d.ts +2 -0
  49. package/dist/migration-engine/auto-from-schema.js +116 -0
  50. package/dist/migration-engine/auto-from-schema.js.map +1 -0
  51. package/dist/migration-engine/create.d.ts +41 -0
  52. package/dist/migration-engine/create.d.ts.map +1 -0
  53. package/dist/migration-engine/create.js +58 -0
  54. package/dist/migration-engine/create.js.map +1 -0
  55. package/dist/migration-engine/shared.d.ts +90 -0
  56. package/dist/migration-engine/shared.d.ts.map +1 -0
  57. package/dist/migration-engine/shared.js +8 -0
  58. package/dist/migration-engine/shared.js.map +1 -0
  59. package/dist/mod.d.ts +55 -2
  60. package/dist/mod.d.ts.map +1 -1
  61. package/dist/mod.js +111 -2
  62. package/dist/mod.js.map +1 -1
  63. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js +108 -0
  64. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js.map +1 -0
  65. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js +55 -0
  66. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js.map +1 -0
  67. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js +18 -0
  68. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js.map +1 -0
  69. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js +183 -0
  70. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js.map +1 -0
  71. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js +58 -0
  72. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js.map +1 -0
  73. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js +68 -0
  74. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js.map +1 -0
  75. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js +56 -0
  76. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js.map +1 -0
  77. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js +65 -0
  78. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js.map +1 -0
  79. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js +81 -0
  80. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js.map +1 -0
  81. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js +13 -0
  82. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js.map +1 -0
  83. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js +10 -0
  84. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js.map +1 -0
  85. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js +372 -0
  86. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js.map +1 -0
  87. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js +23 -0
  88. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js.map +1 -0
  89. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js +62 -0
  90. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js.map +1 -0
  91. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js +6 -0
  92. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js.map +1 -0
  93. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js +8 -0
  94. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js.map +1 -0
  95. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js +8 -0
  96. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js.map +1 -0
  97. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js +6 -0
  98. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js.map +1 -0
  99. package/dist/query/condition-builder.d.ts +41 -0
  100. package/dist/query/condition-builder.d.ts.map +1 -0
  101. package/dist/query/condition-builder.js +93 -0
  102. package/dist/query/condition-builder.js.map +1 -0
  103. package/dist/query/cursor.d.ts +88 -0
  104. package/dist/query/cursor.d.ts.map +1 -0
  105. package/dist/query/cursor.js +103 -0
  106. package/dist/query/cursor.js.map +1 -0
  107. package/dist/query/orm/orm.d.ts +18 -0
  108. package/dist/query/orm/orm.d.ts.map +1 -0
  109. package/dist/query/orm/orm.js +48 -0
  110. package/dist/query/orm/orm.js.map +1 -0
  111. package/dist/query/query.d.ts +79 -0
  112. package/dist/query/query.d.ts.map +1 -0
  113. package/dist/query/query.js +1 -0
  114. package/dist/query/result-transform.js +155 -0
  115. package/dist/query/result-transform.js.map +1 -0
  116. package/dist/query/unit-of-work.d.ts +435 -0
  117. package/dist/query/unit-of-work.d.ts.map +1 -0
  118. package/dist/query/unit-of-work.js +549 -0
  119. package/dist/query/unit-of-work.js.map +1 -0
  120. package/dist/schema/create.d.ts +273 -116
  121. package/dist/schema/create.d.ts.map +1 -1
  122. package/dist/schema/create.js +410 -222
  123. package/dist/schema/create.js.map +1 -1
  124. package/dist/schema/serialize.js +101 -0
  125. package/dist/schema/serialize.js.map +1 -0
  126. package/dist/schema-generator/schema-generator.d.ts +15 -0
  127. package/dist/schema-generator/schema-generator.d.ts.map +1 -0
  128. package/dist/shared/providers.d.ts +6 -0
  129. package/dist/shared/providers.d.ts.map +1 -0
  130. package/dist/util/import-generator.js +26 -0
  131. package/dist/util/import-generator.js.map +1 -0
  132. package/dist/util/parse.js +15 -0
  133. package/dist/util/parse.js.map +1 -0
  134. package/dist/util/types.d.ts +8 -0
  135. package/dist/util/types.d.ts.map +1 -0
  136. package/package.json +63 -2
  137. package/src/adapters/adapters.ts +22 -0
  138. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +433 -0
  139. package/src/adapters/drizzle/drizzle-adapter.test.ts +122 -0
  140. package/src/adapters/drizzle/drizzle-adapter.ts +118 -0
  141. package/src/adapters/drizzle/drizzle-query.ts +234 -0
  142. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +1084 -0
  143. package/src/adapters/drizzle/drizzle-uow-compiler.ts +546 -0
  144. package/src/adapters/drizzle/drizzle-uow-decoder.ts +165 -0
  145. package/src/adapters/drizzle/drizzle-uow-executor.ts +213 -0
  146. package/src/adapters/drizzle/generate.test.ts +643 -0
  147. package/src/adapters/drizzle/generate.ts +481 -0
  148. package/src/adapters/drizzle/join-column-utils.test.ts +79 -0
  149. package/src/adapters/drizzle/join-column-utils.ts +39 -0
  150. package/src/adapters/drizzle/migrate-drizzle.test.ts +226 -0
  151. package/src/adapters/drizzle/shared.ts +22 -0
  152. package/src/adapters/drizzle/test-utils.ts +56 -0
  153. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +789 -0
  154. package/src/adapters/kysely/kysely-adapter.ts +196 -0
  155. package/src/adapters/kysely/kysely-query-builder.test.ts +1344 -0
  156. package/src/adapters/kysely/kysely-query-builder.ts +611 -0
  157. package/src/adapters/kysely/kysely-query-compiler.ts +124 -0
  158. package/src/adapters/kysely/kysely-query.ts +254 -0
  159. package/src/adapters/kysely/kysely-uow-compiler.test.ts +916 -0
  160. package/src/adapters/kysely/kysely-uow-compiler.ts +271 -0
  161. package/src/adapters/kysely/kysely-uow-executor.ts +149 -0
  162. package/src/adapters/kysely/kysely-uow-joins.test.ts +811 -0
  163. package/src/adapters/kysely/migration/execute-mysql.test.ts +1173 -0
  164. package/src/adapters/kysely/migration/execute-postgres.test.ts +2657 -0
  165. package/src/adapters/kysely/migration/execute.ts +382 -0
  166. package/src/adapters/kysely/migration/kysely-migrator.test.ts +197 -0
  167. package/src/fragment.test.ts +287 -0
  168. package/src/fragment.ts +198 -0
  169. package/src/migration-engine/auto-from-schema.test.ts +118 -58
  170. package/src/migration-engine/auto-from-schema.ts +103 -32
  171. package/src/migration-engine/create.test.ts +34 -46
  172. package/src/migration-engine/create.ts +41 -26
  173. package/src/migration-engine/shared.ts +26 -6
  174. package/src/mod.ts +197 -1
  175. package/src/query/condition-builder.test.ts +379 -0
  176. package/src/query/condition-builder.ts +294 -0
  177. package/src/query/cursor.test.ts +296 -0
  178. package/src/query/cursor.ts +147 -0
  179. package/src/query/orm/orm.ts +92 -0
  180. package/src/query/query-type.test.ts +429 -0
  181. package/src/query/query.ts +200 -0
  182. package/src/query/result-transform.test.ts +795 -0
  183. package/src/query/result-transform.ts +247 -0
  184. package/src/query/unit-of-work-types.test.ts +192 -0
  185. package/src/query/unit-of-work.test.ts +947 -0
  186. package/src/query/unit-of-work.ts +1199 -0
  187. package/src/schema/create.test.ts +653 -110
  188. package/src/schema/create.ts +708 -337
  189. package/src/schema/serialize.test.ts +559 -0
  190. package/src/schema/serialize.ts +359 -0
  191. package/src/schema-generator/schema-generator.ts +12 -0
  192. package/src/shared/config.ts +0 -8
  193. package/src/util/import-generator.ts +28 -0
  194. package/src/util/parse.ts +16 -0
  195. package/src/util/types.ts +4 -0
  196. package/tsconfig.json +1 -1
  197. package/tsdown.config.ts +11 -1
  198. package/vitest.config.ts +3 -0
  199. /package/dist/{cuid.js → id.js} +0 -0
  200. /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
+ });