effect-dynamodb 1.9.3 → 1.9.5
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/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +210 -82
- package/dist/Aggregate.js.map +1 -1
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +18 -4
- package/dist/Entity.js.map +1 -1
- package/package.json +2 -2
package/dist/Aggregate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Aggregate.d.ts","sourceRoot":"","sources":["../src/Aggregate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,MAAM,IAAI,qBAAqB,EAAE,MAAM,sCAAsC,CAAA;AAGtF,OAAO,KAAK,KAAK,kBAAkB,MAAM,yCAAyC,CAAA;
|
|
1
|
+
{"version":3,"file":"Aggregate.d.ts","sourceRoot":"","sources":["../src/Aggregate.ts"],"names":[],"mappings":"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"}
|
package/dist/Aggregate.js
CHANGED
|
@@ -12,11 +12,12 @@ import { TypeId as SchemaAggregateTypeId } from "@effect-dynamodb/schema/Aggrega
|
|
|
12
12
|
import * as DynamoModel from "@effect-dynamodb/schema/DynamoModel.js";
|
|
13
13
|
import { composeCollectionKey, composeKey } from "@effect-dynamodb/schema/DynamoSchema.js";
|
|
14
14
|
import { AggregateAssemblyError, AggregateTransactionOverflow, RefNotFound, TransactionCancelled, ValidationError, } from "@effect-dynamodb/schema/Errors.js";
|
|
15
|
-
import { buildDateTransform, validateNoTransformOverride, } from "@effect-dynamodb/schema/internal/EntitySchemas.js";
|
|
15
|
+
import { buildDateTransform, substituteSchemaDeep, validateNoTransformOverride, } from "@effect-dynamodb/schema/internal/EntitySchemas.js";
|
|
16
16
|
import * as KeyComposer from "@effect-dynamodb/schema/KeyComposer.js";
|
|
17
17
|
import { Effect, Schema, SchemaAST } from "effect";
|
|
18
18
|
import * as Batch from "./Batch.js";
|
|
19
19
|
import { DynamoClient } from "./DynamoClient.js";
|
|
20
|
+
import { fromDefinition as entityFromDefinition } from "./Entity.js";
|
|
20
21
|
import { fromAttributeMap, toAttributeMap, toAttributeValue } from "./Marshaller.js";
|
|
21
22
|
// Internal modules (decomposed from Aggregate.ts)
|
|
22
23
|
export { isManyEdge, isOneEdge, isRefEdge, many, one, ref, } from "@effect-dynamodb/schema/internal/AggregateEdges.js";
|
|
@@ -129,6 +130,46 @@ const inferDateEncoding = (ast) => {
|
|
|
129
130
|
}
|
|
130
131
|
return undefined;
|
|
131
132
|
};
|
|
133
|
+
/**
|
|
134
|
+
* Build per-field date ENCODERS for a schema's own date fields. Encoders convert
|
|
135
|
+
* a domain date value to its wire primitive on the WRITE path — decompose works
|
|
136
|
+
* from the schema-decoded domain object, so EVERY date field (Pattern A self-date
|
|
137
|
+
* AND Pattern B transform) must serialize for storage. The READ path is handled
|
|
138
|
+
* entirely by the aggregate's tolerant `decodeSchema` (see `makeAggregate`), so
|
|
139
|
+
* no separate decoders are needed.
|
|
140
|
+
*/
|
|
141
|
+
const buildDateEncoders = (fields) => {
|
|
142
|
+
const encoders = {};
|
|
143
|
+
if (fields) {
|
|
144
|
+
for (const field of Object.keys(fields)) {
|
|
145
|
+
const fieldSchema = fields[field];
|
|
146
|
+
// Precedence: explicit `DynamoModel.storedAs` annotation > inferred default
|
|
147
|
+
// for standard Effect date schemas (handles `Schema.optional()` wrappers).
|
|
148
|
+
const encoding = DynamoModel.getEncoding(fieldSchema) ?? inferDateEncoding(fieldSchema.ast);
|
|
149
|
+
if (!encoding)
|
|
150
|
+
continue;
|
|
151
|
+
const encode = Schema.encodeUnknownSync(buildDateTransform(encoding));
|
|
152
|
+
encoders[field] = (value) => encode(value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return encoders;
|
|
156
|
+
};
|
|
157
|
+
/** A schema's declared fields, if it exposes them (Schema.Struct / Schema.Class). */
|
|
158
|
+
const fieldsOf = (schema) => schema?.fields;
|
|
159
|
+
/**
|
|
160
|
+
* Apply a node's date encoders in place — converts the node entity's own domain
|
|
161
|
+
* date fields to their wire primitives before the decomposed item is marshalled
|
|
162
|
+
* (issue #72: without this, an edge's `DateTime` field marshals to `{M:{}}` and
|
|
163
|
+
* the subsequent `get`/assemble fails decoding it as a string).
|
|
164
|
+
*/
|
|
165
|
+
const applyNodeDateEncoders = (attrs, encoders) => {
|
|
166
|
+
if (!encoders)
|
|
167
|
+
return;
|
|
168
|
+
for (const [field, encode] of Object.entries(encoders)) {
|
|
169
|
+
if (field in attrs && attrs[field] != null)
|
|
170
|
+
attrs[field] = encode(attrs[field]);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
132
173
|
// ---------------------------------------------------------------------------
|
|
133
174
|
// Top-level aggregate construction
|
|
134
175
|
// ---------------------------------------------------------------------------
|
|
@@ -152,8 +193,6 @@ const makeAggregate = (schema, config) => {
|
|
|
152
193
|
// (e.g. `Schema.DateTimeUtcFromString`) cannot be combined with a
|
|
153
194
|
// `DynamoModel.storedAs(...)` annotation that conflicts with the
|
|
154
195
|
// transform's wire kind — same rule as Entity.
|
|
155
|
-
const dateEncoders = {};
|
|
156
|
-
const dateDecoders = {};
|
|
157
196
|
const schemaFields = schema.fields;
|
|
158
197
|
if (schemaFields) {
|
|
159
198
|
// Reject transform + storedAs annotation conflicts (Option (b) policy).
|
|
@@ -161,29 +200,46 @@ const makeAggregate = (schema, config) => {
|
|
|
161
200
|
// so we pass an empty `configuredAttributes` map — only the schema-level
|
|
162
201
|
// annotation conflict is checked.
|
|
163
202
|
validateNoTransformOverride(schemaFields, {});
|
|
164
|
-
for (const field of Object.keys(schemaFields)) {
|
|
165
|
-
const fieldSchema = schemaFields[field];
|
|
166
|
-
const explicit = DynamoModel.getEncoding(fieldSchema);
|
|
167
|
-
let encoding;
|
|
168
|
-
if (explicit) {
|
|
169
|
-
encoding = explicit;
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
// Infer for standard Effect date schemas (Schema.Date, DateTime, etc.)
|
|
173
|
-
// For optional fields (Union with Undefined), unwrap to find the inner type.
|
|
174
|
-
const inferred = inferDateEncoding(fieldSchema.ast);
|
|
175
|
-
if (inferred)
|
|
176
|
-
encoding = inferred;
|
|
177
|
-
}
|
|
178
|
-
if (encoding) {
|
|
179
|
-
const transform = buildDateTransform(encoding);
|
|
180
|
-
const encode = Schema.encodeUnknownSync(transform);
|
|
181
|
-
const decode = Schema.decodeUnknownSync(transform);
|
|
182
|
-
dateEncoders[field] = (value) => encode(value);
|
|
183
|
-
dateDecoders[field] = (stored) => decode(stored);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
203
|
}
|
|
204
|
+
// Date ENCODERS for the write path: decompose works from the schema-decoded
|
|
205
|
+
// domain object, so every root date field (Pattern A self-date AND Pattern B
|
|
206
|
+
// transform) is serialized to its wire primitive for storage.
|
|
207
|
+
const dateEncoders = buildDateEncoders(schemaFields);
|
|
208
|
+
// Decode schema for read/assemble + input validation. Mirrors the raw `schema`
|
|
209
|
+
// but recursively substitutes EVERY date / Redacted leaf — root and nested,
|
|
210
|
+
// Pattern A and Pattern B — with a TOLERANT transform. This is the single
|
|
211
|
+
// decode path for get / create / update:
|
|
212
|
+
// - reads supply the stored wire form (string/number) → lifted to domain;
|
|
213
|
+
// - update re-decodes the mutated state, which carries domain `DateTime`
|
|
214
|
+
// values → accepted as-is (a strict `*FromString` decoder would reject
|
|
215
|
+
// them). It is decode-only, so making transforms tolerant is safe — the
|
|
216
|
+
// stored wire format is still produced by `dateEncoders` / node encoders.
|
|
217
|
+
// Nested edge / ref classes round-trip with their class instance identity
|
|
218
|
+
// preserved (Option A). Returns the raw `schema` unchanged when it carries no
|
|
219
|
+
// date / Redacted leaf at all (zero overhead).
|
|
220
|
+
//
|
|
221
|
+
// SINGLE ref/one edge fields carry the opaque `DynamoModel.ref` annotation, so
|
|
222
|
+
// the substitution can't introspect them — `resolveRef` re-points each to its
|
|
223
|
+
// edge target model (unwrapping `DynamoModel.configure`). MANY edges are
|
|
224
|
+
// excluded: their model field is a `Schema.Array(...)` (or a wrapper class) that
|
|
225
|
+
// `substituteSchemaDeep` introspects directly — re-pointing it at the element
|
|
226
|
+
// model would drop the `Array` and yield "Expected object, got []" on assemble.
|
|
227
|
+
const edgeRefModels = new Map();
|
|
228
|
+
for (const [edgeName, edge] of Object.entries(config.edges)) {
|
|
229
|
+
if (!("_tag" in edge))
|
|
230
|
+
continue;
|
|
231
|
+
if (edge._tag !== "RefEdge" && edge._tag !== "OneEdge")
|
|
232
|
+
continue;
|
|
233
|
+
const entity = edge.entity;
|
|
234
|
+
const model = entity?.model;
|
|
235
|
+
if (!model)
|
|
236
|
+
continue;
|
|
237
|
+
edgeRefModels.set(edgeName, DynamoModel.isConfiguredModel(model) ? model.model : model);
|
|
238
|
+
}
|
|
239
|
+
const decodeSchema = substituteSchemaDeep(schema, {
|
|
240
|
+
tolerantTransforms: true,
|
|
241
|
+
resolveRef: (name) => edgeRefModels.get(name),
|
|
242
|
+
});
|
|
187
243
|
// Build optics at construction time (once per aggregate definition)
|
|
188
244
|
const classToPlain = Schema.toIso(schema);
|
|
189
245
|
const opticRoot = Schema.toIsoFocus(schema);
|
|
@@ -226,7 +282,7 @@ const makeAggregate = (schema, config) => {
|
|
|
226
282
|
key,
|
|
227
283
|
});
|
|
228
284
|
}
|
|
229
|
-
const result = yield* assembleAggregate(
|
|
285
|
+
const result = yield* assembleAggregate(decodeSchema, rootNode, allItems, contextFields, key, aggregateName);
|
|
230
286
|
return result;
|
|
231
287
|
}),
|
|
232
288
|
create: (input) => Effect.gen(function* () {
|
|
@@ -234,8 +290,10 @@ const makeAggregate = (schema, config) => {
|
|
|
234
290
|
const tableConfig = yield* config.table.Tag;
|
|
235
291
|
// 1. Hydrate refs from edges
|
|
236
292
|
const hydrated = yield* hydrateAggregateRefs(input, config.edges, aggregateName);
|
|
237
|
-
// 2. Validate via schema decode
|
|
238
|
-
|
|
293
|
+
// 2. Validate via schema decode (decodeSchema substitutes nested edge
|
|
294
|
+
// self-date/Redacted leaves — Option A — and is identical to `schema`
|
|
295
|
+
// when no edge needs it).
|
|
296
|
+
const decoded = yield* Schema.decodeUnknownEffect(decodeSchema)(hydrated).pipe(Effect.mapError((cause) => new ValidationError({
|
|
239
297
|
entityType: aggregateName,
|
|
240
298
|
operation: "aggregate.create",
|
|
241
299
|
cause,
|
|
@@ -248,10 +306,10 @@ const makeAggregate = (schema, config) => {
|
|
|
248
306
|
const groups = yield* decomposeAggregate(assembled, rootNode, contextFields, dateEncoders, aggregateName);
|
|
249
307
|
// 5. Compose collection SK composites for root item
|
|
250
308
|
const collectionSkComposites = KeyComposer.extractComposites(config.collection.sk.composite, assembled);
|
|
251
|
-
// 6. Build DynamoDB items with composed keys
|
|
309
|
+
// 6. Build DynamoDB items with composed keys (create only ever PUTs)
|
|
252
310
|
const dynamoGroups = buildDynamoItems(groups, config, pkValue, collectionSkComposites);
|
|
253
311
|
// 7. Write via sub-aggregate transactions
|
|
254
|
-
yield* writeTransactionGroups(client, tableConfig.name, dynamoGroups, aggregateName);
|
|
312
|
+
yield* writeTransactionGroups(client, tableConfig.name, dynamoGroups.map((g) => ({ group: g.group, puts: g.items, deletes: [] })), aggregateName);
|
|
255
313
|
return decoded;
|
|
256
314
|
}),
|
|
257
315
|
update: (key, mutationFn) => Effect.gen(function* () {
|
|
@@ -264,7 +322,7 @@ const makeAggregate = (schema, config) => {
|
|
|
264
322
|
});
|
|
265
323
|
}
|
|
266
324
|
// 1. Assemble current state
|
|
267
|
-
const current = yield* assembleAggregate(
|
|
325
|
+
const current = yield* assembleAggregate(decodeSchema, rootNode, allItems, contextFields, key, aggregateName);
|
|
268
326
|
// 2. Apply mutation — provide optics context for composable updates
|
|
269
327
|
const state = classToPlain.get(current);
|
|
270
328
|
const updated = mutationFn({
|
|
@@ -273,8 +331,12 @@ const makeAggregate = (schema, config) => {
|
|
|
273
331
|
optic: opticRoot,
|
|
274
332
|
current: current,
|
|
275
333
|
});
|
|
276
|
-
// 3. Validate
|
|
277
|
-
|
|
334
|
+
// 3. Validate the mutated state via `decodeSchema` — its tolerant date
|
|
335
|
+
// transforms accept the domain `DateTime` values the mutation yields
|
|
336
|
+
// for every date field (root + nested edges, Pattern A and Pattern B),
|
|
337
|
+
// so update no longer trips the "Expected string, got DateTime" decode
|
|
338
|
+
// that a strict `*FromString` schema would raise (#72 update path).
|
|
339
|
+
const decoded = yield* Schema.decodeUnknownEffect(decodeSchema)(updated).pipe(Effect.mapError((cause) => new ValidationError({
|
|
278
340
|
entityType: aggregateName,
|
|
279
341
|
operation: "aggregate.update",
|
|
280
342
|
cause,
|
|
@@ -284,7 +346,14 @@ const makeAggregate = (schema, config) => {
|
|
|
284
346
|
// 4. Decompose both old and new
|
|
285
347
|
const oldGroups = yield* decomposeAggregate(assembledOld, rootNode, contextFields, dateEncoders, aggregateName);
|
|
286
348
|
const newGroups = yield* decomposeAggregate(assembledNew, rootNode, contextFields, dateEncoders, aggregateName);
|
|
287
|
-
// 5. Diff
|
|
349
|
+
// 5. Diff at the ITEM (primary-key) level, not just the group level.
|
|
350
|
+
// A `many`-edge element — and a cleared `one` edge — lives in its PARENT's
|
|
351
|
+
// transaction group, so removing it SHRINKS a group rather than dropping a
|
|
352
|
+
// whole group. A group-level diff would rewrite the group's surviving PUTs
|
|
353
|
+
// but never DELETE the removed row, leaving an orphan that re-appears on
|
|
354
|
+
// read (#74). We therefore compute, per changed group, both the items to
|
|
355
|
+
// PUT (new) and the items to DELETE (in old, absent from the entire new
|
|
356
|
+
// state), and apply them together in one transaction.
|
|
288
357
|
const oldGroupMap = new Map(oldGroups.map((g) => [g.name, g]));
|
|
289
358
|
const newGroupMap = new Map(newGroups.map((g) => [g.name, g]));
|
|
290
359
|
// Check if context fields changed — if so, ALL groups must be rewritten
|
|
@@ -295,23 +364,47 @@ const makeAggregate = (schema, config) => {
|
|
|
295
364
|
break;
|
|
296
365
|
}
|
|
297
366
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
367
|
+
// 6. Build the full new + old DynamoDB item sets (with composed keys). The
|
|
368
|
+
// collection SK composites only affect the LSI/GSI mirror attributes,
|
|
369
|
+
// never the (pk, sk) identity used to PUT or DELETE, so the old set's
|
|
370
|
+
// composites are irrelevant — we read only its (pk, sk) for deletes.
|
|
371
|
+
const newDynamo = buildDynamoItems(newGroups, config, pkValue, KeyComposer.extractComposites(config.collection.sk.composite, assembledNew));
|
|
372
|
+
const oldDynamo = buildDynamoItems(oldGroups, config, pkValue, KeyComposer.extractComposites(config.collection.sk.composite, assembledOld));
|
|
373
|
+
const skOf = (item) => item.sk?.S ?? "";
|
|
374
|
+
const newItemsByGroup = new Map(newDynamo.map((g) => [g.group, g.items]));
|
|
375
|
+
const oldItemsByGroup = new Map(oldDynamo.map((g) => [g.group, g.items]));
|
|
376
|
+
// Every sk that survives anywhere in the new decomposition — an old item is
|
|
377
|
+
// an orphan to DELETE only if its key appears in no new group at all.
|
|
378
|
+
const survivingSks = new Set();
|
|
379
|
+
for (const g of newDynamo) {
|
|
380
|
+
for (const item of g.items)
|
|
381
|
+
survivingSks.add(skOf(item));
|
|
382
|
+
}
|
|
383
|
+
// 7. Per group (union of old + new names), collect PUTs + orphan DELETEs.
|
|
384
|
+
const groupWrites = [];
|
|
385
|
+
const groupNames = new Set([...newGroupMap.keys(), ...oldGroupMap.keys()]);
|
|
386
|
+
for (const name of groupNames) {
|
|
304
387
|
const oldGroup = oldGroupMap.get(name);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
388
|
+
const newGroup = newGroupMap.get(name);
|
|
389
|
+
const changed = contextChanged ||
|
|
390
|
+
oldGroup === undefined ||
|
|
391
|
+
newGroup === undefined ||
|
|
392
|
+
!deepEqualGroups(oldGroup, newGroup);
|
|
393
|
+
if (!changed)
|
|
394
|
+
continue;
|
|
395
|
+
const puts = newItemsByGroup.get(name) ?? [];
|
|
396
|
+
const deletes = (oldItemsByGroup.get(name) ?? [])
|
|
397
|
+
.filter((item) => !survivingSks.has(skOf(item)))
|
|
398
|
+
.map((item) => ({
|
|
399
|
+
[config.pk.field]: item[config.pk.field],
|
|
400
|
+
sk: item.sk,
|
|
401
|
+
}));
|
|
402
|
+
if (puts.length === 0 && deletes.length === 0)
|
|
403
|
+
continue;
|
|
404
|
+
groupWrites.push({ group: name, puts, deletes });
|
|
308
405
|
}
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
const collectionSkComposites = KeyComposer.extractComposites(config.collection.sk.composite, assembledNew);
|
|
312
|
-
// 7. Build and write changed groups
|
|
313
|
-
const dynamoGroups = buildDynamoItems(groupsToWrite, config, pkValue, collectionSkComposites);
|
|
314
|
-
yield* writeTransactionGroups(client, tableConfig.name, dynamoGroups, aggregateName);
|
|
406
|
+
if (groupWrites.length > 0) {
|
|
407
|
+
yield* writeTransactionGroups(client, tableConfig.name, groupWrites, aggregateName);
|
|
315
408
|
}
|
|
316
409
|
return decoded;
|
|
317
410
|
}),
|
|
@@ -385,7 +478,7 @@ const makeAggregate = (schema, config) => {
|
|
|
385
478
|
const { allItems } = yield* fetchPartition(key);
|
|
386
479
|
if (allItems.length === 0)
|
|
387
480
|
return undefined;
|
|
388
|
-
const result = yield* assembleAggregate(
|
|
481
|
+
const result = yield* assembleAggregate(decodeSchema, rootNode, allItems, contextFields, key, aggregateName);
|
|
389
482
|
return result;
|
|
390
483
|
});
|
|
391
484
|
if (assembled !== undefined)
|
|
@@ -461,7 +554,7 @@ const deepEqualGroups = (a, b) => {
|
|
|
461
554
|
// ---------------------------------------------------------------------------
|
|
462
555
|
// Internal: Resolve the graph
|
|
463
556
|
// ---------------------------------------------------------------------------
|
|
464
|
-
const resolveNode = (fieldName, entityType, cardinality, edges, discriminator, assemble, decompose, ownDiscriminator) => {
|
|
557
|
+
const resolveNode = (fieldName, entityType, cardinality, edges, discriminator, assemble, decompose, ownDiscriminator, dateEncoders) => {
|
|
465
558
|
const children = [];
|
|
466
559
|
for (const [field, edge] of Object.entries(edges)) {
|
|
467
560
|
if ("_tag" in edge) {
|
|
@@ -470,14 +563,19 @@ const resolveNode = (fieldName, entityType, cardinality, edges, discriminator, a
|
|
|
470
563
|
const mergedDisc = edge.discriminator
|
|
471
564
|
? { ...(discriminator ?? {}), ...edge.discriminator }
|
|
472
565
|
: discriminator;
|
|
473
|
-
children.push(resolveNode(field, edge.entityType, "one", {}, mergedDisc, undefined, undefined, edge.discriminator
|
|
566
|
+
children.push(resolveNode(field, edge.entityType, "one", {}, mergedDisc, undefined, undefined, edge.discriminator,
|
|
567
|
+
// Encode this edge entity's own date fields on write (issue #72).
|
|
568
|
+
buildDateEncoders(fieldsOf(edge.entity?.model))));
|
|
474
569
|
}
|
|
475
570
|
else if (edge._tag === "ManyEdge") {
|
|
476
|
-
children.push(resolveNode(field, edge.entityType, "many", {}, discriminator, edge.assemble, edge.decompose));
|
|
571
|
+
children.push(resolveNode(field, edge.entityType, "many", {}, discriminator, edge.assemble, edge.decompose, undefined, buildDateEncoders(fieldsOf(edge.entity?.model))));
|
|
477
572
|
}
|
|
478
573
|
else if (edge._tag === "BoundSubAggregate") {
|
|
479
574
|
const bound = edge;
|
|
480
|
-
const subChildren = resolveNode(field, bound.aggregate.root.entityType, "one", bound.aggregate.edges, bound.discriminator, undefined, undefined, bound.discriminator
|
|
575
|
+
const subChildren = resolveNode(field, bound.aggregate.root.entityType, "one", bound.aggregate.edges, bound.discriminator, undefined, undefined, bound.discriminator,
|
|
576
|
+
// The sub-aggregate root item carries the sub-schema's own (non-edge)
|
|
577
|
+
// date fields; its child edges get their own encoders via recursion.
|
|
578
|
+
buildDateEncoders(fieldsOf(bound.aggregate.schema)));
|
|
481
579
|
children.push(subChildren);
|
|
482
580
|
}
|
|
483
581
|
}
|
|
@@ -491,6 +589,7 @@ const resolveNode = (fieldName, entityType, cardinality, edges, discriminator, a
|
|
|
491
589
|
children,
|
|
492
590
|
assemble,
|
|
493
591
|
decompose,
|
|
592
|
+
dateEncoders,
|
|
494
593
|
};
|
|
495
594
|
};
|
|
496
595
|
// ---------------------------------------------------------------------------
|
|
@@ -521,7 +620,7 @@ const queryAllItems = (client, tableName, indexName, pkField, pkValue) => Effect
|
|
|
521
620
|
// ---------------------------------------------------------------------------
|
|
522
621
|
// Internal: Assembly algorithm
|
|
523
622
|
// ---------------------------------------------------------------------------
|
|
524
|
-
const assembleAggregate = (
|
|
623
|
+
const assembleAggregate = (decodeSchema, rootNode, allItems, _contextFields, key, aggregateName) => Effect.gen(function* () {
|
|
525
624
|
// Find the root item
|
|
526
625
|
const rootItems = allItems.filter((item) => item.__edd_e__ === rootNode.entityType);
|
|
527
626
|
if (rootItems.length === 0) {
|
|
@@ -574,19 +673,12 @@ const assembleAggregate = (schema, rootNode, allItems, _contextFields, dateDecod
|
|
|
574
673
|
continue;
|
|
575
674
|
rootFields[fieldKey] = value;
|
|
576
675
|
}
|
|
577
|
-
//
|
|
578
|
-
// Schema.decodeUnknownEffect expects domain types (Date, DateTime), not encoded strings.
|
|
579
|
-
// The decoder is the substituted bidirectional date schema built at make() time —
|
|
580
|
-
// identical wire format to the legacy `deserializeDateFromDynamo` helper.
|
|
581
|
-
for (const [field, decode] of Object.entries(dateDecoders)) {
|
|
582
|
-
if (field in rootFields && rootFields[field] != null) {
|
|
583
|
-
rootFields[field] = decode(rootFields[field]);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
// Merge root fields with assembled edge values
|
|
676
|
+
// Merge raw (wire-form) root fields with assembled edge items.
|
|
587
677
|
const assembled = { ...rootFields, ...edgeValues };
|
|
588
|
-
// Decode through the aggregate
|
|
589
|
-
|
|
678
|
+
// Decode through `decodeSchema` — a tolerant clone of the aggregate schema
|
|
679
|
+
// that lifts every date / Redacted leaf (root + nested edges) from the stored
|
|
680
|
+
// wire form to its domain value in a single pass (#72 + Option A).
|
|
681
|
+
const decoded = yield* Schema.decodeUnknownEffect(decodeSchema)(assembled).pipe(Effect.mapError((cause) => new ValidationError({
|
|
590
682
|
entityType: aggregateName,
|
|
591
683
|
operation: "aggregate.assemble",
|
|
592
684
|
cause,
|
|
@@ -716,15 +808,22 @@ const hydrateAggregateRefs = (input, edges, aggregateName) => Effect.gen(functio
|
|
|
716
808
|
const identifierName = idField?.name ?? "id";
|
|
717
809
|
// Deduplicate IDs for this entity type
|
|
718
810
|
const uniqueIds = [...new Set(reqs.map((r) => r.id))];
|
|
719
|
-
// `RefEntity` is the pure structural bound (no `get` — issue #66)
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
|
|
811
|
+
// `RefEntity` is the pure structural bound (no `get` — issue #66): aggregate
|
|
812
|
+
// edges can be authored from AWS-free `@effect-dynamodb/schema` definitions,
|
|
813
|
+
// which carry no runtime `.get`/operations. Ref hydration is a runtime
|
|
814
|
+
// operation, so promote a pure edge target to a full operational Entity (a
|
|
815
|
+
// thin op-attach over its retained `_data`); runtime-authored targets already
|
|
816
|
+
// carry `.get` and pass through untouched. Mirrors `DynamoClient.make()`'s
|
|
817
|
+
// entity binding. The promoted entity also exposes `.schemas`, used by the
|
|
818
|
+
// encode-back below (issue #72). The loop is already grouped by entity type,
|
|
819
|
+
// so this promotes once per distinct edge entity type per hydrate (issue #71).
|
|
820
|
+
const runtimeEntity = (typeof entity.get === "function"
|
|
821
|
+
? entity
|
|
822
|
+
: entityFromDefinition(entity));
|
|
724
823
|
// Build EntityGet intermediates for batch fetching
|
|
725
824
|
const getOps = uniqueIds.map((id) => runtimeEntity.get({ [identifierName]: id }));
|
|
726
825
|
const results = yield* Batch.get(getOps);
|
|
727
|
-
// Map results back to lookup, failing on missing refs
|
|
826
|
+
// Map results back to lookup, failing on missing refs.
|
|
728
827
|
for (let i = 0; i < uniqueIds.length; i++) {
|
|
729
828
|
const id = uniqueIds[i];
|
|
730
829
|
const result = results[i];
|
|
@@ -736,7 +835,19 @@ const hydrateAggregateRefs = (input, edges, aggregateName) => Effect.gen(functio
|
|
|
736
835
|
refId: id,
|
|
737
836
|
});
|
|
738
837
|
}
|
|
739
|
-
|
|
838
|
+
// `Batch.get` returns the DECODED record — transform fields are lifted to
|
|
839
|
+
// their domain form (e.g. `Schema.DateTimeUtcFromString` → `DateTime`). The
|
|
840
|
+
// denormalized ref is spliced into `input` and decoded again by the
|
|
841
|
+
// aggregate's strict `schema`, whose nested edge field is the ORIGINAL ref
|
|
842
|
+
// class expecting the wire form. Re-encode through the ref's substituted
|
|
843
|
+
// `recordSchema` so the spliced value is wire-form and decoded exactly once
|
|
844
|
+
// (issue #72) — this also keeps it marshall-safe for decompose.
|
|
845
|
+
const wire = yield* Schema.encodeUnknownEffect(runtimeEntity.schemas.recordSchema)(result).pipe(Effect.mapError((cause) => new ValidationError({
|
|
846
|
+
entityType: aggregateName,
|
|
847
|
+
operation: "aggregate.hydrateRefs.encode",
|
|
848
|
+
cause,
|
|
849
|
+
})));
|
|
850
|
+
hydratedLookup.set(`${entityType}:${id}`, wire);
|
|
740
851
|
}
|
|
741
852
|
}
|
|
742
853
|
// Walk the input again and replace IDs with hydrated data
|
|
@@ -963,6 +1074,8 @@ const decomposeNode = (items, node, value, contextValues, parentGroup, parentDis
|
|
|
963
1074
|
continue;
|
|
964
1075
|
subRootAttrs[key] = val;
|
|
965
1076
|
}
|
|
1077
|
+
// Serialize this node's own date fields to their wire primitive (#72).
|
|
1078
|
+
applyNodeDateEncoders(subRootAttrs, node.dateEncoders);
|
|
966
1079
|
// Inject context and discriminator
|
|
967
1080
|
mergeContextValues(subRootAttrs, contextValues);
|
|
968
1081
|
if (node.discriminator) {
|
|
@@ -986,6 +1099,8 @@ const decomposeNode = (items, node, value, contextValues, parentGroup, parentDis
|
|
|
986
1099
|
else if (node.cardinality === "one") {
|
|
987
1100
|
// Simple one-to-one edge (no children)
|
|
988
1101
|
const attrs = { ...value };
|
|
1102
|
+
// Serialize this edge entity's own date fields to their wire primitive (#72).
|
|
1103
|
+
applyNodeDateEncoders(attrs, node.dateEncoders);
|
|
989
1104
|
mergeContextValues(attrs, contextValues);
|
|
990
1105
|
if (node.discriminator) {
|
|
991
1106
|
for (const [k, v] of Object.entries(node.discriminator)) {
|
|
@@ -1015,6 +1130,8 @@ const decomposeNode = (items, node, value, contextValues, parentGroup, parentDis
|
|
|
1015
1130
|
: [];
|
|
1016
1131
|
for (const elem of arrayItems) {
|
|
1017
1132
|
const attrs = { ...elem };
|
|
1133
|
+
// Serialize this edge entity's own date fields to their wire primitive (#72).
|
|
1134
|
+
applyNodeDateEncoders(attrs, node.dateEncoders);
|
|
1018
1135
|
mergeContextValues(attrs, contextValues);
|
|
1019
1136
|
if (node.discriminator) {
|
|
1020
1137
|
for (const [k, v] of Object.entries(node.discriminator)) {
|
|
@@ -1116,22 +1233,33 @@ const buildDynamoItems = (groups, config, pkValue, rootCollectionSkComposites) =
|
|
|
1116
1233
|
// Internal: Write transaction groups to DynamoDB
|
|
1117
1234
|
// ---------------------------------------------------------------------------
|
|
1118
1235
|
const writeTransactionGroups = (client, tableName, groups, aggregateName) => Effect.gen(function* () {
|
|
1236
|
+
// Validate ALL group sizes BEFORE writing any group. Each group is its own
|
|
1237
|
+
// transaction (DynamoDB: 100 items per TransactWriteItems), and the per-group
|
|
1238
|
+
// count now spans Puts AND Deletes together — so an `update` that both adds and
|
|
1239
|
+
// removes many edges roughly doubles the count. Checking up front means an
|
|
1240
|
+
// oversized later group fails fast instead of partially committing earlier ones.
|
|
1119
1241
|
for (const group of groups) {
|
|
1120
|
-
|
|
1121
|
-
if (
|
|
1242
|
+
const itemCount = group.puts.length + group.deletes.length;
|
|
1243
|
+
if (itemCount > 100) {
|
|
1122
1244
|
return yield* new AggregateTransactionOverflow({
|
|
1123
1245
|
aggregate: aggregateName,
|
|
1124
1246
|
subgraph: group.group,
|
|
1125
|
-
itemCount
|
|
1247
|
+
itemCount,
|
|
1126
1248
|
limit: 100,
|
|
1127
1249
|
});
|
|
1128
1250
|
}
|
|
1129
|
-
|
|
1251
|
+
}
|
|
1252
|
+
for (const group of groups) {
|
|
1253
|
+
// Each group is one transaction — Puts AND Deletes applied atomically, so an
|
|
1254
|
+
// edge add and a sibling edge removal in the same sub-aggregate commit together.
|
|
1255
|
+
const itemCount = group.puts.length + group.deletes.length;
|
|
1256
|
+
if (itemCount === 0)
|
|
1130
1257
|
continue;
|
|
1131
|
-
// Build TransactWriteItems request
|
|
1132
|
-
const transactItems =
|
|
1133
|
-
Put: { TableName: tableName, Item: item },
|
|
1134
|
-
|
|
1258
|
+
// Build TransactWriteItems request — Puts first, then Deletes for orphans.
|
|
1259
|
+
const transactItems = [
|
|
1260
|
+
...group.puts.map((item) => ({ Put: { TableName: tableName, Item: item } })),
|
|
1261
|
+
...group.deletes.map((key) => ({ Delete: { TableName: tableName, Key: key } })),
|
|
1262
|
+
];
|
|
1135
1263
|
yield* client.transactWriteItems({ TransactItems: transactItems }).pipe(Effect.mapError((error) => {
|
|
1136
1264
|
if (error.cause != null &&
|
|
1137
1265
|
typeof error.cause === "object" &&
|