From 27e11fd4390587eeab1ee8c11778fe59fee3e150 Mon Sep 17 00:00:00 2001 From: Joshua Williams Date: Sun, 17 Mar 2024 10:38:47 -0500 Subject: [PATCH] chore: update model return type chore: add getPrimaryKey and fresh method to Model class --- .gitignore | 1 + __tests__/model.spec.ts | 1 - dist/src/dynamorm.d.ts | 2 +- dist/src/dynamorm.d.ts.map | 2 +- dist/src/dynamorm.js | 2 +- dist/src/exceptions.d.ts | 4 ++ dist/src/exceptions.d.ts.map | 2 +- dist/src/exceptions.js | 10 ++++- dist/src/model.d.ts | 13 +++++- dist/src/model.d.ts.map | 2 +- dist/src/model.js | 72 ++++++++++++++++++++++++++------- dist/src/table.d.ts.map | 2 +- dist/src/types.d.ts | 8 +++- dist/src/types.d.ts.map | 2 +- package.json | 2 + src/dynamorm.ts | 8 ++-- src/exceptions.ts | 6 +++ src/model.ts | 77 ++++++++++++++++++++++++++++-------- src/table.ts | 1 + src/types.ts | 9 ++++- tsconfig.json | 2 +- 21 files changed, 178 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index 3db223c..1072748 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +/dist /node_modules /src/**/*.js /storage/dynamodb diff --git a/__tests__/model.spec.ts b/__tests__/model.spec.ts index a5ce8fb..74e8662 100644 --- a/__tests__/model.spec.ts +++ b/__tests__/model.spec.ts @@ -47,7 +47,6 @@ describe('model', () => { fillData.author = undefined; model.fill(fillData) const result = model.validate(); - console.log(result); expect(result.valid).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain('author'); diff --git a/dist/src/dynamorm.d.ts b/dist/src/dynamorm.d.ts index e000119..3824f35 100644 --- a/dist/src/dynamorm.d.ts +++ b/dist/src/dynamorm.d.ts @@ -21,7 +21,7 @@ export declare class DynamoRM { * @param modelName * @param attributes */ - model(modelName: string, attributes?: Record): Model; + model(modelName: string, attributes?: Record): Model & T; } export declare const create: (App: Function) => IDynamoRM; declare const _default: { diff --git a/dist/src/dynamorm.d.ts.map b/dist/src/dynamorm.d.ts.map index 254bcb8..bcf2be9 100644 --- a/dist/src/dynamorm.d.ts.map +++ b/dist/src/dynamorm.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"dynamorm.d.ts","sourceRoot":"","sources":["../../src/dynamorm.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,EAEzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,MAAM,SAAS,CAAC;AAE5B,OAAO,EAAC,SAAS,EAAqB,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAEzF;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;gBAE5B,EAAE,EAAE,QAAQ;IASxB,SAAS;IAIT,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;IAQhC,YAAY,IAAI,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAUhE,SAAS,IAAI,KAAK,CAAC,gBAAgB,CAAC;IAIpC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;IAQ7C,OAAO,CAAC,YAAY;IAUpB;;;;OAIG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAE,KAAK;CAsBhE;AAED,eAAO,MAAM,MAAM,QAAS,QAAQ,KAAG,SAEtC,CAAA;;;;AAED,wBAEC"} \ No newline at end of file +{"version":3,"file":"dynamorm.d.ts","sourceRoot":"","sources":["../../src/dynamorm.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,EAEzB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,MAAM,SAAS,CAAC;AAE5B,OAAO,EAAC,SAAS,EAAqB,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,SAAS,CAAC;AAEzF;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;gBAE5B,EAAE,EAAE,QAAQ;IASxB,SAAS;IAIT,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;IAQhC,YAAY,IAAI,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAUhE,SAAS,IAAI,KAAK,CAAC,gBAAgB,CAAC;IAIpC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;IAQ7C,OAAO,CAAC,YAAY;IAUpB;;;;OAIG;IACH,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC;CAsBxE;AAED,eAAO,MAAM,MAAM,QAAS,QAAQ,KAAG,SAEtC,CAAA;;;;AAED,wBAEC"} \ No newline at end of file diff --git a/dist/src/dynamorm.js b/dist/src/dynamorm.js index 2014465..24239e1 100644 --- a/dist/src/dynamorm.js +++ b/dist/src/dynamorm.js @@ -16,7 +16,7 @@ class DynamoRM { client; constructor(DB) { this.tables = Reflect.getMetadata('tables', DB); - this.models = Reflect.getMetadata('models', DB); + this.models = Reflect.getMetadata('models', DB) || []; this.client = Reflect.getMetadata('client', DB); if (!this.tables || !this.client) { throw new Error('A dynamodb client and tables are required'); diff --git a/dist/src/exceptions.d.ts b/dist/src/exceptions.d.ts index 39207a2..ff782ff 100644 --- a/dist/src/exceptions.d.ts +++ b/dist/src/exceptions.d.ts @@ -10,4 +10,8 @@ export declare class ServiceUnavailableException extends DynamormException { export declare class PrimaryKeyException extends DynamormException { constructor(message: any); } +export declare class ValidationError extends DynamormException { + messages: string[]; + constructor(messages: string[]); +} //# sourceMappingURL=exceptions.d.ts.map \ No newline at end of file diff --git a/dist/src/exceptions.d.ts.map b/dist/src/exceptions.d.ts.map index 00f4e19..f27d3d9 100644 --- a/dist/src/exceptions.d.ts.map +++ b/dist/src/exceptions.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"exceptions.d.ts","sourceRoot":"","sources":["../../src/exceptions.ts"],"names":[],"mappings":"AAAA,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,KAAA;CAGpB;AAED,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,KAAA;CAGpB;AAED,qBAAa,2BAA4B,SAAQ,iBAAiB;gBACpD,OAAO,KAAA;CAGpB;AAED,qBAAa,mBAAoB,SAAQ,iBAAiB;gBAC5C,OAAO,KAAA;CAGpB"} \ No newline at end of file +{"version":3,"file":"exceptions.d.ts","sourceRoot":"","sources":["../../src/exceptions.ts"],"names":[],"mappings":"AAAA,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,KAAA;CAGpB;AAED,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,KAAA;CAGpB;AAED,qBAAa,2BAA4B,SAAQ,iBAAiB;gBACpD,OAAO,KAAA;CAGpB;AAED,qBAAa,mBAAoB,SAAQ,iBAAiB;gBAC5C,OAAO,KAAA;CAGpB;AAED,qBAAa,eAAgB,SAAQ,iBAAiB;IAEjC,QAAQ,EAAE,MAAM,EAAE;gBAAlB,QAAQ,EAAE,MAAM,EAAE;CAGtC"} \ No newline at end of file diff --git a/dist/src/exceptions.js b/dist/src/exceptions.js index 9bb31e2..e3ffa21 100644 --- a/dist/src/exceptions.js +++ b/dist/src/exceptions.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.PrimaryKeyException = exports.ServiceUnavailableException = exports.TableNotFoundException = exports.DynamormException = void 0; +exports.ValidationError = exports.PrimaryKeyException = exports.ServiceUnavailableException = exports.TableNotFoundException = exports.DynamormException = void 0; class DynamormException extends Error { constructor(message) { super(message); @@ -25,3 +25,11 @@ class PrimaryKeyException extends DynamormException { } } exports.PrimaryKeyException = PrimaryKeyException; +class ValidationError extends DynamormException { + messages; + constructor(messages) { + super(messages); + this.messages = messages; + } +} +exports.ValidationError = ValidationError; diff --git a/dist/src/model.d.ts b/dist/src/model.d.ts index d758bb6..4251f29 100644 --- a/dist/src/model.d.ts +++ b/dist/src/model.d.ts @@ -1,11 +1,14 @@ -import { AttributeDefinitions, Attributes } from "./types"; +import { AttributeDefinitions, Attributes, PrimaryKey } from "./types"; import { DynamoDBClient, PutItemCommandOutput } from "@aws-sdk/client-dynamodb"; import Table from "./table"; +import Entity from "./entity"; declare class Model { private client; name: string; protected table: Table; + protected entity: Entity; protected attributes: AttributeDefinitions; + readonly primaryKey: PrimaryKey; constructor(client: DynamoDBClient); fill(attribute: Attributes | string, value?: any): void; /** @@ -19,11 +22,16 @@ declare class Model { * @param attributeName */ get(attributeName: string): any; + /** + * @description Gets PrimaryKey including values + */ + getPrimaryKey(): PrimaryKey; getAttributes(): AttributeDefinitions; - getAttributeValues(): {}; + getAttributeValues(omitUndefined?: boolean): {}; static getEntity(): any; getEntity(instance?: boolean): unknown; save(): Promise; + fresh(): Promise; find(pk?: string, sk?: string): Promise; /** * @description Delete an item by primary key @@ -33,6 +41,7 @@ declare class Model { */ delete(pk?: string, sk?: string): Promise; clear(): void; + private validatePrimaryKey; validate(): { valid: boolean; errors: string[]; diff --git a/dist/src/model.d.ts.map b/dist/src/model.d.ts.map index 28aed61..619de11 100644 --- a/dist/src/model.d.ts.map +++ b/dist/src/model.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,oBAAoB,EACpB,UAAU,EAEX,MAAM,SAAS,CAAC;AACjB,OAAO,EAGL,cAAc,EAEd,oBAAoB,EAErB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,KAAK,MAAM,SAAS,CAAC;AAG5B,cAAM,KAAK;IAMI,OAAO,CAAC,MAAM;IALpB,IAAI,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;IAEvB,SAAS,CAAC,UAAU,EAAE,oBAAoB,CAAM;gBAE3B,MAAM,EAAE,cAAc;IAsBpC,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,EAAE,KAAK,CAAC,EAAC,GAAG;IAatD;;;;OAIG;IACI,GAAG,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG;IAM5C;;;OAGG;IACI,GAAG,CAAC,aAAa,EAAE,MAAM;IAOzB,aAAa;IAIb,kBAAkB;IAOzB,MAAM,CAAC,SAAS;IAIT,SAAS,CAAC,QAAQ,GAAE,OAAe;IAI7B,IAAI,IAAI,OAAO,CAAC,oBAAoB,CAAC;IA2BrC,IAAI,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAgC3D;;;;;OAKG;IACU,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM;IAyBrC,KAAK;IAML,QAAQ,IAAI;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAC;IAyCrD,OAAO,CAAC,iBAAiB;IAyCzB,OAAO,CAAC,iBAAiB;CAc1B;AAED,eAAe,KAAK,CAAA"} \ No newline at end of file +{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,oBAAoB,EACpB,UAAU,EACQ,UAAU,EAC7B,MAAM,SAAS,CAAC;AACjB,OAAO,EAGL,cAAc,EAEd,oBAAoB,EAErB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,KAAK,MAAM,SAAS,CAAC;AAC5B,OAAO,MAAM,MAAM,UAAU,CAAC;AAE9B,cAAM,KAAK;IAOI,OAAO,CAAC,MAAM;IANpB,IAAI,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;IACvB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,UAAU,EAAE,oBAAoB,CAAM;IAChD,SAAgB,UAAU,EAAE,UAAU,CAAC;gBAElB,MAAM,EAAE,cAAc;IAuBpC,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,MAAM,EAAE,KAAK,CAAC,EAAC,GAAG;IAatD;;;;OAIG;IACI,GAAG,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG;IAM5C;;;OAGG;IACI,GAAG,CAAC,aAAa,EAAE,MAAM;IAOhC;;OAEG;IACI,aAAa,IAAI,UAAU;IAY3B,aAAa;IAIb,kBAAkB,CAAC,aAAa,GAAE,OAAc;IAOvD,MAAM,CAAC,SAAS;IAIT,SAAS,CAAC,QAAQ,GAAE,OAAe;IAI7B,IAAI,IAAI,OAAO,CAAC,oBAAoB,CAAC;IA8BrC,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;IAezB,IAAI,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IA6B3D;;;;;OAKG;IACU,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM;IAyBrC,KAAK;IAMZ,OAAO,CAAC,kBAAkB;IA0BnB,QAAQ,IAAI;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAC;IA4BrD,OAAO,CAAC,iBAAiB;IAyCzB,OAAO,CAAC,iBAAiB;CAc1B;AAED,eAAe,KAAK,CAAA"} \ No newline at end of file diff --git a/dist/src/model.js b/dist/src/model.js index 2e38391..f0c2d6e 100644 --- a/dist/src/model.js +++ b/dist/src/model.js @@ -6,12 +6,15 @@ class Model { client; name; table; + entity; attributes = {}; + primaryKey; constructor(client) { this.client = client; this.table = new (Reflect.getMetadata('table', this.constructor)); - const entity = this.table.getEntity(true); - this.attributes = entity.getAttributeDefinitions(); + this.entity = this.table.getEntity(true); + this.attributes = this.entity.getAttributeDefinitions(); + this.primaryKey = this.table.getPrimaryKey(); for (let attribute in this.attributes) { Object.defineProperty(this, attribute, { get() { @@ -60,10 +63,24 @@ class Model { } throw new Error(`Attribute not found: ${attributeName}`); } + /** + * @description Gets PrimaryKey including values + */ + getPrimaryKey() { + const primaryKeyDefinition = this.table.getPrimaryKeyDefinition(); + const primaryKey = { + pk: this.attributes[primaryKeyDefinition.pk.AttributeName].value, + sk: undefined + }; + if (primaryKeyDefinition.sk) { + primaryKey.sk = this.attributes[primaryKeyDefinition.sk.AttributeName].value; + } + return primaryKey; + } getAttributes() { return this.attributes; } - getAttributeValues() { + getAttributeValues(omitUndefined = true) { return Object.keys(this.attributes).reduce((attributes, attribute) => { attributes[attribute] = this.attributes[attribute].value; return attributes; @@ -76,6 +93,9 @@ class Model { return this.table.getEntity(instance); } async save() { + const { valid, errors } = this.validate(); + if (!valid) + throw new exceptions_1.ValidationError(errors); const input = this.toPutCommandInput(); const command = new client_dynamodb_1.PutItemCommand(input); let result; @@ -99,19 +119,31 @@ class Model { } return result; } + async fresh() { + const { valid, errors } = this.validatePrimaryKey(); + if (!valid) { + throw new exceptions_1.PrimaryKeyException(errors[0]); + } + const model = await this.find(); + if (model === undefined) + return false; + for (let attributeName in this.attributes) { + if (model.attributes.hasOwnProperty(attributeName)) { + this.attributes[attributeName].value = model.attributes[attributeName].value; + } + } + return true; + } async find(pk, sk) { const primaryKeyDefinition = this.table.getPrimaryKeyDefinition(); if (primaryKeyDefinition.sk && !sk) { throw new exceptions_1.PrimaryKeyException(`Failed to fetch item. Primary key requires partition key and sort key on ${this.table.constructor.name}`); } - const primaryKey = { - pk: pk || this.table.getPrimaryKey().pk, - sk: sk || this.table.getPrimaryKey().sk, - }; + const primaryKeyValues = this.getPrimaryKey(); const input = { TableName: this.table.getName(), // @ts-ignore - Key: this.table.toInputKey(primaryKey) + Key: this.table.toInputKey(primaryKeyValues) }; const command = new client_dynamodb_1.GetItemCommand(input); let result; @@ -169,25 +201,37 @@ class Model { this.attributes[attributeName].value = undefined; } } - validate() { - const errors = []; + validatePrimaryKey() { const primaryKey = this.table.getPrimaryKey(); + const className = this.constructor.name === 'DynamicModel' ? this.entity.constructor.name : this.constructor.name; + const errors = []; // Validate partition key defined on table is also defined as attribute in respective entity if (!this.attributes.hasOwnProperty(primaryKey.pk)) { - errors.push(`Partition key "${primaryKey.pk}" is not defined in ${this.getEntity().constructor.name}`); + errors.push(`Partition key "${primaryKey.pk}" is not defined in ${className}`); } // Validate partition key defined on table is set in respective entity if (this.attributes[primaryKey.pk].value === undefined) { - errors.push(`Partition key "${primaryKey.pk}" is not set in ${this.constructor.name}`); + errors.push(`Partition key "${primaryKey.pk}" is not set in ${className}`); } // Validate sort key defined on table is also defined as attribute in respective entity if (primaryKey.sk && !this.attributes.hasOwnProperty(primaryKey.sk)) { - errors.push(`Sort key "${primaryKey.sk}" is not defined in ${this.getEntity().constructor.name}`); + errors.push(`Sort key "${primaryKey.sk}" is not defined in ${className}`); } // Validate sort key defined on table is set in respective entity - if (this.attributes[primaryKey.sk].value === undefined) { + if (primaryKey.sk && this.attributes[primaryKey.sk].value === undefined) { errors.push(`Sort key "${primaryKey.sk}" is not set in ${this.constructor.name}`); } + return { + valid: !errors.length, + errors + }; + } + validate() { + const className = this.constructor.name === 'DynamicModel' ? this.entity.constructor.name : this.constructor.name; + const errors = []; + const result = this.validatePrimaryKey(); + if (!result.valid) + errors.push(...result.errors); for (let attributeName in this.attributes) { const attribute = this.attributes[attributeName]; // Validate required attribute is set diff --git a/dist/src/table.d.ts.map b/dist/src/table.d.ts.map index 9cb7250..4c8ac73 100644 --- a/dist/src/table.d.ts.map +++ b/dist/src/table.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/table.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,UAAU,EAAE,oBAAoB,EAAC,MAAM,SAAS,CAAC;AAC5E,OAAO,EAEL,uBAAuB,EAAE,wBAAwB,EAAwB,0BAA0B,EACnG,cAAc,EAGf,MAAM,0BAA0B,CAAC;AAGlC,MAAM,CAAC,OAAO,OAAO,KAAK;IACxB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,MAAM,CAAiB;gBAEnB,MAAM,CAAC,EAAE,cAAc;IAYnC,MAAM,CAAC,OAAO;IAIP,OAAO;IAIP,SAAS,CAAC,CAAC,EAAE,QAAQ,UAAQ,GAAG,CAAC;IAKxC,MAAM,CAAC,SAAS,CAAC,QAAQ,UAAQ;IAK1B,uBAAuB,IAAI,oBAAoB;IAoB/C,aAAa;IAIb,qBAAqB;IAgCrB,oBAAoB,IAAI,uBAAuB;IA0B/C,UAAU,CAAC,UAAU,EAAE,UAAU;;;;;IAoB3B,MAAM;IAoBnB;;OAEG;IACU,QAAQ,IAAI,OAAO,CAAC,0BAA0B,CAAC;CAiB7D"} \ No newline at end of file +{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../src/table.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,UAAU,EAAE,oBAAoB,EAAC,MAAM,SAAS,CAAC;AAC5E,OAAO,EAEL,uBAAuB,EAAE,wBAAwB,EAAwB,0BAA0B,EACnG,cAAc,EAGf,MAAM,0BAA0B,CAAC;AAGlC,MAAM,CAAC,OAAO,OAAO,KAAK;IACxB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,MAAM,CAAiB;gBAEnB,MAAM,CAAC,EAAE,cAAc;IAYnC,MAAM,CAAC,OAAO;IAIP,OAAO;IAIP,SAAS,CAAC,CAAC,EAAE,QAAQ,UAAQ,GAAG,CAAC;IAKxC,MAAM,CAAC,SAAS,CAAC,QAAQ,UAAQ;IAK1B,uBAAuB,IAAI,oBAAoB;IAoB/C,aAAa;IAIb,qBAAqB;IAgCrB,oBAAoB,IAAI,uBAAuB;IA2B/C,UAAU,CAAC,UAAU,EAAE,UAAU;;;;;IAoB3B,MAAM;IAoBnB;;OAEG;IACU,QAAQ,IAAI,OAAO,CAAC,0BAA0B,CAAC;CAiB7D"} \ No newline at end of file diff --git a/dist/src/types.d.ts b/dist/src/types.d.ts index 91e6632..5035d64 100644 --- a/dist/src/types.d.ts +++ b/dist/src/types.d.ts @@ -2,11 +2,15 @@ import Entity from "./entity"; import Table from "./table"; import { CreateTableCommandOutput, DynamoDBClient } from "@aws-sdk/client-dynamodb"; import Model from "./model"; -export type EntityConstructor = new () => Entity; +export type EntityConstructor = { + name: string; + new (): Entity; +}; export type Entities = Record; export interface EntityAttributes extends Record { } export type Attributes = Record; +export type DTO = Record; export type AttributeDefinition = { type: string; required?: boolean; @@ -63,7 +67,7 @@ export type IDynamoRM = { getTables: () => TableConstructor[]; getModel: (modelName: string) => ModelConstructor; getModels: () => ModelConstructor[]; - model: (modelName: string, attributes?: Record) => Model; + model: (modelName: string, attributes?: Record) => Model & T; }; export type DynamoRMOptions = { tables: Array; diff --git a/dist/src/types.d.ts.map b/dist/src/types.d.ts.map index 4785474..bf99a8c 100644 --- a/dist/src/types.d.ts.map +++ b/dist/src/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index 72b1542..6e55eb9 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "types": "dist/index.d.ts", "files": ["/dist"], "scripts": { + "build": "tsc", + "build:watch": "tsc -w", "test": "jest", "test:watch": "jest --watch", "start": "docker compose up" diff --git a/src/dynamorm.ts b/src/dynamorm.ts index e3c2337..6e8853f 100644 --- a/src/dynamorm.ts +++ b/src/dynamorm.ts @@ -12,12 +12,12 @@ import {IDynamoRM, EntityConstructor, ModelConstructor, TableConstructor} from " */ export class DynamoRM { private readonly tables: Array; - private readonly models: Array + private readonly models: Array; private readonly client: DynamoDBClient; constructor(DB: Function) { this.tables = Reflect.getMetadata('tables', DB) - this.models = Reflect.getMetadata('models', DB) + this.models = Reflect.getMetadata('models', DB) || [] this.client = Reflect.getMetadata('client', DB); if (!this.tables || !this.client) { throw new Error('A dynamodb client and tables are required') @@ -73,7 +73,7 @@ export class DynamoRM { * @param modelName * @param attributes */ - model(modelName: string, attributes?:Record):Model { + model(modelName: string, attributes?:Record): Model & T { let Constructor:ModelConstructor = this.getModel(modelName); let model:Model; @@ -93,7 +93,7 @@ export class DynamoRM { model.fill(attributes); } - return model; + return model as Model&T; } } diff --git a/src/exceptions.ts b/src/exceptions.ts index da73358..acc4997 100644 --- a/src/exceptions.ts +++ b/src/exceptions.ts @@ -21,3 +21,9 @@ export class PrimaryKeyException extends DynamormException { super(message); } } + +export class ValidationError extends DynamormException { + constructor(public messages: string[]) { + super(messages); + } +} diff --git a/src/model.ts b/src/model.ts index 937a7ce..3a88f5d 100644 --- a/src/model.ts +++ b/src/model.ts @@ -3,6 +3,7 @@ import { AttributeDefinitions, Attributes, ModelConstructor, + PrimaryKey, } from "./types"; import { DeleteItemCommand, @@ -16,7 +17,7 @@ import { DynamormException, PrimaryKeyException, ServiceUnavailableException, - TableNotFoundException + TableNotFoundException, ValidationError } from "./exceptions"; import Table from "./table"; import Entity from "./entity"; @@ -24,13 +25,15 @@ import Entity from "./entity"; class Model { public name: string; protected table: Table; - + protected entity: Entity; protected attributes: AttributeDefinitions = {}; + public readonly primaryKey: PrimaryKey; constructor( private client: DynamoDBClient ) { this.table = new (Reflect.getMetadata('table', this.constructor)); - const entity:Entity = this.table.getEntity(true); - this.attributes = entity.getAttributeDefinitions(); + this.entity = this.table.getEntity(true); + this.attributes = this.entity.getAttributeDefinitions(); + this.primaryKey = this.table.getPrimaryKey(); for (let attribute in this.attributes) { Object.defineProperty(this, attribute, { @@ -84,11 +87,26 @@ class Model { throw new Error(`Attribute not found: ${attributeName}`) } + /** + * @description Gets PrimaryKey including values + */ + public getPrimaryKey(): PrimaryKey { + const primaryKeyDefinition = this.table.getPrimaryKeyDefinition(); + const primaryKey: PrimaryKey = { + pk: this.attributes[primaryKeyDefinition.pk.AttributeName].value, + sk: undefined + } + if (primaryKeyDefinition.sk) { + primaryKey.sk = this.attributes[primaryKeyDefinition.sk.AttributeName].value; + } + return primaryKey; + } + public getAttributes() { return this.attributes; } - public getAttributeValues() { + public getAttributeValues(omitUndefined: boolean = true) { return Object.keys(this.attributes).reduce((attributes, attribute) => { attributes[attribute] = this.attributes[attribute].value; return attributes; @@ -104,6 +122,9 @@ class Model { } public async save(): Promise { + const { valid, errors} = this.validate(); + if (!valid) throw new ValidationError(errors); + const input = this.toPutCommandInput(); const command = new PutItemCommand(input); let result: PutItemCommandOutput; @@ -130,19 +151,31 @@ class Model { return result; } + public async fresh(): Promise { + const {valid, errors} = this.validatePrimaryKey(); + if (!valid) { + throw new PrimaryKeyException(errors[0]); + } + const model = await this.find(); + if (model === undefined) return false; + for (let attributeName in this.attributes) { + if (model.attributes.hasOwnProperty(attributeName)) { + this.attributes[attributeName].value = model.attributes[attributeName].value + } + } + return true; + } + public async find(pk?: string, sk?: string): Promise{ const primaryKeyDefinition = this.table.getPrimaryKeyDefinition(); if (primaryKeyDefinition.sk && !sk) { throw new PrimaryKeyException(`Failed to fetch item. Primary key requires partition key and sort key on ${this.table.constructor.name}`) } - const primaryKey = { - pk: pk || this.table.getPrimaryKey().pk, - sk: sk || this.table.getPrimaryKey().sk, - } + const primaryKeyValues = this.getPrimaryKey(); const input: GetItemCommandInput = { TableName: this.table.getName(), // @ts-ignore - Key: this.table.toInputKey(primaryKey) + Key: this.table.toInputKey(primaryKeyValues) } const command = new GetItemCommand(input); let result: GetItemCommandOutput; @@ -199,25 +232,37 @@ class Model { } } - public validate(): {valid: boolean, errors: string[]} { - const errors = []; + private validatePrimaryKey() { const primaryKey = this.table.getPrimaryKey(); + const className = this.constructor.name === 'DynamicModel' ? this.entity.constructor.name : this.constructor.name; + const errors = []; // Validate partition key defined on table is also defined as attribute in respective entity if (!this.attributes.hasOwnProperty(primaryKey.pk)) { - errors.push(`Partition key "${primaryKey.pk}" is not defined in ${this.getEntity().constructor.name}`) + errors.push(`Partition key "${primaryKey.pk}" is not defined in ${className}`) } // Validate partition key defined on table is set in respective entity if (this.attributes[primaryKey.pk].value === undefined) { - errors.push(`Partition key "${primaryKey.pk}" is not set in ${this.constructor.name}`) + errors.push(`Partition key "${primaryKey.pk}" is not set in ${className}`) } // Validate sort key defined on table is also defined as attribute in respective entity if (primaryKey.sk && !this.attributes.hasOwnProperty(primaryKey.sk)) { - errors.push(`Sort key "${primaryKey.sk}" is not defined in ${this.getEntity().constructor.name}`) + errors.push(`Sort key "${primaryKey.sk}" is not defined in ${className}`) } // Validate sort key defined on table is set in respective entity - if (this.attributes[primaryKey.sk].value === undefined) { + if (primaryKey.sk && this.attributes[primaryKey.sk].value === undefined) { errors.push(`Sort key "${primaryKey.sk}" is not set in ${this.constructor.name}`) } + return { + valid: !errors.length, + errors + } + } + + public validate(): {valid: boolean, errors: string[]} { + const errors = []; + + const result = this.validatePrimaryKey(); + if (!result.valid) errors.push(...result.errors); for (let attributeName in this.attributes) { const attribute = this.attributes[attributeName]; diff --git a/src/table.ts b/src/table.ts index 90de093..92a6845 100644 --- a/src/table.ts +++ b/src/table.ts @@ -126,6 +126,7 @@ export default class Table { } } + public toInputKey(primaryKey: PrimaryKey) { const {pk, sk} = this.getPrimaryKeyDefinition(); if (!sk && primaryKey.sk) { diff --git a/src/types.ts b/src/types.ts index cdccc8e..5c3fc82 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,13 +3,18 @@ import Table from "./table"; import {CreateTableCommandOutput, DynamoDBClient} from "@aws-sdk/client-dynamodb"; import Model from "./model"; -export type EntityConstructor = new() => Entity; +export type EntityConstructor = { + name: string, + new(): Entity +} export type Entities = Record export interface EntityAttributes extends Record {} +// @depricated - Use DTO type instead export type Attributes = Record; +export type DTO = Record export type AttributeDefinition = { type: string, required?: boolean, @@ -77,7 +82,7 @@ export type IDynamoRM = { getTables: () => TableConstructor[], getModel: (modelName: string) => ModelConstructor, getModels: () => ModelConstructor[], - model: (modelName: string, attributes?: Record) => Model, + model: (modelName: string, attributes?: Record) => Model & T, } export type DynamoRMOptions = { diff --git a/tsconfig.json b/tsconfig.json index 8e4b6c6..06a9433 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,5 +11,5 @@ "declarationMap": true, }, - "include": ["*.ts", "src/**/*", "*.ts"] + "include": ["*.ts", "src/**/*"] }