fix(transform): match AWS DynamoDB filter and condition expressions - #297
Conversation
CIThe workflow's https://github.com/localstack/appsync-utils/actions/runs/33681334704
Re-dispatch after any push here with To try both halves of the stack in a LocalStack instance before anything is released, start it with |
`toDynamoDBFilterExpression` accepted exactly one field carrying exactly
one `contains` and threw `Not implemented for <op>` on everything else,
while `toDynamoDBConditionExpression` went through an Amplify derived
builder that diverged from AWS: `undefined` leaked into the value
placeholders as `:_and_0_id_eq`, `between` emitted `_1`/`_2` without
parentheses, the function operators left a space after the comma,
`attributeType` recursed over its operand until the stack overflowed,
`size` was mistaken for a field, and an unknown operator silently
rendered an empty expression.
Both entry points now share one builder. AWS produced byte identical
output for the two of them across every input recorded, along with the
grammar the builder implements: the value placeholder naming, the rule
that a group is wrapped only when it holds more than one member, `size`
as a wrapper around the attribute rather than an operator, the friendly
type names `attributeType` accepts, and `null` in place of an error for
a filter that cannot be rendered.
`util.dynamodb.toDynamoDB` now types `null` as `{"NULL": null}` the way
AWS does, which the transform needs for a null operand.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3f2cf1c to
b9bd600
Compare
2/2 of a stack, based on #296 — review that first. The two are meant to land under one release.
Problem
toDynamoDBFilterExpressionaccepted exactly one field carrying exactly onecontainsand threwNot implemented for <op>on everything else, never calling the general builder next to it.toDynamoDBConditionExpressiondid call it, and that builder, ported fromamplify-appsync-simulator, diverged from AWS:prefix = nulldefaults, soundefinedreached the value names as:_and_0_id_eqbetweenemitted_1/_2and no surrounding parenthesesattributeTypefell into the default recursion with a string operand and recursed per character until the stack overflowedsizewas treated as a field nameWhat this does
Both entry points now share one recursive builder, replacing the token array with its
mergeandscopeExpressionhelpers. AWS produced byte identical output for the two of them across all 66 recorded inputs, so there is nothing to fork.containsnotContainsbeginsWith(contains(#v,:v_contains)), no space after the commabetween(#v BETWEEN :v_between_start AND :v_between_end)in(#v IN (:v_in_0, :v_in_1)), the one operator that does space its commaattributeTypestringtoS,stringSettoSS, and eight moresize(contains(size(#v),:v_size_contains))A group is wrapped only when it holds more than one member, the filter as a whole never is, and
notalways is. Members that render to nothing are joined all the same, which is what leaves"(#a = :a_eq) AND "for{a: {eq: 1}, b: {}}.Two behaviours worth flagging because they contradict the upstream types: the map form of
andandoris not supported, althoughDynamoDBFilterObjectdescribes it, and an input AWS cannot render returnsnullrather than throwing.Also fixed:
util.dynamodb.toDynamoDB(null)threw aTypeError, readingvalue.lengthbefore handling null, where AWS types it as{"NULL": null}. The transform needs it for a null operand.Tests
82 snapshots recorded from
EvaluateCodein a new__tests__/transform.test.js, covering every operator and operand type, the composition and wrapping rules, the filters that render nothing, the 18 inputs that come backnull, the two entry points agreeing, and the shapes the Amplify GraphQL transformer generates.Snapshots are parsed before comparison because AWS orders the keys of
expressionNamesandexpressionValuesinconsistently between runs; two cases keep the raw string to pin the serialisation. The file is new, so recording could not touch the neighbouring snapshots, and the two legacyTransformationsones still pass untouched.npm test: 345 passed, 3 skipped, 337 snapshots. A replay of the whole probe matrix against the new builder matched AWS on 132 of 132 results.🤖 Generated with Claude Code