Follow-up from #725 (EQL v3 in encryptedDynamoDB).
Summary
A v3 column declared under a dotted path is split correctly at runtime but the split is not modelled in the encryptModel / bulkEncryptModels return type. The type describes the nested object as untouched plaintext.
Detail
Given:
const users = encryptedTable('users', {
'profile.ssn': types.TextEq('profile.ssn'),
})
Runtime (correct, tested):
input: { pk: 'u#1', profile: { ssn: '123-45-6789' } }
stored: { pk: 'u#1', profile: { ssn__source: 'CT…', ssn__hmac: '…' } }
Type (EncryptedAttributes, packages/stack/src/dynamodb/types.ts:180-194):
result.data.profile // typed { ssn: string } ← the type's belief
result.data.profile.ssn // typechecks, but undefined at runtime
result.data.profile.ssn__source // TYPE ERROR — but this is the real value
Root cause
EncryptedAttributes iterates K in keyof T (the input-model keys) and rewrites a key only when K extends keyof V3Columns<Table>. The column-map key is the string 'profile.ssn'; the model has no top-level 'profile.ssn' key, only profile (an object). profile fails the match and falls through to the pass-through arm with its original plaintext type. This is the same defect the top-level EncryptedAttributes was created to fix — it's just unfixed for nested paths. Called out in the docblock at types.ts:175-178.
Top-level v3 columns are unaffected. v2 is unaffected by this specific gap (it doesn't apply EncryptedAttributes at all — see the separate v2-overload-return-type follow-up).
Impact
Cosmetic until hit: runtime is correct and tested, but a caller who wants profile.ssn__source (e.g. to build a GSI key condition) gets a type error, and profile.ssn typechecks to undefined.
Fix sketch
Make the mapped type detect a dot in the column key, parse 'profile.ssn' into a path via template-literal types, recurse into the nested object type, and rewrite the leaf to <leaf>__source (+ optional <leaf>__hmac) while preserving plaintext siblings — for arbitrary depth and for multiple dotted columns sharing a prefix. Fiddly recursive mapped-type work; deliberately deferred out of #725.
Acceptance
- Type test in
packages/stack/__tests__/dynamodb/client-compat.test-d.ts asserting the nested __source/__hmac keys exist and the leaf plaintext key does not, with plaintext siblings preserved.
Follow-up from #725 (EQL v3 in
encryptedDynamoDB).Summary
A v3 column declared under a dotted path is split correctly at runtime but the split is not modelled in the
encryptModel/bulkEncryptModelsreturn type. The type describes the nested object as untouched plaintext.Detail
Given:
Runtime (correct, tested):
Type (
EncryptedAttributes,packages/stack/src/dynamodb/types.ts:180-194):Root cause
EncryptedAttributesiteratesK in keyof T(the input-model keys) and rewrites a key only whenK extends keyof V3Columns<Table>. The column-map key is the string'profile.ssn'; the model has no top-level'profile.ssn'key, onlyprofile(an object).profilefails the match and falls through to the pass-through arm with its original plaintext type. This is the same defect the top-levelEncryptedAttributeswas created to fix — it's just unfixed for nested paths. Called out in the docblock attypes.ts:175-178.Top-level v3 columns are unaffected. v2 is unaffected by this specific gap (it doesn't apply
EncryptedAttributesat all — see the separate v2-overload-return-type follow-up).Impact
Cosmetic until hit: runtime is correct and tested, but a caller who wants
profile.ssn__source(e.g. to build a GSI key condition) gets a type error, andprofile.ssntypechecks toundefined.Fix sketch
Make the mapped type detect a dot in the column key, parse
'profile.ssn'into a path via template-literal types, recurse into the nested object type, and rewrite the leaf to<leaf>__source(+ optional<leaf>__hmac) while preserving plaintext siblings — for arbitrary depth and for multiple dotted columns sharing a prefix. Fiddly recursive mapped-type work; deliberately deferred out of #725.Acceptance
packages/stack/__tests__/dynamodb/client-compat.test-d.tsasserting the nested__source/__hmackeys exist and the leaf plaintext key does not, with plaintext siblings preserved.