@fragno-dev/db 0.2.0 → 0.2.2

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 (62) hide show
  1. package/.turbo/turbo-build.log +34 -30
  2. package/CHANGELOG.md +49 -0
  3. package/dist/adapters/generic-sql/query/where-builder.js +1 -1
  4. package/dist/db-fragment-definition-builder.d.ts +31 -39
  5. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  6. package/dist/db-fragment-definition-builder.js +20 -16
  7. package/dist/db-fragment-definition-builder.js.map +1 -1
  8. package/dist/fragments/internal-fragment.d.ts +94 -8
  9. package/dist/fragments/internal-fragment.d.ts.map +1 -1
  10. package/dist/fragments/internal-fragment.js +56 -55
  11. package/dist/fragments/internal-fragment.js.map +1 -1
  12. package/dist/hooks/hooks.d.ts +5 -3
  13. package/dist/hooks/hooks.d.ts.map +1 -1
  14. package/dist/hooks/hooks.js +38 -37
  15. package/dist/hooks/hooks.js.map +1 -1
  16. package/dist/mod.d.ts +3 -3
  17. package/dist/mod.d.ts.map +1 -1
  18. package/dist/mod.js +4 -4
  19. package/dist/mod.js.map +1 -1
  20. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +367 -80
  21. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
  22. package/dist/query/unit-of-work/execute-unit-of-work.js +448 -148
  23. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
  24. package/dist/query/unit-of-work/unit-of-work.d.ts +35 -11
  25. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
  26. package/dist/query/unit-of-work/unit-of-work.js +49 -19
  27. package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
  28. package/dist/query/value-decoding.js +1 -1
  29. package/dist/schema/create.d.ts +2 -3
  30. package/dist/schema/create.d.ts.map +1 -1
  31. package/dist/schema/create.js +2 -5
  32. package/dist/schema/create.js.map +1 -1
  33. package/dist/schema/generate-id.d.ts +20 -0
  34. package/dist/schema/generate-id.d.ts.map +1 -0
  35. package/dist/schema/generate-id.js +28 -0
  36. package/dist/schema/generate-id.js.map +1 -0
  37. package/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -1
  38. package/package.json +3 -3
  39. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +1 -0
  40. package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
  41. package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
  42. package/src/db-fragment-definition-builder.test.ts +58 -42
  43. package/src/db-fragment-definition-builder.ts +78 -88
  44. package/src/db-fragment-instantiator.test.ts +64 -88
  45. package/src/db-fragment-integration.test.ts +292 -142
  46. package/src/fragments/internal-fragment.test.ts +272 -266
  47. package/src/fragments/internal-fragment.ts +155 -122
  48. package/src/hooks/hooks.test.ts +268 -264
  49. package/src/hooks/hooks.ts +74 -63
  50. package/src/mod.ts +14 -4
  51. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1582 -998
  52. package/src/query/unit-of-work/execute-unit-of-work.ts +1746 -343
  53. package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
  54. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +269 -21
  55. package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
  56. package/src/query/unit-of-work/unit-of-work.ts +65 -30
  57. package/src/schema/create.ts +2 -5
  58. package/src/schema/generate-id.test.ts +57 -0
  59. package/src/schema/generate-id.ts +38 -0
  60. package/src/shared/config.ts +0 -10
  61. package/src/shared/connection-pool.ts +0 -24
  62. package/src/shared/prisma.ts +0 -45
@@ -7,7 +7,7 @@ import { defineFragment, instantiate } from "@fragno-dev/core";
7
7
  import { defineRoutes } from "@fragno-dev/core/route";
8
8
  import { withDatabase } from "./with-database";
9
9
  import type { FragnoPublicConfigWithDatabase } from "./db-fragment-definition-builder";
10
- import { ConcurrencyConflictError } from "./query/unit-of-work/execute-unit-of-work";
10
+ import { ConcurrencyConflictError, type TxResult } from "./query/unit-of-work/execute-unit-of-work";
11
11
  import { SQLocalDriverConfig } from "./adapters/generic-sql/driver-config";
12
12
 
13
13
  describe.sequential("Database Fragment Integration", () => {
@@ -48,106 +48,124 @@ describe.sequential("Database Fragment Integration", () => {
48
48
  });
49
49
  });
50
50
 
51
- // Define Users Fragment
51
+ // Define Users Fragment using the new unified serviceTx API
52
52
  const usersFragmentDef = defineFragment("users-fragment")
53
53
  .extend(withDatabase(usersSchema, "users"))
54
54
  .providesService("userService", ({ defineService }) => {
55
55
  return defineService({
56
- createUser(name: string, email: string): FragnoId {
57
- const uow = this.uow(usersSchema);
58
- return uow.create("users", { name, email });
56
+ // Creates a user - returns TxResult<FragnoId>
57
+ createUser(name: string, email: string) {
58
+ return this.serviceTx(usersSchema)
59
+ .mutate(({ uow }) => uow.create("users", { name, email }))
60
+ .build();
59
61
  },
60
- async getUserById(userId: FragnoId | string) {
61
- const uow = this.uow(usersSchema).find("users", (b) =>
62
- b.whereIndex("primary", (eb) => eb("id", "=", userId)),
63
- );
64
- // Note: executeRetrieve() should be called by the caller before awaiting retrievalPhase
65
- const [users] = await uow.retrievalPhase;
66
- return users?.[0] ?? null;
62
+ // Gets a user by ID - returns TxResult<User | null>
63
+ getUserById(userId: FragnoId | string) {
64
+ return this.serviceTx(usersSchema)
65
+ .retrieve((uow) =>
66
+ uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId))),
67
+ )
68
+ .transformRetrieve(
69
+ ([users]): { id: FragnoId; name: string; email: string } | null => users[0] ?? null,
70
+ )
71
+ .build();
67
72
  },
68
- createProfile(userId: FragnoId | string, bio: string): FragnoId {
69
- const uow = this.uow(usersSchema);
70
- return uow.create("profiles", {
71
- user_id: userId,
72
- bio,
73
- });
73
+ // Creates a profile - returns TxResult<FragnoId>
74
+ createProfile(userId: FragnoId | string, bio: string) {
75
+ return this.serviceTx(usersSchema)
76
+ .mutate(({ uow }) =>
77
+ uow.create("profiles", {
78
+ user_id: userId,
79
+ bio,
80
+ }),
81
+ )
82
+ .build();
74
83
  },
75
84
  });
76
85
  })
77
86
  .build();
78
87
 
79
- // Define routes for Users Fragment
80
- const usersRoutes = defineRoutes(usersFragmentDef).create(({ services, defineRoute }) => [
88
+ // Define routes for Users Fragment using new handlerTx API
89
+ const usersRoutes = defineRoutes(usersFragmentDef).create(({ defineRoute }) => [
81
90
  defineRoute({
82
91
  method: "POST",
83
92
  path: "/users",
84
93
  outputSchema: z.object({ userId: z.string(), profileId: z.string() }),
85
94
  handler: async function (_input, { json }) {
86
- const { userId, profileId } = await this.uow(async ({ executeMutate }) => {
87
- const userId = services.userService.createUser("John Doe", "john@example.com");
88
- const profileId = services.userService.createProfile(userId, "Software engineer");
89
- await executeMutate();
90
- return { userId, profileId };
91
- });
95
+ // Use handlerTx with mutate to create both user and profile atomically
96
+ const result = await this.handlerTx()
97
+ .mutate(({ forSchema }) => {
98
+ const uow = forSchema(usersSchema);
99
+ const userId = uow.create("users", { name: "John Doe", email: "john@example.com" });
100
+ const profileId = uow.create("profiles", {
101
+ user_id: userId,
102
+ bio: "Software engineer",
103
+ });
104
+ return { userId, profileId };
105
+ })
106
+ .execute();
92
107
 
93
108
  return json(
94
- { userId: userId.externalId, profileId: profileId.externalId },
109
+ { userId: result.userId.externalId, profileId: result.profileId.externalId },
95
110
  { status: 201 },
96
111
  );
97
112
  },
98
113
  }),
99
114
  ]);
100
115
 
101
- // Define Orders Fragment with cross-fragment service dependency
116
+ // User type for service dependency
117
+ type User = { id: FragnoId; name: string; email: string };
118
+
119
+ // Define Orders Fragment with cross-fragment service dependency using new serviceTx API
102
120
  const ordersFragmentDef = defineFragment("orders-fragment")
103
121
  .extend(withDatabase(ordersSchema, "orders"))
104
122
  .usesService<
105
123
  "userService",
106
124
  {
107
- getUserById: (
108
- userId: FragnoId | string,
109
- ) => Promise<{ id: FragnoId; name: string; email: string } | null>;
125
+ // Service methods now return TxResult instead of Promise
126
+ // TxResult<T> defaults to TxResult<T, T> (deps receive same type as final result)
127
+ getUserById: (userId: FragnoId | string) => TxResult<User | null>;
110
128
  }
111
129
  >("userService")
112
130
  .providesService("orderService", ({ defineService, serviceDeps }) => {
113
131
  return defineService({
114
- async createOrder(
115
- userExternalId: string,
116
- productName: string,
117
- quantity: number,
118
- total: number,
119
- ) {
120
- // Verify user exists by calling the userService from the other fragment
121
- const user = await serviceDeps.userService.getUserById(userExternalId);
122
-
123
- if (!user) {
124
- throw new Error("User not found");
125
- }
126
-
127
- const uow = this.uow(ordersSchema);
128
- const orderId = uow.create("orders", {
129
- user_external_id: userExternalId,
130
- product_name: productName,
131
- quantity,
132
- total,
133
- });
134
-
135
- await uow.mutationPhase;
136
- return orderId;
132
+ createOrder(userExternalId: string, productName: string, quantity: number, total: number) {
133
+ return this.serviceTx(ordersSchema)
134
+ .withServiceCalls(() => [serviceDeps.userService.getUserById(userExternalId)] as const)
135
+ .mutate(({ uow, serviceIntermediateResult: [user] }) => {
136
+ if (!user) {
137
+ throw new Error("User not found");
138
+ }
139
+
140
+ expect(user.id.externalId).toBe(userExternalId);
141
+
142
+ return uow.create("orders", {
143
+ user_external_id: userExternalId,
144
+ product_name: productName,
145
+ quantity,
146
+ total,
147
+ });
148
+ })
149
+ .build();
137
150
  },
138
- async getOrdersByUser(userExternalId: string) {
139
- const uow = this.uow(ordersSchema).find("orders", (b) =>
140
- b.whereIndex("orders_user_idx", (eb) => eb("user_external_id", "=", userExternalId)),
141
- );
142
- // Note: executeRetrieve() should be called by the caller before awaiting retrievalPhase
143
- const [orders] = await uow.retrievalPhase;
144
- return orders;
151
+ // Gets orders by user - returns TxResult<Order[]>
152
+ getOrdersByUser(userExternalId: string) {
153
+ return this.serviceTx(ordersSchema)
154
+ .retrieve((uow) =>
155
+ uow.find("orders", (b) =>
156
+ b.whereIndex("orders_user_idx", (eb) =>
157
+ eb("user_external_id", "=", userExternalId),
158
+ ),
159
+ ),
160
+ )
161
+ .transformRetrieve(([orders]) => orders)
162
+ .build();
145
163
  },
146
164
  });
147
165
  })
148
166
  .build();
149
167
 
150
- // Define routes for Orders Fragment
168
+ // Define routes for Orders Fragment using new handlerTx API
151
169
  const ordersRoutes = defineRoutes(ordersFragmentDef).create(({ services, defineRoute }) => [
152
170
  defineRoute({
153
171
  method: "POST",
@@ -159,23 +177,34 @@ describe.sequential("Database Fragment Integration", () => {
159
177
  total: z.number(),
160
178
  }),
161
179
  outputSchema: z.object({ orderId: z.string() }),
162
- handler: async function ({ input }, { json }) {
180
+ handler: async function ({ input }, { json, error }) {
163
181
  const body = await input.valid();
164
182
 
165
- const { orderId } = await this.uow(async ({ executeMutate }) => {
166
- const orderIdPromise = services.orderService.createOrder(
167
- body.userId,
168
- body.productName,
169
- body.quantity,
170
- body.total,
171
- );
172
-
173
- await executeMutate();
174
-
175
- return { orderId: await orderIdPromise };
176
- });
177
-
178
- return json({ orderId: orderId.externalId }, { status: 201 });
183
+ try {
184
+ // Use handlerTx with withServiceCalls to execute the service TxResult
185
+ // createOrder validates that the user exists
186
+ const result = await this.handlerTx()
187
+ .withServiceCalls(
188
+ () =>
189
+ [
190
+ services.orderService.createOrder(
191
+ body.userId,
192
+ body.productName,
193
+ body.quantity,
194
+ body.total,
195
+ ),
196
+ ] as const,
197
+ )
198
+ .transform(({ serviceResult: [orderId] }) => ({ orderId }))
199
+ .execute();
200
+
201
+ return json({ orderId: result.orderId.externalId }, { status: 201 });
202
+ } catch (e) {
203
+ if (e instanceof Error && e.message === "User not found") {
204
+ return error({ message: "User not found", code: "USER_NOT_FOUND" }, { status: 404 });
205
+ }
206
+ throw e;
207
+ }
179
208
  },
180
209
  }),
181
210
  ]);
@@ -244,11 +273,12 @@ describe.sequential("Database Fragment Integration", () => {
244
273
 
245
274
  it("should verify user was created with profile", async () => {
246
275
  const user = await usersFragment.inContext(async function () {
247
- return await this.uow(async ({ executeRetrieve }) => {
248
- const userPromise = usersFragment.services.userService.getUserById(userId);
249
- await executeRetrieve();
250
- return await userPromise;
251
- });
276
+ // Use handlerTx with withServiceCalls to execute the service TxResult
277
+ const result = await this.handlerTx()
278
+ .withServiceCalls(() => [usersFragment.services.userService.getUserById(userId)] as const)
279
+ .transform(({ serviceResult: [user] }) => user)
280
+ .execute();
281
+ return result;
252
282
  });
253
283
 
254
284
  expect(user).toMatchObject({
@@ -274,15 +304,18 @@ describe.sequential("Database Fragment Integration", () => {
274
304
  `createOrderResponse.type !== json: ${createOrderResponse.type}`,
275
305
  );
276
306
  orderId = createOrderResponse.data.orderId;
277
- });
307
+ }, 500);
278
308
 
279
309
  it("should verify order was created with correct user reference", async () => {
280
310
  const orders = await ordersFragment.inContext(async function () {
281
- return await this.uow(async ({ executeRetrieve }) => {
282
- const ordersPromise = ordersFragment.services.orderService.getOrdersByUser(userId);
283
- await executeRetrieve();
284
- return await ordersPromise;
285
- });
311
+ // Use handlerTx with withServiceCalls to execute the service TxResult
312
+ const result = await this.handlerTx()
313
+ .withServiceCalls(
314
+ () => [ordersFragment.services.orderService.getOrdersByUser(userId)] as const,
315
+ )
316
+ .transform(({ serviceResult: [orders] }) => orders)
317
+ .execute();
318
+ return result;
286
319
  });
287
320
  expect(orders).toHaveLength(1);
288
321
  expect(orders[0]).toMatchObject({
@@ -299,20 +332,25 @@ describe.sequential("Database Fragment Integration", () => {
299
332
  it("should verify cross-fragment service integration works bidirectionally", async () => {
300
333
  // Orders service should be able to query users via the shared userService
301
334
  const ordersByUser = await ordersFragment.inContext(async function () {
302
- return await this.uow(async ({ executeRetrieve }) => {
303
- const ordersPromise = ordersFragment.services.orderService.getOrdersByUser(userId);
304
- await executeRetrieve();
305
- return await ordersPromise;
306
- });
335
+ const result = await this.handlerTx()
336
+ .withServiceCalls(
337
+ () => [ordersFragment.services.orderService.getOrdersByUser(userId)] as const,
338
+ )
339
+ .transform(({ serviceResult: [orders] }) => orders)
340
+ .execute();
341
+ return result;
307
342
  });
308
343
  const userFromOrdersContext = await usersFragment.inContext(async function () {
309
- return await this.uow(async ({ executeRetrieve }) => {
310
- const userPromise = usersFragment.services.userService.getUserById(
311
- ordersByUser[0].user_external_id,
312
- );
313
- await executeRetrieve();
314
- return await userPromise;
315
- });
344
+ const result = await this.handlerTx()
345
+ .withServiceCalls(
346
+ () =>
347
+ [
348
+ usersFragment.services.userService.getUserById(ordersByUser[0].user_external_id),
349
+ ] as const,
350
+ )
351
+ .transform(({ serviceResult: [user] }) => user)
352
+ .execute();
353
+ return result;
316
354
  });
317
355
 
318
356
  expect(userFromOrdersContext).toMatchObject({
@@ -334,27 +372,36 @@ describe.sequential("Database Fragment Integration", () => {
334
372
  },
335
373
  });
336
374
 
337
- // Should return error because user doesn't exist
338
- expect(invalidOrderResponse.type).toBe("error");
375
+ // Should return 404 error because user doesn't exist
376
+ expect(invalidOrderResponse.status).toBe(404);
339
377
  });
340
378
 
341
379
  it("should be able to use inContext to call a service", async () => {
342
- const [user, order] = await usersFragment.inContext(async function () {
343
- return await this.uow(async ({ executeRetrieve }) => {
344
- const user = usersFragment.services.userService.getUserById(userId);
345
- const orders = ordersFragment.services.orderService.getOrdersByUser(userId);
346
- await executeRetrieve();
347
- return [user, orders];
348
- });
380
+ const result = await usersFragment.inContext(async function () {
381
+ // Use handlerTx with multiple deps
382
+ return await this.handlerTx()
383
+ .withServiceCalls(
384
+ () =>
385
+ [
386
+ usersFragment.services.userService.getUserById(userId),
387
+ ordersFragment.services.orderService.getOrdersByUser(userId),
388
+ ] as const,
389
+ )
390
+ .transform(({ serviceResult: [userResult, ordersResult] }) => ({
391
+ user: userResult,
392
+ orders: ordersResult,
393
+ }))
394
+ .execute();
349
395
  });
350
- expect(user).toMatchObject({
396
+
397
+ expect(result.user).toMatchObject({
351
398
  id: expect.objectContaining({
352
399
  externalId: userId,
353
400
  }),
354
401
  });
355
402
 
356
- expect(order).toHaveLength(1);
357
- expect(order[0]).toMatchObject({
403
+ expect(result.orders).toHaveLength(1);
404
+ expect(result.orders[0]).toMatchObject({
358
405
  id: expect.objectContaining({
359
406
  externalId: orderId,
360
407
  }),
@@ -362,45 +409,148 @@ describe.sequential("Database Fragment Integration", () => {
362
409
  });
363
410
  });
364
411
 
365
- it("should provide nonce and currentAttempt in the UOW context", async () => {
366
- let firstNonce: string;
412
+ it("should provide idempotencyKey and currentAttempt in the handlerTx context", async () => {
413
+ let firstIdempotencyKey: string;
367
414
 
368
415
  const result = await usersFragment.inContext(async function () {
369
- return await this.uow(async ({ executeRetrieve, nonce, currentAttempt }) => {
370
- const user = usersFragment.services.userService.getUserById(userId);
371
- await executeRetrieve();
372
-
373
- if (currentAttempt === 0) {
374
- firstNonce = nonce;
375
- // Trigger a conflict by throwing the specific conflict error
376
- throw new ConcurrencyConflictError();
377
- }
416
+ return await this.handlerTx()
417
+ .mutate(({ forSchema, idempotencyKey, currentAttempt }) => {
418
+ if (currentAttempt === 0) {
419
+ firstIdempotencyKey = idempotencyKey;
420
+ // Trigger a conflict by throwing the specific conflict error
421
+ throw new ConcurrencyConflictError();
422
+ }
378
423
 
379
- expect(nonce).toBe(firstNonce);
424
+ expect(idempotencyKey).toBe(firstIdempotencyKey);
380
425
 
381
- // Return context data along with user data
382
- return {
383
- user,
384
- nonce,
385
- currentAttempt,
386
- };
387
- });
426
+ // Create something to verify the mutation works
427
+ const newUserId = forSchema(usersSchema).create("users", {
428
+ name: "Nonce Test User",
429
+ email: `nonce-test-${Date.now()}@example.com`,
430
+ });
431
+
432
+ // Return context data
433
+ return {
434
+ newUserId,
435
+ idempotencyKey,
436
+ currentAttempt,
437
+ };
438
+ })
439
+ .execute();
388
440
  });
389
441
 
390
- // Verify nonce is a string UUID
391
- expect(result.nonce).toBeDefined();
392
- expect(typeof result.nonce).toBe("string");
393
- 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);
442
+ // Verify idempotencyKey is a string UUID
443
+ expect(result.idempotencyKey).toBeDefined();
444
+ expect(typeof result.idempotencyKey).toBe("string");
445
+ expect(result.idempotencyKey).toMatch(
446
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
447
+ );
394
448
 
395
449
  expect(result.currentAttempt).toBe(1);
396
450
 
397
- // Verify user data is still correct
398
- expect(result.user).toMatchObject({
399
- id: expect.objectContaining({
400
- externalId: userId,
401
- }),
402
- name: "John Doe",
403
- email: "john@example.com",
451
+ // Verify user was created
452
+ expect(result.newUserId).toBeDefined();
453
+ expect(result.newUserId.externalId).toBeDefined();
454
+ });
455
+
456
+ describe("Unified Tx Builder API (serviceTx/handlerTx)", () => {
457
+ it("should use handlerTx with mutate to create records", async () => {
458
+ const result = await usersFragment.inContext(async function () {
459
+ return await this.handlerTx()
460
+ .mutate(({ forSchema }) => {
461
+ const newUserId = forSchema(usersSchema).create("users", {
462
+ name: "Unified API User",
463
+ email: "unified@example.com",
464
+ });
465
+ return { newUserId };
466
+ })
467
+ .execute();
468
+ });
469
+
470
+ expect(result.newUserId).toBeDefined();
471
+ expect(result.newUserId.externalId).toBeDefined();
472
+ });
473
+
474
+ it("should use handlerTx with retrieve callback to query data", async () => {
475
+ const result = await usersFragment.inContext(async function () {
476
+ return await this.handlerTx()
477
+ .retrieve(({ forSchema }) =>
478
+ forSchema(usersSchema).find("users", (b) =>
479
+ b.whereIndex("primary", (eb) => eb("id", "=", userId)),
480
+ ),
481
+ )
482
+ .transformRetrieve(([users]) => {
483
+ return { retrieved: true, user: users[0] };
484
+ })
485
+ .execute();
486
+ });
487
+
488
+ expect(result.retrieved).toBe(true);
489
+ expect(result.user).toMatchObject({
490
+ id: expect.objectContaining({ externalId: userId }),
491
+ name: "John Doe",
492
+ });
493
+ });
494
+
495
+ it("should use handlerTx with mutate and transform callbacks", async () => {
496
+ const result = await usersFragment.inContext(async function () {
497
+ return await this.handlerTx()
498
+ .mutate(({ forSchema }) => {
499
+ const newUserId = forSchema(usersSchema).create("users", {
500
+ name: "Success Callback User",
501
+ email: "success-callback@example.com",
502
+ });
503
+ return { newUserId, createdInMutate: true };
504
+ })
505
+ .transform(({ mutateResult }) => {
506
+ return {
507
+ userId: mutateResult.newUserId,
508
+ wasCreatedInMutate: mutateResult.createdInMutate,
509
+ processedAt: new Date().toISOString(),
510
+ };
511
+ })
512
+ .execute();
513
+ });
514
+
515
+ expect(result.userId).toBeDefined();
516
+ expect(result.wasCreatedInMutate).toBe(true);
517
+ expect(result.processedAt).toBeDefined();
518
+ });
519
+
520
+ it("should use handlerTx with retrieve, mutate, and transform callbacks", async () => {
521
+ const result = await ordersFragment.inContext(async function () {
522
+ return await this.handlerTx()
523
+ .retrieve(({ forSchema }) =>
524
+ forSchema(ordersSchema).find("orders", (b) =>
525
+ b.whereIndex("orders_user_idx", (eb) => eb("user_external_id", "=", userId)),
526
+ ),
527
+ )
528
+ .transformRetrieve(([orders]) => {
529
+ return { existingOrders: orders };
530
+ })
531
+ .mutate(({ forSchema, retrieveResult }) => {
532
+ expect(retrieveResult.existingOrders.length).toBeGreaterThan(0);
533
+ const orderId = forSchema(ordersSchema).create("orders", {
534
+ user_external_id: userId,
535
+ product_name: "Full Flow Product",
536
+ quantity: 3,
537
+ total: 3000,
538
+ });
539
+ return { orderId };
540
+ })
541
+ .transform(({ mutateResult, retrieveResult }) => {
542
+ return {
543
+ orderId: mutateResult.orderId,
544
+ hadRetrieve: retrieveResult.existingOrders.length > 0,
545
+ completedAt: new Date().toISOString(),
546
+ };
547
+ })
548
+ .execute();
549
+ });
550
+
551
+ expect(result.orderId).toBeDefined();
552
+ expect(result.hadRetrieve).toBe(true);
553
+ expect(result.completedAt).toBeDefined();
404
554
  });
405
555
  });
406
556
  });