effect-dynamodb 1.25.0 → 1.27.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 +7 -0
- package/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +150 -4
- package/dist/Aggregate.js.map +1 -1
- package/dist/Collection.d.ts +9 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +3 -0
- package/dist/Collection.js.map +1 -1
- package/dist/DynamoClient.d.ts +80 -28
- package/dist/DynamoClient.d.ts.map +1 -1
- package/dist/DynamoClient.js +83 -11
- package/dist/DynamoClient.js.map +1 -1
- package/dist/Entity.d.ts +80 -27
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +406 -76
- package/dist/Entity.js.map +1 -1
- package/dist/EventStore.d.ts +454 -55
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +681 -129
- package/dist/EventStore.js.map +1 -1
- package/dist/Query.d.ts +53 -0
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +88 -6
- package/dist/Query.js.map +1 -1
- package/dist/StorageEncoding.d.ts +160 -0
- package/dist/StorageEncoding.d.ts.map +1 -0
- package/dist/StorageEncoding.js +258 -0
- package/dist/StorageEncoding.js.map +1 -0
- package/dist/Table.d.ts.map +1 -1
- package/dist/Table.js +8 -6
- package/dist/Table.js.map +1 -1
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +5 -0
- package/dist/Transaction.js.map +1 -1
- package/dist/compression/zlib.d.ts +75 -0
- package/dist/compression/zlib.d.ts.map +1 -0
- package/dist/compression/zlib.js +109 -0
- package/dist/compression/zlib.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/BoundCrud.d.ts +39 -35
- package/dist/internal/BoundCrud.d.ts.map +1 -1
- package/dist/internal/BoundCrud.js.map +1 -1
- package/dist/internal/BoundQuery.d.ts +3 -3
- package/dist/internal/BoundQuery.d.ts.map +1 -1
- package/dist/internal/BoundQuery.js.map +1 -1
- package/dist/internal/BoundVectorQuery.d.ts +12 -0
- package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
- package/dist/internal/BoundVectorQuery.js +14 -1
- package/dist/internal/BoundVectorQuery.js.map +1 -1
- package/dist/internal/EntityCombinators.d.ts.map +1 -1
- package/dist/internal/EntityCombinators.js +16 -1
- package/dist/internal/EntityCombinators.js.map +1 -1
- package/dist/internal/Expr.d.ts +5 -3
- package/dist/internal/Expr.d.ts.map +1 -1
- package/dist/internal/Expr.js +1 -1
- package/dist/internal/Expr.js.map +1 -1
- package/dist/internal/Packed.d.ts +289 -0
- package/dist/internal/Packed.d.ts.map +1 -0
- package/dist/internal/Packed.js +654 -0
- package/dist/internal/Packed.js.map +1 -0
- package/dist/internal/PathBuilder.d.ts +45 -2
- package/dist/internal/PathBuilder.d.ts.map +1 -1
- package/dist/internal/PathBuilder.js +4 -0
- package/dist/internal/PathBuilder.js.map +1 -1
- package/dist/internal/TransactableOps.d.ts +1 -1
- package/dist/internal/TransactableOps.d.ts.map +1 -1
- package/dist/internal/TransactableOps.js +2 -0
- package/dist/internal/TransactableOps.js.map +1 -1
- package/package.json +6 -2
package/dist/Entity.js
CHANGED
|
@@ -12,6 +12,7 @@ import { Embedder } from "@effect-dynamodb/schema/Embedder.js";
|
|
|
12
12
|
import { CascadePartialFailure, ConcurrentModification, ConditionalCheckFailed, DeleteAppliedButUnreadable, DynamoError, EmbeddingError, ItemNotDeleted, ItemNotFound, isAwsConditionalCheckFailed, isAwsTransactionCancelled, OptimisticLockError, RefNotFound, StaleAppend, TransactionOverflow, UniqueConstraintViolation, UpdateAppliedButUnreadable, ValidationError, } from "@effect-dynamodb/schema/Errors.js";
|
|
13
13
|
import { makeCompositeKeyForm, toCompositeKeyRecord, } from "@effect-dynamodb/schema/internal/CompositeCodec.js";
|
|
14
14
|
import { makeDefaultCrypto } from "@effect-dynamodb/schema/internal/DefaultCrypto.js";
|
|
15
|
+
import { packedIndexRefusal } from "@effect-dynamodb/schema/internal/EntityPacked.js";
|
|
15
16
|
import * as KeyComposer from "@effect-dynamodb/schema/KeyComposer.js";
|
|
16
17
|
import { normalizeGsiConfig } from "@effect-dynamodb/schema/KeyComposer.js";
|
|
17
18
|
import { deriveSourceText } from "@effect-dynamodb/schema/VectorIndex.js";
|
|
@@ -22,6 +23,7 @@ import { BoundQueryImpl, entityNaming, } from "./internal/BoundQuery.js";
|
|
|
22
23
|
import { compileExpr, createConditionOps, emptyPartProblem, isExpr, nonEmptyCondition, parseShorthand, parseSimpleShorthand, toExpr, } from "./internal/Expr.js";
|
|
23
24
|
import { countOperators, expressionFits } from "./internal/ExpressionLimits.js";
|
|
24
25
|
import { BASE_DELAY_MS, MAX_RETRIES } from "./internal/Hydrate.js";
|
|
26
|
+
import { decodeImage, imageEntityType, ownRowsRawRow, packAttributeValueSync, packedExprRefusal, packedProjectionRefusal, packItemSync, unpackRow, withRegistry, } from "./internal/Packed.js";
|
|
25
27
|
import { compilePath, createPathBuilder } from "./internal/PathBuilder.js";
|
|
26
28
|
import { freshIncarnationToken, generateTimestampPrimitive, INCARNATION_TOKEN, UNSENTINELED_DEFAULTS, } from "./internal/TransactableOps.js";
|
|
27
29
|
import { batchEntries, diagnosedValidationError, diagnoseFailure, keyAttributesOf, targetsFor, transactItemTarget, } from "./internal/WriteDiagnosis.js";
|
|
@@ -614,17 +616,46 @@ const makeImpl = (config, precomputedData) => {
|
|
|
614
616
|
if (encoded.issue !== undefined) {
|
|
615
617
|
return `.set() of immutable "${field}": ${String(encoded.issue)}`;
|
|
616
618
|
}
|
|
617
|
-
out.push({
|
|
619
|
+
out.push({
|
|
620
|
+
field,
|
|
621
|
+
attr: resolveDbName(field),
|
|
622
|
+
value: toAttributeValue(encoded.encoded),
|
|
623
|
+
wire: encoded.encoded,
|
|
624
|
+
packed: field in packedFields,
|
|
625
|
+
});
|
|
618
626
|
}
|
|
619
627
|
return out;
|
|
620
628
|
};
|
|
629
|
+
const immutableChangedMessage = (field) => `.set() of "${field}": "${field}" is immutable and the update's value ` +
|
|
630
|
+
"differs from the stored one.";
|
|
631
|
+
/** A restated immutable value that differs from the stored item's (packed fields excepted). */
|
|
621
632
|
const immutableMismatch = (asserted, stored) => {
|
|
622
|
-
const changed = asserted.find(({ attr, value }) => !attributeValueEquals(stored[attr], value));
|
|
623
|
-
return changed === undefined
|
|
624
|
-
? undefined
|
|
625
|
-
: `.set() of "${changed.field}": "${changed.field}" is immutable and the update's value ` +
|
|
626
|
-
"differs from the stored one.";
|
|
633
|
+
const changed = asserted.find(({ attr, value, packed }) => !packed && !attributeValueEquals(stored[attr], value));
|
|
634
|
+
return changed === undefined ? undefined : immutableChangedMessage(changed.field);
|
|
627
635
|
};
|
|
636
|
+
/**
|
|
637
|
+
* A restated immutable PACKED value that differs from the stored item's,
|
|
638
|
+
* compared by the stored value's unpacked JSON (#147) — whatever codec or
|
|
639
|
+
* threshold it was written under.
|
|
640
|
+
*/
|
|
641
|
+
const packedImmutableMismatch = (asserted, stored) => Effect.gen(function* () {
|
|
642
|
+
for (const { field, attr, wire, packed } of asserted) {
|
|
643
|
+
if (!packed)
|
|
644
|
+
continue;
|
|
645
|
+
const storedValue = stored[attr];
|
|
646
|
+
if (storedValue === undefined) {
|
|
647
|
+
if (wire === undefined || wire === null)
|
|
648
|
+
continue;
|
|
649
|
+
return immutableChangedMessage(field);
|
|
650
|
+
}
|
|
651
|
+
const raw = fromAttributeMap({ [attr]: storedValue });
|
|
652
|
+
yield* unpackFields(raw, "update.decode");
|
|
653
|
+
if (!attributeValueEquals(toAttributeValue(raw[attr]), toAttributeValue(wire))) {
|
|
654
|
+
return immutableChangedMessage(field);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return undefined;
|
|
658
|
+
});
|
|
628
659
|
/**
|
|
629
660
|
* Whether an update can safely create the item it names (#133): its payload
|
|
630
661
|
* supplies every required model field and every primary-key composite, so
|
|
@@ -680,6 +711,136 @@ const makeImpl = (config, precomputedData) => {
|
|
|
680
711
|
return;
|
|
681
712
|
decodeSparseFields(raw, sparseFields);
|
|
682
713
|
};
|
|
714
|
+
// ---------------------------------------------------------------------------
|
|
715
|
+
// Packed fields (`storedAs: DynamoModel.Packed`) — see
|
|
716
|
+
// `docs/designs/storage-encoding.md` §6. Packing runs after renames and
|
|
717
|
+
// sparse flattening, before `toAttributeMap`; unpacking after
|
|
718
|
+
// `fromAttributeMap`, before sparse rebuild and renames — the same points as
|
|
719
|
+
// the sparse layout, keyed by stored attribute names.
|
|
720
|
+
// ---------------------------------------------------------------------------
|
|
721
|
+
const { packedFields, packedAttributes, dispatchAttributes, hasPackedFields } = data;
|
|
722
|
+
/**
|
|
723
|
+
* Pack, in place, the packed attributes of a stored-name item. `only`
|
|
724
|
+
* restricts it to those stored attributes (the read-merge update path packs
|
|
725
|
+
* only the fields it names: the others still hold their stored value).
|
|
726
|
+
*/
|
|
727
|
+
const packFields = (item, operation, only) => {
|
|
728
|
+
if (!hasPackedFields)
|
|
729
|
+
return Effect.void;
|
|
730
|
+
const targets = only === undefined
|
|
731
|
+
? packedAttributes
|
|
732
|
+
: Object.fromEntries(Object.entries(packedAttributes).filter(([attr]) => only.has(attr)));
|
|
733
|
+
if (Object.keys(targets).length === 0)
|
|
734
|
+
return Effect.void;
|
|
735
|
+
return withRegistry((registry) => packItemSync(item, targets, registry, { entityType, operation: `${operation}.packed` }));
|
|
736
|
+
};
|
|
737
|
+
/** Pack one attribute's wire value straight to its `AttributeValue` (an `UpdateItem` SET). */
|
|
738
|
+
const packAttribute = (attribute, wire, operation) => withRegistry((registry) => packAttributeValueSync(packedAttributes[attribute], wire, registry, { entityType, operation: `${operation}.packed` }, attribute));
|
|
739
|
+
/**
|
|
740
|
+
* Read, in place, the dispatch-eligible attributes of a stored-name row by
|
|
741
|
+
* their stored form. A row with no string or binary value on any of them
|
|
742
|
+
* (every native row) costs one `typeof` per attribute.
|
|
743
|
+
*/
|
|
744
|
+
const unpackFields = (raw, operation = "decode") => dispatchAttributes.length === 0
|
|
745
|
+
? Effect.void
|
|
746
|
+
: unpackRow(raw, dispatchAttributes, { entityType, operation });
|
|
747
|
+
/**
|
|
748
|
+
* A domain-named copy of `record` with its packed (or once-packed) attributes
|
|
749
|
+
* read by their stored form. The update paths derive vector source text from
|
|
750
|
+
* it: an untouched packed field still holds its stored value there (a JSON
|
|
751
|
+
* string or compressed bytes), not the value the source `compose` expects.
|
|
752
|
+
* `record` itself is left as is, so the write still carries the stored bytes.
|
|
753
|
+
*/
|
|
754
|
+
const unpackedDomainCopy = (record, operation) => {
|
|
755
|
+
if (dispatchAttributes.length === 0)
|
|
756
|
+
return Effect.succeed(record);
|
|
757
|
+
const stored = { ...record };
|
|
758
|
+
renameToDynamo(stored);
|
|
759
|
+
return Effect.map(unpackFields(stored, operation), () => {
|
|
760
|
+
renameFromDynamo(stored);
|
|
761
|
+
return stored;
|
|
762
|
+
});
|
|
763
|
+
};
|
|
764
|
+
/** The packed attributes an expression or projection may not look inside (EDD-9076). */
|
|
765
|
+
const packedGuards = Object.entries(packedFields).flatMap(([field, storedAs]) => {
|
|
766
|
+
const guard = (attribute) => ({
|
|
767
|
+
attribute,
|
|
768
|
+
owner: `Entity "${entityType}"`,
|
|
769
|
+
storedAs,
|
|
770
|
+
alternative: () => `To filter on it, use .filterBy((item) => ...), which runs on the decoded item; to ` +
|
|
771
|
+
`condition a write on it, read the item and compare in code, then write the whole ` +
|
|
772
|
+
`value with .set({ ${field} }).`,
|
|
773
|
+
});
|
|
774
|
+
const stored = resolveDbName(field);
|
|
775
|
+
return stored === field ? [guard(field)] : [guard(field), guard(stored)];
|
|
776
|
+
});
|
|
777
|
+
/** Throw `[EDD-9076]` when an expression looks inside a packed field. */
|
|
778
|
+
const refusePackedExpr = (expr, usage) => {
|
|
779
|
+
const refused = packedExprRefusal(expr, packedGuards, usage);
|
|
780
|
+
if (refused !== undefined)
|
|
781
|
+
throw new Error(refused);
|
|
782
|
+
};
|
|
783
|
+
/**
|
|
784
|
+
* The `Query.make` options of this entity's queries: the packed guards, and
|
|
785
|
+
* the raw-row hook that reads packed attributes of a projected row of THIS
|
|
786
|
+
* entity type by their stored form (parsed JSON, not schema-decoded) — rows
|
|
787
|
+
* of other types (`ignoreOwnership()`) are left untouched.
|
|
788
|
+
*/
|
|
789
|
+
const packedQueryOptions = {
|
|
790
|
+
packed: packedGuards,
|
|
791
|
+
rawRow: ownRowsRawRow(entityType, dispatchAttributes, (raw) => unpackFields(raw)),
|
|
792
|
+
};
|
|
793
|
+
/**
|
|
794
|
+
* `[EDD-9077]`: an update applies anything but a whole-value `.set()` /
|
|
795
|
+
* `.remove()` to a packed field — DynamoDB cannot compute inside one opaque
|
|
796
|
+
* value. `undefined` when the update is fine.
|
|
797
|
+
*/
|
|
798
|
+
const refusedPackedUpdate = (uState) => {
|
|
799
|
+
if (!hasPackedFields)
|
|
800
|
+
return undefined;
|
|
801
|
+
const refuse = (op, field) => `[EDD-9077] Entity "${entityType}": ${op} on "${field}", but "${field}" is stored packed ` +
|
|
802
|
+
`(format "${packedFields[field].format}"): DynamoDB holds it as one opaque string or ` +
|
|
803
|
+
`binary value and cannot apply ${op} inside it. Write the whole value with ` +
|
|
804
|
+
`.set({ ${field} }) (or .remove(["${field}"])).`;
|
|
805
|
+
const records = [
|
|
806
|
+
[".add()", uState.add],
|
|
807
|
+
[".subtract()", uState.subtract],
|
|
808
|
+
[".append()", uState.append],
|
|
809
|
+
[".deleteFromSet()", uState.deleteFromSet],
|
|
810
|
+
];
|
|
811
|
+
for (const [op, values] of records) {
|
|
812
|
+
for (const field of Object.keys(values ?? {})) {
|
|
813
|
+
if (field in packedFields)
|
|
814
|
+
return refuse(op, field);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const pathOps = [
|
|
818
|
+
["pathSet", uState.pathSets ?? []],
|
|
819
|
+
["pathRemove", (uState.pathRemoves ?? []).map((segments) => ({ segments }))],
|
|
820
|
+
["pathAdd", uState.pathAdds ?? []],
|
|
821
|
+
["pathSubtract", uState.pathSubtracts ?? []],
|
|
822
|
+
["pathAppend", uState.pathAppends ?? []],
|
|
823
|
+
["pathPrepend", uState.pathPrepends ?? []],
|
|
824
|
+
["pathIfNotExists", uState.pathIfNotExists ?? []],
|
|
825
|
+
["pathDelete", uState.pathDeletes ?? []],
|
|
826
|
+
];
|
|
827
|
+
for (const [op, ops] of pathOps) {
|
|
828
|
+
for (const { segments } of ops) {
|
|
829
|
+
const head = segments[0];
|
|
830
|
+
if (typeof head === "string" && head in packedFields)
|
|
831
|
+
return refuse(op, head);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
for (const { field } of uState.sparseRemoveEntries ?? []) {
|
|
835
|
+
if (field in packedFields)
|
|
836
|
+
return refuse(".removeEntries()", field);
|
|
837
|
+
}
|
|
838
|
+
for (const field of uState.sparseClearFields ?? []) {
|
|
839
|
+
if (field in packedFields)
|
|
840
|
+
return refuse(".clearMap()", field);
|
|
841
|
+
}
|
|
842
|
+
return undefined;
|
|
843
|
+
};
|
|
683
844
|
/**
|
|
684
845
|
* A DOMAIN-keyed VIEW of a row that came off the wire.
|
|
685
846
|
*
|
|
@@ -973,43 +1134,68 @@ const makeImpl = (config, precomputedData) => {
|
|
|
973
1134
|
const corruption = versionCorruption(raw, "decode");
|
|
974
1135
|
if (corruption !== undefined)
|
|
975
1136
|
return Effect.fail(corruption);
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1137
|
+
return Effect.flatMap(unpackFields(raw), () => {
|
|
1138
|
+
deserializeSparseFields(raw);
|
|
1139
|
+
renameFromDynamo(raw);
|
|
1140
|
+
return Schema.decodeUnknownEffect(schemas.recordSchema)(raw).pipe(Effect.map(attachPrototype), Effect.mapError((cause) => new ValidationError({
|
|
1141
|
+
entityType,
|
|
1142
|
+
operation: "decode",
|
|
1143
|
+
cause,
|
|
1144
|
+
})));
|
|
1145
|
+
});
|
|
983
1146
|
};
|
|
984
1147
|
/** Decode a raw item using the schema selected by mode */
|
|
985
1148
|
const decodeAs = (raw, marshalled, mode) => {
|
|
986
1149
|
const corruption = versionCorruption(raw, "decode");
|
|
987
1150
|
if (corruption !== undefined)
|
|
988
1151
|
return Effect.fail(corruption);
|
|
1152
|
+
// Native: the item as stored — packed values stay their `S` / `B`.
|
|
989
1153
|
if (mode === "native")
|
|
990
1154
|
return Effect.succeed(marshalled);
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1155
|
+
return Effect.flatMap(unpackFields(raw), () => {
|
|
1156
|
+
// Collect flattened sparse-map attributes back into the domain Record.
|
|
1157
|
+
// Must happen before rename so the rebuilt Record sits at the domain field
|
|
1158
|
+
// name (sparse field renaming is not supported).
|
|
1159
|
+
deserializeSparseFields(raw);
|
|
1160
|
+
// Rename DynamoDB attributes back to domain field names. Substituted
|
|
1161
|
+
// schemas handle wire→domain conversion (date primitives, Redacted, etc.).
|
|
1162
|
+
renameFromDynamo(raw);
|
|
1163
|
+
const targetSchema = mode === "model"
|
|
1164
|
+
? schemas.modelSchema
|
|
1165
|
+
: mode === "record"
|
|
1166
|
+
? schemas.recordSchema
|
|
1167
|
+
: schemas.itemSchema;
|
|
1168
|
+
return Schema.decodeUnknownEffect(targetSchema)(raw).pipe(Effect.map((decoded) => mode === "model" && isSchemaClass && !hasHiddenFields
|
|
1169
|
+
? new rawModel(decoded)
|
|
1170
|
+
: mode !== "item"
|
|
1171
|
+
? attachPrototype(decoded)
|
|
1172
|
+
: decoded), Effect.mapError((cause) => new ValidationError({
|
|
1173
|
+
entityType,
|
|
1174
|
+
operation: "decode",
|
|
1175
|
+
cause,
|
|
1176
|
+
})));
|
|
1177
|
+
});
|
|
1178
|
+
};
|
|
1179
|
+
/**
|
|
1180
|
+
* Decode a DynamoDB Streams image into an item record (`Entity.Item`): the
|
|
1181
|
+
* image normalised (DynamoDB JSON or unmarshalled; `B` as bytes or base64),
|
|
1182
|
+
* its dispatch-eligible attributes read by stored form, then sparse rebuild,
|
|
1183
|
+
* renames and the item-schema decode — exactly what a table read applies.
|
|
1184
|
+
*/
|
|
1185
|
+
const decodeImageItem = (image) => Effect.gen(function* () {
|
|
1186
|
+
// An image of another entity type is decoded as 1.25.0 decoded it: its
|
|
1187
|
+
// attributes are never read as packed values (a plain string of that
|
|
1188
|
+
// type is not this entity's JSON). The type is read off the raw image.
|
|
1189
|
+
const owner = imageEntityType(image);
|
|
1190
|
+
const raw = yield* decodeImage(image, owner === undefined || owner === entityType ? dispatchAttributes : [], { entityType, operation: "decodeMarshalledItem" }, { probe: config.indexes.primary.pk.field });
|
|
1191
|
+
const corruption = versionCorruption(raw, "decodeMarshalledItem");
|
|
1192
|
+
if (corruption !== undefined)
|
|
1193
|
+
return yield* corruption;
|
|
994
1194
|
deserializeSparseFields(raw);
|
|
995
|
-
// Rename DynamoDB attributes back to domain field names. Substituted
|
|
996
|
-
// schemas handle wire→domain conversion (date primitives, Redacted, etc.).
|
|
997
1195
|
renameFromDynamo(raw);
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
? schemas.recordSchema
|
|
1002
|
-
: schemas.itemSchema;
|
|
1003
|
-
return Schema.decodeUnknownEffect(targetSchema)(raw).pipe(Effect.map((decoded) => mode === "model" && isSchemaClass && !hasHiddenFields
|
|
1004
|
-
? new rawModel(decoded)
|
|
1005
|
-
: mode !== "item"
|
|
1006
|
-
? attachPrototype(decoded)
|
|
1007
|
-
: decoded), Effect.mapError((cause) => new ValidationError({
|
|
1008
|
-
entityType,
|
|
1009
|
-
operation: "decode",
|
|
1010
|
-
cause,
|
|
1011
|
-
})));
|
|
1012
|
-
};
|
|
1196
|
+
const decoded = yield* Schema.decodeUnknownEffect(schemas.itemSchema)(raw).pipe(Effect.mapError((cause) => new ValidationError({ entityType, operation: "decodeMarshalledItem", cause })));
|
|
1197
|
+
return attachPrototype(decoded);
|
|
1198
|
+
});
|
|
1013
1199
|
const nowIso = (now) => DateTime.formatIso(now);
|
|
1014
1200
|
/**
|
|
1015
1201
|
* Generate a timestamp value as a wire primitive ready for marshalling.
|
|
@@ -1314,6 +1500,9 @@ const makeImpl = (config, precomputedData) => {
|
|
|
1314
1500
|
const current = yield* client.getItem({ TableName: tableName, Key: marshalledKey });
|
|
1315
1501
|
if (current.Item) {
|
|
1316
1502
|
const currentDomain = fromAttributeMap(current.Item);
|
|
1503
|
+
// Packed fields are read by their stored form: the source text is
|
|
1504
|
+
// derived from the value, not its JSON string or compressed bytes.
|
|
1505
|
+
yield* unpackFields(currentDomain, "update.vector");
|
|
1317
1506
|
renameFromDynamo(currentDomain);
|
|
1318
1507
|
merged = { ...currentDomain, ...merged };
|
|
1319
1508
|
}
|
|
@@ -1680,16 +1869,18 @@ const makeImpl = (config, precomputedData) => {
|
|
|
1680
1869
|
if (mode === "native")
|
|
1681
1870
|
return Effect.succeed(projected);
|
|
1682
1871
|
const raw = fromAttributeMap(projected);
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1872
|
+
return Effect.flatMap(unpackFields(raw), () => {
|
|
1873
|
+
deserializeSparseFields(raw);
|
|
1874
|
+
renameFromDynamo(raw);
|
|
1875
|
+
const target = mode === "model"
|
|
1876
|
+
? schemas.modelSchema
|
|
1877
|
+
: mode === "record"
|
|
1878
|
+
? schemas.recordSchema
|
|
1879
|
+
: schemas.itemSchema;
|
|
1880
|
+
const fields = target.fields;
|
|
1881
|
+
const present = Object.fromEntries(Object.entries(fields).filter(([name]) => name in raw));
|
|
1882
|
+
return Schema.decodeUnknownEffect(Schema.Struct(present))(raw).pipe(Effect.mapError((cause) => new ValidationError({ entityType, operation: "decode", cause })));
|
|
1883
|
+
});
|
|
1693
1884
|
};
|
|
1694
1885
|
/**
|
|
1695
1886
|
* The value an update returns for its `returnValues` mode, from the item
|
|
@@ -2737,6 +2928,11 @@ const makeImpl = (config, precomputedData) => {
|
|
|
2737
2928
|
cause: e instanceof Error ? e.message : String(e),
|
|
2738
2929
|
});
|
|
2739
2930
|
}
|
|
2931
|
+
// Pack `storedAs: DynamoModel.Packed` fields to their stored form
|
|
2932
|
+
// (JSON string, or compressed binary over the threshold) — after
|
|
2933
|
+
// renames and sparse flattening, before marshalling, so every size
|
|
2934
|
+
// measured from here on is the stored size.
|
|
2935
|
+
yield* packFields(item, operation);
|
|
2740
2936
|
// The caller's condition, ANDed onto `create`'s not-exists guard —
|
|
2741
2937
|
// a guard no `.condition()` can replace (#133). `upsert`'s create
|
|
2742
2938
|
// attempt brings its own (`runGuardedPut` requires the item missing).
|
|
@@ -2823,6 +3019,14 @@ const makeImpl = (config, precomputedData) => {
|
|
|
2823
3019
|
let projectionExpression;
|
|
2824
3020
|
let projectionNames;
|
|
2825
3021
|
if (opts.projection && opts.projection.length > 0) {
|
|
3022
|
+
const packedProjection = packedProjectionRefusal(opts.projection.map((attribute) => [attribute]), packedGuards);
|
|
3023
|
+
if (packedProjection !== undefined) {
|
|
3024
|
+
return yield* new ValidationError({
|
|
3025
|
+
entityType,
|
|
3026
|
+
operation: "get.projection",
|
|
3027
|
+
cause: packedProjection,
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
2826
3030
|
const proj = Projection.projection(opts.projection);
|
|
2827
3031
|
projectionExpression = proj.expression;
|
|
2828
3032
|
projectionNames = proj.names;
|
|
@@ -2839,9 +3043,12 @@ const makeImpl = (config, precomputedData) => {
|
|
|
2839
3043
|
if (!result.Item) {
|
|
2840
3044
|
return yield* new ItemNotFound({ entityType, key: encodedKey });
|
|
2841
3045
|
}
|
|
2842
|
-
// Raw mode: return unmarshalled record without schema decode
|
|
3046
|
+
// Raw mode: return unmarshalled record without schema decode — a
|
|
3047
|
+
// packed attribute as its parsed JSON (#147).
|
|
2843
3048
|
if (mode === "raw") {
|
|
2844
|
-
|
|
3049
|
+
const row = fromAttributeMap(result.Item);
|
|
3050
|
+
yield* unpackFields(row);
|
|
3051
|
+
return row;
|
|
2845
3052
|
}
|
|
2846
3053
|
// Unmarshall and decode using selected mode
|
|
2847
3054
|
const raw = fromAttributeMap(result.Item);
|
|
@@ -2856,6 +3063,15 @@ const makeImpl = (config, precomputedData) => {
|
|
|
2856
3063
|
* Used instead of reading it again: the write is guarded on it either way.
|
|
2857
3064
|
*/
|
|
2858
3065
|
preRead) => Effect.gen(function* () {
|
|
3066
|
+
// A packed field takes only a whole-value .set() / .remove() (#147).
|
|
3067
|
+
const packedRefusal = refusedPackedUpdate(requested);
|
|
3068
|
+
if (packedRefusal !== undefined) {
|
|
3069
|
+
return yield* new ValidationError({
|
|
3070
|
+
entityType,
|
|
3071
|
+
operation: "update",
|
|
3072
|
+
cause: packedRefusal,
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
2859
3075
|
// Path operations on a field that feeds a key or a unique sentinel
|
|
2860
3076
|
// become the record operations they are equivalent to, or are
|
|
2861
3077
|
// refused (#133) — before anything reads the state.
|
|
@@ -3248,7 +3464,10 @@ const makeImpl = (config, precomputedData) => {
|
|
|
3248
3464
|
const embedFor = new Set(vectorIndexesNeedingUpdate(collectTouchedFields(hydratedUpdates, uState), uState.withVectors).map(([logicalName]) => logicalName));
|
|
3249
3465
|
// `newItem` is the fully merged post-update item (removals already
|
|
3250
3466
|
// applied above), so a put-style recompute is exact.
|
|
3251
|
-
|
|
3467
|
+
// Untouched packed fields still hold their stored form on `newItem`
|
|
3468
|
+
// (kept byte-identical for the write diff), so the source text is
|
|
3469
|
+
// derived from an unpacked copy.
|
|
3470
|
+
const vectorWrite = yield* computeVectorAttributes(yield* unpackedDomainCopy(newItem, "update.vector"), uState.withVectors, embedFor);
|
|
3252
3471
|
Object.assign(newItem, vectorWrite.sets);
|
|
3253
3472
|
for (const field of vectorWrite.removes)
|
|
3254
3473
|
delete newItem[field];
|
|
@@ -3266,9 +3485,17 @@ const makeImpl = (config, precomputedData) => {
|
|
|
3266
3485
|
cause: e instanceof Error ? e.message : String(e),
|
|
3267
3486
|
});
|
|
3268
3487
|
}
|
|
3488
|
+
// Pack only the packed fields this update names: the others still
|
|
3489
|
+
// hold the stored value read above (a string or bytes), carried
|
|
3490
|
+
// byte-identical so the "write only what differs" diff below leaves
|
|
3491
|
+
// them alone (#147).
|
|
3492
|
+
yield* packFields(newItem, "update", new Set(Object.entries(hydratedUpdates)
|
|
3493
|
+
.filter(([field, value]) => field in packedFields && value != null)
|
|
3494
|
+
.map(([field]) => resolveDbName(field))));
|
|
3269
3495
|
const currentItem = currentResult.Item;
|
|
3270
3496
|
yield* checkVersion(currentItem, "update");
|
|
3271
|
-
const immutableChange = immutableMismatch(immutables, currentItem)
|
|
3497
|
+
const immutableChange = immutableMismatch(immutables, currentItem) ??
|
|
3498
|
+
(yield* packedImmutableMismatch(immutables, currentItem));
|
|
3272
3499
|
if (immutableChange !== undefined) {
|
|
3273
3500
|
return yield* new ValidationError({
|
|
3274
3501
|
entityType,
|
|
@@ -3609,7 +3836,8 @@ const makeImpl = (config, precomputedData) => {
|
|
|
3609
3836
|
return yield* missingItem();
|
|
3610
3837
|
}
|
|
3611
3838
|
yield* checkVersion(current.Item, "update");
|
|
3612
|
-
const immutableChange = immutableMismatch(immutables, current.Item)
|
|
3839
|
+
const immutableChange = immutableMismatch(immutables, current.Item) ??
|
|
3840
|
+
(yield* packedImmutableMismatch(immutables, current.Item));
|
|
3613
3841
|
if (immutableChange !== undefined) {
|
|
3614
3842
|
return yield* new ValidationError({
|
|
3615
3843
|
entityType,
|
|
@@ -3662,6 +3890,28 @@ const makeImpl = (config, precomputedData) => {
|
|
|
3662
3890
|
[uState.remove, uState.sparseRemoveEntries, uState.sparseClearFields].every((ops) => ops === undefined || ops.length === 0) &&
|
|
3663
3891
|
[uState.add, uState.subtract, uState.append, uState.deleteFromSet].every((ops) => ops === undefined || Object.keys(ops).length === 0) &&
|
|
3664
3892
|
!hasPathOps(uState);
|
|
3893
|
+
// A restated packed immutable cannot be asserted by a condition on its
|
|
3894
|
+
// stored bytes (they depend on the codec and threshold it was written
|
|
3895
|
+
// under): an unread write reads it and compares the unpacked JSON
|
|
3896
|
+
// (#147). The write still requires the item (`attribute_exists`), and
|
|
3897
|
+
// no library write changes an immutable value in between.
|
|
3898
|
+
if (plainWrite && immutables.some((asserted) => asserted.packed)) {
|
|
3899
|
+
const stored = yield* client.getItem({
|
|
3900
|
+
TableName: tableName,
|
|
3901
|
+
Key: marshalledKey,
|
|
3902
|
+
ConsistentRead: true,
|
|
3903
|
+
});
|
|
3904
|
+
if (stored.Item !== undefined) {
|
|
3905
|
+
const packedChange = yield* packedImmutableMismatch(immutables, stored.Item);
|
|
3906
|
+
if (packedChange !== undefined) {
|
|
3907
|
+
return yield* new ValidationError({
|
|
3908
|
+
entityType,
|
|
3909
|
+
operation: "update",
|
|
3910
|
+
cause: packedChange,
|
|
3911
|
+
});
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3665
3915
|
// Build UpdateExpression
|
|
3666
3916
|
const setClauses = [];
|
|
3667
3917
|
const names = {};
|
|
@@ -3723,8 +3973,13 @@ const makeImpl = (config, precomputedData) => {
|
|
|
3723
3973
|
}
|
|
3724
3974
|
const nameKey = `#u${counter}`;
|
|
3725
3975
|
const valKey = `:u${counter}`;
|
|
3726
|
-
|
|
3727
|
-
|
|
3976
|
+
const storedName = resolveDbName(attr);
|
|
3977
|
+
names[nameKey] = storedName;
|
|
3978
|
+
// A packed field is written whole, as its stored form (#147).
|
|
3979
|
+
values[valKey] =
|
|
3980
|
+
attr in packedFields
|
|
3981
|
+
? (yield* packAttribute(storedName, val, "update"))
|
|
3982
|
+
: toAttributeValue(val);
|
|
3728
3983
|
setClauses.push(`${nameKey} = ${valKey}`);
|
|
3729
3984
|
counter++;
|
|
3730
3985
|
}
|
|
@@ -4123,7 +4378,9 @@ const makeImpl = (config, precomputedData) => {
|
|
|
4123
4378
|
}
|
|
4124
4379
|
if (plainWrite) {
|
|
4125
4380
|
// Restated immutable values must match the item's.
|
|
4126
|
-
for (const [i, { attr, value }] of immutables.entries()) {
|
|
4381
|
+
for (const [i, { attr, value, packed }] of immutables.entries()) {
|
|
4382
|
+
if (packed)
|
|
4383
|
+
continue; // read and compared above (#147)
|
|
4127
4384
|
names[`#imm${i}`] = attr;
|
|
4128
4385
|
values[`:imm${i}`] = value;
|
|
4129
4386
|
condParts.push(`#imm${i} = :imm${i}`);
|
|
@@ -4870,8 +5127,13 @@ const makeImpl = (config, precomputedData) => {
|
|
|
4870
5127
|
}
|
|
4871
5128
|
const nameKey = `#u${counter}`;
|
|
4872
5129
|
const valKey = `:u${counter}`;
|
|
4873
|
-
|
|
4874
|
-
|
|
5130
|
+
const storedName = resolveDbName(attr);
|
|
5131
|
+
names[nameKey] = storedName;
|
|
5132
|
+
// A packed field is written whole, as its stored form (#147).
|
|
5133
|
+
values[valKey] =
|
|
5134
|
+
attr in packedFields
|
|
5135
|
+
? (yield* packAttribute(storedName, val, "upsert"))
|
|
5136
|
+
: toAttributeValue(val);
|
|
4875
5137
|
// Immutable fields use if_not_exists — only set on first create
|
|
4876
5138
|
if (immutableFields.has(attr)) {
|
|
4877
5139
|
setClauses.push(`${nameKey} = if_not_exists(${nameKey}, ${valKey})`);
|
|
@@ -5102,6 +5364,16 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5102
5364
|
// `append` takes its condition as an argument — no `condition`
|
|
5103
5365
|
// combinator — so an empty one becomes none here (#133).
|
|
5104
5366
|
const userCondition = nonEmptyCondition(givenCondition);
|
|
5367
|
+
if (userCondition !== undefined) {
|
|
5368
|
+
const packedRefusal = packedExprRefusal(toExpr(userCondition), packedGuards, "condition");
|
|
5369
|
+
if (packedRefusal !== undefined) {
|
|
5370
|
+
return yield* new ValidationError({
|
|
5371
|
+
entityType,
|
|
5372
|
+
operation: "append.condition",
|
|
5373
|
+
cause: packedRefusal,
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5105
5377
|
const emptyPart = userCondition === undefined ? undefined : emptyPartProblem(toExpr(userCondition));
|
|
5106
5378
|
if (emptyPart !== undefined) {
|
|
5107
5379
|
return yield* new ValidationError({
|
|
@@ -5199,6 +5471,15 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5199
5471
|
});
|
|
5200
5472
|
// `encoded` is already in wire-form via Schema.encode — used directly.
|
|
5201
5473
|
const serialisedInput = { ...encoded };
|
|
5474
|
+
// Packed `appendInput` fields (#147), packed ONCE: the same stored value
|
|
5475
|
+
// is SET on the current item and written on the event item.
|
|
5476
|
+
const packedStored = {};
|
|
5477
|
+
for (const field of Object.keys(packedFields)) {
|
|
5478
|
+
if (serialisedInput[field] !== undefined) {
|
|
5479
|
+
packedStored[resolveDbName(field)] = serialisedInput[field];
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
yield* packFields(packedStored, "append");
|
|
5202
5483
|
// ---------- Build UpdateItem (scoped SET + CAS) ----------
|
|
5203
5484
|
const setClauses = [];
|
|
5204
5485
|
const names = {};
|
|
@@ -5215,8 +5496,9 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5215
5496
|
continue;
|
|
5216
5497
|
const nameKey = `#a${counter}`;
|
|
5217
5498
|
const valKey = `:a${counter}`;
|
|
5218
|
-
|
|
5219
|
-
|
|
5499
|
+
const storedName = resolveDbName(attr);
|
|
5500
|
+
names[nameKey] = storedName;
|
|
5501
|
+
values[valKey] = toAttributeValue(attr in packedFields ? packedStored[storedName] : val);
|
|
5220
5502
|
setClauses.push(`${nameKey} = ${valKey}`);
|
|
5221
5503
|
counter++;
|
|
5222
5504
|
}
|
|
@@ -5315,6 +5597,8 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5315
5597
|
const eventItem = { ...encoded };
|
|
5316
5598
|
// Rename domain → DB names for the Put (matches put path)
|
|
5317
5599
|
renameToDynamo(eventItem);
|
|
5600
|
+
// The packed fields' stored values — the same ones SET above (#147).
|
|
5601
|
+
Object.assign(eventItem, packedStored);
|
|
5318
5602
|
// pk + sk (event)
|
|
5319
5603
|
eventItem[primary.pk.field] = pkValue;
|
|
5320
5604
|
eventItem[primary.sk.field] = eventSk;
|
|
@@ -5484,13 +5768,15 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5484
5768
|
// NOT carry `<prefix>#*` attributes. `deserializeSparseFields` populates
|
|
5485
5769
|
// empty `{}` Records under the domain field names so the historyRecord
|
|
5486
5770
|
// schema (which still includes the sparse Record fields) decodes.
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5771
|
+
return Effect.flatMap(unpackFields(raw, "history.decode"), () => {
|
|
5772
|
+
deserializeSparseFields(raw);
|
|
5773
|
+
renameFromDynamo(raw);
|
|
5774
|
+
return Schema.decodeUnknownEffect(schemas.historyRecordSchema)(raw).pipe(Effect.map(attachPrototype), Effect.mapError((cause) => new ValidationError({
|
|
5775
|
+
entityType,
|
|
5776
|
+
operation: "history.decode",
|
|
5777
|
+
cause,
|
|
5778
|
+
})));
|
|
5779
|
+
});
|
|
5494
5780
|
};
|
|
5495
5781
|
return Query.make({
|
|
5496
5782
|
tableName: "",
|
|
@@ -5500,6 +5786,7 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5500
5786
|
skField: primary.sk.field,
|
|
5501
5787
|
entityTypes: [entityType],
|
|
5502
5788
|
decoder: (raw) => decodeHistory(raw),
|
|
5789
|
+
...packedQueryOptions,
|
|
5503
5790
|
resolveTableName: tableTag.useSync((tc) => tc.name),
|
|
5504
5791
|
keyFields: [primary.pk.field, primary.sk.field],
|
|
5505
5792
|
}).pipe(Query.where({ beginsWith: prefix }));
|
|
@@ -5526,6 +5813,8 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5526
5813
|
skField: indexDef.sk.field,
|
|
5527
5814
|
entityTypes: [entityType],
|
|
5528
5815
|
decoder: (raw) => decodeRecord(raw),
|
|
5816
|
+
...packedQueryOptions,
|
|
5817
|
+
...packedQueryOptions,
|
|
5529
5818
|
resolveTableName: tableTag.useSync((tc) => tc.name),
|
|
5530
5819
|
keyFields: [
|
|
5531
5820
|
indexDef.pk.field,
|
|
@@ -5555,6 +5844,7 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5555
5844
|
indexName: undefined,
|
|
5556
5845
|
entityTypes: [entityType],
|
|
5557
5846
|
decoder: (raw) => decodeRecord(raw),
|
|
5847
|
+
...packedQueryOptions,
|
|
5558
5848
|
resolveTableName: tableTag.useSync((tc) => tc.name),
|
|
5559
5849
|
keyFields: [config.indexes.primary.pk.field, config.indexes.primary.sk.field],
|
|
5560
5850
|
liveRows: liveRows(),
|
|
@@ -5658,6 +5948,7 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5658
5948
|
skField: primary.sk.field,
|
|
5659
5949
|
entityTypes: [entityType],
|
|
5660
5950
|
decoder: (raw) => decodeRecord(raw),
|
|
5951
|
+
...packedQueryOptions,
|
|
5661
5952
|
resolveTableName: tableTag.useSync((tc) => tc.name),
|
|
5662
5953
|
keyFields: [primary.pk.field, primary.sk.field],
|
|
5663
5954
|
prepare: withLegacy,
|
|
@@ -5684,6 +5975,9 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5684
5975
|
// (itemSchema would fail because it expects GSI key fields that aren't present)
|
|
5685
5976
|
if (mode === "native")
|
|
5686
5977
|
return tombstone;
|
|
5978
|
+
// Packed fields: read by their stored form (copied as-is onto the
|
|
5979
|
+
// tombstone, #147).
|
|
5980
|
+
yield* unpackFields(raw, "deleted.get.decode");
|
|
5687
5981
|
// Sparse Map fields: rebuild domain Records from flattened attrs so
|
|
5688
5982
|
// soft-deleted items decode correctly. Sparse data is preserved
|
|
5689
5983
|
// verbatim across soft-delete (GSI keys are stripped, sparse data is
|
|
@@ -5712,16 +6006,18 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5712
6006
|
const corruption = versionCorruption(raw, "deleted.list.decode");
|
|
5713
6007
|
if (corruption !== undefined)
|
|
5714
6008
|
return Effect.fail(corruption);
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
6009
|
+
return Effect.flatMap(unpackFields(raw, "deleted.list.decode"), () => {
|
|
6010
|
+
// Sparse fields are domain data and are preserved across soft-delete.
|
|
6011
|
+
// Rebuild Records from flattened attrs before schema decode.
|
|
6012
|
+
deserializeSparseFields(raw);
|
|
6013
|
+
// Then attribute → domain names, same order as `decodeRecord` (#127).
|
|
6014
|
+
renameFromDynamo(raw);
|
|
6015
|
+
return Schema.decodeUnknownEffect(schemas.deletedRecordSchema)(raw).pipe(Effect.map(attachPrototype), Effect.mapError((cause) => new ValidationError({
|
|
6016
|
+
entityType,
|
|
6017
|
+
operation: "deleted.list.decode",
|
|
6018
|
+
cause,
|
|
6019
|
+
})));
|
|
6020
|
+
});
|
|
5725
6021
|
};
|
|
5726
6022
|
return Query.make({
|
|
5727
6023
|
tableName: "",
|
|
@@ -5731,6 +6027,7 @@ const makeImpl = (config, precomputedData) => {
|
|
|
5731
6027
|
skField: primary.sk.field,
|
|
5732
6028
|
entityTypes: [],
|
|
5733
6029
|
decoder: (raw) => decodeDeleted(raw),
|
|
6030
|
+
...packedQueryOptions,
|
|
5734
6031
|
resolveTableName: tableTag.useSync((tc) => tc.name),
|
|
5735
6032
|
keyFields: [primary.pk.field, primary.sk.field],
|
|
5736
6033
|
}).pipe(Query.where({ beginsWith: deletedPrefix }));
|
|
@@ -6151,6 +6448,7 @@ const makeImpl = (config, precomputedData) => {
|
|
|
6151
6448
|
const expr = typeof cbOrShorthand === "function"
|
|
6152
6449
|
? cbOrShorthand(entityPathBuilder, entityConditionOps)
|
|
6153
6450
|
: parseSimpleShorthand(cbOrShorthand);
|
|
6451
|
+
refusePackedExpr(expr, "condition");
|
|
6154
6452
|
return (self) => conditionCombinator(self, expr);
|
|
6155
6453
|
};
|
|
6156
6454
|
/**
|
|
@@ -6161,17 +6459,25 @@ const makeImpl = (config, precomputedData) => {
|
|
|
6161
6459
|
const expr = typeof cbOrShorthand === "function"
|
|
6162
6460
|
? cbOrShorthand(entityPathBuilder, entityConditionOps)
|
|
6163
6461
|
: parseSimpleShorthand(cbOrShorthand);
|
|
6462
|
+
refusePackedExpr(expr, "filter");
|
|
6164
6463
|
return (q) => filterExpr(q, expr);
|
|
6165
6464
|
};
|
|
6166
6465
|
/**
|
|
6167
6466
|
* Build a select (projection) combinator from callback or string array.
|
|
6168
6467
|
*/
|
|
6169
6468
|
const entitySelect = (cbOrAttrs) => {
|
|
6469
|
+
const refuse = (paths) => {
|
|
6470
|
+
const refused = packedProjectionRefusal(paths, packedGuards);
|
|
6471
|
+
if (refused !== undefined)
|
|
6472
|
+
throw new Error(refused);
|
|
6473
|
+
};
|
|
6170
6474
|
if (typeof cbOrAttrs === "function") {
|
|
6171
6475
|
const paths = cbOrAttrs(entityPathBuilder);
|
|
6172
6476
|
const segments = paths.map((p) => p.segments);
|
|
6477
|
+
refuse(segments);
|
|
6173
6478
|
return (q) => selectPaths(q, segments);
|
|
6174
6479
|
}
|
|
6480
|
+
refuse(cbOrAttrs.map((attribute) => [attribute]));
|
|
6175
6481
|
return (q) => Query.select(q, cbOrAttrs);
|
|
6176
6482
|
};
|
|
6177
6483
|
// ---------------------------------------------------------------------------
|
|
@@ -6199,8 +6505,14 @@ const makeImpl = (config, precomputedData) => {
|
|
|
6199
6505
|
generatedId: config.generatedId,
|
|
6200
6506
|
vectorIndexes: config.vectorIndexes,
|
|
6201
6507
|
_vectorIndexes: vectorIndexes,
|
|
6508
|
+
_packedFields: Object.keys(packedFields),
|
|
6509
|
+
_packFields: packFields,
|
|
6510
|
+
_unpackFields: unpackFields,
|
|
6511
|
+
_dispatchAttributes: dispatchAttributes,
|
|
6512
|
+
_packedGuards: packedGuards,
|
|
6513
|
+
_decodeImage: decodeImageItem,
|
|
6202
6514
|
_resolvedRefs: resolvedRefs,
|
|
6203
|
-
/** @internal Full decode pipeline: rename + schema decode. Used by Batch/Aggregate. */
|
|
6515
|
+
/** @internal Full decode pipeline: unpack + sparse + rename + schema decode. Used by Batch/Aggregate. */
|
|
6204
6516
|
_decodeRecord: decodeRecord,
|
|
6205
6517
|
/** @internal Domain field name → stored attribute name (`storedAs` renames). */
|
|
6206
6518
|
_resolveDbName: resolveDbName,
|
|
@@ -6222,6 +6534,9 @@ const makeImpl = (config, precomputedData) => {
|
|
|
6222
6534
|
tableTag = injectedTableTag;
|
|
6223
6535
|
},
|
|
6224
6536
|
_injectIndex: (name, def) => {
|
|
6537
|
+
const refused = packedIndexRefusal(entityType, packedFields, name, def);
|
|
6538
|
+
if (refused !== undefined)
|
|
6539
|
+
throw new Error(refused);
|
|
6225
6540
|
allIndexes = { ...allIndexes, [name]: def };
|
|
6226
6541
|
},
|
|
6227
6542
|
get _schema() {
|
|
@@ -6636,12 +6951,27 @@ export const itemSchema = (entity) => entity.schemas.itemSchema;
|
|
|
6636
6951
|
/**
|
|
6637
6952
|
* Decode a marshalled DynamoDB item (AttributeValue format) into a typed Entity.Record.
|
|
6638
6953
|
*
|
|
6639
|
-
*
|
|
6640
|
-
*
|
|
6954
|
+
* Built for DynamoDB Streams records (`NewImage` / `OldImage`) and raw SDK
|
|
6955
|
+
* responses. Applies exactly what a table read applies:
|
|
6956
|
+
*
|
|
6957
|
+
* - **Packed fields** (`storedAs: DynamoModel.Packed`) are read by their
|
|
6958
|
+
* stored form — a legacy native map, a JSON string (`S`), or compressed
|
|
6959
|
+
* binary (`B`). Lambda and EventBridge Pipes deliver `B` / `BS` members as
|
|
6960
|
+
* base64 text; they are decoded at any depth.
|
|
6961
|
+
* - Sparse-map fields are rebuilt and `field:` renames reversed.
|
|
6962
|
+
*
|
|
6963
|
+
* Accepts DynamoDB JSON (`{ pk: { S: "…" }, … }`, `B` as bytes or base64) or
|
|
6964
|
+
* an already-unmarshalled item. Fails with `ValidationError` if the item does
|
|
6965
|
+
* not match the schema, or when a compressed value's codec is not registered —
|
|
6966
|
+
* its `cause` is then a `CompressorNotRegistered` (register the codec with a
|
|
6967
|
+
* `Compression.layer([...])` provided where the decode runs).
|
|
6641
6968
|
*
|
|
6642
|
-
*
|
|
6969
|
+
* Note: {@link itemSchema} is a pure schema over unmarshalled items and does
|
|
6970
|
+
* not unpack — use this for entities with packed fields.
|
|
6643
6971
|
*/
|
|
6644
6972
|
export const decodeMarshalledItem = (entity, marshalledItem) => {
|
|
6973
|
+
if (entity._decodeImage !== undefined)
|
|
6974
|
+
return entity._decodeImage(marshalledItem);
|
|
6645
6975
|
const corruption = entity._versionCorruption?.(marshalledItem, "decodeMarshalledItem");
|
|
6646
6976
|
if (corruption !== undefined)
|
|
6647
6977
|
return Effect.fail(corruption);
|