effect-dynamodb 1.3.3 → 1.4.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/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +51 -23
- package/dist/Aggregate.js.map +1 -1
- package/dist/Entity.d.ts +1 -3
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +152 -204
- package/dist/Entity.js.map +1 -1
- package/dist/Marshaller.d.ts +6 -17
- package/dist/Marshaller.d.ts.map +1 -1
- package/dist/Marshaller.js +6 -102
- package/dist/Marshaller.js.map +1 -1
- package/dist/internal/EntitySchemas.d.ts +93 -16
- package/dist/internal/EntitySchemas.d.ts.map +1 -1
- package/dist/internal/EntitySchemas.js +396 -79
- package/dist/internal/EntitySchemas.js.map +1 -1
- package/dist/internal/TransactableOps.d.ts +5 -0
- package/dist/internal/TransactableOps.d.ts.map +1 -1
- package/dist/internal/TransactableOps.js +33 -27
- package/dist/internal/TransactableOps.js.map +1 -1
- package/package.json +1 -1
package/dist/Entity.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* carries derived schemas for 7 type extractors (Model, Record, Input, Update,
|
|
7
7
|
* Key, Item, Marshalled).
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { Duration, Effect, Schema, Stream } from "effect";
|
|
10
10
|
import { DynamoClient } from "./DynamoClient.js";
|
|
11
|
-
import {
|
|
11
|
+
import { getIdentifierField, isConfiguredModel, isHidden, isRef, isRefField, } from "./DynamoModel.js";
|
|
12
12
|
import * as DynamoSchema from "./DynamoSchema.js";
|
|
13
13
|
import { CascadePartialFailure, ConditionalCheckFailed, ItemNotFound, isAwsConditionalCheckFailed, isAwsTransactionCancelled, OptimisticLockError, RefNotFound, TransactionOverflow, UniqueConstraintViolation, ValidationError, } from "./Errors.js";
|
|
14
14
|
import { makeBoundDelete, makeBoundPut, makeBoundUpdate } from "./internal/BoundCrud.js";
|
|
@@ -17,7 +17,7 @@ import { compileExpr, createConditionOps, isExpr, parseShorthand, parseSimpleSho
|
|
|
17
17
|
import { compilePath, createPathBuilder } from "./internal/PathBuilder.js";
|
|
18
18
|
import * as KeyComposer from "./KeyComposer.js";
|
|
19
19
|
import { normalizeGsiConfig } from "./KeyComposer.js";
|
|
20
|
-
import {
|
|
20
|
+
import { fromAttributeMap, toAttributeMap, toAttributeValue } from "./Marshaller.js";
|
|
21
21
|
import * as Query from "./Query.js";
|
|
22
22
|
import { filterExpr, selectPaths } from "./Query.js";
|
|
23
23
|
export { EntityDeleteImpl, EntityDeleteTypeId, EntityGetImpl, EntityOpTypeId, EntityPutImpl, EntityUpdateImpl, EntityUpdateTypeId, emptyUpdateState, returnValuesMap, } from "./internal/EntityOps.js";
|
|
@@ -25,7 +25,7 @@ import { EntityDeleteImpl, EntityDeleteTypeId, EntityGetImpl, EntityOpTypeId, En
|
|
|
25
25
|
import * as Projection from "./Projection.js";
|
|
26
26
|
export { add, append, asItem, asModel, asNative, asRecord, cascade, consistentRead, deleteFromSet, expectedVersion, pathAdd, pathAppend, pathDelete, pathIfNotExists, pathPrepend, pathRemove, pathSet, pathSubtract, project, remove, returnValues, set, subtract, } from "./internal/EntityCombinators.js";
|
|
27
27
|
import { asModel, condition as conditionCombinator, expectedVersion, set, } from "./internal/EntityCombinators.js";
|
|
28
|
-
import { allCompositeAttributes, allKeyFieldNames, buildDerivedSchemas,
|
|
28
|
+
import { allCompositeAttributes, allKeyFieldNames, buildDerivedSchemas, buildFieldEncodings, getFields, primaryKeyComposites, resolveSystemFields, resolveUniqueFields, validateNoTransformOverride, } from "./internal/EntitySchemas.js";
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Sparse-aware unique sentinel composition
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
@@ -61,6 +61,40 @@ const composeUniqueSentinel = (schema, entityType, constraintName, constraintDef
|
|
|
61
61
|
};
|
|
62
62
|
};
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
|
+
// Encode-or-decode-encode helper
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Validate user input and produce wire-form output, accepting both Type and
|
|
68
|
+
* Encoded shapes. Strategy:
|
|
69
|
+
*
|
|
70
|
+
* 1. Try `Schema.encode(input)`. This is the canonical Type → Encoded
|
|
71
|
+
* conversion and works when the user passes Type values for transforms
|
|
72
|
+
* (e.g. a `DateTime.Utc` for `Schema.DateTimeUtcFromString`, a `number`
|
|
73
|
+
* for `Schema.NumberFromString`, a `Redacted<string>` for our
|
|
74
|
+
* RedactedFromValue substitute).
|
|
75
|
+
*
|
|
76
|
+
* 2. On encode failure, fall back to `Schema.decode → Schema.encode`. This
|
|
77
|
+
* lifts plain inputs to the Type shape first (`Schema.Class` decode is
|
|
78
|
+
* forgiving — it constructs a class instance from a plain object), then
|
|
79
|
+
* re-encodes back to wire form. Required for nested `Schema.Class`
|
|
80
|
+
* fields whose Type is a class instance but whose Encoded is a plain
|
|
81
|
+
* object — users typically construct the plain object directly.
|
|
82
|
+
*
|
|
83
|
+
* Returns the Encoded shape (a plain object whose values are wire
|
|
84
|
+
* primitives), ready for `toAttributeMap`.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
const encodeOrDecodeEncode = (schema, input, entityType, operation) => Schema.encodeUnknownEffect(schema)(input).pipe(Effect.catch((primaryCause) => Schema.decodeUnknownEffect(schema)(input).pipe(Effect.flatMap((decoded) => Schema.encodeUnknownEffect(schema)(decoded)),
|
|
89
|
+
// On fallback failure, surface the original encode error — its
|
|
90
|
+
// message is keyed on the caller's input shape, which is what the
|
|
91
|
+
// user expects to see.
|
|
92
|
+
Effect.catch(() => Effect.fail(new ValidationError({
|
|
93
|
+
entityType,
|
|
94
|
+
operation: `${operation}.decode`,
|
|
95
|
+
cause: primaryCause,
|
|
96
|
+
}))))));
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
64
98
|
// Transaction limit pre-check (100-item DynamoDB limit)
|
|
65
99
|
// ---------------------------------------------------------------------------
|
|
66
100
|
/**
|
|
@@ -306,7 +340,15 @@ const makeImpl = (config) => {
|
|
|
306
340
|
}
|
|
307
341
|
// Resolve identifier field name from model annotation
|
|
308
342
|
const resolvedIdentifier = getIdentifierField(config.model)?.name;
|
|
309
|
-
|
|
343
|
+
// Validate: no model field combines a transform schema with a ConfiguredModel
|
|
344
|
+
// storage override. The two configuration paths are mutually exclusive.
|
|
345
|
+
validateNoTransformOverride(modelFields, configuredAttributes);
|
|
346
|
+
// Resolve effective field encodings (annotation + ConfiguredModel override).
|
|
347
|
+
// Used by buildDerivedSchemas to substitute self date schemas with bidirectional
|
|
348
|
+
// transforms, and (below) by system-field generation to produce the right
|
|
349
|
+
// wire primitive when the field is library-managed.
|
|
350
|
+
const fieldEncodings = buildFieldEncodings(modelFields, configuredAttributes);
|
|
351
|
+
const schemas = buildDerivedSchemas(modelFields, allIndexes, systemFields, resolvedRefs, immutableFields, resolvedIdentifier, config.timeSeries, fieldEncodings);
|
|
310
352
|
// schema and tableTag are injected via _configure() when the entity is registered
|
|
311
353
|
// on a Table and bound through DynamoClient.make(). They are captured by operation
|
|
312
354
|
// closures and resolved at runtime (inside Effects), not at definition time.
|
|
@@ -315,37 +357,10 @@ const makeImpl = (config) => {
|
|
|
315
357
|
let tableTag;
|
|
316
358
|
const entityType = config.entityType;
|
|
317
359
|
const entityVersion = 1;
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
const fieldEncodings = {};
|
|
323
|
-
for (const [fieldName, fieldSchema] of Object.entries(modelFields)) {
|
|
324
|
-
const encoding = getEncoding(fieldSchema);
|
|
325
|
-
if (encoding) {
|
|
326
|
-
fieldEncodings[fieldName] = encoding;
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
// Auto-detect standard Effect date schemas (Pattern B: pure model)
|
|
330
|
-
const inferred = inferDefaultEncoding(fieldSchema);
|
|
331
|
-
if (inferred)
|
|
332
|
-
fieldEncodings[fieldName] = inferred;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
// Apply ConfiguredModel storage overrides (takes precedence over schema annotations)
|
|
336
|
-
for (const [fieldName, attrConfig] of Object.entries(configuredAttributes)) {
|
|
337
|
-
if (attrConfig.encoding) {
|
|
338
|
-
const existing = fieldEncodings[fieldName];
|
|
339
|
-
fieldEncodings[fieldName] = {
|
|
340
|
-
storage: attrConfig.encoding.storage,
|
|
341
|
-
domain: existing?.domain ?? attrConfig.encoding.domain,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
// Note: System timestamp encodings are NOT added to fieldEncodings.
|
|
346
|
-
// generateTimestamp() handles serialization directly for timestamps.
|
|
347
|
-
// This avoids double-serialization in serializeDateFields.
|
|
348
|
-
const hasDateFields = Object.keys(fieldEncodings).length > 0;
|
|
360
|
+
// Note: `fieldEncodings` is resolved earlier (above buildDerivedSchemas) and
|
|
361
|
+
// used to substitute self date schemas in derived schemas. System timestamp
|
|
362
|
+
// encodings are NOT in `fieldEncodings`; `generateTimestamp` produces wire
|
|
363
|
+
// primitives directly for non-colliding system fields.
|
|
349
364
|
// ---------------------------------------------------------------------------
|
|
350
365
|
// Field renaming: domain name → DynamoDB attribute name (from ConfiguredModel)
|
|
351
366
|
// ---------------------------------------------------------------------------
|
|
@@ -385,32 +400,6 @@ const makeImpl = (config) => {
|
|
|
385
400
|
};
|
|
386
401
|
/** Resolve a domain field name to its DynamoDB attribute name. */
|
|
387
402
|
const resolveDbName = (domainName) => fieldRenames[domainName] ?? domainName;
|
|
388
|
-
/**
|
|
389
|
-
* Convert domain date values to storage primitives for DynamoEncoding-annotated fields.
|
|
390
|
-
* Called before toAttributeMap (put path).
|
|
391
|
-
*/
|
|
392
|
-
const serializeDateFields = (item) => {
|
|
393
|
-
if (!hasDateFields)
|
|
394
|
-
return;
|
|
395
|
-
for (const [field, encoding] of Object.entries(fieldEncodings)) {
|
|
396
|
-
if (field in item && item[field] != null) {
|
|
397
|
-
item[field] = serializeDateForDynamo(item[field], encoding);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
/**
|
|
402
|
-
* Convert storage primitives back to domain values for DynamoEncoding-annotated fields.
|
|
403
|
-
* Called after fromAttributeMap (get path).
|
|
404
|
-
*/
|
|
405
|
-
const deserializeDateFields = (raw) => {
|
|
406
|
-
if (!hasDateFields)
|
|
407
|
-
return;
|
|
408
|
-
for (const [field, encoding] of Object.entries(fieldEncodings)) {
|
|
409
|
-
if (field in raw && raw[field] != null) {
|
|
410
|
-
raw[field] = deserializeDateFromDynamo(raw[field], encoding);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
403
|
// ---------------------------------------------------------------------------
|
|
415
404
|
// Helpers
|
|
416
405
|
// ---------------------------------------------------------------------------
|
|
@@ -426,7 +415,6 @@ const makeImpl = (config) => {
|
|
|
426
415
|
const attachPrototype = (decoded) => isSchemaClass ? Object.assign(Object.create(rawModel.prototype), decoded) : decoded;
|
|
427
416
|
const decodeRecord = (raw) => {
|
|
428
417
|
renameFromDynamo(raw);
|
|
429
|
-
deserializeDateFields(raw);
|
|
430
418
|
return Schema.decodeUnknownEffect(schemas.recordSchema)(raw).pipe(Effect.map(attachPrototype), Effect.mapError((cause) => new ValidationError({
|
|
431
419
|
entityType,
|
|
432
420
|
operation: "decode",
|
|
@@ -437,15 +425,11 @@ const makeImpl = (config) => {
|
|
|
437
425
|
const decodeAs = (raw, marshalled, mode) => {
|
|
438
426
|
if (mode === "native")
|
|
439
427
|
return Effect.succeed(marshalled);
|
|
440
|
-
// Rename DynamoDB attributes back to domain field names
|
|
428
|
+
// Rename DynamoDB attributes back to domain field names. Substituted
|
|
429
|
+
// schemas handle wire→domain conversion (date primitives, Redacted, etc.).
|
|
441
430
|
renameFromDynamo(raw);
|
|
442
|
-
// Convert date storage primitives to domain values before Schema decode.
|
|
443
|
-
// recordSchema/itemSchema use fromSelf variants for annotated fields,
|
|
444
|
-
// so they accept domain values directly. modelSchema also uses fromSelf
|
|
445
|
-
// for the decode path after deserialization.
|
|
446
|
-
deserializeDateFields(raw);
|
|
447
431
|
const targetSchema = mode === "model"
|
|
448
|
-
? schemas.
|
|
432
|
+
? schemas.modelSchema
|
|
449
433
|
: mode === "record"
|
|
450
434
|
? schemas.recordSchema
|
|
451
435
|
: schemas.itemSchema;
|
|
@@ -461,33 +445,26 @@ const makeImpl = (config) => {
|
|
|
461
445
|
};
|
|
462
446
|
const nowIso = () => new Date().toISOString();
|
|
463
447
|
/**
|
|
464
|
-
* Generate a timestamp value
|
|
448
|
+
* Generate a timestamp value as a wire primitive ready for marshalling.
|
|
465
449
|
* Default (no encoding): ISO string. Custom encoding: serialized primitive.
|
|
466
450
|
*
|
|
467
|
-
* Used for
|
|
468
|
-
*
|
|
469
|
-
* `serializeDateFields` (because the field is absent from `fieldEncodings`).
|
|
451
|
+
* Used for system fields whether colliding or not — the value is always a
|
|
452
|
+
* wire primitive (string or number) ready for `toAttributeMap`.
|
|
470
453
|
*/
|
|
471
454
|
const generateTimestamp = (encoding) => {
|
|
472
455
|
if (!encoding)
|
|
473
456
|
return nowIso();
|
|
474
|
-
const now =
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
case "DateTime.Utc":
|
|
486
|
-
return DateTime.makeUnsafe(new Date());
|
|
487
|
-
case "DateTime.Zoned":
|
|
488
|
-
return DateTime.makeZonedUnsafe(new Date(), { timeZone: "UTC" });
|
|
489
|
-
case "Date":
|
|
490
|
-
return new Date();
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
switch (encoding.storage) {
|
|
459
|
+
case "string":
|
|
460
|
+
// For DateTime.Zoned this would need the extended ISO format, but
|
|
461
|
+
// system timestamps generated here are UTC — DateTime.Zoned-typed
|
|
462
|
+
// collision fields are rare and not previously special-cased either.
|
|
463
|
+
return new Date(now).toISOString();
|
|
464
|
+
case "epochMs":
|
|
465
|
+
return now;
|
|
466
|
+
case "epochSeconds":
|
|
467
|
+
return Math.floor(now / 1000);
|
|
491
468
|
}
|
|
492
469
|
};
|
|
493
470
|
// ---------------------------------------------------------------------------
|
|
@@ -777,41 +754,41 @@ const makeImpl = (config) => {
|
|
|
777
754
|
const put = (input) => new EntityPutImpl((mode, opts) => Effect.gen(function* () {
|
|
778
755
|
const client = yield* DynamoClient;
|
|
779
756
|
const { name: tableName } = yield* tableTag;
|
|
780
|
-
//
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
//
|
|
787
|
-
|
|
757
|
+
// Encode user input → wire form. Users typically construct domain
|
|
758
|
+
// values for transforms (DateTime, Redacted, Number) and plain
|
|
759
|
+
// objects for Schema.Class fields. To handle both:
|
|
760
|
+
// 1. Try `encode` directly — the canonical Type → Encoded
|
|
761
|
+
// conversion. Works when the input is fully Type-shaped
|
|
762
|
+
// (which is the case for the transforms that issue #29
|
|
763
|
+
// surfaced: RedactedFromValue, NumberFromString, etc.).
|
|
764
|
+
// 2. On failure, fall back to `decode → encode` — `decode` is
|
|
765
|
+
// forgiving for Schema.Class (lifts plain objects to
|
|
766
|
+
// instances) and the substituted date/Redacted transforms
|
|
767
|
+
// tolerate either form. The subsequent `encode` produces
|
|
768
|
+
// the canonical wire shape.
|
|
769
|
+
const encodedInput = yield* encodeOrDecodeEncode(schemas.inputSchema, input, entityType, "put");
|
|
770
|
+
// Compose all keys from the encoded input (ID fields still present
|
|
771
|
+
// and key composites are wire-format strings/numbers).
|
|
772
|
+
const keys = composeAllKeys(encodedInput);
|
|
788
773
|
// Hydrate refs: replace ID fields with full entity domain data
|
|
789
|
-
const
|
|
790
|
-
? yield* hydrateRefs(
|
|
791
|
-
:
|
|
792
|
-
// Build the DynamoDB item
|
|
793
|
-
const item = { ...
|
|
774
|
+
const encoded = hasRefs
|
|
775
|
+
? yield* hydrateRefs(encodedInput)
|
|
776
|
+
: encodedInput;
|
|
777
|
+
// Build the DynamoDB item — already in wire-format after encode
|
|
778
|
+
const item = { ...encoded };
|
|
794
779
|
// Add system fields. When a timestamp field collides with a
|
|
795
780
|
// model-declared field, the user may have supplied their own value
|
|
796
|
-
// (
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
// through `serializeDateFields` downstream. Non-colliding fields
|
|
800
|
-
// get a storage primitive directly (no `serializeDateFields` pass).
|
|
781
|
+
// (optional on collision in `inputSchema`). Respect their value if
|
|
782
|
+
// present (already encoded to wire); otherwise generate a wire
|
|
783
|
+
// primitive directly.
|
|
801
784
|
if (systemFields.createdAt) {
|
|
802
785
|
if (item[systemFields.createdAt] === undefined) {
|
|
803
|
-
item[systemFields.createdAt] =
|
|
804
|
-
systemFields.createdAtCollision && systemFields.createdAtEncoding
|
|
805
|
-
? generateDomainTimestamp(systemFields.createdAtEncoding)
|
|
806
|
-
: generateTimestamp(systemFields.createdAtEncoding);
|
|
786
|
+
item[systemFields.createdAt] = generateTimestamp(systemFields.createdAtEncoding);
|
|
807
787
|
}
|
|
808
788
|
}
|
|
809
789
|
if (systemFields.updatedAt) {
|
|
810
790
|
if (item[systemFields.updatedAt] === undefined) {
|
|
811
|
-
item[systemFields.updatedAt] =
|
|
812
|
-
systemFields.updatedAtCollision && systemFields.updatedAtEncoding
|
|
813
|
-
? generateDomainTimestamp(systemFields.updatedAtEncoding)
|
|
814
|
-
: generateTimestamp(systemFields.updatedAtEncoding);
|
|
791
|
+
item[systemFields.updatedAt] = generateTimestamp(systemFields.updatedAtEncoding);
|
|
815
792
|
}
|
|
816
793
|
}
|
|
817
794
|
if (systemFields.version)
|
|
@@ -820,8 +797,6 @@ const makeImpl = (config) => {
|
|
|
820
797
|
item.__edd_e__ = entityType;
|
|
821
798
|
// Apply composed keys
|
|
822
799
|
Object.assign(item, keys);
|
|
823
|
-
// Convert date fields from domain values to storage primitives
|
|
824
|
-
serializeDateFields(item);
|
|
825
800
|
// Rename domain fields to DynamoDB attribute names
|
|
826
801
|
renameToDynamo(item);
|
|
827
802
|
const marshalledItem = toAttributeMap(item);
|
|
@@ -851,7 +826,7 @@ const makeImpl = (config) => {
|
|
|
851
826
|
const sentinelConstraints = [];
|
|
852
827
|
if (hasUniqueConstraints) {
|
|
853
828
|
for (const [constraintName, constraintDef] of Object.entries(config.unique)) {
|
|
854
|
-
const sentinel = composeUniqueSentinel(schema, entityType, constraintName, constraintDef,
|
|
829
|
+
const sentinel = composeUniqueSentinel(schema, entityType, constraintName, constraintDef, encoded);
|
|
855
830
|
if (!sentinel)
|
|
856
831
|
continue;
|
|
857
832
|
sentinelConstraints.push(constraintName);
|
|
@@ -894,7 +869,7 @@ const makeImpl = (config) => {
|
|
|
894
869
|
if (opts.condition && reasons[0]?.Code === "ConditionalCheckFailed") {
|
|
895
870
|
return new ConditionalCheckFailed({
|
|
896
871
|
entityType,
|
|
897
|
-
key:
|
|
872
|
+
key: encoded,
|
|
898
873
|
});
|
|
899
874
|
}
|
|
900
875
|
// Sentinel items start at index 1, in the order recorded in
|
|
@@ -908,7 +883,7 @@ const makeImpl = (config) => {
|
|
|
908
883
|
: [];
|
|
909
884
|
const fieldsRecord = {};
|
|
910
885
|
for (const f of uniqueFields) {
|
|
911
|
-
const v =
|
|
886
|
+
const v = encoded[f];
|
|
912
887
|
if (v !== undefined && v !== null) {
|
|
913
888
|
fieldsRecord[f] = KeyComposer.serializeValue(v);
|
|
914
889
|
}
|
|
@@ -940,7 +915,10 @@ const makeImpl = (config) => {
|
|
|
940
915
|
}
|
|
941
916
|
yield* client.putItem(putInput).pipe(Effect.mapError((err) => {
|
|
942
917
|
if (opts.condition && isAwsConditionalCheckFailed(err.cause)) {
|
|
943
|
-
return new ConditionalCheckFailed({
|
|
918
|
+
return new ConditionalCheckFailed({
|
|
919
|
+
entityType,
|
|
920
|
+
key: encoded,
|
|
921
|
+
});
|
|
944
922
|
}
|
|
945
923
|
return err;
|
|
946
924
|
}));
|
|
@@ -1025,17 +1003,13 @@ const makeImpl = (config) => {
|
|
|
1025
1003
|
operation: "update.decodeKey",
|
|
1026
1004
|
cause,
|
|
1027
1005
|
})));
|
|
1028
|
-
//
|
|
1029
|
-
const
|
|
1030
|
-
entityType,
|
|
1031
|
-
operation: "update.decodeUpdates",
|
|
1032
|
-
cause,
|
|
1033
|
-
})));
|
|
1006
|
+
// Encode update payload → wire form (see `put` for the strategy).
|
|
1007
|
+
const encodedUpdates = yield* encodeOrDecodeEncode(schemas.updateSchema, updates ?? {}, entityType, "update");
|
|
1034
1008
|
// Hydrate refs in updates: for any ${field}Id present, fetch and embed
|
|
1035
|
-
let hydratedUpdates =
|
|
1009
|
+
let hydratedUpdates = encodedUpdates;
|
|
1036
1010
|
if (hasRefs) {
|
|
1037
|
-
const refsToHydrate = resolvedRefs.filter((ref) => ref.idFieldName in
|
|
1038
|
-
|
|
1011
|
+
const refsToHydrate = resolvedRefs.filter((ref) => ref.idFieldName in encodedUpdates &&
|
|
1012
|
+
encodedUpdates[ref.idFieldName] !==
|
|
1039
1013
|
undefined);
|
|
1040
1014
|
if (refsToHydrate.length > 0) {
|
|
1041
1015
|
hydratedUpdates = yield* hydrateRefs(hydratedUpdates, refsToHydrate);
|
|
@@ -1083,24 +1057,17 @@ const makeImpl = (config) => {
|
|
|
1083
1057
|
});
|
|
1084
1058
|
}
|
|
1085
1059
|
// Build new item: merge current + updates + rich ops + recompose keys.
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
|
|
1092
|
-
// `toAttributeMap(newItem)` marshals the DateTime class instances
|
|
1093
|
-
// as DynamoDB Maps (via AWS SDK's `convertClassInstanceToMap`),
|
|
1094
|
-
// which corrupts the write and makes subsequent reads fail to
|
|
1095
|
-
// decode.
|
|
1096
|
-
const serializedRetainUpdates = {
|
|
1097
|
-
...hydratedUpdates,
|
|
1098
|
-
};
|
|
1099
|
-
serializeDateFields(serializedRetainUpdates);
|
|
1100
|
-
const newItem = {
|
|
1060
|
+
// `currentRaw` uses DynamoDB attribute names (e.g. `dn`, `hd`);
|
|
1061
|
+
// `hydratedUpdates` uses domain names. To avoid the merge leaving
|
|
1062
|
+
// both names (which would later have the rename clobber the user's
|
|
1063
|
+
// value), translate the current item's keys to domain names first
|
|
1064
|
+
// so the spread + assign overlay cleanly.
|
|
1065
|
+
const currentDomainItem = {
|
|
1101
1066
|
...currentRaw,
|
|
1102
1067
|
};
|
|
1103
|
-
|
|
1068
|
+
renameFromDynamo(currentDomainItem);
|
|
1069
|
+
const newItem = { ...currentDomainItem };
|
|
1070
|
+
for (const [attr, val] of Object.entries(hydratedUpdates)) {
|
|
1104
1071
|
if (val !== undefined)
|
|
1105
1072
|
newItem[attr] = val;
|
|
1106
1073
|
}
|
|
@@ -1138,24 +1105,21 @@ const makeImpl = (config) => {
|
|
|
1138
1105
|
}
|
|
1139
1106
|
// Increment version and update timestamp. If the caller supplied a
|
|
1140
1107
|
// value for `updatedAt` (allowed when the field collides with a
|
|
1141
|
-
// model-declared field), respect it
|
|
1142
|
-
//
|
|
1143
|
-
// (ISO string / epoch number) — not a domain DateTime instance.
|
|
1108
|
+
// model-declared field), respect it (already wire-form via encode);
|
|
1109
|
+
// else generate a fresh wire primitive.
|
|
1144
1110
|
const newVersion = currentVersion + 1;
|
|
1145
1111
|
if (systemFields.version)
|
|
1146
1112
|
newItem[systemFields.version] = newVersion;
|
|
1147
1113
|
if (systemFields.updatedAt) {
|
|
1148
|
-
const userSupplied =
|
|
1114
|
+
const userSupplied = hydratedUpdates[systemFields.updatedAt];
|
|
1149
1115
|
newItem[systemFields.updatedAt] =
|
|
1150
1116
|
userSupplied !== undefined
|
|
1151
1117
|
? userSupplied
|
|
1152
1118
|
: generateTimestamp(systemFields.updatedAtEncoding);
|
|
1153
1119
|
}
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
//
|
|
1157
|
-
renameFromDynamo(newItem);
|
|
1158
|
-
// Recompose all keys with updated attributes
|
|
1120
|
+
// `newItem` is already in domain names (the merge built it from
|
|
1121
|
+
// `currentDomainItem` + domain-keyed updates) — no rename needed
|
|
1122
|
+
// here. Recompose all keys with the updated attributes.
|
|
1159
1123
|
const newKeys = composeAllKeys(newItem);
|
|
1160
1124
|
Object.assign(newItem, newKeys);
|
|
1161
1125
|
const sentinelRotations = [];
|
|
@@ -1320,18 +1284,17 @@ const makeImpl = (config) => {
|
|
|
1320
1284
|
const names = {};
|
|
1321
1285
|
const values = {};
|
|
1322
1286
|
let counter = 0;
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
const
|
|
1327
|
-
serializeDateFields(serializedUpdates);
|
|
1287
|
+
// `hydratedUpdates` was produced by `Schema.encode(updateSchema)` and
|
|
1288
|
+
// is already in wire-format (ISO string / epoch number / etc.). Use
|
|
1289
|
+
// it directly — no `serializeDateFields` pass required.
|
|
1290
|
+
const encodedUpdatesMap = hydratedUpdates;
|
|
1328
1291
|
// System-colliding updatedAt is handled by the system-field block below.
|
|
1329
|
-
// createdAt and version are already excluded by `
|
|
1292
|
+
// createdAt and version are already excluded by `updateSchema`.
|
|
1330
1293
|
const updateSystemColliders = new Set();
|
|
1331
1294
|
if (systemFields.updatedAtCollision && systemFields.updatedAt)
|
|
1332
1295
|
updateSystemColliders.add(systemFields.updatedAt);
|
|
1333
1296
|
// Add user-provided updates
|
|
1334
|
-
for (const [attr, val] of Object.entries(
|
|
1297
|
+
for (const [attr, val] of Object.entries(encodedUpdatesMap)) {
|
|
1335
1298
|
if (val === undefined)
|
|
1336
1299
|
continue;
|
|
1337
1300
|
if (updateSystemColliders.has(attr))
|
|
@@ -1343,10 +1306,11 @@ const makeImpl = (config) => {
|
|
|
1343
1306
|
setClauses.push(`${nameKey} = ${valKey}`);
|
|
1344
1307
|
counter++;
|
|
1345
1308
|
}
|
|
1346
|
-
// Add updatedAt timestamp. User-supplied value wins (
|
|
1347
|
-
//
|
|
1309
|
+
// Add updatedAt timestamp. User-supplied value wins (already in wire
|
|
1310
|
+
// form via encode); else fall back to a freshly generated wire
|
|
1311
|
+
// primitive.
|
|
1348
1312
|
if (systemFields.updatedAt) {
|
|
1349
|
-
const userSupplied =
|
|
1313
|
+
const userSupplied = encodedUpdatesMap[systemFields.updatedAt];
|
|
1350
1314
|
const nameKey = `#u${counter}`;
|
|
1351
1315
|
const valKey = `:u${counter}`;
|
|
1352
1316
|
names[nameKey] = systemFields.updatedAt;
|
|
@@ -1774,17 +1738,13 @@ const makeImpl = (config) => {
|
|
|
1774
1738
|
const upsert = (input) => new EntityPutImpl((mode, opts) => Effect.gen(function* () {
|
|
1775
1739
|
const client = yield* DynamoClient;
|
|
1776
1740
|
const { name: tableName } = yield* tableTag;
|
|
1777
|
-
//
|
|
1778
|
-
const
|
|
1779
|
-
entityType,
|
|
1780
|
-
operation: "upsert.decode",
|
|
1781
|
-
cause,
|
|
1782
|
-
})));
|
|
1741
|
+
// Encode user input → wire form (see `put` for strategy).
|
|
1742
|
+
const encodedInput = yield* encodeOrDecodeEncode(schemas.inputSchema, input, entityType, "upsert");
|
|
1783
1743
|
// Hydrate refs: replace ID fields with full entity domain data
|
|
1784
|
-
const
|
|
1785
|
-
? yield* hydrateRefs(
|
|
1786
|
-
:
|
|
1787
|
-
const item =
|
|
1744
|
+
const encoded = hasRefs
|
|
1745
|
+
? yield* hydrateRefs(encodedInput)
|
|
1746
|
+
: encodedInput;
|
|
1747
|
+
const item = encoded;
|
|
1788
1748
|
// Compose primary key
|
|
1789
1749
|
const primaryKey = composePrimaryKey(item);
|
|
1790
1750
|
const marshalledKey = toAttributeMap(primaryKey);
|
|
@@ -1804,10 +1764,7 @@ const makeImpl = (config) => {
|
|
|
1804
1764
|
systemColliders.add(systemFields.updatedAt);
|
|
1805
1765
|
if (systemFields.versionCollision && systemFields.version)
|
|
1806
1766
|
systemColliders.add(systemFields.version);
|
|
1807
|
-
//
|
|
1808
|
-
if (hasDateFields) {
|
|
1809
|
-
serializeDateFields(item);
|
|
1810
|
-
}
|
|
1767
|
+
// `item` is already in wire-form via `Schema.encode(inputSchema)`.
|
|
1811
1768
|
// All model fields (excluding PK composites, which are in the Key)
|
|
1812
1769
|
for (const [attr, val] of Object.entries(item)) {
|
|
1813
1770
|
if (pkComposites.has(attr))
|
|
@@ -1948,26 +1905,19 @@ const makeImpl = (config) => {
|
|
|
1948
1905
|
const orderByField = timeSeriesConfig.orderBy;
|
|
1949
1906
|
const ttlDuration = timeSeriesConfig.ttl;
|
|
1950
1907
|
const appendInputSchema = schemas.appendInputSchema;
|
|
1951
|
-
//
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
cause,
|
|
1956
|
-
})));
|
|
1957
|
-
const decoded = decodedInput;
|
|
1958
|
-
// Compose current-item primary key (pk + sk derived from PK/SK composites in decoded)
|
|
1908
|
+
// Encode user input via appendInputSchema (see `put` for strategy).
|
|
1909
|
+
const encodedInput = yield* encodeOrDecodeEncode(appendInputSchema, input, entityType, "append");
|
|
1910
|
+
const encoded = encodedInput;
|
|
1911
|
+
// Compose current-item primary key (pk + sk derived from PK/SK composites)
|
|
1959
1912
|
const primary = config.indexes.primary;
|
|
1960
|
-
const pkValue = KeyComposer.composePk(schema, entityType, primary,
|
|
1961
|
-
const currentSk = KeyComposer.composeSk(schema, entityType, entityVersion, primary,
|
|
1913
|
+
const pkValue = KeyComposer.composePk(schema, entityType, primary, encoded);
|
|
1914
|
+
const currentSk = KeyComposer.composeSk(schema, entityType, entityVersion, primary, encoded);
|
|
1962
1915
|
const marshalledKey = toAttributeMap({
|
|
1963
1916
|
[primary.pk.field]: pkValue,
|
|
1964
1917
|
[primary.sk.field]: currentSk,
|
|
1965
1918
|
});
|
|
1966
|
-
//
|
|
1967
|
-
|
|
1968
|
-
// before marshalling. This mirrors the put path.
|
|
1969
|
-
const serialisedInput = { ...decoded };
|
|
1970
|
-
serializeDateFields(serialisedInput);
|
|
1919
|
+
// `encoded` is already in wire-form via Schema.encode — used directly.
|
|
1920
|
+
const serialisedInput = { ...encoded };
|
|
1971
1921
|
// ---------- Build UpdateItem (scoped SET + CAS) ----------
|
|
1972
1922
|
const setClauses = [];
|
|
1973
1923
|
const names = {};
|
|
@@ -1996,7 +1946,7 @@ const makeImpl = (config) => {
|
|
|
1996
1946
|
// never change during an append. Non-PK composites in appendInput that
|
|
1997
1947
|
// overlap a GSI's composites trigger per-GSI resolution via indexPolicy.
|
|
1998
1948
|
const nonPkAppendFields = {};
|
|
1999
|
-
for (const [attr, val] of Object.entries(
|
|
1949
|
+
for (const [attr, val] of Object.entries(encoded)) {
|
|
2000
1950
|
if (!pkCompositeSet.has(attr)) {
|
|
2001
1951
|
nonPkAppendFields[attr] = val;
|
|
2002
1952
|
}
|
|
@@ -2031,7 +1981,7 @@ const makeImpl = (config) => {
|
|
|
2031
1981
|
},
|
|
2032
1982
|
};
|
|
2033
1983
|
}
|
|
2034
|
-
const gsiUpdate = KeyComposer.composeGsiKeysForUpdatePolicyAware(schema, entityType, entityVersion, policyAwareIndexes, nonPkAppendFields,
|
|
1984
|
+
const gsiUpdate = KeyComposer.composeGsiKeysForUpdatePolicyAware(schema, entityType, entityVersion, policyAwareIndexes, nonPkAppendFields, encoded);
|
|
2035
1985
|
for (const [field, value] of Object.entries(gsiUpdate.sets)) {
|
|
2036
1986
|
const nameKey = `#a${counter}`;
|
|
2037
1987
|
const valKey = `:a${counter}`;
|
|
@@ -2091,9 +2041,9 @@ const makeImpl = (config) => {
|
|
|
2091
2041
|
const updateExpression = exprParts.join(" ");
|
|
2092
2042
|
// ---------- Build Put of event item ----------
|
|
2093
2043
|
const eventSk = KeyComposer.composeEventSk(currentSk, newObValue, schema.casing);
|
|
2094
|
-
|
|
2095
|
-
//
|
|
2096
|
-
|
|
2044
|
+
// `encoded` is already in wire-form via Schema.encode; no extra
|
|
2045
|
+
// serialisation step is required for the Put.
|
|
2046
|
+
const eventItem = { ...encoded };
|
|
2097
2047
|
// Rename domain → DB names for the Put (matches put path)
|
|
2098
2048
|
renameToDynamo(eventItem);
|
|
2099
2049
|
// pk + sk (event)
|
|
@@ -2187,7 +2137,6 @@ const makeImpl = (config) => {
|
|
|
2187
2137
|
const prefix = KeyComposer.composeEventSkPrefix(currentSk, schema.casing);
|
|
2188
2138
|
const decodeHistory = (raw) => {
|
|
2189
2139
|
renameFromDynamo(raw);
|
|
2190
|
-
deserializeDateFields(raw);
|
|
2191
2140
|
return Schema.decodeUnknownEffect(schemas.historyRecordSchema)(raw).pipe(Effect.map(attachPrototype), Effect.mapError((cause) => new ValidationError({
|
|
2192
2141
|
entityType,
|
|
2193
2142
|
operation: "history.decode",
|
|
@@ -2658,9 +2607,8 @@ const makeImpl = (config) => {
|
|
|
2658
2607
|
identifier: resolvedIdentifier,
|
|
2659
2608
|
timeSeries: config.timeSeries,
|
|
2660
2609
|
_resolvedRefs: resolvedRefs,
|
|
2661
|
-
/** @internal Full decode pipeline: rename +
|
|
2610
|
+
/** @internal Full decode pipeline: rename + schema decode. Used by Batch/Aggregate. */
|
|
2662
2611
|
_decodeRecord: decodeRecord,
|
|
2663
|
-
_serializeDateFields: serializeDateFields,
|
|
2664
2612
|
_attachPrototype: attachPrototype,
|
|
2665
2613
|
_configure: (injectedSchema, injectedTableTag) => {
|
|
2666
2614
|
schema = injectedSchema;
|