diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml index 9c6a9be..c846087 100644 --- a/.github/workflows/feature.yml +++ b/.github/workflows/feature.yml @@ -7,12 +7,13 @@ on: branches: [master] jobs: - build: + featureTests: + name: FeatureTests runs-on: ubuntu-latest strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v2 @@ -20,7 +21,15 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} + + - name: Create database for tests + run: docker compose -f ./elastic-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 e22e2dc..9db5984 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: [14.x, 16.x, 18.x] + node-version: [18.x, 20.x, 22.x] steps: - uses: actions/checkout@v2 @@ -24,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 diff --git a/README.md b/README.md index 4ad34df..5e4cd3b 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,41 @@ ![Unit Tests](https://github.com/monolithst/functional-models-orm-elastic/actions/workflows/ut.yml/badge.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/monolithst/functional-models-orm-elastic/badge.svg?branch=master)](https://coveralls.io/github/monolithst/functional-models-orm-elastic?branch=master) -## Run Feature Tests +# How To Install -To run the feature tests, you need to set up an actual Elasticsearch cluster within AWS and then call cucumber like the following: +`npm install functional-models-orm-elastic` -`npm run feature-tests -- --world-parameters '{"elasticUsername":"USERNAME", "elasticPassword":"PASSWORD", "cloudId": "CLOUD_ID"}'` +# How To Use -IMPORTANT WORD OF CAUTION: I would not attempt to use this database for anything other than this feature tests, as the indexes are completely deleted without remorse. +```typescript +import { createOrm } from 'functional-models' +import { datastoreAdapter as elasticDatastore } from 'functional-models-orm-elastic' +import { Client } from '@opensearch-project/opensearch' + +// Create your client. +const client = new Client({ + node: 'http://localhost:9200', +}) + +// Create datastoreAdapter +const datastoreAdapter = elasticDatastore.create({ + client, +}) + +// Create an orm for use with your models. +const orm = createOrm({ datastoreAdapter }) +``` + +### Notes on Running with AWS OpenSearch + +One way to use the client is with a username/password and the elastic url. + +Here is an example: + +```javascript +const url = `https://${elasticUsername}:${elasticPassword}@${elasticUrl}` + +const client = new Client({ + node: url, +}) +``` diff --git a/elastic-feature-tests.yml b/elastic-feature-tests.yml new file mode 100644 index 0000000..e5ad3e4 --- /dev/null +++ b/elastic-feature-tests.yml @@ -0,0 +1,20 @@ +version: '3.1' +services: + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.17.1 + container_name: functional-models-orm-elastic-feature-tests + environment: + - cluster.name=docker-cluster + - bootstrap.memory_lock=true + - 'ES_JAVA_OPTS=-Xms512m -Xmx512m' + - xpack.security.enabled=false + - xpack.security.enrollment.enabled=false + - discovery.type=single-node + ulimits: + memlock: + soft: -1 + hard: -1 + #volumes: + # - esdata1:/usr/share/elasticsearch/data + ports: + - 5121:9200 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/datastore.feature b/features/datastore.feature index 3fe06aa..cad1e73 100644 --- a/features/datastore.feature +++ b/features/datastore.feature @@ -2,7 +2,7 @@ Feature: Datastore Provider Scenario: Saving, Retrieving and Deleting Given a configured elastic client is created - And a configured datastore provider is created + And a configured datastore adapter is created And test models are created And the indices for models are cleared When a MODEL_1 with DATA_1 is created @@ -15,7 +15,7 @@ Feature: Datastore Provider Scenario: Bulk Inserting Given a configured elastic client is created - And a configured datastore provider is created + And a configured datastore adapter is created And test models are created And the indices for models are cleared When many instances of MODEL_1 are created using DATA_2 @@ -27,9 +27,28 @@ Feature: Datastore Provider When MODEL_1 retrieve is called with the id from DATA_2c Then the object data matches DATA_2c + Scenario: Sorting, Paging, and Takes + Given a configured elastic client is created + And a configured datastore adapter is created + And test models are created + And the indices for models are cleared + When many instances of MODEL_1 are created using DATA_2 + And bulk insert is called on MODEL_1 with the model instances + When a search is called on MODEL_1 with SORTING_1_SEARCH + Then the search results matches SORTING_1_SEARCH_RESULTS + When a search is called on MODEL_1 with SORTING_2_SEARCH + Then the search results matches SORTING_2_SEARCH_RESULTS + When a search is called on MODEL_1 with EMPTY_SEARCH + Then there are 11 instances returned + When a search is called on MODEL_1 with TAKE_SEARCH + Then there are 10 instances returned + Then there is a page matching PAGE_1 + When a search is called on MODEL_1 with EMPTY_SEARCH_WITH_PAGE + Then there are 1 instances returned + Scenario: Search Given a configured elastic client is created - And a configured datastore provider is created + And a configured datastore adapter is created And test models are created And the indices for models are cleared When many instances of MODEL_1 are created using DATA_2 diff --git a/features/stepDefinitions/steps.ts b/features/step_definitions/steps.ts similarity index 66% rename from features/stepDefinitions/steps.ts rename to features/step_definitions/steps.ts index 1eff160..da644aa 100644 --- a/features/stepDefinitions/steps.ts +++ b/features/step_definitions/steps.ts @@ -1,26 +1,29 @@ -import { Given, When, Then } from '@cucumber/cucumber' +import { Given, Then, When } from '@cucumber/cucumber' import { assert } from 'chai' import { - TextProperty, - NumberProperty, - DateProperty, BooleanProperty, + createOrm, + DatastoreValueType, + DateProperty, + EqualitySymbol, + NumberProperty, + queryBuilder, + SortOrder, + TextProperty, } from 'functional-models' -import { - orm, - ormQuery, - interfaces as ormInterfaces, -} from 'functional-models-orm' -import { create as createDatastoreProvider } from '../../src/datastoreProvider' +import * as datastoreAdapter from '../../src/datastoreAdapter' import { Client } from '@opensearch-project/opensearch' -const createModels = (datastoreProvider: any) => { - const ormInstance = orm({ - datastoreProvider, +const createModels = (datastoreAdapter: any) => { + const ormInstance = createOrm({ + datastoreAdapter: datastoreAdapter, }) return { - MODEL_1: ormInstance.BaseModel('MODEL_1', { + MODEL_1: ormInstance.Model({ + pluralName: 'Model1', + namespace: 'functional-models-orm-elastic', properties: { + id: TextProperty(), name: TextProperty(), aNumber: NumberProperty(), aDate: DateProperty(), @@ -31,6 +34,9 @@ const createModels = (datastoreProvider: any) => { } const DATA: any = { + PAGE_1: () => ({ + from: 10, + }), DATA_1: () => ({ id: 'c11a4cfa-6c44-40f3-bf37-7369d9d7c929', name: 'test-me', @@ -92,6 +98,14 @@ const DATA: any = { name: 'something not big', aDate: '2023-06-01T00:00:01.000Z', }, + { + id: '10', + name: 'at edge', + }, + { + id: '11', + name: 'not part of normal section', + }, ], DATA_2a: () => ({ id: '1', @@ -115,102 +129,163 @@ const DATA: any = { aBool: false, }), TEXT_MATCH_SEARCH: () => - ormQuery.ormQueryBuilder().property('name', 'test-me-2').compile(), + queryBuilder().property('name', 'test-me-2').compile(), CASE_TEXT_MATCH_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'UPPER_CASE', { caseSensitive: true }) .compile(), BAD_CASE_TEXT_MATCH_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'upper_case', { caseSensitive: true }) .compile(), NUMBER_RANGE_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('aNumber', 2, { - type: ormInterfaces.ORMType.number, - equalitySymbol: ormInterfaces.EQUALITY_SYMBOLS.GTE, + type: DatastoreValueType.number, + equalitySymbol: EqualitySymbol.gte, }) + .and() .property('aNumber', 4, { - type: ormInterfaces.ORMType.number, - equalitySymbol: ormInterfaces.EQUALITY_SYMBOLS.LT, + type: DatastoreValueType.number, + equalitySymbol: EqualitySymbol.lt, }) .compile(), TEXT_STARTS_WITH_SEARCH: () => - ormQuery - .ormQueryBuilder() - .property('name', 'test-me', { startsWith: true }) - .compile(), + queryBuilder().property('name', 'test-me', { startsWith: true }).compile(), TEXT_ENDS_WITH_SEARCH: () => - ormQuery - .ormQueryBuilder() - .property('name', 'me-3', { endsWith: true }) - .compile(), + queryBuilder().property('name', 'me-3', { endsWith: true }).compile(), CASE_TEXT_ENDS_WITH_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'BIG', { endsWith: true, caseSensitive: true }) .compile(), FREE_FORM_TEXT_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'is', { startsWith: true, endsWith: true }) .compile(), BOOLEAN_SEARCH: () => - ormQuery - .ormQueryBuilder() - .property('aBool', false, { type: ormInterfaces.ORMType.boolean }) + queryBuilder() + .property('aBool', false, { + type: DatastoreValueType.boolean, + }) .compile(), TWO_TEXT_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'test-me-1') + .and() .property('name', 'test-me-2') .compile(), TWO_PROPERTY_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'test-me-1') - .property('aNumber', 1, { type: ormInterfaces.ORMType.number }) + .and() + .property('aNumber', 1, { type: DatastoreValueType.number }) .compile(), SIMPLE_OR_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'test-me-1') .or() .property('name', 'test-me-2') .compile(), SIMPLE_AND_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'test-me', { startsWith: true }) .and() - .property('aBool', false, { type: ormInterfaces.ORMType.boolean }) + .property('aBool', false, { type: DatastoreValueType.boolean }) .compile(), DATE_RANGE_SEARCH: () => - ormQuery - .ormQueryBuilder() - //@ts-ignore + queryBuilder() .datesBefore('aDate', new Date('2023-05-01T00:00:01.000Z'), {}) - //@ts-ignore + .and() .datesAfter('aDate', new Date('2023-03-01T00:00:01.000Z'), {}) .compile(), + SORTING_1_SEARCH: () => + queryBuilder() + .property('name', 'test-me', { startsWith: true }) + .sort('name', SortOrder.dsc) + .compile(), + SORTING_2_SEARCH: () => { + return queryBuilder() + .property('name', 'test-me', { startsWith: true }) + .sort('name', SortOrder.dsc) + .take(2) + .compile() + }, + EMPTY_SEARCH: () => { + return queryBuilder().compile() + }, + TAKE_SEARCH: () => { + return queryBuilder().take(10).compile() + }, + EMPTY_SEARCH_WITH_PAGE: () => + queryBuilder().pagination({ from: 10 }).compile(), COMPLEX_BOOLEAN_SEARCH: () => - ormQuery - .ormQueryBuilder() + queryBuilder() .property('name', 'test-me', { startsWith: true }) .and() - .property('aNumber', 1, { type: ormInterfaces.ORMType.number }) - .or() - .property('aNumber', 2, { type: ormInterfaces.ORMType.number }) - .or() - .property('aNumber', 3, { type: ormInterfaces.ORMType.number }) - .and() - .property('aBool', true, { type: ormInterfaces.ORMType.boolean }) + .complex(b => + b + .property('aNumber', 1, { type: DatastoreValueType.number }) + .or() + .complex(c => + c + .property('aNumber', 2, { type: DatastoreValueType.number }) + .or() + .complex(d => + d + .property('aNumber', 3, { type: DatastoreValueType.number }) + .and() + .property('aBool', true, { type: DatastoreValueType.boolean }) + ) + ) + ) .compile(), EMPTY_RESULT: () => [], + SORTING_2_SEARCH_RESULTS: () => [ + { + id: '4', + name: 'test-me-4', + aNumber: 4, + aDate: '2023-04-01T00:00:01.000Z', + aBool: false, + }, + { + id: '3', + name: 'test-me-3', + aNumber: 3, + aDate: '2023-03-01T00:00:01.000Z', + aBool: false, + }, + ], + SORTING_1_SEARCH_RESULTS: () => [ + { + id: '4', + name: 'test-me-4', + aNumber: 4, + aDate: '2023-04-01T00:00:01.000Z', + aBool: false, + }, + { + id: '3', + name: 'test-me-3', + aNumber: 3, + aDate: '2023-03-01T00:00:01.000Z', + aBool: false, + }, + { + id: '2', + name: 'test-me-2', + aNumber: 2, + aDate: '2023-02-01T00:00:01.000Z', + aBool: true, + }, + { + id: '1', + name: 'test-me-1', + aNumber: 1, + aDate: '2023-01-01T00:00:01.000Z', + aBool: true, + }, + ], SEARCH_RESULT_13: () => [ { id: '1', @@ -408,56 +483,50 @@ const DATA: any = { } Given('a configured elastic client is created', function () { - if (!this.parameters.elasticUrl) { - throw new Error(`Must include elasticUrl in the world parameters.`) - } - if (!this.parameters.elasticUsername) { - throw new Error(`Must include elasticUsername in the world parameters.`) - } - if (!this.parameters.elasticPassword) { - throw new Error(`Must include elasticPassword in the world parameters.`) - } - - const url = `https://${this.parameters.elasticUsername}:${this.parameters.elasticPassword}@${this.parameters.elasticUrl}` + //const url = `https://${this.parameters.elasticUsername}:${this.parameters.elasticPassword}@${this.parameters.elasticUrl}` + const url = 'http://localhost:5121' this.client = new Client({ node: url, }) }) -Given('a configured datastore provider is created', function () { - this.datastoreProvider = createDatastoreProvider({ client: this.client }) +Given('a configured datastore adapter is created', function () { + this.datastoreAdapter = datastoreAdapter.create({ client: this.client }) }) Given('test models are created', function () { - this.models = createModels(this.datastoreProvider) + this.models = createModels(this.datastoreAdapter) }) Given( 'the indices for models are cleared', { timeout: 60 * 1000 }, async function () { - await Object.keys(this.models).reduce(async (accP, index) => { - index = index.toLowerCase() - const acc = await accP - if (await this.client.indices.exists({ index })) { - await this.client.indices.delete({ index }) - } - await this.client.indices.create({ - index, - body: { - mappings: { - properties: { - id: { type: 'keyword' }, - name: { type: 'keyword' }, - aNumber: { type: 'double' }, - aBool: { type: 'boolean' }, - aDate: { type: 'date' }, + await Object.values(this.models) + // @ts-ignore + .map(m => m.getName()) + .reduce(async (accP, index) => { + index = index.toLowerCase() + const acc = await accP + if ((await this.client.indices.exists({ index })).statusCode !== 404) { + await this.client.indices.delete({ index }) + } + await this.client.indices.create({ + index, + body: { + mappings: { + properties: { + id: { type: 'keyword' }, + name: { type: 'keyword' }, + aNumber: { type: 'float' }, + aBool: { type: 'boolean' }, + aDate: { type: 'date' }, + }, }, }, - }, - }) - return - }, Promise.resolve()) + }) + return + }, Promise.resolve()) } ) @@ -537,3 +606,15 @@ Then('the search results matches {word}', async function (dataKey) { ).sort((x: any, y: any) => y.id - x.id) assert.deepEqual(actual, expected) }) + +Then('there are {int} instances returned', async function (count) { + const actual = this.results.instances.length + const expected = count + assert.deepEqual(actual, expected) +}) + +Then('there is a page matching {word}', function (key: string) { + const expected = DATA[key]() + const actual = this.results.page + assert.deepEqual(actual, expected) +}) diff --git a/package.json b/package.json index 64f706e..23af1b2 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,35 @@ { "name": "functional-models-orm-elastic", - "version": "1.0.5", - "description": "A functional-models-orm datastore provider for Opensearch.", + "version": "3.0.0", + "description": "A functional-models datastore adapter for Opensearch/Elastic.", "main": "index.js", "types": "index.d.ts", "scripts": { - "build:watch": "tsc -w -p ./tsconfig.build.json", + "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 .", + "prettier:check": "prettier -c .", "test": "mocha -r ts-node/register test/**/*.test.ts", "test:coverage": "nyc npm run test", - "feature-tests": "node ./node_modules/.bin/cucumber-js --require-module ts-node/register --require ./features/stepDefinitions/*.ts", - "coverage": "nyc --all --reporter=lcov npm test", - "build": "tsc -p ./tsconfig.build.json && cp package.json ./dist && cp README.md ./dist", - "prettier": "prettier --write .", - "eslint": "eslint .", - "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" }, "repository": { "type": "git", "url": "git+https://github.com/monolithst/functional-models-orm-opensearch.git" }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, "nyc": { + "branches": 1, + "lines": 1, + "functions": 1, + "statements": 1, "extends": "@istanbuljs/nyc-config-typescript", "check-coverage": true, "all": true, @@ -54,35 +64,40 @@ "registry": "https://registry.npmjs.org" }, "devDependencies": { - "@cucumber/cucumber": "^11.0.0", - "@istanbuljs/nyc-config-typescript": "^1.0.1", - "@types/chai": "^4.3.0", - "@types/lodash": "^4.14.177", - "@types/mocha": "^9.0.0", - "@types/node": "^16.11.7", - "@types/sinon": "^10.0.6", - "@typescript-eslint/eslint-plugin": "^6.3.0", - "@typescript-eslint/parser": "^6.3.0", - "babel-eslint": "^10.1.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.16", + "@types/chai-as-promised": "^7.1.8", + "@types/lodash": "^4.17.1", + "@types/mocha": "^10.0.6", + "@types/sinon": "^17.0.3", + "@typescript-eslint/eslint-plugin": "^8.20.0", + "@typescript-eslint/parser": "^8.20.0", "chai": "^4.3.0", - "eslint": "^8.46.0", - "eslint-config-prettier": "^9.0.0", - "eslint-import-resolver-typescript": "^3.6.0", - "eslint-plugin-functional": "^6.0.0", - "eslint-plugin-import": "^2.28.0", - "mocha": "^10.7.3", + "chai-as-promised": "^7.1.2", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^9.1.0", + "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.3.3", + "prettier-plugin-organize-imports": "^3.2.4", "proxyquire": "^2.1.3", - "sinon": "^18.0.1", + "sinon": "^11.1.2", "source-map-support": "^0.5.21", - "ts-node": "^10.4.0", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", "typescript": "^5.7.2" }, "dependencies": { - "@opensearch-project/opensearch": "^2.3.1", - "functional-models": "^2.1.14", - "functional-models-orm": "^2.1.12", + "@opensearch-project/opensearch": "^2.13.0", + "functional-models": "^3.0.12", "lodash": "^4.17.21" } } diff --git a/src/datastoreAdapter.ts b/src/datastoreAdapter.ts new file mode 100644 index 0000000..a37ab2c --- /dev/null +++ b/src/datastoreAdapter.ts @@ -0,0 +1,140 @@ +import get from 'lodash/get' +import merge from 'lodash/merge' +import { + DatastoreAdapter, + OrmSearch, + DataDescription, + PrimaryKeyType, + ModelType, + ModelInstance, + OrmModel, +} from 'functional-models' +import * as types from './types' +import { toElasticSearch } from './lib' + +type WithRequired = T & { [P in K]-?: T[P] } +const DEFAULT_TAKE = 10000 + +export const defaultGetIndexForModel = ( + model: ModelType +) => { + const x = model.getName().toLowerCase() + return x +} + +const create = ({ + client, + getIndexForModel = defaultGetIndexForModel, +}: types.DatastoreAdapterInputs): WithRequired< + DatastoreAdapter, + 'bulkInsert' +> => { + const retrieve = async ( + model: ModelType, + id: PrimaryKeyType + ) => { + const index = getIndexForModel(model) + const { body } = await client.get({ + index, + id, + }) + + return body._source + } + + const search = ( + model: ModelType, + ormQuery: OrmSearch + ) => { + return Promise.resolve().then(async () => { + const index = getIndexForModel(model) + const updatedSearch = !ormQuery.take + ? merge(ormQuery, { take: DEFAULT_TAKE }) + : ormQuery + const search = toElasticSearch(index, updatedSearch) + const response = await client.search(search) + const toMap = get(response, 'body.hits.hits', []) + + // We have to build the paging ourselves. + const took = toMap.length + const total = response.body.hits.total.value + const isMore = total - took > 0 + const page = isMore ? { from: took } : undefined + + const instances = toMap.map((raw: any) => raw._source) + return { + instances, + page, + } + }) + } + + const save = async ( + instance: ModelInstance + ) => { + const index = getIndexForModel(instance.getModel()) + const data = await instance.toObj() + const id = instance.getPrimaryKey() + await client.index({ + id, + index, + body: data, + }) + return data + } + + const bulkInsert = async ( + model: OrmModel, + instances: readonly ModelInstance[] + ) => { + if (instances.length < 1) { + return + } + const index = getIndexForModel(instances[0].getModel()) + const operations = await instances.reduce( + async (accP: Promise, instance: ModelInstance) => { + const acc = await accP + const data = await instance.toObj() + const id = instance.getPrimaryKey() + return acc.concat([ + { + index: { _index: index, _id: id }, + }, + data, + ]) + return acc.concat(data) + }, + Promise.resolve([] as any[]) + ) + await client.bulk({ + index, + refresh: true, + body: operations, + }) + //TODO: Handle exceptions + return + } + + const deleteObj = async ( + model: OrmModel, + primarykey: PrimaryKeyType + ) => { + const index = getIndexForModel(model) + await client.delete({ + index, + id: primarykey, + }) + return + } + + return { + bulkInsert, + //@ts-ignore + search, + retrieve, + save, + delete: deleteObj, + } +} + +export { create } diff --git a/src/datastoreProvider.ts b/src/datastoreProvider.ts deleted file mode 100644 index 8d42490..0000000 --- a/src/datastoreProvider.ts +++ /dev/null @@ -1,119 +0,0 @@ -import get from 'lodash/get' -import { DatastoreProvider, OrmQuery } from 'functional-models-orm/interfaces' -import { - FunctionalModel, - PrimaryKeyType, - Model, - ModelInstance, -} from 'functional-models/interfaces' -import * as types from './types' -import { toElasticSearch } from './lib' - -export const defaultGetIndexForModel = ( - model: Model -) => { - return model.getName().toLowerCase() -} - -export const create = ({ - client, - getIndexForModel = defaultGetIndexForModel, -}: types.DatastoreProviderInputs): DatastoreProvider => { - const retrieve = async ( - model: Model, - id: PrimaryKeyType - ) => { - const index = getIndexForModel(model) - const { body } = await client.get({ - index, - id, - }) - - return body._source - } - - const search = ( - model: Model, - ormQuery: OrmQuery - ) => { - return Promise.resolve().then(async () => { - const index = getIndexForModel(model) - const search = toElasticSearch(index, ormQuery) - const results = await client.search(search).then((response: any) => { - const toMap = get(response, 'body.hits.hits', []) - const instances = toMap.map((raw: any) => raw._source) - return { - instances, - page: undefined, - } - }) - return results - }) - } - - const save = async >( - instance: ModelInstance - ) => { - const index = getIndexForModel(instance.getModel()) - const data = await instance.toObj() - const id = data[instance.getPrimaryKeyName()] - await client.index({ - id, - index, - body: data, - }) - return data - } - - const bulkInsert = async >( - model: TModel, - instances: readonly ModelInstance[] - ) => { - if (instances.length < 1) { - return - } - const index = getIndexForModel(instances[0].getModel()) - const operations = await instances.reduce( - async (accP: Promise, instance: ModelInstance) => { - const acc = await accP - const data = await instance.toObj() - const id = data[instance.getPrimaryKeyName()] - return acc.concat([ - { - index: { _index: index, _id: id }, - }, - data, - ]) - return acc.concat(data) - }, - Promise.resolve([] as any[]) - ) - await client.bulk({ - index, - refresh: true, - body: operations, - }) - //TODO: Handle exceptions - return - } - - const deleteObj = async >( - instance: ModelInstance - ) => { - const index = getIndexForModel(instance.getModel()) - await client.delete({ - index, - id: await instance.getPrimaryKey(), - }) - return - } - - return { - bulkInsert, - //@ts-ignore - search, - retrieve, - save, - delete: deleteObj, - } -} diff --git a/src/index.ts b/src/index.ts index f53d004..07c1bd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export * as datastoreProvider from './datastoreProvider' +export * as datastoreAdapter from './datastoreAdapter' export * as types from './types' diff --git a/src/lib.ts b/src/lib.ts index 16ff8d6..422c7f3 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,9 +1,23 @@ import merge from 'lodash/merge' -import uniq from 'lodash/uniq' import { - interfaces as ormInterfaces, - ormQuery as ormQueryLib, -} from 'functional-models-orm' + BooleanQuery, + DatastoreValueType, + DatesAfterQuery, + DatesBeforeQuery, + EqualitySymbol, + isALinkToken, + isPropertyBasedQuery, + OrmSearch, + PropertyQuery, + Query, + QueryTokens, + SortOrder, + SortStatement, + threeitize, + validateOrmSearch, +} from 'functional-models' + +const MAX_TAKE = 10000 enum ElasticEqualityType { lt = 'lt', @@ -19,18 +33,16 @@ enum ElasticQueryType { range = 'range', } -const EQUALITY_SYMBOL_MAP: { - [key in ormInterfaces.EQUALITY_SYMBOLS]: string | undefined -} = { - [ormInterfaces.EQUALITY_SYMBOLS.LT]: ElasticEqualityType.lt, - [ormInterfaces.EQUALITY_SYMBOLS.LTE]: ElasticEqualityType.lte, - [ormInterfaces.EQUALITY_SYMBOLS.GT]: ElasticEqualityType.gt, - [ormInterfaces.EQUALITY_SYMBOLS.GTE]: ElasticEqualityType.gte, - [ormInterfaces.EQUALITY_SYMBOLS.EQUALS]: undefined, +const EQUALITY_SYMBOL_MAP: Record = { + [EqualitySymbol.lt]: ElasticEqualityType.lt, + [EqualitySymbol.lte]: ElasticEqualityType.lte, + [EqualitySymbol.gt]: ElasticEqualityType.gt, + [EqualitySymbol.gte]: ElasticEqualityType.gte, + [EqualitySymbol.eq]: undefined, } export const toElasticValue = ( - s: ormInterfaces.PropertyStatement, + s: PropertyQuery, queryType: ElasticQueryType ) => { if (queryType === ElasticQueryType.term) { @@ -46,9 +58,7 @@ export const toElasticValue = ( } if (queryType === ElasticQueryType.range) { const equalitySymbol = - EQUALITY_SYMBOL_MAP[ - s.options.equalitySymbol as ormInterfaces.EQUALITY_SYMBOLS - ] + EQUALITY_SYMBOL_MAP[s.equalitySymbol as EqualitySymbol] if (!equalitySymbol) { throw new Error(`Unexpected equality symbol ${equalitySymbol}`) } @@ -59,55 +69,54 @@ export const toElasticValue = ( throw new Error(`Unhandled queryType: ${queryType}`) } -export const getElasticQueryType = ( - s: ormInterfaces.PropertyStatement -): ElasticQueryType => { +export const getElasticQueryType = (s: PropertyQuery): ElasticQueryType => { if ( - s.valueType === ormInterfaces.ORMType.string || - s.valueType === ormInterfaces.ORMType.date + s.valueType === DatastoreValueType.string || + s.valueType === DatastoreValueType.date ) { if (s.options.startsWith || s.options.endsWith) { return ElasticQueryType.wildcard } return ElasticQueryType.term } - if (s.valueType === ormInterfaces.ORMType.number) { - if (s.options.equalitySymbol !== ormInterfaces.EQUALITY_SYMBOLS.EQUALS) { + if (s.valueType === DatastoreValueType.number) { + if (s.equalitySymbol !== EqualitySymbol.eq) { return ElasticQueryType.range } } return ElasticQueryType.term } -export const toElasticSize = (take: number | undefined) => { - return take - ? { - size: take, +export const toElasticSize = (search: OrmSearch) => { + if (search.take) { + if (search.page) { + const isAboveMax = search.take + search.page.from > MAX_TAKE + if (isAboveMax) { + return { size: MAX_TAKE - search.page.from } } - : {} + } + return { size: search.take } + } + return {} } -export const toElasticSort = ( - sort: ormInterfaces.SortStatement | undefined -) => { +export const toElasticSort = (sort: SortStatement | undefined) => { return sort ? { - sort: `${sort.key}:${sort.order ? 'asc' : 'desc'}`, + sort: `${sort.key}:${sort.order === SortOrder.asc ? 'asc' : 'desc'}`, } : {} } -export const toElasticPaging = ( - _: ormInterfaces.PaginationStatement | undefined -) => { - return '' +export const toElasticPaging = (page?: { from: number }) => { + return page } -export const toElasticQuery = (s: ormInterfaces.PropertyStatement) => { +export const toElasticQuery = (s: PropertyQuery) => { const queryType = getElasticQueryType(s) return { [queryType]: { - [`${s.name}`]: toElasticValue(s, queryType), + [`${s.key}`]: toElasticValue(s, queryType), }, } } @@ -117,9 +126,7 @@ const _getDateValue = (d: Date | string) => { } export const toElasticDateRange = ( - statement: - | ormInterfaces.DatesBeforeStatement - | ormInterfaces.DatesAfterStatement + statement: DatesBeforeQuery | DatesAfterQuery ) => { const name = statement.type === 'datesBefore' @@ -138,126 +145,81 @@ export const toElasticDateRange = ( } } -export const toElasticDateQuery = ( - datesBefore: { [s: string]: ormInterfaces.DatesBeforeStatement }, - datesAfter: { [s: string]: ormInterfaces.DatesAfterStatement } -) => { - const dateProps = uniq( - Object.keys(datesBefore).concat(Object.keys(datesAfter)) - ) - if (dateProps.length < 1) { - return undefined - } - return { - range: dateProps.reduce((acc, name) => { - const before = datesBefore[name] - ? { - [datesBefore[name].options.equalToAndBefore ? 'lte' : 'lt']: - _getDateValue(datesBefore[name].date), - } - : {} - const after = datesAfter[name] - ? { - [datesAfter[name].options.equalToAndAfter ? 'gte' : 'gt']: - _getDateValue(datesAfter[name].date), - } - : {} - return merge(acc, { [name]: merge(before, after) }) - }, {}), - } -} - -export const propertiesToElasticQuery = ( - properties: readonly ormInterfaces.PropertyStatement[] -) => { - const statements = properties.reduce((acc, statement) => { - const query = toElasticQuery(statement) - return acc.concat(query) - }, [] as any[]) - // TODO: We need to group the statements together by "and" and "or" statements. all and statements are 'must' and all or are "should" - return [ - { - bool: { - must: statements, - }, - }, - ] +const _getBooleanFunc = (token: BooleanQuery) => { + return token === 'AND' ? 'must' : 'should' } -export const getPropertyStatements = ( - statements: readonly ormInterfaces.OrmQueryStatement[] -): ormInterfaces.PropertyStatement[] => { - return statements.filter(s => { - return s.type === 'property' - }) as ormInterfaces.PropertyStatement[] +const _buildProperty = (statement: Query) => { + if (statement.type === 'datesBefore' || statement.type === 'datesAfter') { + return toElasticDateRange(statement) + } + return toElasticQuery(statement) } -const createMust = ( - statement: - | ormInterfaces.PropertyStatement - | ormInterfaces.DatesBeforeStatement - | ormInterfaces.DatesAfterStatement -) => { - if (statement.type === 'datesBefore' || statement.type === 'datesAfter') { - const dateQuery = toElasticDateRange(statement) +const _buildQuery = ( + tokens: QueryTokens +): { + bool: { + must: any[] + } +} => { + if (isPropertyBasedQuery(tokens)) { + const query = _buildProperty(tokens) return { bool: { - must: [dateQuery], + must: [query], }, } } - - const query = toElasticQuery(statement) - return { - bool: { - must: [query], - }, - } -} - -const createShould = ( - orProperties: ( - | ormInterfaces.PropertyStatement - | ormInterfaces.DatesBeforeStatement - | ormInterfaces.DatesAfterStatement - )[] -) => { - const queries = orProperties.reduce((acc, statement) => { - if (statement.type === 'datesBefore' || statement.type === 'datesAfter') { - const dateQuery = toElasticDateRange(statement) - return acc.concat(dateQuery) + if (Array.isArray(tokens)) { + // Is everything just a property query? If so, they are all ANDS. + if (tokens.every(t => !isALinkToken(t))) { + const queries = tokens.map(_buildProperty) + return { + bool: { + must: queries, + }, + } } - - const query = toElasticQuery(statement) - return acc.concat(query) - }, [] as any[]) - return { - bool: { - should: queries, - }, + const threes = threeitize(tokens) + const innerStatements = threes.map(([a, l, b]) => { + const aSearch = _buildQuery(a as Query) + const bSearch = _buildQuery(b as Query) + const linker = _getBooleanFunc(l) + return { + bool: { + [linker]: [aSearch, bSearch], + }, + } + }) + return { + bool: { + must: innerStatements, + }, + } + // Dealing with complex situation } + throw new Error('Never going to get here') } -export const toElasticSearch = ( - index: string, - ormQuery: ormInterfaces.OrmQuery -) => { +export const toElasticSearch = (index: string, ormQuery: OrmSearch) => { + validateOrmSearch(ormQuery) + /* const booleanChains = ormQueryLib.createBooleanChains(ormQuery) const musts = booleanChains.ands.map(createMust) const shoulds = booleanChains.orChains.map(createShould) + */ + const sort = toElasticSort(ormQuery.sort) - const size = toElasticSize(ormQuery.take) + const size = toElasticSize(ormQuery) + // @ts-ignore const paging = toElasticPaging(ormQuery.page) return merge( { index }, { body: { - query: { - bool: { - must: [...musts, ...shoulds], - }, - }, + query: _buildQuery(ormQuery.query), }, }, sort, diff --git a/src/types.ts b/src/types.ts index c2f74ee..544b27a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ -import { FunctionalModel, Model } from 'functional-models/interfaces' +import { DataDescription, ModelType } from 'functional-models' -export type DatastoreProviderInputs = { +export type DatastoreAdapterInputs = { client: any - getIndexForModel?: (model: Model) => string + getIndexForModel?: (model: ModelType) => string } export type ErrorOperation = { diff --git a/test/src/lib.test.ts b/test/src/lib.test.ts index 0b288cf..5977a80 100644 --- a/test/src/lib.test.ts +++ b/test/src/lib.test.ts @@ -1,11 +1,11 @@ import { assert } from 'chai' -import { ormQueryBuilder } from 'functional-models-orm' +import { queryBuilder } from 'functional-models' import { toElasticSearch } from '../../src/lib' describe('/src/lib.ts', () => { describe('#toElasticSearch()', () => { it('should produce expected query from one property', () => { - const input = ormQueryBuilder().property('a', 1).compile() + const input = queryBuilder().property('a', 1).compile() const actual = toElasticSearch('my-index', input) const expected = { index: 'my-index', @@ -14,16 +14,10 @@ describe('/src/lib.ts', () => { bool: { must: [ { - bool: { - must: [ - { - term: { - a: { - value: 1, - }, - }, - }, - ], + term: { + a: { + value: 1, + }, }, }, ], @@ -35,8 +29,9 @@ describe('/src/lib.ts', () => { assert.deepEqual(actual, expected) }) it('should produce expected query from two properties', () => { - const input = ormQueryBuilder() + const input = queryBuilder() .property('a', 1) + .and() .property('b', 2) .compile() const actual = toElasticSearch('my-index', input) @@ -50,23 +45,29 @@ describe('/src/lib.ts', () => { bool: { must: [ { - term: { - a: { - value: 1, - }, + bool: { + must: [ + { + term: { + a: { + value: 1, + }, + }, + }, + ], }, }, - ], - }, - }, - { - bool: { - must: [ { - term: { - b: { - value: 2, - }, + bool: { + must: [ + { + term: { + b: { + value: 2, + }, + }, + }, + ], }, }, ], @@ -81,10 +82,11 @@ describe('/src/lib.ts', () => { assert.deepEqual(actual, expected) }) it('should produce expected query from two AND properties and two OR properties', () => { - const input = ormQueryBuilder() + const input = queryBuilder() .property('a', 1) .and() .property('b', 2) + .and() .property('c', 3) .or() .property('c', 4) @@ -100,10 +102,29 @@ describe('/src/lib.ts', () => { bool: { must: [ { - term: { - a: { - value: 1, - }, + bool: { + must: [ + { + term: { + a: { + value: 1, + }, + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + b: { + value: 2, + }, + }, + }, + ], }, }, ], @@ -113,10 +134,29 @@ describe('/src/lib.ts', () => { bool: { must: [ { - term: { - b: { - value: 2, - }, + bool: { + must: [ + { + term: { + b: { + value: 2, + }, + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + c: { + value: 3, + }, + }, + }, + ], }, }, ], @@ -126,17 +166,29 @@ describe('/src/lib.ts', () => { bool: { should: [ { - term: { - c: { - value: 3, - }, + bool: { + must: [ + { + term: { + c: { + value: 3, + }, + }, + }, + ], }, }, { - term: { - c: { - value: 4, - }, + bool: { + must: [ + { + term: { + c: { + value: 4, + }, + }, + }, + ], }, }, ], @@ -147,6 +199,7 @@ describe('/src/lib.ts', () => { }, }, } + // @ts-ignore assert.deepEqual(actual, expected) }) 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 c6b0fae..b2fb974 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,26 +1,18 @@ { "compilerOptions": { - "target": "es5", - "lib": ["es2021", "dom", "es2015"], - "module": "commonjs", - "rootDir": "./src", - "allowJs": true, + "target": "es6", + "lib": ["es2023", "es2021", "dom", "es2017"], + "module": "node16", + "moduleResolution": "node16", + "resolveJsonModule": true, "declaration": true, "sourceMap": true, - "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "rootDir": "./src", + "outDir": "./dist" }, - "include": ["./src"], - "exclude": [ - "./index.js", - "./index.d.ts", - "./src/index.js", - "./src/index.d.ts", - "node_modules", - "dist", - "features" - ] + "exclude": ["node_modules", "dist", "features", "coverage", "test"] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..1fa6c98 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es2021", "dom", "es2015"], + "module": "commonjs", + //"moduleResolution": "node16", + "rootDirs": ["./src", "./test"], + "allowJs": true, + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": false, + "skipLibCheck": true + }, + "include": ["./test"], + "exclude": [ + "src/index.ts", + "src/index.js", + "src/index.d.ts", + "node_modules", + "dist", + "features", + "container", + "features" + ] +} +