Skip to content

Commit

Permalink
Fix removing unversioned documents via versioned model, and add warni…
Browse files Browse the repository at this point in the history
…ng. Add tests for remove versioning.
  • Loading branch information
autopulated committed Mar 7, 2024
1 parent 7717b06 commit dbb2d4e
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 11 deletions.
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### 1.3.1
* Fixes removal of unversioned documents via a versioned model.

## 1.3.0
* Adds support for model versioning, with associated schema fragment to name
the version field. To disable versioning pass options.versioning: false to
Expand Down
36 changes: 26 additions & 10 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,8 @@ class BaseModel {
} else {
throw new Error(`Version error: the model .${schema.idFieldName}="${this[schema.idFieldName]}" was updated by another process between loading and saving.`);
}
/* c8 ignore next 3 */
} else {
/* c8 ignore next 2 */
throw e;
}
}
Expand All @@ -479,8 +479,8 @@ class BaseModel {
async #remove(){
const table = this.constructor[kModelTable],
schema = this.constructor[kModelSchema];
/* c8 ignore next 3 */
if (!table[kTableIsReady]) {
/* c8 ignore next 2 */
await table.ready();
}
const commandArgs = {
Expand All @@ -489,13 +489,29 @@ class BaseModel {
};
// check that the version field is the same as it was when we loaded this model.
if (schema.versionFieldName) {
commandArgs.ConditionExpression = '#v = :v';
commandArgs.ExpressionAttributeNames = { '#v': schema.versionFieldName };
commandArgs.ExpressionAttributeValues = { ':v': this[schema.versionFieldName] };
if (!this[schema.versionFieldName]) {
this.#logger.warn('Removing versioned document .%s="%s" missing version field.', schema.versionFieldName, this.id);
commandArgs.ConditionExpression = 'attribute_not_exists(#v)';
commandArgs.ExpressionAttributeNames = { '#v': schema.versionFieldName };
} else {
commandArgs.ConditionExpression = '#v = :v';
commandArgs.ExpressionAttributeNames = { '#v': schema.versionFieldName };
commandArgs.ExpressionAttributeValues = { ':v': this[schema.versionFieldName] };
}
}
const command = new DeleteCommand(commandArgs);
this.#logger.trace({command}, 'remove %s', this.id);
const data = await table[kTableDDBClient].send(command);
let data;
try {
data = await table[kTableDDBClient].send(command);
} catch (e) {
if (e.name === 'ConditionalCheckFailedException') {
throw new Error(`Version error: the model .${schema.idFieldName}="${this[schema.idFieldName]}" was updated by another process between loading and removing.`);
/* c8 ignore next 3 */
} else {
throw e;
}
}
this.#logger.trace({response: data}, 'remove %s response', this.id);
return this;
}
Expand Down Expand Up @@ -583,8 +599,8 @@ class BaseModel {
const { ConsistentRead, abortSignal } = rawOptions ?? {};
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
/* c8 ignore next 3 */
if (!table[kTableIsReady]) {
/* c8 ignore next 2 */
await table.ready();
}
if (!Array.isArray(ids)) {
Expand Down Expand Up @@ -640,8 +656,8 @@ class BaseModel {
const {limit, abortSignal} = options?? {};
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
/* c8 ignore next 3 */
if (!table[kTableIsReady]) {
/* c8 ignore next 2 */
await table.ready();
}
const sendOptions = {
Expand Down Expand Up @@ -676,8 +692,8 @@ class BaseModel {
const {limit, abortSignal} = options?? {};
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
/* c8 ignore next 3 */
if (!table[kTableIsReady]) {
/* c8 ignore next 2 */
await table.ready();
}
const sendOptions = {
Expand All @@ -704,8 +720,8 @@ class BaseModel {
static async #rawQueryOneId(DerivedModel, rawQuery, options) {
const table = DerivedModel[kModelTable];
const schema = DerivedModel[kModelSchema];
/* c8 ignore next 3 */
if (!table[kTableIsReady]) {
/* c8 ignore next 2 */
await table.ready();
}
const commandParams = {
Expand Down
68 changes: 67 additions & 1 deletion test/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -698,13 +698,79 @@ t.test('versioning', async t => {
await a.save();
const b = await AModel.getById(a.id);

// should log warning about adding a version field
const results = t.capture(logger, 'warn');

// should log warning about adding a version field
await b.save();

t.match(results(), [{args:['Adding missing version field %s to document %s.', 'v', a.id]}], 'should have called logger.warn with a suitable message');
});

t.test('creating, saving, then removing', async t => {
const a = new AModel({foo:'remove'});
const id = a.id;
await a.save();

await a.remove();

t.equal(await AModel.getById(id), null, 'document should have been removed');
});

t.test('loading then removing', async t => {
const a = new AModel({foo:'load and remove'});
const id = a.id;
await a.save();

const b = await AModel.getById(a.id);
t.equal(b[ASchema.versionFieldName], 1, 'loaded version should be 1');

await b.remove();
t.equal(await AModel.getById(id), null, 'document should have been removed');
});

t.test('loading, saving, then removing', async t => {
const a = new AModel({foo:'bar'});
const id = a.id;
await a.save();

const b = await AModel.getById(a.id);

b.foo = 'loaded';
await b.save();

await b.remove();
t.equal(await AModel.getById(id), null, 'document should have been removed');
});

t.test('removing an outdated doc causing version error', async t => {
const a = new AModel({foo:''});

await a.save();

const b = await AModel.getById(a.id);
const c = await AModel.getById(a.id);

b.foo = 'remove version error';

await b.save();

t.rejects(c.remove(), {message:`Version error: the model .id="${a.id}" was updated by another process between loading and removing.`}, 'removing an updated model should cause a version error');
});

t.test('removing unversioned model', async t => {
const a = new AModel_unversioned({foo:'unversioned save'});

await a.save();
const b = await AModel.getById(a.id);

const results = t.capture(logger, 'warn');

// should log warning about a missing version field
await b.remove();

t.match(results(), [{args:['Removing versioned document .%s="%s" missing version field.', 'v', a.id]}], 'should have called logger.warn with a suitable message');
});

t.test('naming a version field when Schema versioning option is false', async t => {
const logger = require('pino')({level:'warn'});
// stop child logger creation so we can intercept messages
Expand Down

0 comments on commit dbb2d4e

Please sign in to comment.