Skip to content

Commit

Permalink
fix(save): throw validation error if required key is missing
Browse files Browse the repository at this point in the history
  • Loading branch information
SajidHamza9 committed Jan 31, 2024
1 parent 4ce8d2b commit 2f1fad5
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 3 deletions.
9 changes: 7 additions & 2 deletions src/base-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Scan from './scan';
import { IUpdateActions, buildUpdateActions, put } from './update-operators';
import ValidationError from './validation-error';
import { PutCommandInput } from '@aws-sdk/lib-dynamodb/dist-types/commands/PutCommand';
import { hashKey, item, rangeKey, validItem, validate } from './decorators';

export type KeyValue = string | number | Buffer | boolean | null;
type SimpleKey = KeyValue;
Expand All @@ -34,10 +35,13 @@ const isComposite = (hashKeys_compositeKeys: Keys): hashKeys_compositeKeys is Co
export default abstract class Model<T> {
protected tableName: string | undefined;

@item
protected item: T | undefined;

@hashKey
protected pk: string | undefined;

@rangeKey
protected sk: string | undefined;

protected documentClient: DynamoDBDocumentClient;
Expand Down Expand Up @@ -188,13 +192,14 @@ export default abstract class Model<T> {
*/
async save(item: T, options?: Partial<PutCommandInput>): Promise<T>;

@validate
async save(
item_options?: T | Partial<PutCommandInput>,
@validItem item_options?: T | Partial<PutCommandInput>,
options?: Partial<PutCommandInput>,
): Promise<T> {
// Handle typescript method overloading
const toSave: T | undefined =
item_options != null && this.isItem(item_options) ? item_options : this.item;
item_options != null ? item_options as T : this.item;
const putOptions: Partial<PutCommandInput> | undefined =
item_options != null && this.isItem(item_options)
? options
Expand Down
98 changes: 98 additions & 0 deletions src/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import "reflect-metadata";
import joi from 'joi';
import ValidationError from "./validation-error";

const itemParamMetadataKey = Symbol("ItemParam");
const itemPropertyKey = Symbol("ItemProperty");
const pkKey = Symbol('PK');
const skKey = Symbol('SK');

export function hashKey(target: any, key: string) {

Check warning on line 10 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 10 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
Reflect.defineProperty(target, key, {
get: function () {
return this[pkKey];
},
set: function (newVal: string) {
this[pkKey] = newVal
}
})
}

export function rangeKey(target: any, key: string) {

Check warning on line 21 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 21 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
Reflect.defineProperty(target, key, {
get: function () {
return this[skKey];
},
set: function (newVal: string) {
this[skKey] = newVal
}
})
}

export function item(target: any, key: string) {

Check warning on line 32 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 32 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
Reflect.defineProperty(target, key, {
get: function () {
const pk = this[pkKey]
const sk = this[skKey]
const item = this[itemPropertyKey];
validateSchema(pk, sk, item);
return item;
},
set: function (newVal: unknown) {
this[itemPropertyKey] = newVal;
}
})
}

export function validItem(
target: any,

Check warning on line 48 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 48 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
propertyKey: string,
parameterIndex: number
) {
const existingParameters: number[] =
Reflect.getOwnMetadata(itemParamMetadataKey, target, propertyKey) || [];
existingParameters.push(parameterIndex);
Reflect.defineMetadata(
itemParamMetadataKey,
existingParameters,
target,
propertyKey
);
}

export function validate(
target: any,

Check warning on line 64 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 64 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
propertyName: string,
descriptor: any

Check warning on line 66 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 66 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {

Check warning on line 69 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 69 in src/decorators.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
const pk = this[pkKey]
const sk = this[skKey]
const parameters: number[] = Reflect.getOwnMetadata(
itemParamMetadataKey,
target,
propertyName
);
if (parameters) {
for (const parameter of parameters) {
validateSchema(pk, sk, args[parameter]);
}
}
return original.apply(this, args);
};
}

function validateSchema(pk: string | undefined, sk: string | undefined, item: unknown) {
if (!pk) {
throw new Error('Model error: hash key is not defined on this Model');
}
const schema = joi.object().keys({
[pk]: joi.required(),
...(!!sk && { [sk]: joi.required() })
}).options({ allowUnknown: true })
const { error } = schema.validate(item);
if (error) {
throw new ValidationError('Validation error', error);
}
}
38 changes: 37 additions & 1 deletion test/save.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { clearTables } from './hooks/create-tables';
import HashKeyModel from './models/hashkey';
import HashKeyModel, { HashKeyEntity } from './models/hashkey';
import TimeTrackedModel from './models/autoCreatedAt-autoUpdatedAt';
import HashKeyJoiModel from './models/hashkey-joi';
import CompositeKeyModel, { CompositeKeyEntity } from './models/composite-keys';

describe('The save method', () => {
beforeEach(async () => {
Expand Down Expand Up @@ -64,6 +65,41 @@ describe('The save method', () => {
expect((e as Error).message.includes('No item to save')).toBe(true);
}
});
test('should throw an error if the hash keys is missing', async () => {
const foo = new HashKeyModel();
const item = {
string: 'whatever',
stringmap: { foo: 'bar' },
stringset: ['bar, bar'],
number: 43,
bool: true,
list: ['foo', 42],
};
try {
await foo.save(item as unknown as HashKeyEntity);
fail('should throw');
} catch (e) {
expect((e as Error).message.includes('Validation error')).toBe(true);
}
});
test('should throw an error if the range key is missing', async () => {
const foo = new CompositeKeyModel();
const item = {
hashkey: 'bar',
string: 'whatever',
stringmap: { foo: 'bar' },
stringset: ['bar, bar'],
number: 43,
bool: true,
list: ['foo', 42],
};
try {
await foo.save(item as unknown as CompositeKeyEntity);
fail('should throw');
} catch (e) {
expect((e as Error).message.includes('Validation error')).toBe(true);
}
});
test('should throw an error if a Joi schema is specified and validation failed', async () => {
const foo = new HashKeyJoiModel({
hashkey: 'bar',
Expand Down

0 comments on commit 2f1fad5

Please sign in to comment.