effect-dynamodb 0.1.0
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/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/Aggregate.d.ts +210 -0
- package/dist/Aggregate.d.ts.map +1 -0
- package/dist/Aggregate.js +1169 -0
- package/dist/Aggregate.js.map +1 -0
- package/dist/Batch.d.ts +62 -0
- package/dist/Batch.d.ts.map +1 -0
- package/dist/Batch.js +218 -0
- package/dist/Batch.js.map +1 -0
- package/dist/Collection.d.ts +58 -0
- package/dist/Collection.d.ts.map +1 -0
- package/dist/Collection.js +195 -0
- package/dist/Collection.js.map +1 -0
- package/dist/Collections.d.ts +121 -0
- package/dist/Collections.d.ts.map +1 -0
- package/dist/Collections.js +121 -0
- package/dist/Collections.js.map +1 -0
- package/dist/DynamoClient.d.ts +317 -0
- package/dist/DynamoClient.d.ts.map +1 -0
- package/dist/DynamoClient.js +653 -0
- package/dist/DynamoClient.js.map +1 -0
- package/dist/DynamoModel.d.ts +278 -0
- package/dist/DynamoModel.d.ts.map +1 -0
- package/dist/DynamoModel.js +453 -0
- package/dist/DynamoModel.js.map +1 -0
- package/dist/DynamoSchema.d.ts +137 -0
- package/dist/DynamoSchema.d.ts.map +1 -0
- package/dist/DynamoSchema.js +188 -0
- package/dist/DynamoSchema.js.map +1 -0
- package/dist/Entity.d.ts +628 -0
- package/dist/Entity.d.ts.map +1 -0
- package/dist/Entity.js +2371 -0
- package/dist/Entity.js.map +1 -0
- package/dist/Errors.d.ts +227 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +93 -0
- package/dist/Errors.js.map +1 -0
- package/dist/EventStore.d.ts +208 -0
- package/dist/EventStore.d.ts.map +1 -0
- package/dist/EventStore.js +333 -0
- package/dist/EventStore.js.map +1 -0
- package/dist/Expression.d.ts +50 -0
- package/dist/Expression.d.ts.map +1 -0
- package/dist/Expression.js +170 -0
- package/dist/Expression.js.map +1 -0
- package/dist/KeyComposer.d.ts +125 -0
- package/dist/KeyComposer.d.ts.map +1 -0
- package/dist/KeyComposer.js +257 -0
- package/dist/KeyComposer.js.map +1 -0
- package/dist/Marshaller.d.ts +30 -0
- package/dist/Marshaller.d.ts.map +1 -0
- package/dist/Marshaller.js +115 -0
- package/dist/Marshaller.js.map +1 -0
- package/dist/Projection.d.ts +28 -0
- package/dist/Projection.d.ts.map +1 -0
- package/dist/Projection.js +39 -0
- package/dist/Projection.js.map +1 -0
- package/dist/Query.d.ts +214 -0
- package/dist/Query.d.ts.map +1 -0
- package/dist/Query.js +397 -0
- package/dist/Query.js.map +1 -0
- package/dist/Table.d.ts +178 -0
- package/dist/Table.d.ts.map +1 -0
- package/dist/Table.js +211 -0
- package/dist/Table.js.map +1 -0
- package/dist/Transaction.d.ts +79 -0
- package/dist/Transaction.d.ts.map +1 -0
- package/dist/Transaction.js +215 -0
- package/dist/Transaction.js.map +1 -0
- package/dist/_AggregateCursor.d.ts +44 -0
- package/dist/_AggregateCursor.d.ts.map +1 -0
- package/dist/_AggregateCursor.js +18 -0
- package/dist/_AggregateCursor.js.map +1 -0
- package/dist/_AggregateEdges.d.ts +77 -0
- package/dist/_AggregateEdges.d.ts.map +1 -0
- package/dist/_AggregateEdges.js +41 -0
- package/dist/_AggregateEdges.js.map +1 -0
- package/dist/_AggregateSchemas.d.ts +69 -0
- package/dist/_AggregateSchemas.d.ts.map +1 -0
- package/dist/_AggregateSchemas.js +194 -0
- package/dist/_AggregateSchemas.js.map +1 -0
- package/dist/_AggregateTypes.d.ts +94 -0
- package/dist/_AggregateTypes.d.ts.map +1 -0
- package/dist/_AggregateTypes.js +7 -0
- package/dist/_AggregateTypes.js.map +1 -0
- package/dist/_EntityCombinators.d.ts +155 -0
- package/dist/_EntityCombinators.d.ts.map +1 -0
- package/dist/_EntityCombinators.js +158 -0
- package/dist/_EntityCombinators.js.map +1 -0
- package/dist/_EntityConfig.d.ts +100 -0
- package/dist/_EntityConfig.d.ts.map +1 -0
- package/dist/_EntityConfig.js +7 -0
- package/dist/_EntityConfig.js.map +1 -0
- package/dist/_EntityOps.d.ts +205 -0
- package/dist/_EntityOps.d.ts.map +1 -0
- package/dist/_EntityOps.js +137 -0
- package/dist/_EntityOps.js.map +1 -0
- package/dist/_EntitySchemas.d.ts +57 -0
- package/dist/_EntitySchemas.d.ts.map +1 -0
- package/dist/_EntitySchemas.js +298 -0
- package/dist/_EntitySchemas.js.map +1 -0
- package/dist/_EntityTypes.d.ts +81 -0
- package/dist/_EntityTypes.d.ts.map +1 -0
- package/dist/_EntityTypes.js +7 -0
- package/dist/_EntityTypes.js.map +1 -0
- package/dist/_sparse_check.d.ts +2 -0
- package/dist/_sparse_check.d.ts.map +1 -0
- package/dist/_sparse_check.js +193 -0
- package/dist/_sparse_check.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/AggregateCursor.d.ts +44 -0
- package/dist/internal/AggregateCursor.d.ts.map +1 -0
- package/dist/internal/AggregateCursor.js +18 -0
- package/dist/internal/AggregateCursor.js.map +1 -0
- package/dist/internal/AggregateEdges.d.ts +77 -0
- package/dist/internal/AggregateEdges.d.ts.map +1 -0
- package/dist/internal/AggregateEdges.js +41 -0
- package/dist/internal/AggregateEdges.js.map +1 -0
- package/dist/internal/AggregateSchemas.d.ts +69 -0
- package/dist/internal/AggregateSchemas.d.ts.map +1 -0
- package/dist/internal/AggregateSchemas.js +193 -0
- package/dist/internal/AggregateSchemas.js.map +1 -0
- package/dist/internal/AggregateTypes.d.ts +94 -0
- package/dist/internal/AggregateTypes.d.ts.map +1 -0
- package/dist/internal/AggregateTypes.js +7 -0
- package/dist/internal/AggregateTypes.js.map +1 -0
- package/dist/internal/BoundQuery.d.ts +107 -0
- package/dist/internal/BoundQuery.d.ts.map +1 -0
- package/dist/internal/BoundQuery.js +111 -0
- package/dist/internal/BoundQuery.js.map +1 -0
- package/dist/internal/EntityCombinators.d.ts +173 -0
- package/dist/internal/EntityCombinators.d.ts.map +1 -0
- package/dist/internal/EntityCombinators.js +183 -0
- package/dist/internal/EntityCombinators.js.map +1 -0
- package/dist/internal/EntityConfig.d.ts +100 -0
- package/dist/internal/EntityConfig.d.ts.map +1 -0
- package/dist/internal/EntityConfig.js +7 -0
- package/dist/internal/EntityConfig.js.map +1 -0
- package/dist/internal/EntityOps.d.ts +268 -0
- package/dist/internal/EntityOps.d.ts.map +1 -0
- package/dist/internal/EntityOps.js +153 -0
- package/dist/internal/EntityOps.js.map +1 -0
- package/dist/internal/EntitySchemas.d.ts +57 -0
- package/dist/internal/EntitySchemas.d.ts.map +1 -0
- package/dist/internal/EntitySchemas.js +299 -0
- package/dist/internal/EntitySchemas.js.map +1 -0
- package/dist/internal/EntityTypes.d.ts +128 -0
- package/dist/internal/EntityTypes.d.ts.map +1 -0
- package/dist/internal/EntityTypes.js +7 -0
- package/dist/internal/EntityTypes.js.map +1 -0
- package/dist/internal/Expr.d.ts +141 -0
- package/dist/internal/Expr.d.ts.map +1 -0
- package/dist/internal/Expr.js +332 -0
- package/dist/internal/Expr.js.map +1 -0
- package/dist/internal/PathBuilder.d.ts +88 -0
- package/dist/internal/PathBuilder.d.ts.map +1 -0
- package/dist/internal/PathBuilder.js +100 -0
- package/dist/internal/PathBuilder.js.map +1 -0
- package/dist/internal/TransactableOps.d.ts +25 -0
- package/dist/internal/TransactableOps.d.ts.map +1 -0
- package/dist/internal/TransactableOps.js +58 -0
- package/dist/internal/TransactableOps.js.map +1 -0
- package/dist/internal/sparse_check.d.ts +2 -0
- package/dist/internal/sparse_check.d.ts.map +1 -0
- package/dist/internal/sparse_check.js +196 -0
- package/dist/internal/sparse_check.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate — Graph-based composite domain model for DynamoDB.
|
|
3
|
+
*
|
|
4
|
+
* An Aggregate binds a Schema.Class hierarchy to a graph of underlying entities
|
|
5
|
+
* sharing a partition key. The read path queries the collection, discriminates
|
|
6
|
+
* items by entity type + discriminators, and assembles them leaves-to-root into
|
|
7
|
+
* a Schema.Class instance.
|
|
8
|
+
*
|
|
9
|
+
* Created via {@link make}. Sub-aggregates compose recursively via `.with()`.
|
|
10
|
+
*/
|
|
11
|
+
import { Effect, Schema, SchemaAST } from "effect";
|
|
12
|
+
import * as Batch from "./Batch.js";
|
|
13
|
+
import { DynamoClient } from "./DynamoClient.js";
|
|
14
|
+
import * as DynamoModel from "./DynamoModel.js";
|
|
15
|
+
import { composeCollectionKey, composeKey } from "./DynamoSchema.js";
|
|
16
|
+
import { AggregateAssemblyError, AggregateTransactionOverflow, RefNotFound, TransactionCancelled, ValidationError, } from "./Errors.js";
|
|
17
|
+
import * as KeyComposer from "./KeyComposer.js";
|
|
18
|
+
import { deserializeDateFromDynamo, fromAttributeMap, serializeDateForDynamo, toAttributeMap, toAttributeValue, } from "./Marshaller.js";
|
|
19
|
+
// Internal modules (decomposed from Aggregate.ts)
|
|
20
|
+
export { isManyEdge, isOneEdge, isRefEdge, many, one, ref, } from "./internal/AggregateEdges.js";
|
|
21
|
+
export { deriveAggregateSchemas, deriveElementInputSchema, deriveEntityFieldName, extractArrayElement, getSchemaFields, isFieldOptional, isSchemaMatchingEntity, unwrapModel, } from "./internal/AggregateSchemas.js";
|
|
22
|
+
import { makeCursor, } from "./internal/AggregateCursor.js";
|
|
23
|
+
import { deriveAggregateSchemas, deriveEntityFieldName } from "./internal/AggregateSchemas.js";
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// TypeId
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
export const TypeId = Symbol.for("effect-dynamodb/Aggregate");
|
|
28
|
+
export function make(schema, config) {
|
|
29
|
+
if (!("table" in config) || config.table === undefined) {
|
|
30
|
+
// Sub-aggregate form
|
|
31
|
+
return makeSubAggregate(schema, config);
|
|
32
|
+
}
|
|
33
|
+
// Top-level form
|
|
34
|
+
return makeAggregate(schema, config);
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Sub-aggregate construction
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
const makeSubAggregate = (schema, config) => {
|
|
40
|
+
const sub = {
|
|
41
|
+
_tag: "SubAggregate",
|
|
42
|
+
schema,
|
|
43
|
+
root: config.root,
|
|
44
|
+
edges: config.edges,
|
|
45
|
+
with: (discConfig) => ({
|
|
46
|
+
_tag: "BoundSubAggregate",
|
|
47
|
+
aggregate: sub,
|
|
48
|
+
discriminator: discConfig.discriminator,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
return sub;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Try to match a date encoding from a SchemaAST.resolve() result.
|
|
55
|
+
* SchemaAST.resolve flattens annotations to the top level (typeConstructor, meta).
|
|
56
|
+
*/
|
|
57
|
+
const matchResolvedDateAST = (resolved) => {
|
|
58
|
+
const tc = resolved.typeConstructor;
|
|
59
|
+
if (tc?._tag === "effect/DateTime.Utc")
|
|
60
|
+
return { storage: "string", domain: "DateTime.Utc" };
|
|
61
|
+
if (tc?._tag === "effect/DateTime.Zoned")
|
|
62
|
+
return { storage: "string", domain: "DateTime.Zoned" };
|
|
63
|
+
if (tc?._tag === "Date")
|
|
64
|
+
return { storage: "string", domain: "Date" };
|
|
65
|
+
const meta = resolved.meta;
|
|
66
|
+
if (meta?._tag === "isDateValid")
|
|
67
|
+
return { storage: "string", domain: "Date" };
|
|
68
|
+
return undefined;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Try to match a date encoding from a raw AST member (Union type member).
|
|
72
|
+
* Raw AST nodes have annotations nested under .annotations (not flattened).
|
|
73
|
+
*/
|
|
74
|
+
const matchRawDateAST = (node) => {
|
|
75
|
+
const ann = node.annotations;
|
|
76
|
+
if (!ann)
|
|
77
|
+
return undefined;
|
|
78
|
+
const tc = ann.typeConstructor;
|
|
79
|
+
if (tc?._tag === "effect/DateTime.Utc")
|
|
80
|
+
return { storage: "string", domain: "DateTime.Utc" };
|
|
81
|
+
if (tc?._tag === "effect/DateTime.Zoned")
|
|
82
|
+
return { storage: "string", domain: "DateTime.Zoned" };
|
|
83
|
+
if (tc?._tag === "Date")
|
|
84
|
+
return { storage: "string", domain: "Date" };
|
|
85
|
+
const meta = ann.meta;
|
|
86
|
+
if (meta?._tag === "isDateValid")
|
|
87
|
+
return { storage: "string", domain: "Date" };
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Infer date encoding from a schema AST, handling Schema.optional() wrappers.
|
|
92
|
+
* Optional fields have Union AST with [InnerType, Undefined] — unwrap to find dates.
|
|
93
|
+
*/
|
|
94
|
+
const inferDateEncoding = (ast) => {
|
|
95
|
+
// Try SchemaAST.resolve first (works for non-optional date fields)
|
|
96
|
+
const resolved = SchemaAST.resolve(ast);
|
|
97
|
+
if (resolved) {
|
|
98
|
+
const direct = matchResolvedDateAST(resolved);
|
|
99
|
+
if (direct)
|
|
100
|
+
return direct;
|
|
101
|
+
}
|
|
102
|
+
// Handle optional wrapper: Union with Undefined + inner type.
|
|
103
|
+
// SchemaAST.resolve returns undefined for Union ASTs, so check the raw AST.
|
|
104
|
+
const rawAst = ast;
|
|
105
|
+
if (rawAst._tag === "Union" && rawAst.types) {
|
|
106
|
+
for (const member of rawAst.types) {
|
|
107
|
+
if (member._tag === "Undefined")
|
|
108
|
+
continue;
|
|
109
|
+
// Try SchemaAST.resolve on the inner member (e.g., Declaration for Schema.Date)
|
|
110
|
+
const memberResolved = SchemaAST.resolve(member);
|
|
111
|
+
if (memberResolved) {
|
|
112
|
+
const enc = matchResolvedDateAST(memberResolved);
|
|
113
|
+
if (enc)
|
|
114
|
+
return enc;
|
|
115
|
+
}
|
|
116
|
+
// Fallback: check raw member annotations
|
|
117
|
+
const enc = matchRawDateAST(member);
|
|
118
|
+
if (enc)
|
|
119
|
+
return enc;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
};
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Top-level aggregate construction
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
const makeAggregate = (schema, config) => {
|
|
128
|
+
// Build resolved graph by walking edges recursively
|
|
129
|
+
const rootNode = resolveNode(null, config.root.entityType, "root", config.edges);
|
|
130
|
+
const aggregateName = config.root.entityType;
|
|
131
|
+
const contextFields = config.context ?? [];
|
|
132
|
+
// Detect date encodings for all root schema fields (once, at make() time).
|
|
133
|
+
// Fields with Date/DateTime types need serialization before DynamoDB
|
|
134
|
+
// marshalling and deserialization before Schema decode, matching Entity's
|
|
135
|
+
// serializeDateFields/deserializeDateFields pattern.
|
|
136
|
+
const dateEncodings = {};
|
|
137
|
+
const schemaFields = schema.fields;
|
|
138
|
+
if (schemaFields) {
|
|
139
|
+
for (const field of Object.keys(schemaFields)) {
|
|
140
|
+
const fieldSchema = schemaFields[field];
|
|
141
|
+
const explicit = DynamoModel.getEncoding(fieldSchema);
|
|
142
|
+
if (explicit) {
|
|
143
|
+
dateEncodings[field] = explicit;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Infer for standard Effect date schemas (Schema.Date, DateTime, etc.)
|
|
147
|
+
// For optional fields (Union with Undefined), unwrap to find the inner type.
|
|
148
|
+
const inferred = inferDateEncoding(fieldSchema.ast);
|
|
149
|
+
if (inferred)
|
|
150
|
+
dateEncodings[field] = inferred;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Build optics at construction time (once per aggregate definition)
|
|
155
|
+
const classToPlain = Schema.toIso(schema);
|
|
156
|
+
const opticRoot = Schema.toIsoFocus(schema);
|
|
157
|
+
/** Shared: compose PK and query all items */
|
|
158
|
+
const fetchPartition = (key) => Effect.gen(function* () {
|
|
159
|
+
const client = yield* DynamoClient;
|
|
160
|
+
const tableConfig = yield* config.table.Tag;
|
|
161
|
+
const composites = KeyComposer.extractComposites(config.pk.composite, key);
|
|
162
|
+
const pkValue = composeCollectionKey(config.schema, config.collection.name, composites);
|
|
163
|
+
const allItems = yield* queryAllItems(client, tableConfig.name, config.collection.index, config.pk.field, pkValue);
|
|
164
|
+
return { client, tableConfig, pkValue, composites, allItems };
|
|
165
|
+
});
|
|
166
|
+
const derivedSchemas = deriveAggregateSchemas(schema, config.edges, config.pk.composite);
|
|
167
|
+
return {
|
|
168
|
+
[TypeId]: TypeId,
|
|
169
|
+
_tag: "Aggregate",
|
|
170
|
+
_tableTag: config.table.Tag,
|
|
171
|
+
schema,
|
|
172
|
+
pkField: config.pk.field,
|
|
173
|
+
collection: {
|
|
174
|
+
index: config.collection.index,
|
|
175
|
+
sk: { field: config.collection.sk.field },
|
|
176
|
+
},
|
|
177
|
+
listIndex: config.list
|
|
178
|
+
? {
|
|
179
|
+
index: config.list.index,
|
|
180
|
+
pk: { field: config.list.pk.field },
|
|
181
|
+
sk: { field: config.list.sk.field },
|
|
182
|
+
}
|
|
183
|
+
: undefined,
|
|
184
|
+
inputSchema: derivedSchemas.inputSchema,
|
|
185
|
+
createSchema: derivedSchemas.inputSchema,
|
|
186
|
+
updateSchema: derivedSchemas.updateSchema,
|
|
187
|
+
get: (key) => Effect.gen(function* () {
|
|
188
|
+
const { allItems } = yield* fetchPartition(key);
|
|
189
|
+
if (allItems.length === 0) {
|
|
190
|
+
return yield* new AggregateAssemblyError({
|
|
191
|
+
aggregate: aggregateName,
|
|
192
|
+
reason: "No items found for aggregate key",
|
|
193
|
+
key,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const result = yield* assembleAggregate(schema, rootNode, allItems, contextFields, dateEncodings, key, aggregateName);
|
|
197
|
+
return result;
|
|
198
|
+
}),
|
|
199
|
+
create: (input) => Effect.gen(function* () {
|
|
200
|
+
const client = yield* DynamoClient;
|
|
201
|
+
const tableConfig = yield* config.table.Tag;
|
|
202
|
+
// 1. Hydrate refs from edges
|
|
203
|
+
const hydrated = yield* hydrateAggregateRefs(input, config.edges, aggregateName);
|
|
204
|
+
// 2. Validate via schema decode
|
|
205
|
+
const decoded = yield* Schema.decodeUnknownEffect(schema)(hydrated).pipe(Effect.mapError((cause) => new ValidationError({
|
|
206
|
+
entityType: aggregateName,
|
|
207
|
+
operation: "aggregate.create",
|
|
208
|
+
cause,
|
|
209
|
+
})));
|
|
210
|
+
const assembled = { ...decoded };
|
|
211
|
+
// 3. Compose PK
|
|
212
|
+
const composites = KeyComposer.extractComposites(config.pk.composite, assembled);
|
|
213
|
+
const pkValue = composeCollectionKey(config.schema, config.collection.name, composites);
|
|
214
|
+
// 4. Decompose into items grouped by sub-aggregate transaction boundaries
|
|
215
|
+
const groups = yield* decomposeAggregate(assembled, rootNode, contextFields, dateEncodings, aggregateName);
|
|
216
|
+
// 5. Compose collection SK composites for root item
|
|
217
|
+
const collectionSkComposites = KeyComposer.extractComposites(config.collection.sk.composite, assembled);
|
|
218
|
+
// 6. Build DynamoDB items with composed keys
|
|
219
|
+
const dynamoGroups = buildDynamoItems(groups, config, pkValue, collectionSkComposites);
|
|
220
|
+
// 7. Write via sub-aggregate transactions
|
|
221
|
+
yield* writeTransactionGroups(client, tableConfig.name, dynamoGroups, aggregateName);
|
|
222
|
+
return decoded;
|
|
223
|
+
}),
|
|
224
|
+
update: (key, mutationFn) => Effect.gen(function* () {
|
|
225
|
+
const { client, tableConfig, pkValue, allItems } = yield* fetchPartition(key);
|
|
226
|
+
if (allItems.length === 0) {
|
|
227
|
+
return yield* new AggregateAssemblyError({
|
|
228
|
+
aggregate: aggregateName,
|
|
229
|
+
reason: "No items found for aggregate key",
|
|
230
|
+
key,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// 1. Assemble current state
|
|
234
|
+
const current = yield* assembleAggregate(schema, rootNode, allItems, contextFields, dateEncodings, key, aggregateName);
|
|
235
|
+
// 2. Apply mutation — provide optics context for composable updates
|
|
236
|
+
const state = classToPlain.get(current);
|
|
237
|
+
const updated = mutationFn({
|
|
238
|
+
state,
|
|
239
|
+
cursor: makeCursor(state, opticRoot),
|
|
240
|
+
optic: opticRoot,
|
|
241
|
+
current: current,
|
|
242
|
+
});
|
|
243
|
+
// 3. Validate updated state via schema decode
|
|
244
|
+
const decoded = yield* Schema.decodeUnknownEffect(schema)(updated).pipe(Effect.mapError((cause) => new ValidationError({
|
|
245
|
+
entityType: aggregateName,
|
|
246
|
+
operation: "aggregate.update",
|
|
247
|
+
cause,
|
|
248
|
+
})));
|
|
249
|
+
const assembledOld = { ...current };
|
|
250
|
+
const assembledNew = { ...decoded };
|
|
251
|
+
// 4. Decompose both old and new
|
|
252
|
+
const oldGroups = yield* decomposeAggregate(assembledOld, rootNode, contextFields, dateEncodings, aggregateName);
|
|
253
|
+
const newGroups = yield* decomposeAggregate(assembledNew, rootNode, contextFields, dateEncodings, aggregateName);
|
|
254
|
+
// 5. Diff: determine which transaction groups changed
|
|
255
|
+
const oldGroupMap = new Map(oldGroups.map((g) => [g.name, g]));
|
|
256
|
+
const newGroupMap = new Map(newGroups.map((g) => [g.name, g]));
|
|
257
|
+
// Check if context fields changed — if so, ALL groups must be rewritten
|
|
258
|
+
let contextChanged = false;
|
|
259
|
+
for (const field of contextFields) {
|
|
260
|
+
if (!deepEqual(assembledOld[field], assembledNew[field])) {
|
|
261
|
+
contextChanged = true;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const groupsToWrite = [];
|
|
266
|
+
for (const [name, newGroup] of newGroupMap) {
|
|
267
|
+
if (contextChanged) {
|
|
268
|
+
groupsToWrite.push(newGroup);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const oldGroup = oldGroupMap.get(name);
|
|
272
|
+
if (!oldGroup || !deepEqualGroups(oldGroup, newGroup)) {
|
|
273
|
+
groupsToWrite.push(newGroup);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (groupsToWrite.length > 0) {
|
|
277
|
+
// 6. Compose collection SK composites
|
|
278
|
+
const collectionSkComposites = KeyComposer.extractComposites(config.collection.sk.composite, assembledNew);
|
|
279
|
+
// 7. Build and write changed groups
|
|
280
|
+
const dynamoGroups = buildDynamoItems(groupsToWrite, config, pkValue, collectionSkComposites);
|
|
281
|
+
yield* writeTransactionGroups(client, tableConfig.name, dynamoGroups, aggregateName);
|
|
282
|
+
}
|
|
283
|
+
return decoded;
|
|
284
|
+
}),
|
|
285
|
+
delete: (key) => Effect.gen(function* () {
|
|
286
|
+
const { client, tableConfig, allItems } = yield* fetchPartition(key);
|
|
287
|
+
if (allItems.length === 0) {
|
|
288
|
+
return yield* new AggregateAssemblyError({
|
|
289
|
+
aggregate: aggregateName,
|
|
290
|
+
reason: "No items found for aggregate key",
|
|
291
|
+
key,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
yield* deleteAllItems(client, tableConfig.name, allItems, config.pk.field);
|
|
295
|
+
}),
|
|
296
|
+
list: (filter, options) => Effect.gen(function* () {
|
|
297
|
+
const client = yield* DynamoClient;
|
|
298
|
+
const tableConfig = yield* config.table.Tag;
|
|
299
|
+
if (!config.list) {
|
|
300
|
+
return yield* new ValidationError({
|
|
301
|
+
entityType: aggregateName,
|
|
302
|
+
operation: "list",
|
|
303
|
+
cause: "Aggregate.list() requires a `list` collection config. " +
|
|
304
|
+
"Define `list: { index, name, pk, sk }` in your aggregate config.",
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const listConfig = config.list;
|
|
308
|
+
const limit = options?.limit;
|
|
309
|
+
const startKey = options?.cursor
|
|
310
|
+
? JSON.parse(atob(options.cursor))
|
|
311
|
+
: undefined;
|
|
312
|
+
// Compose PK from filter values matching PK composites
|
|
313
|
+
const listPkComposites = KeyComposer.extractComposites(listConfig.pk.composite, filter ?? {});
|
|
314
|
+
// Build SK prefix from contiguous filter values matching SK composites
|
|
315
|
+
const skValues = [];
|
|
316
|
+
for (const attr of listConfig.sk.composite) {
|
|
317
|
+
if (filter?.[attr] !== undefined)
|
|
318
|
+
skValues.push(String(filter[attr]));
|
|
319
|
+
else
|
|
320
|
+
break; // Stop at first gap (prefix matching)
|
|
321
|
+
}
|
|
322
|
+
let rootItems;
|
|
323
|
+
let nextKey;
|
|
324
|
+
if (listConfig.cardinality) {
|
|
325
|
+
// Fan out: query each shard in parallel, merge results
|
|
326
|
+
// Pagination not supported for sharded lists — returns all results
|
|
327
|
+
const shardQueries = Array.from({ length: listConfig.cardinality }, (_, shard) => {
|
|
328
|
+
const shardPkValue = composeCollectionKey(config.schema, listConfig.name, [
|
|
329
|
+
...listPkComposites,
|
|
330
|
+
String(shard),
|
|
331
|
+
]);
|
|
332
|
+
return queryListPartition(client, tableConfig, listConfig, config.schema, shardPkValue, skValues);
|
|
333
|
+
});
|
|
334
|
+
const shardResults = yield* Effect.all(shardQueries);
|
|
335
|
+
rootItems = shardResults.flatMap((r) => r.items);
|
|
336
|
+
nextKey = undefined;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const listPkValue = composeCollectionKey(config.schema, listConfig.name, listPkComposites);
|
|
340
|
+
const result = yield* queryListPartition(client, tableConfig, listConfig, config.schema, listPkValue, skValues, limit, startKey);
|
|
341
|
+
rootItems = result.items;
|
|
342
|
+
nextKey = result.lastKey;
|
|
343
|
+
}
|
|
344
|
+
// Extract PK composite values from each root item and assemble
|
|
345
|
+
const aggregates = [];
|
|
346
|
+
for (const rootItem of rootItems) {
|
|
347
|
+
const key = {};
|
|
348
|
+
for (const composite of config.pk.composite) {
|
|
349
|
+
key[composite] = rootItem[composite];
|
|
350
|
+
}
|
|
351
|
+
const assembled = yield* Effect.gen(function* () {
|
|
352
|
+
const { allItems } = yield* fetchPartition(key);
|
|
353
|
+
if (allItems.length === 0)
|
|
354
|
+
return undefined;
|
|
355
|
+
const result = yield* assembleAggregate(schema, rootNode, allItems, contextFields, dateEncodings, key, aggregateName);
|
|
356
|
+
return result;
|
|
357
|
+
});
|
|
358
|
+
if (assembled !== undefined)
|
|
359
|
+
aggregates.push(assembled);
|
|
360
|
+
}
|
|
361
|
+
const cursor = nextKey ? btoa(JSON.stringify(nextKey)) : null;
|
|
362
|
+
return { data: aggregates, cursor };
|
|
363
|
+
}),
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Aggregate binding — resolve services, return BoundAggregate with R = never
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
/**
|
|
370
|
+
* Bind an Aggregate to resolved `DynamoClient` and `TableConfig` services.
|
|
371
|
+
* Returns a {@link BoundAggregate} where all operations have `R = never`.
|
|
372
|
+
*
|
|
373
|
+
* @internal Used by `DynamoClient.make()` to bind aggregates.
|
|
374
|
+
*/
|
|
375
|
+
export const bind = (aggregate) => Effect.gen(function* () {
|
|
376
|
+
const ctx = yield* Effect.context();
|
|
377
|
+
const provide = (effect) => Effect.provide(effect, ctx);
|
|
378
|
+
return {
|
|
379
|
+
schema: aggregate.schema,
|
|
380
|
+
get: (key) => provide(aggregate.get(key)),
|
|
381
|
+
create: (input) => provide(aggregate.create(input)),
|
|
382
|
+
update: (key, mutationFn) => provide(aggregate.update(key, mutationFn)),
|
|
383
|
+
delete: (key) => provide(aggregate.delete(key)),
|
|
384
|
+
list: (filter, options) => provide(aggregate.list(filter, options)),
|
|
385
|
+
inputSchema: aggregate.inputSchema,
|
|
386
|
+
createSchema: aggregate.createSchema,
|
|
387
|
+
updateSchema: aggregate.updateSchema,
|
|
388
|
+
provide,
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// Internal: Deep equality helpers for diff-based update
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
const deepEqual = (a, b) => {
|
|
395
|
+
if (a === b)
|
|
396
|
+
return true;
|
|
397
|
+
if (a == null || b == null)
|
|
398
|
+
return false;
|
|
399
|
+
if (typeof a !== typeof b)
|
|
400
|
+
return false;
|
|
401
|
+
if (typeof a !== "object")
|
|
402
|
+
return false;
|
|
403
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
404
|
+
if (a.length !== b.length)
|
|
405
|
+
return false;
|
|
406
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
407
|
+
}
|
|
408
|
+
if (Array.isArray(a) || Array.isArray(b))
|
|
409
|
+
return false;
|
|
410
|
+
const aObj = a;
|
|
411
|
+
const bObj = b;
|
|
412
|
+
const aKeys = Object.keys(aObj);
|
|
413
|
+
const bKeys = Object.keys(bObj);
|
|
414
|
+
if (aKeys.length !== bKeys.length)
|
|
415
|
+
return false;
|
|
416
|
+
return aKeys.every((k) => deepEqual(aObj[k], bObj[k]));
|
|
417
|
+
};
|
|
418
|
+
const deepEqualGroups = (a, b) => {
|
|
419
|
+
if (a.items.length !== b.items.length)
|
|
420
|
+
return false;
|
|
421
|
+
return a.items.every((item, i) => {
|
|
422
|
+
const other = b.items[i];
|
|
423
|
+
if (item.entityType !== other.entityType)
|
|
424
|
+
return false;
|
|
425
|
+
return deepEqual(item.attributes, other.attributes);
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
// Internal: Resolve the graph
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
const resolveNode = (fieldName, entityType, cardinality, edges, discriminator, assemble, decompose, ownDiscriminator) => {
|
|
432
|
+
const children = [];
|
|
433
|
+
for (const [field, edge] of Object.entries(edges)) {
|
|
434
|
+
if ("_tag" in edge) {
|
|
435
|
+
if (edge._tag === "OneEdge") {
|
|
436
|
+
// Merge parent discriminator with edge's own discriminator
|
|
437
|
+
const mergedDisc = edge.discriminator
|
|
438
|
+
? { ...(discriminator ?? {}), ...edge.discriminator }
|
|
439
|
+
: discriminator;
|
|
440
|
+
children.push(resolveNode(field, edge.entityType, "one", {}, mergedDisc, undefined, undefined, edge.discriminator));
|
|
441
|
+
}
|
|
442
|
+
else if (edge._tag === "ManyEdge") {
|
|
443
|
+
children.push(resolveNode(field, edge.entityType, "many", {}, discriminator, edge.assemble, edge.decompose));
|
|
444
|
+
}
|
|
445
|
+
else if (edge._tag === "BoundSubAggregate") {
|
|
446
|
+
const bound = edge;
|
|
447
|
+
const subChildren = resolveNode(field, bound.aggregate.root.entityType, "one", bound.aggregate.edges, bound.discriminator, undefined, undefined, bound.discriminator);
|
|
448
|
+
children.push(subChildren);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
fieldName,
|
|
454
|
+
entityType,
|
|
455
|
+
cardinality,
|
|
456
|
+
discriminator,
|
|
457
|
+
ownDiscriminator,
|
|
458
|
+
children,
|
|
459
|
+
assemble,
|
|
460
|
+
decompose,
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// Internal: Query all items in the aggregate partition
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
const queryAllItems = (client, tableName, indexName, pkField, pkValue) => Effect.gen(function* () {
|
|
467
|
+
const allItems = [];
|
|
468
|
+
let exclusiveStartKey;
|
|
469
|
+
// Paginate through all results
|
|
470
|
+
do {
|
|
471
|
+
const result = yield* client.query({
|
|
472
|
+
TableName: tableName,
|
|
473
|
+
IndexName: indexName,
|
|
474
|
+
KeyConditionExpression: "#pk = :pk",
|
|
475
|
+
ExpressionAttributeNames: { "#pk": pkField },
|
|
476
|
+
ExpressionAttributeValues: { ":pk": toAttributeValue(pkValue) },
|
|
477
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
478
|
+
});
|
|
479
|
+
if (result.Items) {
|
|
480
|
+
for (const item of result.Items) {
|
|
481
|
+
allItems.push(fromAttributeMap(item));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
485
|
+
} while (exclusiveStartKey !== undefined);
|
|
486
|
+
return allItems;
|
|
487
|
+
});
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Internal: Assembly algorithm
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
const assembleAggregate = (schema, rootNode, allItems, _contextFields, dateEncodings, key, aggregateName) => Effect.gen(function* () {
|
|
492
|
+
// Find the root item
|
|
493
|
+
const rootItems = allItems.filter((item) => item.__edd_e__ === rootNode.entityType);
|
|
494
|
+
if (rootItems.length === 0) {
|
|
495
|
+
return yield* new AggregateAssemblyError({
|
|
496
|
+
aggregate: aggregateName,
|
|
497
|
+
reason: "Missing root item",
|
|
498
|
+
key,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (rootItems.length > 1) {
|
|
502
|
+
return yield* new AggregateAssemblyError({
|
|
503
|
+
aggregate: aggregateName,
|
|
504
|
+
reason: "Multiple root items found",
|
|
505
|
+
key,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
const rootItem = rootItems[0];
|
|
509
|
+
// Assemble edge values from the item collection
|
|
510
|
+
const edgeValues = {};
|
|
511
|
+
for (const child of rootNode.children) {
|
|
512
|
+
const assembled = yield* assembleNode(child, allItems, key, aggregateName);
|
|
513
|
+
if (child.fieldName !== null && assembled !== undefined) {
|
|
514
|
+
edgeValues[child.fieldName] = assembled;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Combine root item fields (minus DynamoDB metadata + context) with assembled edges
|
|
518
|
+
const rootFields = {};
|
|
519
|
+
const edgeFieldNames = new Set(rootNode.children.map((c) => c.fieldName).filter(Boolean));
|
|
520
|
+
const metaFields = new Set([
|
|
521
|
+
"__edd_e__",
|
|
522
|
+
"pk",
|
|
523
|
+
"sk",
|
|
524
|
+
"gsi1pk",
|
|
525
|
+
"gsi1sk",
|
|
526
|
+
"gsi2pk",
|
|
527
|
+
"gsi2sk",
|
|
528
|
+
"gsi3pk",
|
|
529
|
+
"gsi3sk",
|
|
530
|
+
"gsi4pk",
|
|
531
|
+
"gsi4sk",
|
|
532
|
+
"gsi5pk",
|
|
533
|
+
"gsi5sk",
|
|
534
|
+
"lsi1sk",
|
|
535
|
+
"lsi2sk",
|
|
536
|
+
]);
|
|
537
|
+
for (const [fieldKey, value] of Object.entries(rootItem)) {
|
|
538
|
+
if (metaFields.has(fieldKey))
|
|
539
|
+
continue;
|
|
540
|
+
if (edgeFieldNames.has(fieldKey))
|
|
541
|
+
continue;
|
|
542
|
+
rootFields[fieldKey] = value;
|
|
543
|
+
}
|
|
544
|
+
// Deserialize date context fields from DynamoDB storage primitives to domain values.
|
|
545
|
+
// Schema.decodeUnknownEffect expects domain types (Date, DateTime), not encoded strings.
|
|
546
|
+
for (const [field, encoding] of Object.entries(dateEncodings)) {
|
|
547
|
+
if (field in rootFields && rootFields[field] != null) {
|
|
548
|
+
rootFields[field] = deserializeDateFromDynamo(rootFields[field], encoding);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Merge root fields with assembled edge values
|
|
552
|
+
const assembled = { ...rootFields, ...edgeValues };
|
|
553
|
+
// Decode through the aggregate's Schema.Class
|
|
554
|
+
const decoded = yield* Schema.decodeUnknownEffect(schema)(assembled).pipe(Effect.mapError((cause) => new ValidationError({
|
|
555
|
+
entityType: aggregateName,
|
|
556
|
+
operation: "aggregate.assemble",
|
|
557
|
+
cause,
|
|
558
|
+
})));
|
|
559
|
+
return decoded;
|
|
560
|
+
});
|
|
561
|
+
const assembleNode = (node, allItems, key, aggregateName) => Effect.gen(function* () {
|
|
562
|
+
// Find items matching this node's entity type and discriminator
|
|
563
|
+
const matchingItems = allItems.filter((item) => {
|
|
564
|
+
if (item.__edd_e__ !== node.entityType)
|
|
565
|
+
return false;
|
|
566
|
+
// If this node has a discriminator, check that item attributes match
|
|
567
|
+
if (node.discriminator) {
|
|
568
|
+
for (const [attr, value] of Object.entries(node.discriminator)) {
|
|
569
|
+
// Skip computed discriminators (functions) — only match on static values
|
|
570
|
+
if (typeof value === "function")
|
|
571
|
+
continue;
|
|
572
|
+
if (item[attr] !== value)
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return true;
|
|
577
|
+
});
|
|
578
|
+
if (node.cardinality === "one") {
|
|
579
|
+
// One-to-one: sub-aggregate root or simple one edge
|
|
580
|
+
if (node.children.length > 0) {
|
|
581
|
+
// Sub-aggregate — assemble recursively
|
|
582
|
+
// If no matching items found, return undefined (supports optional sub-aggregates)
|
|
583
|
+
if (matchingItems.length === 0)
|
|
584
|
+
return undefined;
|
|
585
|
+
if (matchingItems.length > 1) {
|
|
586
|
+
return yield* new AggregateAssemblyError({
|
|
587
|
+
aggregate: aggregateName,
|
|
588
|
+
reason: `Multiple sub-aggregate root items for "${node.fieldName}" (entityType: ${node.entityType})`,
|
|
589
|
+
key,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
const subRootItem = matchingItems[0];
|
|
593
|
+
const edgeValues = {};
|
|
594
|
+
for (const child of node.children) {
|
|
595
|
+
// For children within a sub-aggregate, propagate the parent's discriminator
|
|
596
|
+
const childNode = node.discriminator
|
|
597
|
+
? { ...child, discriminator: { ...node.discriminator, ...child.discriminator } }
|
|
598
|
+
: child;
|
|
599
|
+
const assembled = yield* assembleNode(childNode, allItems, key, aggregateName);
|
|
600
|
+
if (child.fieldName !== null && assembled !== undefined) {
|
|
601
|
+
edgeValues[child.fieldName] = assembled;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Build sub-aggregate result: sub-root fields + assembled edges
|
|
605
|
+
const subFields = {};
|
|
606
|
+
const edgeFieldNames = new Set(node.children.map((c) => c.fieldName).filter(Boolean));
|
|
607
|
+
for (const [fieldKey, value] of Object.entries(subRootItem)) {
|
|
608
|
+
// Skip DynamoDB metadata, discriminator attributes, and edge fields
|
|
609
|
+
if (fieldKey === "__edd_e__")
|
|
610
|
+
continue;
|
|
611
|
+
if (fieldKey.startsWith("pk") || fieldKey.startsWith("sk"))
|
|
612
|
+
continue;
|
|
613
|
+
if (fieldKey.startsWith("gsi") || fieldKey.startsWith("lsi"))
|
|
614
|
+
continue;
|
|
615
|
+
if (edgeFieldNames.has(fieldKey))
|
|
616
|
+
continue;
|
|
617
|
+
// Skip discriminator attributes
|
|
618
|
+
if (node.discriminator && fieldKey in node.discriminator)
|
|
619
|
+
continue;
|
|
620
|
+
subFields[fieldKey] = value;
|
|
621
|
+
}
|
|
622
|
+
return { ...subFields, ...edgeValues };
|
|
623
|
+
}
|
|
624
|
+
// Simple one-to-one edge — if no matching items, return undefined
|
|
625
|
+
// (the Schema decode will catch truly required missing fields)
|
|
626
|
+
if (matchingItems.length === 0)
|
|
627
|
+
return undefined;
|
|
628
|
+
// Return the item (stripped of DynamoDB metadata)
|
|
629
|
+
const item = matchingItems[0];
|
|
630
|
+
return stripMetadata(item);
|
|
631
|
+
}
|
|
632
|
+
// Many edge: collect all matching items
|
|
633
|
+
const stripped = matchingItems.map(stripMetadata);
|
|
634
|
+
if (node.assemble) {
|
|
635
|
+
return node.assemble(stripped);
|
|
636
|
+
}
|
|
637
|
+
return stripped;
|
|
638
|
+
});
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// Internal: Strip DynamoDB metadata from items
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
const stripMetadata = (item) => {
|
|
643
|
+
const result = {};
|
|
644
|
+
for (const [key, value] of Object.entries(item)) {
|
|
645
|
+
if (key === "__edd_e__")
|
|
646
|
+
continue;
|
|
647
|
+
if (key === "pk" || key === "sk")
|
|
648
|
+
continue;
|
|
649
|
+
if (key.startsWith("gsi") || key.startsWith("lsi"))
|
|
650
|
+
continue;
|
|
651
|
+
result[key] = value;
|
|
652
|
+
}
|
|
653
|
+
return result;
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* Walk the input recursively following graph edges, collect ref IDs,
|
|
657
|
+
* fetch referenced entities, and replace IDs with hydrated domain data.
|
|
658
|
+
*/
|
|
659
|
+
const hydrateAggregateRefs = (input, edges, aggregateName) => Effect.gen(function* () {
|
|
660
|
+
// Collect all ref IDs from the entire input hierarchy
|
|
661
|
+
const refRequests = [];
|
|
662
|
+
collectRefIdsFromEdges(refRequests, input, edges, []);
|
|
663
|
+
if (refRequests.length === 0)
|
|
664
|
+
return input;
|
|
665
|
+
// Group by entity type for efficient fetching
|
|
666
|
+
const byEntityType = new Map();
|
|
667
|
+
for (const req of refRequests) {
|
|
668
|
+
const key = req.entity.entityType;
|
|
669
|
+
const existing = byEntityType.get(key);
|
|
670
|
+
if (existing) {
|
|
671
|
+
existing.reqs.push(req);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
byEntityType.set(key, { entity: req.entity, reqs: [req] });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// Batch-fetch all refs — one Batch.get call per entity type
|
|
678
|
+
const hydratedLookup = new Map();
|
|
679
|
+
for (const [entityType, { entity, reqs }] of byEntityType) {
|
|
680
|
+
const idField = DynamoModel.getIdentifierField(entity.model);
|
|
681
|
+
const identifierName = idField?.name ?? "id";
|
|
682
|
+
// Deduplicate IDs for this entity type
|
|
683
|
+
const uniqueIds = [...new Set(reqs.map((r) => r.id))];
|
|
684
|
+
// Build EntityGet intermediates for batch fetching
|
|
685
|
+
const getOps = uniqueIds.map((id) => entity.get({ [identifierName]: id }));
|
|
686
|
+
const results = yield* Batch.get(getOps);
|
|
687
|
+
// Map results back to lookup, failing on missing refs
|
|
688
|
+
for (let i = 0; i < uniqueIds.length; i++) {
|
|
689
|
+
const id = uniqueIds[i];
|
|
690
|
+
const result = results[i];
|
|
691
|
+
if (result == null) {
|
|
692
|
+
return yield* new RefNotFound({
|
|
693
|
+
entity: aggregateName,
|
|
694
|
+
field: reqs.find((r) => r.id === id)?.fieldName ?? "",
|
|
695
|
+
refEntity: entityType,
|
|
696
|
+
refId: id,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
hydratedLookup.set(`${entityType}:${id}`, { ...result });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
// Walk the input again and replace IDs with hydrated data
|
|
703
|
+
const result = replaceRefIds(input, refRequests, hydratedLookup);
|
|
704
|
+
renameInputFields(result, edges);
|
|
705
|
+
return result;
|
|
706
|
+
});
|
|
707
|
+
/**
|
|
708
|
+
* Rename ManyEdge inputField keys back to model field names so the domain
|
|
709
|
+
* schema can decode correctly. Walks edges recursively through sub-aggregates.
|
|
710
|
+
*/
|
|
711
|
+
const renameInputFields = (data, edges) => {
|
|
712
|
+
for (const [edgeName, edge] of Object.entries(edges)) {
|
|
713
|
+
if (!("_tag" in edge))
|
|
714
|
+
continue;
|
|
715
|
+
if (edge._tag === "ManyEdge" && edge.inputField && edge.inputField !== edgeName) {
|
|
716
|
+
if (edge.inputField in data) {
|
|
717
|
+
data[edgeName] = data[edge.inputField];
|
|
718
|
+
delete data[edge.inputField];
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
else if (edge._tag === "BoundSubAggregate") {
|
|
722
|
+
const sub = data[edgeName];
|
|
723
|
+
if (sub != null && typeof sub === "object") {
|
|
724
|
+
renameInputFields(sub, edge.aggregate.edges);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
/**
|
|
730
|
+
* Recursively walk input following graph edges to collect ref ID references.
|
|
731
|
+
* Edge-driven: entity lookup comes from the edge directly, not from a refs bag.
|
|
732
|
+
*/
|
|
733
|
+
const collectRefIdsFromEdges = (result, input, edges, path) => {
|
|
734
|
+
for (const [edgeName, edge] of Object.entries(edges)) {
|
|
735
|
+
if (!("_tag" in edge))
|
|
736
|
+
continue;
|
|
737
|
+
if (edge._tag === "RefEdge") {
|
|
738
|
+
// RefEdge → ${field}Id in input
|
|
739
|
+
const idFieldName = `${edgeName}Id`;
|
|
740
|
+
const idValue = input[idFieldName];
|
|
741
|
+
if (typeof idValue === "string") {
|
|
742
|
+
result.push({
|
|
743
|
+
entity: edge.entity,
|
|
744
|
+
id: idValue,
|
|
745
|
+
path: [...path],
|
|
746
|
+
fieldName: edgeName,
|
|
747
|
+
idFieldName,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
else if (edge._tag === "OneEdge" && edge.entity) {
|
|
752
|
+
// OneEdge with entity → ${field}Id in input
|
|
753
|
+
const idFieldName = `${edgeName}Id`;
|
|
754
|
+
const idValue = input[idFieldName];
|
|
755
|
+
if (typeof idValue === "string") {
|
|
756
|
+
result.push({
|
|
757
|
+
entity: edge.entity,
|
|
758
|
+
id: idValue,
|
|
759
|
+
path: [...path],
|
|
760
|
+
fieldName: edgeName,
|
|
761
|
+
idFieldName,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
else if (edge._tag === "BoundSubAggregate") {
|
|
766
|
+
const bound = edge;
|
|
767
|
+
const subInput = input[edgeName];
|
|
768
|
+
if (subInput != null && typeof subInput === "object") {
|
|
769
|
+
collectRefIdsFromEdges(result, subInput, bound.aggregate.edges, [
|
|
770
|
+
...path,
|
|
771
|
+
edgeName,
|
|
772
|
+
]);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
else if (edge._tag === "ManyEdge" && edge.entity) {
|
|
776
|
+
// ManyEdge with entity → walk array elements for ref IDs
|
|
777
|
+
const inputKey = edge.inputField ?? edgeName;
|
|
778
|
+
const arr = input[inputKey];
|
|
779
|
+
if (Array.isArray(arr)) {
|
|
780
|
+
const entityFieldName = deriveEntityFieldName(edge.entity);
|
|
781
|
+
for (let i = 0; i < arr.length; i++) {
|
|
782
|
+
const elem = arr[i];
|
|
783
|
+
if (typeof elem === "string") {
|
|
784
|
+
// Element IS the ref ID (e.g., Array<string> for Array<Umpire>)
|
|
785
|
+
result.push({
|
|
786
|
+
entity: edge.entity,
|
|
787
|
+
id: elem,
|
|
788
|
+
path: [...path, inputKey, String(i)],
|
|
789
|
+
fieldName: edgeName,
|
|
790
|
+
idFieldName: String(i),
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
else if (elem != null && typeof elem === "object") {
|
|
794
|
+
// Element wraps entity + attributes — find the entityFieldName + "Id"
|
|
795
|
+
const elemObj = elem;
|
|
796
|
+
const idFieldName = `${entityFieldName}Id`;
|
|
797
|
+
const idValue = elemObj[idFieldName];
|
|
798
|
+
if (typeof idValue === "string") {
|
|
799
|
+
result.push({
|
|
800
|
+
entity: edge.entity,
|
|
801
|
+
id: idValue,
|
|
802
|
+
path: [...path, inputKey, String(i)],
|
|
803
|
+
fieldName: entityFieldName,
|
|
804
|
+
idFieldName,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
/**
|
|
814
|
+
* Replace ref ID fields in the input with hydrated domain data.
|
|
815
|
+
*/
|
|
816
|
+
const replaceRefIds = (input, refRequests, lookup) => {
|
|
817
|
+
// Deep clone the input for mutation
|
|
818
|
+
const result = structuredClone(input);
|
|
819
|
+
for (const req of refRequests) {
|
|
820
|
+
const lookupKey = `${req.entity.entityType}:${req.id}`;
|
|
821
|
+
const data = lookup.get(lookupKey);
|
|
822
|
+
if (!data)
|
|
823
|
+
continue;
|
|
824
|
+
// Navigate to the target location in the result
|
|
825
|
+
let target = result;
|
|
826
|
+
for (const segment of req.path) {
|
|
827
|
+
const next = target[segment];
|
|
828
|
+
if (next == null || typeof next !== "object")
|
|
829
|
+
break;
|
|
830
|
+
target = next;
|
|
831
|
+
}
|
|
832
|
+
// For ManyEdge elements that are direct IDs (element IS the ref),
|
|
833
|
+
// the target is the array itself — replace the element at the index
|
|
834
|
+
if (Array.isArray(target) && /^\d+$/.test(req.idFieldName)) {
|
|
835
|
+
target[Number(req.idFieldName)] = data;
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
// Replace: remove ${field}Id, add ${field}: data
|
|
839
|
+
delete target[req.idFieldName];
|
|
840
|
+
target[req.fieldName] = data;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return result;
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* Decompose an assembled domain object into DynamoDB items grouped by
|
|
847
|
+
* sub-aggregate transaction boundaries.
|
|
848
|
+
*/
|
|
849
|
+
const decomposeAggregate = (assembled, rootNode, contextFields, dateEncodings, aggregateName) => Effect.gen(function* () {
|
|
850
|
+
const items = [];
|
|
851
|
+
// Extract context values from the root, serializing date fields for DynamoDB storage.
|
|
852
|
+
// Domain Date/DateTime objects must be encoded before toAttributeMap() marshalling,
|
|
853
|
+
// otherwise Date objects become { M: {} } (no enumerable properties).
|
|
854
|
+
const contextValues = {};
|
|
855
|
+
for (const field of contextFields) {
|
|
856
|
+
const value = assembled[field];
|
|
857
|
+
const encoding = dateEncodings[field];
|
|
858
|
+
contextValues[field] =
|
|
859
|
+
encoding && value != null ? serializeDateForDynamo(value, encoding) : value;
|
|
860
|
+
}
|
|
861
|
+
// Root item: fields not claimed by edges
|
|
862
|
+
const edgeFieldNames = new Set(rootNode.children.map((c) => c.fieldName).filter(Boolean));
|
|
863
|
+
const rootAttrs = {};
|
|
864
|
+
for (const [key, value] of Object.entries(assembled)) {
|
|
865
|
+
if (edgeFieldNames.has(key))
|
|
866
|
+
continue;
|
|
867
|
+
// Serialize date fields in root attributes (same as context values)
|
|
868
|
+
const encoding = dateEncodings[key];
|
|
869
|
+
rootAttrs[key] = encoding && value != null ? serializeDateForDynamo(value, encoding) : value;
|
|
870
|
+
}
|
|
871
|
+
items.push({
|
|
872
|
+
entityType: rootNode.entityType,
|
|
873
|
+
attributes: rootAttrs,
|
|
874
|
+
transactionGroup: "root",
|
|
875
|
+
skComposites: [],
|
|
876
|
+
});
|
|
877
|
+
// Decompose each edge
|
|
878
|
+
for (const child of rootNode.children) {
|
|
879
|
+
const fieldValue = assembled[child.fieldName];
|
|
880
|
+
yield* decomposeNode(items, child, fieldValue, contextValues, "root", [], aggregateName);
|
|
881
|
+
}
|
|
882
|
+
// Group by transaction
|
|
883
|
+
const groups = new Map();
|
|
884
|
+
for (const item of items) {
|
|
885
|
+
const list = groups.get(item.transactionGroup) ?? [];
|
|
886
|
+
list.push(item);
|
|
887
|
+
groups.set(item.transactionGroup, list);
|
|
888
|
+
}
|
|
889
|
+
return [...groups.entries()].map(([name, groupItems]) => ({ name, items: groupItems }));
|
|
890
|
+
});
|
|
891
|
+
/**
|
|
892
|
+
* Merge context values into decomposed item attributes.
|
|
893
|
+
* Context values always override entity-specific fields — the aggregate root
|
|
894
|
+
* defines the authoritative values for context fields like "name" and "gender".
|
|
895
|
+
*/
|
|
896
|
+
const mergeContextValues = (attrs, contextValues) => {
|
|
897
|
+
for (const [key, value] of Object.entries(contextValues)) {
|
|
898
|
+
attrs[key] = value;
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
/**
|
|
902
|
+
* Recursively decompose a node's value into DynamoDB items.
|
|
903
|
+
*/
|
|
904
|
+
const decomposeNode = (items, node, value, contextValues, parentGroup, parentDiscriminatorValues, aggregateName) => Effect.gen(function* () {
|
|
905
|
+
// Optional edges may have null/undefined values — skip decomposition
|
|
906
|
+
if (value == null)
|
|
907
|
+
return;
|
|
908
|
+
if (node.cardinality === "one" && node.children.length > 0) {
|
|
909
|
+
// Sub-aggregate — becomes its own transaction group
|
|
910
|
+
const subValue = value;
|
|
911
|
+
const txGroup = node.fieldName ?? node.entityType;
|
|
912
|
+
// Discriminator values for SK composition (name#value pairs from own discriminator)
|
|
913
|
+
const discValues = node.ownDiscriminator
|
|
914
|
+
? Object.entries(node.ownDiscriminator)
|
|
915
|
+
.filter(([, v]) => typeof v !== "function")
|
|
916
|
+
.flatMap(([k, v]) => [k, KeyComposer.serializeValue(v)])
|
|
917
|
+
: [];
|
|
918
|
+
// Sub-aggregate root item: fields not claimed by child edges
|
|
919
|
+
const childEdgeNames = new Set(node.children.map((c) => c.fieldName).filter(Boolean));
|
|
920
|
+
const subRootAttrs = {};
|
|
921
|
+
for (const [key, val] of Object.entries(subValue)) {
|
|
922
|
+
if (childEdgeNames.has(key))
|
|
923
|
+
continue;
|
|
924
|
+
subRootAttrs[key] = val;
|
|
925
|
+
}
|
|
926
|
+
// Inject context and discriminator
|
|
927
|
+
mergeContextValues(subRootAttrs, contextValues);
|
|
928
|
+
if (node.discriminator) {
|
|
929
|
+
for (const [k, v] of Object.entries(node.discriminator)) {
|
|
930
|
+
if (typeof v !== "function")
|
|
931
|
+
subRootAttrs[k] = v;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
items.push({
|
|
935
|
+
entityType: node.entityType,
|
|
936
|
+
attributes: subRootAttrs,
|
|
937
|
+
transactionGroup: txGroup,
|
|
938
|
+
skComposites: discValues,
|
|
939
|
+
});
|
|
940
|
+
// Decompose children within this sub-aggregate
|
|
941
|
+
for (const child of node.children) {
|
|
942
|
+
const childValue = subValue[child.fieldName];
|
|
943
|
+
yield* decomposeNode(items, child, childValue, contextValues, txGroup, discValues, aggregateName);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
else if (node.cardinality === "one") {
|
|
947
|
+
// Simple one-to-one edge (no children)
|
|
948
|
+
const attrs = { ...value };
|
|
949
|
+
mergeContextValues(attrs, contextValues);
|
|
950
|
+
if (node.discriminator) {
|
|
951
|
+
for (const [k, v] of Object.entries(node.discriminator)) {
|
|
952
|
+
if (typeof v !== "function")
|
|
953
|
+
attrs[k] = v;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// Own discriminator values as name#value pairs for SK composition
|
|
957
|
+
const ownDiscValues = node.ownDiscriminator
|
|
958
|
+
? Object.entries(node.ownDiscriminator)
|
|
959
|
+
.filter(([, v]) => typeof v !== "function")
|
|
960
|
+
.flatMap(([k, v]) => [k, KeyComposer.serializeValue(v)])
|
|
961
|
+
: [];
|
|
962
|
+
items.push({
|
|
963
|
+
entityType: node.entityType,
|
|
964
|
+
attributes: attrs,
|
|
965
|
+
transactionGroup: parentGroup,
|
|
966
|
+
skComposites: [...parentDiscriminatorValues, ...ownDiscValues],
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
// Many edge
|
|
971
|
+
const arrayItems = node.decompose
|
|
972
|
+
? node.decompose(value)
|
|
973
|
+
: Array.isArray(value)
|
|
974
|
+
? value
|
|
975
|
+
: [];
|
|
976
|
+
for (const elem of arrayItems) {
|
|
977
|
+
const attrs = { ...elem };
|
|
978
|
+
mergeContextValues(attrs, contextValues);
|
|
979
|
+
if (node.discriminator) {
|
|
980
|
+
for (const [k, v] of Object.entries(node.discriminator)) {
|
|
981
|
+
if (typeof v !== "function")
|
|
982
|
+
attrs[k] = v;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// Extract ref identifiers for SK composites
|
|
986
|
+
const itemComposites = extractRefIdentifiers(attrs);
|
|
987
|
+
items.push({
|
|
988
|
+
entityType: node.entityType,
|
|
989
|
+
attributes: attrs,
|
|
990
|
+
transactionGroup: parentGroup,
|
|
991
|
+
skComposites: [...parentDiscriminatorValues, ...itemComposites],
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
/**
|
|
997
|
+
* Extract ref identifier values from an item's attributes for use as SK composites.
|
|
998
|
+
* Walks fields looking for embedded objects that have an identifier-annotated field.
|
|
999
|
+
*/
|
|
1000
|
+
const extractRefIdentifiers = (attrs) => {
|
|
1001
|
+
const ids = [];
|
|
1002
|
+
// Case 1: Element wraps entity — look for nested objects with id-like fields
|
|
1003
|
+
for (const [, value] of Object.entries(attrs)) {
|
|
1004
|
+
if (value != null && typeof value === "object" && !Array.isArray(value)) {
|
|
1005
|
+
const obj = value;
|
|
1006
|
+
if (typeof obj.id === "string") {
|
|
1007
|
+
ids.push(obj.id);
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1011
|
+
if (k.endsWith("Id") && typeof v === "string") {
|
|
1012
|
+
ids.push(v);
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// Case 2: Element IS entity — top-level id field (e.g., Umpire with { id: "ump-1", ... })
|
|
1020
|
+
if (ids.length === 0 && typeof attrs.id === "string") {
|
|
1021
|
+
ids.push(attrs.id);
|
|
1022
|
+
}
|
|
1023
|
+
return ids;
|
|
1024
|
+
};
|
|
1025
|
+
// ---------------------------------------------------------------------------
|
|
1026
|
+
// Internal: Hash-based shard selection for list collection cardinality
|
|
1027
|
+
// ---------------------------------------------------------------------------
|
|
1028
|
+
const hashToShard = (value, cardinality) => {
|
|
1029
|
+
let hash = 5381;
|
|
1030
|
+
for (let i = 0; i < value.length; i++) {
|
|
1031
|
+
hash = ((hash << 5) + hash + value.charCodeAt(i)) | 0;
|
|
1032
|
+
}
|
|
1033
|
+
return Math.abs(hash) % cardinality;
|
|
1034
|
+
};
|
|
1035
|
+
// ---------------------------------------------------------------------------
|
|
1036
|
+
// Internal: Compose DynamoDB items from decomposed data
|
|
1037
|
+
// ---------------------------------------------------------------------------
|
|
1038
|
+
/**
|
|
1039
|
+
* Convert decomposed items into DynamoDB attribute maps with composed keys.
|
|
1040
|
+
*/
|
|
1041
|
+
const buildDynamoItems = (groups, config, pkValue, rootCollectionSkComposites) => groups.map((group) => ({
|
|
1042
|
+
group: group.name,
|
|
1043
|
+
items: group.items.map((item) => {
|
|
1044
|
+
const attrs = { ...item.attributes };
|
|
1045
|
+
// Add entity type discriminator
|
|
1046
|
+
attrs.__edd_e__ = item.entityType;
|
|
1047
|
+
// PK: shared collection key
|
|
1048
|
+
attrs[config.pk.field] = pkValue;
|
|
1049
|
+
// SK: entity-type specific
|
|
1050
|
+
attrs.sk = composeKey(config.schema, item.entityType, [...item.skComposites]);
|
|
1051
|
+
// Collection SK (LSI): root uses collection composites, others mirror their SK
|
|
1052
|
+
const isRootItem = item.entityType === config.root.entityType && item.skComposites.length === 0;
|
|
1053
|
+
attrs[config.collection.sk.field] = isRootItem
|
|
1054
|
+
? composeCollectionKey(config.schema, config.collection.name, rootCollectionSkComposites)
|
|
1055
|
+
: composeKey(config.schema, item.entityType, [...item.skComposites]);
|
|
1056
|
+
// List collection GSI keys (root items only)
|
|
1057
|
+
if (isRootItem && config.list) {
|
|
1058
|
+
const listPkComposites = KeyComposer.extractComposites(config.list.pk.composite, item.attributes);
|
|
1059
|
+
if (config.list.cardinality) {
|
|
1060
|
+
const shard = hashToShard(pkValue, config.list.cardinality);
|
|
1061
|
+
attrs[config.list.pk.field] = composeCollectionKey(config.schema, config.list.name, [
|
|
1062
|
+
...listPkComposites,
|
|
1063
|
+
String(shard),
|
|
1064
|
+
]);
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
attrs[config.list.pk.field] = composeCollectionKey(config.schema, config.list.name, listPkComposites);
|
|
1068
|
+
}
|
|
1069
|
+
const listSkComposites = KeyComposer.extractComposites(config.list.sk.composite, item.attributes);
|
|
1070
|
+
attrs[config.list.sk.field] = composeCollectionKey(config.schema, config.list.name, listSkComposites);
|
|
1071
|
+
}
|
|
1072
|
+
return toAttributeMap(attrs);
|
|
1073
|
+
}),
|
|
1074
|
+
}));
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1076
|
+
// Internal: Write transaction groups to DynamoDB
|
|
1077
|
+
// ---------------------------------------------------------------------------
|
|
1078
|
+
const writeTransactionGroups = (client, tableName, groups, aggregateName) => Effect.gen(function* () {
|
|
1079
|
+
for (const group of groups) {
|
|
1080
|
+
// Check transaction size limit
|
|
1081
|
+
if (group.items.length > 100) {
|
|
1082
|
+
return yield* new AggregateTransactionOverflow({
|
|
1083
|
+
aggregate: aggregateName,
|
|
1084
|
+
subgraph: group.group,
|
|
1085
|
+
itemCount: group.items.length,
|
|
1086
|
+
limit: 100,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
if (group.items.length === 0)
|
|
1090
|
+
continue;
|
|
1091
|
+
// Build TransactWriteItems request
|
|
1092
|
+
const transactItems = group.items.map((item) => ({
|
|
1093
|
+
Put: { TableName: tableName, Item: item },
|
|
1094
|
+
}));
|
|
1095
|
+
yield* client.transactWriteItems({ TransactItems: transactItems }).pipe(Effect.mapError((error) => {
|
|
1096
|
+
if (error.cause != null &&
|
|
1097
|
+
typeof error.cause === "object" &&
|
|
1098
|
+
"name" in error.cause &&
|
|
1099
|
+
error.cause.name === "TransactionCanceledException") {
|
|
1100
|
+
const cancelled = error.cause;
|
|
1101
|
+
return new TransactionCancelled({
|
|
1102
|
+
operation: "TransactWriteItems",
|
|
1103
|
+
reasons: (cancelled.CancellationReasons ?? []).map((r) => ({
|
|
1104
|
+
code: r?.Code,
|
|
1105
|
+
message: r?.Message,
|
|
1106
|
+
})),
|
|
1107
|
+
cause: error.cause,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
return error;
|
|
1111
|
+
}));
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
// ---------------------------------------------------------------------------
|
|
1115
|
+
// Internal: Delete all items in an aggregate partition
|
|
1116
|
+
// ---------------------------------------------------------------------------
|
|
1117
|
+
const deleteAllItems = (client, tableName, allItems, pkField) => Effect.gen(function* () {
|
|
1118
|
+
// Extract primary keys from items
|
|
1119
|
+
const deleteRequests = allItems.map((item) => ({
|
|
1120
|
+
DeleteRequest: {
|
|
1121
|
+
Key: toAttributeMap({
|
|
1122
|
+
[pkField]: item[pkField],
|
|
1123
|
+
sk: item.sk,
|
|
1124
|
+
}),
|
|
1125
|
+
},
|
|
1126
|
+
}));
|
|
1127
|
+
// Chunk into batches of 25 (DynamoDB batchWriteItem limit)
|
|
1128
|
+
for (let i = 0; i < deleteRequests.length; i += 25) {
|
|
1129
|
+
const chunk = deleteRequests.slice(i, i + 25);
|
|
1130
|
+
yield* client.batchWriteItem({
|
|
1131
|
+
RequestItems: { [tableName]: chunk },
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
const queryListPartition = (client, tableConfig, listConfig, schema, pkValue, skValues, limit, startKey) => Effect.gen(function* () {
|
|
1136
|
+
const exprNames = { "#pk": listConfig.pk.field };
|
|
1137
|
+
const exprValues = { ":pk": toAttributeValue(pkValue) };
|
|
1138
|
+
let keyCondition = "#pk = :pk";
|
|
1139
|
+
if (skValues.length > 0) {
|
|
1140
|
+
const skPrefix = composeCollectionKey(schema, listConfig.name, skValues);
|
|
1141
|
+
exprNames["#sk"] = listConfig.sk.field;
|
|
1142
|
+
exprValues[":skPrefix"] = toAttributeValue(skPrefix);
|
|
1143
|
+
keyCondition += " AND begins_with(#sk, :skPrefix)";
|
|
1144
|
+
}
|
|
1145
|
+
const items = [];
|
|
1146
|
+
let lastKey = startKey;
|
|
1147
|
+
do {
|
|
1148
|
+
const result = yield* client.query({
|
|
1149
|
+
TableName: tableConfig.name,
|
|
1150
|
+
IndexName: listConfig.index,
|
|
1151
|
+
KeyConditionExpression: keyCondition,
|
|
1152
|
+
ExpressionAttributeNames: exprNames,
|
|
1153
|
+
ExpressionAttributeValues: exprValues,
|
|
1154
|
+
ExclusiveStartKey: lastKey,
|
|
1155
|
+
...(limit !== undefined ? { Limit: limit - items.length } : {}),
|
|
1156
|
+
});
|
|
1157
|
+
if (result.Items) {
|
|
1158
|
+
for (const item of result.Items) {
|
|
1159
|
+
items.push(fromAttributeMap(item));
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
lastKey = result.LastEvaluatedKey;
|
|
1163
|
+
// Stop when we've collected enough items
|
|
1164
|
+
if (limit !== undefined && items.length >= limit)
|
|
1165
|
+
break;
|
|
1166
|
+
} while (lastKey !== undefined);
|
|
1167
|
+
return { items, lastKey };
|
|
1168
|
+
});
|
|
1169
|
+
//# sourceMappingURL=Aggregate.js.map
|