@fragno-dev/db 0.1.13 → 0.1.15

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 (178) hide show
  1. package/.turbo/turbo-build.log +179 -132
  2. package/CHANGELOG.md +30 -0
  3. package/dist/adapters/adapters.d.ts +27 -1
  4. package/dist/adapters/adapters.d.ts.map +1 -1
  5. package/dist/adapters/adapters.js.map +1 -1
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +5 -1
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-adapter.js +15 -3
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-query.js +7 -5
  11. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  12. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
  13. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.js +76 -44
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  16. package/dist/adapters/drizzle/drizzle-uow-decoder.js +23 -16
  17. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  18. package/dist/adapters/drizzle/drizzle-uow-executor.js +18 -7
  19. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -1
  20. package/dist/adapters/drizzle/generate.d.ts +4 -1
  21. package/dist/adapters/drizzle/generate.d.ts.map +1 -1
  22. package/dist/adapters/drizzle/generate.js +11 -18
  23. package/dist/adapters/drizzle/generate.js.map +1 -1
  24. package/dist/adapters/drizzle/shared.d.ts +14 -1
  25. package/dist/adapters/drizzle/shared.d.ts.map +1 -0
  26. package/dist/adapters/kysely/kysely-adapter.d.ts +5 -1
  27. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  28. package/dist/adapters/kysely/kysely-adapter.js +14 -3
  29. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  30. package/dist/adapters/kysely/kysely-query-builder.js +1 -1
  31. package/dist/adapters/kysely/kysely-query-compiler.js +3 -2
  32. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -1
  33. package/dist/adapters/kysely/kysely-query.d.ts +1 -0
  34. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
  35. package/dist/adapters/kysely/kysely-query.js +28 -19
  36. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  37. package/dist/adapters/kysely/kysely-shared.d.ts +14 -0
  38. package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
  39. package/dist/adapters/kysely/kysely-shared.js +16 -1
  40. package/dist/adapters/kysely/kysely-shared.js.map +1 -1
  41. package/dist/adapters/kysely/kysely-uow-compiler.js +68 -16
  42. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  43. package/dist/adapters/kysely/kysely-uow-executor.js +8 -4
  44. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  45. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  46. package/dist/adapters/kysely/migration/execute-base.js.map +1 -1
  47. package/dist/db-fragment-definition-builder.d.ts +152 -0
  48. package/dist/db-fragment-definition-builder.d.ts.map +1 -0
  49. package/dist/db-fragment-definition-builder.js +137 -0
  50. package/dist/db-fragment-definition-builder.js.map +1 -0
  51. package/dist/fragments/internal-fragment.d.ts +19 -0
  52. package/dist/fragments/internal-fragment.d.ts.map +1 -0
  53. package/dist/fragments/internal-fragment.js +39 -0
  54. package/dist/fragments/internal-fragment.js.map +1 -0
  55. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  56. package/dist/migration-engine/generation-engine.js +35 -15
  57. package/dist/migration-engine/generation-engine.js.map +1 -1
  58. package/dist/mod.d.ts +8 -18
  59. package/dist/mod.d.ts.map +1 -1
  60. package/dist/mod.js +7 -34
  61. package/dist/mod.js.map +1 -1
  62. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js +165 -0
  63. package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js.map +1 -0
  64. package/dist/packages/fragno/dist/api/bind-services.js +20 -0
  65. package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
  66. package/dist/packages/fragno/dist/api/error.js +48 -0
  67. package/dist/packages/fragno/dist/api/error.js.map +1 -0
  68. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
  69. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
  70. package/dist/packages/fragno/dist/api/fragment-instantiator.js +487 -0
  71. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
  72. package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
  73. package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
  74. package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
  75. package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
  76. package/dist/packages/fragno/dist/api/internal/route.js +10 -0
  77. package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
  78. package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
  79. package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
  80. package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
  81. package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
  82. package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
  83. package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
  84. package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
  85. package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
  86. package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
  87. package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
  88. package/dist/packages/fragno/dist/api/route.js +17 -0
  89. package/dist/packages/fragno/dist/api/route.js.map +1 -0
  90. package/dist/packages/fragno/dist/internal/symbols.js +10 -0
  91. package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
  92. package/dist/query/cursor.d.ts +10 -2
  93. package/dist/query/cursor.d.ts.map +1 -1
  94. package/dist/query/cursor.js +11 -4
  95. package/dist/query/cursor.js.map +1 -1
  96. package/dist/query/execute-unit-of-work.d.ts +123 -0
  97. package/dist/query/execute-unit-of-work.d.ts.map +1 -0
  98. package/dist/query/execute-unit-of-work.js +184 -0
  99. package/dist/query/execute-unit-of-work.js.map +1 -0
  100. package/dist/query/query.d.ts +3 -3
  101. package/dist/query/query.d.ts.map +1 -1
  102. package/dist/query/result-transform.js +4 -2
  103. package/dist/query/result-transform.js.map +1 -1
  104. package/dist/query/retry-policy.d.ts +88 -0
  105. package/dist/query/retry-policy.d.ts.map +1 -0
  106. package/dist/query/retry-policy.js +61 -0
  107. package/dist/query/retry-policy.js.map +1 -0
  108. package/dist/query/unit-of-work.d.ts +171 -32
  109. package/dist/query/unit-of-work.d.ts.map +1 -1
  110. package/dist/query/unit-of-work.js +530 -133
  111. package/dist/query/unit-of-work.js.map +1 -1
  112. package/dist/schema/serialize.js +12 -7
  113. package/dist/schema/serialize.js.map +1 -1
  114. package/dist/with-database.d.ts +28 -0
  115. package/dist/with-database.d.ts.map +1 -0
  116. package/dist/with-database.js +34 -0
  117. package/dist/with-database.js.map +1 -0
  118. package/package.json +10 -3
  119. package/src/adapters/adapters.ts +30 -0
  120. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +86 -17
  121. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +291 -7
  122. package/src/adapters/drizzle/drizzle-adapter.test.ts +3 -51
  123. package/src/adapters/drizzle/drizzle-adapter.ts +35 -7
  124. package/src/adapters/drizzle/drizzle-query.ts +25 -15
  125. package/src/adapters/drizzle/drizzle-uow-compiler-mysql.test.ts +1442 -0
  126. package/src/adapters/drizzle/drizzle-uow-compiler-sqlite.test.ts +1414 -0
  127. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +78 -61
  128. package/src/adapters/drizzle/drizzle-uow-compiler.ts +123 -42
  129. package/src/adapters/drizzle/drizzle-uow-decoder.ts +34 -27
  130. package/src/adapters/drizzle/drizzle-uow-executor.ts +41 -8
  131. package/src/adapters/drizzle/generate.test.ts +102 -269
  132. package/src/adapters/drizzle/generate.ts +12 -30
  133. package/src/adapters/drizzle/test-utils.ts +36 -5
  134. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +66 -22
  135. package/src/adapters/kysely/kysely-adapter-sqlite.test.ts +156 -0
  136. package/src/adapters/kysely/kysely-adapter.ts +25 -2
  137. package/src/adapters/kysely/kysely-query-compiler.ts +3 -8
  138. package/src/adapters/kysely/kysely-query.ts +57 -37
  139. package/src/adapters/kysely/kysely-shared.ts +34 -0
  140. package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -74
  141. package/src/adapters/kysely/kysely-uow-compiler.ts +92 -24
  142. package/src/adapters/kysely/kysely-uow-executor.ts +26 -7
  143. package/src/adapters/kysely/kysely-uow-joins.test.ts +33 -50
  144. package/src/adapters/kysely/migration/execute-base.ts +1 -1
  145. package/src/db-fragment-definition-builder.test.ts +887 -0
  146. package/src/db-fragment-definition-builder.ts +506 -0
  147. package/src/db-fragment-instantiator.test.ts +467 -0
  148. package/src/db-fragment-integration.test.ts +408 -0
  149. package/src/fragments/internal-fragment.test.ts +160 -0
  150. package/src/fragments/internal-fragment.ts +85 -0
  151. package/src/migration-engine/generation-engine.test.ts +58 -15
  152. package/src/migration-engine/generation-engine.ts +78 -25
  153. package/src/mod.ts +35 -43
  154. package/src/query/cursor.test.ts +119 -0
  155. package/src/query/cursor.ts +17 -4
  156. package/src/query/execute-unit-of-work.test.ts +1310 -0
  157. package/src/query/execute-unit-of-work.ts +463 -0
  158. package/src/query/query.ts +4 -4
  159. package/src/query/result-transform.test.ts +129 -0
  160. package/src/query/result-transform.ts +4 -1
  161. package/src/query/retry-policy.test.ts +217 -0
  162. package/src/query/retry-policy.ts +141 -0
  163. package/src/query/unit-of-work-coordinator.test.ts +833 -0
  164. package/src/query/unit-of-work-types.test.ts +15 -2
  165. package/src/query/unit-of-work.test.ts +878 -200
  166. package/src/query/unit-of-work.ts +963 -321
  167. package/src/schema/serialize.ts +22 -11
  168. package/src/with-database.ts +140 -0
  169. package/tsdown.config.ts +1 -0
  170. package/dist/fragment.d.ts +0 -54
  171. package/dist/fragment.d.ts.map +0 -1
  172. package/dist/fragment.js +0 -92
  173. package/dist/fragment.js.map +0 -1
  174. package/dist/shared/settings-schema.js +0 -36
  175. package/dist/shared/settings-schema.js.map +0 -1
  176. package/src/fragment.test.ts +0 -341
  177. package/src/fragment.ts +0 -198
  178. package/src/shared/settings-schema.ts +0 -61
@@ -0,0 +1,408 @@
1
+ import { Kysely } from "kysely";
2
+ import { SQLocalKysely } from "sqlocal/kysely";
3
+ import { assert, beforeAll, describe, expect, it } from "vitest";
4
+ import { z } from "zod";
5
+ import { KyselyAdapter } from "./adapters/kysely/kysely-adapter";
6
+ import { column, idColumn, referenceColumn, schema, type FragnoId } from "./schema/create";
7
+ import { defineFragment, instantiate } from "@fragno-dev/core";
8
+ import { defineRoutes } from "@fragno-dev/core/route";
9
+ import { withDatabase } from "./with-database";
10
+ import type { FragnoPublicConfigWithDatabase } from "./db-fragment-definition-builder";
11
+
12
+ describe.sequential("Database Fragment Integration", () => {
13
+ // Schema 1: Users fragment
14
+ const usersSchema = schema((s) => {
15
+ return s
16
+ .addTable("users", (t) => {
17
+ return t
18
+ .addColumn("id", idColumn())
19
+ .addColumn("name", column("string"))
20
+ .addColumn("email", column("string"))
21
+ .createIndex("email_idx", ["email"], { unique: true });
22
+ })
23
+ .addTable("profiles", (t) => {
24
+ return t
25
+ .addColumn("id", idColumn())
26
+ .addColumn("user_id", referenceColumn())
27
+ .addColumn("bio", column("string"))
28
+ .createIndex("profile_user_idx", ["user_id"]);
29
+ })
30
+ .addReference("user", {
31
+ type: "one",
32
+ from: { table: "profiles", column: "user_id" },
33
+ to: { table: "users", column: "id" },
34
+ });
35
+ });
36
+
37
+ // Schema 2: Orders fragment
38
+ const ordersSchema = schema((s) => {
39
+ return s.addTable("orders", (t) => {
40
+ return t
41
+ .addColumn("id", idColumn())
42
+ .addColumn("user_external_id", column("string")) // Store external user ID from users fragment
43
+ .addColumn("product_name", column("string"))
44
+ .addColumn("quantity", column("integer"))
45
+ .addColumn("total", column("integer"))
46
+ .createIndex("orders_user_idx", ["user_external_id"]);
47
+ });
48
+ });
49
+
50
+ // Define Users Fragment
51
+ const usersFragmentDef = defineFragment("users-fragment")
52
+ .extend(withDatabase(usersSchema, "users"))
53
+ .providesService("userService", ({ defineService }) => {
54
+ return defineService({
55
+ createUser(name: string, email: string): FragnoId {
56
+ const uow = this.uow(usersSchema);
57
+ return uow.create("users", { name, email });
58
+ },
59
+ async getUserById(userId: FragnoId | string) {
60
+ const uow = this.uow(usersSchema).find("users", (b) =>
61
+ b.whereIndex("primary", (eb) => eb("id", "=", userId)),
62
+ );
63
+ // Note: executeRetrieve() should be called by the caller before awaiting retrievalPhase
64
+ const [users] = await uow.retrievalPhase;
65
+ return users?.[0] ?? null;
66
+ },
67
+ createProfile(userId: FragnoId | string, bio: string): FragnoId {
68
+ const uow = this.uow(usersSchema);
69
+ return uow.create("profiles", {
70
+ user_id: userId,
71
+ bio,
72
+ });
73
+ },
74
+ });
75
+ })
76
+ .build();
77
+
78
+ // Define routes for Users Fragment
79
+ const usersRoutes = defineRoutes(usersFragmentDef).create(({ services, defineRoute }) => [
80
+ defineRoute({
81
+ method: "POST",
82
+ path: "/users",
83
+ outputSchema: z.object({ userId: z.string(), profileId: z.string() }),
84
+ handler: async function (_input, { json }) {
85
+ const { userId, profileId } = await this.uow(async ({ executeMutate }) => {
86
+ const userId = services.userService.createUser("John Doe", "john@example.com");
87
+ const profileId = services.userService.createProfile(userId, "Software engineer");
88
+ await executeMutate();
89
+ return { userId, profileId };
90
+ });
91
+
92
+ return json(
93
+ { userId: userId.externalId, profileId: profileId.externalId },
94
+ { status: 201 },
95
+ );
96
+ },
97
+ }),
98
+ ]);
99
+
100
+ // Define Orders Fragment with cross-fragment service dependency
101
+ const ordersFragmentDef = defineFragment("orders-fragment")
102
+ .extend(withDatabase(ordersSchema, "orders"))
103
+ .usesService<
104
+ "userService",
105
+ {
106
+ getUserById: (
107
+ userId: FragnoId | string,
108
+ ) => Promise<{ id: FragnoId; name: string; email: string } | null>;
109
+ }
110
+ >("userService")
111
+ .providesService("orderService", ({ defineService, serviceDeps }) => {
112
+ return defineService({
113
+ async createOrder(
114
+ userExternalId: string,
115
+ productName: string,
116
+ quantity: number,
117
+ total: number,
118
+ ) {
119
+ // Verify user exists by calling the userService from the other fragment
120
+ const user = await serviceDeps.userService.getUserById(userExternalId);
121
+
122
+ if (!user) {
123
+ throw new Error("User not found");
124
+ }
125
+
126
+ const uow = this.uow(ordersSchema);
127
+ const orderId = uow.create("orders", {
128
+ user_external_id: userExternalId,
129
+ product_name: productName,
130
+ quantity,
131
+ total,
132
+ });
133
+
134
+ await uow.mutationPhase;
135
+ return orderId;
136
+ },
137
+ async getOrdersByUser(userExternalId: string) {
138
+ const uow = this.uow(ordersSchema).find("orders", (b) =>
139
+ b.whereIndex("orders_user_idx", (eb) => eb("user_external_id", "=", userExternalId)),
140
+ );
141
+ // Note: executeRetrieve() should be called by the caller before awaiting retrievalPhase
142
+ const [orders] = await uow.retrievalPhase;
143
+ return orders;
144
+ },
145
+ });
146
+ })
147
+ .build();
148
+
149
+ // Define routes for Orders Fragment
150
+ const ordersRoutes = defineRoutes(ordersFragmentDef).create(({ services, defineRoute }) => [
151
+ defineRoute({
152
+ method: "POST",
153
+ path: "/orders",
154
+ inputSchema: z.object({
155
+ userId: z.string(),
156
+ productName: z.string(),
157
+ quantity: z.number(),
158
+ total: z.number(),
159
+ }),
160
+ outputSchema: z.object({ orderId: z.string() }),
161
+ handler: async function ({ input }, { json }) {
162
+ const body = await input.valid();
163
+
164
+ const { orderId } = await this.uow(async ({ executeMutate }) => {
165
+ const orderIdPromise = services.orderService.createOrder(
166
+ body.userId,
167
+ body.productName,
168
+ body.quantity,
169
+ body.total,
170
+ );
171
+
172
+ await executeMutate();
173
+
174
+ return { orderId: await orderIdPromise };
175
+ });
176
+
177
+ return json({ orderId: orderId.externalId }, { status: 201 });
178
+ },
179
+ }),
180
+ ]);
181
+
182
+ let adapter: KyselyAdapter;
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ let kysely: Kysely<any>;
185
+ let usersFragment: ReturnType<typeof instantiateUsersFragment>;
186
+ let ordersFragment: ReturnType<typeof instantiateOrdersFragment>;
187
+
188
+ // Shared state between tests
189
+ let userId: string;
190
+ let orderId: string;
191
+
192
+ function instantiateUsersFragment(options: FragnoPublicConfigWithDatabase) {
193
+ return instantiate(usersFragmentDef)
194
+ .withConfig({})
195
+ .withRoutes([usersRoutes])
196
+ .withOptions(options)
197
+ .build();
198
+ }
199
+
200
+ function instantiateOrdersFragment(options: FragnoPublicConfigWithDatabase) {
201
+ return instantiate(ordersFragmentDef)
202
+ .withConfig({})
203
+ .withRoutes([ordersRoutes])
204
+ .withOptions(options)
205
+ .withServices({
206
+ userService: usersFragment.services.userService,
207
+ })
208
+ .build();
209
+ }
210
+
211
+ beforeAll(async () => {
212
+ // Create in-memory SQLite database with Kysely
213
+ const { dialect } = new SQLocalKysely(":memory:");
214
+ kysely = new Kysely({
215
+ dialect,
216
+ });
217
+
218
+ adapter = new KyselyAdapter({
219
+ db: kysely,
220
+ provider: "sqlite",
221
+ });
222
+
223
+ // Run migrations for both schemas
224
+ const usersMigrator = adapter.createMigrationEngine(usersSchema, "users");
225
+ const usersPrep = await usersMigrator.prepareMigration({ updateSettings: false });
226
+ await usersPrep.execute();
227
+
228
+ const ordersMigrator = adapter.createMigrationEngine(ordersSchema, "orders");
229
+ const ordersPrep = await ordersMigrator.prepareMigration({ updateSettings: false });
230
+ await ordersPrep.execute();
231
+
232
+ // Instantiate fragments with shared database adapter
233
+ const options: FragnoPublicConfigWithDatabase = {
234
+ databaseAdapter: adapter,
235
+ };
236
+
237
+ usersFragment = instantiateUsersFragment(options);
238
+ ordersFragment = instantiateOrdersFragment(options);
239
+ }, 12000);
240
+
241
+ it("should create a user via API route", async () => {
242
+ const createUserResponse = await usersFragment.callRoute("POST", "/users");
243
+ assert(createUserResponse.type === "json");
244
+ expect(createUserResponse.data).toHaveProperty("userId");
245
+ userId = createUserResponse.data.userId;
246
+ });
247
+
248
+ it("should verify user was created with profile", async () => {
249
+ const user = await usersFragment.inContext(async function () {
250
+ return await this.uow(async ({ executeRetrieve }) => {
251
+ const userPromise = usersFragment.services.userService.getUserById(userId);
252
+ await executeRetrieve();
253
+ return await userPromise;
254
+ });
255
+ });
256
+
257
+ expect(user).toMatchObject({
258
+ id: expect.objectContaining({
259
+ externalId: userId,
260
+ }),
261
+ name: "John Doe",
262
+ email: "john@example.com",
263
+ });
264
+ });
265
+
266
+ it("should create an order via API route with cross-fragment service call", async () => {
267
+ const createOrderResponse = await ordersFragment.callRoute("POST", "/orders", {
268
+ body: {
269
+ userId: userId,
270
+ productName: "TypeScript Book",
271
+ quantity: 2,
272
+ total: 4999,
273
+ },
274
+ });
275
+ assert(
276
+ createOrderResponse.type === "json",
277
+ `createOrderResponse.type !== json: ${createOrderResponse.type}`,
278
+ );
279
+ orderId = createOrderResponse.data.orderId;
280
+ });
281
+
282
+ it("should verify order was created with correct user reference", async () => {
283
+ const orders = await ordersFragment.inContext(async function () {
284
+ return await this.uow(async ({ executeRetrieve }) => {
285
+ const ordersPromise = ordersFragment.services.orderService.getOrdersByUser(userId);
286
+ await executeRetrieve();
287
+ return await ordersPromise;
288
+ });
289
+ });
290
+ expect(orders).toHaveLength(1);
291
+ expect(orders[0]).toMatchObject({
292
+ id: expect.objectContaining({
293
+ externalId: orderId,
294
+ }),
295
+ user_external_id: userId,
296
+ product_name: "TypeScript Book",
297
+ quantity: 2,
298
+ total: 4999,
299
+ });
300
+ });
301
+
302
+ it("should verify cross-fragment service integration works bidirectionally", async () => {
303
+ // Orders service should be able to query users via the shared userService
304
+ const ordersByUser = await ordersFragment.inContext(async function () {
305
+ return await this.uow(async ({ executeRetrieve }) => {
306
+ const ordersPromise = ordersFragment.services.orderService.getOrdersByUser(userId);
307
+ await executeRetrieve();
308
+ return await ordersPromise;
309
+ });
310
+ });
311
+ const userFromOrdersContext = await usersFragment.inContext(async function () {
312
+ return await this.uow(async ({ executeRetrieve }) => {
313
+ const userPromise = usersFragment.services.userService.getUserById(
314
+ ordersByUser[0].user_external_id,
315
+ );
316
+ await executeRetrieve();
317
+ return await userPromise;
318
+ });
319
+ });
320
+
321
+ expect(userFromOrdersContext).toMatchObject({
322
+ id: expect.objectContaining({
323
+ externalId: userId,
324
+ }),
325
+ name: "John Doe",
326
+ email: "john@example.com",
327
+ });
328
+ });
329
+
330
+ it("should reject order creation for non-existent user", async () => {
331
+ const invalidOrderResponse = await ordersFragment.callRoute("POST", "/orders", {
332
+ body: {
333
+ userId: "non-existent-user-id",
334
+ productName: "Invalid Order",
335
+ quantity: 1,
336
+ total: 100,
337
+ },
338
+ });
339
+
340
+ // Should return error because user doesn't exist
341
+ expect(invalidOrderResponse.type).toBe("error");
342
+ });
343
+
344
+ it("should be able to use inContext to call a service", async () => {
345
+ const [user, order] = await usersFragment.inContext(async function () {
346
+ return await this.uow(async ({ executeRetrieve }) => {
347
+ const user = usersFragment.services.userService.getUserById(userId);
348
+ const orders = ordersFragment.services.orderService.getOrdersByUser(userId);
349
+ await executeRetrieve();
350
+ return [user, orders];
351
+ });
352
+ });
353
+ expect(user).toMatchObject({
354
+ id: expect.objectContaining({
355
+ externalId: userId,
356
+ }),
357
+ });
358
+
359
+ expect(order).toHaveLength(1);
360
+ expect(order[0]).toMatchObject({
361
+ id: expect.objectContaining({
362
+ externalId: orderId,
363
+ }),
364
+ user_external_id: userId,
365
+ });
366
+ });
367
+
368
+ it("should provide nonce and currentAttempt in the UOW context", async () => {
369
+ let firstNonce: string;
370
+
371
+ const result = await usersFragment.inContext(async function () {
372
+ return await this.uow(async ({ executeRetrieve, nonce, currentAttempt }) => {
373
+ const user = usersFragment.services.userService.getUserById(userId);
374
+ await executeRetrieve();
375
+
376
+ if (currentAttempt === 0) {
377
+ firstNonce = nonce;
378
+ throw new Error("Test error");
379
+ }
380
+
381
+ expect(nonce).toBe(firstNonce);
382
+
383
+ // Return context data along with user data
384
+ return {
385
+ user,
386
+ nonce,
387
+ currentAttempt,
388
+ };
389
+ });
390
+ });
391
+
392
+ // Verify nonce is a string UUID
393
+ expect(result.nonce).toBeDefined();
394
+ expect(typeof result.nonce).toBe("string");
395
+ expect(result.nonce).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
396
+
397
+ expect(result.currentAttempt).toBe(1);
398
+
399
+ // Verify user data is still correct
400
+ expect(result.user).toMatchObject({
401
+ id: expect.objectContaining({
402
+ externalId: userId,
403
+ }),
404
+ name: "John Doe",
405
+ email: "john@example.com",
406
+ });
407
+ });
408
+ });
@@ -0,0 +1,160 @@
1
+ import { drizzle } from "drizzle-orm/libsql";
2
+ import { beforeAll, describe, expect, it } from "vitest";
3
+ import { instantiate } from "@fragno-dev/core";
4
+ import { internalFragmentDef, settingsSchema } from "./internal-fragment";
5
+ import type { FragnoPublicConfigWithDatabase } from "../db-fragment-definition-builder";
6
+ import { DrizzleAdapter } from "../adapters/drizzle/drizzle-adapter";
7
+ import type { DBType } from "../adapters/drizzle/shared";
8
+ import { writeAndLoadSchema } from "../adapters/drizzle/test-utils";
9
+ import { createClient } from "@libsql/client";
10
+ import { createRequire } from "node:module";
11
+
12
+ // Import drizzle-kit for migrations
13
+ const require = createRequire(import.meta.url);
14
+ const { generateSQLiteDrizzleJson, generateSQLiteMigration } =
15
+ require("drizzle-kit/api") as typeof import("drizzle-kit/api");
16
+
17
+ describe("Internal Fragment", () => {
18
+ let adapter: DrizzleAdapter;
19
+ let db: DBType;
20
+ let fragment: ReturnType<typeof instantiateFragment>;
21
+
22
+ function instantiateFragment(options: FragnoPublicConfigWithDatabase) {
23
+ return instantiate(internalFragmentDef).withConfig({}).withOptions(options).build();
24
+ }
25
+
26
+ beforeAll(async () => {
27
+ const { schemaModule, cleanup } = await writeAndLoadSchema(
28
+ "internal-fragment",
29
+ settingsSchema,
30
+ "sqlite",
31
+ "",
32
+ );
33
+
34
+ const client = createClient({
35
+ url: "file::memory:?cache=shared",
36
+ });
37
+
38
+ db = drizzle(client, {
39
+ schema: schemaModule,
40
+ }) as unknown as DBType;
41
+
42
+ // Generate and run migrations for both schemas
43
+ const emptyJson = await generateSQLiteDrizzleJson({});
44
+ const targetJson = await generateSQLiteDrizzleJson(schemaModule);
45
+
46
+ const migrationStatements = await generateSQLiteMigration(emptyJson, targetJson);
47
+
48
+ for (const statement of migrationStatements) {
49
+ await client.execute(statement);
50
+ }
51
+
52
+ adapter = new DrizzleAdapter({
53
+ db,
54
+ provider: "sqlite",
55
+ });
56
+
57
+ // Instantiate fragment with shared database adapter
58
+ const options: FragnoPublicConfigWithDatabase = {
59
+ databaseAdapter: adapter,
60
+ };
61
+
62
+ fragment = instantiateFragment(options);
63
+
64
+ return async () => {
65
+ client.close();
66
+ await cleanup();
67
+ };
68
+ }, 12000);
69
+
70
+ it("should get undefined for non-existent key", async () => {
71
+ const result = await fragment.inContext(async function () {
72
+ return await this.uow(async ({ executeRetrieve }) => {
73
+ const valuePromise = fragment.services.settingsService.get("test-key");
74
+ await executeRetrieve();
75
+ return await valuePromise;
76
+ });
77
+ });
78
+
79
+ expect(result).toBeUndefined();
80
+ });
81
+
82
+ it("should set and get a value", async () => {
83
+ await fragment.inContext(async function () {
84
+ return await this.uow(async ({ executeMutate }) => {
85
+ const setPromise = fragment.services.settingsService.set("test-key", "test-value");
86
+ await executeMutate();
87
+ await setPromise;
88
+ });
89
+ });
90
+
91
+ const result = await fragment.inContext(async function () {
92
+ return await this.uow(async ({ executeRetrieve }) => {
93
+ const valuePromise = fragment.services.settingsService.get("test-key");
94
+ await executeRetrieve();
95
+ return await valuePromise;
96
+ });
97
+ });
98
+
99
+ expect(result).toMatchObject({
100
+ key: "fragno-db-settings.test-key",
101
+ value: "test-value",
102
+ });
103
+ });
104
+
105
+ it("should update an existing value", async () => {
106
+ await fragment.inContext(async function () {
107
+ return await this.uow(async ({ executeMutate }) => {
108
+ const setPromise = fragment.services.settingsService.set("test-key", "updated-value");
109
+ await executeMutate();
110
+ await setPromise;
111
+ });
112
+ });
113
+
114
+ const result = await fragment.inContext(async function () {
115
+ return await this.uow(async ({ executeRetrieve }) => {
116
+ const valuePromise = fragment.services.settingsService.get("test-key");
117
+ await executeRetrieve();
118
+ return await valuePromise;
119
+ });
120
+ });
121
+
122
+ expect(result).toMatchObject({
123
+ key: "fragno-db-settings.test-key",
124
+ value: "updated-value",
125
+ });
126
+ });
127
+
128
+ it("should delete a value", async () => {
129
+ // First get the ID
130
+ const setting = await fragment.inContext(async function () {
131
+ return await this.uow(async ({ executeRetrieve }) => {
132
+ const valuePromise = fragment.services.settingsService.get("test-key");
133
+ await executeRetrieve();
134
+ return await valuePromise;
135
+ });
136
+ });
137
+
138
+ expect(setting).toBeDefined();
139
+
140
+ // Delete it
141
+ await fragment.inContext(async function () {
142
+ return await this.uow(async ({ executeMutate }) => {
143
+ const deletePromise = fragment.services.settingsService.delete(setting!.id);
144
+ await executeMutate();
145
+ await deletePromise;
146
+ });
147
+ });
148
+
149
+ // Verify it's gone
150
+ const result = await fragment.inContext(async function () {
151
+ return await this.uow(async ({ executeRetrieve }) => {
152
+ const valuePromise = fragment.services.settingsService.get("test-key");
153
+ await executeRetrieve();
154
+ return await valuePromise;
155
+ });
156
+ });
157
+
158
+ expect(result).toBeUndefined();
159
+ });
160
+ });
@@ -0,0 +1,85 @@
1
+ import { FragmentDefinitionBuilder } from "@fragno-dev/core";
2
+ import {
3
+ DatabaseFragmentDefinitionBuilder,
4
+ type DatabaseHandlerContext,
5
+ type DatabaseRequestStorage,
6
+ type DatabaseServiceContext,
7
+ type FragnoPublicConfigWithDatabase,
8
+ type ImplicitDatabaseDependencies,
9
+ } from "../db-fragment-definition-builder";
10
+ import type { FragnoId } from "../schema/create";
11
+ import { schema, idColumn, column } from "../schema/create";
12
+
13
+ // Constants for Fragno's internal settings table
14
+ export const SETTINGS_TABLE_NAME = "fragno_db_settings" as const;
15
+ export const SETTINGS_NAMESPACE = "fragno-db-settings" as const;
16
+
17
+ // Settings schema for storing Fragno's internal key-value settings
18
+ export const settingsSchema = schema((s) => {
19
+ return s.addTable(SETTINGS_TABLE_NAME, (t) => {
20
+ return t
21
+ .addColumn("id", idColumn())
22
+ .addColumn("key", column("string"))
23
+ .addColumn("value", column("string"))
24
+ .createIndex("unique_key", ["key"], { unique: true });
25
+ });
26
+ });
27
+
28
+ // This uses DatabaseFragmentDefinitionBuilder directly
29
+ // to avoid circular dependency (it doesn't need to link to itself)
30
+ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
31
+ new FragmentDefinitionBuilder<
32
+ {},
33
+ FragnoPublicConfigWithDatabase,
34
+ ImplicitDatabaseDependencies<typeof settingsSchema>,
35
+ {},
36
+ {},
37
+ {},
38
+ {},
39
+ DatabaseServiceContext,
40
+ DatabaseHandlerContext,
41
+ DatabaseRequestStorage
42
+ >("$fragno-internal-fragment"),
43
+ settingsSchema,
44
+ "", // intentionally blank namespace so there is no prefix
45
+ )
46
+ .providesService("settingsService", ({ defineService }) => {
47
+ return defineService({
48
+ async get(key: string): Promise<{ id: FragnoId; key: string; value: string } | undefined> {
49
+ const uow = this.uow(settingsSchema).find(SETTINGS_TABLE_NAME, (b) =>
50
+ b.whereIndex("unique_key", (eb) => eb("key", "=", `${SETTINGS_NAMESPACE}.${key}`)),
51
+ );
52
+ const [results] = await uow.retrievalPhase;
53
+ return results?.[0];
54
+ },
55
+
56
+ async set(key: string, value: string) {
57
+ const uow = this.uow(settingsSchema);
58
+
59
+ // First, find if the key already exists
60
+ const findUow = uow.find(SETTINGS_TABLE_NAME, (b) =>
61
+ b.whereIndex("unique_key", (eb) => eb("key", "=", `${SETTINGS_NAMESPACE}.${key}`)),
62
+ );
63
+ const [existing] = await findUow.retrievalPhase;
64
+
65
+ if (existing?.[0]) {
66
+ uow.update(SETTINGS_TABLE_NAME, existing[0].id, (b) => b.set({ value }).check());
67
+ } else {
68
+ uow.create(SETTINGS_TABLE_NAME, {
69
+ key: `${SETTINGS_NAMESPACE}.${key}`,
70
+ value,
71
+ });
72
+ }
73
+
74
+ // Await mutation phase - will throw if mutation fails
75
+ await uow.mutationPhase;
76
+ },
77
+
78
+ async delete(id: FragnoId) {
79
+ const uow = this.uow(settingsSchema);
80
+ uow.delete(SETTINGS_TABLE_NAME, id);
81
+ await uow.mutationPhase;
82
+ },
83
+ });
84
+ })
85
+ .build();