From 27143cf2d4a439f1b5904e62756e59e501b3f67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20J=C3=A4ggi?= <1872195+solaris007@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:24:11 +0100 Subject: [PATCH] feat: add guards and use spacecat-shared-utils (#6) * feat: add guards and use spacecat-shared-utils * chore: add jsdoc * chore: update readme * chore: update readme * fix: TS support for eslint, type defs for utils * fix: linting --- .eslintrc.cjs | 25 +- package-lock.json | 243 +++++++++++++++++- package.json | 5 +- packages/spacecat-shared-dynamo/README.md | 76 +++++- packages/spacecat-shared-dynamo/package.json | 3 +- .../spacecat-shared-dynamo/src/index.d.ts | 10 +- .../src/modules/getItem.js | 11 +- .../src/modules/putItem.js | 6 +- .../src/modules/query.js | 3 + .../src/modules/removeItem.js | 11 +- .../src/utils/guards.js | 62 +++++ .../test/modules/getItem.test.js | 4 +- .../test/modules/putItem.test.js | 2 +- .../test/modules/query.test.js | 14 +- .../test/modules/removeItem.test.js | 4 +- .../test/utils/guards.test.js | 63 +++++ packages/spacecat-shared-example/package.json | 4 +- packages/spacecat-shared-utils/README.md | 58 ++++- .../spacecat-shared-utils/src/functions.js | 20 +- packages/spacecat-shared-utils/src/index.d.ts | 35 +++ 20 files changed, 602 insertions(+), 57 deletions(-) create mode 100644 packages/spacecat-shared-dynamo/src/utils/guards.js create mode 100644 packages/spacecat-shared-dynamo/test/utils/guards.test.js create mode 100644 packages/spacecat-shared-utils/src/index.d.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 19305ccb..3462360d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -12,6 +12,27 @@ module.exports = { root: true, - extends: '@adobe/helix', - plugins: ['import'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + extends: [ + '@adobe/helix', + 'plugin:@typescript-eslint/recommended', + ], + plugins: [ + 'import', + '@typescript-eslint', + ], + overrides: [ + { + files: ['*.ts'], + rules: {}, + }, + { + files: ['*.js', '*.cjs'], + rules: {}, + }, + ], }; diff --git a/package-lock.json b/package-lock.json index e6344d0b..839be8fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@semantic-release/changelog": "6.0.3", "@semantic-release/git": "10.0.1", "@semantic-release/npm": "9.0.2", + "@typescript-eslint/eslint-plugin": "6.13.1", + "@typescript-eslint/parser": "6.13.1", "ajv": "8.12.0", "c8": "8.0.1", "eslint": "8.54.0", @@ -26,7 +28,8 @@ "mocha-multi-reporters": "1.5.1", "nock": "13.3.8", "semantic-release": "19.0.5", - "semantic-release-monorepo": "7.0.5" + "semantic-release-monorepo": "7.0.5", + "typescript": "^5.3.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2005,6 +2008,12 @@ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2051,6 +2060,201 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, + "node_modules/@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -11301,6 +11505,18 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -11416,6 +11632,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/typical": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/typical/-/typical-2.6.1.tgz", @@ -11873,10 +12102,18 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { + "@adobe/spacecat-shared-utils": "1.0.1", "@aws-sdk/client-dynamodb": "3.454.0", "@aws-sdk/lib-dynamodb": "3.454.0" }, - "devDependencies": {} + "devDependencies": { + "chai": "4.3.10" + } + }, + "packages/spacecat-shared-dynamo/node_modules/@adobe/spacecat-shared-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.0.1.tgz", + "integrity": "sha512-6HiCywan5y9jpbFYkQX87Vg0q5dXoJf2m6MiDvNBEJdREdUxFwE+oYA6Fr86qqd32ECnCeYCFO13HcNRn1RxkQ==" }, "packages/spacecat-shared-example": { "name": "@adobe/spacecat-shared-example", @@ -11894,7 +12131,7 @@ }, "packages/spacecat-shared-utils": { "name": "@adobe/spacecat-shared-utils", - "version": "1.0.0", + "version": "1.0.1", "license": "Apache-2.0", "devDependencies": { "chai": "4.3.10" diff --git a/package.json b/package.json index 585e1c46..fd5e2b50 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@semantic-release/changelog": "6.0.3", "@semantic-release/git": "10.0.1", "@semantic-release/npm": "9.0.2", + "@typescript-eslint/eslint-plugin": "6.13.1", + "@typescript-eslint/parser": "6.13.1", "ajv": "8.12.0", "c8": "8.0.1", "eslint": "8.54.0", @@ -41,7 +43,8 @@ "mocha-multi-reporters": "1.5.1", "nock": "13.3.8", "semantic-release": "19.0.5", - "semantic-release-monorepo": "7.0.5" + "semantic-release-monorepo": "7.0.5", + "typescript": "5.3.2" }, "lint-staged": { "*.js": "eslint" diff --git a/packages/spacecat-shared-dynamo/README.md b/packages/spacecat-shared-dynamo/README.md index d9047069..2fde3f1c 100644 --- a/packages/spacecat-shared-dynamo/README.md +++ b/packages/spacecat-shared-dynamo/README.md @@ -1,15 +1,77 @@ -# Spacecat Shared - DynamoDB Client +# Dynamo Client -TBD +## Overview +This package, `@adobe/spacecat-shared-dynamo`, is a shared module designed for interacting with Amazon DynamoDB. It is a part of the Spacecat Services, providing a streamlined interface for DynamoDB operations. -## Usage +## Features +- **Query Operations**: Perform read operations using primary or secondary indexes. +- **Get Item**: Retrieve single items from DynamoDB using a table name and key. +- **Put Item**: Insert or update items in DynamoDB. +- **Remove Item**: Delete items from a DynamoDB table. + +## Installation +Install the package using npm: +``` +npm install @adobe/spacecat-shared-dynamo +``` -```js +## Usage +First, import the `createClient` function from the package: +```javascript import { createClient } from '@adobe/spacecat-shared-dynamo'; +``` +Then, use it to create a DynamoDB client: +```javascript +const dynamoClient = createClient(); +``` + +### API Overview +- `query(params)`: Queries DynamoDB with the specified parameters. +- `getItem(tableName, key)`: Retrieves an item from a specified table using a key. +- `putItem(tableName, item)`: Inserts or updates an item in the specified table. +- `removeItem(tableName, key)`: Removes an item from the specified table. + +### Example +```javascript +const tableName = 'YourTableName'; +const key = { primaryKey: 'YourPrimaryKey' }; + +// Get an item +const item = await dynamoClient.getItem(tableName, key); + +// Put an item +await dynamoClient.putItem(tableName, { primaryKey: 'NewKey', data: 'YourData' }); -const dynamoDbClient = createClient(); +// Query +const queryResult = await dynamoClient.query({ TableName: tableName, KeyConditionExpression: 'primaryKey = :pk', ExpressionAttributeValues: { ':pk': 'YourPrimaryKey' } }); -const result = await dynamoDbClient.query(...); -... +// Remove an item +await dynamoClient.removeItem(tableName, key); +``` + +## Testing +Run the included tests with the following command: +``` +npm test +``` +## Linting +Lint the codebase using: +``` +npm run lint ``` + +## Cleaning +To clean the package (remove `node_modules` and `package-lock.json`): +``` +npm run clean +``` + +## Repository +Find the source code and contribute [here](https://github.com/adobe-rnd/spacecat-shared.git). + +## Issues +Report issues or bugs [here](https://github.com/adobe-rnd/spacecat-shared/issues). + +## License +This project is licensed under the Apache-2.0 License. diff --git a/packages/spacecat-shared-dynamo/package.json b/packages/spacecat-shared-dynamo/package.json index 520ec762..cf2041a6 100644 --- a/packages/spacecat-shared-dynamo/package.json +++ b/packages/spacecat-shared-dynamo/package.json @@ -30,7 +30,8 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "3.454.0", - "@aws-sdk/lib-dynamodb": "3.454.0" + "@aws-sdk/lib-dynamodb": "3.454.0", + "@adobe/spacecat-shared-utils": "1.0.1" }, "devDependencies": { "chai": "4.3.10" diff --git a/packages/spacecat-shared-dynamo/src/index.d.ts b/packages/spacecat-shared-dynamo/src/index.d.ts index 98a79528..8599d9c1 100644 --- a/packages/spacecat-shared-dynamo/src/index.d.ts +++ b/packages/spacecat-shared-dynamo/src/index.d.ts @@ -14,8 +14,8 @@ import { DynamoDB } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, QueryCommandInput } from '@aws-sdk/lib-dynamodb'; export declare interface Logger { - error(message: string, ...args: any[]): void; - info(message: string, ...args: any[]): void; + error(message: string, ...args: unknown[]): void; + info(message: string, ...args: unknown[]): void; } export declare interface DynamoDbKey { @@ -30,4 +30,8 @@ export declare interface DynamoDbClient { removeItem(tableName: string, key: DynamoDbKey): Promise<{ message: string }>; } -export function createClient(logger: Logger, dbClient?: DynamoDB, docClient?: DynamoDBDocumentClient): DynamoDbClient; +export function createClient( + logger: Logger, + dbClient?: DynamoDB, + docClient?: DynamoDBDocumentClient +): DynamoDbClient; diff --git a/packages/spacecat-shared-dynamo/src/modules/getItem.js b/packages/spacecat-shared-dynamo/src/modules/getItem.js index eaa64a3f..5a4e24e8 100644 --- a/packages/spacecat-shared-dynamo/src/modules/getItem.js +++ b/packages/spacecat-shared-dynamo/src/modules/getItem.js @@ -12,6 +12,8 @@ import { performance } from 'perf_hooks'; +import { guardKey, guardTableName } from '../utils/guards.js'; + /** * Retrieves an item from DynamoDB using a table name and key object. * @@ -23,13 +25,8 @@ import { performance } from 'perf_hooks'; * @throws {Error} Throws an error if the DynamoDB get operation fails or input validation fails. */ async function getItem(docClient, tableName, key, log = console) { - if (!tableName || typeof tableName !== 'string') { - throw new Error('Invalid tableName: must be a non-empty string.'); - } - - if (!key || typeof key !== 'object' || !key.partitionKey) { - throw new Error('Invalid key: must be an object with a partitionKey.'); - } + guardTableName(tableName); + guardKey(key); const params = { TableName: tableName, diff --git a/packages/spacecat-shared-dynamo/src/modules/putItem.js b/packages/spacecat-shared-dynamo/src/modules/putItem.js index 39fb28ea..21cc9be6 100644 --- a/packages/spacecat-shared-dynamo/src/modules/putItem.js +++ b/packages/spacecat-shared-dynamo/src/modules/putItem.js @@ -12,6 +12,8 @@ import { performance } from 'perf_hooks'; +import { guardTableName } from '../utils/guards.js'; + /** * Inserts or updates an item in a DynamoDB table. * @@ -23,9 +25,7 @@ import { performance } from 'perf_hooks'; * @throws {Error} Throws an error if the DynamoDB put operation fails. */ async function putItem(docClient, tableName, item, log = console) { - if (!tableName || typeof tableName !== 'string') { - throw new Error('Invalid tableName: must be a non-empty string.'); - } + guardTableName(tableName); const params = { TableName: tableName, diff --git a/packages/spacecat-shared-dynamo/src/modules/query.js b/packages/spacecat-shared-dynamo/src/modules/query.js index 0bec8a76..6738b8cb 100644 --- a/packages/spacecat-shared-dynamo/src/modules/query.js +++ b/packages/spacecat-shared-dynamo/src/modules/query.js @@ -11,6 +11,7 @@ */ import { performance } from 'perf_hooks'; +import { guardQueryParameters } from '../utils/guards.js'; /** * Queries DynamoDB and automatically handles pagination to retrieve all items. @@ -22,6 +23,8 @@ import { performance } from 'perf_hooks'; * @throws {Error} Throws an error if the DynamoDB query operation fails. */ async function query(docClient, originalParams, log = console) { + guardQueryParameters(originalParams); + let items = []; const params = { ...originalParams }; diff --git a/packages/spacecat-shared-dynamo/src/modules/removeItem.js b/packages/spacecat-shared-dynamo/src/modules/removeItem.js index a89a4ace..2978278c 100644 --- a/packages/spacecat-shared-dynamo/src/modules/removeItem.js +++ b/packages/spacecat-shared-dynamo/src/modules/removeItem.js @@ -12,6 +12,8 @@ import { performance } from 'perf_hooks'; +import { guardKey, guardTableName } from '../utils/guards.js'; + /** * Removes an item from a DynamoDB table. * @@ -23,13 +25,8 @@ import { performance } from 'perf_hooks'; * @throws {Error} Throws an error if the DynamoDB delete operation fails or input validation fails. */ async function removeItem(docClient, tableName, key, log = console) { - if (!tableName || typeof tableName !== 'string') { - throw new Error('Invalid tableName: must be a non-empty string.'); - } - - if (!key || typeof key !== 'object' || !key.partitionKey) { - throw new Error('Invalid key: must be an object with a partitionKey.'); - } + guardTableName(tableName); + guardKey(key); const params = { TableName: tableName, diff --git a/packages/spacecat-shared-dynamo/src/utils/guards.js b/packages/spacecat-shared-dynamo/src/utils/guards.js new file mode 100644 index 00000000..62b39272 --- /dev/null +++ b/packages/spacecat-shared-dynamo/src/utils/guards.js @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { hasText, isObject } from '@adobe/spacecat-shared-utils'; + +/** + * Validates that the provided table name is a non-empty string. + * + * @param {string} tableName - The name of the table to validate. + * @throws {Error} If the table name is empty or not a string. + */ +const guardTableName = (tableName) => { + if (!hasText(tableName)) { + throw new Error('Table name is required.'); + } +}; + +/** + * Validates that the provided key is an object and contains a partitionKey. + * + * @param {object} key - The key object to validate. + * @throws {Error} If the key is not an object or does not contain a partitionKey. + */ +const guardKey = (key) => { + if (!isObject(key) || !key.partitionKey) { + throw new Error('Key must be an object with a partitionKey.'); + } +}; + +/** + * Validates that the provided parameters object contains the required query parameters. + * + * @param {object} params - The parameters object to validate. + * @throws {Error} If the parameters object is not an object or is missing required properties. + */ +const guardQueryParameters = (params) => { + if (!isObject(params)) { + throw new Error('Query parameters must be an object.'); + } + + const requiredProps = ['TableName', 'KeyConditionExpression', 'ExpressionAttributeValues']; + for (const prop of requiredProps) { + if (!Object.prototype.hasOwnProperty.call(params, prop)) { + throw new Error(`Query parameters is missing required parameter: ${prop}`); + } + } +}; + +export { + guardKey, + guardQueryParameters, + guardTableName, +}; diff --git a/packages/spacecat-shared-dynamo/test/modules/getItem.test.js b/packages/spacecat-shared-dynamo/test/modules/getItem.test.js index 16fdbd28..f89a131b 100644 --- a/packages/spacecat-shared-dynamo/test/modules/getItem.test.js +++ b/packages/spacecat-shared-dynamo/test/modules/getItem.test.js @@ -45,7 +45,7 @@ describe('getItem', () => { await dynamoDbClient.getItem('', key); expect.fail('getItem did not throw with empty tableName'); } catch (error) { - expect(error.message).to.equal('Invalid tableName: must be a non-empty string.'); + expect(error.message).to.equal('Table name is required.'); } }); @@ -54,7 +54,7 @@ describe('getItem', () => { await dynamoDbClient.getItem('TestTable', null); expect.fail('getItem did not throw with invalid key'); } catch (error) { - expect(error.message).to.equal('Invalid key: must be an object with a partitionKey.'); + expect(error.message).to.equal('Key must be an object with a partitionKey.'); } }); diff --git a/packages/spacecat-shared-dynamo/test/modules/putItem.test.js b/packages/spacecat-shared-dynamo/test/modules/putItem.test.js index 2c32df75..cbccbdcf 100644 --- a/packages/spacecat-shared-dynamo/test/modules/putItem.test.js +++ b/packages/spacecat-shared-dynamo/test/modules/putItem.test.js @@ -37,7 +37,7 @@ describe('putItem', () => { await dynamoDbClient.putItem('', { someKey: 'someValue' }); expect.fail('putItem did not throw with empty tableName'); } catch (error) { - expect(error.message).to.equal('Invalid tableName: must be a non-empty string.'); + expect(error.message).to.equal('Table name is required.'); } }); diff --git a/packages/spacecat-shared-dynamo/test/modules/query.test.js b/packages/spacecat-shared-dynamo/test/modules/query.test.js index 3424f47d..45c1e885 100644 --- a/packages/spacecat-shared-dynamo/test/modules/query.test.js +++ b/packages/spacecat-shared-dynamo/test/modules/query.test.js @@ -16,6 +16,14 @@ import { expect } from 'chai'; import { createClient } from '../../src/index.js'; describe('query', () => { + const queryParams = { + TableName: 'TestTable', + KeyConditionExpression: 'partitionKey = :partitionKey', + ExpressionAttributeValues: { + ':partitionKey': 'testPartitionKey', + }, + }; + let dynamoDbClient; let mockDocClient; @@ -35,12 +43,12 @@ describe('query', () => { }); it('queries items from the database', async () => { - const result = await dynamoDbClient.query({ TableName: 'TestTable' }); + const result = await dynamoDbClient.query(queryParams); expect(result).to.be.an('array'); }); it('queries items from the database with pagination', async () => { - const result = await dynamoDbClient.query({ TableName: 'TestTable' }); + const result = await dynamoDbClient.query(queryParams); expect(result).to.have.lengthOf(3); expect(result).to.deep.equal(['item1', 'item2', 'item3']); }); @@ -51,7 +59,7 @@ describe('query', () => { }; try { - await dynamoDbClient.query({ TableName: 'TestTable' }); + await dynamoDbClient.query(queryParams); expect.fail('queryDb did not throw as expected'); } catch (error) { expect(error.message).to.equal('Query failed'); diff --git a/packages/spacecat-shared-dynamo/test/modules/removeItem.test.js b/packages/spacecat-shared-dynamo/test/modules/removeItem.test.js index f1c772f7..37f94fd6 100644 --- a/packages/spacecat-shared-dynamo/test/modules/removeItem.test.js +++ b/packages/spacecat-shared-dynamo/test/modules/removeItem.test.js @@ -45,7 +45,7 @@ describe('removeItem', () => { await dynamoDbClient.removeItem('', key); expect.fail('removeItem did not throw with empty tableName'); } catch (error) { - expect(error.message).to.equal('Invalid tableName: must be a non-empty string.'); + expect(error.message).to.equal('Table name is required.'); } }); @@ -54,7 +54,7 @@ describe('removeItem', () => { await dynamoDbClient.removeItem('TestTable', null); expect.fail('removeItem did not throw with invalid key'); } catch (error) { - expect(error.message).to.equal('Invalid key: must be an object with a partitionKey.'); + expect(error.message).to.equal('Key must be an object with a partitionKey.'); } }); diff --git a/packages/spacecat-shared-dynamo/test/utils/guards.test.js b/packages/spacecat-shared-dynamo/test/utils/guards.test.js new file mode 100644 index 00000000..814b2c4d --- /dev/null +++ b/packages/spacecat-shared-dynamo/test/utils/guards.test.js @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect } from 'chai'; +import { guardKey, guardQueryParameters, guardTableName } from '../../src/utils/guards.js'; + +describe('Query Parameter Guards', () => { + // Test guardTableName + describe('guardTableName', () => { + it('should throw an error if tableName is empty', () => { + expect(() => guardTableName('')).to.throw('Table name is required.'); + }); + + it('should not throw an error for valid tableName', () => { + expect(() => guardTableName('validTableName')).to.not.throw(); + }); + }); + + // Test guardKey + describe('guardKey', () => { + it('should throw an error if key is not an object', () => { + expect(() => guardKey('notAnObject')).to.throw('Key must be an object with a partitionKey.'); + }); + + it('should throw an error if key does not have partitionKey', () => { + expect(() => guardKey({})).to.throw('Key must be an object with a partitionKey.'); + }); + + it('should not throw an error for a valid key', () => { + expect(() => guardKey({ partitionKey: 'value' })).to.not.throw(); + }); + }); + + describe('guardQueryParameters', () => { + it('should throw an error if params is not an object', () => { + expect(() => guardQueryParameters('notAnObject')).to.throw('Query parameters must be an object.'); + }); + + it('should throw an error if any required parameter is missing', () => { + expect(() => guardQueryParameters({ TableName: 'table' })).to.throw('Query parameters is missing required parameter: KeyConditionExpression'); + }); + + it('should not throw an error for valid params', () => { + const validParams = { + TableName: 'table', + KeyConditionExpression: 'expression', + ExpressionAttributeValues: {}, + }; + expect(() => guardQueryParameters(validParams)).to.not.throw(); + }); + }); +}); diff --git a/packages/spacecat-shared-example/package.json b/packages/spacecat-shared-example/package.json index 576614ee..00eb49c6 100644 --- a/packages/spacecat-shared-example/package.json +++ b/packages/spacecat-shared-example/package.json @@ -31,10 +31,8 @@ "dependencies": { "@adobe/fetch": "4.1.1", "@adobe/helix-shared-wrap": "2.0.0", + "@adobe/helix-universal": "4.4.1", "aws4": "1.12.0" }, - "optionalDependencies": { - "@adobe/helix-universal": "4.4.1" - }, "devDependencies": {} } diff --git a/packages/spacecat-shared-utils/README.md b/packages/spacecat-shared-utils/README.md index 219cd85e..bc7db61f 100644 --- a/packages/spacecat-shared-utils/README.md +++ b/packages/spacecat-shared-utils/README.md @@ -1,3 +1,57 @@ -# Spacecat Shared - Utils +# SpaceCat Shared Utilities -A collection of utility functions. +This repository contains a collection of shared utility functions used across various SpaceCat projects. These utilities provide a range of checks and validations, from basic data type validation to more complex checks like ISO date strings and URL validation. + +## Installation + +To install the SpaceCat Shared Utilities, you can use npm: + +```bash +npm install spacecat-shared-utils +``` + +Or, if you are using yarn: + +```bash +yarn add spacecat-shared-utils +``` + +## Usage + +Here's how you can use the different utility functions in your project: + +```javascript +import { isBoolean, isValidUrl } from 'spacecat-shared-utils'; + +console.log(isBoolean('true')); // true +console.log(isValidUrl('https://www.example.com')); // true +``` + +## Functions + +The library includes the following utility functions: + +- `isBoolean(value)`: Determines if the given value is a boolean or a string representation of a boolean. +- `isInteger(value)`: Checks if the given value is an integer. +- `isValidDate(obj)`: Checks whether the given object is a valid JavaScript Date. +- `isIsoDate(str)`: Validates whether the given string is a JavaScript ISO date string in Zulu (UTC) timezone. +- `isIsoTimeOffsetsDate(str)`: Validates whether the given string is a JavaScript ISO date string following UTC time offsets format. +- `isNumber(value)`: Determines if the given value is a number. +- `isObject(obj)`: Checks if the given parameter is an object and not an array or null. +- `isString(str)`: Determines if the given parameter is a string. +- `toBoolean(value)`: Converts a given value to a boolean. Throws an error if the value is not a boolean. +- `arrayEquals(a, b)`: Compares two arrays for equality. +- `isValidUrl(urlString)`: Validates whether the given string is a valid URL with http or https protocol. +- `hasText(str)`: Checks if the given string is not empty. + +## Testing + +This library includes a comprehensive test suite to ensure the reliability of the utility functions. To run the tests, use the following command: + +```bash +npm test +``` + +## License + +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE.txt) file for details. diff --git a/packages/spacecat-shared-utils/src/functions.js b/packages/spacecat-shared-utils/src/functions.js index 4e877714..1fc87f96 100644 --- a/packages/spacecat-shared-utils/src/functions.js +++ b/packages/spacecat-shared-utils/src/functions.js @@ -49,21 +49,21 @@ function isNumber(value) { /** * Checks if the given parameter is an object and not an array or null. * - * @param {*} obj - The object to check. + * @param {*} value - The value to check. * @returns {boolean} True if the parameter is an object, false otherwise. */ -function isObject(obj) { - return !Array.isArray(obj) && obj !== null && typeof obj === 'object'; +function isObject(value) { + return !Array.isArray(value) && value !== null && typeof value === 'object'; } /** * Determines if the given parameter is a string. * - * @param {*} str - The string to check. + * @param {*} value - The value to check. * @returns {boolean} True if the parameter is a string, false otherwise. */ -function isString(str) { - return (!!str || str === '') && typeof str === 'string'; +function isString(value) { + return (!!value || value === '') && typeof value === 'string'; } /** @@ -73,17 +73,17 @@ function isString(str) { * @returns {boolean} True if the string is not empty, false otherwise. */ function hasText(str) { - return !!str && typeof str === 'string'; + return !!str && isString(str); } /** * Checks whether the given object is a valid JavaScript Date. * - * @param {*} obj - The object to check. + * @param {*} value - The value to check. * @returns {boolean} True if the given object is a valid Date object, false otherwise. */ -function isValidDate(obj) { - return obj instanceof Date && !Number.isNaN(obj.getTime()); +function isValidDate(value) { + return value instanceof Date && !Number.isNaN(value.getTime()); } /** diff --git a/packages/spacecat-shared-utils/src/index.d.ts b/packages/spacecat-shared-utils/src/index.d.ts new file mode 100644 index 00000000..93989ddf --- /dev/null +++ b/packages/spacecat-shared-utils/src/index.d.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export function arrayEquals(a: T[], b: T[]): boolean; + +export function hasText(str: string): boolean; + +export function isBoolean(value: unknown): boolean; + +export function isInteger(value: unknown): boolean; + +export function isValidDate(value: unknown): boolean; + +export function isIsoDate(str: string): boolean; + +export function isIsoTimeOffsetsDate(str: string): boolean; + +export function isNumber(value: unknown): boolean; + +export function isObject(value: unknown): boolean; + +export function isString(value: unknown): boolean; + +export function toBoolean(value: unknown): boolean; + +export function isValidUrl(urlString: string): boolean;