@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.
- package/.turbo/turbo-build.log +179 -132
- package/CHANGELOG.md +30 -0
- package/dist/adapters/adapters.d.ts +27 -1
- package/dist/adapters/adapters.d.ts.map +1 -1
- package/dist/adapters/adapters.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +5 -1
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.js +15 -3
- package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +7 -5
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +76 -44
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +23 -16
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-executor.js +18 -7
- package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -1
- package/dist/adapters/drizzle/generate.d.ts +4 -1
- package/dist/adapters/drizzle/generate.d.ts.map +1 -1
- package/dist/adapters/drizzle/generate.js +11 -18
- package/dist/adapters/drizzle/generate.js.map +1 -1
- package/dist/adapters/drizzle/shared.d.ts +14 -1
- package/dist/adapters/drizzle/shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +5 -1
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.js +14 -3
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
- package/dist/adapters/kysely/kysely-query-builder.js +1 -1
- package/dist/adapters/kysely/kysely-query-compiler.js +3 -2
- package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -1
- package/dist/adapters/kysely/kysely-query.d.ts +1 -0
- package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-query.js +28 -19
- package/dist/adapters/kysely/kysely-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-shared.d.ts +14 -0
- package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-shared.js +16 -1
- package/dist/adapters/kysely/kysely-shared.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-compiler.js +68 -16
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-executor.js +8 -4
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
- package/dist/adapters/kysely/migration/execute-base.js +1 -1
- package/dist/adapters/kysely/migration/execute-base.js.map +1 -1
- package/dist/db-fragment-definition-builder.d.ts +152 -0
- package/dist/db-fragment-definition-builder.d.ts.map +1 -0
- package/dist/db-fragment-definition-builder.js +137 -0
- package/dist/db-fragment-definition-builder.js.map +1 -0
- package/dist/fragments/internal-fragment.d.ts +19 -0
- package/dist/fragments/internal-fragment.d.ts.map +1 -0
- package/dist/fragments/internal-fragment.js +39 -0
- package/dist/fragments/internal-fragment.js.map +1 -0
- package/dist/migration-engine/generation-engine.d.ts.map +1 -1
- package/dist/migration-engine/generation-engine.js +35 -15
- package/dist/migration-engine/generation-engine.js.map +1 -1
- package/dist/mod.d.ts +8 -18
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +7 -34
- package/dist/mod.js.map +1 -1
- package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js +165 -0
- package/dist/node_modules/.pnpm/rou3@0.7.8/node_modules/rou3/dist/index.js.map +1 -0
- package/dist/packages/fragno/dist/api/bind-services.js +20 -0
- package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
- package/dist/packages/fragno/dist/api/error.js +48 -0
- package/dist/packages/fragno/dist/api/error.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
- package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragment-instantiator.js +487 -0
- package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
- package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
- package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
- package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
- package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
- package/dist/packages/fragno/dist/api/internal/route.js +10 -0
- package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
- package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
- package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
- package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
- package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
- package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
- package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
- package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
- package/dist/packages/fragno/dist/api/route.js +17 -0
- package/dist/packages/fragno/dist/api/route.js.map +1 -0
- package/dist/packages/fragno/dist/internal/symbols.js +10 -0
- package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
- package/dist/query/cursor.d.ts +10 -2
- package/dist/query/cursor.d.ts.map +1 -1
- package/dist/query/cursor.js +11 -4
- package/dist/query/cursor.js.map +1 -1
- package/dist/query/execute-unit-of-work.d.ts +123 -0
- package/dist/query/execute-unit-of-work.d.ts.map +1 -0
- package/dist/query/execute-unit-of-work.js +184 -0
- package/dist/query/execute-unit-of-work.js.map +1 -0
- package/dist/query/query.d.ts +3 -3
- package/dist/query/query.d.ts.map +1 -1
- package/dist/query/result-transform.js +4 -2
- package/dist/query/result-transform.js.map +1 -1
- package/dist/query/retry-policy.d.ts +88 -0
- package/dist/query/retry-policy.d.ts.map +1 -0
- package/dist/query/retry-policy.js +61 -0
- package/dist/query/retry-policy.js.map +1 -0
- package/dist/query/unit-of-work.d.ts +171 -32
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +530 -133
- package/dist/query/unit-of-work.js.map +1 -1
- package/dist/schema/serialize.js +12 -7
- package/dist/schema/serialize.js.map +1 -1
- package/dist/with-database.d.ts +28 -0
- package/dist/with-database.d.ts.map +1 -0
- package/dist/with-database.js +34 -0
- package/dist/with-database.js.map +1 -0
- package/package.json +10 -3
- package/src/adapters/adapters.ts +30 -0
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +86 -17
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +291 -7
- package/src/adapters/drizzle/drizzle-adapter.test.ts +3 -51
- package/src/adapters/drizzle/drizzle-adapter.ts +35 -7
- package/src/adapters/drizzle/drizzle-query.ts +25 -15
- package/src/adapters/drizzle/drizzle-uow-compiler-mysql.test.ts +1442 -0
- package/src/adapters/drizzle/drizzle-uow-compiler-sqlite.test.ts +1414 -0
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +78 -61
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +123 -42
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +34 -27
- package/src/adapters/drizzle/drizzle-uow-executor.ts +41 -8
- package/src/adapters/drizzle/generate.test.ts +102 -269
- package/src/adapters/drizzle/generate.ts +12 -30
- package/src/adapters/drizzle/test-utils.ts +36 -5
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +66 -22
- package/src/adapters/kysely/kysely-adapter-sqlite.test.ts +156 -0
- package/src/adapters/kysely/kysely-adapter.ts +25 -2
- package/src/adapters/kysely/kysely-query-compiler.ts +3 -8
- package/src/adapters/kysely/kysely-query.ts +57 -37
- package/src/adapters/kysely/kysely-shared.ts +34 -0
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -74
- package/src/adapters/kysely/kysely-uow-compiler.ts +92 -24
- package/src/adapters/kysely/kysely-uow-executor.ts +26 -7
- package/src/adapters/kysely/kysely-uow-joins.test.ts +33 -50
- package/src/adapters/kysely/migration/execute-base.ts +1 -1
- package/src/db-fragment-definition-builder.test.ts +887 -0
- package/src/db-fragment-definition-builder.ts +506 -0
- package/src/db-fragment-instantiator.test.ts +467 -0
- package/src/db-fragment-integration.test.ts +408 -0
- package/src/fragments/internal-fragment.test.ts +160 -0
- package/src/fragments/internal-fragment.ts +85 -0
- package/src/migration-engine/generation-engine.test.ts +58 -15
- package/src/migration-engine/generation-engine.ts +78 -25
- package/src/mod.ts +35 -43
- package/src/query/cursor.test.ts +119 -0
- package/src/query/cursor.ts +17 -4
- package/src/query/execute-unit-of-work.test.ts +1310 -0
- package/src/query/execute-unit-of-work.ts +463 -0
- package/src/query/query.ts +4 -4
- package/src/query/result-transform.test.ts +129 -0
- package/src/query/result-transform.ts +4 -1
- package/src/query/retry-policy.test.ts +217 -0
- package/src/query/retry-policy.ts +141 -0
- package/src/query/unit-of-work-coordinator.test.ts +833 -0
- package/src/query/unit-of-work-types.test.ts +15 -2
- package/src/query/unit-of-work.test.ts +878 -200
- package/src/query/unit-of-work.ts +963 -321
- package/src/schema/serialize.ts +22 -11
- package/src/with-database.ts +140 -0
- package/tsdown.config.ts +1 -0
- package/dist/fragment.d.ts +0 -54
- package/dist/fragment.d.ts.map +0 -1
- package/dist/fragment.js +0 -92
- package/dist/fragment.js.map +0 -1
- package/dist/shared/settings-schema.js +0 -36
- package/dist/shared/settings-schema.js.map +0 -1
- package/src/fragment.test.ts +0 -341
- package/src/fragment.ts +0 -198
- package/src/shared/settings-schema.ts +0 -61
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { schema, idColumn, type FragnoId, referenceColumn } from "../schema/create";
|
|
3
|
+
import {
|
|
4
|
+
type UOWCompiler,
|
|
5
|
+
type UOWDecoder,
|
|
6
|
+
createUnitOfWork,
|
|
7
|
+
type RetrievalOperation,
|
|
8
|
+
type MutationOperation,
|
|
9
|
+
type CompiledMutation,
|
|
10
|
+
} from "./unit-of-work";
|
|
11
|
+
import type { AnySchema } from "../schema/create";
|
|
12
|
+
|
|
13
|
+
// Mock compiler that tracks operations
|
|
14
|
+
function createMockCompiler(): UOWCompiler<string> {
|
|
15
|
+
return {
|
|
16
|
+
compileRetrievalOperation: (op: RetrievalOperation<AnySchema>) => {
|
|
17
|
+
return `SELECT from ${op.table.ormName}`;
|
|
18
|
+
},
|
|
19
|
+
compileMutationOperation: (op: MutationOperation<AnySchema>) => {
|
|
20
|
+
return {
|
|
21
|
+
query: `${op.type.toUpperCase()} ${op.table}`,
|
|
22
|
+
expectedAffectedRows: op.type === "create" ? null : op.type === "check" ? null : 1,
|
|
23
|
+
expectedReturnedRows: op.type === "check" ? 1 : null,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Mock executor that tracks execution
|
|
30
|
+
function createMockExecutor() {
|
|
31
|
+
const executionLog: string[] = [];
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
executeRetrievalPhase: async (queries: string[]) => {
|
|
35
|
+
executionLog.push(`RETRIEVAL: ${queries.length} queries`);
|
|
36
|
+
// Return mock results for each query
|
|
37
|
+
return queries.map(() => [{ id: "mock-id", name: "Mock User" }]);
|
|
38
|
+
},
|
|
39
|
+
executeMutationPhase: async (mutations: CompiledMutation<string>[]) => {
|
|
40
|
+
executionLog.push(`MUTATION: ${mutations.length} mutations`);
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
createdInternalIds: mutations.map(() => BigInt(Math.floor(Math.random() * 1000))),
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
getLog: () => executionLog,
|
|
47
|
+
clearLog: () => {
|
|
48
|
+
executionLog.length = 0;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createMockDecoder(): UOWDecoder {
|
|
54
|
+
return (rawResults, operations) => {
|
|
55
|
+
if (rawResults.length !== operations.length) {
|
|
56
|
+
throw new Error("rawResults and operations must have the same length");
|
|
57
|
+
}
|
|
58
|
+
return rawResults;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("UOW Coordinator - Parent-Child Execution", () => {
|
|
63
|
+
it("should allow child UOWs to add operations and parent to execute them", async () => {
|
|
64
|
+
const testSchema = schema((s) =>
|
|
65
|
+
s.addTable("users", (t) =>
|
|
66
|
+
t.addColumn("id", idColumn()).addColumn("name", "string").addColumn("email", "string"),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const executor = createMockExecutor();
|
|
71
|
+
const parentUow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
72
|
+
|
|
73
|
+
// Simulate service method 1: adds retrieval operation via child UOW
|
|
74
|
+
const serviceMethod1 = () => {
|
|
75
|
+
const childUow = parentUow.restrict();
|
|
76
|
+
childUow.forSchema(testSchema).find("users", (b) => b.whereIndex("primary"));
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Simulate service method 2: adds mutation operation via child UOW
|
|
80
|
+
const serviceMethod2 = () => {
|
|
81
|
+
const childUow = parentUow.restrict();
|
|
82
|
+
childUow.forSchema(testSchema).create("users", { name: "Alice", email: "alice@example.com" });
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Call both service methods
|
|
86
|
+
serviceMethod1();
|
|
87
|
+
serviceMethod2();
|
|
88
|
+
|
|
89
|
+
// Parent should see both operations
|
|
90
|
+
expect(parentUow.getRetrievalOperations()).toHaveLength(1);
|
|
91
|
+
expect(parentUow.getMutationOperations()).toHaveLength(1);
|
|
92
|
+
|
|
93
|
+
// Execute retrieval phase
|
|
94
|
+
const results = await parentUow.executeRetrieve();
|
|
95
|
+
expect(results).toHaveLength(1);
|
|
96
|
+
|
|
97
|
+
// Execute mutation phase
|
|
98
|
+
const mutationResult = await parentUow.executeMutations();
|
|
99
|
+
expect(mutationResult.success).toBe(true);
|
|
100
|
+
|
|
101
|
+
// Verify execution happened
|
|
102
|
+
const log = executor.getLog();
|
|
103
|
+
expect(log).toEqual(["RETRIEVAL: 1 queries", "MUTATION: 1 mutations"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should handle nested service calls that await phase promises without deadlock", async () => {
|
|
107
|
+
const testSchema = schema((s) =>
|
|
108
|
+
s
|
|
109
|
+
.addTable("users", (t) =>
|
|
110
|
+
t.addColumn("id", idColumn()).addColumn("name", "string").addColumn("email", "string"),
|
|
111
|
+
)
|
|
112
|
+
.addTable("posts", (t) =>
|
|
113
|
+
t
|
|
114
|
+
.addColumn("id", idColumn())
|
|
115
|
+
.addColumn("userId", "string")
|
|
116
|
+
.addColumn("title", "string")
|
|
117
|
+
.addColumn("content", "string")
|
|
118
|
+
.createIndex("idx_user", ["userId"]),
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const executor = createMockExecutor();
|
|
123
|
+
const parentUow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
124
|
+
|
|
125
|
+
// Service A: Get user by ID, awaits retrieval phase
|
|
126
|
+
const getUserById = async (userId: string) => {
|
|
127
|
+
const childUow = parentUow.restrict();
|
|
128
|
+
const typedUow = childUow
|
|
129
|
+
.forSchema(testSchema)
|
|
130
|
+
.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)));
|
|
131
|
+
|
|
132
|
+
// Await retrieval phase - should not deadlock!
|
|
133
|
+
const [users] = await typedUow.retrievalPhase;
|
|
134
|
+
return users?.[0] ?? null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Service B: Get posts by user ID, awaits retrieval phase
|
|
138
|
+
const getPostsByUserId = async (userId: string) => {
|
|
139
|
+
const childUow = parentUow.restrict();
|
|
140
|
+
const typedUow = childUow
|
|
141
|
+
.forSchema(testSchema)
|
|
142
|
+
.find("posts", (b) => b.whereIndex("idx_user", (eb) => eb("userId", "=", userId)));
|
|
143
|
+
|
|
144
|
+
// Await retrieval phase - should not deadlock!
|
|
145
|
+
const [posts] = await typedUow.retrievalPhase;
|
|
146
|
+
return posts;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Handler: Orchestrates multiple service calls that each await phase promises
|
|
150
|
+
const handler = async () => {
|
|
151
|
+
// Both services add retrieval operations and return promises that await retrievalPhase
|
|
152
|
+
const userPromise = getUserById("user-123");
|
|
153
|
+
const postsPromise = getPostsByUserId("user-123");
|
|
154
|
+
|
|
155
|
+
// Execute retrieval phase - this should resolve both service promises
|
|
156
|
+
await parentUow.executeRetrieve();
|
|
157
|
+
|
|
158
|
+
// Now we can await the service results
|
|
159
|
+
const user = await userPromise;
|
|
160
|
+
const posts = await postsPromise;
|
|
161
|
+
|
|
162
|
+
return { user, posts };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Execute handler
|
|
166
|
+
const result = await handler();
|
|
167
|
+
|
|
168
|
+
// Verify results
|
|
169
|
+
expect(result.user).toEqual({ id: "mock-id", name: "Mock User" });
|
|
170
|
+
expect(result.posts).toEqual([{ id: "mock-id", name: "Mock User" }]);
|
|
171
|
+
|
|
172
|
+
// Verify both retrieval operations were registered
|
|
173
|
+
expect(parentUow.getRetrievalOperations()).toHaveLength(2);
|
|
174
|
+
|
|
175
|
+
// Verify execution happened
|
|
176
|
+
const log = executor.getLog();
|
|
177
|
+
expect(log).toEqual(["RETRIEVAL: 2 queries"]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should handle retrieval-to-mutation flow with service composition", async () => {
|
|
181
|
+
const testSchema = schema((s) =>
|
|
182
|
+
s
|
|
183
|
+
.addTable("users", (t) =>
|
|
184
|
+
t
|
|
185
|
+
.addColumn("id", idColumn())
|
|
186
|
+
.addColumn("name", "string")
|
|
187
|
+
.addColumn("email", "string")
|
|
188
|
+
.addColumn("status", "string"),
|
|
189
|
+
)
|
|
190
|
+
.addTable("orders", (t) =>
|
|
191
|
+
t
|
|
192
|
+
.addColumn("id", idColumn())
|
|
193
|
+
.addColumn("userId", "string")
|
|
194
|
+
.addColumn("total", "integer")
|
|
195
|
+
.addColumn("status", "string")
|
|
196
|
+
.createIndex("idx_user", ["userId"]),
|
|
197
|
+
)
|
|
198
|
+
.addTable("payments", (t) =>
|
|
199
|
+
t
|
|
200
|
+
.addColumn("id", idColumn())
|
|
201
|
+
.addColumn("orderId", "string")
|
|
202
|
+
.addColumn("amount", "integer")
|
|
203
|
+
.createIndex("idx_order", ["orderId"]),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const executor = createMockExecutor();
|
|
208
|
+
const parentUow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
209
|
+
|
|
210
|
+
// Service A: Check if user exists
|
|
211
|
+
const validateUser = async (userId: string) => {
|
|
212
|
+
const childUow = parentUow.restrict();
|
|
213
|
+
const typedUow = childUow
|
|
214
|
+
.forSchema(testSchema)
|
|
215
|
+
.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)));
|
|
216
|
+
|
|
217
|
+
const [users] = await typedUow.retrievalPhase;
|
|
218
|
+
const user = users?.[0];
|
|
219
|
+
|
|
220
|
+
if (!user) {
|
|
221
|
+
throw new Error("User not found");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return user;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Service B: Get user's pending orders
|
|
228
|
+
const getPendingOrders = async (userId: string) => {
|
|
229
|
+
const childUow = parentUow.restrict();
|
|
230
|
+
const typedUow = childUow
|
|
231
|
+
.forSchema(testSchema)
|
|
232
|
+
.find("orders", (b) => b.whereIndex("idx_user", (eb) => eb("userId", "=", userId)));
|
|
233
|
+
|
|
234
|
+
const [orders] = await typedUow.retrievalPhase;
|
|
235
|
+
return orders;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Service C: Create order and payment (uses validation results)
|
|
239
|
+
const createOrderWithPayment = (userId: string, amount: number) => {
|
|
240
|
+
const childUow = parentUow.restrict();
|
|
241
|
+
const typedUow = childUow.forSchema(testSchema);
|
|
242
|
+
|
|
243
|
+
// Create order
|
|
244
|
+
const orderId = typedUow.create("orders", {
|
|
245
|
+
userId,
|
|
246
|
+
total: amount,
|
|
247
|
+
status: "pending",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Create payment for the order
|
|
251
|
+
typedUow.create("payments", {
|
|
252
|
+
orderId: orderId.externalId,
|
|
253
|
+
amount,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Return the ID immediately - don't await mutation phase here
|
|
257
|
+
// The handler will execute the mutation phase
|
|
258
|
+
return orderId;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Handler: Orchestrates validation, retrieval, and mutations
|
|
262
|
+
// Pattern: validate → retrieve data → use data to drive mutations
|
|
263
|
+
const handler = async () => {
|
|
264
|
+
// Phase 1: Retrieval - validate user and get existing orders
|
|
265
|
+
const userPromise = validateUser("user-123");
|
|
266
|
+
const ordersPromise = getPendingOrders("user-123");
|
|
267
|
+
|
|
268
|
+
// Execute retrieval phase - resolves all service promises
|
|
269
|
+
await parentUow.executeRetrieve();
|
|
270
|
+
|
|
271
|
+
const user = await userPromise;
|
|
272
|
+
const existingOrders = await ordersPromise;
|
|
273
|
+
|
|
274
|
+
// Business logic: Only allow order if user has < 5 pending orders
|
|
275
|
+
if (existingOrders.length >= 5) {
|
|
276
|
+
throw new Error("Too many pending orders");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Phase 2: Mutation - create new order based on retrieval results
|
|
280
|
+
const orderId = createOrderWithPayment("user-123", 9999);
|
|
281
|
+
|
|
282
|
+
// Execute mutation phase
|
|
283
|
+
await parentUow.executeMutations();
|
|
284
|
+
|
|
285
|
+
return { user, orderId, existingOrderCount: existingOrders.length };
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Execute handler
|
|
289
|
+
const result = await handler();
|
|
290
|
+
|
|
291
|
+
// Verify results
|
|
292
|
+
expect(result.user).toEqual({ id: "mock-id", name: "Mock User" });
|
|
293
|
+
expect(result.orderId.externalId).toBeTruthy();
|
|
294
|
+
expect(result.existingOrderCount).toBe(1);
|
|
295
|
+
|
|
296
|
+
// Verify operations were registered
|
|
297
|
+
// 1 user validation + 1 orders query = 2 retrieval operations
|
|
298
|
+
expect(parentUow.getRetrievalOperations()).toHaveLength(2);
|
|
299
|
+
// 1 order create + 1 payment create = 2 mutation operations
|
|
300
|
+
expect(parentUow.getMutationOperations()).toHaveLength(2);
|
|
301
|
+
|
|
302
|
+
// Verify execution order
|
|
303
|
+
const log = executor.getLog();
|
|
304
|
+
expect(log).toEqual(["RETRIEVAL: 2 queries", "MUTATION: 2 mutations"]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should handle deeply nested child UOWs (3+ levels)", async () => {
|
|
308
|
+
const testSchema = schema((s) =>
|
|
309
|
+
s
|
|
310
|
+
.addTable("users", (t) =>
|
|
311
|
+
t.addColumn("id", idColumn()).addColumn("name", "string").addColumn("email", "string"),
|
|
312
|
+
)
|
|
313
|
+
.addTable("posts", (t) =>
|
|
314
|
+
t
|
|
315
|
+
.addColumn("id", idColumn())
|
|
316
|
+
.addColumn("userId", "string")
|
|
317
|
+
.addColumn("title", "string")
|
|
318
|
+
.createIndex("idx_user", ["userId"]),
|
|
319
|
+
)
|
|
320
|
+
.addTable("comments", (t) =>
|
|
321
|
+
t
|
|
322
|
+
.addColumn("id", idColumn())
|
|
323
|
+
.addColumn("postId", "string")
|
|
324
|
+
.addColumn("content", "string")
|
|
325
|
+
.createIndex("idx_post", ["postId"]),
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const executor = createMockExecutor();
|
|
330
|
+
const parentUow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
331
|
+
|
|
332
|
+
// Level 1: Handler (root)
|
|
333
|
+
const handler = async () => {
|
|
334
|
+
// Level 2: Service A - orchestrates user operations
|
|
335
|
+
const getUserWithPosts = async (userId: string) => {
|
|
336
|
+
const childUow1 = parentUow.restrict();
|
|
337
|
+
|
|
338
|
+
// Level 3: Service B - gets just the user
|
|
339
|
+
const getUser = async () => {
|
|
340
|
+
const childUow2 = childUow1.restrict();
|
|
341
|
+
const typedUow = childUow2
|
|
342
|
+
.forSchema(testSchema)
|
|
343
|
+
.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)));
|
|
344
|
+
|
|
345
|
+
const [users] = await typedUow.retrievalPhase;
|
|
346
|
+
return users?.[0];
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Level 3: Service C - gets user's posts
|
|
350
|
+
const getUserPosts = async () => {
|
|
351
|
+
const childUow2 = childUow1.restrict();
|
|
352
|
+
const typedUow = childUow2
|
|
353
|
+
.forSchema(testSchema)
|
|
354
|
+
.find("posts", (b) => b.whereIndex("idx_user", (eb) => eb("userId", "=", userId)));
|
|
355
|
+
|
|
356
|
+
const [posts] = await typedUow.retrievalPhase;
|
|
357
|
+
return posts;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Await both nested services
|
|
361
|
+
const userPromise = getUser();
|
|
362
|
+
const postsPromise = getUserPosts();
|
|
363
|
+
|
|
364
|
+
// Execute retrieval at this level - should resolve nested promises
|
|
365
|
+
await parentUow.executeRetrieve();
|
|
366
|
+
|
|
367
|
+
const user = await userPromise;
|
|
368
|
+
const posts = await postsPromise;
|
|
369
|
+
|
|
370
|
+
return { user, posts };
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Call service A and get results
|
|
374
|
+
const { user, posts } = await getUserWithPosts("user-123");
|
|
375
|
+
|
|
376
|
+
// Level 2: Service D - creates comments for the posts
|
|
377
|
+
const createComment = (postId: string, content: string) => {
|
|
378
|
+
const childUow1 = parentUow.restrict();
|
|
379
|
+
|
|
380
|
+
// Level 3: Service E - validates post exists (hypothetically)
|
|
381
|
+
// In real code this might query, but here we just create
|
|
382
|
+
const childUow2 = childUow1.restrict();
|
|
383
|
+
const typedUow2 = childUow2.forSchema(testSchema);
|
|
384
|
+
|
|
385
|
+
typedUow2.create("comments", { postId, content });
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Create comments for each post
|
|
389
|
+
if (posts && posts.length > 0) {
|
|
390
|
+
for (const post of posts) {
|
|
391
|
+
// Use string coercion since mock data returns string ids
|
|
392
|
+
createComment(String(post.id), "Great post!");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Execute mutations
|
|
397
|
+
await parentUow.executeMutations();
|
|
398
|
+
|
|
399
|
+
return { user, postCount: posts?.length ?? 0 };
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Execute handler
|
|
403
|
+
const result = await handler();
|
|
404
|
+
|
|
405
|
+
// Verify results
|
|
406
|
+
expect(result.user).toBeDefined();
|
|
407
|
+
expect(result.postCount).toBe(1);
|
|
408
|
+
|
|
409
|
+
// Verify operations were registered at root level
|
|
410
|
+
// 1 user query + 1 posts query = 2 retrieval operations
|
|
411
|
+
expect(parentUow.getRetrievalOperations()).toHaveLength(2);
|
|
412
|
+
// 1 comment create = 1 mutation operation
|
|
413
|
+
expect(parentUow.getMutationOperations()).toHaveLength(1);
|
|
414
|
+
|
|
415
|
+
// Verify execution happened in correct order
|
|
416
|
+
const log = executor.getLog();
|
|
417
|
+
expect(log).toEqual(["RETRIEVAL: 2 queries", "MUTATION: 1 mutations"]);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should handle sibling child UOWs at same nesting level", async () => {
|
|
421
|
+
const testSchema = schema((s) =>
|
|
422
|
+
s
|
|
423
|
+
.addTable("users", (t) =>
|
|
424
|
+
t
|
|
425
|
+
.addColumn("id", idColumn())
|
|
426
|
+
.addColumn("name", "string")
|
|
427
|
+
.addColumn("email", "string")
|
|
428
|
+
.createIndex("idx_email", ["email"]),
|
|
429
|
+
)
|
|
430
|
+
.addTable("products", (t) =>
|
|
431
|
+
t.addColumn("id", idColumn()).addColumn("name", "string").addColumn("price", "integer"),
|
|
432
|
+
)
|
|
433
|
+
.addTable("orders", (t) =>
|
|
434
|
+
t
|
|
435
|
+
.addColumn("id", idColumn())
|
|
436
|
+
.addColumn("userId", "string")
|
|
437
|
+
.addColumn("productId", "string")
|
|
438
|
+
.addColumn("quantity", "integer"),
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
const executor = createMockExecutor();
|
|
443
|
+
const parentUow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
444
|
+
|
|
445
|
+
// Service method that creates TWO sibling child UOWs
|
|
446
|
+
const processUserOrder = async (email: string) => {
|
|
447
|
+
// Sibling 1: Look up user by email
|
|
448
|
+
const findUserByEmail = () => {
|
|
449
|
+
const childUow = parentUow.restrict();
|
|
450
|
+
const typedUow = childUow
|
|
451
|
+
.forSchema(testSchema)
|
|
452
|
+
.find("users", (b) => b.whereIndex("idx_email", (eb) => eb("email", "=", email)));
|
|
453
|
+
|
|
454
|
+
return typedUow.retrievalPhase.then(([users]) => users?.[0] ?? null);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// Sibling 2: Look up product by scanning (for simplicity)
|
|
458
|
+
const findProductByName = () => {
|
|
459
|
+
const childUow = parentUow.restrict();
|
|
460
|
+
const typedUow = childUow
|
|
461
|
+
.forSchema(testSchema)
|
|
462
|
+
.find("products", (b) => b.whereIndex("primary"));
|
|
463
|
+
|
|
464
|
+
return typedUow.retrievalPhase.then(([products]) => products?.[0] ?? null);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// Create both sibling UOWs at the same time
|
|
468
|
+
const userPromise = findUserByEmail();
|
|
469
|
+
const productPromise = findProductByName();
|
|
470
|
+
|
|
471
|
+
// Execute retrieval - should resolve both siblings
|
|
472
|
+
await parentUow.executeRetrieve();
|
|
473
|
+
|
|
474
|
+
const user = await userPromise;
|
|
475
|
+
const product = await productPromise;
|
|
476
|
+
|
|
477
|
+
if (!user || !product) {
|
|
478
|
+
throw new Error("User or product not found");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Sibling 3: Create order using the results
|
|
482
|
+
const createOrder = () => {
|
|
483
|
+
const childUow = parentUow.restrict();
|
|
484
|
+
const typedUow = childUow.forSchema(testSchema);
|
|
485
|
+
|
|
486
|
+
return typedUow.create("orders", {
|
|
487
|
+
userId: String(user.id),
|
|
488
|
+
productId: String(product.id),
|
|
489
|
+
quantity: 1,
|
|
490
|
+
});
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Sibling 4: Update user (hypothetically, just another mutation)
|
|
494
|
+
const updateUserEmail = () => {
|
|
495
|
+
const childUow = parentUow.restrict();
|
|
496
|
+
const typedUow = childUow.forSchema(testSchema);
|
|
497
|
+
|
|
498
|
+
typedUow.update("users", user.id, (b) =>
|
|
499
|
+
b.set({ email: `updated-${email}`, name: user.name }),
|
|
500
|
+
);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Execute mutations from both siblings
|
|
504
|
+
createOrder();
|
|
505
|
+
updateUserEmail();
|
|
506
|
+
|
|
507
|
+
await parentUow.executeMutations();
|
|
508
|
+
|
|
509
|
+
return { user, product };
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// Execute the service
|
|
513
|
+
const result = await processUserOrder("test@example.com");
|
|
514
|
+
|
|
515
|
+
// Verify results
|
|
516
|
+
expect(result.user).toBeDefined();
|
|
517
|
+
expect(result.product).toBeDefined();
|
|
518
|
+
|
|
519
|
+
// Verify operations registered from all siblings
|
|
520
|
+
// Sibling 1 (user lookup) + Sibling 2 (product lookup) = 2 retrieval operations
|
|
521
|
+
expect(parentUow.getRetrievalOperations()).toHaveLength(2);
|
|
522
|
+
// Sibling 3 (order create) + Sibling 4 (user update) = 2 mutation operations
|
|
523
|
+
expect(parentUow.getMutationOperations()).toHaveLength(2);
|
|
524
|
+
|
|
525
|
+
// Verify execution order
|
|
526
|
+
const log = executor.getLog();
|
|
527
|
+
expect(log).toEqual(["RETRIEVAL: 2 queries", "MUTATION: 2 mutations"]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("should support transaction rollback pattern", async () => {
|
|
531
|
+
const testSchema = schema((s) =>
|
|
532
|
+
s
|
|
533
|
+
.addTable("accounts", (t) =>
|
|
534
|
+
t
|
|
535
|
+
.addColumn("id", idColumn())
|
|
536
|
+
.addColumn("userId", "string")
|
|
537
|
+
.addColumn("balance", "integer")
|
|
538
|
+
.createIndex("idx_user", ["userId"]),
|
|
539
|
+
)
|
|
540
|
+
.addTable("transactions", (t) =>
|
|
541
|
+
t
|
|
542
|
+
.addColumn("id", idColumn())
|
|
543
|
+
.addColumn("fromAccountId", referenceColumn())
|
|
544
|
+
.addColumn("toAccountId", referenceColumn())
|
|
545
|
+
.addColumn("amount", "integer"),
|
|
546
|
+
),
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// Create mock executor that returns account data with low balance
|
|
550
|
+
const executionLog: string[] = [];
|
|
551
|
+
const customExecutor = {
|
|
552
|
+
executeRetrievalPhase: async (queries: string[]) => {
|
|
553
|
+
executionLog.push(`RETRIEVAL: ${queries.length} queries`);
|
|
554
|
+
// Return mock account data with balance field set to low value
|
|
555
|
+
return queries.map(() => [{ id: "mock-id", userId: "user-1", balance: 100 }]);
|
|
556
|
+
},
|
|
557
|
+
executeMutationPhase: async (mutations: CompiledMutation<string>[]) => {
|
|
558
|
+
executionLog.push(`MUTATION: ${mutations.length} mutations`);
|
|
559
|
+
return {
|
|
560
|
+
success: true,
|
|
561
|
+
createdInternalIds: mutations.map(() => BigInt(Math.floor(Math.random() * 1000))),
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const parentUow = createUnitOfWork(createMockCompiler(), customExecutor, createMockDecoder());
|
|
567
|
+
|
|
568
|
+
// Service: Get account balance
|
|
569
|
+
const getAccountBalance = async (userId: string) => {
|
|
570
|
+
const childUow = parentUow.restrict();
|
|
571
|
+
const typedUow = childUow
|
|
572
|
+
.forSchema(testSchema)
|
|
573
|
+
.find("accounts", (b) => b.whereIndex("idx_user", (eb) => eb("userId", "=", userId)));
|
|
574
|
+
|
|
575
|
+
const [accounts] = await typedUow.retrievalPhase;
|
|
576
|
+
return accounts?.[0] ?? null;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// Service: Create transfer (mutation)
|
|
580
|
+
const createTransfer = (fromAccountId: FragnoId, toAccountId: FragnoId, amount: number) => {
|
|
581
|
+
const childUow = parentUow.restrict();
|
|
582
|
+
const typedUow = childUow.forSchema(testSchema);
|
|
583
|
+
|
|
584
|
+
// Check that both accounts haven't changed since retrieval
|
|
585
|
+
typedUow.check("accounts", fromAccountId);
|
|
586
|
+
typedUow.check("accounts", toAccountId);
|
|
587
|
+
|
|
588
|
+
return typedUow.create("transactions", {
|
|
589
|
+
fromAccountId,
|
|
590
|
+
toAccountId,
|
|
591
|
+
amount,
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// Handler: Attempt transfer but abort if insufficient funds
|
|
596
|
+
const attemptTransfer = async (fromUserId: string, toUserId: string, amount: number) => {
|
|
597
|
+
// Phase 1: Retrieval - get both accounts
|
|
598
|
+
const fromAccountPromise = getAccountBalance(fromUserId);
|
|
599
|
+
const toAccountPromise = getAccountBalance(toUserId);
|
|
600
|
+
|
|
601
|
+
// Execute retrieval phase
|
|
602
|
+
await parentUow.executeRetrieve();
|
|
603
|
+
|
|
604
|
+
const fromAccount = await fromAccountPromise;
|
|
605
|
+
const toAccount = await toAccountPromise;
|
|
606
|
+
|
|
607
|
+
// Validation: Check if accounts exist
|
|
608
|
+
if (!fromAccount || !toAccount) {
|
|
609
|
+
throw new Error("One or both accounts not found");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Validation: Check if sufficient balance (this should fail in our test)
|
|
613
|
+
if (fromAccount.balance < amount) {
|
|
614
|
+
throw new Error("Insufficient funds");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Phase 2: Mutation - would create transfer, but we never get here
|
|
618
|
+
createTransfer(fromAccount.id, toAccount.id, amount);
|
|
619
|
+
await parentUow.executeMutations();
|
|
620
|
+
|
|
621
|
+
return { success: true };
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// Execute handler - expect it to throw due to insufficient funds
|
|
625
|
+
await expect(attemptTransfer("user-1", "user-2", 10000)).rejects.toThrow("Insufficient funds");
|
|
626
|
+
|
|
627
|
+
// Verify retrieval phase was executed
|
|
628
|
+
expect(parentUow.getRetrievalOperations()).toHaveLength(2);
|
|
629
|
+
|
|
630
|
+
// Verify NO mutations were executed (rollback pattern)
|
|
631
|
+
expect(parentUow.getMutationOperations()).toHaveLength(0);
|
|
632
|
+
|
|
633
|
+
// Verify only retrieval phase was executed, no mutations
|
|
634
|
+
expect(executionLog).toEqual(["RETRIEVAL: 2 queries"]);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("should handle errors thrown by service methods without unhandled rejections", async () => {
|
|
638
|
+
const testSchema = schema((s) =>
|
|
639
|
+
s
|
|
640
|
+
.addTable("users", (t) =>
|
|
641
|
+
t
|
|
642
|
+
.addColumn("id", idColumn())
|
|
643
|
+
.addColumn("name", "string")
|
|
644
|
+
.addColumn("email", "string")
|
|
645
|
+
.addColumn("status", "string"),
|
|
646
|
+
)
|
|
647
|
+
.addTable("posts", (t) =>
|
|
648
|
+
t
|
|
649
|
+
.addColumn("id", idColumn())
|
|
650
|
+
.addColumn("userId", "string")
|
|
651
|
+
.addColumn("title", "string")
|
|
652
|
+
.createIndex("idx_user", ["userId"]),
|
|
653
|
+
),
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const executor = createMockExecutor();
|
|
657
|
+
const parentUow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
658
|
+
|
|
659
|
+
// Service A: Validates user and throws if not active
|
|
660
|
+
const validateActiveUser = async (userId: string) => {
|
|
661
|
+
const childUow = parentUow.restrict();
|
|
662
|
+
const typedUow = childUow
|
|
663
|
+
.forSchema(testSchema)
|
|
664
|
+
.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)));
|
|
665
|
+
|
|
666
|
+
const [users] = await typedUow.retrievalPhase;
|
|
667
|
+
const user = users?.[0];
|
|
668
|
+
|
|
669
|
+
if (!user) {
|
|
670
|
+
throw new Error("User not found");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Mock check: assume user status is "inactive"
|
|
674
|
+
if (user.name === "Mock User") {
|
|
675
|
+
throw new Error("User is not active");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return user;
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// Service B: Gets user posts (won't be reached due to validation error)
|
|
682
|
+
const getUserPosts = async (userId: string) => {
|
|
683
|
+
const childUow = parentUow.restrict();
|
|
684
|
+
const typedUow = childUow
|
|
685
|
+
.forSchema(testSchema)
|
|
686
|
+
.find("posts", (b) => b.whereIndex("idx_user", (eb) => eb("userId", "=", userId)));
|
|
687
|
+
|
|
688
|
+
const [posts] = await typedUow.retrievalPhase;
|
|
689
|
+
return posts;
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// Handler: Orchestrates service calls that may throw
|
|
693
|
+
const handler = async () => {
|
|
694
|
+
// Both services add retrieval operations
|
|
695
|
+
const userPromise = validateActiveUser("user-123");
|
|
696
|
+
const postsPromise = getUserPosts("user-123");
|
|
697
|
+
|
|
698
|
+
// Execute retrieval phase - this resolves the retrievalPhase promises
|
|
699
|
+
await parentUow.executeRetrieve();
|
|
700
|
+
|
|
701
|
+
// Now await the service results - validateActiveUser will throw
|
|
702
|
+
const user = await userPromise; // This will throw "User is not active"
|
|
703
|
+
const posts = await postsPromise; // Won't be reached
|
|
704
|
+
|
|
705
|
+
return { user, posts };
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// Execute handler and expect it to throw
|
|
709
|
+
await expect(handler()).rejects.toThrow("User is not active");
|
|
710
|
+
|
|
711
|
+
// Verify retrieval phase was executed (both operations were registered)
|
|
712
|
+
expect(parentUow.getRetrievalOperations()).toHaveLength(2);
|
|
713
|
+
|
|
714
|
+
// Verify execution happened
|
|
715
|
+
const log = executor.getLog();
|
|
716
|
+
expect(log).toEqual(["RETRIEVAL: 2 queries"]);
|
|
717
|
+
|
|
718
|
+
// No mutations should have been added since we threw during retrieval validation
|
|
719
|
+
expect(parentUow.getMutationOperations()).toHaveLength(0);
|
|
720
|
+
|
|
721
|
+
// Give Node.js event loop time to detect any unhandled rejections
|
|
722
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
723
|
+
|
|
724
|
+
// If we got here without Node.js throwing an unhandled rejection, the test passes
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("should inherit nonce from parent to children for idempotent operations", () => {
|
|
728
|
+
const executor = createMockExecutor();
|
|
729
|
+
const parentUow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
730
|
+
|
|
731
|
+
// Parent UOW should have a nonce
|
|
732
|
+
const parentNonce = parentUow.nonce;
|
|
733
|
+
expect(parentNonce).toBeDefined();
|
|
734
|
+
expect(typeof parentNonce).toBe("string");
|
|
735
|
+
expect(parentNonce.length).toBeGreaterThan(0);
|
|
736
|
+
|
|
737
|
+
// Create first child
|
|
738
|
+
const child1 = parentUow.restrict();
|
|
739
|
+
expect(child1.nonce).toBe(parentNonce);
|
|
740
|
+
|
|
741
|
+
// Create second child (sibling to child1)
|
|
742
|
+
const child2 = parentUow.restrict();
|
|
743
|
+
expect(child2.nonce).toBe(parentNonce);
|
|
744
|
+
|
|
745
|
+
// Create nested child (child of child1)
|
|
746
|
+
const grandchild = child1.restrict();
|
|
747
|
+
expect(grandchild.nonce).toBe(parentNonce);
|
|
748
|
+
|
|
749
|
+
// All UOWs in the hierarchy should share the same nonce
|
|
750
|
+
expect(parentUow.nonce).toBe(child1.nonce);
|
|
751
|
+
expect(child1.nonce).toBe(child2.nonce);
|
|
752
|
+
expect(child2.nonce).toBe(grandchild.nonce);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("should generate different nonces for separate UOW hierarchies", () => {
|
|
756
|
+
const executor = createMockExecutor();
|
|
757
|
+
|
|
758
|
+
// Create two separate parent UOWs
|
|
759
|
+
const parentUow1 = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
760
|
+
const parentUow2 = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
|
|
761
|
+
|
|
762
|
+
// They should have different nonces
|
|
763
|
+
expect(parentUow1.nonce).not.toBe(parentUow2.nonce);
|
|
764
|
+
|
|
765
|
+
// But children within each hierarchy should inherit their parent's nonce
|
|
766
|
+
const child1 = parentUow1.restrict();
|
|
767
|
+
const child2 = parentUow2.restrict();
|
|
768
|
+
|
|
769
|
+
expect(child1.nonce).toBe(parentUow1.nonce);
|
|
770
|
+
expect(child2.nonce).toBe(parentUow2.nonce);
|
|
771
|
+
expect(child1.nonce).not.toBe(child2.nonce);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it.skip("should not cause unhandled rejection when service method awaits retrievalPhase and executeRetrieve fails", async () => {
|
|
775
|
+
const testSchema = schema((s) =>
|
|
776
|
+
s.addTable("settings", (t) =>
|
|
777
|
+
t
|
|
778
|
+
.addColumn("id", idColumn())
|
|
779
|
+
.addColumn("key", "string")
|
|
780
|
+
.addColumn("value", "string")
|
|
781
|
+
.createIndex("unique_key", ["key"], { unique: true }),
|
|
782
|
+
),
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
// Create executor that throws "table does not exist" error
|
|
786
|
+
const failingExecutor = {
|
|
787
|
+
executeRetrievalPhase: async () => {
|
|
788
|
+
throw new Error('relation "settings" does not exist');
|
|
789
|
+
},
|
|
790
|
+
executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const parentUow = createUnitOfWork(createMockCompiler(), failingExecutor, createMockDecoder());
|
|
794
|
+
|
|
795
|
+
// Service method that awaits retrievalPhase (simulating settingsService.get())
|
|
796
|
+
const getSettingValue = async (key: string) => {
|
|
797
|
+
const childUow = parentUow.restrict();
|
|
798
|
+
const typedUow = childUow
|
|
799
|
+
.forSchema(testSchema)
|
|
800
|
+
.find("settings", (b) => b.whereIndex("unique_key", (eb) => eb("key", "=", key)));
|
|
801
|
+
|
|
802
|
+
// This is the critical line - accessing retrievalPhase creates a new promise
|
|
803
|
+
// If not cached properly, this new promise won't have a catch handler attached
|
|
804
|
+
const [results] = await typedUow.retrievalPhase;
|
|
805
|
+
return results?.[0];
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const deferred = Promise.withResolvers<string>();
|
|
809
|
+
|
|
810
|
+
// Handler that calls the service and handles the error
|
|
811
|
+
const handler = async () => {
|
|
812
|
+
try {
|
|
813
|
+
const settingPromise = getSettingValue("version");
|
|
814
|
+
|
|
815
|
+
await parentUow.executeRetrieve();
|
|
816
|
+
|
|
817
|
+
// Won't reach here
|
|
818
|
+
return await settingPromise;
|
|
819
|
+
} catch (error) {
|
|
820
|
+
// Error is caught here - this is expected behavior
|
|
821
|
+
expect(error).toBeInstanceOf(Error);
|
|
822
|
+
deferred.resolve((error as Error).message);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// Execute handler - should catch the error without unhandled rejection
|
|
828
|
+
const result = await handler();
|
|
829
|
+
expect(result).toBeNull();
|
|
830
|
+
|
|
831
|
+
expect(await deferred.promise).toContain('relation "settings" does not exist');
|
|
832
|
+
});
|
|
833
|
+
});
|