Sneaky Bugs: using defaults in schema with dynamodb-onetable
bugs dynamodb aws dynamodb-onetabledynamodb-onetable
is basically mongoose but for dynamodb. It allows you to model data, and takes care of validation etc. based on your schema. It also does a bunch of other things, including simplification of dynamodb requests, as they can get a bit complex.
The schema def allows you to define fields with their types and an optional default value, with a behaviour that I didn’t expect, and caused a bug to sneak in.
I was not going to write about it since it didn’t happen to me, but I wanted to get one post in for 2024, and I’m out of ideas and time.
The schema def looks something like this:
const schema = {
version: "0.0.1",
indexes: {
primary: { hash: "pk", sort: "sk" },
},
models: {
TestModel: {
pk: { type: String, required: true, value: "test" },
sk: { type: String, required: true, value: "${_type}" },
withoutDefault: { type: Boolean },
withDefault: { type: Boolean, default: false },
},
},
};
The model TestModel
has two fields other than the primary keys. In the current state, the pk/sk fields will always equal to test/TestModel
, which would be an issue if you were trying to write multiple records, but not in this case.
The usecase involved upserting records, and dynamodb-onetable
updates fields with default values if they’re missing from the upsert
call args. This was a bit unexpected as you’d only expect the default
value to come into play when the field is previously unset.
create
// without args, only populates the default field
await TestModel.create({});
// expected and actual is same
// {
// withDefault: false,
// }
// with both fields, so populates them both
await TestModel.create({
withDefault: true,
withoutDefault: true,
});
// expected and actual is same
// {
// withDefault: true,
// withoutDefault: true,
// }
All good so far.
upsert vs update
// current item
// {
// withoutDefault: true,
// withDefault: true,
// }
// update works as expected
await TestModel.update({
// ...pk/sk
withoutDefault: false,
});
// expected and actual is same
// {
// withoutDefault: false,
// withDefault: true,
// }
// upsert replaces all the missing fields with their default values if it exists
await TestModel.upsert({
// ...pk/sk
withoutDefault: true,
});
// expected
// {
// withoutDefault: true,
// withDefault: true,
// }
// actual
// {
// withoutDefault: false, // replaced this
// withDefault: true,
// }
Since the record got upserted multiple times with different set of args, not always containing the field with default, the actual value got replaced with the default value.