diff --git a/.eslintrc b/.eslintrc index 52cc21c..841497d 100755 --- a/.eslintrc +++ b/.eslintrc @@ -172,6 +172,7 @@ } }, "parserOptions": { + "project": ['./tsconfig.json'], "ecmaFeatures": { "jsx": true } diff --git a/README.md b/README.md index 6bef1f7..782b4e3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ -# Functional Models ORM Dynamo +# Functional Models ORM Dynamo ![Unit Tests](https://github.com/monolithst/functional-models-orm-dynamo/actions/workflows/ut.yml/badge.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/monolithst/functional-models-orm-dynamo/badge.svg?branch=master)](https://coveralls.io/github/monolithst/functional-models-orm-dynamo?branch=master) +Provides an functional-models-orm datastore provider for AWS Dynamo. +## AWS SDK 3.0 + +This library now supports AWS SDK 3.0 as an injectable library. ## Run Feature Tests + To run the feature tests, you need to set up an actual Dynamodb table within AWS and then call cucumber lik the following: -```npm run feature-tests -- --world-parameters '{"awsRegion":"YOUR_REGION", "testTable":"YOUR_TEST_TABLE"}'``` +`npm run feature-tests -- --world-parameters '{"awsRegion":"YOUR_REGION", "testTable":"YOUR_TEST_TABLE"}'` IMPORTANT WORD OF CAUTION: I would not attempt to use this table for anything other than this feature tests, as the table is completely deleted without remorse. diff --git a/features/stepDefinitions/steps.js b/features/stepDefinitions/steps.mjs similarity index 65% rename from features/stepDefinitions/steps.js rename to features/stepDefinitions/steps.mjs index 7c511fd..c43a585 100644 --- a/features/stepDefinitions/steps.js +++ b/features/stepDefinitions/steps.mjs @@ -1,25 +1,34 @@ -const assert = require('chai').assert -const { Before, After, Given, When, Then } = require('cucumber') -const { TextProperty, Model, UniqueId } = require('functional-models') -const { ormQuery, orm } = require('functional-models-orm') -const createDatastoreProvider = require('../../dist/datastoreProvider').default +import { assert } from 'chai' +import { Before, After, Given, When, Then } from '@cucumber/cucumber' +import functionalModels from 'functional-models' +import { ormQuery, orm } from 'functional-models-orm' +import createDatastoreProvider from '../../dist/datastoreProvider.js' +import * as dynamo from '@aws-sdk/client-dynamodb' +import * as libDynamo from '@aws-sdk/lib-dynamodb' -const createDynamoDatastoreProvider = (context) => { +const { TextProperty, Model, UniqueId } = functionalModels + +const createDynamoDatastoreProvider = context => { if (!context.parameters.testTable) { - throw new Error(`Must include a testing table that exists in the world parameters.`) + throw new Error( + `Must include a testing table that exists in the world parameters.` + ) } if (!context.parameters.awsRegion) { throw new Error(`Must include awsRegion in the world parameters.`) } - this.table = context.parameters.testTable - return createDatastoreProvider({ - AWS: require('aws-sdk'), + context.table = context.parameters.testTable + return createDatastoreProvider.default({ + aws3: { + ...dynamo, + ...libDynamo, + }, dynamoOptions: { - region: context.parameters.awsRegion + region: context.parameters.awsRegion, }, getTableNameForModel: () => { - return `${this.table}` - } + return `${context.table}` + }, }) } @@ -33,7 +42,7 @@ const MODELS = { { properties: { name: TextProperty(), - } + }, }, ], } @@ -44,28 +53,36 @@ const MODEL_DATA = { name: 'test-name', }, SearchResult1: { - instances: [{ - id: 'test-id', - name: 'test-name', - }], - page: null + instances: [ + { + id: 'test-id', + name: 'test-name', + }, + ], + page: null, }, EmptyModel: {}, - 'undefined': undefined, + undefined: undefined, } const QUERIES = { - SearchQuery1: ormQuery.ormQueryBuilder() + SearchQuery1: ormQuery + .ormQueryBuilder() .property('name', 'test-name') .take(1) .compile(), } const _emptyDatastoreProvider = async (model, datastoreProvider) => { - await datastoreProvider.search(model, ormQuery.ormQueryBuilder().compile()) - .then(async obj => Promise.all(obj.instances.map(x => { - return model.create(x).delete() - }))) + await datastoreProvider + .search(model, ormQuery.ormQueryBuilder().compile()) + .then(async obj => + Promise.all( + obj.instances.map(x => { + return model.create(x).delete() + }) + ) + ) } Given('orm using the {word}', function (store) { @@ -78,7 +95,7 @@ Given('orm using the {word}', function (store) { this.datastoreProvider = store }) -Given('the datastore is emptied of models', function() { +Given('the datastore is emptied of models', function () { return _emptyDatastoreProvider(this.model, this.datastoreProvider) }) @@ -105,13 +122,16 @@ When('instances of the model are created with {word}', function (dataKey) { this.instances = MODEL_DATA[dataKey].map(this.model.create) }) -When('an instance of the model is created with {word}', async function (dataKey) { - const data = MODEL_DATA[dataKey] - if (!data) { - throw new Error(`${dataKey} did not result in a data object.`) +When( + 'an instance of the model is created with {word}', + async function (dataKey) { + const data = MODEL_DATA[dataKey] + if (!data) { + throw new Error(`${dataKey} did not result in a data object.`) + } + this.modelInstance = this.model.create(data) } - this.modelInstance = this.model.create(data) -}) +) When('save is called on the instances', function () { return Promise.all(this.instances.map(x => x.save())) @@ -122,9 +142,7 @@ When('save is called on the model', function () { }) When('delete is called on the model', function () { - return this.modelInstance - .delete() - .then(x => (this.deleteResult = x)) + return this.modelInstance.delete().then(x => (this.deleteResult = x)) }) When("the datastore's retrieve is called with values", function (table) { @@ -144,7 +162,6 @@ When("the datastore's delete is called with modelInstance", function () { When("the datastore's search is called with {word}", function (key) { const query = QUERIES[key] return this.datastoreProvider.search(this.model, query).then(async obj => { - console.log(obj) this.result = obj }) }) diff --git a/package.json b/package.json index 03f45f2..85e576e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "functional-models-orm-dynamo", - "version": "2.0.4", + "version": "2.1.0", "description": "An implmentation of functional-models-orm for dynamodb.", "main": "index.js", "types": "index.d.ts", "scripts": { - "test": "mocha -r ts-node/register test/**/*.test.ts", + "test": "export TS_NODE_TRANSPILE_ONLY=true && export TS_NODE_PROJECT='./tsconfig.test.json' && mocha -r ts-node/register 'test/**/*.test.ts'", "test:coverage": "nyc npm run test", "feature-tests": "./node_modules/.bin/cucumber-js", "coverage": "nyc --all --reporter=lcov npm test", @@ -49,32 +49,37 @@ "report-dir": "coverage" }, "dependencies": { - "functional-models": "^2.0.5", + "@aws-sdk/client-dynamodb": "^3.529.1", + "@aws-sdk/lib-dynamodb": "^3.529.1", + "functional-models": "^2.0.14", "functional-models-orm": "^2.0.17", "lodash": "^4.17.21" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.1", - "@types/chai": "^4.3.0", - "@types/lodash": "^4.14.177", - "@types/mocha": "^9.0.0", - "@types/node": "^16.11.7", - "@types/proxyquire": "^1.3.28", - "@types/sinon": "^10.0.6", - "@typescript-eslint/eslint-plugin": "^5.7.0", + "@cucumber/cucumber": "^9.6.0", + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/chai": "^4.3.12", + "@types/lodash": "^4.17.0", + "@types/mocha": "^10.0.6", + "@types/node": "^20.11.27", + "@types/proxyquire": "^1.3.31", + "@types/sinon": "^17.0.3", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", "babel-eslint": "^10.1.0", - "chai": "^4.3.0", - "cucumber": "^7.0.0-rc.0", - "eslint": "^7.19.0", - "eslint-config-prettier": "^7.2.0", - "eslint-plugin-functional": "^3.2.1", - "eslint-plugin-import": "^2.22.1", - "mocha": "^8.2.1", + "chai": "^4.2.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-functional": "^6.1.1", + "eslint-plugin-import": "^2.29.1", + "mocha": "^9.1.3", "nyc": "^15.1.0", + "prettier": "^3.2.5", "proxyquire": "^2.1.3", - "sinon": "^11.1.2", - "ts-node": "^10.4.0", - "typescript": "^4.5.2" + "sinon": "^17.0.1", + "ts-mocha": "^10.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.2" }, "homepage": "https://github.com/monolithst/functional-models-orm-dynamo#readme" } diff --git a/src/constants.ts b/src/constants.ts index b45cd23..2369550 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -581,7 +581,4 @@ const RESERVED_KEYWORDS = { const SCAN_RETURN_THRESHOLD = 1000 -export { - RESERVED_KEYWORDS, - SCAN_RETURN_THRESHOLD, -} +export { RESERVED_KEYWORDS, SCAN_RETURN_THRESHOLD } diff --git a/src/datastoreProvider.ts b/src/datastoreProvider.ts index 50dd5f6..47c525a 100644 --- a/src/datastoreProvider.ts +++ b/src/datastoreProvider.ts @@ -1,26 +1,41 @@ import { get, merge } from 'lodash' -import { Model, FunctionalModel, ModelInstance, PrimaryKeyType } from 'functional-models/interfaces' +import { + Model, + FunctionalModel, + ModelInstance, + PrimaryKeyType, +} from 'functional-models/interfaces' import { OrmQuery, DatastoreProvider } from 'functional-models-orm/interfaces' -// This is a BS error -// eslint-disable-next-line no-unused-vars import { getTableNameForModel as defaultTableModelName } from './utils' -import dynamoClient from './dynamoClient' import queryBuilder from './queryBuilder' import { SCAN_RETURN_THRESHOLD } from './constants' type DatastoreProviderInputs = { - AWS: any, - dynamoOptions: object, - getTableNameForModel: (m: Model) => string, - createUniqueId: ((s: any) => string)|undefined, + aws3: Aws3Client + dynamoOptions: DynamoOptions + getTableNameForModel?: (m: Model) => string + createUniqueId?: ((s: any) => string) | undefined +} + +type Aws3Client = { + DynamoDBClient: any + DynamoDBDocumentClient: any + PutCommand: any + GetCommand: any + DeleteCommand: any + ScanCommand: any +} + +type DynamoOptions = { + region: string } const dynamoDatastoreProvider = ({ - AWS, + aws3, dynamoOptions, getTableNameForModel = defaultTableModelName, createUniqueId = undefined, -}: DatastoreProviderInputs) : DatastoreProvider => { +}: DatastoreProviderInputs): DatastoreProvider => { const _doSearchUntilThresholdOrNoLastEvaluatedKey = ( dynamo: any, tableName: string, @@ -28,42 +43,45 @@ const dynamoDatastoreProvider = ({ oldInstancesFound = [] ) => { const query = queryBuilder({ createUniqueId })(tableName, ormQuery) - return dynamo - .scan(query) - .promise() - .then((data: any) => { - const instances = data.Items.map((item: object) => { - return Object.entries(item).reduce((acc, [key, obj]) => { - return merge(acc, { [key]: obj}) - }, {}) - }).concat(oldInstancesFound) + const command = new aws3.ScanCommand(query) + return dynamo.send(command).then((data: any) => { + const instances = data.Items.map((item: object) => { + return Object.entries(item).reduce((acc, [key, obj]) => { + return merge(acc, { [key]: obj }) + }, {}) + }).concat(oldInstancesFound) - const usingTake = ormQuery.take && ormQuery.take > 0 - const take = usingTake ? ormQuery.take : SCAN_RETURN_THRESHOLD - const lastEvaluatedKey = get(data, 'LastEvaluatedKey', null) - /* + const usingTake = ormQuery.take && ormQuery.take > 0 + const take = usingTake ? ormQuery.take : SCAN_RETURN_THRESHOLD + const lastEvaluatedKey = get(data, 'LastEvaluatedKey', null) + /* We want to keep scanning until we've met our threshold OR there is no more keys to evaluate OR we have a "take" and we've hit our max. */ - const stopForThreshold = instances.length > take - const stopForNoMore = !lastEvaluatedKey - if (stopForThreshold || stopForNoMore) { - return { - instances: instances.slice(0, take), - page: usingTake ? null : lastEvaluatedKey, - } + const stopForThreshold = instances.length > take + const stopForNoMore = !lastEvaluatedKey + if (stopForThreshold || stopForNoMore) { + return { + instances: instances.slice(0, take), + page: usingTake ? null : lastEvaluatedKey, } - const newQuery = merge(ormQuery, { - page: lastEvaluatedKey, - }) - return _doSearchUntilThresholdOrNoLastEvaluatedKey( - dynamo, - tableName, - newQuery, - instances - ) + } + const newQuery = merge(ormQuery, { + page: lastEvaluatedKey, }) + return _doSearchUntilThresholdOrNoLastEvaluatedKey( + dynamo, + tableName, + newQuery, + instances + ) + }) + } + + const _getDocClient = () => { + const dynamo = new aws3.DynamoDBClient(dynamoOptions) + return aws3.DynamoDBDocumentClient.from(dynamo) } const search = ( @@ -72,8 +90,7 @@ const dynamoDatastoreProvider = ({ ) => { return Promise.resolve().then(async () => { const tableName = getTableNameForModel(model) - const dynamo = new AWS.DynamoDB(dynamoOptions) - const docClient = new AWS.DynamoDB.DocumentClient({ service: dynamo }) + const docClient = _getDocClient() return _doSearchUntilThresholdOrNoLastEvaluatedKey( docClient, tableName, @@ -88,9 +105,13 @@ const dynamoDatastoreProvider = ({ ) => { return Promise.resolve().then(() => { const tableName = getTableNameForModel(model) - const client = dynamoClient({ tableName, dynamoOptions, AWS }) + const docClient = _getDocClient() const primaryKeyName = model.getPrimaryKeyName() - return client.get({ key: { [primaryKeyName]: `${id}` }}) + const command = new aws3.GetCommand({ + TableName: tableName, + Key: { [primaryKeyName]: `${id}` }, + }) + return docClient.send(command).then((x: any) => x.Item as T) }) } @@ -99,11 +120,17 @@ const dynamoDatastoreProvider = ({ ) => { return Promise.resolve().then(async () => { const tableName = getTableNameForModel(instance.getModel()) - const client = dynamoClient({ tableName, AWS, dynamoOptions }) + const docClient = _getDocClient() + const primaryKeyName = instance.getModel().getPrimaryKeyName() const data = await instance.toObj() - const key = instance.getModel().getPrimaryKeyName() - await client.update({ key: { [key]: `${(data as any)[key]}` }, item: data }) - return data + const key = `${(data as any)[primaryKeyName]}` + const keyObj = { [primaryKeyName]: key } + const command = new aws3.PutCommand({ + TableName: tableName, + Key: keyObj, + Item: { ...data, ...keyObj }, + }) + return docClient.send(command).then(() => data) }) } @@ -112,10 +139,15 @@ const dynamoDatastoreProvider = ({ ) => { return Promise.resolve().then(async () => { const tableName = getTableNameForModel(instance.getModel()) - const client = dynamoClient({ tableName, AWS, dynamoOptions }) - const id = await instance.getPrimaryKey() + const docClient = _getDocClient() const primaryKeyName = instance.getModel().getPrimaryKeyName() - await client.delete({ key: { [primaryKeyName]: `${id}` } }) + const id = await instance.getPrimaryKey() + const keyObj = { [primaryKeyName]: `${id}` } + const command = new aws3.DeleteCommand({ + TableName: tableName, + Key: keyObj, + }) + return docClient.send(command).then(() => undefined) }) } diff --git a/src/dynamoClient.ts b/src/dynamoClient.ts deleted file mode 100644 index 1ee1ce9..0000000 --- a/src/dynamoClient.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ModelInstanceInputData, FunctionalModel } from 'functional-models/interfaces' - -const dynamoClient = ({ tableName, dynamoOptions, AWS }:{ AWS: any, tableName: string, dynamoOptions: object}) => { - const dynamo = new AWS.DynamoDB(dynamoOptions) - const docClient = new AWS.DynamoDB.DocumentClient({ service: dynamo }) - - const get = async ({ key }:{ key: {[s: string]: string}}) => { - const params = { - TableName: tableName, - Key: key, - } - - return docClient - .get(params) - .promise() - .then((data: any) => data.Item as ModelInstanceInputData) - } - - const update = async ({ key, item }:{ key: {[s: string]: string}, item: object}) => { - const params = { - TableName: tableName, - Item: { ...item, ...key }, - } - - return docClient.put(params).promise() - } - - const deleteObj = async ({ key }:{ key: {[s: string]: string}}) => { - const params = { - TableName: tableName, - Key: key, - } - - return docClient.delete(params).promise() - } - - return { - get, - update, - delete: deleteObj, - } -} - -export default dynamoClient diff --git a/src/interfaces.ts b/src/interfaces.ts deleted file mode 100644 index 7213907..0000000 --- a/src/interfaces.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { -} diff --git a/src/queryBuilder.ts b/src/queryBuilder.ts index 0d72ad6..d690271 100644 --- a/src/queryBuilder.ts +++ b/src/queryBuilder.ts @@ -1,10 +1,14 @@ +import { randomUUID } from 'crypto' import { merge, get, identity } from 'lodash' -import { OrmQuery, OrmQueryStatement, PropertyStatement } from 'functional-models-orm/interfaces' +import { + OrmQuery, + OrmQueryStatement, + PropertyStatement, +} from 'functional-models-orm/interfaces' import { ORMType } from 'functional-models-orm/constants' -import { v4 } from 'uuid' -type DynamoOrmStatement = OrmQueryStatement & { myUniqueId: string} -type DynamoPropertyStatement = PropertyStatement & {myUniqueId: string} +type DynamoOrmStatement = OrmQueryStatement & { myUniqueId: string } +type DynamoPropertyStatement = PropertyStatement & { myUniqueId: string } const COMPARISONS = { EQ: 'EQ', @@ -19,18 +23,17 @@ const DATA_TYPES = { OBJECT: 'M', } -const ORM_TYPE_TO_DYNAMO_TYPE : {[key in ORMType]: string} = { +const ORM_TYPE_TO_DYNAMO_TYPE: { [key in ORMType]: string } = { string: DATA_TYPES.STRING, number: DATA_TYPES.NUMBER, date: DATA_TYPES.STRING, object: DATA_TYPES.OBJECT, boolean: DATA_TYPES.BOOLEAN, -} - +} -const _idGenerator = (existingFunc: any): ((s: any)=>string) => { +const _idGenerator = (existingFunc: any): ((s: any) => string) => { if (!existingFunc) { - return (_: string) => v4().replace(/-/gu, '').replace(/_/gu, '') + return (_: string) => randomUUID().replace(/-/gu, '').replace(/_/gu, '') } return existingFunc } @@ -39,58 +42,70 @@ const _isPropertyStatement = (obj: any): obj is DynamoPropertyStatement => { return get(obj, 'type', '') === 'property' } -const queryBuilder = ({ createUniqueId }:{ createUniqueId?: ((s: any)=>string)} ) => (tableName: string, queryData: OrmQuery) => { - const idGenerator = _idGenerator(createUniqueId) - const properties = queryData.properties - const page = queryData.page - - const flowObjs : DynamoOrmStatement[] = queryData.chain.map(x => ({ - ...x, - myUniqueId: idGenerator(x), - })) - const realProperties : DynamoPropertyStatement[] = flowObjs.filter(_isPropertyStatement).map(x=>x as DynamoPropertyStatement) - - const startKey = page - ? { - ExclusiveStartKey: page, - FilterExpression: undefined, - ExpressionAttributeNames: undefined, - ExpressionAttributeValues: undefined, - } - : {} +const queryBuilder = + ({ createUniqueId }: { createUniqueId?: (s: any) => string }) => + (tableName: string, queryData: OrmQuery) => { + const idGenerator = _idGenerator(createUniqueId) + const properties = queryData.properties + const page = queryData.page + + const flowObjs: DynamoOrmStatement[] = queryData.chain.map(x => ({ + ...x, + myUniqueId: idGenerator(x), + })) + const realProperties: DynamoPropertyStatement[] = flowObjs + .filter(_isPropertyStatement) + .map(x => x as DynamoPropertyStatement) + + const startKey = page + ? { + ExclusiveStartKey: page, + FilterExpression: undefined, + ExpressionAttributeNames: undefined, + ExpressionAttributeValues: undefined, + } + : {} - if (Object.keys(properties).length < 1) { - return { - ...startKey, - TableName: tableName, + if (Object.keys(properties).length < 1) { + return { + ...startKey, + TableName: tableName, + } } - } - const propKeyToKey : {[s: string]: string}= realProperties.reduce((acc, obj) => { - return merge(acc, { - [obj.myUniqueId]: `my${obj.myUniqueId - .replace('-', '') - .replace('_', '')}`, - }) - }, {}) - - const propNametoExpressionAttribute : {[s: string]: string} = realProperties.reduce((acc, obj) => { - const newKey = `#my${obj.myUniqueId.replace('-', '').replace('_', '')}` - return merge(acc, { [obj.myUniqueId]: newKey }) - }, {}) - - const expressionAttributeNames = realProperties.reduce((acc, obj) => { - return merge(acc, { - [propNametoExpressionAttribute[obj.myUniqueId]]: obj.name, - }) - }, {}) - - const _getEqualitySymbol = (flowObj: DynamoOrmStatement) => { - return get(flowObj, 'options.equalitySymbol') - } + const propKeyToKey: { [s: string]: string } = realProperties.reduce( + (acc, obj) => { + return merge(acc, { + [obj.myUniqueId]: `my${obj.myUniqueId + .replace('-', '') + .replace('_', '')}`, + }) + }, + {} + ) + + const propNametoExpressionAttribute: { [s: string]: string } = + realProperties.reduce((acc, obj) => { + const newKey = `#my${obj.myUniqueId.replace('-', '').replace('_', '')}` + return merge(acc, { [obj.myUniqueId]: newKey }) + }, {}) + + const expressionAttributeNames = realProperties.reduce((acc, obj) => { + return merge(acc, { + [propNametoExpressionAttribute[obj.myUniqueId]]: obj.name, + }) + }, {}) + + const _getEqualitySymbol = (flowObj: DynamoOrmStatement) => { + return get(flowObj, 'options.equalitySymbol') + } - // I broke this out into its own function, because typescript was having a really hard time with it. - const _combineStatementAndGetPreviousOrm = (acc: string, previous: DynamoOrmStatement|undefined, flowObj: DynamoOrmStatement) : [string, DynamoOrmStatement|undefined]=> { + // I broke this out into its own function, because typescript was having a really hard time with it. + const _combineStatementAndGetPreviousOrm = ( + acc: string, + previous: DynamoOrmStatement | undefined, + flowObj: DynamoOrmStatement + ): [string, DynamoOrmStatement | undefined] => { if (flowObj.type === 'property') { const key = flowObj.myUniqueId const expressionAttribute = propNametoExpressionAttribute[key] @@ -104,12 +119,14 @@ const queryBuilder = ({ createUniqueId }:{ createUniqueId?: ((s: any)=>string)} ] } return [ - acc + `${expressionAttribute} ${_getEqualitySymbol(flowObj)} :${propKey}`, + acc + + `${expressionAttribute} ${_getEqualitySymbol(flowObj)} :${propKey}`, flowObj, ] } return [ - acc + `${expressionAttribute} ${_getEqualitySymbol(flowObj)} :${propKey}`, + acc + + `${expressionAttribute} ${_getEqualitySymbol(flowObj)} :${propKey}`, flowObj, ] } else if (flowObj.type === 'and') { @@ -128,51 +145,51 @@ const queryBuilder = ({ createUniqueId }:{ createUniqueId?: ((s: any)=>string)} return [acc + ' OR ', flowObj] } return [acc, flowObj] - } - - const _createFilterExpression = () : string => { - return flowObjs.reduce( - ([acc, previous]: [string, DynamoOrmStatement|undefined], flowObj: DynamoOrmStatement) => { - return _combineStatementAndGetPreviousOrm(acc, previous, flowObj) - }, - ['', undefined] - )[0] - } - - const filterExpression = _createFilterExpression() + } - const _getStringValue = (value: any) => { - return value === null || value === undefined ? '' : value - } + const _createFilterExpression = (): string => { + return flowObjs.reduce( + ( + [acc, previous]: [string, DynamoOrmStatement | undefined], + flowObj: DynamoOrmStatement + ) => { + return _combineStatementAndGetPreviousOrm(acc, previous, flowObj) + }, + ['', undefined] + )[0] + } - const DATA_TYPE_TO_METHOD = { - [DATA_TYPES.STRING]: _getStringValue, - [DATA_TYPES.NUMBER]: identity, - [DATA_TYPES.BOOLEAN]: identity, - [DATA_TYPES.NULL]: identity, - [DATA_TYPES.ARRAY]: identity, - [DATA_TYPES.OBJECT]: identity, - } + const filterExpression = _createFilterExpression() - const expressionAttributeValues = realProperties.reduce((acc, obj) => { - const dataType = ORM_TYPE_TO_DYNAMO_TYPE[obj.valueType] - const valueMethod = DATA_TYPE_TO_METHOD[dataType] - const value = valueMethod(obj.value) - return merge(acc, { - [`:${propKeyToKey[obj.myUniqueId]}`]: value, - }) - }, {}) - - return { - ...startKey, - TableName: tableName, - FilterExpression: filterExpression, - ExpressionAttributeNames: expressionAttributeNames, - ExpressionAttributeValues: expressionAttributeValues, - } -} + const _getStringValue = (value: any) => { + return value === null || value === undefined ? '' : value + } + const DATA_TYPE_TO_METHOD = { + [DATA_TYPES.STRING]: _getStringValue, + [DATA_TYPES.NUMBER]: identity, + [DATA_TYPES.BOOLEAN]: identity, + [DATA_TYPES.NULL]: identity, + [DATA_TYPES.ARRAY]: identity, + [DATA_TYPES.OBJECT]: identity, + } + const expressionAttributeValues = realProperties.reduce((acc, obj) => { + const dataType = ORM_TYPE_TO_DYNAMO_TYPE[obj.valueType] + const valueMethod = DATA_TYPE_TO_METHOD[dataType] + const value = valueMethod(obj.value) + return merge(acc, { + [`:${propKeyToKey[obj.myUniqueId]}`]: value, + }) + }, {}) + return { + ...startKey, + TableName: tableName, + FilterExpression: filterExpression, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: expressionAttributeValues, + } + } export default queryBuilder diff --git a/src/utils.ts b/src/utils.ts index 8daf7c2..fe5f848 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,4 @@ const getTableNameForModel = (model: Model) => { return model.getName().toLowerCase().replace('_', '-').replace(' ', '-') } -export { - getTableNameForModel, -} +export { getTableNameForModel } diff --git a/test/commonMocks.ts b/test/commonMocks.ts index b8ff447..b189252 100644 --- a/test/commonMocks.ts +++ b/test/commonMocks.ts @@ -1,82 +1,62 @@ import sinon from 'sinon' -const createAwsMocks = () => { - const get = sinon.stub().returns({ promise: () => Promise.resolve() }) - const scan = sinon.stub().returns({ promise: () => Promise.resolve() }) - const put = sinon.stub().returns({ promise: () => Promise.resolve() }) - const deleteObj = sinon.stub().returns({ promise: () => Promise.resolve() }) - const constructor = sinon.stub().returns({ promise: () => Promise.resolve() }) - - class DocumentClient { - public constructor(...args: any[]) { - constructor(...args) - } - get(...args: any[]) { - return get(...args) - } - scan(...args: any[]) { - return scan(...args) - } - put(...args: any[]) { - return put(...args) - } - delete(...args: any[]) { - return deleteObj(...args) +const createAws3MockClient = () => { + const dynamoDbClient = sinon.stub() + const sendSinon = sinon.stub().resolves({ Items: [] }) + class DynamoDBClient { + constructor(...args: any) { + dynamoDbClient(...args) + } + public send(...args: any) { + return sendSinon(...args) + } + static sinon = dynamoDbClient + static sendSinon = sendSinon + } + const dynamoDbDocumentClientSend = sinon.stub().resolves({ Items: [] }) + const DynamoDBDocumentClient = { + from: sinon.stub().returns({ + send: dynamoDbDocumentClientSend, + }), + sendSinon: dynamoDbDocumentClientSend, + } + const putCommand = sinon.stub() + class PutCommand { + constructor(...args: any) { + putCommand(...args) } + static sinon = putCommand } - - // @ts-ignore - DocumentClient.constructor = constructor - // @ts-ignore - DocumentClient.get = get - // @ts-ignore - DocumentClient.put = put - // @ts-ignore - DocumentClient.scan = scan - // @ts-ignore - DocumentClient.delete = deleteObj - - const dynamoConstructor = sinon.stub() - const scan2 = sinon.stub() - class DynamoDB { - public constructor(...args: any[]) { - dynamoConstructor(...args) + const getCommand = sinon.stub() + class GetCommand { + constructor(...args: any) { + getCommand(...args) } - - public scan(...args: any[]) { - return scan2(...args) + static sinon = getCommand + } + const deleteCommand = sinon.stub() + class DeleteCommand { + constructor(...args: any) { + deleteCommand(...args) } + static sinon = deleteCommand } - // @ts-ignore - DynamoDB.theConstructor = dynamoConstructor - // @ts-ignore - DynamoDB.DocumentClient = DocumentClient - // @ts-ignore - DynamoDB.scan = scan2 - const AWS = { - DynamoDB, + const scanCommand = sinon.stub() + class ScanCommand { + constructor(...args: any) { + scanCommand(...args) + } + static sinon = scanCommand } - return AWS -} -const createDynamoClient = () => { - const get = sinon.stub().resolves({}) - const update = sinon.stub().resolves({}) - const deleteObj = sinon.stub().resolves({}) - const client = () => { - return { - delete: deleteObj, - get, - update, - } + return { + DynamoDBClient, + DynamoDBDocumentClient, + PutCommand, + GetCommand, + DeleteCommand, + ScanCommand, } - client.get = get - client.update = update - client.delete = deleteObj - return client } -export { - createAwsMocks, - createDynamoClient, -} +export { createAws3MockClient } diff --git a/test/src/datastoreProvider.test.ts b/test/src/datastoreProvider.test.ts index 65711cc..83a602b 100644 --- a/test/src/datastoreProvider.test.ts +++ b/test/src/datastoreProvider.test.ts @@ -1,11 +1,13 @@ import { assert } from 'chai' import sinon from 'sinon' import proxyquire from 'proxyquire' -import { Model, ModelInstance } from 'functional-models/interfaces' +import { Model, FunctionalModel } from 'functional-models/interfaces' +import { OrmModel, OrmModelInstance } from 'functional-models-orm/interfaces' import { ormQueryBuilder } from 'functional-models-orm/ormQuery' -import { createAwsMocks, createDynamoClient } from '../commonMocks' +import { createAws3MockClient } from '../commonMocks' +import createDatastoreProvider from '../../src/datastoreProvider' -const createTestModel1 = ({ id, name }:{id: string, name: string}) => { +const createTestModel1 = ({ id, name }: { id: string; name: string }) => { return { get: { name: () => name, @@ -18,39 +20,24 @@ const createTestModel1 = ({ id, name }:{id: string, name: string}) => { return { getName: () => 'TestModel1', getPrimaryKeyName: () => 'id', - } as Model - } - } as unknown as ModelInstance + } as OrmModel + }, + } as unknown as OrmModelInstance> } -const createTestModel2 = ({ notId, name }: any) => ({ - get: { - notId: () => notId, - name: () => name, - }, - toObj: () => Promise.resolve({ notId, name }), - getPrimaryKey: () => notId, - getModel: () => ({ - getName: () => 'TestModel1', - getPrimaryKeyName: () => 'notId', - }), -}) as unknown as ModelInstance - -const setupMocks = () => { - const AWS = createAwsMocks() - const dynamoClient = createDynamoClient() - const datastoreProvider = proxyquire('../../src/datastoreProvider', { - './dynamoClient': { - default: dynamoClient, - } - }) - - return { - datastoreProvider: datastoreProvider.default, - dynamoClient, - AWS, - } -} +const createTestModel2 = ({ notId, name }: any) => + ({ + get: { + notId: () => notId, + name: () => name, + }, + toObj: () => Promise.resolve({ notId, name }), + getPrimaryKey: () => notId, + getModel: () => ({ + getName: () => 'TestModel1', + getPrimaryKeyName: () => 'notId', + }), + }) as unknown as OrmModelInstance> const _createDynamoStringResult = (key: string, value: string) => { return { @@ -70,70 +57,100 @@ const _createDynamoNullResult = (key: string) => { } } -describe('/src/datastoreProvider.js', function() { +describe('/src/datastoreProvider.ts', function () { this.timeout(20000) describe('#()', () => { it('should not throw an exception with basic arguments', () => { - const { datastoreProvider, AWS } = setupMocks() + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } assert.doesNotThrow(() => { - const instance = datastoreProvider({AWS}) + const datastoreProvider = createDatastoreProvider({ + aws3, + dynamoOptions, + }) }) }) it('should have a "search" function', () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) assert.isFunction(instance.search) }) it('should have a "retrieve" function', () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) assert.isFunction(instance.retrieve) }) it('should have a "save" function', () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) assert.isFunction(instance.save) }) it('should have a "delete" function', () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({ AWS }) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) assert.isFunction(instance.delete) }) describe('#search()', () => { + it('should pass createUniqueId into queryBuilder', async () => { + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const myFunc = () => 'fake-id' + const instance = createDatastoreProvider({ + aws3, + dynamoOptions, + createUniqueId: myFunc, + }) + const obj = { id: 'my-id', name: 'my-name' } + const query = ormQueryBuilder().property('name', 'my-name').compile() + // @ts-ignore + aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ + Items: [], + LastEvaluatedKey: null, + }) + await instance.search(createTestModel1(obj).getModel(), query) + const actual = aws3.ScanCommand.sinon.getCall(0).args[0] + const expected = { + TableName: 'testmodel1', + FilterExpression: '#myfakeid = :myfakeid', + ExpressionAttributeNames: { '#myfakeid': 'name' }, + ExpressionAttributeValues: { ':myfakeid': 'my-name' }, + } + assert.deepInclude(actual, expected) + }) it('should call dynamo.scan once when LastEvaluatedKey is empty', async () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const obj = { id: 'my-id', name: 'my-name' } const query = ormQueryBuilder().property('name', 'my-name').compile() // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onFirstCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [], - LastEvaluatedKey: null, - })), + aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ + Items: [], + LastEvaluatedKey: null, }) await instance.search(createTestModel1(obj).getModel(), query) // @ts-ignore - sinon.assert.calledOnce(AWS.DynamoDB.DocumentClient.scan) + sinon.assert.calledOnce(aws3.ScanCommand.sinon) }) it('should be able to process a string value result', async () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const obj = { id: 'my-id', name: 'my-name' } const query = ormQueryBuilder().property('name', 'my-name').compile() // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onFirstCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [ - { - ..._createDynamoStringResult('id', 'my-id'), - ..._createDynamoStringResult('name', 'my-name'), - }, - ], - LastEvaluatedKey: null, - })), + aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ + Items: [ + { + ..._createDynamoStringResult('id', 'my-id'), + ..._createDynamoStringResult('name', 'my-name'), + }, + ], + LastEvaluatedKey: null, }) const actual = await instance.search( createTestModel1(obj).getModel(), @@ -146,22 +163,20 @@ describe('/src/datastoreProvider.js', function() { assert.deepEqual(actual, expected) }) it('should be able to process a null value results', async () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const obj = { id: 'my-id', name: 'my-name' } const query = ormQueryBuilder().property('name', null).compile() // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onFirstCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [ - { - ..._createDynamoStringResult('id', 'my-id'), - ..._createDynamoNullResult('name'), - }, - ], - LastEvaluatedKey: null, - })), + aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ + Items: [ + { + ..._createDynamoStringResult('id', 'my-id'), + ..._createDynamoNullResult('name'), + }, + ], + LastEvaluatedKey: null, }) const actual = await instance.search( createTestModel1(obj).getModel(), @@ -174,22 +189,20 @@ describe('/src/datastoreProvider.js', function() { assert.deepEqual(actual, expected) }) it('should be able to process an array of strings', async () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const obj = { id: 'my-id', name: 'my-name' } const query = ormQueryBuilder().property('names', 'my-name').compile() // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onFirstCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [ - { - ..._createDynamoStringResult('id', 'my-id'), - ..._createDynamoStingArrayResult('names', ['a', 'b']), - }, - ], - LastEvaluatedKey: null, - })), + aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ + Items: [ + { + ..._createDynamoStringResult('id', 'my-id'), + ..._createDynamoStingArrayResult('names', ['a', 'b']), + }, + ], + LastEvaluatedKey: null, }) const actual = await instance.search( createTestModel1(obj).getModel(), @@ -202,26 +215,24 @@ describe('/src/datastoreProvider.js', function() { assert.deepEqual(actual, expected) }) it('should return only 1 object if query has "take:1" even if there are two results from dynamo', async () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const obj = { id: 'my-id', name: 'my-name' } const query = ormQueryBuilder().property('name', null).take(1).compile() // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onFirstCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [ - { - ..._createDynamoStringResult('id', 'my-id'), - ..._createDynamoStringResult('name', 'name1'), - }, - { - ..._createDynamoStringResult('id2', 'my-id'), - ..._createDynamoStringResult('name', 'name2'), - }, - ], - LastEvaluatedKey: null, - })), + aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ + Items: [ + { + ..._createDynamoStringResult('id', 'my-id'), + ..._createDynamoStringResult('name', 'name1'), + }, + { + ..._createDynamoStringResult('id2', 'my-id'), + ..._createDynamoStringResult('name', 'name2'), + }, + ], + LastEvaluatedKey: null, }) const actual = await instance.search( createTestModel1(obj).getModel(), @@ -234,112 +245,122 @@ describe('/src/datastoreProvider.js', function() { assert.deepEqual(actual, expected) }) it('should call dynamo.scan twice when LastEvaluatedKey is empty the second time', async () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const obj = { id: 'my-id', name: 'my-name' } const query = ormQueryBuilder().property('name', 'my-name').compile() // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onFirstCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [], - LastEvaluatedKey: 'try-again', - })), + aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ + Items: [], + LastEvaluatedKey: 'try-again', }) // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onSecondCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [{ something: 'returned' }], - LastEvaluatedKey: null, - })), + aws3.ScanCommand.sinon.onSecondCall().resolves({ + Items: [{ something: 'returned' }], + LastEvaluatedKey: null, }) await instance.search(createTestModel1(obj).getModel(), query) // @ts-ignore - sinon.assert.calledTwice(AWS.DynamoDB.DocumentClient.scan) + sinon.assert.calledTwice(aws3.ScanCommand.sinon) }) it('should call dynamo.scan twice when LastEvaluatedKey has a value the second time but take:2 and 3 items are returned', async () => { - const { datastoreProvider, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const obj = { id: 'my-id', name: 'my-name' } const query = ormQueryBuilder() .property('name', 'my-name') .take(2) .compile() // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onFirstCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [], - LastEvaluatedKey: 'try-again', - })), + aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ + Items: [], + LastEvaluatedKey: 'try-again', }) // @ts-ignore - AWS.DynamoDB.DocumentClient.scan.onSecondCall().returns({ - promise: () => - Promise.resolve().then(() => ({ - Items: [ - { something: 'returned' }, - { something2: 'returned2' }, - { something3: 'returned3' }, - ], - LastEvaluatedKey: 'another-value', - })), + aws3.DynamoDBDocumentClient.sendSinon.onSecondCall().resolves({ + Items: [ + { something: 'returned' }, + { something2: 'returned2' }, + { something3: 'returned3' }, + ], + LastEvaluatedKey: 'another-value', }) await instance.search(createTestModel1(obj).getModel(), query) // @ts-ignore - const actual = AWS.DynamoDB.DocumentClient.scan.getCalls().length + const actual = aws3.DynamoDBDocumentClient.sendSinon.getCalls().length const expected = 2 assert.equal(actual, expected) }) }) describe('#retrieve()', () => { - it('should pass the correct params to dynamoClient.get', async () => { - const { datastoreProvider, dynamoClient, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + it('should pass the correct table name into GetCommand', async () => { + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ + aws3, + dynamoOptions, + getTableNameForModel: () => 'FakeTable', + }) + const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) + await instance.retrieve(modelInstance.getModel(), 'my-id') + const actual = aws3.GetCommand.sinon.getCall(0).args[0] + const expected = { Key: { id: 'my-id' }, TableName: 'FakeTable' } + assert.deepEqual(actual, expected) + }) + it('should pass the correct params to GetCommand', async () => { + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) await instance.retrieve(modelInstance.getModel(), 'my-id') - const actual = dynamoClient.get.getCall(0).args[0] - const expected = { key: { id: 'my-id' } } + const actual = aws3.GetCommand.sinon.getCall(0).args[0] + const expected = { Key: { id: 'my-id' }, TableName: 'testmodel1' } assert.deepEqual(actual, expected) }) }) describe('#delete()', () => { - it('should pass the correct params to dynamoClient.delete', async () => { - const { datastoreProvider, dynamoClient, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + it('should pass the correct params to DeleteCommand', async () => { + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) await instance.delete(modelInstance) - const actual = dynamoClient.delete.getCall(0).args[0] - const expected = { key: { id: 'my-id' } } + const actual = aws3.DeleteCommand.sinon.getCall(0).args[0] + const expected = { Key: { id: 'my-id' }, TableName: 'testmodel1' } assert.deepEqual(actual, expected) }) }) describe('#save()', () => { - it('should pass results of modelInstance.functions.toObj() to dynamoClient.update', async () => { - const { datastoreProvider, dynamoClient, AWS } = setupMocks() - const instance = datastoreProvider({AWS}) + it('should pass results of modelInstance.functions.toObj() to PutCommand', async () => { + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) await instance.save(modelInstance) - const actual = dynamoClient.update.getCall(0).args[0] + const actual = aws3.PutCommand.sinon.getCall(0).args[0] const expected = { - key: { id: 'my-id' }, - item: { id: 'my-id', name: 'my-name' }, + Key: { id: 'my-id' }, + Item: { id: 'my-id', name: 'my-name' }, + TableName: 'testmodel1', } assert.deepEqual(actual, expected) }) - it('should pass the correct primary key when changed by the model to dynamoClient.update', async () => { - const { datastoreProvider, dynamoClient, AWS } = setupMocks() - const instance = datastoreProvider({ AWS }) + it('should pass the correct primary key when changed by the model to PutCommand', async () => { + const aws3 = createAws3MockClient() + const dynamoOptions = { region: 'fake-region' } + const instance = createDatastoreProvider({ aws3, dynamoOptions }) const modelInstance = createTestModel2({ notId: 'my-id', name: 'my-name', }) await instance.save(modelInstance) - const actual = dynamoClient.update.getCall(0).args[0] + const actual = aws3.PutCommand.sinon.getCall(0).args[0] const expected = { - key: { notId: 'my-id' }, - item: { notId: 'my-id', name: 'my-name' }, + Key: { notId: 'my-id' }, + Item: { notId: 'my-id', name: 'my-name' }, + TableName: 'testmodel1', } assert.deepEqual(actual, expected) }) diff --git a/test/src/dynamoClient.test.ts b/test/src/dynamoClient.test.ts deleted file mode 100644 index 7c6bc37..0000000 --- a/test/src/dynamoClient.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { assert } from 'chai' -import sinon from 'sinon' -import { createAwsMocks } from '../commonMocks' -import dynamoClient from '../../src/dynamoClient' - -const setupMocks = () => { - const AWS = createAwsMocks() - return { - AWS, - } -} - -describe('/src/dynamoClient.ts', function() { - describe('#()', () => { - it('should call new DynamoDB() with correct params', function() { - const { AWS } = setupMocks() - const input = { AWS, tableName: '', dynamoOptions: { passed: 'in' } } - const instance = dynamoClient(input) - // @ts-ignore - const actual = AWS.DynamoDB.theConstructor.getCall(0).args[0] - const expected = { passed: 'in' } - assert.deepEqual(actual, expected) - }) - it('should call new DocumentClient() with correct params', () => { - const { AWS } = setupMocks() - const input = { AWS, tableName: '', dynamoOptions: { passed: 'in' } } - const instance = dynamoClient(input) - // @ts-ignore - const actual = AWS.DynamoDB.DocumentClient.constructor.getCall(0).args[0] - assert.isOk(actual.service) - }) - describe('#get()', () => { - it('should call documentClient.get() with the correct params', async () => { - const { AWS } = setupMocks() - const input = { AWS, tableName: 'my-table', dynamoOptions: { passed: 'in' } } - const instance = dynamoClient(input) - // @ts-ignore - AWS.DynamoDB.DocumentClient.get.returns({ - promise: () => - Promise.resolve().then(() => ({ Item: { my: 'result' } })), - }) - await instance.get({ key: { id: 'my-key' } }) - // @ts-ignore - const actual = AWS.DynamoDB.DocumentClient.get.getCall(0).args[0] - const expected = { TableName: 'my-table', Key: { id: 'my-key' } } - assert.deepEqual(actual, expected) - }) - it('should return "Item" from documentClient.get() with the correct params', async () => { - const { AWS } = setupMocks() - const input = { AWS, tableName: 'my-table', dynamoOptions: { passed: 'in' } } - const instance = dynamoClient(input) - // @ts-ignore - AWS.DynamoDB.DocumentClient.get.returns({ - promise: () => - Promise.resolve().then(() => ({ Item: { my: 'result' } })), - }) - const actual = await instance.get({ key: { key: 'my-key' } }) - const expected = { my: 'result' } - assert.deepEqual(actual, expected) - }) - }) - describe('#update()', () => { - it('should return "Item" from documentClient.put() with the correct params', async () => { - const { AWS } = setupMocks() - const input = { AWS, tableName: 'my-table', dynamoOptions: { passed: 'in' } } - const instance = dynamoClient(input) - // @ts-ignore - AWS.DynamoDB.DocumentClient.put.returns({ - promise: () => Promise.resolve(), - }) - await instance.update({ - item: { myarg: 'here', id: 'bad-id' }, - key: { id: 'my-key' }, - }) - // @ts-ignore - const actual = AWS.DynamoDB.DocumentClient.put.getCall(0).args[0] - const expected = { - TableName: 'my-table', - Item: { myarg: 'here', id: 'my-key' }, - } - assert.deepEqual(actual, expected) - }) - }) - describe('#delete()', () => { - it('should return "Item" from documentClient.delete() with the correct params', async () => { - const { AWS } = setupMocks() - const input = { AWS, tableName: 'my-table', dynamoOptions: { passed: 'in' } } - const instance = dynamoClient(input) - // @ts-ignore - AWS.DynamoDB.DocumentClient.delete.returns({ - promise: () => Promise.resolve(), - }) - await instance.delete({ key: { id: 'my-key' } }) - // @ts-ignore - const actual = AWS.DynamoDB.DocumentClient.delete.getCall(0).args[0] - const expected = { TableName: 'my-table', Key: { id: 'my-key' } } - assert.deepEqual(actual, expected) - }) - }) - }) -}) diff --git a/test/src/queryBuilder.test.ts b/test/src/queryBuilder.test.ts index d812fab..f1b2f43 100644 --- a/test/src/queryBuilder.test.ts +++ b/test/src/queryBuilder.test.ts @@ -5,13 +5,16 @@ import { ormQueryBuilder } from 'functional-models-orm/ormQuery' import { EQUALITY_SYMBOLS, ORMType } from 'functional-models-orm/constants' import queryBuilder from '../../src/queryBuilder' -const _nameId = () => ({ createUniqueId: (obj: any) => obj.name}) +const _nameId = () => ({ createUniqueId: (obj: any) => obj.name }) describe('/src/queryBuilder.ts', () => { describe('#()', () => { it('should set TableName to what is passed in', () => { const query = ormQueryBuilder().property('name', 'value').compile() - const actual = get(queryBuilder(_nameId())('my-table', query), 'TableName') + const actual = get( + queryBuilder(_nameId())('my-table', query), + 'TableName' + ) const expected = 'my-table' assert.deepEqual(actual, expected) }) @@ -24,7 +27,10 @@ describe('/src/queryBuilder.ts', () => { */ it('should TableName to what is passed in', () => { const query = ormQueryBuilder().property('name', 'value').compile() - const actual = get(queryBuilder(_nameId())('my-table', query), 'TableName') + const actual = get( + queryBuilder(_nameId())('my-table', query), + 'TableName' + ) const expected = 'my-table' assert.deepEqual(actual, expected) }) @@ -35,7 +41,12 @@ describe('/src/queryBuilder.ts', () => { assert.deepEqual(actual, expected) }) it('should produce an expected FilterExpression for a single property with a > symbol', () => { - const query = ormQueryBuilder().property('name', 5, { type: ORMType.number, equalitySymbol: EQUALITY_SYMBOLS.GT}).compile() + const query = ormQueryBuilder() + .property('name', 5, { + type: ORMType.number, + equalitySymbol: EQUALITY_SYMBOLS.GT, + }) + .compile() const actual = queryBuilder(_nameId())('my-table', query).FilterExpression const expected = '#myname > :myname' assert.deepEqual(actual, expected) @@ -83,10 +94,12 @@ describe('/src/queryBuilder.ts', () => { .property('secondname', 'value') .compile() try { - const actual = queryBuilder(_nameId())('my-table', query).FilterExpression + const actual = queryBuilder(_nameId())( + 'my-table', + query + ).FilterExpression throw new Error(`No exception thrown`) - } catch { - } + } catch {} }) it('should throw an exception if two AND are called', () => { const query = ormQueryBuilder() @@ -96,14 +109,19 @@ describe('/src/queryBuilder.ts', () => { .property('secondname', 'value') .compile() try { - const actual = queryBuilder(_nameId())('my-table', query).FilterExpression + const actual = queryBuilder(_nameId())( + 'my-table', + query + ).FilterExpression throw new Error(`No exception thrown`) - } catch { - } + } catch {} }) it('should produce an expected ExpressionAttributeNames for a single property when the name has a dash', () => { const query = ormQueryBuilder().property('name-name', 'value').compile() - const actual = queryBuilder(_nameId())('my-table', query).ExpressionAttributeNames + const actual = queryBuilder(_nameId())( + 'my-table', + query + ).ExpressionAttributeNames const expected = { '#mynamename': 'name-name', } @@ -114,7 +132,10 @@ describe('/src/queryBuilder.ts', () => { .property('name', 'value') .property('description', 'the-description') .compile() - const actual = queryBuilder(_nameId())('my-table', query).ExpressionAttributeNames + const actual = queryBuilder(_nameId())( + 'my-table', + query + ).ExpressionAttributeNames const expected = { '#myname': 'name', '#mydescription': 'description', @@ -123,7 +144,10 @@ describe('/src/queryBuilder.ts', () => { }) it('should produce an expected ExpressionAttributeValues for one property', () => { const query = ormQueryBuilder().property('name', 'value').compile() - const actual = queryBuilder(_nameId())('my-table', query).ExpressionAttributeValues + const actual = queryBuilder(_nameId())( + 'my-table', + query + ).ExpressionAttributeValues const expected = { ':myname': 'value', } @@ -131,9 +155,12 @@ describe('/src/queryBuilder.ts', () => { }) it('should produce an expected ExpressionAttributeValues for one property that has a null value', () => { const query = ormQueryBuilder().property('name', null).compile() - const actual = queryBuilder(_nameId())('my-table', query).ExpressionAttributeValues + const actual = queryBuilder(_nameId())( + 'my-table', + query + ).ExpressionAttributeValues const expected = { - ':myname': "", + ':myname': '', } assert.deepEqual(actual, expected) }) @@ -142,10 +169,13 @@ describe('/src/queryBuilder.ts', () => { .property('name', 'value') .property('other', null) .compile() - const actual = queryBuilder(_nameId())('my-table', query).ExpressionAttributeValues + const actual = queryBuilder(_nameId())( + 'my-table', + query + ).ExpressionAttributeValues const expected = { ':myname': 'value', - ':myother': "", + ':myother': '', } assert.deepEqual(actual, expected) }) diff --git a/test/src/utils.test.ts b/test/src/utils.test.ts index 3bbf476..e84b416 100644 --- a/test/src/utils.test.ts +++ b/test/src/utils.test.ts @@ -3,7 +3,7 @@ import { assert } from 'chai' import sinon from 'sinon' import { getTableNameForModel } from '../../src/utils' -const buildModel = (name: string) : Model => { +const buildModel = (name: string): Model => { return { getName: () => name, } as Model diff --git a/tsconfig.json b/tsconfig.json index 7dec27f..c59ee45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,101 +1,15 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Projects */ - // "incremental": true, /* Enable incremental compilation */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "lib": ["es2021", "dom", "es2015"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - //"rootDir": "./src" /* Specify the root folder within your source files. */, - // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - //"rootDirs": ["./src","], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "resolveJsonModule": true, /* Enable importing .json files */ - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - //"outDir": "./dist" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "target": "es6", + "lib": ["es2021", "dom", "es2015"], + "declaration": true, + "module": "commonjs", + "sourceMap": true, + "moduleResolution": "Node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true }, "exclude": [ "node_modules", diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..1fa6c98 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es2021", "dom", "es2015"], + "module": "commonjs", + //"moduleResolution": "node16", + "rootDirs": ["./src", "./test"], + "allowJs": true, + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": false, + "skipLibCheck": true + }, + "include": ["./test"], + "exclude": [ + "src/index.ts", + "src/index.js", + "src/index.d.ts", + "node_modules", + "dist", + "features", + "container", + "features" + ] +} +