@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
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import type { AnySchema, AnyTable, Index, IdColumn, AnyColumn, Relation } from "../schema/create";
|
|
2
2
|
import { FragnoId } from "../schema/create";
|
|
3
3
|
import type { Condition, ConditionBuilder } from "./condition-builder";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
SelectClause,
|
|
6
|
+
TableToInsertValues,
|
|
7
|
+
TableToUpdateValues,
|
|
8
|
+
SelectResult,
|
|
9
|
+
ExtractSelect,
|
|
10
|
+
ExtractJoinOut,
|
|
11
|
+
} from "./query";
|
|
5
12
|
import { buildCondition } from "./condition-builder";
|
|
6
13
|
import type { CompiledJoin } from "./orm/orm";
|
|
7
14
|
import type { CursorResult } from "./cursor";
|
|
8
15
|
import { Cursor } from "./cursor";
|
|
16
|
+
import type { Prettify } from "../util/types";
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Builder for updateMany operations that supports both whereIndex and set chaining
|
|
@@ -143,13 +151,18 @@ export type RetrievalOperation<
|
|
|
143
151
|
> =
|
|
144
152
|
| {
|
|
145
153
|
type: "find";
|
|
154
|
+
schema: TSchema;
|
|
155
|
+
namespace?: string;
|
|
146
156
|
table: TTable;
|
|
147
157
|
indexName: string;
|
|
148
158
|
options: FindOptions<TTable, SelectClause<TTable>>;
|
|
149
159
|
withCursor?: boolean;
|
|
160
|
+
withSingleResult?: boolean;
|
|
150
161
|
}
|
|
151
162
|
| {
|
|
152
163
|
type: "count";
|
|
164
|
+
schema: TSchema;
|
|
165
|
+
namespace?: string;
|
|
153
166
|
table: TTable;
|
|
154
167
|
indexName: string;
|
|
155
168
|
options: Pick<FindOptions<TTable>, "where" | "useIndex">;
|
|
@@ -164,6 +177,8 @@ export type MutationOperation<
|
|
|
164
177
|
> =
|
|
165
178
|
| {
|
|
166
179
|
type: "update";
|
|
180
|
+
schema: TSchema;
|
|
181
|
+
namespace?: string;
|
|
167
182
|
table: TTable["name"];
|
|
168
183
|
id: FragnoId | string;
|
|
169
184
|
checkVersion: boolean;
|
|
@@ -171,15 +186,26 @@ export type MutationOperation<
|
|
|
171
186
|
}
|
|
172
187
|
| {
|
|
173
188
|
type: "create";
|
|
189
|
+
schema: TSchema;
|
|
190
|
+
namespace?: string;
|
|
174
191
|
table: TTable["name"];
|
|
175
192
|
values: TableToInsertValues<TTable>;
|
|
176
193
|
generatedExternalId: string;
|
|
177
194
|
}
|
|
178
195
|
| {
|
|
179
196
|
type: "delete";
|
|
197
|
+
schema: TSchema;
|
|
198
|
+
namespace?: string;
|
|
180
199
|
table: TTable["name"];
|
|
181
200
|
id: FragnoId | string;
|
|
182
201
|
checkVersion: boolean;
|
|
202
|
+
}
|
|
203
|
+
| {
|
|
204
|
+
type: "check";
|
|
205
|
+
schema: TSchema;
|
|
206
|
+
namespace?: string;
|
|
207
|
+
table: TTable["name"];
|
|
208
|
+
id: FragnoId;
|
|
183
209
|
};
|
|
184
210
|
|
|
185
211
|
/**
|
|
@@ -193,21 +219,27 @@ export interface CompiledMutation<TOutput> {
|
|
|
193
219
|
* null means don't check affected rows (e.g., for create operations).
|
|
194
220
|
*/
|
|
195
221
|
expectedAffectedRows: number | null;
|
|
222
|
+
/**
|
|
223
|
+
* Number of rows this SELECT query must return for the transaction to succeed.
|
|
224
|
+
* Used for check operations to verify version without modifying data.
|
|
225
|
+
* null means this is not a SELECT query that needs row count validation.
|
|
226
|
+
*/
|
|
227
|
+
expectedReturnedRows: number | null;
|
|
196
228
|
}
|
|
197
229
|
|
|
198
230
|
/**
|
|
199
231
|
* Compiler interface for Unit of Work operations
|
|
200
232
|
*/
|
|
201
|
-
export interface UOWCompiler<
|
|
233
|
+
export interface UOWCompiler<TOutput> {
|
|
202
234
|
/**
|
|
203
235
|
* Compile a retrieval operation to the adapter's query format
|
|
204
236
|
*/
|
|
205
|
-
compileRetrievalOperation(op: RetrievalOperation<
|
|
237
|
+
compileRetrievalOperation(op: RetrievalOperation<AnySchema>): TOutput | null;
|
|
206
238
|
|
|
207
239
|
/**
|
|
208
240
|
* Compile a mutation operation to the adapter's query format
|
|
209
241
|
*/
|
|
210
|
-
compileMutationOperation(op: MutationOperation<
|
|
242
|
+
compileMutationOperation(op: MutationOperation<AnySchema>): CompiledMutation<TOutput> | null;
|
|
211
243
|
}
|
|
212
244
|
|
|
213
245
|
export type MutationResult =
|
|
@@ -237,7 +269,7 @@ export interface UOWExecutor<TOutput, TRawResult = unknown> {
|
|
|
237
269
|
* Transforms raw database results into application format (e.g., converting raw columns
|
|
238
270
|
* into FragnoId objects with external ID, internal ID, and version).
|
|
239
271
|
*/
|
|
240
|
-
export interface UOWDecoder<
|
|
272
|
+
export interface UOWDecoder<TRawInput = unknown> {
|
|
241
273
|
/**
|
|
242
274
|
* Decode raw database results from the retrieval phase
|
|
243
275
|
*
|
|
@@ -245,7 +277,7 @@ export interface UOWDecoder<TSchema extends AnySchema, TRawInput = unknown> {
|
|
|
245
277
|
* @param operations - Array of retrieval operations that produced these results
|
|
246
278
|
* @returns Decoded results in application format
|
|
247
279
|
*/
|
|
248
|
-
(rawResults: TRawInput[], operations: RetrievalOperation<
|
|
280
|
+
(rawResults: TRawInput[], operations: RetrievalOperation<AnySchema>[]): unknown[];
|
|
249
281
|
}
|
|
250
282
|
|
|
251
283
|
/**
|
|
@@ -843,27 +875,259 @@ export function buildJoinIndexed<TTable extends AnyTable, TJoinOut>(
|
|
|
843
875
|
return compiled;
|
|
844
876
|
}
|
|
845
877
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
schema
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
878
|
+
/**
|
|
879
|
+
* Full Unit of Work interface with all operations including execution.
|
|
880
|
+
* This allows UOW instances to be passed between different contexts that use different schemas.
|
|
881
|
+
*/
|
|
882
|
+
export interface IUnitOfWork {
|
|
883
|
+
// Getters (schema-agnostic)
|
|
884
|
+
readonly state: UOWState;
|
|
885
|
+
readonly name: string | undefined;
|
|
886
|
+
readonly nonce: string;
|
|
887
|
+
readonly retrievalPhase: Promise<unknown[]>;
|
|
888
|
+
readonly mutationPhase: Promise<void>;
|
|
889
|
+
|
|
890
|
+
// Execution (schema-agnostic)
|
|
891
|
+
executeRetrieve(): Promise<unknown[]>;
|
|
892
|
+
executeMutations(): Promise<{ success: boolean }>;
|
|
893
|
+
|
|
894
|
+
// Inspection (schema-agnostic)
|
|
895
|
+
getRetrievalOperations(): ReadonlyArray<RetrievalOperation<AnySchema>>;
|
|
896
|
+
getMutationOperations(): ReadonlyArray<MutationOperation<AnySchema>>;
|
|
897
|
+
getCreatedIds(): FragnoId[];
|
|
898
|
+
|
|
899
|
+
// Parent-child relationships
|
|
900
|
+
restrict(): IUnitOfWork;
|
|
901
|
+
|
|
902
|
+
// Reset for retry support
|
|
903
|
+
reset(): void;
|
|
904
|
+
|
|
905
|
+
// Schema-specific view (for cross-schema operations)
|
|
906
|
+
forSchema<TOtherSchema extends AnySchema>(
|
|
907
|
+
schema: TOtherSchema,
|
|
908
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
909
|
+
): TypedUnitOfWork<TOtherSchema, [], any>;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Restricted UOW interface without execute methods.
|
|
914
|
+
* Useful when you want to allow building operations but not executing them,
|
|
915
|
+
* to prevent deadlocks or enforce execution control at a higher level.
|
|
916
|
+
*
|
|
917
|
+
* Note: This is just a marker interface. Restriction is enforced by the UnitOfWork class itself.
|
|
918
|
+
*/
|
|
919
|
+
export interface IUnitOfWorkRestricted
|
|
920
|
+
extends Omit<IUnitOfWork, "executeRetrieve" | "executeMutations"> {}
|
|
921
|
+
|
|
922
|
+
export function createUnitOfWork(
|
|
923
|
+
compiler: UOWCompiler<unknown>,
|
|
924
|
+
executor: UOWExecutor<unknown, unknown>,
|
|
925
|
+
decoder: UOWDecoder<unknown>,
|
|
926
|
+
schemaNamespaceMap?: WeakMap<AnySchema, string>,
|
|
855
927
|
name?: string,
|
|
856
|
-
): UnitOfWork
|
|
857
|
-
return new UnitOfWork(
|
|
858
|
-
TSchema,
|
|
859
|
-
TRetrievalResults,
|
|
860
|
-
TRawInput
|
|
861
|
-
>;
|
|
928
|
+
): UnitOfWork {
|
|
929
|
+
return new UnitOfWork(compiler, executor, decoder, name, undefined, schemaNamespaceMap);
|
|
862
930
|
}
|
|
863
931
|
|
|
864
932
|
export interface UnitOfWorkConfig {
|
|
865
933
|
dryRun?: boolean;
|
|
866
934
|
onQuery?: (query: unknown) => void;
|
|
935
|
+
nonce?: string;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Encapsulates a promise with its resolver/rejecter functions.
|
|
940
|
+
* Simplifies management of deferred promises with built-in error handling.
|
|
941
|
+
*/
|
|
942
|
+
class DeferredPromise<T> {
|
|
943
|
+
#resolve?: (value: T) => void;
|
|
944
|
+
#reject?: (error: Error) => void;
|
|
945
|
+
#promise: Promise<T>;
|
|
946
|
+
|
|
947
|
+
constructor() {
|
|
948
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
949
|
+
this.#promise = promise;
|
|
950
|
+
this.#resolve = resolve;
|
|
951
|
+
this.#reject = reject;
|
|
952
|
+
// Attach no-op error handler to prevent unhandled rejection warnings
|
|
953
|
+
this.#promise.catch(() => {});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
get promise(): Promise<T> {
|
|
957
|
+
return this.#promise;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
resolve(value: T): void {
|
|
961
|
+
this.#resolve?.(value);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
reject(error: Error): void {
|
|
965
|
+
this.#reject?.(error);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Reset to a new promise
|
|
970
|
+
*/
|
|
971
|
+
reset(): void {
|
|
972
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
973
|
+
this.#promise = promise;
|
|
974
|
+
this.#resolve = resolve;
|
|
975
|
+
this.#reject = reject;
|
|
976
|
+
// Attach no-op error handler to prevent unhandled rejection warnings
|
|
977
|
+
this.#promise.catch(() => {});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Tracks readiness signals from a group of children.
|
|
983
|
+
* Maintains a promise that resolves when all registered children have signaled.
|
|
984
|
+
*/
|
|
985
|
+
class ReadinessTracker {
|
|
986
|
+
#expectedCount = 0;
|
|
987
|
+
#signalCount = 0;
|
|
988
|
+
#resolve?: () => void;
|
|
989
|
+
#promise: Promise<void> = Promise.resolve();
|
|
990
|
+
|
|
991
|
+
get promise(): Promise<void> {
|
|
992
|
+
return this.#promise;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Register that we're expecting a signal from a child
|
|
997
|
+
*/
|
|
998
|
+
registerChild(): void {
|
|
999
|
+
if (this.#expectedCount === 0) {
|
|
1000
|
+
// First child - create new promise
|
|
1001
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
1002
|
+
this.#promise = promise;
|
|
1003
|
+
this.#resolve = resolve;
|
|
1004
|
+
}
|
|
1005
|
+
this.#expectedCount++;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Signal that one child is ready
|
|
1010
|
+
*/
|
|
1011
|
+
signal(): void {
|
|
1012
|
+
this.#signalCount++;
|
|
1013
|
+
if (this.#signalCount >= this.#expectedCount && this.#resolve) {
|
|
1014
|
+
this.#resolve();
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Reset to initial state
|
|
1020
|
+
*/
|
|
1021
|
+
reset(): void {
|
|
1022
|
+
this.#expectedCount = 0;
|
|
1023
|
+
this.#signalCount = 0;
|
|
1024
|
+
this.#resolve = undefined;
|
|
1025
|
+
this.#promise = Promise.resolve();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Manages parent-child relationships and readiness coordination for Unit of Work instances.
|
|
1031
|
+
* This allows parent UOWs to wait for all child UOWs to signal readiness before executing phases.
|
|
1032
|
+
*/
|
|
1033
|
+
class UOWChildCoordinator<TRawInput> {
|
|
1034
|
+
#parent: UnitOfWork<TRawInput> | null = null;
|
|
1035
|
+
#parentCoordinator: UOWChildCoordinator<TRawInput> | null = null;
|
|
1036
|
+
#children: Set<UnitOfWork<TRawInput>> = new Set();
|
|
1037
|
+
#isRestricted = false;
|
|
1038
|
+
|
|
1039
|
+
#retrievalTracker = new ReadinessTracker();
|
|
1040
|
+
#mutationTracker = new ReadinessTracker();
|
|
1041
|
+
|
|
1042
|
+
get isRestricted(): boolean {
|
|
1043
|
+
return this.#isRestricted;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
get parent(): UnitOfWork<TRawInput> | null {
|
|
1047
|
+
return this.#parent;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
get children(): ReadonlySet<UnitOfWork<TRawInput>> {
|
|
1051
|
+
return this.#children;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
get retrievalReadinessPromise(): Promise<void> {
|
|
1055
|
+
return this.#retrievalTracker.promise;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
get mutationReadinessPromise(): Promise<void> {
|
|
1059
|
+
return this.#mutationTracker.promise;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Mark this UOW as a restricted child of the given parent
|
|
1064
|
+
*/
|
|
1065
|
+
setAsRestricted(
|
|
1066
|
+
parent: UnitOfWork<TRawInput>,
|
|
1067
|
+
parentCoordinator: UOWChildCoordinator<TRawInput>,
|
|
1068
|
+
): void {
|
|
1069
|
+
this.#parent = parent;
|
|
1070
|
+
this.#parentCoordinator = parentCoordinator;
|
|
1071
|
+
this.#isRestricted = true;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Register a child UOW
|
|
1076
|
+
*/
|
|
1077
|
+
addChild(child: UnitOfWork<TRawInput>): void {
|
|
1078
|
+
this.#children.add(child);
|
|
1079
|
+
this.#retrievalTracker.registerChild();
|
|
1080
|
+
this.#mutationTracker.registerChild();
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* Signal that this child is ready for retrieval phase execution.
|
|
1085
|
+
* Only valid for restricted (child) UOWs.
|
|
1086
|
+
*/
|
|
1087
|
+
signalReadyForRetrieval(): void {
|
|
1088
|
+
if (!this.#parentCoordinator) {
|
|
1089
|
+
throw new Error("signalReadyForRetrieval() can only be called on restricted child UOWs");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.#parentCoordinator.notifyChildReadyForRetrieval();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Signal that this child is ready for mutation phase execution.
|
|
1097
|
+
* Only valid for restricted (child) UOWs.
|
|
1098
|
+
*/
|
|
1099
|
+
signalReadyForMutation(): void {
|
|
1100
|
+
if (!this.#parentCoordinator) {
|
|
1101
|
+
throw new Error("signalReadyForMutation() can only be called on restricted child UOWs");
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
this.#parentCoordinator.notifyChildReadyForMutation();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Notify this coordinator that a child is ready for retrieval (internal use).
|
|
1109
|
+
* Called by child UOWs when they signal readiness.
|
|
1110
|
+
*/
|
|
1111
|
+
notifyChildReadyForRetrieval(): void {
|
|
1112
|
+
this.#retrievalTracker.signal();
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Notify this coordinator that a child is ready for mutation (internal use).
|
|
1117
|
+
* Called by child UOWs when they signal readiness.
|
|
1118
|
+
*/
|
|
1119
|
+
notifyChildReadyForMutation(): void {
|
|
1120
|
+
this.#mutationTracker.signal();
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Reset coordination state for retry support
|
|
1125
|
+
*/
|
|
1126
|
+
reset(): void {
|
|
1127
|
+
this.#children.clear();
|
|
1128
|
+
this.#retrievalTracker.reset();
|
|
1129
|
+
this.#mutationTracker.reset();
|
|
1130
|
+
}
|
|
867
1131
|
}
|
|
868
1132
|
|
|
869
1133
|
/**
|
|
@@ -873,19 +1137,22 @@ export interface UnitOfWorkConfig {
|
|
|
873
1137
|
* 1. Retrieval phase: Read operations to fetch entities with their versions
|
|
874
1138
|
* 2. Mutation phase: Write operations that check versions before committing
|
|
875
1139
|
*
|
|
1140
|
+
* This is the untyped base storage. Use TypedUnitOfWork for type-safe operations.
|
|
1141
|
+
*
|
|
876
1142
|
* @example
|
|
877
1143
|
* ```ts
|
|
878
1144
|
* const uow = queryEngine.createUnitOfWork("update-user-balance");
|
|
1145
|
+
* const typedUow = uow.forSchema(mySchema);
|
|
879
1146
|
*
|
|
880
1147
|
* // Retrieval phase
|
|
881
|
-
*
|
|
1148
|
+
* typedUow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)));
|
|
882
1149
|
*
|
|
883
1150
|
* // Execute retrieval and transition to mutation phase
|
|
884
1151
|
* const [users] = await uow.executeRetrieve();
|
|
885
1152
|
*
|
|
886
1153
|
* // Mutation phase with version check
|
|
887
1154
|
* const user = users[0];
|
|
888
|
-
*
|
|
1155
|
+
* typedUow.update("users", user.id, (b) => b.set({ balance: newBalance }).check());
|
|
889
1156
|
*
|
|
890
1157
|
* // Execute mutations
|
|
891
1158
|
* const { success } = await uow.executeMutations();
|
|
@@ -894,46 +1161,146 @@ export interface UnitOfWorkConfig {
|
|
|
894
1161
|
* }
|
|
895
1162
|
* ```
|
|
896
1163
|
*/
|
|
897
|
-
export class UnitOfWork<
|
|
898
|
-
const TSchema extends AnySchema,
|
|
899
|
-
const TRetrievalResults extends unknown[] = [],
|
|
900
|
-
const TRawInput = unknown,
|
|
901
|
-
> {
|
|
902
|
-
#schema: TSchema;
|
|
903
|
-
|
|
1164
|
+
export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
|
|
904
1165
|
#name?: string;
|
|
905
1166
|
#config?: UnitOfWorkConfig;
|
|
1167
|
+
#nonce: string;
|
|
906
1168
|
|
|
907
1169
|
#state: UOWState = "building-retrieval";
|
|
908
1170
|
|
|
909
|
-
|
|
910
|
-
#
|
|
1171
|
+
// Operations can come from any schema
|
|
1172
|
+
#retrievalOps: RetrievalOperation<AnySchema>[] = [];
|
|
1173
|
+
#mutationOps: MutationOperation<AnySchema>[] = [];
|
|
911
1174
|
|
|
912
|
-
#compiler: UOWCompiler<
|
|
1175
|
+
#compiler: UOWCompiler<unknown>;
|
|
913
1176
|
#executor: UOWExecutor<unknown, TRawInput>;
|
|
914
|
-
#decoder: UOWDecoder<
|
|
1177
|
+
#decoder: UOWDecoder<TRawInput>;
|
|
1178
|
+
#schemaNamespaceMap?: WeakMap<AnySchema, string>;
|
|
915
1179
|
|
|
916
|
-
#retrievalResults?:
|
|
1180
|
+
#retrievalResults?: unknown[];
|
|
917
1181
|
#createdInternalIds: (bigint | null)[] = [];
|
|
918
1182
|
|
|
1183
|
+
// Phase coordination promises
|
|
1184
|
+
#retrievalPhaseDeferred = new DeferredPromise<unknown[]>();
|
|
1185
|
+
#mutationPhaseDeferred = new DeferredPromise<void>();
|
|
1186
|
+
|
|
1187
|
+
// Error tracking
|
|
1188
|
+
#retrievalError: Error | null = null;
|
|
1189
|
+
#mutationError: Error | null = null;
|
|
1190
|
+
|
|
1191
|
+
// Child coordination
|
|
1192
|
+
#coordinator: UOWChildCoordinator<TRawInput> = new UOWChildCoordinator();
|
|
1193
|
+
|
|
919
1194
|
constructor(
|
|
920
|
-
|
|
921
|
-
compiler: UOWCompiler<TSchema, unknown>,
|
|
1195
|
+
compiler: UOWCompiler<unknown>,
|
|
922
1196
|
executor: UOWExecutor<unknown, TRawInput>,
|
|
923
|
-
decoder: UOWDecoder<
|
|
1197
|
+
decoder: UOWDecoder<TRawInput>,
|
|
924
1198
|
name?: string,
|
|
925
1199
|
config?: UnitOfWorkConfig,
|
|
1200
|
+
schemaNamespaceMap?: WeakMap<AnySchema, string>,
|
|
926
1201
|
) {
|
|
927
|
-
this.#schema = schema;
|
|
928
1202
|
this.#compiler = compiler;
|
|
929
1203
|
this.#executor = executor;
|
|
930
1204
|
this.#decoder = decoder;
|
|
1205
|
+
this.#schemaNamespaceMap = schemaNamespaceMap;
|
|
931
1206
|
this.#name = name;
|
|
932
1207
|
this.#config = config;
|
|
1208
|
+
this.#nonce = config?.nonce ?? crypto.randomUUID();
|
|
933
1209
|
}
|
|
934
1210
|
|
|
935
|
-
|
|
936
|
-
|
|
1211
|
+
/**
|
|
1212
|
+
* Get a schema-specific typed view of this UOW for type-safe operations.
|
|
1213
|
+
* Returns a wrapper that provides typed operations for the given schema.
|
|
1214
|
+
* The namespace is automatically resolved from the schema-namespace map.
|
|
1215
|
+
*/
|
|
1216
|
+
forSchema<TOtherSchema extends AnySchema, TRawInput>(
|
|
1217
|
+
schema: TOtherSchema,
|
|
1218
|
+
): TypedUnitOfWork<TOtherSchema, [], TRawInput> {
|
|
1219
|
+
// Look up namespace from map
|
|
1220
|
+
const resolvedNamespace = this.#schemaNamespaceMap?.get(schema);
|
|
1221
|
+
|
|
1222
|
+
return new TypedUnitOfWork(schema, resolvedNamespace, this as unknown as UnitOfWork<TRawInput>);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Create a restricted child UOW that cannot execute phases.
|
|
1227
|
+
* The child shares the same operation storage but must signal readiness
|
|
1228
|
+
* before the parent can execute each phase.
|
|
1229
|
+
*/
|
|
1230
|
+
restrict(): UnitOfWork<TRawInput> {
|
|
1231
|
+
const child = new UnitOfWork(
|
|
1232
|
+
this.#compiler,
|
|
1233
|
+
this.#executor,
|
|
1234
|
+
this.#decoder,
|
|
1235
|
+
this.#name,
|
|
1236
|
+
{ ...this.#config, nonce: this.#nonce },
|
|
1237
|
+
this.#schemaNamespaceMap,
|
|
1238
|
+
);
|
|
1239
|
+
child.#coordinator.setAsRestricted(this, this.#coordinator);
|
|
1240
|
+
|
|
1241
|
+
// Share state with parent
|
|
1242
|
+
child.#state = this.#state;
|
|
1243
|
+
child.#retrievalOps = this.#retrievalOps;
|
|
1244
|
+
child.#mutationOps = this.#mutationOps;
|
|
1245
|
+
child.#retrievalResults = this.#retrievalResults;
|
|
1246
|
+
child.#createdInternalIds = this.#createdInternalIds;
|
|
1247
|
+
child.#retrievalPhaseDeferred = this.#retrievalPhaseDeferred;
|
|
1248
|
+
child.#mutationPhaseDeferred = this.#mutationPhaseDeferred;
|
|
1249
|
+
child.#retrievalError = this.#retrievalError;
|
|
1250
|
+
child.#mutationError = this.#mutationError;
|
|
1251
|
+
|
|
1252
|
+
this.#coordinator.addChild(child);
|
|
1253
|
+
|
|
1254
|
+
// For synchronous usage (the common case), immediately signal readiness
|
|
1255
|
+
// This allows services called directly from handlers to work without explicit signaling
|
|
1256
|
+
child.signalReadyForRetrieval();
|
|
1257
|
+
child.signalReadyForMutation();
|
|
1258
|
+
|
|
1259
|
+
return child;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Signal that this child is ready for retrieval phase execution.
|
|
1264
|
+
* Only valid for restricted (child) UOWs.
|
|
1265
|
+
*/
|
|
1266
|
+
signalReadyForRetrieval(): void {
|
|
1267
|
+
this.#coordinator.signalReadyForRetrieval();
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Signal that this child is ready for mutation phase execution.
|
|
1272
|
+
* Only valid for restricted (child) UOWs.
|
|
1273
|
+
*/
|
|
1274
|
+
signalReadyForMutation(): void {
|
|
1275
|
+
this.#coordinator.signalReadyForMutation();
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Reset the UOW to initial state for retry support.
|
|
1280
|
+
* Clears operations, resets state, and resets phase promises.
|
|
1281
|
+
*/
|
|
1282
|
+
reset(): void {
|
|
1283
|
+
if (this.#coordinator.isRestricted) {
|
|
1284
|
+
throw new Error("reset() cannot be called on restricted child UOWs");
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Clear operations
|
|
1288
|
+
this.#retrievalOps = [];
|
|
1289
|
+
this.#mutationOps = [];
|
|
1290
|
+
this.#retrievalResults = undefined;
|
|
1291
|
+
this.#createdInternalIds = [];
|
|
1292
|
+
|
|
1293
|
+
// Reset state
|
|
1294
|
+
this.#state = "building-retrieval";
|
|
1295
|
+
this.#retrievalError = null;
|
|
1296
|
+
this.#mutationError = null;
|
|
1297
|
+
|
|
1298
|
+
// Reset phase promises
|
|
1299
|
+
this.#retrievalPhaseDeferred.reset();
|
|
1300
|
+
this.#mutationPhaseDeferred.reset();
|
|
1301
|
+
|
|
1302
|
+
// Reset child coordination
|
|
1303
|
+
this.#coordinator.reset();
|
|
937
1304
|
}
|
|
938
1305
|
|
|
939
1306
|
get state(): UOWState {
|
|
@@ -944,13 +1311,33 @@ export class UnitOfWork<
|
|
|
944
1311
|
return this.#name;
|
|
945
1312
|
}
|
|
946
1313
|
|
|
1314
|
+
get nonce(): string {
|
|
1315
|
+
return this.#nonce;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Promise that resolves when the retrieval phase is executed
|
|
1320
|
+
* Service methods can await this to coordinate multi-phase logic
|
|
1321
|
+
*/
|
|
1322
|
+
get retrievalPhase(): Promise<unknown[]> {
|
|
1323
|
+
return this.#retrievalPhaseDeferred.promise;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Promise that resolves when the mutation phase is executed
|
|
1328
|
+
* Service methods can await this to coordinate multi-phase logic
|
|
1329
|
+
*/
|
|
1330
|
+
get mutationPhase(): Promise<void> {
|
|
1331
|
+
return this.#mutationPhaseDeferred.promise;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
947
1334
|
/**
|
|
948
1335
|
* Execute the retrieval phase and transition to mutation phase
|
|
949
1336
|
* Returns all results from find operations
|
|
950
1337
|
*/
|
|
951
|
-
async executeRetrieve(): Promise<
|
|
952
|
-
if (this.#
|
|
953
|
-
|
|
1338
|
+
async executeRetrieve(): Promise<unknown[]> {
|
|
1339
|
+
if (this.#coordinator.isRestricted) {
|
|
1340
|
+
throw new Error("executeRetrieve() cannot be called on restricted child UOWs");
|
|
954
1341
|
}
|
|
955
1342
|
|
|
956
1343
|
if (this.#state !== "building-retrieval") {
|
|
@@ -959,169 +1346,529 @@ export class UnitOfWork<
|
|
|
959
1346
|
);
|
|
960
1347
|
}
|
|
961
1348
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const compiled = this.#compiler.compileRetrievalOperation(op);
|
|
966
|
-
if (compiled !== null) {
|
|
967
|
-
this.#config?.onQuery?.(compiled);
|
|
1349
|
+
try {
|
|
1350
|
+
// Wait for all children to signal readiness
|
|
1351
|
+
await this.#coordinator.retrievalReadinessPromise;
|
|
968
1352
|
|
|
969
|
-
|
|
1353
|
+
if (this.#retrievalOps.length === 0) {
|
|
1354
|
+
this.#state = "building-mutation";
|
|
1355
|
+
const emptyResults: unknown[] = [];
|
|
1356
|
+
this.#retrievalPhaseDeferred.resolve(emptyResults);
|
|
1357
|
+
return emptyResults;
|
|
970
1358
|
}
|
|
971
|
-
}
|
|
972
1359
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1360
|
+
// Compile retrieval operations using single compiler
|
|
1361
|
+
const retrievalBatch: unknown[] = [];
|
|
1362
|
+
for (const op of this.#retrievalOps) {
|
|
1363
|
+
const compiled = this.#compiler.compileRetrievalOperation(op);
|
|
1364
|
+
if (compiled !== null) {
|
|
1365
|
+
this.#config?.onQuery?.(compiled);
|
|
1366
|
+
retrievalBatch.push(compiled);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
977
1369
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1370
|
+
if (this.#config?.dryRun) {
|
|
1371
|
+
this.#state = "executed";
|
|
1372
|
+
const emptyResults: unknown[] = [];
|
|
1373
|
+
this.#retrievalPhaseDeferred.resolve(emptyResults);
|
|
1374
|
+
return emptyResults;
|
|
1375
|
+
}
|
|
982
1376
|
|
|
983
|
-
|
|
984
|
-
this.#retrievalResults = results as TRetrievalResults;
|
|
985
|
-
this.#state = "building-mutation";
|
|
1377
|
+
const rawResults = await this.#executor.executeRetrievalPhase(retrievalBatch);
|
|
986
1378
|
|
|
987
|
-
|
|
1379
|
+
const results = this.#decoder(rawResults, this.#retrievalOps);
|
|
1380
|
+
|
|
1381
|
+
// Store results and transition to mutation phase
|
|
1382
|
+
this.#retrievalResults = results;
|
|
1383
|
+
this.#state = "building-mutation";
|
|
1384
|
+
|
|
1385
|
+
this.#retrievalPhaseDeferred.resolve(this.#retrievalResults);
|
|
1386
|
+
|
|
1387
|
+
return this.#retrievalResults;
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
this.#retrievalError = error instanceof Error ? error : new Error(String(error));
|
|
1390
|
+
throw error;
|
|
1391
|
+
}
|
|
988
1392
|
}
|
|
989
1393
|
|
|
990
1394
|
/**
|
|
991
|
-
*
|
|
1395
|
+
* Execute the mutation phase
|
|
1396
|
+
* Returns success flag indicating if mutations completed without conflicts
|
|
992
1397
|
*/
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
TJoinOut = {},
|
|
997
|
-
>(
|
|
998
|
-
tableName: TTableName,
|
|
999
|
-
builderFn?: (
|
|
1000
|
-
// We omit "build" because we don't want to expose it to the user
|
|
1001
|
-
builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
|
|
1002
|
-
) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
|
|
1003
|
-
): UnitOfWork<
|
|
1004
|
-
TSchema,
|
|
1005
|
-
[...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>[]],
|
|
1006
|
-
TRawInput
|
|
1007
|
-
> {
|
|
1008
|
-
if (this.#state !== "building-retrieval") {
|
|
1009
|
-
throw new Error(
|
|
1010
|
-
`find() can only be called during retrieval phase. Current state: ${this.#state}`,
|
|
1011
|
-
);
|
|
1398
|
+
async executeMutations(): Promise<{ success: boolean }> {
|
|
1399
|
+
if (this.#coordinator.isRestricted) {
|
|
1400
|
+
throw new Error("executeMutations() cannot be called on restricted child UOWs");
|
|
1012
1401
|
}
|
|
1013
1402
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
throw new Error(`Table ${tableName} not found in schema`);
|
|
1403
|
+
if (this.#state === "executed") {
|
|
1404
|
+
throw new Error(`Cannot execute mutations from state ${this.#state}.`);
|
|
1017
1405
|
}
|
|
1018
1406
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1407
|
+
try {
|
|
1408
|
+
// Wait for all children to signal readiness
|
|
1409
|
+
await this.#coordinator.mutationReadinessPromise;
|
|
1410
|
+
|
|
1411
|
+
// Compile mutation operations using single compiler
|
|
1412
|
+
const mutationBatch: CompiledMutation<unknown>[] = [];
|
|
1413
|
+
for (const op of this.#mutationOps) {
|
|
1414
|
+
const compiled = this.#compiler.compileMutationOperation(op);
|
|
1415
|
+
if (compiled !== null) {
|
|
1416
|
+
this.#config?.onQuery?.(compiled);
|
|
1417
|
+
mutationBatch.push(compiled);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (this.#config?.dryRun) {
|
|
1422
|
+
this.#state = "executed";
|
|
1423
|
+
this.#mutationPhaseDeferred.resolve();
|
|
1424
|
+
return {
|
|
1425
|
+
success: true,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Execute mutation phase
|
|
1430
|
+
const result = await this.#executor.executeMutationPhase(mutationBatch);
|
|
1431
|
+
this.#state = "executed";
|
|
1432
|
+
|
|
1433
|
+
if (result.success) {
|
|
1434
|
+
this.#createdInternalIds = result.createdInternalIds;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Resolve the mutation phase promise to unblock waiting service methods
|
|
1438
|
+
this.#mutationPhaseDeferred.resolve();
|
|
1439
|
+
|
|
1440
|
+
return {
|
|
1441
|
+
success: result.success,
|
|
1442
|
+
};
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
this.#mutationError = error instanceof Error ? error : new Error(String(error));
|
|
1445
|
+
throw error;
|
|
1026
1446
|
}
|
|
1027
|
-
|
|
1447
|
+
}
|
|
1028
1448
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1036
|
-
options: options as any,
|
|
1037
|
-
});
|
|
1449
|
+
/**
|
|
1450
|
+
* Get the retrieval operations (for inspection/debugging)
|
|
1451
|
+
*/
|
|
1452
|
+
getRetrievalOperations(): ReadonlyArray<RetrievalOperation<AnySchema>> {
|
|
1453
|
+
return this.#retrievalOps;
|
|
1454
|
+
}
|
|
1038
1455
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1456
|
+
/**
|
|
1457
|
+
* Get the mutation operations (for inspection/debugging)
|
|
1458
|
+
*/
|
|
1459
|
+
getMutationOperations(): ReadonlyArray<MutationOperation<AnySchema>> {
|
|
1460
|
+
return this.#mutationOps;
|
|
1044
1461
|
}
|
|
1045
1462
|
|
|
1046
1463
|
/**
|
|
1047
|
-
*
|
|
1464
|
+
* @internal
|
|
1465
|
+
* Add a retrieval operation (used by TypedUnitOfWork)
|
|
1048
1466
|
*/
|
|
1049
|
-
|
|
1050
|
-
TTableName extends keyof TSchema["tables"] & string,
|
|
1051
|
-
TSelect extends SelectClause<TSchema["tables"][TTableName]> = true,
|
|
1052
|
-
TJoinOut = {},
|
|
1053
|
-
>(
|
|
1054
|
-
tableName: TTableName,
|
|
1055
|
-
builderFn: (
|
|
1056
|
-
// We omit "build" because we don't want to expose it to the user
|
|
1057
|
-
builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
|
|
1058
|
-
) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
|
|
1059
|
-
): UnitOfWork<
|
|
1060
|
-
TSchema,
|
|
1061
|
-
[
|
|
1062
|
-
...TRetrievalResults,
|
|
1063
|
-
CursorResult<SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>>,
|
|
1064
|
-
],
|
|
1065
|
-
TRawInput
|
|
1066
|
-
> {
|
|
1467
|
+
addRetrievalOperation(op: RetrievalOperation<AnySchema>): number {
|
|
1067
1468
|
if (this.#state !== "building-retrieval") {
|
|
1068
1469
|
throw new Error(
|
|
1069
|
-
`
|
|
1470
|
+
`Cannot add retrieval operation in state ${this.#state}. Must be in building-retrieval state.`,
|
|
1070
1471
|
);
|
|
1071
1472
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
if (!table) {
|
|
1075
|
-
throw new Error(`Table ${tableName} not found in schema`);
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// Create builder and pass to callback
|
|
1079
|
-
const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
|
|
1080
|
-
builderFn(builder);
|
|
1081
|
-
const { indexName, options, type } = builder.build();
|
|
1082
|
-
|
|
1083
|
-
this.#retrievalOps.push({
|
|
1084
|
-
type,
|
|
1085
|
-
// Safe: we know the table is part of the schema from the findWithCursor() method
|
|
1086
|
-
table: table as TSchema["tables"][TTableName],
|
|
1087
|
-
indexName,
|
|
1088
|
-
// Safe: we're storing the options for later compilation
|
|
1089
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1090
|
-
options: options as any,
|
|
1091
|
-
withCursor: true,
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
return this as unknown as UnitOfWork<
|
|
1095
|
-
TSchema,
|
|
1096
|
-
[
|
|
1097
|
-
...TRetrievalResults,
|
|
1098
|
-
CursorResult<SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>>,
|
|
1099
|
-
],
|
|
1100
|
-
TRawInput
|
|
1101
|
-
>;
|
|
1473
|
+
this.#retrievalOps.push(op);
|
|
1474
|
+
return this.#retrievalOps.length - 1;
|
|
1102
1475
|
}
|
|
1103
1476
|
|
|
1104
1477
|
/**
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1478
|
+
* @internal
|
|
1479
|
+
* Add a mutation operation (used by TypedUnitOfWork)
|
|
1107
1480
|
*/
|
|
1108
|
-
|
|
1109
|
-
table: TableName,
|
|
1110
|
-
values: TableToInsertValues<TSchema["tables"][TableName]>,
|
|
1111
|
-
): FragnoId {
|
|
1481
|
+
addMutationOperation(op: MutationOperation<AnySchema>): void {
|
|
1112
1482
|
if (this.#state === "executed") {
|
|
1113
|
-
throw new Error(`
|
|
1483
|
+
throw new Error(`Cannot add mutation operation in executed state.`);
|
|
1114
1484
|
}
|
|
1485
|
+
this.#mutationOps.push(op);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Get the IDs of created entities after executeMutations() has been called.
|
|
1490
|
+
* Returns FragnoId objects with external IDs (always available) and internal IDs
|
|
1491
|
+
* (available when database supports RETURNING).
|
|
1492
|
+
*
|
|
1493
|
+
* @throws Error if called before executeMutations()
|
|
1494
|
+
* @returns Array of FragnoIds in the same order as create() calls
|
|
1495
|
+
*/
|
|
1496
|
+
getCreatedIds(): FragnoId[] {
|
|
1497
|
+
if (this.#state !== "executed") {
|
|
1498
|
+
throw new Error(
|
|
1499
|
+
`getCreatedIds() can only be called after executeMutations(). Current state: ${this.#state}`,
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const createdIds: FragnoId[] = [];
|
|
1504
|
+
let createIndex = 0;
|
|
1505
|
+
|
|
1506
|
+
for (const op of this.#mutationOps) {
|
|
1507
|
+
if (op.type === "create") {
|
|
1508
|
+
const internalId = this.#createdInternalIds[createIndex] ?? undefined;
|
|
1509
|
+
createdIds.push(
|
|
1510
|
+
new FragnoId({
|
|
1511
|
+
externalId: op.generatedExternalId,
|
|
1512
|
+
internalId,
|
|
1513
|
+
version: 0, // New records always start at version 0
|
|
1514
|
+
}),
|
|
1515
|
+
);
|
|
1516
|
+
createIndex++;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
return createdIds;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* @internal
|
|
1525
|
+
* Compile the unit of work to executable queries for testing
|
|
1526
|
+
*/
|
|
1527
|
+
compile<TOutput>(compiler: UOWCompiler<TOutput>): {
|
|
1528
|
+
name?: string;
|
|
1529
|
+
retrievalBatch: TOutput[];
|
|
1530
|
+
mutationBatch: CompiledMutation<TOutput>[];
|
|
1531
|
+
} {
|
|
1532
|
+
const retrievalBatch: TOutput[] = [];
|
|
1533
|
+
for (const op of this.#retrievalOps) {
|
|
1534
|
+
const compiled = compiler.compileRetrievalOperation(op);
|
|
1535
|
+
if (compiled !== null) {
|
|
1536
|
+
retrievalBatch.push(compiled);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const mutationBatch: CompiledMutation<TOutput>[] = [];
|
|
1541
|
+
for (const op of this.#mutationOps) {
|
|
1542
|
+
const compiled = compiler.compileMutationOperation(op);
|
|
1543
|
+
if (compiled !== null) {
|
|
1544
|
+
mutationBatch.push(compiled);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
return {
|
|
1549
|
+
name: this.#name,
|
|
1550
|
+
retrievalBatch,
|
|
1551
|
+
mutationBatch,
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* A typed facade around a UnitOfWork that provides type-safe operations for a specific schema.
|
|
1558
|
+
* All operations are stored in the underlying UOW, but this facade ensures type safety and
|
|
1559
|
+
* filters retrieval results to only include operations added through this facade.
|
|
1560
|
+
*/
|
|
1561
|
+
export class TypedUnitOfWork<
|
|
1562
|
+
const TSchema extends AnySchema,
|
|
1563
|
+
const TRetrievalResults extends unknown[] = [],
|
|
1564
|
+
const TRawInput = unknown,
|
|
1565
|
+
> implements IUnitOfWork
|
|
1566
|
+
{
|
|
1567
|
+
#schema: TSchema;
|
|
1568
|
+
#namespace?: string;
|
|
1569
|
+
#uow: UnitOfWork<TRawInput>;
|
|
1570
|
+
#operationIndices: number[] = [];
|
|
1571
|
+
#cachedRetrievalPhase?: Promise<TRetrievalResults>;
|
|
1572
|
+
|
|
1573
|
+
constructor(schema: TSchema, namespace: string | undefined, uow: UnitOfWork<TRawInput>) {
|
|
1574
|
+
this.#schema = schema;
|
|
1575
|
+
this.#namespace = namespace;
|
|
1576
|
+
this.#uow = uow;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
get $results(): Prettify<TRetrievalResults> {
|
|
1580
|
+
throw new Error("type only");
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
get schema(): TSchema {
|
|
1584
|
+
return this.#schema;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
get name(): string | undefined {
|
|
1588
|
+
return this.#uow.name;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
get nonce(): string {
|
|
1592
|
+
return this.#uow.nonce;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
get state() {
|
|
1596
|
+
return this.#uow.state;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
get retrievalPhase(): Promise<TRetrievalResults> {
|
|
1600
|
+
// Cache the filtered promise to avoid recreating it on every access
|
|
1601
|
+
if (!this.#cachedRetrievalPhase) {
|
|
1602
|
+
this.#cachedRetrievalPhase = this.#uow.retrievalPhase.then((allResults) => {
|
|
1603
|
+
const allOperations = this.#uow.getRetrievalOperations();
|
|
1604
|
+
const filteredResults = this.#operationIndices.map((opIndex) => {
|
|
1605
|
+
const result = allResults[opIndex];
|
|
1606
|
+
const operation = allOperations[opIndex];
|
|
1607
|
+
// Transform array to single item for findFirst operations
|
|
1608
|
+
if (operation?.type === "find" && operation.withSingleResult) {
|
|
1609
|
+
return Array.isArray(result) ? (result[0] ?? null) : result;
|
|
1610
|
+
}
|
|
1611
|
+
return result;
|
|
1612
|
+
});
|
|
1613
|
+
return filteredResults as TRetrievalResults;
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
return this.#cachedRetrievalPhase;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
get mutationPhase(): Promise<void> {
|
|
1620
|
+
return this.#uow.mutationPhase;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
getRetrievalOperations() {
|
|
1624
|
+
return this.#uow.getRetrievalOperations();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
getMutationOperations() {
|
|
1628
|
+
return this.#uow.getMutationOperations();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
getCreatedIds() {
|
|
1632
|
+
return this.#uow.getCreatedIds();
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
async executeRetrieve(): Promise<TRetrievalResults> {
|
|
1636
|
+
return this.#uow.executeRetrieve() as Promise<TRetrievalResults>;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
async executeMutations(): Promise<{ success: boolean }> {
|
|
1640
|
+
return this.#uow.executeMutations();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
restrict(): IUnitOfWork {
|
|
1644
|
+
return this.#uow.restrict();
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
reset(): void {
|
|
1648
|
+
return this.#uow.reset();
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
forSchema<TOtherSchema extends AnySchema>(
|
|
1652
|
+
schema: TOtherSchema,
|
|
1653
|
+
): TypedUnitOfWork<TOtherSchema, [], TRawInput> {
|
|
1654
|
+
return this.#uow.forSchema(schema);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
compile<TOutput>(compiler: UOWCompiler<TOutput>): {
|
|
1658
|
+
name?: string;
|
|
1659
|
+
retrievalBatch: TOutput[];
|
|
1660
|
+
mutationBatch: CompiledMutation<TOutput>[];
|
|
1661
|
+
} {
|
|
1662
|
+
return this.#uow.compile(compiler);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
|
|
1666
|
+
tableName: TTableName,
|
|
1667
|
+
builderFn: (
|
|
1668
|
+
builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
|
|
1669
|
+
) => TBuilderResult,
|
|
1670
|
+
): TypedUnitOfWork<
|
|
1671
|
+
TSchema,
|
|
1672
|
+
[
|
|
1673
|
+
...TRetrievalResults,
|
|
1674
|
+
SelectResult<
|
|
1675
|
+
TSchema["tables"][TTableName],
|
|
1676
|
+
ExtractJoinOut<TBuilderResult>,
|
|
1677
|
+
Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
|
|
1678
|
+
>[],
|
|
1679
|
+
],
|
|
1680
|
+
TRawInput
|
|
1681
|
+
>;
|
|
1682
|
+
find<TTableName extends keyof TSchema["tables"] & string>(
|
|
1683
|
+
tableName: TTableName,
|
|
1684
|
+
): TypedUnitOfWork<
|
|
1685
|
+
TSchema,
|
|
1686
|
+
[...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true>[]],
|
|
1687
|
+
TRawInput
|
|
1688
|
+
>;
|
|
1689
|
+
find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
|
|
1690
|
+
tableName: TTableName,
|
|
1691
|
+
builderFn?: (
|
|
1692
|
+
builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
|
|
1693
|
+
) => TBuilderResult,
|
|
1694
|
+
): TypedUnitOfWork<
|
|
1695
|
+
TSchema,
|
|
1696
|
+
[
|
|
1697
|
+
...TRetrievalResults,
|
|
1698
|
+
SelectResult<
|
|
1699
|
+
TSchema["tables"][TTableName],
|
|
1700
|
+
ExtractJoinOut<TBuilderResult>,
|
|
1701
|
+
Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
|
|
1702
|
+
>[],
|
|
1703
|
+
],
|
|
1704
|
+
TRawInput
|
|
1705
|
+
> {
|
|
1706
|
+
const table = this.#schema.tables[tableName];
|
|
1707
|
+
if (!table) {
|
|
1708
|
+
throw new Error(`Table ${tableName} not found in schema`);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
|
|
1712
|
+
if (builderFn) {
|
|
1713
|
+
builderFn(builder);
|
|
1714
|
+
} else {
|
|
1715
|
+
builder.whereIndex("primary");
|
|
1716
|
+
}
|
|
1717
|
+
const { indexName, options, type } = builder.build();
|
|
1718
|
+
|
|
1719
|
+
const operationIndex = this.#uow.addRetrievalOperation({
|
|
1720
|
+
type,
|
|
1721
|
+
schema: this.#schema,
|
|
1722
|
+
namespace: this.#namespace,
|
|
1723
|
+
table: table as TSchema["tables"][TTableName],
|
|
1724
|
+
indexName,
|
|
1725
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1726
|
+
options: options as any,
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
// Track which operation index belongs to this view
|
|
1730
|
+
this.#operationIndices.push(operationIndex);
|
|
1731
|
+
|
|
1732
|
+
// Safe: return type is correctly specified in the method signature
|
|
1733
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1734
|
+
return this as any;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
findFirst<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
|
|
1738
|
+
tableName: TTableName,
|
|
1739
|
+
builderFn: (
|
|
1740
|
+
builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
|
|
1741
|
+
) => TBuilderResult,
|
|
1742
|
+
): TypedUnitOfWork<
|
|
1743
|
+
TSchema,
|
|
1744
|
+
[
|
|
1745
|
+
...TRetrievalResults,
|
|
1746
|
+
SelectResult<
|
|
1747
|
+
TSchema["tables"][TTableName],
|
|
1748
|
+
ExtractJoinOut<TBuilderResult>,
|
|
1749
|
+
Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
|
|
1750
|
+
> | null,
|
|
1751
|
+
],
|
|
1752
|
+
TRawInput
|
|
1753
|
+
>;
|
|
1754
|
+
findFirst<TTableName extends keyof TSchema["tables"] & string>(
|
|
1755
|
+
tableName: TTableName,
|
|
1756
|
+
): TypedUnitOfWork<
|
|
1757
|
+
TSchema,
|
|
1758
|
+
[...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true> | null],
|
|
1759
|
+
TRawInput
|
|
1760
|
+
>;
|
|
1761
|
+
findFirst<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
|
|
1762
|
+
tableName: TTableName,
|
|
1763
|
+
builderFn?: (
|
|
1764
|
+
builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
|
|
1765
|
+
) => TBuilderResult,
|
|
1766
|
+
): TypedUnitOfWork<
|
|
1767
|
+
TSchema,
|
|
1768
|
+
[
|
|
1769
|
+
...TRetrievalResults,
|
|
1770
|
+
SelectResult<
|
|
1771
|
+
TSchema["tables"][TTableName],
|
|
1772
|
+
ExtractJoinOut<TBuilderResult>,
|
|
1773
|
+
Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
|
|
1774
|
+
> | null,
|
|
1775
|
+
],
|
|
1776
|
+
TRawInput
|
|
1777
|
+
> {
|
|
1778
|
+
const table = this.#schema.tables[tableName];
|
|
1779
|
+
if (!table) {
|
|
1780
|
+
throw new Error(`Table ${tableName} not found in schema`);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
|
|
1784
|
+
if (builderFn) {
|
|
1785
|
+
builderFn(builder);
|
|
1786
|
+
} else {
|
|
1787
|
+
builder.whereIndex("primary");
|
|
1788
|
+
}
|
|
1789
|
+
// Automatically set pageSize to 1 for findFirst
|
|
1790
|
+
builder.pageSize(1);
|
|
1791
|
+
const { indexName, options, type } = builder.build();
|
|
1792
|
+
|
|
1793
|
+
const operationIndex = this.#uow.addRetrievalOperation({
|
|
1794
|
+
type,
|
|
1795
|
+
schema: this.#schema,
|
|
1796
|
+
namespace: this.#namespace,
|
|
1797
|
+
table: table as TSchema["tables"][TTableName],
|
|
1798
|
+
indexName,
|
|
1799
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1800
|
+
options: options as any,
|
|
1801
|
+
withSingleResult: true,
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
// Track which operation index belongs to this view
|
|
1805
|
+
this.#operationIndices.push(operationIndex);
|
|
1806
|
+
|
|
1807
|
+
// Safe: return type is correctly specified in the method signature
|
|
1808
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1809
|
+
return this as any;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
findWithCursor<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
|
|
1813
|
+
tableName: TTableName,
|
|
1814
|
+
builderFn: (
|
|
1815
|
+
builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
|
|
1816
|
+
) => TBuilderResult,
|
|
1817
|
+
): TypedUnitOfWork<
|
|
1818
|
+
TSchema,
|
|
1819
|
+
[
|
|
1820
|
+
...TRetrievalResults,
|
|
1821
|
+
CursorResult<
|
|
1822
|
+
SelectResult<
|
|
1823
|
+
TSchema["tables"][TTableName],
|
|
1824
|
+
ExtractJoinOut<TBuilderResult>,
|
|
1825
|
+
Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
|
|
1826
|
+
>
|
|
1827
|
+
>,
|
|
1828
|
+
],
|
|
1829
|
+
TRawInput
|
|
1830
|
+
> {
|
|
1831
|
+
const table = this.#schema.tables[tableName];
|
|
1832
|
+
if (!table) {
|
|
1833
|
+
throw new Error(`Table ${tableName} not found in schema`);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
|
|
1837
|
+
builderFn(builder);
|
|
1838
|
+
const { indexName, options, type } = builder.build();
|
|
1839
|
+
|
|
1840
|
+
const operationIndex = this.#uow.addRetrievalOperation({
|
|
1841
|
+
type,
|
|
1842
|
+
schema: this.#schema,
|
|
1843
|
+
namespace: this.#namespace,
|
|
1844
|
+
table: table as TSchema["tables"][TTableName],
|
|
1845
|
+
indexName,
|
|
1846
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1847
|
+
options: options as any,
|
|
1848
|
+
withCursor: true,
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
// Track which operation index belongs to this view
|
|
1852
|
+
this.#operationIndices.push(operationIndex);
|
|
1853
|
+
|
|
1854
|
+
// Safe: return type is correctly specified in the method signature
|
|
1855
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1856
|
+
return this as any;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
create<TableName extends keyof TSchema["tables"] & string>(
|
|
1860
|
+
tableName: TableName,
|
|
1861
|
+
values: TableToInsertValues<TSchema["tables"][TableName]>,
|
|
1862
|
+
): FragnoId {
|
|
1863
|
+
const tableSchema = this.#schema.tables[tableName];
|
|
1864
|
+
if (!tableSchema) {
|
|
1865
|
+
throw new Error(`Table ${tableName} not found in schema`);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const idColumn = tableSchema.getIdColumn();
|
|
1869
|
+
let externalId: string;
|
|
1870
|
+
let updatedValues = values;
|
|
1115
1871
|
|
|
1116
|
-
const tableSchema = this.#schema.tables[table];
|
|
1117
|
-
if (!tableSchema) {
|
|
1118
|
-
throw new Error(`Table ${table} not found in schema`);
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const idColumn = tableSchema.getIdColumn();
|
|
1122
|
-
let externalId: string;
|
|
1123
|
-
let updatedValues = values;
|
|
1124
|
-
|
|
1125
1872
|
// Check if ID value is provided in values
|
|
1126
1873
|
const providedIdValue = (values as Record<string, unknown>)[idColumn.ormName];
|
|
1127
1874
|
|
|
@@ -1153,9 +1900,11 @@ export class UnitOfWork<
|
|
|
1153
1900
|
} as TableToInsertValues<TSchema["tables"][TableName]>;
|
|
1154
1901
|
}
|
|
1155
1902
|
|
|
1156
|
-
this.#
|
|
1903
|
+
this.#uow.addMutationOperation({
|
|
1157
1904
|
type: "create",
|
|
1158
|
-
|
|
1905
|
+
schema: this.#schema,
|
|
1906
|
+
namespace: this.#namespace,
|
|
1907
|
+
table: tableName,
|
|
1159
1908
|
values: updatedValues,
|
|
1160
1909
|
generatedExternalId: externalId,
|
|
1161
1910
|
});
|
|
@@ -1163,180 +1912,73 @@ export class UnitOfWork<
|
|
|
1163
1912
|
return FragnoId.fromExternal(externalId, 0);
|
|
1164
1913
|
}
|
|
1165
1914
|
|
|
1166
|
-
/**
|
|
1167
|
-
* Add an update operation using a builder callback (mutation phase only)
|
|
1168
|
-
*/
|
|
1169
1915
|
update<TableName extends keyof TSchema["tables"] & string>(
|
|
1170
|
-
|
|
1916
|
+
tableName: TableName,
|
|
1171
1917
|
id: FragnoId | string,
|
|
1172
1918
|
builderFn: (
|
|
1173
|
-
// We omit "build" because we don't want to expose it to the user
|
|
1174
1919
|
builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
|
|
1175
1920
|
) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"> | void,
|
|
1176
1921
|
): void {
|
|
1177
|
-
|
|
1178
|
-
throw new Error(`update() can only be called during mutation phase.`);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Create builder, pass to callback, then extract configuration
|
|
1182
|
-
const builder = new UpdateBuilder<TSchema["tables"][TableName]>(table, id);
|
|
1922
|
+
const builder = new UpdateBuilder<TSchema["tables"][TableName]>(tableName, id);
|
|
1183
1923
|
builderFn(builder);
|
|
1184
1924
|
const { id: opId, checkVersion, set } = builder.build();
|
|
1185
1925
|
|
|
1186
|
-
this.#
|
|
1926
|
+
this.#uow.addMutationOperation({
|
|
1187
1927
|
type: "update",
|
|
1188
|
-
|
|
1928
|
+
schema: this.#schema,
|
|
1929
|
+
namespace: this.#namespace,
|
|
1930
|
+
table: tableName,
|
|
1189
1931
|
id: opId,
|
|
1190
1932
|
checkVersion,
|
|
1191
1933
|
set,
|
|
1192
1934
|
});
|
|
1193
1935
|
}
|
|
1194
1936
|
|
|
1195
|
-
/**
|
|
1196
|
-
* Add a delete operation using a builder callback (mutation phase only)
|
|
1197
|
-
*/
|
|
1198
1937
|
delete<TableName extends keyof TSchema["tables"] & string>(
|
|
1199
|
-
|
|
1938
|
+
tableName: TableName,
|
|
1200
1939
|
id: FragnoId | string,
|
|
1201
|
-
builderFn?: (
|
|
1202
|
-
// We omit "build" because we don't want to expose it to the user
|
|
1203
|
-
builder: Omit<DeleteBuilder, "build">,
|
|
1204
|
-
) => Omit<DeleteBuilder, "build"> | void,
|
|
1940
|
+
builderFn?: (builder: Omit<DeleteBuilder, "build">) => Omit<DeleteBuilder, "build"> | void,
|
|
1205
1941
|
): void {
|
|
1206
|
-
|
|
1207
|
-
throw new Error(`delete() can only be called during mutation phase.`);
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
// Create builder, optionally pass to callback, then extract configuration
|
|
1211
|
-
const builder = new DeleteBuilder(table, id);
|
|
1942
|
+
const builder = new DeleteBuilder(tableName, id);
|
|
1212
1943
|
builderFn?.(builder);
|
|
1213
1944
|
const { id: opId, checkVersion } = builder.build();
|
|
1214
1945
|
|
|
1215
|
-
this.#
|
|
1946
|
+
this.#uow.addMutationOperation({
|
|
1216
1947
|
type: "delete",
|
|
1217
|
-
|
|
1948
|
+
schema: this.#schema,
|
|
1949
|
+
namespace: this.#namespace,
|
|
1950
|
+
table: tableName,
|
|
1218
1951
|
id: opId,
|
|
1219
1952
|
checkVersion,
|
|
1220
1953
|
});
|
|
1221
1954
|
}
|
|
1222
1955
|
|
|
1223
1956
|
/**
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*/
|
|
1227
|
-
async executeMutations(): Promise<{ success: boolean }> {
|
|
1228
|
-
if (this.#state === "executed") {
|
|
1229
|
-
throw new Error(`Cannot execute mutations from state ${this.#state}.`);
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// Compile mutation operations
|
|
1233
|
-
const mutationBatch: CompiledMutation<unknown>[] = [];
|
|
1234
|
-
for (const op of this.#mutationOps) {
|
|
1235
|
-
const compiled = this.#compiler.compileMutationOperation(op);
|
|
1236
|
-
if (compiled !== null) {
|
|
1237
|
-
this.#config?.onQuery?.(compiled);
|
|
1238
|
-
mutationBatch.push(compiled);
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
if (this.#config?.dryRun) {
|
|
1243
|
-
this.#state = "executed";
|
|
1244
|
-
return {
|
|
1245
|
-
success: true,
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Execute mutation phase
|
|
1250
|
-
const result = await this.#executor.executeMutationPhase(mutationBatch);
|
|
1251
|
-
this.#state = "executed";
|
|
1252
|
-
|
|
1253
|
-
if (result.success) {
|
|
1254
|
-
this.#createdInternalIds = result.createdInternalIds;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
return {
|
|
1258
|
-
success: result.success,
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
/**
|
|
1263
|
-
* Get the retrieval operations (for inspection/debugging)
|
|
1264
|
-
*/
|
|
1265
|
-
getRetrievalOperations(): ReadonlyArray<RetrievalOperation<TSchema>> {
|
|
1266
|
-
return this.#retrievalOps;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
/**
|
|
1270
|
-
* Get the mutation operations (for inspection/debugging)
|
|
1271
|
-
*/
|
|
1272
|
-
getMutationOperations(): ReadonlyArray<MutationOperation<TSchema>> {
|
|
1273
|
-
return this.#mutationOps;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
/**
|
|
1277
|
-
* Get the IDs of created entities after executeMutations() has been called.
|
|
1278
|
-
* Returns FragnoId objects with external IDs (always available) and internal IDs
|
|
1279
|
-
* (available when database supports RETURNING).
|
|
1957
|
+
* Check that a record's version hasn't changed since retrieval.
|
|
1958
|
+
* This is useful for ensuring related records remain unchanged during a transaction.
|
|
1280
1959
|
*
|
|
1281
|
-
* @
|
|
1282
|
-
* @
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
let createIndex = 0;
|
|
1293
|
-
|
|
1294
|
-
for (const op of this.#mutationOps) {
|
|
1295
|
-
if (op.type === "create") {
|
|
1296
|
-
const internalId = this.#createdInternalIds[createIndex] ?? undefined;
|
|
1297
|
-
createdIds.push(
|
|
1298
|
-
new FragnoId({
|
|
1299
|
-
externalId: op.generatedExternalId,
|
|
1300
|
-
internalId,
|
|
1301
|
-
version: 0, // New records always start at version 0
|
|
1302
|
-
}),
|
|
1303
|
-
);
|
|
1304
|
-
createIndex++;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
return createdIds;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
/**
|
|
1312
|
-
* @internal
|
|
1313
|
-
* Compile the unit of work to executable queries for testing
|
|
1960
|
+
* @param tableName - The table name
|
|
1961
|
+
* @param id - The FragnoId with version information (string IDs are not allowed)
|
|
1962
|
+
* @throws Error if the ID is a string without version information
|
|
1963
|
+
*
|
|
1964
|
+
* @example
|
|
1965
|
+
* ```ts
|
|
1966
|
+
* // Ensure both accounts haven't changed before creating a transfer
|
|
1967
|
+
* uow.check("accounts", fromAccount.id);
|
|
1968
|
+
* uow.check("accounts", toAccount.id);
|
|
1969
|
+
* uow.create("transactions", { fromAccountId, toAccountId, amount });
|
|
1970
|
+
* ```
|
|
1314
1971
|
*/
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
const mutationBatch: CompiledMutation<TOutput>[] = [];
|
|
1329
|
-
for (const op of this.#mutationOps) {
|
|
1330
|
-
const compiled = compiler.compileMutationOperation(op);
|
|
1331
|
-
if (compiled !== null) {
|
|
1332
|
-
mutationBatch.push(compiled);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
return {
|
|
1337
|
-
name: this.#name,
|
|
1338
|
-
retrievalBatch,
|
|
1339
|
-
mutationBatch,
|
|
1340
|
-
};
|
|
1972
|
+
check<TableName extends keyof TSchema["tables"] & string>(
|
|
1973
|
+
tableName: TableName,
|
|
1974
|
+
id: FragnoId,
|
|
1975
|
+
): void {
|
|
1976
|
+
this.#uow.addMutationOperation({
|
|
1977
|
+
type: "check",
|
|
1978
|
+
schema: this.#schema,
|
|
1979
|
+
namespace: this.#namespace,
|
|
1980
|
+
table: tableName,
|
|
1981
|
+
id,
|
|
1982
|
+
});
|
|
1341
1983
|
}
|
|
1342
1984
|
}
|