From 4233c2cc2f5c760846b5b23a7fe5a7f380e83566 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Thu, 21 Jan 2021 16:40:13 +0100 Subject: [PATCH 01/13] feat: remove DynamodbWrapper client dependency --- src/batch-get-item.js | 1 + src/batch-write-item.js | 7 ++++++- src/client.js | 24 ++++++++++-------------- src/count.js | 6 ++++-- src/get-all.js | 23 ++++++++++++++++++++--- src/get.js | 5 +++-- src/insert.js | 3 ++- src/query.js | 25 ++++++++++++++++++------- src/remove.js | 5 +++-- src/update.js | 17 +++-------------- src/with-pagination-helper.js | 20 ++++++++++++++++++++ 11 files changed, 90 insertions(+), 46 deletions(-) create mode 100644 src/with-pagination-helper.js diff --git a/src/batch-get-item.js b/src/batch-get-item.js index 7806971..a9adead 100644 --- a/src/batch-get-item.js +++ b/src/batch-get-item.js @@ -36,6 +36,7 @@ const generateKeys = compose( const createBatchGetFor = curry((batchGetItem, table) => compose( + request => request.promise(), apply(batchGetItem), overFirst(compose(objOf('RequestItems'), objOf(table))), mapMergeFirstPairOfArgs(generateKeys) diff --git a/src/batch-write-item.js b/src/batch-write-item.js index eaf2176..4617173 100644 --- a/src/batch-write-item.js +++ b/src/batch-write-item.js @@ -92,11 +92,16 @@ const generateRequestItems = table => ); const createBatchWriteFor = curry((batchWriteItem, table) => - compose(apply(batchWriteItem), mapMergeFirstPairOfArgs(generateRequestItems(table))) + compose( + request => request.promise(), + apply(batchWriteItem), + mapMergeFirstPairOfArgs(generateRequestItems(table)) + ) ); const createBatchRequestFor = curry((batchWriteItem, requestType, table) => compose( + request => request.promise(), apply(batchWriteItem), mapMergeFirstPairOfArgs(compose(generateRequestItems(table), objOf(requestType))) ) diff --git a/src/client.js b/src/client.js index e2257b7..f9f8e78 100644 --- a/src/client.js +++ b/src/client.js @@ -8,7 +8,6 @@ const createUpdater = require('./update'); const createCounter = require('./count'); const createWriteBatcher = require('./batch-write-item'); const createGetBatcher = require('./batch-get-item'); -const DynamoDBWrapper = require('dynamodb-wrapper'); /** * Wraps an AWS DynamoDB `client` and returns Flynamo's API to access @@ -17,20 +16,17 @@ const DynamoDBWrapper = require('dynamodb-wrapper'); * @see https://github.com/Shadowblazen/dynamodb-wrapper#setup * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html * @param {Object} client A DynamoDB client - * @param {Object} [config={}] `DynamoDBWrapper` configuration (optional). */ -function flynamo(client, config = {}) { - const clientWrapper = new DynamoDBWrapper(client, config); - - const { get, getFor } = createGetter(clientWrapper); - const { getAll, getAllFor } = createAllGetter(clientWrapper); - const { insert, insertFor } = createInserter(clientWrapper); - const { query, queryFor } = createQuerier(clientWrapper); - const { remove, removeFor } = createRemover(clientWrapper); - const { update, updateFor } = createUpdater(clientWrapper); - const { count, countFor } = createCounter(clientWrapper); - const { batchWriteFor, batchRemoveFor, batchInsertFor } = createWriteBatcher(clientWrapper); - const { batchGetFor } = createGetBatcher(clientWrapper); +function flynamo(client) { + const { get, getFor } = createGetter(client); + const { getAll, getAllFor } = createAllGetter(client); + const { insert, insertFor } = createInserter(client); + const { query, queryFor } = createQuerier(client); + const { remove, removeFor } = createRemover(client); + const { update, updateFor } = createUpdater(client); + const { count, countFor } = createCounter(client); + const { batchWriteFor, batchRemoveFor, batchInsertFor } = createWriteBatcher(client); + const { batchGetFor } = createGetBatcher(client); function forTable(table) { return { diff --git a/src/count.js b/src/count.js index f140cc3..1313348 100644 --- a/src/count.js +++ b/src/count.js @@ -4,13 +4,15 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html * @module Count */ -const { curry, compose, pipeP, bind, prop } = require('ramda'); +const { curry, compose, bind, prop } = require('ramda'); const addTableName = require('./table-name'); +const withPaginationHelper = require('./with-pagination-helper'); /** * @private */ -const createCount = scan => pipeP(scan, prop('Count')); +const createCount = scan => params => + withPaginationHelper('scan', params, true).then(prop('Count')); /** * @private diff --git a/src/get-all.js b/src/get-all.js index 7933144..15dc019 100644 --- a/src/get-all.js +++ b/src/get-all.js @@ -4,14 +4,25 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html * @module Scan */ -const { curry, bind, pipeP, compose } = require('ramda'); -const { unwrapAll } = require('./wrapper'); +const { curry, bind, compose, mergeRight } = require('ramda'); +const { unwrapAll, unwrapOverAll } = require('./wrapper'); const addTableName = require('./table-name'); +const withPaginationHelper = require('./with-pagination-helper'); +const DEFAULT_OPTIONS = { + raw: false, + pagination: true +}; +const mergeWithDefaults = mergeRight(DEFAULT_OPTIONS); /** * @private */ -const createGetAll = scan => pipeP(scan, unwrapAll('Items')); +const createGetAll = scan => (params, options = {}) => { + options = mergeWithDefaults(options); + return withPaginationHelper('scan', params, options.pagination).then( + options.raw ? unwrapOverAll('Items') : unwrapAll('Items') + ); +}; /** * @private @@ -32,6 +43,9 @@ function createAllGetter(dynamoWrapper) { * * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html#API_Scan_RequestSyntax * @param {Object} request Parameters as expected by DynamoDB `Scan` operation. + * @param {Object} [options] The configuration options parameters. + * @param {number} [options.pagination=true] Wheter to return all the DynamoDB response pages or just one page. + * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. * @returns {Promise} A promise that resolves to an array of `Items` returned by the DynamoDB response */ getAll: createGetAll(scan), @@ -49,6 +63,9 @@ function createAllGetter(dynamoWrapper) { * @param tableName The name of the table to perform the operation on. This will override any `TableName` * attribute set on `request`. * @param {Object=} request Parameters as expected by DynamoDB `Scan` operation. + * @param {Object} [options] The configuration options parameters. + * @param {number} [options.pagination=true] Wheter to return all the DynamoDB response pages or just one page. + * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. * @returns {Promise} A promise that resolves to an array of `Items` returned by the DynamoDB response */ getAllFor: createGetAllFor(scan) diff --git a/src/get.js b/src/get.js index ed61d1c..532ba3a 100644 --- a/src/get.js +++ b/src/get.js @@ -4,7 +4,7 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html * @module GetItem */ -const { curry, bind, pipeP, compose, apply } = require('ramda'); +const { curry, bind, compose, apply } = require('ramda'); const { unwrapProp } = require('./wrapper'); const { mapMergeFirstPairOfArgs } = require('./map-merge-args'); const generateKey = require('./generate-key'); @@ -13,7 +13,8 @@ const addTableName = require('./table-name'); /** * @private */ -const getUnwrappedItem = getItem => pipeP(apply(getItem), unwrapProp('Item')); +const getUnwrappedItem = getItem => + compose(unwrapProp('Item'), request => request.promise(), apply(getItem)); /** * @private diff --git a/src/insert.js b/src/insert.js index da6dc84..f73d30b 100644 --- a/src/insert.js +++ b/src/insert.js @@ -12,7 +12,8 @@ const { mapMergeFirstPairOfArgs } = require('./map-merge-args'); /** * @private */ -const createInsert = putItem => compose(apply(putItem), mapMergeFirstPairOfArgs(generateItem)); +const createInsert = putItem => + compose(request => request.promise(), apply(putItem), mapMergeFirstPairOfArgs(generateItem)); /** * @private diff --git a/src/query.js b/src/query.js index 5a7bd49..b20b504 100644 --- a/src/query.js +++ b/src/query.js @@ -5,15 +5,26 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html * @module Query */ -const { bind, curry } = require('ramda'); +const { bind, curry, mergeRight } = require('ramda'); const { unwrapAll, unwrapOverAll } = require('./wrapper'); const addTableName = require('./table-name'); +const withPaginationHelper = require('./with-pagination-helper'); + +const DEFAULT_OPTIONS = { + raw: false, + pagination: true +}; +const mergeWithDefaults = mergeRight(DEFAULT_OPTIONS); /** * @private */ -const createQuery = query => (params, options) => - query(params, options).then(options && options.raw ? unwrapOverAll('Items') : unwrapAll('Items')); +const createQuery = query => (params, options = {}) => { + options = mergeWithDefaults(options); + return withPaginationHelper('query', params, options.pagination).then( + options.raw ? unwrapOverAll('Items') : unwrapAll('Items') + ); +}; /** * @private @@ -24,8 +35,8 @@ const createQueryFor = curry((query, table) => { return (params, options) => queryFn(withTableName(params), options); }); -function createQuerier(dynamoWrapper) { - const query = bind(dynamoWrapper.query, dynamoWrapper); +function createQuerier(dynamodb) { + const query = bind(dynamodb.query, dynamodb); return { /** * Finds items based on primary key values. @@ -47,7 +58,7 @@ function createQuerier(dynamoWrapper) { * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#API_Query_RequestSyntax * @param {Object} request Parameters as expected by DynamoDB `Query` operation. Must contain, at least, `TableName` attribute. * @param {Object} [options] The configuration options parameters. - * @param {number} [options.groupDelayMs=100] The delay between individual requests. Defaults to 100 ms. + * @param {number} [options.pagination=true] Wheter to return all the DynamoDB response pages or just one page. * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. * @returns {Promise} A promise that resolves to the response from DynamoDB. */ @@ -88,7 +99,7 @@ function createQuerier(dynamoWrapper) { * @param {String} tableName The name of the table to perform the operation on * @param {Object=} request Parameters as expected by DynamoDB `Query` operation. A `TableName` attributes specified here will override `tableName` argument. * @param {Object} [options] The configuration options parameters. - * @param {number} [options.groupDelayMs=100] The delay between individual requests. Defaults to 100 ms. + * @param {number} [options.pagination=true] Wheter to return all the DynamoDB response pages or just one page. * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. * @returns {Promise} A promise that resolves to the response from DynamoDB. */ diff --git a/src/remove.js b/src/remove.js index 4852611..ed51046 100644 --- a/src/remove.js +++ b/src/remove.js @@ -7,7 +7,7 @@ * @module DeleteItem * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html */ -const { apply, bind, compose, curry, pipeP } = require('ramda'); +const { apply, bind, compose, curry } = require('ramda'); const { unwrapProp } = require('./wrapper'); const addTableName = require('./table-name'); const { mapMergeFirstPairOfArgs } = require('./map-merge-args'); @@ -17,7 +17,8 @@ const addReturnValues = require('./return-values'); /** * @private */ -const removeAndUnwrapAttributes = deleteItem => pipeP(apply(deleteItem), unwrapProp('Attributes')); +const removeAndUnwrapAttributes = deleteItem => + compose(unwrapProp('Attributes'), request => request.promise(), apply(deleteItem)); /** * @private diff --git a/src/update.js b/src/update.js index 6af06b4..7cbe0b5 100644 --- a/src/update.js +++ b/src/update.js @@ -5,19 +5,7 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html * @module UpdateItem */ -const { - apply, - applyTo, - bind, - compose, - curry, - ifElse, - is, - pipeP, - unless, - has, - partial -} = require('ramda'); +const { apply, applyTo, bind, compose, curry, ifElse, is, unless, has, partial } = require('ramda'); const { getUpdateExpression } = require('dynamodb-update-expression'); const { unwrapProp, wrapOver } = require('./wrapper'); const addTableName = require('./table-name'); @@ -28,7 +16,8 @@ const generateKey = require('./generate-key'); /** * @private */ -const updateAndUnwrapAttributes = updateItem => pipeP(apply(updateItem), unwrapProp('Attributes')); +const updateAndUnwrapAttributes = updateItem => + compose(unwrapProp('Attributes'), request => request.promise(), apply(updateItem)); /** * @private diff --git a/src/with-pagination-helper.js b/src/with-pagination-helper.js new file mode 100644 index 0000000..5bb5ca6 --- /dev/null +++ b/src/with-pagination-helper.js @@ -0,0 +1,20 @@ +'use strict'; + +async function withPaginationHelper(method, params, pagination) { + const items = []; + let result = await method(params).promise(); + items.push(...result.Items); + + if (pagination) { + while (result.LastEvaluatedKey) { + result = await method({ ...params, ExclusiveStartKey: result.LastEvaluatedKey }).promise(); + items.push(...result.Items); + } + } + + result.Items = items; + + return result; +} + +module.exports = withPaginationHelper; From 73f3fb016efc2f682654680121d53067d61aab38 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Thu, 21 Jan 2021 18:04:25 +0100 Subject: [PATCH 02/13] fix: pass method fn instead of string --- src/count.js | 5 ++--- src/get-all.js | 12 ++++++------ src/query.js | 12 ++++++------ src/with-pagination-helper.js | 20 -------------------- src/with-paginator-helper.js | 30 ++++++++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 35 deletions(-) delete mode 100644 src/with-pagination-helper.js create mode 100644 src/with-paginator-helper.js diff --git a/src/count.js b/src/count.js index 1313348..9ed68be 100644 --- a/src/count.js +++ b/src/count.js @@ -6,13 +6,12 @@ */ const { curry, compose, bind, prop } = require('ramda'); const addTableName = require('./table-name'); -const withPaginationHelper = require('./with-pagination-helper'); +const withPaginatorHelper = require('./with-paginator-helper'); /** * @private */ -const createCount = scan => params => - withPaginationHelper('scan', params, true).then(prop('Count')); +const createCount = scan => params => withPaginatorHelper(scan, params, true).then(prop('Count')); /** * @private diff --git a/src/get-all.js b/src/get-all.js index 15dc019..e3946e6 100644 --- a/src/get-all.js +++ b/src/get-all.js @@ -7,11 +7,11 @@ const { curry, bind, compose, mergeRight } = require('ramda'); const { unwrapAll, unwrapOverAll } = require('./wrapper'); const addTableName = require('./table-name'); -const withPaginationHelper = require('./with-pagination-helper'); +const withPaginatorHelper = require('./with-paginator-helper'); const DEFAULT_OPTIONS = { - raw: false, - pagination: true + autopagination: true, + raw: false }; const mergeWithDefaults = mergeRight(DEFAULT_OPTIONS); /** @@ -19,7 +19,7 @@ const mergeWithDefaults = mergeRight(DEFAULT_OPTIONS); */ const createGetAll = scan => (params, options = {}) => { options = mergeWithDefaults(options); - return withPaginationHelper('scan', params, options.pagination).then( + return withPaginatorHelper(scan, params, options.autopagination).then( options.raw ? unwrapOverAll('Items') : unwrapAll('Items') ); }; @@ -44,7 +44,7 @@ function createAllGetter(dynamoWrapper) { * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html#API_Scan_RequestSyntax * @param {Object} request Parameters as expected by DynamoDB `Scan` operation. * @param {Object} [options] The configuration options parameters. - * @param {number} [options.pagination=true] Wheter to return all the DynamoDB response pages or just one page. + * @param {boolean} [options.autopagination=true] Wheter to return all the DynamoDB response pages or just one page. * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. * @returns {Promise} A promise that resolves to an array of `Items` returned by the DynamoDB response */ @@ -64,7 +64,7 @@ function createAllGetter(dynamoWrapper) { * attribute set on `request`. * @param {Object=} request Parameters as expected by DynamoDB `Scan` operation. * @param {Object} [options] The configuration options parameters. - * @param {number} [options.pagination=true] Wheter to return all the DynamoDB response pages or just one page. + * @param {boolean} [options.autopagination=true] Wheter to return all the DynamoDB response pages or just one page. * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. * @returns {Promise} A promise that resolves to an array of `Items` returned by the DynamoDB response */ diff --git a/src/query.js b/src/query.js index b20b504..f074c4f 100644 --- a/src/query.js +++ b/src/query.js @@ -8,11 +8,11 @@ const { bind, curry, mergeRight } = require('ramda'); const { unwrapAll, unwrapOverAll } = require('./wrapper'); const addTableName = require('./table-name'); -const withPaginationHelper = require('./with-pagination-helper'); +const withPaginatorHelper = require('./with-paginator-helper'); const DEFAULT_OPTIONS = { - raw: false, - pagination: true + autopagination: true, + raw: false }; const mergeWithDefaults = mergeRight(DEFAULT_OPTIONS); @@ -21,7 +21,7 @@ const mergeWithDefaults = mergeRight(DEFAULT_OPTIONS); */ const createQuery = query => (params, options = {}) => { options = mergeWithDefaults(options); - return withPaginationHelper('query', params, options.pagination).then( + return withPaginatorHelper(query, params, options.autopagination).then( options.raw ? unwrapOverAll('Items') : unwrapAll('Items') ); }; @@ -58,7 +58,7 @@ function createQuerier(dynamodb) { * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#API_Query_RequestSyntax * @param {Object} request Parameters as expected by DynamoDB `Query` operation. Must contain, at least, `TableName` attribute. * @param {Object} [options] The configuration options parameters. - * @param {number} [options.pagination=true] Wheter to return all the DynamoDB response pages or just one page. + * @param {boolean} [options.autopagination=true] Whether to return all the DynamoDB response pages or just one page. * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. * @returns {Promise} A promise that resolves to the response from DynamoDB. */ @@ -99,7 +99,7 @@ function createQuerier(dynamodb) { * @param {String} tableName The name of the table to perform the operation on * @param {Object=} request Parameters as expected by DynamoDB `Query` operation. A `TableName` attributes specified here will override `tableName` argument. * @param {Object} [options] The configuration options parameters. - * @param {number} [options.pagination=true] Wheter to return all the DynamoDB response pages or just one page. + * @param {boolean} [options.autopagination=true] Wheter to return all the DynamoDB response pages or just one page. * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. * @returns {Promise} A promise that resolves to the response from DynamoDB. */ diff --git a/src/with-pagination-helper.js b/src/with-pagination-helper.js deleted file mode 100644 index 5bb5ca6..0000000 --- a/src/with-pagination-helper.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -async function withPaginationHelper(method, params, pagination) { - const items = []; - let result = await method(params).promise(); - items.push(...result.Items); - - if (pagination) { - while (result.LastEvaluatedKey) { - result = await method({ ...params, ExclusiveStartKey: result.LastEvaluatedKey }).promise(); - items.push(...result.Items); - } - } - - result.Items = items; - - return result; -} - -module.exports = withPaginationHelper; diff --git a/src/with-paginator-helper.js b/src/with-paginator-helper.js new file mode 100644 index 0000000..06e2ee4 --- /dev/null +++ b/src/with-paginator-helper.js @@ -0,0 +1,30 @@ +'use strict'; + +/** + * Calls the given operation method with the parameters and if the autopagination + * argument is `true` it will do calls to the operation method until the LastEvaluatedKey value from the + * method response has a falsy value. + * + * @param {Function} methodFn Any DynamoDB operation method to call. + * @param {Object} params Parameters as expected by DynamoDB operation to use. + * @param {boolean} autopagination Whether to return all the records from DynamoDB or just the first batch of records. + * @returns {Promise} A promise that resolves to the response from DynamoDB. + */ +async function withPaginatorHelper(methodFn, params, autopagination) { + const items = []; + let result = await methodFn(params).promise(); + result.Items && result.Items.length >= 0 && items.push(...result.Items); + + if (autopagination) { + while (result.LastEvaluatedKey) { + result = await methodFn({ ...params, ExclusiveStartKey: result.LastEvaluatedKey }).promise(); + result.Items && result.Items.length >= 0 && items.push(...result.Items); + } + } + + result.Items = items; + + return result; +} + +module.exports = withPaginatorHelper; From af31af3e5cad86809449dd0febc2d39bda21a440 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Thu, 21 Jan 2021 18:04:44 +0100 Subject: [PATCH 03/13] chore: remove dynamodbwrapper dependency --- package.json | 1 - yarn.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/package.json b/package.json index da4772b..0bee2e7 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "dependencies": { "dynamodb-data-types": "^3.0.1", "dynamodb-update-expression": "^0.1.21", - "dynamodb-wrapper": "^1.4.1", "ramda": "^0.27.1" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 6dc760d..8bfe8cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1637,11 +1637,6 @@ dynamodb-update-expression@^0.1.21: deepmerge "^1.2.0" lodash "^4.16.1" -dynamodb-wrapper@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/dynamodb-wrapper/-/dynamodb-wrapper-1.4.1.tgz#e4835f2d84dc06181879dd8a1c1e6db953569a54" - integrity sha1-5INfLYTcBhgYed2KHB5tuVNWmlQ= - ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" From 94abfc69cfc18eefecdc40212a6336c7527c7c45 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Thu, 21 Jan 2021 18:05:18 +0100 Subject: [PATCH 04/13] feat: remove deprecated pipeP --- src/get.js | 4 ++-- src/remove.js | 4 ++-- src/update.js | 16 ++++++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/get.js b/src/get.js index 532ba3a..2ce75dc 100644 --- a/src/get.js +++ b/src/get.js @@ -4,7 +4,7 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html * @module GetItem */ -const { curry, bind, compose, apply } = require('ramda'); +const { andThen, curry, bind, compose, apply } = require('ramda'); const { unwrapProp } = require('./wrapper'); const { mapMergeFirstPairOfArgs } = require('./map-merge-args'); const generateKey = require('./generate-key'); @@ -14,7 +14,7 @@ const addTableName = require('./table-name'); * @private */ const getUnwrappedItem = getItem => - compose(unwrapProp('Item'), request => request.promise(), apply(getItem)); + compose(andThen(unwrapProp('Item')), request => request.promise(), apply(getItem)); /** * @private diff --git a/src/remove.js b/src/remove.js index ed51046..e1632b8 100644 --- a/src/remove.js +++ b/src/remove.js @@ -7,7 +7,7 @@ * @module DeleteItem * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html */ -const { apply, bind, compose, curry } = require('ramda'); +const { apply, bind, compose, curry, andThen } = require('ramda'); const { unwrapProp } = require('./wrapper'); const addTableName = require('./table-name'); const { mapMergeFirstPairOfArgs } = require('./map-merge-args'); @@ -18,7 +18,7 @@ const addReturnValues = require('./return-values'); * @private */ const removeAndUnwrapAttributes = deleteItem => - compose(unwrapProp('Attributes'), request => request.promise(), apply(deleteItem)); + compose(andThen(unwrapProp('Attributes')), request => request.promise(), apply(deleteItem)); /** * @private diff --git a/src/update.js b/src/update.js index 7cbe0b5..6f63aac 100644 --- a/src/update.js +++ b/src/update.js @@ -5,7 +5,19 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html * @module UpdateItem */ -const { apply, applyTo, bind, compose, curry, ifElse, is, unless, has, partial } = require('ramda'); +const { + andThen, + apply, + applyTo, + bind, + compose, + curry, + ifElse, + is, + unless, + has, + partial +} = require('ramda'); const { getUpdateExpression } = require('dynamodb-update-expression'); const { unwrapProp, wrapOver } = require('./wrapper'); const addTableName = require('./table-name'); @@ -17,7 +29,7 @@ const generateKey = require('./generate-key'); * @private */ const updateAndUnwrapAttributes = updateItem => - compose(unwrapProp('Attributes'), request => request.promise(), apply(updateItem)); + compose(andThen(unwrapProp('Attributes')), request => request.promise(), apply(updateItem)); /** * @private From 3216cac16fa5bb46383a3b2d7ccf9eb3ce92fb52 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Thu, 21 Jan 2021 18:05:54 +0100 Subject: [PATCH 05/13] test: modify to use native dynamodb client --- src/batch-get-item.test.js | 15 ++++--- src/batch-write-item.test.js | 9 ++-- src/count.test.js | 12 ++++-- src/get-all.test.js | 18 +++++--- src/get.test.js | 21 ++++++---- src/insert.test.js | 15 ++++--- src/query.test.js | 32 ++++++++------ src/remove.test.js | 15 ++++--- src/update.test.js | 24 +++++++---- src/with-paginator-helper.test.js | 69 +++++++++++++++++++++++++++++++ 10 files changed, 175 insertions(+), 55 deletions(-) create mode 100644 src/with-paginator-helper.test.js diff --git a/src/batch-get-item.test.js b/src/batch-get-item.test.js index e8a7d79..b686990 100644 --- a/src/batch-get-item.test.js +++ b/src/batch-get-item.test.js @@ -2,7 +2,8 @@ const createGetBatcher = require('./batch-get-item'); test('should generate valid `RequestItems` with generated keys', async () => { - const mockBatchGetItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockBatchGetItem = jest.fn().mockReturnValue(mockRequest); const { batchGetFor } = createGetBatcher({ batchGetItem: mockBatchGetItem }); const batchGet = batchGetFor('some_table'); await batchGet([42, 33]); @@ -16,7 +17,8 @@ test('should generate valid `RequestItems` with generated keys', async () => { }); test('should support sending a single key as its first argument', async () => { - const mockBatchGetItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockBatchGetItem = jest.fn().mockReturnValue(mockRequest); const { batchGetFor } = createGetBatcher({ batchGetItem: mockBatchGetItem }); const batchGet = batchGetFor('some_table'); await batchGet(42); @@ -30,7 +32,8 @@ test('should support sending a single key as its first argument', async () => { }); test('should support retrieving documents with composite keys', async () => { - const mockBatchGetItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockBatchGetItem = jest.fn().mockReturnValue(mockRequest); const { batchGetFor } = createGetBatcher({ batchGetItem: mockBatchGetItem }); const batchGet = batchGetFor('some_table'); await batchGet([ @@ -50,7 +53,8 @@ test('should support retrieving documents with composite keys', async () => { }); test('should allow sending additional request properties as its sencond argument', async () => { - const mockBatchGetItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockBatchGetItem = jest.fn().mockReturnValue(mockRequest); const { batchGetFor } = createGetBatcher({ batchGetItem: mockBatchGetItem }); const batchGet = batchGetFor('some_table'); await batchGet([42, 33], { ProjectionExpression: 'name, messages, views', ConsistentRead: true }); @@ -65,7 +69,8 @@ test('should allow sending additional request properties as its sencond argument }); }); test('should forward any additional arguments to the original `batchGetItem` function', async () => { - const mockBatchGetItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockBatchGetItem = jest.fn().mockReturnValue(mockRequest); const { batchGetFor } = createGetBatcher({ batchGetItem: mockBatchGetItem }); const batchGet = batchGetFor('some_table'); await batchGet([42, 33], { ProjectionExpression: 'name' }, { extra: true }, 42); diff --git a/src/batch-write-item.test.js b/src/batch-write-item.test.js index 6dd00d8..bfee316 100644 --- a/src/batch-write-item.test.js +++ b/src/batch-write-item.test.js @@ -3,7 +3,8 @@ const createWriteBatcher = require('./batch-write-item'); describe('the `batchWriteFor` function', () => { test('should generate valid `RequestItems` with mixed `PutRequest` and `DeleteRequest` from given input', async () => { - const mockBatchWriteItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockBatchWriteItem = jest.fn().mockReturnValue(mockRequest); const { batchWriteFor } = createWriteBatcher({ batchWriteItem: mockBatchWriteItem }); const batchWrite = batchWriteFor('some_table'); await batchWrite({ @@ -24,7 +25,8 @@ describe('the `batchWriteFor` function', () => { describe('the `batchRemoveFor` function', () => { test('should generate valid `RequestItems` with `DeleteRequest` exclusively from given input', async () => { - const mockBatchWriteItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockBatchWriteItem = jest.fn().mockReturnValue(mockRequest); const { batchRemoveFor } = createWriteBatcher({ batchWriteItem: mockBatchWriteItem }); const batchRemove = batchRemoveFor('some_table'); await batchRemove([1, 2]); @@ -39,7 +41,8 @@ describe('the `batchRemoveFor` function', () => { }); test('should support removing using composite keys as input', async () => { - const mockBatchWriteItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockBatchWriteItem = jest.fn().mockReturnValue(mockRequest); const { batchRemoveFor } = createWriteBatcher({ batchWriteItem: mockBatchWriteItem }); const batchRemove = batchRemoveFor('some_table'); await batchRemove([ diff --git a/src/count.test.js b/src/count.test.js index 8f477cf..cbdf279 100644 --- a/src/count.test.js +++ b/src/count.test.js @@ -2,26 +2,30 @@ const createCounter = require('./count'); test('should call `scan` internally', async () => { - const mockScan = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({ Items: [] }) }; + const mockScan = jest.fn().mockReturnValue(mockRequest); const { count } = createCounter({ scan: mockScan }); await count(); expect(mockScan).toHaveBeenCalled(); }); test('should return `undefined` if no items are returned', async () => { - const mockScan = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({ Items: [] }) }; + const mockScan = jest.fn().mockReturnValue(mockRequest); const { count } = createCounter({ scan: mockScan }); expect(await count()).toBeUndefined(); }); test('should return the count of items according to `Count`', async () => { - const mockScan = jest.fn().mockResolvedValue({ Count: 7 }); + const mockRequest = { promise: jest.fn().mockResolvedValue({ Items: [], Count: 7 }) }; + const mockScan = jest.fn().mockReturnValue(mockRequest); const { count } = createCounter({ scan: mockScan }); expect(await count()).toEqual(7); }); test('should add a `TableName` if using countFor', async () => { - const mockScan = jest.fn().mockResolvedValue({ Count: 7 }); + const mockRequest = { promise: jest.fn().mockResolvedValue({ Items: [], Count: 7 }) }; + const mockScan = jest.fn().mockReturnValue(mockRequest); const { countFor } = createCounter({ scan: mockScan }); await countFor('Foo')(); expect(mockScan).toHaveBeenCalledWith( diff --git a/src/get-all.test.js b/src/get-all.test.js index 8d4dd1e..0870ebd 100644 --- a/src/get-all.test.js +++ b/src/get-all.test.js @@ -3,20 +3,25 @@ const createAllGetter = require('./get-all'); describe('the getAllFor function', () => { test('should call `scan` internally', async () => { - const mockScan = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockScan = jest.fn().mockReturnValue(mockRequest); const { getAll } = createAllGetter({ scan: mockScan }); await getAll(); expect(mockScan).toHaveBeenCalled(); }); - test('should return `undefined` if no items are returned', async () => { - const mockScan = jest.fn().mockResolvedValue({}); + test('should return `[]` if no items are returned', async () => { + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockScan = jest.fn().mockReturnValue(mockRequest); const { getAll } = createAllGetter({ scan: mockScan }); - expect(await getAll()).toBeUndefined(); + expect(await getAll()).toStrictEqual([]); }); test('should return unwrapped items', async () => { - const mockScan = jest.fn().mockResolvedValue({ Items: [{ foo: { S: 'bar' } }] }); + const mockRequest = { + promise: jest.fn().mockResolvedValue({ Items: [{ foo: { S: 'bar' } }] }) + }; + const mockScan = jest.fn().mockReturnValue(mockRequest); const { getAll } = createAllGetter({ scan: mockScan }); expect(await getAll()).toEqual([{ foo: 'bar' }]); }); @@ -24,7 +29,8 @@ describe('the getAllFor function', () => { describe('the getAllFor function', () => { test('should add `TableName` automatically to any request', async () => { - const mockScan = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockScan = jest.fn().mockReturnValue(mockRequest); const { getAllFor } = createAllGetter({ scan: mockScan }); await getAllFor('some_table')({}); expect(mockScan).toHaveBeenCalledWith( diff --git a/src/get.test.js b/src/get.test.js index 0c56823..92f1c5d 100644 --- a/src/get.test.js +++ b/src/get.test.js @@ -3,14 +3,16 @@ const createGetter = require('./get'); describe('the get function', () => { test('should call `getItem` internally', async () => { - const mockGetItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockGetItem = jest.fn().mockReturnValue(mockRequest); const { get } = createGetter({ getItem: mockGetItem }); await get(); expect(mockGetItem).toHaveBeenCalled(); }); test('should generate a wrapped `Key` attribute from the first argument', async () => { - const mockGetItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockGetItem = jest.fn().mockReturnValue(mockRequest); const { get } = createGetter({ getItem: mockGetItem }); await get({ id: 5 }); expect(mockGetItem).toHaveBeenCalledWith({ @@ -19,7 +21,8 @@ describe('the get function', () => { }); test('should default to `id` when generating `Key attribute`', async () => { - const mockGetItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockGetItem = jest.fn().mockReturnValue(mockRequest); const { get } = createGetter({ getItem: mockGetItem }); await get('41ab0092-45bc-4cf7-8d5c-9bd4fcfa37ae'); expect(mockGetItem).toHaveBeenCalledWith({ @@ -28,7 +31,8 @@ describe('the get function', () => { }); test('should accept and merge additional request attributes', async () => { - const mockGetItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockGetItem = jest.fn().mockReturnValue(mockRequest); const { get } = createGetter({ getItem: mockGetItem }); await get(5, { foo: 'bar' }); expect(mockGetItem).toHaveBeenCalledWith({ @@ -38,13 +42,15 @@ describe('the get function', () => { }); test('should return `undefined` if no item is returned', async () => { - const mockGetItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockGetItem = jest.fn().mockReturnValue(mockRequest); const { get } = createGetter({ getItem: mockGetItem }); expect(await get()).toBeUndefined(); }); test('should return an unwrapped item', async () => { - const mockGetItem = jest.fn().mockResolvedValue({ Item: { foo: { S: 'bar' } } }); + const mockRequest = { promise: jest.fn().mockResolvedValue({ Item: { foo: { S: 'bar' } } }) }; + const mockGetItem = jest.fn().mockReturnValue(mockRequest); const { get } = createGetter({ getItem: mockGetItem }); expect(await get()).toEqual({ foo: 'bar' }); }); @@ -52,7 +58,8 @@ describe('the get function', () => { describe('the getFor function', () => { test('should add `TableName` automatically to any request', async () => { - const mockGetItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockGetItem = jest.fn().mockReturnValue(mockRequest); const { getFor } = createGetter({ getItem: mockGetItem }); await getFor('some_table')({}); expect(mockGetItem).toHaveBeenCalledWith( diff --git a/src/insert.test.js b/src/insert.test.js index 0c2f3e0..b31a619 100644 --- a/src/insert.test.js +++ b/src/insert.test.js @@ -3,14 +3,16 @@ const createInserter = require('./insert'); describe('the insert function', () => { test('should call `putItem` internally', async () => { - const mockPutItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockPutItem = jest.fn().mockReturnValue(mockRequest); const { insert } = createInserter({ putItem: mockPutItem }); await insert(); expect(mockPutItem).toHaveBeenCalled(); }); test('should generate an `Item` out of first argument', async () => { - const mockPutItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockPutItem = jest.fn().mockReturnValue(mockRequest); const { insert } = createInserter({ putItem: mockPutItem }); await insert({ foo: 'bar' }); expect(mockPutItem).toHaveBeenCalledWith({ @@ -19,7 +21,8 @@ describe('the insert function', () => { }); test('should respect `Item` if present on first argument', async () => { - const mockPutItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockPutItem = jest.fn().mockReturnValue(mockRequest); const { insert } = createInserter({ putItem: mockPutItem }); await insert({ Item: { foo: 'bar' } }); expect(mockPutItem).toHaveBeenCalledWith({ @@ -28,7 +31,8 @@ describe('the insert function', () => { }); test('should merge first pair of provided arguments', async () => { - const mockPutItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockPutItem = jest.fn().mockReturnValue(mockRequest); const { insert } = createInserter({ putItem: mockPutItem }); await insert({ foo: 'bar' }, { TableName: 'some_table' }); expect(mockPutItem).toHaveBeenCalledWith({ @@ -40,7 +44,8 @@ describe('the insert function', () => { describe('the insertFor function', () => { test('should add `TableName` automatically to any request', async () => { - const mockPutItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockPutItem = jest.fn().mockReturnValue(mockRequest); const { insertFor } = createInserter({ putItem: mockPutItem }); await insertFor('some_table')({}); expect(mockPutItem).toHaveBeenCalledWith( diff --git a/src/query.test.js b/src/query.test.js index 9563aa6..0215a9c 100644 --- a/src/query.test.js +++ b/src/query.test.js @@ -3,26 +3,34 @@ const createQuerier = require('./query'); describe('the query function', () => { test('should call `query` internally', async () => { - const mockQuery = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockQuery = jest.fn().mockReturnValue(mockRequest); const { query } = createQuerier({ query: mockQuery }); await query(); expect(mockQuery).toHaveBeenCalled(); }); - test('should return `undefined` if no items are returned', async () => { - const mockQuery = jest.fn().mockResolvedValue({}); + test('should return `[]` if no items are returned', async () => { + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockQuery = jest.fn().mockReturnValue(mockRequest); const { query } = createQuerier({ query: mockQuery }); - expect(await query()).toBeUndefined(); + expect(await query()).toStrictEqual([]); }); test('should return unwrapped items', async () => { - const mockQuery = jest.fn().mockResolvedValue({ Items: [{ foo: { S: 'bar' } }] }); + const mockRequest = { + promise: jest.fn().mockResolvedValue({ Items: [{ foo: { S: 'bar' } }] }) + }; + const mockQuery = jest.fn().mockReturnValue(mockRequest); const { query } = createQuerier({ query: mockQuery }); expect(await query()).toEqual([{ foo: 'bar' }]); }); test('should return a full response object with the items unwrapped', async () => { - const mockQuery = jest.fn().mockResolvedValue({ Items: [{ foo: { S: 'bar' } }], Count: 1 }); + const mockRequest = { + promise: jest.fn().mockResolvedValue({ Items: [{ foo: { S: 'bar' } }], Count: 1 }) + }; + const mockQuery = jest.fn().mockReturnValue(mockRequest); const { query } = createQuerier({ query: mockQuery }); expect(await query({}, { raw: true })).toEqual({ Items: [{ foo: 'bar' }], Count: 1 }); }); @@ -30,19 +38,20 @@ describe('the query function', () => { describe('the queryFor function', () => { test('should add `TableName` automatically to any request', async () => { - const mockQuery = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockQuery = jest.fn().mockReturnValue(mockRequest); const { queryFor } = createQuerier({ query: mockQuery }); await queryFor('some_table')({}); expect(mockQuery).toHaveBeenCalledWith( expect.objectContaining({ TableName: 'some_table' - }), - undefined + }) ); }); test('should create a query function with two arguments', async () => { - const mockQuery = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockQuery = jest.fn().mockReturnValue(mockRequest); const { queryFor } = createQuerier({ query: mockQuery }); const query = queryFor('some_table'); await query({}, { raw: true }); @@ -50,8 +59,7 @@ describe('the queryFor function', () => { expect(mockQuery).toHaveBeenCalledWith( expect.objectContaining({ TableName: 'some_table' - }), - { raw: true } + }) ); }); }); diff --git a/src/remove.test.js b/src/remove.test.js index 69d9425..0cffbc3 100644 --- a/src/remove.test.js +++ b/src/remove.test.js @@ -3,14 +3,16 @@ const createRemover = require('./remove'); describe('the remove function', () => { test('should call `deleteItem` internally', async () => { - const mockDeleteItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockDeleteItem = jest.fn().mockReturnValue(mockRequest); const { remove } = createRemover({ deleteItem: mockDeleteItem }); await remove(); expect(mockDeleteItem).toHaveBeenCalled(); }); test('should generate a wrapped `Key` attribute from the first argument', async () => { - const mockDeleteItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockDeleteItem = jest.fn().mockReturnValue(mockRequest); const { remove } = createRemover({ deleteItem: mockDeleteItem }); await remove({ id: 5 }); expect(mockDeleteItem).toHaveBeenCalledWith( @@ -21,7 +23,8 @@ describe('the remove function', () => { }); test('should default to `id` when generating `Key` attribute', async () => { - const mockDeleteItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockDeleteItem = jest.fn().mockReturnValue(mockRequest); const { remove } = createRemover({ deleteItem: mockDeleteItem }); await remove('41ab0092-45bc-4cf7-8d5c-9bd4fcfa37ae'); expect(mockDeleteItem).toHaveBeenCalledWith( @@ -32,7 +35,8 @@ describe('the remove function', () => { }); test('should make `ReturnValues` default to `ALL_OLD` when generating request', async () => { - const mockDeleteItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockDeleteItem = jest.fn().mockReturnValue(mockRequest); const { remove } = createRemover({ deleteItem: mockDeleteItem }); await remove('41ab0092-45bc-4cf7-8d5c-9bd4fcfa37ae'); expect(mockDeleteItem).toHaveBeenCalledWith( @@ -45,7 +49,8 @@ describe('the remove function', () => { describe('the removeFor function', () => { test('should add `TableName` automatically to any request', async () => { - const mockDeleteItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockDeleteItem = jest.fn().mockReturnValue(mockRequest); const { removeFor } = createRemover({ deleteItem: mockDeleteItem }); await removeFor('some_table')({ id: 5 }); expect(mockDeleteItem).toHaveBeenCalledWith( diff --git a/src/update.test.js b/src/update.test.js index 58903c8..4c3e0d0 100644 --- a/src/update.test.js +++ b/src/update.test.js @@ -3,14 +3,16 @@ const createUpdater = require('./update'); describe('the update function', () => { test('should call `updateItem` internally', async () => { - const mockUpdateItem = jest.fn().mockResolvedValue({}); + const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; + const mockUpdateItem = jest.fn().mockReturnValue(mockRequest); const { update } = createUpdater({ updateItem: mockUpdateItem }); await update(); expect(mockUpdateItem).toHaveBeenCalled(); }); test('should generate a wrapped `Key` attribute from the first argument', async () => { - const mockUpdateItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockUpdateItem = jest.fn().mockReturnValue(mockRequest); const { update } = createUpdater({ updateItem: mockUpdateItem }); await update({ id: 5 }); expect(mockUpdateItem).toHaveBeenCalledWith( @@ -21,7 +23,8 @@ describe('the update function', () => { }); test('should default to `id` when generating `Key attribute`', async () => { - const mockUpdateItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockUpdateItem = jest.fn().mockReturnValue(mockRequest); const { update } = createUpdater({ updateItem: mockUpdateItem }); await update(5); expect(mockUpdateItem).toHaveBeenCalledWith( @@ -32,7 +35,8 @@ describe('the update function', () => { }); test('should generate SET `UpdateExpression` from second argument', async () => { - const mockUpdateItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockUpdateItem = jest.fn().mockReturnValue(mockRequest); const { update } = createUpdater({ updateItem: mockUpdateItem }); await update(5, { foo: 'bar' }); expect(mockUpdateItem).toHaveBeenCalledWith( @@ -46,7 +50,8 @@ describe('the update function', () => { }); test('should honor existing `UpdateExpression` if provided', async () => { - const mockUpdateItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockUpdateItem = jest.fn().mockReturnValue(mockRequest); const { update } = createUpdater({ updateItem: mockUpdateItem }); await update(5, { UpdateExpression: 'SET #bar = :bar' @@ -60,7 +65,8 @@ describe('the update function', () => { }); test('should run the helper to generate `UpdateExpression` if the second argument is a function', async () => { - const mockUpdateItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockUpdateItem = jest.fn().mockReturnValue(mockRequest); const { update } = createUpdater({ updateItem: mockUpdateItem }); const expressionBuilder = () => ({ UpdateExpression: 'SET #bar = :bar' @@ -75,7 +81,8 @@ describe('the update function', () => { }); test('should merge the first three arguments and pass the rest as is', async () => { - const mockUpdateItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockUpdateItem = jest.fn().mockReturnValue(mockRequest); const { update } = createUpdater({ updateItem: mockUpdateItem }); await update(5, {}, { bar: 'baz' }, { meh: 42 }); expect(mockUpdateItem).toHaveBeenCalledWith( @@ -93,7 +100,8 @@ describe('the update function', () => { describe('the updateFor function', () => { test('should add `TableName` automatically to any request', async () => { - const mockUpdateItem = jest.fn().mockResolvedValue(true); + const mockRequest = { promise: jest.fn().mockResolvedValue(true) }; + const mockUpdateItem = jest.fn().mockReturnValue(mockRequest); const { updateFor } = createUpdater({ updateItem: mockUpdateItem }); await updateFor('some_table')({ id: 5 }); expect(mockUpdateItem).toHaveBeenCalledWith( diff --git a/src/with-paginator-helper.test.js b/src/with-paginator-helper.test.js new file mode 100644 index 0000000..9c276ee --- /dev/null +++ b/src/with-paginator-helper.test.js @@ -0,0 +1,69 @@ +'use strict'; + +const withPaginatorHelper = require('./with-paginator-helper'); + +describe('the `withPaginatorHelper` function', () => { + test('should call given fn only once with the given parameters', async () => { + const mockRequest = { + promise: jest.fn().mockResolvedValue({ + Items: [{ id: { S: 'foo' } }, { id: { S: 'bar' } }], + LastEvaluatedKey: { id: { S: 'bar' } } + }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + const result = await withPaginatorHelper(mockFn, { TableName: 'foo' }); + + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenLastCalledWith({ TableName: 'foo' }); + expect(result).toEqual({ + Items: [{ id: { S: 'foo' } }, { id: { S: 'bar' } }], + LastEvaluatedKey: { id: { S: 'bar' } } + }); + }); + + test('should call given fn multiple times with the given parameters', async () => { + const mockRequest = { + promise: jest + .fn() + .mockResolvedValueOnce({ + Items: [{ id: { S: 'foo' } }], + LastEvaluatedKey: { id: { S: 'foo' } } + }) + .mockResolvedValueOnce({ Items: [{ id: { S: 'bar' } }] }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + + const result = await withPaginatorHelper(mockFn, { TableName: 'foo' }, true); + + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenNthCalledWith(1, { TableName: 'foo' }); + expect(mockFn).toHaveBeenNthCalledWith(2, { + TableName: 'foo', + ExclusiveStartKey: { id: { S: 'foo' } } + }); + expect(result).toHaveProperty('Items', [{ id: { S: 'foo' } }, { id: { S: 'bar' } }]); + }); + + test('should return a single object with all the responses Items', async () => { + const mockRequest = { + promise: jest + .fn() + .mockResolvedValueOnce({ + Items: [{ id: { S: 'foo' } }], + LastEvaluatedKey: { id: { S: 'foo' } } + }) + .mockResolvedValueOnce({ Items: [] }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + + const result = await withPaginatorHelper(mockFn, { TableName: 'foo' }, true); + + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenNthCalledWith(1, { TableName: 'foo' }); + expect(mockFn).toHaveBeenNthCalledWith(2, { + TableName: 'foo', + ExclusiveStartKey: { id: { S: 'foo' } } + }); + expect(result).toHaveProperty('Items', [{ id: { S: 'foo' } }]); + }); +}); From e66922ab23955af887f890a9f5ac41213d75a65d Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Thu, 21 Jan 2021 18:13:58 +0100 Subject: [PATCH 06/13] chore: update type declaration --- index.d.ts | 96 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 28 deletions(-) diff --git a/index.d.ts b/index.d.ts index 77c6a4f..fb2b88f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,12 +1,10 @@ import { DynamoDB } from 'aws-sdk'; -import DynamoDBWrapper from 'dynamodb-wrapper'; -declare module "@flybondi/flynamo" { +declare module '@flybondi/flynamo' { module Flynamo { export type KeyAttributes = { [key: string]: string | number }; export type Key = string | number | KeyAttributes | { Key: KeyAttributes }; export type Item = { [key: string]: any }; - export interface BatchGetItemInput { ConsistentRead?: DynamoDB.ConsistentRead; ReturnConsumedCapacity?: DynamoDB.ReturnConsumedCapacity; @@ -17,29 +15,50 @@ declare module "@flybondi/flynamo" { export type BatchWriteItemInput = Omit; export interface BatchWriteItemOperations { - insert?: Item | Item[], - remove?: Key | Key[] + insert?: Item | Item[]; + remove?: Key | Key[]; } export type GetItemInput = Omit; - export type PutItemInput = Omit; + export type PutItemInput = Omit< + DynamoDB.PutItemInput, + 'TableName' | 'Item' | 'Expected' | 'ConditionalOperator' + >; - export type QueryInput = Omit; + export type QueryInput = Omit< + DynamoDB.QueryInput, + 'TableName' | 'AttributesToGet' | 'KeyConditions' | 'QueryFilter' | 'ConditionalOperator' + >; - export type DeleteItemInput = Omit; + export type DeleteItemInput = Omit< + DynamoDB.DeleteItemInput, + 'TableName' | 'Key' | 'Expected' | 'ConditionalOperator' + >; - export type UpdateItemInput = Omit; + export type UpdateItemInput = Omit< + DynamoDB.UpdateItemInput, + 'TableName' | 'Key' | 'AttributeUpdates' | 'Expected' | 'ConditionalOperator' + >; - export interface QueryOptions extends DynamoDBWrapper.IQueryOptions { + export interface OperationOptions { + /** + * Wheter to return all the DynamoDB response pages or just one page. + */ + autopagination?: boolean; /** * Whether to return the full `AWS.DynamoDB` response object when `true` or just the `Items` property value. */ raw?: boolean; } - type UpdateExpressionBuilderInput = Pick; - export type UpdateExpressionBuilder = (input: UpdateExpressionBuilderInput) => DynamoDB.ExpressionAttributeValueMap; + type UpdateExpressionBuilderInput = Pick< + DynamoDB.Update, + 'UpdateExpression' | 'ExpressionAttributeNames' | 'ExpressionAttributeValues' + >; + export type UpdateExpressionBuilder = ( + input: UpdateExpressionBuilderInput + ) => DynamoDB.ExpressionAttributeValueMap; } export interface Flynamo { /** @@ -66,7 +85,10 @@ declare module "@flybondi/flynamo" { * @param input - Optional settings supported by `AWS.DynamoDB` for this operation. * @returns Resolves to the response from `AWS.DynamoDB` client for a `BatchGetItem` operation. */ - batchGet: (keys: Flynamo.Key | Flynamo.Key[], input?: Flynamo.BatchGetItemInput) => Promise, + batchGet: ( + keys: Flynamo.Key | Flynamo.Key[], + input?: Flynamo.BatchGetItemInput + ) => Promise; /** * Puts (i.e.: inserts) items in a table in a single batch. @@ -81,7 +103,10 @@ declare module "@flybondi/flynamo" { * @param input - Optional settings supported by `AWS.DynamoDB` for this operation. * @returns Resolves to the response from `AWS.DynamoDB` client for a `BatchWriteItem` operation. */ - batchInsert: (items: Flynamo.Item | Flynamo.Item[], input?: Flynamo.BatchWriteItemInput) => Promise, + batchInsert: ( + items: Flynamo.Item | Flynamo.Item[], + input?: Flynamo.BatchWriteItemInput + ) => Promise; /** * Deletes multiple items from a table in a single batch. @@ -102,8 +127,10 @@ declare module "@flybondi/flynamo" { * @param input - Optional settings supported by `AWS.DynamoDB` for this operation. * @returns Resolves to the response from `AWS.DynamoDB` client for a `BatchWriteItem` operation. */ - batchRemove: (keys: Flynamo.Key | Flynamo.Key[], input?: Flynamo.BatchWriteItemInput) => Promise, - + batchRemove: ( + keys: Flynamo.Key | Flynamo.Key[], + input?: Flynamo.BatchWriteItemInput + ) => Promise; /** * Puts (i.e.: inserts) or deletes multiple items in a table in a single operation. It expects an @@ -124,7 +151,10 @@ declare module "@flybondi/flynamo" { * @param operations - An object containing `insert` and/or `remove` properties. * @returns - Resolves to the response from `AWS.DynamoDB` client for a `BatchWriteItem` operation. */ - batchWrite: (operations: Flynamo.BatchWriteItemOperations, input?: Flynamo.BatchWriteItemInput) => Promise, + batchWrite: ( + operations: Flynamo.BatchWriteItemOperations, + input?: Flynamo.BatchWriteItemInput + ) => Promise; /** * Return the number of elements in a table or a secondary index. This function @@ -138,7 +168,7 @@ declare module "@flybondi/flynamo" { * @param request - Parameters as expected by DynamoDB `Scan` operation. * @returns A `Promise` that resolves to the total number of elements */ - count: () => Promise, + count: () => Promise; /** * Returns a set of attributes for the item with the given primary key. @@ -167,7 +197,7 @@ declare module "@flybondi/flynamo" { * @returns A `Promise` that resolves to the item returned by `AWS.DynamoDB` response or `undefined` if it * does not exist. */ - get: (key: Flynamo.Key, input?: Flynamo.GetItemInput) => Promise, + get: (key: Flynamo.Key, input?: Flynamo.GetItemInput) => Promise; /** * Returns all items in a table or a secondary index. This uses `Scan` internally. @@ -178,9 +208,13 @@ declare module "@flybondi/flynamo" { * await getAll(); * * @param input - Parameters as expected by `AWS.DynamoDB` `Scan` operation. + * @param options - The configuration options parameters. * @returns A `Promise` that resolves to an array of `Items` returned by the `AWS.DynamoDB` response. */ - getAll: (input?: DynamoDB.ScanInput) => Promise, + getAll: ( + input?: DynamoDB.ScanInput, + options?: Flynamo.OperationOptions + ) => Promise; /** * Creates a new item, or replaces an old item with a new item. @@ -197,7 +231,7 @@ declare module "@flybondi/flynamo" { * @param input - Optional parameters as expected by `AWS.DynamoDB` `PutItem` operation. * @returns Resolves to the response from DynamoDB client. */ - insert: (item: Flynamo.Item, input?: Flynamo.PutItemInput) => Promise, + insert: (item: Flynamo.Item, input?: Flynamo.PutItemInput) => Promise; /** * Finds items based on primary key values. @@ -218,7 +252,10 @@ declare module "@flybondi/flynamo" { * @param options - Configuration options parameters. * @returns A `Promise` that resolves to either an array of returned `Items` or the full, raw response from `AWS.DynamoDB`. */ - query: (input: Flynamo.QueryInput, options?: Flynamo.QueryOptions) => Promise, + query: ( + input: Flynamo.QueryInput, + options?: Flynamo.OperationOptions + ) => Promise; /** * Deletes a single item in a table by primary key. Returns the recently removed item by default @@ -235,7 +272,7 @@ declare module "@flybondi/flynamo" { * @returns A `Promise` that resolves to the `Attributes` property of the DynamoDB response. Unless, `ReturnValues` has * been explicitly set, this will match all attributes of the recently deleted element. */ - remove: (key: Flynamo.Key, input?: Flynamo.DeleteItemInput) => Promise, + remove: (key: Flynamo.Key, input?: Flynamo.DeleteItemInput) => Promise; /** * Edits an existing item's attributes, or adds a new item to the table if it does not @@ -257,7 +294,11 @@ declare module "@flybondi/flynamo" { * will override any value automatically derived from `itemOrBuilder`. * @returns A `Promise` that resolves to the `Attributes` property of the `AWS.DynamoDB` response. */ - update: (key: Flynamo.Key, itemOrBuilder: Flynamo.Item | Flynamo.UpdateExpressionBuilder, input?: Flynamo.UpdateItemInput) => Promise + update: ( + key: Flynamo.Key, + itemOrBuilder: Flynamo.Item | Flynamo.UpdateExpressionBuilder, + input?: Flynamo.UpdateItemInput + ) => Promise; } export interface FlynamoClient { @@ -274,7 +315,7 @@ declare module "@flybondi/flynamo" { * @param tableName - The value of `TableName`. * @returns The entire `Flynamo` scoped to a single table. */ - forTable(tableName: string): Flynamo + forTable(tableName: string): Flynamo; } /** @@ -284,9 +325,8 @@ declare module "@flybondi/flynamo" { * @see https://github.com/Shadowblazen/dynamodb-wrapper#setup * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html * @param client - A `AWS.DynamoDB` client. - * @param options - `DynamoDBWrapper` configuration (optional). - */ - export declare function flynamo(dynamodb: DynamoDB, options?: DynamoDBWrapper.IDynamoDBWrapperOptions): FlynamoClient; + */ + export declare function flynamo(dynamodb: DynamoDB): FlynamoClient; export = flynamo; } From b59162df6572fcaef8ebb88c49017bbba1aadedc Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Thu, 21 Jan 2021 18:20:22 +0100 Subject: [PATCH 07/13] chore: rename dynamoWrapper --- src/batch-get-item.js | 6 +++--- src/batch-write-item.js | 6 +++--- src/count.js | 4 ++-- src/describe-table.js | 4 ++-- src/get-all.js | 4 ++-- src/get.js | 4 ++-- src/insert.js | 4 ++-- src/query.js | 16 ++++++++-------- src/remove.js | 4 ++-- src/update.js | 4 ++-- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/batch-get-item.js b/src/batch-get-item.js index a9adead..7066684 100644 --- a/src/batch-get-item.js +++ b/src/batch-get-item.js @@ -47,11 +47,11 @@ const createBatchGetFor = curry((batchGetItem, table) => * Creates a function to allow retrieval of several documents in a single batch. * * @private - * @param {Object} dynamoWrapper The AWS DynamoDB client + * @param {Object} dynamodb The AWS DynamoDB client * @returns {Object} */ -function createGetBatcher(dynamoWrapper) { - const batchGetItem = bind(dynamoWrapper.batchGetItem, dynamoWrapper); +function createGetBatcher(dynamodb) { + const batchGetItem = bind(dynamodb.batchGetItem, dynamodb); return { /** * Returns the attributes of one or more items from a table. Requested items are identified by primary key. diff --git a/src/batch-write-item.js b/src/batch-write-item.js index 4617173..8acf95e 100644 --- a/src/batch-write-item.js +++ b/src/batch-write-item.js @@ -111,11 +111,11 @@ const createBatchRequestFor = curry((batchWriteItem, requestType, table) => * Creates functions to allow removal and insertions of documents in a single batch. * * @private - * @param {Object} dynamoWrapper The AWS DynamoDB client + * @param {Object} dynamodb The AWS DynamoDB client * @returns {Object} */ -function createWriteBatcher(dynamoWrapper) { - const batchWriteItem = bind(dynamoWrapper.batchWriteItem, dynamoWrapper); +function createWriteBatcher(dynamodb) { + const batchWriteItem = bind(dynamodb.batchWriteItem, dynamodb); return { /** * Returns a function that puts or deletes multiple items in a table. It expects a single diff --git a/src/count.js b/src/count.js index 9ed68be..fbc742c 100644 --- a/src/count.js +++ b/src/count.js @@ -18,8 +18,8 @@ const createCount = scan => params => withPaginatorHelper(scan, params, true).th */ const createCountFor = curry((scan, table) => compose(createCount(scan), addTableName(table))); -function createCounter(dynamoWrapper) { - const scan = bind(dynamoWrapper.scan, dynamoWrapper); +function createCounter(dynamodb) { + const scan = bind(dynamodb.scan, dynamodb); return { /** * Return the number of elements in a table or a secondary index. This function diff --git a/src/describe-table.js b/src/describe-table.js index 0708bca..d17ce07 100644 --- a/src/describe-table.js +++ b/src/describe-table.js @@ -14,8 +14,8 @@ const createDescribeTableFor = curry((describeTable, table) => compose(describeTable, addTableName(table)) ); -function createDescribers(dynamoWrapper) { - const describeTable = bind(dynamoWrapper.describeTable, dynamoWrapper); +function createDescribers(dynamodb) { + const describeTable = bind(dynamodb.describeTable, dynamodb); return { /** * Retrieves information about the table. diff --git a/src/get-all.js b/src/get-all.js index e3946e6..f899fd4 100644 --- a/src/get-all.js +++ b/src/get-all.js @@ -29,8 +29,8 @@ const createGetAll = scan => (params, options = {}) => { */ const createGetAllFor = curry((scan, table) => compose(createGetAll(scan), addTableName(table))); -function createAllGetter(dynamoWrapper) { - const scan = bind(dynamoWrapper.scan, dynamoWrapper); +function createAllGetter(dynamodb) { + const scan = bind(dynamodb.scan, dynamodb); return { /** * Returns all items in a table or a secondary index. This uses `Scan` internally. diff --git a/src/get.js b/src/get.js index 2ce75dc..e2b9258 100644 --- a/src/get.js +++ b/src/get.js @@ -32,8 +32,8 @@ const createGetFor = curry((getItem, table) => ) ); -function createGetter(dynamoWrapper) { - const getItem = bind(dynamoWrapper.getItem, dynamoWrapper); +function createGetter(dynamodb) { + const getItem = bind(dynamodb.getItem, dynamodb); return { /** * Returns a set of attributes for the item with the given primary key. diff --git a/src/insert.js b/src/insert.js index f73d30b..8b6878a 100644 --- a/src/insert.js +++ b/src/insert.js @@ -22,8 +22,8 @@ const createInsertFor = curry((putItem, table) => compose(apply(putItem), mapMergeFirstPairOfArgs(compose(addTableName(table), generateItem))) ); -function createInserter(dynamoWrapper) { - const putItem = bind(dynamoWrapper.putItem, dynamoWrapper); +function createInserter(dynamodb) { + const putItem = bind(dynamodb.putItem, dynamodb); return { /** * Creates a new item, or replaces an old item with a new item. diff --git a/src/query.js b/src/query.js index f074c4f..66a3e6e 100644 --- a/src/query.js +++ b/src/query.js @@ -56,11 +56,11 @@ function createQuerier(dynamodb) { * }); * * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#API_Query_RequestSyntax - * @param {Object} request Parameters as expected by DynamoDB `Query` operation. Must contain, at least, `TableName` attribute. + * @param {Object} request Parameters as expected by dynamodb `Query` operation. Must contain, at least, `TableName` attribute. * @param {Object} [options] The configuration options parameters. - * @param {boolean} [options.autopagination=true] Whether to return all the DynamoDB response pages or just one page. - * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. - * @returns {Promise} A promise that resolves to the response from DynamoDB. + * @param {boolean} [options.autopagination=true] Whether to return all the dynamodb response pages or just one page. + * @param {boolean} [options.raw=false] Whether to return the full dynamodb response object when `true` or just the `Items` property value. + * @returns {Promise} A promise that resolves to the response from dynamodb. */ query: createQuery(query), @@ -97,11 +97,11 @@ function createQuerier(dynamodb) { * * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#API_Query_RequestSyntax * @param {String} tableName The name of the table to perform the operation on - * @param {Object=} request Parameters as expected by DynamoDB `Query` operation. A `TableName` attributes specified here will override `tableName` argument. + * @param {Object=} request Parameters as expected by dynamodb `Query` operation. A `TableName` attributes specified here will override `tableName` argument. * @param {Object} [options] The configuration options parameters. - * @param {boolean} [options.autopagination=true] Wheter to return all the DynamoDB response pages or just one page. - * @param {boolean} [options.raw=false] Whether to return the full DynamoDB response object when `true` or just the `Items` property value. - * @returns {Promise} A promise that resolves to the response from DynamoDB. + * @param {boolean} [options.autopagination=true] Wheter to return all the dynamodb response pages or just one page. + * @param {boolean} [options.raw=false] Whether to return the full dynamodb response object when `true` or just the `Items` property value. + * @returns {Promise} A promise that resolves to the response from dynamodb. */ queryFor: createQueryFor(query) }; diff --git a/src/remove.js b/src/remove.js index e1632b8..0543390 100644 --- a/src/remove.js +++ b/src/remove.js @@ -39,8 +39,8 @@ const createRemoveFor = curry((deleteItem, table) => ) ); -function createRemover(dynamoWrapper) { - const deleteItem = bind(dynamoWrapper.deleteItem, dynamoWrapper); +function createRemover(dynamodb) { + const deleteItem = bind(dynamodb.deleteItem, dynamodb); return { /** * Deletes a single item in a table by primary key. Returns the `Attributes` present in the diff --git a/src/update.js b/src/update.js index 6f63aac..28f490f 100644 --- a/src/update.js +++ b/src/update.js @@ -93,8 +93,8 @@ const createUpdateFor = curry((updateItem, table) => ) ); -function createUpdater(dynamoWrapper) { - const updateItem = bind(dynamoWrapper.updateItem, dynamoWrapper); +function createUpdater(dynamodb) { + const updateItem = bind(dynamodb.updateItem, dynamodb); return { /** * Edits an existing item's attributes, or adds a new item to the table if it does not From 8f37a8f232fbb3d8ccb9770afeb7d0fb59e5a4e1 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Thu, 21 Jan 2021 18:43:25 +0100 Subject: [PATCH 08/13] docs: remove old reference for wrapper --- index.d.ts | 5 ++--- src/client.js | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/index.d.ts b/index.d.ts index fb2b88f..a665753 100644 --- a/index.d.ts +++ b/index.d.ts @@ -319,10 +319,9 @@ declare module '@flybondi/flynamo' { } /** - * Wraps an `AWS.DynamoDB` instance and returns a new `Flynamo` client. - * Configurable `options` for `dynamodb-wrapper` may be provided. + * Grabs an AWS DynamoDB `client` and returns Flynamo's API to access + * its methods. * - * @see https://github.com/Shadowblazen/dynamodb-wrapper#setup * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html * @param client - A `AWS.DynamoDB` client. */ diff --git a/src/client.js b/src/client.js index f9f8e78..60415bb 100644 --- a/src/client.js +++ b/src/client.js @@ -10,10 +10,9 @@ const createWriteBatcher = require('./batch-write-item'); const createGetBatcher = require('./batch-get-item'); /** - * Wraps an AWS DynamoDB `client` and returns Flynamo's API to access - * its methods. Optionally, a `config` object for `dynamodb-wrapper` may be provided. + * Grabs an AWS DynamoDB `client` and returns Flynamo's API to access + * its methods. * - * @see https://github.com/Shadowblazen/dynamodb-wrapper#setup * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html * @param {Object} client A DynamoDB client */ From c5ac2c935fc94c333b3675d678bbd8e99c849755 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Fri, 22 Jan 2021 15:24:41 +0100 Subject: [PATCH 09/13] feat: add aggregate function for counts and capacities --- src/with-paginator-helper.js | 30 ---- src/with-paginator-helper.test.js | 69 ------- src/with-paginator.js | 125 +++++++++++++ src/with-paginator.test.js | 286 ++++++++++++++++++++++++++++++ 4 files changed, 411 insertions(+), 99 deletions(-) delete mode 100644 src/with-paginator-helper.js delete mode 100644 src/with-paginator-helper.test.js create mode 100644 src/with-paginator.js create mode 100644 src/with-paginator.test.js diff --git a/src/with-paginator-helper.js b/src/with-paginator-helper.js deleted file mode 100644 index 06e2ee4..0000000 --- a/src/with-paginator-helper.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -/** - * Calls the given operation method with the parameters and if the autopagination - * argument is `true` it will do calls to the operation method until the LastEvaluatedKey value from the - * method response has a falsy value. - * - * @param {Function} methodFn Any DynamoDB operation method to call. - * @param {Object} params Parameters as expected by DynamoDB operation to use. - * @param {boolean} autopagination Whether to return all the records from DynamoDB or just the first batch of records. - * @returns {Promise} A promise that resolves to the response from DynamoDB. - */ -async function withPaginatorHelper(methodFn, params, autopagination) { - const items = []; - let result = await methodFn(params).promise(); - result.Items && result.Items.length >= 0 && items.push(...result.Items); - - if (autopagination) { - while (result.LastEvaluatedKey) { - result = await methodFn({ ...params, ExclusiveStartKey: result.LastEvaluatedKey }).promise(); - result.Items && result.Items.length >= 0 && items.push(...result.Items); - } - } - - result.Items = items; - - return result; -} - -module.exports = withPaginatorHelper; diff --git a/src/with-paginator-helper.test.js b/src/with-paginator-helper.test.js deleted file mode 100644 index 9c276ee..0000000 --- a/src/with-paginator-helper.test.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -const withPaginatorHelper = require('./with-paginator-helper'); - -describe('the `withPaginatorHelper` function', () => { - test('should call given fn only once with the given parameters', async () => { - const mockRequest = { - promise: jest.fn().mockResolvedValue({ - Items: [{ id: { S: 'foo' } }, { id: { S: 'bar' } }], - LastEvaluatedKey: { id: { S: 'bar' } } - }) - }; - const mockFn = jest.fn().mockReturnValue(mockRequest); - const result = await withPaginatorHelper(mockFn, { TableName: 'foo' }); - - expect(mockFn).toHaveBeenCalledTimes(1); - expect(mockFn).toHaveBeenLastCalledWith({ TableName: 'foo' }); - expect(result).toEqual({ - Items: [{ id: { S: 'foo' } }, { id: { S: 'bar' } }], - LastEvaluatedKey: { id: { S: 'bar' } } - }); - }); - - test('should call given fn multiple times with the given parameters', async () => { - const mockRequest = { - promise: jest - .fn() - .mockResolvedValueOnce({ - Items: [{ id: { S: 'foo' } }], - LastEvaluatedKey: { id: { S: 'foo' } } - }) - .mockResolvedValueOnce({ Items: [{ id: { S: 'bar' } }] }) - }; - const mockFn = jest.fn().mockReturnValue(mockRequest); - - const result = await withPaginatorHelper(mockFn, { TableName: 'foo' }, true); - - expect(mockFn).toHaveBeenCalledTimes(2); - expect(mockFn).toHaveBeenNthCalledWith(1, { TableName: 'foo' }); - expect(mockFn).toHaveBeenNthCalledWith(2, { - TableName: 'foo', - ExclusiveStartKey: { id: { S: 'foo' } } - }); - expect(result).toHaveProperty('Items', [{ id: { S: 'foo' } }, { id: { S: 'bar' } }]); - }); - - test('should return a single object with all the responses Items', async () => { - const mockRequest = { - promise: jest - .fn() - .mockResolvedValueOnce({ - Items: [{ id: { S: 'foo' } }], - LastEvaluatedKey: { id: { S: 'foo' } } - }) - .mockResolvedValueOnce({ Items: [] }) - }; - const mockFn = jest.fn().mockReturnValue(mockRequest); - - const result = await withPaginatorHelper(mockFn, { TableName: 'foo' }, true); - - expect(mockFn).toHaveBeenCalledTimes(2); - expect(mockFn).toHaveBeenNthCalledWith(1, { TableName: 'foo' }); - expect(mockFn).toHaveBeenNthCalledWith(2, { - TableName: 'foo', - ExclusiveStartKey: { id: { S: 'foo' } } - }); - expect(result).toHaveProperty('Items', [{ id: { S: 'foo' } }]); - }); -}); diff --git a/src/with-paginator.js b/src/with-paginator.js new file mode 100644 index 0000000..aef542a --- /dev/null +++ b/src/with-paginator.js @@ -0,0 +1,125 @@ +'use strict'; +const { isNotNilOrEmpty } = require('@flybondi/ramda-land'); +const { compose, mergeRight, reduce } = require('ramda'); +const { rejectNilOrEmpty } = require('@flybondi/ramda-land'); + +const DEFAULT_CAPACITY = Object.freeze({ + CapacityUnits: 0, + ReadCapacityUnits: 0, + WriteCapacityUnits: 0 +}); +const mergeWithDefaults = compose(mergeRight(DEFAULT_CAPACITY), rejectNilOrEmpty); + +function createCapacities(prev, next) { + prev = mergeWithDefaults(prev); + return { + CapacityUnits: prev.CapacityUnits + next.CapacityUnits, + ReadCapacityUnits: prev.ReadCapacityUnits + next.ReadCapacityUnits, + WriteCapacityUnits: prev.WriteCapacityUnits + next.WriteCapacityUnits + }; +} + +function aggregateConsumedCapacityForIndexes(prev = {}, next) { + const result = {}; + for (const key in next) { + result[key] = createCapacities(prev[key], next[key]); + } + return result; +} + +function aggregateConsumedCapacity(prev = {}, next) { + const { Table, LocalSecondaryIndexes, GlobalSecondaryIndexes } = next; + const capacities = { + ...createCapacities(prev, next) + }; + + if (isNotNilOrEmpty(Table)) { + capacities.Table = createCapacities(prev.Table, Table); + } + if (isNotNilOrEmpty(LocalSecondaryIndexes)) { + capacities.LocalSecondaryIndexes = aggregateConsumedCapacityForIndexes( + prev.LocalSecondaryIndexes, + LocalSecondaryIndexes + ); + } + if (isNotNilOrEmpty(GlobalSecondaryIndexes)) { + capacities.GlobalSecondaryIndexes = aggregateConsumedCapacityForIndexes( + prev.GlobalSecondaryIndexes, + GlobalSecondaryIndexes + ); + } + + return capacities; +} + +function createResponseFrom(responses) { + return reduce( + (result, response) => { + const { Items, Count, ScannedCount, ConsumedCapacity, LastEvaluatedKey } = response; + if (isNotNilOrEmpty(Items)) { + result.Items = result.Items || []; + result.Items.push(...Items); + } + + if (isNotNilOrEmpty(Count)) { + result.Count = result.Count || 0; + result.Count += Count; + } + + if (isNotNilOrEmpty(ScannedCount)) { + result.ScannedCount = result.ScannedCount || 0; + result.ScannedCount += ScannedCount; + } + + if (isNotNilOrEmpty(LastEvaluatedKey)) { + // TODO: If the last LastEvaluatedKey from the responses is undefined means that there is no pages + // left to retrieve. We should not send it back. + result.LastEvaluatedKey = LastEvaluatedKey; + } + + if (isNotNilOrEmpty(ConsumedCapacity)) { + result.ConsumedCapacity = { + TableName: ConsumedCapacity.TableName, + ...aggregateConsumedCapacity(result.ConsumedCapacity, ConsumedCapacity) + }; + } + + return result; + }, + {}, + responses + ); +} + +/** + * + * @param {Function} operationFn Any DynamoDB operation method to call. + * @returns {Function} A helper function that will do the actual request to DynamoDB. + */ +function withPaginator(operationFn) { + /** + * Calls the given operation method with the parameters and if the autopagination + * argument is `true` it will do calls to the operation method until the LastEvaluatedKey value from the + * method response has a falsy value. + * + * @param {Object} input Parameters as expected by DynamoDB operation to use. + * @returns {Promise} A promise that resolves to the response from DynamoDB. + */ + return async function autopaginate(input) { + const responses = []; + let result = await operationFn(input).promise(); + responses.push(result); + + while (result.LastEvaluatedKey) { + result = await operationFn({ + ...input, + ExclusiveStartKey: result.LastEvaluatedKey + }).promise(); + responses.push(result); + } + + return createResponseFrom(responses); + }; +} + +module.exports = withPaginator; diff --git a/src/with-paginator.test.js b/src/with-paginator.test.js new file mode 100644 index 0000000..00c851a --- /dev/null +++ b/src/with-paginator.test.js @@ -0,0 +1,286 @@ +'use strict'; + +const withPaginator = require('./with-paginator'); + +describe('the `withPaginator` function', () => { + test('should return a function', () => { + expect(withPaginator()).toBeInstanceOf(Function); + }); + + test('should call given fn only once with the given parameters', async () => { + const mockRequest = { + promise: jest.fn().mockResolvedValue({ + Items: [{ id: { S: 'foo' } }, { id: { S: 'bar' } }] + }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + const result = await withPaginator(mockFn)({ TableName: 'foo' }); + + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenLastCalledWith({ TableName: 'foo' }); + expect(result).toEqual({ + Items: [{ id: { S: 'foo' } }, { id: { S: 'bar' } }] + }); + }); + + test('should call given fn multiple times with the given parameters', async () => { + const mockRequest = { + promise: jest + .fn() + .mockResolvedValueOnce({ + Items: [{ id: { S: 'foo' } }], + LastEvaluatedKey: { id: { S: 'foo' } }, + Count: 1, + ScannedCount: 1 + }) + .mockResolvedValueOnce({ Items: [{ id: { S: 'bar' } }], Count: 1, ScannedCount: 1 }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + + const result = await withPaginator(mockFn)({ TableName: 'foo' }); + + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenNthCalledWith(1, { TableName: 'foo' }); + expect(mockFn).toHaveBeenNthCalledWith(2, { + TableName: 'foo', + ExclusiveStartKey: { id: { S: 'foo' } } + }); + expect(result).toHaveProperty('Items', [{ id: { S: 'foo' } }, { id: { S: 'bar' } }]); + expect(result).toHaveProperty('Count', 2); + expect(result).toHaveProperty('ScannedCount', 2); + }); + + test('should return a single object with all the responses Items', async () => { + const mockRequest = { + promise: jest + .fn() + .mockResolvedValueOnce({ + Items: [{ id: { S: 'foo' } }], + LastEvaluatedKey: { id: { S: 'foo' } } + }) + .mockResolvedValueOnce({ Items: [] }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + + const result = await withPaginator(mockFn)({ TableName: 'foo' }); + + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenNthCalledWith(1, { TableName: 'foo' }); + expect(mockFn).toHaveBeenNthCalledWith(2, { + TableName: 'foo', + ExclusiveStartKey: { id: { S: 'foo' } } + }); + expect(result).toHaveProperty('Items', [{ id: { S: 'foo' } }]); + }); + + test('should aggregate the consumed capacities from multiple responses', async () => { + const mockRequest = { + promise: jest + .fn() + .mockResolvedValueOnce({ + Items: [{ id: { S: 'foo' } }], + LastEvaluatedKey: { id: { S: 'foo' } }, + Count: 1, + ScannedCount: 1, + ConsumedCapacity: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + TableName: 'foo', + Table: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + } + } + }) + .mockResolvedValueOnce({ + Items: [{ id: { S: 'bar' } }], + Count: 1, + ScannedCount: 1, + ConsumedCapacity: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + TableName: 'foo', + Table: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + } + } + }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + + const result = await withPaginator(mockFn)({ TableName: 'foo' }); + + expect(result).toHaveProperty('ConsumedCapacity', { + CapacityUnits: 2, + ReadCapacityUnits: 2, + Table: { CapacityUnits: 2, ReadCapacityUnits: 2, WriteCapacityUnits: 2 }, + TableName: 'foo', + WriteCapacityUnits: 2 + }); + }); + + test('should aggregate the consumed capacities from multiple responses on Local indexes', async () => { + const mockRequest = { + promise: jest + .fn() + .mockResolvedValueOnce({ + Items: [{ id: { S: 'foo' } }], + LastEvaluatedKey: { id: { S: 'foo' } }, + Count: 1, + ScannedCount: 1, + ConsumedCapacity: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + TableName: 'foo', + Table: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + }, + LocalSecondaryIndexes: { + 'foo-local-index': { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + }, + 'bar-local-index': { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + } + } + } + }) + .mockResolvedValueOnce({ + Items: [{ id: { S: 'bar' } }], + Count: 1, + ScannedCount: 1, + ConsumedCapacity: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + TableName: 'foo', + Table: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + }, + LocalSecondaryIndexes: { + 'foo-local-index': { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + }, + 'bar-local-index': { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + } + } + } + }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + + const result = await withPaginator(mockFn)({ TableName: 'foo' }); + + expect(result).toHaveProperty('ConsumedCapacity.LocalSecondaryIndexes', { + 'bar-local-index': { + CapacityUnits: 2, + ReadCapacityUnits: 2, + WriteCapacityUnits: 2 + }, + 'foo-local-index': { + CapacityUnits: 2, + ReadCapacityUnits: 2, + WriteCapacityUnits: 2 + } + }); + }); + + test('should aggregate the consumed capacities from multiple responses on Global indexes', async () => { + const mockRequest = { + promise: jest + .fn() + .mockResolvedValueOnce({ + Items: [{ id: { S: 'foo' } }], + LastEvaluatedKey: { id: { S: 'foo' } }, + Count: 1, + ScannedCount: 1, + ConsumedCapacity: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + TableName: 'foo', + Table: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + }, + GlobalSecondaryIndexes: { + 'foo-local-index': { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + }, + 'bar-local-index': { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + } + } + } + }) + .mockResolvedValueOnce({ + Items: [{ id: { S: 'bar' } }], + Count: 1, + ScannedCount: 1, + ConsumedCapacity: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + TableName: 'foo', + Table: { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + }, + GlobalSecondaryIndexes: { + 'foo-local-index': { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + }, + 'bar-local-index': { + CapacityUnits: 1, + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + } + } + } + }) + }; + const mockFn = jest.fn().mockReturnValue(mockRequest); + + const result = await withPaginator(mockFn)({ TableName: 'foo' }); + + expect(result).toHaveProperty('ConsumedCapacity.GlobalSecondaryIndexes', { + 'bar-local-index': { + CapacityUnits: 2, + ReadCapacityUnits: 2, + WriteCapacityUnits: 2 + }, + 'foo-local-index': { + CapacityUnits: 2, + ReadCapacityUnits: 2, + WriteCapacityUnits: 2 + } + }); + }); +}); From d21058d7c29fa1bd2116eb975e0241a67fedacec Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Fri, 22 Jan 2021 15:24:56 +0100 Subject: [PATCH 10/13] feat: update use of with-paginator --- package.json | 1 + src/and-then.js | 23 +++++++++++++++++++++++ src/and-then.test.js | 11 +++++++++++ src/count.js | 6 +++--- src/count.test.js | 2 +- src/get-all.js | 17 +++++++++-------- src/get-all.test.js | 4 ++-- src/get.js | 6 +++--- src/insert.js | 3 ++- src/query.js | 20 ++++++++++---------- src/query.test.js | 4 ++-- src/remove.js | 6 +++--- src/update.js | 17 +++-------------- yarn.lock | 12 ++++++++++++ 14 files changed, 85 insertions(+), 47 deletions(-) create mode 100644 src/and-then.js create mode 100644 src/and-then.test.js diff --git a/package.json b/package.json index 0bee2e7..68ff128 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ ] }, "dependencies": { + "@flybondi/ramda-land": "^4.0.6", "dynamodb-data-types": "^3.0.1", "dynamodb-update-expression": "^0.1.21", "ramda": "^0.27.1" diff --git a/src/and-then.js b/src/and-then.js new file mode 100644 index 0000000..373ffc2 --- /dev/null +++ b/src/and-then.js @@ -0,0 +1,23 @@ +'use strict'; +const { compose, invoker, andThen: RAndThen } = require('ramda'); + +/** + * Invokes `promise` on the given object passing + * in no arguments. + * + * @param {object} obj The obj to invoke `promise` on. + * @returns {Promise.<*>} A `Promise` as returned by a call to `promise`. + */ +const toPromise = invoker(0, 'promise'); + +/** + * Returns the result of applying an `fn` function to the value inside a fulfilled promise, + * after having invoked `promise()` on the target object. + * + * @private + * @see https://ramdajs.com/docs/#andThen + * @param {Function} fn The function to apply. Can return a value or a promise of a value. + */ +const andThen = fn => compose(RAndThen(fn), toPromise); + +module.exports = { toPromise, andThen }; diff --git a/src/and-then.test.js b/src/and-then.test.js new file mode 100644 index 0000000..d3d9580 --- /dev/null +++ b/src/and-then.test.js @@ -0,0 +1,11 @@ +'use strict'; +const { andThen } = require('./and-then'); + +test('should call the promise function and then the callback function', async () => { + const mockFn = { promise: jest.fn().mockResolvedValue({ Items: ['foo'] }) }; + const mockCallbackFn = jest.fn().mockReturnValue(['foo']); + + expect(await andThen(mockCallbackFn)(mockFn)).toEqual(['foo']); + expect(mockFn.promise).toHaveBeenCalled(); + expect(mockCallbackFn).toHaveBeenCalled(); +}); diff --git a/src/count.js b/src/count.js index fbc742c..373e3f6 100644 --- a/src/count.js +++ b/src/count.js @@ -4,14 +4,14 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Scan.html * @module Count */ -const { curry, compose, bind, prop } = require('ramda'); +const { andThen, curry, compose, bind, prop } = require('ramda'); const addTableName = require('./table-name'); -const withPaginatorHelper = require('./with-paginator-helper'); +const withPaginator = require('./with-paginator'); /** * @private */ -const createCount = scan => params => withPaginatorHelper(scan, params, true).then(prop('Count')); +const createCount = scan => compose(andThen(prop('Count')), withPaginator(scan)); /** * @private diff --git a/src/count.test.js b/src/count.test.js index cbdf279..2734cb0 100644 --- a/src/count.test.js +++ b/src/count.test.js @@ -9,7 +9,7 @@ test('should call `scan` internally', async () => { expect(mockScan).toHaveBeenCalled(); }); -test('should return `undefined` if no items are returned', async () => { +test.only('should return `undefined` if no items are returned', async () => { const mockRequest = { promise: jest.fn().mockResolvedValue({ Items: [] }) }; const mockScan = jest.fn().mockReturnValue(mockRequest); const { count } = createCounter({ scan: mockScan }); diff --git a/src/get-all.js b/src/get-all.js index f899fd4..46932ff 100644 --- a/src/get-all.js +++ b/src/get-all.js @@ -5,23 +5,24 @@ * @module Scan */ const { curry, bind, compose, mergeRight } = require('ramda'); +const { rejectNilOrEmpty } = require('@flybondi/ramda-land'); const { unwrapAll, unwrapOverAll } = require('./wrapper'); const addTableName = require('./table-name'); -const withPaginatorHelper = require('./with-paginator-helper'); +const withPaginator = require('./with-paginator'); -const DEFAULT_OPTIONS = { +const DEFAULT_OPTIONS = Object.freeze({ autopagination: true, raw: false -}; -const mergeWithDefaults = mergeRight(DEFAULT_OPTIONS); +}); +const mergeWithDefaults = compose(mergeRight(DEFAULT_OPTIONS), rejectNilOrEmpty); + /** * @private */ const createGetAll = scan => (params, options = {}) => { - options = mergeWithDefaults(options); - return withPaginatorHelper(scan, params, options.autopagination).then( - options.raw ? unwrapOverAll('Items') : unwrapAll('Items') - ); + const { raw, autopagination } = mergeWithDefaults(options); + const scanFn = autopagination ? withPaginator(scan) : scan; + return scanFn(params).then(raw ? unwrapOverAll('Items') : unwrapAll('Items')); }; /** diff --git a/src/get-all.test.js b/src/get-all.test.js index 0870ebd..6ed0549 100644 --- a/src/get-all.test.js +++ b/src/get-all.test.js @@ -10,11 +10,11 @@ describe('the getAllFor function', () => { expect(mockScan).toHaveBeenCalled(); }); - test('should return `[]` if no items are returned', async () => { + test('should return `undefined` if no items are returned', async () => { const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; const mockScan = jest.fn().mockReturnValue(mockRequest); const { getAll } = createAllGetter({ scan: mockScan }); - expect(await getAll()).toStrictEqual([]); + expect(await getAll()).toBeUndefined(); }); test('should return unwrapped items', async () => { diff --git a/src/get.js b/src/get.js index e2b9258..fb09fee 100644 --- a/src/get.js +++ b/src/get.js @@ -4,17 +4,17 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html * @module GetItem */ -const { andThen, curry, bind, compose, apply } = require('ramda'); +const { curry, bind, compose, apply } = require('ramda'); const { unwrapProp } = require('./wrapper'); const { mapMergeFirstPairOfArgs } = require('./map-merge-args'); +const { andThen } = require('./and-then'); const generateKey = require('./generate-key'); const addTableName = require('./table-name'); /** * @private */ -const getUnwrappedItem = getItem => - compose(andThen(unwrapProp('Item')), request => request.promise(), apply(getItem)); +const getUnwrappedItem = getItem => compose(andThen(unwrapProp('Item')), apply(getItem)); /** * @private diff --git a/src/insert.js b/src/insert.js index 8b6878a..c09e04c 100644 --- a/src/insert.js +++ b/src/insert.js @@ -8,12 +8,13 @@ const { curry, bind, compose, apply } = require('ramda'); const generateItem = require('./generate-item'); const addTableName = require('./table-name'); const { mapMergeFirstPairOfArgs } = require('./map-merge-args'); +const { toPromise } = require('./and-then'); /** * @private */ const createInsert = putItem => - compose(request => request.promise(), apply(putItem), mapMergeFirstPairOfArgs(generateItem)); + compose(toPromise, apply(putItem), mapMergeFirstPairOfArgs(generateItem)); /** * @private diff --git a/src/query.js b/src/query.js index 66a3e6e..9d2be92 100644 --- a/src/query.js +++ b/src/query.js @@ -5,25 +5,25 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html * @module Query */ -const { bind, curry, mergeRight } = require('ramda'); +const { curry, bind, compose, mergeRight } = require('ramda'); +const { rejectNilOrEmpty } = require('@flybondi/ramda-land'); const { unwrapAll, unwrapOverAll } = require('./wrapper'); const addTableName = require('./table-name'); -const withPaginatorHelper = require('./with-paginator-helper'); +const withPaginator = require('./with-paginator'); -const DEFAULT_OPTIONS = { +const DEFAULT_OPTIONS = Object.freeze({ autopagination: true, raw: false -}; -const mergeWithDefaults = mergeRight(DEFAULT_OPTIONS); +}); +const mergeWithDefaults = compose(mergeRight(DEFAULT_OPTIONS), rejectNilOrEmpty); /** * @private */ -const createQuery = query => (params, options = {}) => { - options = mergeWithDefaults(options); - return withPaginatorHelper(query, params, options.autopagination).then( - options.raw ? unwrapOverAll('Items') : unwrapAll('Items') - ); +const createQuery = query => (params, options) => { + const { raw, autopagination } = mergeWithDefaults(options); + const queryFn = autopagination ? withPaginator(query) : query; + return queryFn(params).then(raw ? unwrapOverAll('Items') : unwrapAll('Items')); }; /** diff --git a/src/query.test.js b/src/query.test.js index 0215a9c..b0e0c45 100644 --- a/src/query.test.js +++ b/src/query.test.js @@ -10,11 +10,11 @@ describe('the query function', () => { expect(mockQuery).toHaveBeenCalled(); }); - test('should return `[]` if no items are returned', async () => { + test('should return `undefined` if no items are returned', async () => { const mockRequest = { promise: jest.fn().mockResolvedValue({}) }; const mockQuery = jest.fn().mockReturnValue(mockRequest); const { query } = createQuerier({ query: mockQuery }); - expect(await query()).toStrictEqual([]); + expect(await query()).toBeUndefined(); }); test('should return unwrapped items', async () => { diff --git a/src/remove.js b/src/remove.js index 0543390..9b9f942 100644 --- a/src/remove.js +++ b/src/remove.js @@ -7,18 +7,18 @@ * @module DeleteItem * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html */ -const { apply, bind, compose, curry, andThen } = require('ramda'); +const { apply, bind, compose, curry } = require('ramda'); const { unwrapProp } = require('./wrapper'); const addTableName = require('./table-name'); const { mapMergeFirstPairOfArgs } = require('./map-merge-args'); const generateKey = require('./generate-key'); const addReturnValues = require('./return-values'); - +const { andThen } = require('./and-then'); /** * @private */ const removeAndUnwrapAttributes = deleteItem => - compose(andThen(unwrapProp('Attributes')), request => request.promise(), apply(deleteItem)); + compose(andThen(unwrapProp('Attributes')), apply(deleteItem)); /** * @private diff --git a/src/update.js b/src/update.js index 28f490f..a23a94b 100644 --- a/src/update.js +++ b/src/update.js @@ -5,31 +5,20 @@ * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html * @module UpdateItem */ -const { - andThen, - apply, - applyTo, - bind, - compose, - curry, - ifElse, - is, - unless, - has, - partial -} = require('ramda'); +const { apply, applyTo, bind, compose, curry, ifElse, is, unless, has, partial } = require('ramda'); const { getUpdateExpression } = require('dynamodb-update-expression'); const { unwrapProp, wrapOver } = require('./wrapper'); const addTableName = require('./table-name'); const addReturnValues = require('./return-values'); const { mapMergeNArgs } = require('./map-merge-args'); const generateKey = require('./generate-key'); +const { andThen } = require('./and-then'); /** * @private */ const updateAndUnwrapAttributes = updateItem => - compose(andThen(unwrapProp('Attributes')), request => request.promise(), apply(updateItem)); + compose(andThen(unwrapProp('Attributes')), apply(updateItem)); /** * @private diff --git a/yarn.lock b/yarn.lock index 8bfe8cb..2f84b51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -437,6 +437,13 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@flybondi/ramda-land@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@flybondi/ramda-land/-/ramda-land-4.0.6.tgz#ad5d12d1020371ff4c6b9c1822a4045e261518a5" + integrity sha512-jfrSsXUedanMSmS/QeWQLjyy/o8I1PbI1OQUiw/x6XN7MMr91rHlD9c/Q3NHf2bL8b2wFqcCL/mFjZOFamFDkQ== + dependencies: + yn "^4.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -5500,6 +5507,11 @@ yargs@^15.1.0, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-4.0.0.tgz#611480051ea43b510da1dfdbe177ed159f00a979" + integrity sha512-huWiiCS4TxKc4SfgmTwW1K7JmXPPAmuXWYy4j9qjQo4+27Kni8mGhAAi1cloRWmBe2EqcLgt3IGqQoRL/MtPgg== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From fd41694b202d566309ecaa02a521cff73adb81d2 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Fri, 22 Jan 2021 16:11:13 +0100 Subject: [PATCH 11/13] test: remove only from test --- src/count.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/count.test.js b/src/count.test.js index 2734cb0..cbdf279 100644 --- a/src/count.test.js +++ b/src/count.test.js @@ -9,7 +9,7 @@ test('should call `scan` internally', async () => { expect(mockScan).toHaveBeenCalled(); }); -test.only('should return `undefined` if no items are returned', async () => { +test('should return `undefined` if no items are returned', async () => { const mockRequest = { promise: jest.fn().mockResolvedValue({ Items: [] }) }; const mockScan = jest.fn().mockReturnValue(mockRequest); const { count } = createCounter({ scan: mockScan }); From 70a259602bf8340650480970877a4a7c638f1d91 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Fri, 22 Jan 2021 18:22:54 +0100 Subject: [PATCH 12/13] fix: compose scan/query methods with toPromise --- src/and-then.js | 4 ++-- src/get-all.js | 3 ++- src/query.js | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/and-then.js b/src/and-then.js index 373ffc2..c0b47c0 100644 --- a/src/and-then.js +++ b/src/and-then.js @@ -1,5 +1,5 @@ 'use strict'; -const { compose, invoker, andThen: RAndThen } = require('ramda'); +const { compose, invoker, andThen: RAndThen, when, hasIn } = require('ramda'); /** * Invokes `promise` on the given object passing @@ -8,7 +8,7 @@ const { compose, invoker, andThen: RAndThen } = require('ramda'); * @param {object} obj The obj to invoke `promise` on. * @returns {Promise.<*>} A `Promise` as returned by a call to `promise`. */ -const toPromise = invoker(0, 'promise'); +const toPromise = when(hasIn('promise'), invoker(0, 'promise')); /** * Returns the result of applying an `fn` function to the value inside a fulfilled promise, diff --git a/src/get-all.js b/src/get-all.js index 46932ff..e9eda4a 100644 --- a/src/get-all.js +++ b/src/get-all.js @@ -7,6 +7,7 @@ const { curry, bind, compose, mergeRight } = require('ramda'); const { rejectNilOrEmpty } = require('@flybondi/ramda-land'); const { unwrapAll, unwrapOverAll } = require('./wrapper'); +const { toPromise } = require('./and-then'); const addTableName = require('./table-name'); const withPaginator = require('./with-paginator'); @@ -21,7 +22,7 @@ const mergeWithDefaults = compose(mergeRight(DEFAULT_OPTIONS), rejectNilOrEmpty) */ const createGetAll = scan => (params, options = {}) => { const { raw, autopagination } = mergeWithDefaults(options); - const scanFn = autopagination ? withPaginator(scan) : scan; + const scanFn = autopagination ? withPaginator(scan) : compose(toPromise, scan); return scanFn(params).then(raw ? unwrapOverAll('Items') : unwrapAll('Items')); }; diff --git a/src/query.js b/src/query.js index 9d2be92..25e55bd 100644 --- a/src/query.js +++ b/src/query.js @@ -8,6 +8,7 @@ const { curry, bind, compose, mergeRight } = require('ramda'); const { rejectNilOrEmpty } = require('@flybondi/ramda-land'); const { unwrapAll, unwrapOverAll } = require('./wrapper'); +const { toPromise } = require('./and-then'); const addTableName = require('./table-name'); const withPaginator = require('./with-paginator'); @@ -22,7 +23,7 @@ const mergeWithDefaults = compose(mergeRight(DEFAULT_OPTIONS), rejectNilOrEmpty) */ const createQuery = query => (params, options) => { const { raw, autopagination } = mergeWithDefaults(options); - const queryFn = autopagination ? withPaginator(query) : query; + const queryFn = autopagination ? withPaginator(query) : compose(toPromise, query); return queryFn(params).then(raw ? unwrapOverAll('Items') : unwrapAll('Items')); }; From f357ab617073db9d9fe6a998362e958cf326c812 Mon Sep 17 00:00:00 2001 From: Luciano Fantone Date: Tue, 26 Jan 2021 09:57:31 +0100 Subject: [PATCH 13/13] fix: add missing toPromise --- src/insert.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/insert.js b/src/insert.js index c09e04c..860e3f5 100644 --- a/src/insert.js +++ b/src/insert.js @@ -13,14 +13,18 @@ const { toPromise } = require('./and-then'); /** * @private */ -const createInsert = putItem => - compose(toPromise, apply(putItem), mapMergeFirstPairOfArgs(generateItem)); +const insert = putItem => compose(toPromise, apply(putItem)); + +/** + * @private + */ +const createInsert = putItem => compose(insert(putItem), mapMergeFirstPairOfArgs(generateItem)); /** * @private */ const createInsertFor = curry((putItem, table) => - compose(apply(putItem), mapMergeFirstPairOfArgs(compose(addTableName(table), generateItem))) + compose(insert(putItem), mapMergeFirstPairOfArgs(compose(addTableName(table), generateItem))) ); function createInserter(dynamodb) {