@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.
- package/.turbo/turbo-build.log +34 -30
- package/CHANGELOG.md +49 -0
- package/dist/adapters/generic-sql/query/where-builder.js +1 -1
- package/dist/db-fragment-definition-builder.d.ts +31 -39
- package/dist/db-fragment-definition-builder.d.ts.map +1 -1
- package/dist/db-fragment-definition-builder.js +20 -16
- package/dist/db-fragment-definition-builder.js.map +1 -1
- package/dist/fragments/internal-fragment.d.ts +94 -8
- package/dist/fragments/internal-fragment.d.ts.map +1 -1
- package/dist/fragments/internal-fragment.js +56 -55
- package/dist/fragments/internal-fragment.js.map +1 -1
- package/dist/hooks/hooks.d.ts +5 -3
- package/dist/hooks/hooks.d.ts.map +1 -1
- package/dist/hooks/hooks.js +38 -37
- package/dist/hooks/hooks.js.map +1 -1
- package/dist/mod.d.ts +3 -3
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +4 -4
- package/dist/mod.js.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts +367 -80
- package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/execute-unit-of-work.js +448 -148
- package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.d.ts +35 -11
- package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work/unit-of-work.js +49 -19
- package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
- package/dist/query/value-decoding.js +1 -1
- package/dist/schema/create.d.ts +2 -3
- package/dist/schema/create.d.ts.map +1 -1
- package/dist/schema/create.js +2 -5
- package/dist/schema/create.js.map +1 -1
- package/dist/schema/generate-id.d.ts +20 -0
- package/dist/schema/generate-id.d.ts.map +1 -0
- package/dist/schema/generate-id.js +28 -0
- package/dist/schema/generate-id.js.map +1 -0
- package/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +1 -0
- package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
- package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
- package/src/db-fragment-definition-builder.test.ts +58 -42
- package/src/db-fragment-definition-builder.ts +78 -88
- package/src/db-fragment-instantiator.test.ts +64 -88
- package/src/db-fragment-integration.test.ts +292 -142
- package/src/fragments/internal-fragment.test.ts +272 -266
- package/src/fragments/internal-fragment.ts +155 -122
- package/src/hooks/hooks.test.ts +268 -264
- package/src/hooks/hooks.ts +74 -63
- package/src/mod.ts +14 -4
- package/src/query/unit-of-work/execute-unit-of-work.test.ts +1582 -998
- package/src/query/unit-of-work/execute-unit-of-work.ts +1746 -343
- package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
- package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +269 -21
- package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
- package/src/query/unit-of-work/unit-of-work.ts +65 -30
- package/src/schema/create.ts +2 -5
- package/src/schema/generate-id.test.ts +57 -0
- package/src/schema/generate-id.ts +38 -0
- package/src/shared/config.ts +0 -10
- package/src/shared/connection-pool.ts +0 -24
- 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
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
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(({
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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.
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
396
|
+
|
|
397
|
+
expect(result.user).toMatchObject({
|
|
351
398
|
id: expect.objectContaining({
|
|
352
399
|
externalId: userId,
|
|
353
400
|
}),
|
|
354
401
|
});
|
|
355
402
|
|
|
356
|
-
expect(
|
|
357
|
-
expect(
|
|
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
|
|
366
|
-
let
|
|
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.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
424
|
+
expect(idempotencyKey).toBe(firstIdempotencyKey);
|
|
380
425
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
391
|
-
expect(result.
|
|
392
|
-
expect(typeof result.
|
|
393
|
-
expect(result.
|
|
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
|
|
398
|
-
expect(result.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
});
|