From af3b2a9de69dd50bf8e119b6bdf12622977c1fe3 Mon Sep 17 00:00:00 2001 From: Mike Cornwell Date: Thu, 30 Jan 2025 12:09:17 -0500 Subject: [PATCH 1/4] feat(3.0): add support for 3.0 --- .github/workflows/feature.yml | 35 ++ .github/workflows/quality.yml | 31 ++ .github/workflows/ut.yml | 5 +- README.md | 15 +- dynamo-feature-tests.yml | 36 +++ eslint.config.mjs | 298 ++++++++++++++++++ features/schemas/model1.json | 19 ++ features/schemas/model2.json | 19 ++ features/schemas/model3.json | 19 ++ .../steps.mjs => step_definitions/steps.ts} | 165 ++++++---- package.json | 66 ++-- ...tastoreProvider.ts => datastoreAdapter.ts} | 156 ++++----- src/index.ts | 4 +- src/lib.ts | 41 ++- src/queryBuilder.ts | 195 ------------ ...vider.test.ts => datastoreAdapter.test.ts} | 167 ++++------ test/src/lib.test.ts | 11 +- test/src/queryBuilder.test.ts | 203 ------------ tsconfig.build.json | 106 ------- tsconfig.cucumber.json | 20 ++ tsconfig.json | 16 +- 21 files changed, 830 insertions(+), 797 deletions(-) create mode 100644 .github/workflows/feature.yml create mode 100644 .github/workflows/quality.yml create mode 100644 dynamo-feature-tests.yml create mode 100644 eslint.config.mjs create mode 100644 features/schemas/model1.json create mode 100644 features/schemas/model2.json create mode 100644 features/schemas/model3.json rename features/{stepDefinitions/steps.mjs => step_definitions/steps.ts} (64%) rename src/{datastoreProvider.ts => datastoreAdapter.ts} (53%) delete mode 100644 src/queryBuilder.ts rename test/src/{datastoreProvider.test.ts => datastoreAdapter.test.ts} (69%) delete mode 100644 test/src/queryBuilder.test.ts delete mode 100644 tsconfig.build.json create mode 100644 tsconfig.cucumber.json diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml new file mode 100644 index 0000000..b9d5d65 --- /dev/null +++ b/.github/workflows/feature.yml @@ -0,0 +1,35 @@ +name: Feature Tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + featureTests: + name: FeatureTests + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Create dynamo db for tests + run: docker-compose -f ./dynamodb-feature-tests.yml up -d + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run build + + - name: Run Cucumber Tests + run: npm run test:features diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..26795a3 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,31 @@ +name: Quality + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + quality: + name: Quality + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [22.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + + - name: Prettier + run: npm run prettier:check + + - name: Eslint + run: npm run eslint diff --git a/.github/workflows/ut.yml b/.github/workflows/ut.yml index cd3edec..2817c07 100644 --- a/.github/workflows/ut.yml +++ b/.github/workflows/ut.yml @@ -7,12 +7,13 @@ on: branches: [master] jobs: - build: + tests: + name: Tests runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 782b4e3..e9d4afc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,20 @@ ![Unit Tests](https://github.com/monolithst/functional-models-orm-dynamo/actions/workflows/ut.yml/badge.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/monolithst/functional-models-orm-dynamo/badge.svg?branch=master)](https://coveralls.io/github/monolithst/functional-models-orm-dynamo?branch=master) -Provides an functional-models-orm datastore provider for AWS Dynamo. +Provides an functional-models datastore adapter for AWS Dynamo. + +# Important Notes - Don't Skip + +Dynamodb is great for the one thing that its good at (being a key->value store). But any searching is a bad idea. + +While this datastore adapter absolutely 100% works and works well in production (from years of experience), it should never ever +be used for searching in production. This isn't a limitation of the adapter, it's a limitation of the datastore itself. + +Our implementation of `search` is deliberately unoptimized and will indeed pull the entire database across the wire one section at a time. + +`save`, `retrieve`, `bulkInsert`, `delete` work great, but `search` should only be used for limited development purposes. + +You have been warned. ## AWS SDK 3.0 diff --git a/dynamo-feature-tests.yml b/dynamo-feature-tests.yml new file mode 100644 index 0000000..fa741db --- /dev/null +++ b/dynamo-feature-tests.yml @@ -0,0 +1,36 @@ +version: '3' + +services: + dynamodb-local: + command: '-jar DynamoDBLocal.jar -sharedDb -dbPath ./data' + image: 'amazon/dynamodb-local:latest' + user: root + ports: + - '42514:8000' + volumes: + - './docker/dynamodb:/home/dynamodblocal/data' + working_dir: /home/dynamodblocal + healthcheck: + test: + [ + 'CMD-SHELL', + '[ "$(curl -s -o /dev/null -I -w ''%{http_code}'' http://localhost:8000)" == "400" ]', + ] + interval: 10s + timeout: 10s + retries: 10 + dynamodb-local-setup: + container_name: dynamodb-local-setup + depends_on: + dynamodb-local: + condition: service_healthy + image: amazon/aws-cli + volumes: + - './features/schemas:/tmp/dynamo' + environment: + AWS_ACCESS_KEY_ID: 'FAKEID' + AWS_SECRET_ACCESS_KEY: 'FAKEKEY' + AWS_REGION: 'us-east-1' + entrypoint: + - bash + command: '-c "for f in /tmp/dynamo/*.json; do aws dynamodb create-table --endpoint-url "http://dynamodb-local:8000" --cli-input-json file://"$${f#./}"; done"' diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..2328be6 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,298 @@ +import { fixupConfigRules, fixupPluginRules } from '@eslint/compat' +import { FlatCompat } from '@eslint/eslintrc' +import js from '@eslint/js' +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import tsParser from '@typescript-eslint/parser' +import functional from 'eslint-plugin-functional' +import _import from 'eslint-plugin-import' +import parser from 'esprima' +import globals from 'globals' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +export default [ + { + ignores: [ + 'buildDocs', + 'eslint.config.mjs', + 'dist/', + 'node_modules/', + 'test/', + 'coverage/', + 'features/', + 'stepDefinitions/', + 'cucumber.js', + ], + }, + ...fixupConfigRules( + compat.extends( + 'eslint:recommended', + 'prettier', + 'plugin:import/typescript', + 'plugin:import/recommended', + 'plugin:@typescript-eslint/recommended' + ) + ), + { + plugins: { + import: fixupPluginRules(_import), + functional, + '@typescript-eslint': fixupPluginRules(typescriptEslint), + }, + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + ...globals.mocha, + }, + ecmaVersion: 2020, + sourceType: 'commonjs', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + + project: ['./tsconfig.json'], + }, + }, + settings: { + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + 'import/ignore': ['node_modules'], + 'import/resolver': { + typescript: true, + + moduleDirectory: ['node_modules', 'src/'], + node: { + extensions: ['.ts', '.tsx'], + }, + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/ban-ts-comment': 0, + 'no-await-in-loop': ['error'], + 'no-console': [ + 'error', + { + allow: ['warn', 'error', 'info', 'debug'], + }, + ], + 'no-constant-condition': ['error'], + 'no-extra-parens': 0, + 'no-extra-semi': ['error'], + 'no-loss-of-precision': ['error'], + 'no-promise-executor-return': ['error'], + 'no-template-curly-in-string': ['error'], + 'no-useless-backreference': ['error'], + 'no-unused-vars': 0, + 'require-atomic-updates': ['error'], + 'accessor-pairs': ['error'], + 'array-callback-return': ['error'], + 'block-scoped-var': ['error'], + 'class-methods-use-this': ['error'], + complexity: ['error'], + 'consistent-return': ['error'], + curly: ['error'], + 'default-case': ['error'], + 'default-case-last': ['error'], + 'default-param-last': 0, + 'dot-location': ['error', 'property'], + 'dot-notation': ['error'], + eqeqeq: ['error'], + 'grouped-accessor-pairs': ['error'], + 'guard-for-in': ['error'], + 'max-classes-per-file': ['error'], + 'no-alert': ['error'], + 'no-caller': ['error'], + 'no-constructor-return': ['error'], + 'no-div-regex': ['error'], + 'no-else-return': ['error'], + 'no-empty-function': ['error'], + 'no-eq-null': ['error'], + 'no-eval': ['error'], + 'no-extend-native': ['error'], + 'no-extra-bind': ['error'], + 'no-extra-label': ['error'], + 'no-floating-decimal': ['error'], + 'no-implicit-coercion': ['error'], + 'no-implicit-globals': ['error'], + 'no-implied-eval': ['error'], + 'no-invalid-this': ['error'], + 'no-iterator': ['error'], + 'no-labels': ['error'], + 'no-lone-blocks': ['error'], + 'no-loop-func': ['error'], + 'no-magic-numbers': [ + 'error', + { + ignore: [1, -1, 0, 2], + }, + ], + 'no-multi-spaces': ['error'], + 'no-multi-str': ['error'], + 'no-new': ['error'], + 'no-new-func': ['error'], + 'no-new-wrappers': ['error'], + 'no-octal-escape': ['error'], + 'no-proto': ['error'], + 'no-restricted-properties': ['error'], + 'no-return-assign': ['error'], + 'no-return-await': ['error'], + 'no-script-url': ['error'], + 'no-self-compare': ['error'], + 'no-sequences': ['error'], + 'no-throw-literal': ['error'], + 'no-unmodified-loop-condition': ['error'], + 'no-unused-expressions': ['error'], + 'no-useless-call': ['error'], + 'no-useless-concat': ['error'], + 'no-void': ['error'], + 'no-warning-comments': ['warn'], + 'prefer-named-capture-group': ['error'], + 'prefer-promise-reject-errors': 0, + 'prefer-regex-literals': ['error'], + radix: ['error'], + 'require-unicode-regexp': ['error'], + 'vars-on-top': ['error'], + 'wrap-iife': ['error'], + yoda: ['error'], + 'arrow-body-style': 0, + 'eol-last': ['error', 'always'], + 'comma-dangle': 0, + 'linebreak-style': 0, + 'no-underscore-dangle': 0, + 'no-unused-labels': 0, + 'object-shorthand': 0, + 'prefer-rest-params': 0, + semi: ['error', 'never'], + 'functional/immutable-data': [ + 'error', + { + ignoreAccessorPattern: 'module.exports*', + }, + ], + 'functional/no-let': ['error'], + 'functional/prefer-property-signatures': ['error'], + 'functional/prefer-readonly-type': 0, + 'functional/prefer-immutable-types': 0, + 'functional/prefer-tacit': ['error'], + 'functional/no-classes': ['error'], + 'functional/no-mixed-type': 0, + 'functional/no-this-expressions': ['error'], + 'functional/no-conditional-statements': 0, + 'functional/no-expression-statement': 0, + 'functional/no-loop-statements': ['error'], + 'functional/no-return-void': 0, + 'functional/no-promise-reject': 0, + 'functional/no-throw-statement': 0, + 'functional/no-try-statements': ['error'], + 'functional/readonly-type': ['error'], + 'functional/functional-parameters': 0, + 'import/no-unresolved': ['error'], + 'import/named': ['error'], + 'import/default': ['error'], + 'import/namespace': ['error'], + 'import/no-restricted-paths': ['error'], + 'import/no-absolute-path': ['error'], + 'import/no-dynamic-require': ['error'], + 'import/no-internal-modules': 0, + 'import/no-webpack-loader-syntax': ['error'], + 'import/no-self-import': ['error'], + 'import/no-cycle': ['error'], + 'import/no-useless-path-segments': ['error'], + 'import/no-relative-parent-imports': 0, + 'import/export': ['error'], + 'import/no-named-as-default': ['error'], + 'import/no-named-as-default-member': ['error'], + 'import/no-deprecated': ['error'], + 'import/no-extraneous-dependencies': ['error'], + 'import/no-mutable-exports': ['error'], + 'import/no-unused-modules': ['error'], + 'import/unambiguous': ['error'], + 'import/no-commonjs': 0, + 'import/no-amd': ['error'], + 'import/no-nodejs-modules': 0, + 'import/first': ['error'], + 'import/exports-last': 0, + 'import/no-duplicates': ['error'], + 'import/no-namespace': 0, + 'import/extensions': ['error'], + 'import/order': ['error'], + 'import/newline-after-import': ['error'], + 'import/prefer-default-export': 0, + 'import/no-unassigned-import': ['error'], + 'import/no-named-default': 0, + 'import/no-named-export': 0, + 'import/no-anonymous-default-export': 0, + 'import/group-exports': 0, + 'import/dynamic-import-chuckname': 0, + }, + }, + { + files: ['./src/*.js', './*.js'], + languageOptions: { + parser: parser, + ecmaVersion: 2020, + sourceType: 'script', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + project: [], + }, + }, + }, + { + files: ['./src/*.ts', './src/**/*.ts'], + languageOptions: { + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'script', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + project: ['./tsconfig.json'], + }, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + vars: 'all', + argsIgnorePattern: '(_+)|(action)', + args: 'after-used', + ignoreRestSiblings: false, + }, + ], + }, + }, + { + files: ['./src/orval/**/*.ts'], + languageOptions: { + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'script', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + project: ['./tsconfig.json'], + }, + }, + rules: { + 'import/max-dependencies': 0, + 'import/order': 0, + }, + }, +] diff --git a/features/schemas/model1.json b/features/schemas/model1.json new file mode 100644 index 0000000..91423b5 --- /dev/null +++ b/features/schemas/model1.json @@ -0,0 +1,19 @@ +{ + "TableName": "functional-models-orm-dynamo-model-1", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } +} diff --git a/features/schemas/model2.json b/features/schemas/model2.json new file mode 100644 index 0000000..bec4001 --- /dev/null +++ b/features/schemas/model2.json @@ -0,0 +1,19 @@ +{ + "TableName": "functional-models-orm-dynamo-model-2", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } +} diff --git a/features/schemas/model3.json b/features/schemas/model3.json new file mode 100644 index 0000000..d29c963 --- /dev/null +++ b/features/schemas/model3.json @@ -0,0 +1,19 @@ +{ + "TableName": "functional-models-orm-dynamo-model-3", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } +} diff --git a/features/stepDefinitions/steps.mjs b/features/step_definitions/steps.ts similarity index 64% rename from features/stepDefinitions/steps.mjs rename to features/step_definitions/steps.ts index dc2192a..bcfdbcb 100644 --- a/features/stepDefinitions/steps.mjs +++ b/features/step_definitions/steps.ts @@ -1,36 +1,69 @@ import { randomUUID } from 'crypto' import { assert } from 'chai' import { Before, After, Given, When, Then } from '@cucumber/cucumber' -import functionalModels, { ObjectProperty } from 'functional-models' -import { ormQuery, orm } from 'functional-models-orm' -import createDatastoreProvider from '../../dist/datastoreProvider.js' +import { + ObjectProperty, + TextProperty, + ModelType, + PrimaryKeyUuidProperty, + createOrm, + queryBuilder, +} from 'functional-models' +import * as dynamoDatastoreAdapter from '../../src/datastoreAdapter' import * as dynamo from '@aws-sdk/client-dynamodb' import * as libDynamo from '@aws-sdk/lib-dynamodb' -const { TextProperty, Model, UniqueId } = functionalModels - -const createDynamoDatastoreProvider = context => { - if (!context.parameters.testTable) { - throw new Error( - `Must include a testing table that exists in the world parameters.` - ) - } - if (!context.parameters.awsRegion) { - throw new Error(`Must include awsRegion in the world parameters.`) - } - context.table = context.parameters.testTable +const createDynamoDatastoreProvider = async () => { const dynamoDbClient = new dynamo.DynamoDBClient({ - region: context.parameters.awsRegion, + region: 'http://127.0.0.1:42514', + credentials: { + accessKeyId: 'x', + secretAccessKey: 'x', + }, + endpoint: { + hostname: 'localhost', + port: 42514, + path: '', + protocol: 'http:', + }, }) - return createDatastoreProvider.default({ + /* + const tableNames = ['model-1', 'model-2', 'model-3'].map( + name => `functional-models-orm-dynamo-${name}` + ) + const commands = tableNames.map(name => { + return new dynamo.CreateTableCommand({ + TableName: name, + AttributeDefinitions: [ + { + AttributeName: 'id', + AttributeType: 'S', + }, + ], + KeySchema: [ + { + KeyType: 'HASH', + AttributeName: 'id', + }, + ], + }) + }) + await Promise.all( + commands.map(x => + dynamoDbClient.send(x).catch(e => { + console.log(e) + }) + ) + ) + + */ + + return dynamoDatastoreAdapter.create({ aws3: { ...dynamo, ...libDynamo, dynamoDbClient, }, - getTableNameForModel: () => { - return `${context.table}` - }, }) } @@ -40,26 +73,32 @@ const DATASTORES = { const MODELS = { Model1: [ - 'Model1', { + pluralName: 'Model1', + namespace: 'functional-models-orm-dynamo', properties: { + id: TextProperty(), name: TextProperty(), }, }, ], Model2: [ - 'Model2', { + pluralName: 'Model2', + namespace: 'functional-models-orm-dynamo', properties: { + id: TextProperty(), name: TextProperty({ required: false }), key: TextProperty({ required: false }), }, }, ], Model3: [ - 'Model3', { + pluralName: 'Model3', + namespace: 'functional-models-orm-dynamo', properties: { + id: TextProperty(), name: TextProperty({ required: true }), obj: ObjectProperty({ required: true }), }, @@ -72,31 +111,31 @@ const MODEL_DATA = { id: 'test-id', name: 'test-name', }), - ModelData2: { + ModelData2: () => ({ id: 'test-id', name: 'test-name', key: undefined, - }, - StoredModelData2: { + }), + StoredModelData2: () => ({ id: 'test-id', name: 'test-name', key: null, - }, - ModelData3: { + }), + ModelData3: () => ({ id: 'test-id', name: 'test-name', obj: { nested: 'value', nested2: undefined, }, - }, - StoredModelData3: { + }), + StoredModelData3: () => ({ id: 'test-id', name: 'test-name', obj: { nested: 'value', }, - }, + }), BulkModelData1: () => ({ id: randomUUID(), name: 'test-me', @@ -115,20 +154,15 @@ const MODEL_DATA = { } const QUERIES = { - SearchQuery1: ormQuery - .ormQueryBuilder() - .property('name', 'test-name') - .take(1) - .compile(), - BulkSearchQuery1: ormQuery - .ormQueryBuilder() + SearchQuery1: queryBuilder().property('name', 'test-name').take(1).compile(), + BulkSearchQuery1: queryBuilder() //.property('name', 'test-me') .compile(), } -const _emptyDatastoreProvider = async (model, datastoreProvider) => { - await datastoreProvider - .search(model, ormQuery.ormQueryBuilder().compile()) +const _emptyDatastoreProvider = async (model, datastoreAdapter) => { + await datastoreAdapter + .search(model, queryBuilder().compile()) .then(async obj => Promise.all( obj.instances.map(x => { @@ -138,18 +172,18 @@ const _emptyDatastoreProvider = async (model, datastoreProvider) => { ) } -Given('orm using the {word}', function (store) { - store = DATASTORES[store](this) +Given('orm using the {word}', async function (store) { + store = await DATASTORES[store](this) if (!store) { throw new Error(`${store} did not result in a datastore.`) } - this.BaseModel = orm({ datastoreProvider: store }).BaseModel - this.datastoreProvider = store + this.Model = createOrm({ datastoreAdapter: store }).Model + this.datastoreAdapter = store }) Given('the datastore is emptied of models', function () { - return _emptyDatastoreProvider(this.model, this.datastoreProvider) + return _emptyDatastoreProvider(this.model, this.datastoreAdapter) }) Given('the orm is used to create {word}', function (modelType) { @@ -157,15 +191,7 @@ Given('the orm is used to create {word}', function (modelType) { if (!model) { throw new Error(`${modelType} did not result in a model.`) } - this.model = this.BaseModel(...model) -}) - -Given('the ormQueryBuilder is used to make {word}', function (queryKey) { - if (!QUERY_BUILDER_FUNCS[queryKey]) { - throw new Error(`${queryKey} did not result in a query`) - } - - this.query = QUERY_BUILDER_FUNCS[queryKey]() + this.model = this.Model(...model) }) When('instances of the model are created with {word}', function (dataKey) { @@ -191,7 +217,13 @@ When('save is called on the instances', function () { }) When('save is called on the model', function () { - return this.modelInstance.save().then(x => (this.saveResult = x)) + return this.modelInstance + .save() + .catch(e => { + console.log(e) + throw e + }) + .then(x => (this.saveResult = x)) }) When('delete is called on the model', function () { @@ -201,20 +233,25 @@ When('delete is called on the model', function () { When("the datastore's retrieve is called with values", function (table) { const rows = table.rowsHash() const id = rows.id - return this.datastoreProvider.retrieve(this.model, id).then(obj => { + return this.datastoreAdapter.retrieve(this.model, id).then(obj => { this.result = obj }) }) -When("the datastore's delete is called with modelInstance", function () { - return this.datastoreProvider.delete(this.modelInstance).then(obj => { - this.result = obj - }) +When("the datastore's delete is called with modelInstance", async function () { + return this.datastoreAdapter + .delete( + this.modelInstance.getModel(), + await this.modelInstance.getPrimaryKey() + ) + .then(obj => { + this.result = obj + }) }) When("the datastore's search is called with {word}", function (key) { const query = QUERIES[key] - return this.datastoreProvider.search(this.model, query).then(async obj => { + return this.datastoreAdapter.search(this.model, query).then(async obj => { this.result = obj }) }) @@ -224,12 +261,10 @@ When('search is called on the Model using the query', function () { }) Then('the result matches {word}', function (dataKey) { - const data = MODEL_DATA[dataKey] + const data = MODEL_DATA[dataKey]() try { assert.deepEqual(this.result, data) } catch (e) { - console.log(this.result) - console.log(data) throw e } }) diff --git a/package.json b/package.json index 980d564..a955e02 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,20 @@ { "name": "functional-models-orm-dynamo", - "version": "2.1.7", - "description": "An implmentation of functional-models-orm for dynamodb.", + "version": "3.0", + "description": "An implmentation of functional-models for dynamodb.", "main": "index.js", "types": "index.d.ts", "scripts": { + "build": "rm -Rf ./dist && tsc && cp package.json ./dist && cp README.md ./dist", + "build:watch": "nodemon -e '*' --watch ./src --exec npm run build", + "commit": "cz", + "dist": "npm run build && cd dist && npm publish", "eslint": "eslint .", "prettier": "prettier --write .", - "test": "export TS_NODE_TRANSPILE_ONLY=true && export TS_NODE_PROJECT='./tsconfig.test.json' && mocha -r ts-node/register 'test/**/*.test.ts'", + "prettier:check": "prettier -c .", + "test": "mocha -r ts-node/register test/**/*.test.ts", "test:coverage": "nyc npm run test", - "feature-tests": "./node_modules/.bin/cucumber-js", - "coverage": "nyc --all --reporter=lcov npm test", - "build": "tsc -p ./tsconfig.build.json && cp package.json ./dist && cp README.md ./dist", - "watch": "tsc -w -p ./tsconfig.build.json -noEmit", - "dist": "npm run build && cd dist && npm publish" + "test:features": "TS_NODE_PROJECT=tsconfig.cucumber.json ./node_modules/.bin/cucumber-js --require ./features/step_definitions/steps.ts --require-module ts-node/register" }, "publishConfig": { "registry": "https://registry.npmjs.org" @@ -22,10 +23,17 @@ "type": "git", "url": "git+https://github.com/monolithst/functional-models-orm-dynamo.git" }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, "keywords": [ "orm", "dynamodb", "nodejs", + "functional-models", + "aws", "functional" ], "author": "Mike Cornwell", @@ -54,34 +62,40 @@ "dependencies": { "@aws-sdk/client-dynamodb": "^3.623.0", "@aws-sdk/lib-dynamodb": "^3.556.0", - "functional-models": "^2.1.14", - "functional-models-orm": "^2.1.12", + "functional-models": "^3.0.12", + "functional-models-orm-memory": "^3.0.0", "lodash": "^4.17.21" }, "devDependencies": { - "@cucumber/cucumber": "^9.6.0", + "@cucumber/cucumber": "^11.2.0", + "@eslint/compat": "^1.2.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.12.0", "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@types/chai": "^4.3.12", - "@types/lodash": "^4.17.0", + "@types/chai": "^4.3.16", + "@types/chai-as-promised": "^7.1.8", + "@types/lodash": "^4.17.1", "@types/mocha": "^10.0.6", - "@types/node": "^20.11.27", - "@types/proxyquire": "^1.3.31", "@types/sinon": "^17.0.3", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "babel-eslint": "^10.1.0", - "chai": "^4.2.0", - "eslint": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", + "chai": "^4.3.0", + "chai-as-promised": "^7.1.2", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^9.18.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-functional": "^6.1.1", - "eslint-plugin-import": "^2.29.1", - "mocha": "^10.8.2", + "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-functional": "~7.1.0", + "eslint-plugin-import": "^2.31.0", + "mocha": "^10.4.0", + "nodemon": "^3.1.9", "nyc": "^15.1.0", - "prettier": "^3.2.5", + "prettier-plugin-organize-imports": "^3.2.4", "proxyquire": "^2.1.3", - "sinon": "^17.0.1", - "ts-mocha": "^10.0.0", + "sinon": "^11.1.2", + "source-map-support": "^0.5.21", "ts-node": "^10.9.2", + "tsx": "^4.19.2", "typescript": "^5.7.2" }, "homepage": "https://github.com/monolithst/functional-models-orm-dynamo#readme" diff --git a/src/datastoreProvider.ts b/src/datastoreAdapter.ts similarity index 53% rename from src/datastoreProvider.ts rename to src/datastoreAdapter.ts index c527eb4..4840de4 100644 --- a/src/datastoreProvider.ts +++ b/src/datastoreAdapter.ts @@ -1,87 +1,89 @@ import { get, merge } from 'lodash' import { - Model, - FunctionalModel, + ModelType, + DataDescription, ModelInstance, PrimaryKeyType, -} from 'functional-models/interfaces' -import { OrmQuery, DatastoreProvider } from 'functional-models-orm/interfaces' + DatastoreAdapter, + OrmSearch, +} from 'functional-models' +import { filterResults } from 'functional-models-orm-memory' import { + fromDynamo, getTableNameForModel as defaultTableModelName, splitArrayIntoArraysOfMaxSize, + buildScanQuery, } from './lib' -import queryBuilder from './queryBuilder' import { SCAN_RETURN_THRESHOLD } from './constants' const MAX_BATCH_SIZE = 25 +type WithRequired = T & { [P in K]-?: T[P] } -type DatastoreProviderInputs = { - readonly aws3: Aws3Client - readonly getTableNameForModel?: ( - m: Model - ) => string - readonly createUniqueId?: ((s: any) => string) | undefined -} +type DatastoreAdapterInputs = Readonly<{ + aws3: Aws3Client + getTableNameForModel?: (m: ModelType) => string +}> -type Aws3Client = { - readonly dynamoDbClient: any - readonly DynamoDBDocumentClient: any - readonly PutCommand: any - readonly GetCommand: any - readonly DeleteCommand: any - readonly ScanCommand: any - readonly BatchWriteCommand: any -} +type Aws3Client = Readonly<{ + dynamoDbClient: any + DynamoDBDocumentClient: any + PutCommand: any + GetCommand: any + DeleteCommand: any + ScanCommand: any + BatchWriteCommand: any +}> -const dynamoDatastoreProvider = ({ +const create = ({ aws3, getTableNameForModel = defaultTableModelName, - createUniqueId = undefined, -}: DatastoreProviderInputs): DatastoreProvider => { +}: DatastoreAdapterInputs): WithRequired => { if (!getTableNameForModel) { throw new Error(`Must include ${getTableNameForModel}`) } - const _doSearchUntilThresholdOrNoLastEvaluatedKey = ( + const _doSearchUntilThresholdOrNoLastEvaluatedKey = async < + T extends DataDescription, + >( dynamo: any, tableName: string, - ormQuery: OrmQuery, - oldInstancesFound = [] + ormQuery: OrmSearch, + oldInstancesFound: T[] = [] ) => { - const query = queryBuilder({ createUniqueId })(tableName, ormQuery) + const query = buildScanQuery(tableName, ormQuery.page) const command = new aws3.ScanCommand(query) - return dynamo.send(command).then((data: any) => { - const instances = data.Items.map((item: object) => { - return Object.entries(item).reduce((acc, [key, obj]) => { - return merge(acc, { [key]: obj }) - }, {}) - }).concat(oldInstancesFound) + const data = await dynamo.send(command) + const unfiltered: T[] = data.Items.map(fromDynamo) + + const filtered = filterResults(ormQuery, unfiltered).concat( + oldInstancesFound + ) - const usingTake = ormQuery.take && ormQuery.take > 0 - const take = usingTake ? ormQuery.take : SCAN_RETURN_THRESHOLD - const lastEvaluatedKey = get(data, 'LastEvaluatedKey', null) - /* - We want to keep scanning until we've met our threshold OR - there is no more keys to evaluate OR - we have a "take" and we've hit our max. - */ - const stopForThreshold = instances.length > take - const stopForNoMore = !lastEvaluatedKey - if (stopForThreshold || stopForNoMore) { - return { - instances: instances.slice(0, take), - page: usingTake ? null : lastEvaluatedKey, - } + const usingTake = ormQuery.take && ormQuery.take > 0 + const take = usingTake ? ormQuery.take : SCAN_RETURN_THRESHOLD + const lastEvaluatedKey = get(data, 'LastEvaluatedKey', null) + /* + We want to keep scanning until we've met our threshold OR + there is no more keys to evaluate OR + we have a "take" and we've hit our max. + */ + // @ts-ignore + const stopForThreshold = filtered.length > take + const stopForNoMore = !lastEvaluatedKey + if (stopForThreshold || stopForNoMore) { + return { + instances: filtered.slice(0, take), + page: usingTake ? null : lastEvaluatedKey, } - const newQuery = merge(ormQuery, { - page: lastEvaluatedKey, - }) - return _doSearchUntilThresholdOrNoLastEvaluatedKey( - dynamo, - tableName, - newQuery, - instances - ) + } + const newQuery = merge(ormQuery, { + page: lastEvaluatedKey, }) + return _doSearchUntilThresholdOrNoLastEvaluatedKey( + dynamo, + tableName, + newQuery, + filtered + ) } const _getDocClient = () => { @@ -95,9 +97,9 @@ const dynamoDatastoreProvider = ({ ) } - const search = ( - model: Model, - ormQuery: OrmQuery + const search = ( + model: ModelType, + ormQuery: OrmSearch ) => { return Promise.resolve().then(async () => { const tableName = getTableNameForModel(model) @@ -110,14 +112,14 @@ const dynamoDatastoreProvider = ({ }) } - const retrieve = ( - model: Model, + const retrieve = ( + model: ModelType, id: PrimaryKeyType ) => { return Promise.resolve().then(() => { const tableName = getTableNameForModel(model) const docClient = _getDocClient() - const primaryKeyName = model.getPrimaryKeyName() + const primaryKeyName = model.getModelDefinition().primaryKeyName const command = new aws3.GetCommand({ TableName: tableName, Key: { [primaryKeyName]: `${id}` }, @@ -126,13 +128,15 @@ const dynamoDatastoreProvider = ({ }) } - const save = async >( - instance: ModelInstance + const save = async ( + instance: ModelInstance ) => { return Promise.resolve().then(async () => { const tableName = getTableNameForModel(instance.getModel()) const docClient = _getDocClient() - const primaryKeyName = instance.getModel().getPrimaryKeyName() + const primaryKeyName = instance + .getModel() + .getModelDefinition().primaryKeyName const data = await instance.toObj() const key = `${(data as any)[primaryKeyName]}` const keyObj = { [primaryKeyName]: key } @@ -145,15 +149,15 @@ const dynamoDatastoreProvider = ({ }) } - const deleteObj = >( - instance: ModelInstance + const deleteObj = ( + model: ModelType, + primaryKey: PrimaryKeyType ) => { return Promise.resolve().then(async () => { - const tableName = getTableNameForModel(instance.getModel()) + const tableName = getTableNameForModel(model) const docClient = _getDocClient() - const primaryKeyName = instance.getModel().getPrimaryKeyName() - const id = await instance.getPrimaryKey() - const keyObj = { [primaryKeyName]: `${id}` } + const primaryKeyName = model.getModelDefinition().primaryKeyName + const keyObj = { [primaryKeyName]: `${primaryKey}` } const command = new aws3.DeleteCommand({ TableName: tableName, Key: keyObj, @@ -162,8 +166,8 @@ const dynamoDatastoreProvider = ({ }) } - const bulkInsert = ( - model: Model, + const bulkInsert = ( + model: ModelType, instances: readonly ModelInstance[] ): Promise => { return Promise.resolve().then(async () => { @@ -209,4 +213,4 @@ const dynamoDatastoreProvider = ({ } } -export default dynamoDatastoreProvider +export { create } diff --git a/src/index.ts b/src/index.ts index affba1a..d022e5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -import datastoreProvider from './datastoreProvider' +import * as datastoreAdapter from './datastoreAdapter' -export { datastoreProvider } +export { datastoreAdapter } diff --git a/src/lib.ts b/src/lib.ts index c8548c8..4e5f862 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,7 +1,11 @@ -import { FunctionalModel, Model } from 'functional-models/interfaces' +import kebabCase from 'lodash/kebabCase' +import { DataDescription, ModelType, ToObjectResult } from 'functional-models' +import { merge } from 'lodash' -const getTableNameForModel = (model: Model) => { - return model.getName().toLowerCase().replace('_', '-').replace(' ', '-') +const getTableNameForModel = ( + model: ModelType +) => { + return kebabCase(model.getName()).toLowerCase() } const _recursiveSplitArray = ( @@ -34,4 +38,33 @@ const splitArrayIntoArraysOfMaxSize = ( return _recursiveSplitArray([], array, maxSize) } -export { getTableNameForModel, splitArrayIntoArraysOfMaxSize } +const fromDynamo = ( + obj: object +): ToObjectResult => { + return Object.entries(obj).reduce((acc, [key, obj]) => { + return merge(acc, { [key]: obj }) + }, {}) as ToObjectResult +} + +const buildScanQuery = (table: string, page?: string) => { + const startKey = page + ? { + ExclusiveStartKey: page, + FilterExpression: undefined, + ExpressionAttributeNames: undefined, + ExpressionAttributeValues: undefined, + } + : {} + + return { + ...startKey, + TableName: table, + } +} + +export { + getTableNameForModel, + splitArrayIntoArraysOfMaxSize, + fromDynamo, + buildScanQuery, +} diff --git a/src/queryBuilder.ts b/src/queryBuilder.ts deleted file mode 100644 index d690271..0000000 --- a/src/queryBuilder.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { randomUUID } from 'crypto' -import { merge, get, identity } from 'lodash' -import { - OrmQuery, - OrmQueryStatement, - PropertyStatement, -} from 'functional-models-orm/interfaces' -import { ORMType } from 'functional-models-orm/constants' - -type DynamoOrmStatement = OrmQueryStatement & { myUniqueId: string } -type DynamoPropertyStatement = PropertyStatement & { myUniqueId: string } - -const COMPARISONS = { - EQ: 'EQ', -} - -const DATA_TYPES = { - STRING: 'S', - NUMBER: 'N', - BOOLEAN: 'BOOL', - NULL: 'NULL', - ARRAY: 'L', - OBJECT: 'M', -} - -const ORM_TYPE_TO_DYNAMO_TYPE: { [key in ORMType]: string } = { - string: DATA_TYPES.STRING, - number: DATA_TYPES.NUMBER, - date: DATA_TYPES.STRING, - object: DATA_TYPES.OBJECT, - boolean: DATA_TYPES.BOOLEAN, -} - -const _idGenerator = (existingFunc: any): ((s: any) => string) => { - if (!existingFunc) { - return (_: string) => randomUUID().replace(/-/gu, '').replace(/_/gu, '') - } - return existingFunc -} - -const _isPropertyStatement = (obj: any): obj is DynamoPropertyStatement => { - return get(obj, 'type', '') === 'property' -} - -const queryBuilder = - ({ createUniqueId }: { createUniqueId?: (s: any) => string }) => - (tableName: string, queryData: OrmQuery) => { - const idGenerator = _idGenerator(createUniqueId) - const properties = queryData.properties - const page = queryData.page - - const flowObjs: DynamoOrmStatement[] = queryData.chain.map(x => ({ - ...x, - myUniqueId: idGenerator(x), - })) - const realProperties: DynamoPropertyStatement[] = flowObjs - .filter(_isPropertyStatement) - .map(x => x as DynamoPropertyStatement) - - const startKey = page - ? { - ExclusiveStartKey: page, - FilterExpression: undefined, - ExpressionAttributeNames: undefined, - ExpressionAttributeValues: undefined, - } - : {} - - if (Object.keys(properties).length < 1) { - return { - ...startKey, - TableName: tableName, - } - } - - const propKeyToKey: { [s: string]: string } = realProperties.reduce( - (acc, obj) => { - return merge(acc, { - [obj.myUniqueId]: `my${obj.myUniqueId - .replace('-', '') - .replace('_', '')}`, - }) - }, - {} - ) - - const propNametoExpressionAttribute: { [s: string]: string } = - realProperties.reduce((acc, obj) => { - const newKey = `#my${obj.myUniqueId.replace('-', '').replace('_', '')}` - return merge(acc, { [obj.myUniqueId]: newKey }) - }, {}) - - const expressionAttributeNames = realProperties.reduce((acc, obj) => { - return merge(acc, { - [propNametoExpressionAttribute[obj.myUniqueId]]: obj.name, - }) - }, {}) - - const _getEqualitySymbol = (flowObj: DynamoOrmStatement) => { - return get(flowObj, 'options.equalitySymbol') - } - - // I broke this out into its own function, because typescript was having a really hard time with it. - const _combineStatementAndGetPreviousOrm = ( - acc: string, - previous: DynamoOrmStatement | undefined, - flowObj: DynamoOrmStatement - ): [string, DynamoOrmStatement | undefined] => { - if (flowObj.type === 'property') { - const key = flowObj.myUniqueId - const expressionAttribute = propNametoExpressionAttribute[key] - const propKey = propKeyToKey[key] - if (previous && _isPropertyStatement(flowObj)) { - if (previous.type !== 'and' && previous.type !== 'or') { - return [ - acc + - ` AND ${expressionAttribute} ${_getEqualitySymbol(flowObj)} :${propKey}`, - flowObj, - ] - } - return [ - acc + - `${expressionAttribute} ${_getEqualitySymbol(flowObj)} :${propKey}`, - flowObj, - ] - } - return [ - acc + - `${expressionAttribute} ${_getEqualitySymbol(flowObj)} :${propKey}`, - flowObj, - ] - } else if (flowObj.type === 'and') { - if (previous && previous.type !== 'property') { - throw new Error( - `Cannot preclude and/or with anything except a property` - ) - } - return [acc + ' AND ', flowObj] - } else if (flowObj.type === 'or') { - if (previous && previous.type !== 'property') { - throw new Error( - `Cannot preclude and/or with anything except a property` - ) - } - return [acc + ' OR ', flowObj] - } - return [acc, flowObj] - } - - const _createFilterExpression = (): string => { - return flowObjs.reduce( - ( - [acc, previous]: [string, DynamoOrmStatement | undefined], - flowObj: DynamoOrmStatement - ) => { - return _combineStatementAndGetPreviousOrm(acc, previous, flowObj) - }, - ['', undefined] - )[0] - } - - const filterExpression = _createFilterExpression() - - const _getStringValue = (value: any) => { - return value === null || value === undefined ? '' : value - } - - const DATA_TYPE_TO_METHOD = { - [DATA_TYPES.STRING]: _getStringValue, - [DATA_TYPES.NUMBER]: identity, - [DATA_TYPES.BOOLEAN]: identity, - [DATA_TYPES.NULL]: identity, - [DATA_TYPES.ARRAY]: identity, - [DATA_TYPES.OBJECT]: identity, - } - - const expressionAttributeValues = realProperties.reduce((acc, obj) => { - const dataType = ORM_TYPE_TO_DYNAMO_TYPE[obj.valueType] - const valueMethod = DATA_TYPE_TO_METHOD[dataType] - const value = valueMethod(obj.value) - return merge(acc, { - [`:${propKeyToKey[obj.myUniqueId]}`]: value, - }) - }, {}) - - return { - ...startKey, - TableName: tableName, - FilterExpression: filterExpression, - ExpressionAttributeNames: expressionAttributeNames, - ExpressionAttributeValues: expressionAttributeValues, - } - } - -export default queryBuilder diff --git a/test/src/datastoreProvider.test.ts b/test/src/datastoreAdapter.test.ts similarity index 69% rename from test/src/datastoreProvider.test.ts rename to test/src/datastoreAdapter.test.ts index 070c9ac..e59de87 100644 --- a/test/src/datastoreProvider.test.ts +++ b/test/src/datastoreAdapter.test.ts @@ -1,11 +1,14 @@ import { assert } from 'chai' import sinon from 'sinon' -import proxyquire from 'proxyquire' -import { Model, FunctionalModel } from 'functional-models/interfaces' -import { OrmModel, OrmModelInstance } from 'functional-models-orm/interfaces' -import { ormQueryBuilder } from 'functional-models-orm/ormQuery' +import { + Model, + queryBuilder, + DataDescription, + OrmModel, + OrmModelInstance, +} from 'functional-models' import { createAws3MockClient } from '../commonMocks' -import createDatastoreProvider from '../../src/datastoreProvider' +import * as datastoreAdapter from '../../src/datastoreAdapter' const createTestModel1 = ({ id, name }: { id: string; name: string }) => { return { @@ -15,14 +18,15 @@ const createTestModel1 = ({ id, name }: { id: string; name: string }) => { }, toObj: () => Promise.resolve({ id, name }), getPrimaryKey: () => id, - getPrimaryKeyName: () => 'id', getModel: () => { return { getName: () => 'TestModel1', - getPrimaryKeyName: () => 'id', + getModelDefinition: () => ({ + primaryKeyName: 'id', + }), } as OrmModel }, - } as unknown as OrmModelInstance> + } as unknown as OrmModelInstance> } const createTestModel2 = ({ notId, name }: any) => @@ -35,9 +39,11 @@ const createTestModel2 = ({ notId, name }: any) => getPrimaryKey: () => notId, getModel: () => ({ getName: () => 'TestModel1', - getPrimaryKeyName: () => 'notId', + getModelDefinition: () => ({ + primaryKeyName: 'notId', + }), }), - }) as unknown as OrmModelInstance> + }) as unknown as OrmModelInstance> const _createDynamoStringResult = (key: string, value: string) => { return { @@ -57,87 +63,53 @@ const _createDynamoNullResult = (key: string) => { } } -describe('/src/datastoreProvider.ts', function () { +describe('/src/datastoreAdapter.ts', function () { this.timeout(20000) describe('#()', () => { it('should not throw an exception with basic arguments', () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } assert.doesNotThrow(() => { - const datastoreProvider = createDatastoreProvider({ + datastoreAdapter.create({ aws3, - dynamoOptions, }) }) }) it('should throw an exception if getTaqbleNameForModel is null', () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } assert.throws(() => { - const datastoreProvider = createDatastoreProvider({ + datastoreAdapter.create({ aws3, - dynamoOptions, + // @ts-ignore getTableNameForModel: null, }) }) }) it('should have a "search" function', () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) assert.isFunction(instance.search) }) it('should have a "retrieve" function', () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) assert.isFunction(instance.retrieve) }) it('should have a "save" function', () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) assert.isFunction(instance.save) }) it('should have a "delete" function', () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) assert.isFunction(instance.delete) }) describe('#search()', () => { - it('should pass createUniqueId into queryBuilder', async () => { - const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const myFunc = () => 'fake-id' - const instance = createDatastoreProvider({ - aws3, - dynamoOptions, - createUniqueId: myFunc, - }) - const obj = { id: 'my-id', name: 'my-name' } - const query = ormQueryBuilder().property('name', 'my-name').compile() - // @ts-ignore - aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ - Items: [], - LastEvaluatedKey: null, - }) - await instance.search(createTestModel1(obj).getModel(), query) - const actual = aws3.ScanCommand.sinon.getCall(0).args[0] - const expected = { - TableName: 'testmodel1', - FilterExpression: '#myfakeid = :myfakeid', - ExpressionAttributeNames: { '#myfakeid': 'name' }, - ExpressionAttributeValues: { ':myfakeid': 'my-name' }, - } - assert.deepInclude(actual, expected) - }) it('should call dynamo.scan once when LastEvaluatedKey is empty', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const obj = { id: 'my-id', name: 'my-name' } - const query = ormQueryBuilder().property('name', 'my-name').compile() + const query = queryBuilder().property('name', 'my-name').compile() // @ts-ignore aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ Items: [], @@ -149,10 +121,9 @@ describe('/src/datastoreProvider.ts', function () { }) it('should be able to process a string value result', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const obj = { id: 'my-id', name: 'my-name' } - const query = ormQueryBuilder().property('name', 'my-name').compile() + const query = queryBuilder().property('name', 'my-name').compile() // @ts-ignore aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ Items: [ @@ -175,10 +146,9 @@ describe('/src/datastoreProvider.ts', function () { }) it('should be able to process a null value results', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const obj = { id: 'my-id', name: 'my-name' } - const query = ormQueryBuilder().property('name', null).compile() + const query = queryBuilder().property('name', null).compile() // @ts-ignore aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ Items: [ @@ -201,10 +171,9 @@ describe('/src/datastoreProvider.ts', function () { }) it('should be able to process an array of strings', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const obj = { id: 'my-id', name: 'my-name' } - const query = ormQueryBuilder().property('names', 'my-name').compile() + const query = queryBuilder().property('id', 'my-id').compile() // @ts-ignore aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ Items: [ @@ -227,10 +196,12 @@ describe('/src/datastoreProvider.ts', function () { }) it('should return only 1 object if query has "take:1" even if there are two results from dynamo', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const obj = { id: 'my-id', name: 'my-name' } - const query = ormQueryBuilder().property('name', null).take(1).compile() + const query = queryBuilder() + .property('name', 'name', { startsWith: true }) + .take(1) + .compile() // @ts-ignore aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ Items: [ @@ -257,10 +228,9 @@ describe('/src/datastoreProvider.ts', function () { }) it('should call dynamo.scan twice when LastEvaluatedKey is empty the second time', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const obj = { id: 'my-id', name: 'my-name' } - const query = ormQueryBuilder().property('name', 'my-name').compile() + const query = queryBuilder().property('name', 'my-name').compile() // @ts-ignore aws3.DynamoDBDocumentClient.sendSinon.onFirstCall().resolves({ Items: [], @@ -277,11 +247,10 @@ describe('/src/datastoreProvider.ts', function () { }) it('should call dynamo.scan twice when LastEvaluatedKey has a value the second time but take:2 and 3 items are returned', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) - const obj = { id: 'my-id', name: 'my-name' } - const query = ormQueryBuilder() - .property('name', 'my-name') + const instance = datastoreAdapter.create({ aws3 }) + const obj = { id: 'name', name: 'returned' } + const query = queryBuilder() + .property('name', 'returned', { startsWith: true }) .take(2) .compile() // @ts-ignore @@ -292,9 +261,9 @@ describe('/src/datastoreProvider.ts', function () { // @ts-ignore aws3.DynamoDBDocumentClient.sendSinon.onSecondCall().resolves({ Items: [ - { something: 'returned' }, - { something2: 'returned2' }, - { something3: 'returned3' }, + { name: 'returned' }, + { name: 'returned2' }, + { name: 'returned3' }, ], LastEvaluatedKey: 'another-value', }) @@ -308,10 +277,8 @@ describe('/src/datastoreProvider.ts', function () { describe('#retrieve()', () => { it('should create a document client with marshallOptions.removeUndefinedValues=true', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ + const instance = datastoreAdapter.create({ aws3, - dynamoOptions, getTableNameForModel: () => 'FakeTable', }) const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) @@ -326,10 +293,8 @@ describe('/src/datastoreProvider.ts', function () { }) it('should pass the correct table name into GetCommand', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ + const instance = datastoreAdapter.create({ aws3, - dynamoOptions, getTableNameForModel: () => 'FakeTable', }) const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) @@ -340,46 +305,42 @@ describe('/src/datastoreProvider.ts', function () { }) it('should pass the correct params to GetCommand', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) await instance.retrieve(modelInstance.getModel(), 'my-id') const actual = aws3.GetCommand.sinon.getCall(0).args[0] - const expected = { Key: { id: 'my-id' }, TableName: 'testmodel1' } + const expected = { Key: { id: 'my-id' }, TableName: 'test-model-1' } assert.deepEqual(actual, expected) }) }) describe('#delete()', () => { it('should pass the correct params to DeleteCommand', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) - await instance.delete(modelInstance) + await instance.delete(modelInstance.getModel(), 'my-id') const actual = aws3.DeleteCommand.sinon.getCall(0).args[0] - const expected = { Key: { id: 'my-id' }, TableName: 'testmodel1' } + const expected = { Key: { id: 'my-id' }, TableName: 'test-model-1' } assert.deepEqual(actual, expected) }) }) describe('#save()', () => { it('should pass results of modelInstance.functions.toObj() to PutCommand', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const modelInstance = createTestModel1({ id: 'my-id', name: 'my-name' }) await instance.save(modelInstance) const actual = aws3.PutCommand.sinon.getCall(0).args[0] const expected = { Key: { id: 'my-id' }, Item: { id: 'my-id', name: 'my-name' }, - TableName: 'testmodel1', + TableName: 'test-model-1', } assert.deepEqual(actual, expected) }) it('should pass the correct primary key when changed by the model to PutCommand', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const modelInstance = createTestModel2({ notId: 'my-id', name: 'my-name', @@ -389,7 +350,7 @@ describe('/src/datastoreProvider.ts', function () { const expected = { Key: { notId: 'my-id' }, Item: { notId: 'my-id', name: 'my-name' }, - TableName: 'testmodel1', + TableName: 'test-model-1', } assert.deepEqual(actual, expected) }) @@ -397,8 +358,7 @@ describe('/src/datastoreProvider.ts', function () { describe('#bulkInsert()', () => { it('should format the objects correctly when passed to BatchWriteCommand', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) + const instance = datastoreAdapter.create({ aws3 }) const models = [ createTestModel1({ id: '1', name: 'my-name' }), createTestModel1({ id: '2', name: 'my-name' }), @@ -406,8 +366,8 @@ describe('/src/datastoreProvider.ts', function () { createTestModel1({ id: '4', name: 'my-name' }), createTestModel1({ id: '5', name: 'my-name' }), ] - // @ts-ignore await instance.bulkInsert( + // @ts-ignore { getName: () => 'TestName', }, @@ -416,7 +376,7 @@ describe('/src/datastoreProvider.ts', function () { const actual = aws3.BatchWriteCommand.sinon.getCall(0).args[0] const expected = { RequestItems: { - testname: [ + 'test-name': [ { PutRequest: { Item: { id: '1', name: 'my-name' } } }, { PutRequest: { Item: { id: '2', name: 'my-name' } } }, { PutRequest: { Item: { id: '3', name: 'my-name' } } }, @@ -429,11 +389,10 @@ describe('/src/datastoreProvider.ts', function () { }) it('should not call BulkWriteCommand if there are no models', async () => { const aws3 = createAws3MockClient() - const dynamoOptions = { region: 'fake-region' } - const instance = createDatastoreProvider({ aws3, dynamoOptions }) - const models = [] - // @ts-ignore + const instance = datastoreAdapter.create({ aws3 }) + const models: any[] = [] await instance.bulkInsert( + // @ts-ignore { getName: () => 'TestName', }, diff --git a/test/src/lib.test.ts b/test/src/lib.test.ts index b40df2c..7205b0b 100644 --- a/test/src/lib.test.ts +++ b/test/src/lib.test.ts @@ -1,4 +1,4 @@ -import { Model } from 'functional-models/interfaces' +import { ModelType } from 'functional-models' import { assert } from 'chai' import sinon from 'sinon' import { @@ -6,16 +6,17 @@ import { splitArrayIntoArraysOfMaxSize, } from '../../src/lib' -const buildModel = (name: string): Model => { +const buildModel = (name: string): ModelType => { return { getName: () => name, - } as Model + } as ModelType } describe('/src/lib.ts', () => { describe('#splitArrayIntoArraysOfMaxSize()', () => { it('should throw an exception if the input is not an array', () => { assert.throws(() => { + // @ts-ignore splitArrayIntoArraysOfMaxSize('input', 2) }) }) @@ -54,9 +55,9 @@ describe('/src/lib.ts', () => { const expected = 'my-table' assert.deepEqual(actual, expected) }) - it('should return "mytable" for "MyTable"', () => { + it('should return "my-table" for "MyTable"', () => { const actual = getTableNameForModel(buildModel('MyTable')) - const expected = 'mytable' + const expected = 'my-table' assert.deepEqual(actual, expected) }) }) diff --git a/test/src/queryBuilder.test.ts b/test/src/queryBuilder.test.ts deleted file mode 100644 index f1b2f43..0000000 --- a/test/src/queryBuilder.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { get } from 'lodash' -import { assert } from 'chai' -import sinon from 'sinon' -import { ormQueryBuilder } from 'functional-models-orm/ormQuery' -import { EQUALITY_SYMBOLS, ORMType } from 'functional-models-orm/constants' -import queryBuilder from '../../src/queryBuilder' - -const _nameId = () => ({ createUniqueId: (obj: any) => obj.name }) - -describe('/src/queryBuilder.ts', () => { - describe('#()', () => { - it('should set TableName to what is passed in', () => { - const query = ormQueryBuilder().property('name', 'value').compile() - const actual = get( - queryBuilder(_nameId())('my-table', query), - 'TableName' - ) - const expected = 'my-table' - assert.deepEqual(actual, expected) - }) - /* - situations - null values - page and no page - _ in name - multiple properties and one property - */ - it('should TableName to what is passed in', () => { - const query = ormQueryBuilder().property('name', 'value').compile() - const actual = get( - queryBuilder(_nameId())('my-table', query), - 'TableName' - ) - const expected = 'my-table' - assert.deepEqual(actual, expected) - }) - it('should produce an expected FilterExpression for a single property', () => { - const query = ormQueryBuilder().property('name', 'value').compile() - const actual = queryBuilder(_nameId())('my-table', query).FilterExpression - const expected = '#myname = :myname' - assert.deepEqual(actual, expected) - }) - it('should produce an expected FilterExpression for a single property with a > symbol', () => { - const query = ormQueryBuilder() - .property('name', 5, { - type: ORMType.number, - equalitySymbol: EQUALITY_SYMBOLS.GT, - }) - .compile() - const actual = queryBuilder(_nameId())('my-table', query).FilterExpression - const expected = '#myname > :myname' - assert.deepEqual(actual, expected) - }) - it('should produce an expected FilterExpression for a single property when the name has a dash', () => { - const query = ormQueryBuilder().property('name-name', 'value').compile() - const actual = queryBuilder(_nameId())('my-table', query).FilterExpression - const expected = '#mynamename = :mynamename' - assert.deepEqual(actual, expected) - }) - it('should produce an expected FilterExpression for two properties', () => { - const query = ormQueryBuilder() - .property('name', 'value') - .property('secondname', 'value') - .compile() - const actual = queryBuilder(_nameId())('my-table', query).FilterExpression - const expected = '#myname = :myname AND #mysecondname = :mysecondname' - assert.deepEqual(actual, expected) - }) - it('should produce an expected FilterExpression for two properties with an AND', () => { - const query = ormQueryBuilder() - .property('name', 'value') - .and() - .property('secondname', 'value') - .compile() - const actual = queryBuilder(_nameId())('my-table', query).FilterExpression - const expected = '#myname = :myname AND #mysecondname = :mysecondname' - assert.deepEqual(actual, expected) - }) - it('should produce an expected FilterExpression for two properties with an OR', () => { - const query = ormQueryBuilder() - .property('name', 'value') - .or() - .property('secondname', 'value') - .compile() - const actual = queryBuilder(_nameId())('my-table', query).FilterExpression - const expected = '#myname = :myname OR #mysecondname = :mysecondname' - assert.deepEqual(actual, expected) - }) - it('should throw an exception if two OR are called', () => { - const query = ormQueryBuilder() - .property('name', 'value') - .or() - .or() - .property('secondname', 'value') - .compile() - try { - const actual = queryBuilder(_nameId())( - 'my-table', - query - ).FilterExpression - throw new Error(`No exception thrown`) - } catch {} - }) - it('should throw an exception if two AND are called', () => { - const query = ormQueryBuilder() - .property('name', 'value') - .and() - .and() - .property('secondname', 'value') - .compile() - try { - const actual = queryBuilder(_nameId())( - 'my-table', - query - ).FilterExpression - throw new Error(`No exception thrown`) - } catch {} - }) - it('should produce an expected ExpressionAttributeNames for a single property when the name has a dash', () => { - const query = ormQueryBuilder().property('name-name', 'value').compile() - const actual = queryBuilder(_nameId())( - 'my-table', - query - ).ExpressionAttributeNames - const expected = { - '#mynamename': 'name-name', - } - assert.deepEqual(actual, expected) - }) - it('should produce an expected ExpressionAttributeNames for two properties', () => { - const query = ormQueryBuilder() - .property('name', 'value') - .property('description', 'the-description') - .compile() - const actual = queryBuilder(_nameId())( - 'my-table', - query - ).ExpressionAttributeNames - const expected = { - '#myname': 'name', - '#mydescription': 'description', - } - assert.deepEqual(actual, expected) - }) - it('should produce an expected ExpressionAttributeValues for one property', () => { - const query = ormQueryBuilder().property('name', 'value').compile() - const actual = queryBuilder(_nameId())( - 'my-table', - query - ).ExpressionAttributeValues - const expected = { - ':myname': 'value', - } - assert.deepEqual(actual, expected) - }) - it('should produce an expected ExpressionAttributeValues for one property that has a null value', () => { - const query = ormQueryBuilder().property('name', null).compile() - const actual = queryBuilder(_nameId())( - 'my-table', - query - ).ExpressionAttributeValues - const expected = { - ':myname': '', - } - assert.deepEqual(actual, expected) - }) - it('should produce an expected ExpressionAttributeValues for two properties with one being null', () => { - const query = ormQueryBuilder() - .property('name', 'value') - .property('other', null) - .compile() - const actual = queryBuilder(_nameId())( - 'my-table', - query - ).ExpressionAttributeValues - const expected = { - ':myname': 'value', - ':myother': '', - } - assert.deepEqual(actual, expected) - }) - it('should produce an expected search when no properties are provided', () => { - const query = ormQueryBuilder().compile() - const actual = queryBuilder(_nameId())('my-table', query) - const expected = { - TableName: 'my-table', - } - assert.deepEqual(actual, expected) - }) - it('should produce an expected search when no properties are provided but there is a page', () => { - const query = ormQueryBuilder().pagination({ custom: 'page' }).compile() - const actual = queryBuilder(_nameId())('my-table', query) - const expected = { - TableName: 'my-table', - ExclusiveStartKey: { custom: 'page' }, - ExpressionAttributeNames: undefined, - ExpressionAttributeValues: undefined, - FilterExpression: undefined, - } - assert.deepEqual(actual as object, expected as object) - }) - }) -}) diff --git a/tsconfig.build.json b/tsconfig.build.json deleted file mode 100644 index 7827a17..0000000 --- a/tsconfig.build.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Projects */ - // "incremental": true, /* Enable incremental compilation */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "lib": ["es2021", "dom", "es2015"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - "rootDir": "./src" /* Specify the root folder within your source files. */, - // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - //"rootDirs": ["./src","], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "resolveJsonModule": true, /* Enable importing .json files */ - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - - /* Emit */ - "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "exclude": [ - "node_modules", - "dist", - "stepDefinitions", - "test" - ] -} diff --git a/tsconfig.cucumber.json b/tsconfig.cucumber.json new file mode 100644 index 0000000..733bb33 --- /dev/null +++ b/tsconfig.cucumber.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["es2023", "es2021", "dom", "es2017"], + "module": "node16", + "moduleResolution": "node16", + "resolveJsonModule": true, + "noImplicitAny": false, + "suppressImplicitAnyIndexErrors": true, + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": false, + "skipLibCheck": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "exclude": ["node_modules", "dist", "features", "coverage", "test"] +} diff --git a/tsconfig.json b/tsconfig.json index c59ee45..b2fb974 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,18 @@ { "compilerOptions": { "target": "es6", - "lib": ["es2021", "dom", "es2015"], + "lib": ["es2023", "es2021", "dom", "es2017"], + "module": "node16", + "moduleResolution": "node16", + "resolveJsonModule": true, "declaration": true, - "module": "commonjs", "sourceMap": true, - "moduleResolution": "Node", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "rootDir": "./src", + "outDir": "./dist" }, - "exclude": [ - "node_modules", - "dist" - ] + "exclude": ["node_modules", "dist", "features", "coverage", "test"] } From b150c6f3fab5a71518e8f696274c008496d72ef6 Mon Sep 17 00:00:00 2001 From: Mike Cornwell Date: Thu, 30 Jan 2025 12:12:26 -0500 Subject: [PATCH 2/4] ci(docker): fix docker issues --- .github/workflows/feature.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index b9d5d65..dfb92cd 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -23,7 +23,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Create dynamo db for tests - run: docker-compose -f ./dynamodb-feature-tests.yml up -d + run: docker compose -f ./dynamodb-feature-tests.yml up -d - name: Install dependencies run: npm install From 2bdcbedefa83e33898de84c0170d300a21f6d2f5 Mon Sep 17 00:00:00 2001 From: Mike Cornwell Date: Thu, 30 Jan 2025 12:13:39 -0500 Subject: [PATCH 3/4] ci(builds): fix ci builds --- .github/workflows/feature.yml | 2 +- .github/workflows/ut.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index dfb92cd..429c869 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -23,7 +23,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Create dynamo db for tests - run: docker compose -f ./dynamodb-feature-tests.yml up -d + run: docker compose -f ./dynamo-feature-tests.yml up -d - name: Install dependencies run: npm install diff --git a/.github/workflows/ut.yml b/.github/workflows/ut.yml index 2817c07..9db5984 100644 --- a/.github/workflows/ut.yml +++ b/.github/workflows/ut.yml @@ -25,7 +25,7 @@ jobs: run: npm install - name: Run Unit Tests run: npm test - - run: npm run coverage + - run: npm run test:coverage - name: Coveralls uses: coverallsapp/github-action@master From 537396a36e13a5e4e473f457f3aea58f851df8d2 Mon Sep 17 00:00:00 2001 From: Mike Cornwell Date: Thu, 30 Jan 2025 12:15:20 -0500 Subject: [PATCH 4/4] chore(version): fix version to 3.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a955e02..2fd161b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "functional-models-orm-dynamo", - "version": "3.0", + "version": "3.0.0", "description": "An implmentation of functional-models for dynamodb.", "main": "index.js", "types": "index.d.ts",