├── .eslintrc.js ├── .github └── workflows │ ├── issue.yaml │ ├── main.yml │ └── pull_request.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierrc.js ├── .vscode └── launch.json ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── __test__ ├── collection-operators.spec.ts ├── create-many.spec.ts ├── ensure_collections_and_indexes.spec.ts ├── errors-name.spec.ts ├── exceptions.spec.ts ├── fts.spec.ts ├── global-plugin.spec.ts ├── handler-remove-many.spec.ts ├── handler-update-many.spec.ts ├── hooks.spec.ts ├── ignore-case.spec.ts ├── indexes-query.spec.ts ├── indexes.spec.ts ├── jest.setup.ts ├── let-clause.spec.ts ├── max-expiry.spec.ts ├── model-crud.spec.ts ├── model-find-one-and-update.spec.ts ├── model-find-projection.spec.ts ├── model-populate.spec.ts ├── model-schema-integration.spec.ts ├── model-schema-ref-check.spec.ts ├── model-utils.spec.ts ├── nested-model-key.spec.ts ├── ottoman-instances.spec.ts ├── query-array.spec.ts ├── query-builder.spec.ts ├── query-index.spec.ts ├── query-lean.spec.ts ├── query-select.spec.ts ├── query-utils.spec.ts ├── schema-cast.spec.ts ├── schema-inmutable.spec.ts ├── schema-timestamps.spec.ts ├── schema.add.spec.ts ├── schema.complex.types.spec.ts ├── schema.helpers.spec.ts ├── schema.native.types.spec.ts ├── schema.spec.ts ├── schema.types.spec.ts ├── schema.validators.spec.ts ├── server-ops.spec-broken.ts ├── testData.ts ├── transaction-indexes.spec.ts ├── transactions.spec.ts └── utils.spec.ts ├── codecov.yml ├── docusaurus ├── .env.example ├── .gitignore ├── CNAME ├── README.md ├── babel.config.js ├── docs │ ├── advanced │ │ ├── _category_.yml │ │ ├── create.png │ │ ├── findById.png │ │ ├── fts.md │ │ ├── how-ottoman-works.md │ │ ├── mongodb-to-couchbase.jpg │ │ ├── mongoose-to-couchbase.md │ │ ├── ottoman-couchbase-node-sdk.jpg │ │ ├── ottoman-couchbase.md │ │ ├── ottoman.md │ │ ├── ottomanV1-to-ottomanV2.md │ │ ├── sdk-comparison.md │ │ └── transactions.md │ ├── basic │ │ ├── _category_.yml │ │ ├── connection-anatomy.png │ │ ├── document.md │ │ ├── how-to-use.png │ │ ├── howToUse.jpg │ │ ├── install-screen-sm.png │ │ ├── model.md │ │ ├── ottoman.md │ │ ├── query-builder.md │ │ └── schema.md │ ├── cli │ │ ├── _category_.yml │ │ ├── cli.md │ │ └── generate.png │ ├── faq.md │ ├── first-app.md │ ├── intro.md │ └── quick-start.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.js │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.js │ │ ├── index.module.css │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── couchbase.png │ │ ├── familiar.svg │ │ ├── fast.svg │ │ ├── favicon.ico │ │ ├── generate.png │ │ └── logo.svg └── yarn.lock ├── jest.config.js ├── package.json ├── release.config.js ├── src ├── couchbase.ts ├── exceptions │ ├── exceptions.ts │ └── ottoman-errors.ts ├── handler │ ├── create-many.ts │ ├── find │ │ ├── find-by-id-options.ts │ │ ├── find-options.ts │ │ └── find.ts │ ├── index.ts │ ├── remove-many.ts │ ├── remove.ts │ ├── store.ts │ ├── types.ts │ ├── update_many.ts │ └── utils.ts ├── index.ts ├── model │ ├── create-model.ts │ ├── document.ts │ ├── hooks │ │ └── exec-hooks.ts │ ├── index │ │ ├── helpers │ │ │ └── index-field-names.ts │ │ ├── n1ql │ │ │ ├── build-index-query.ts │ │ │ ├── ensure-n1ql-indexes.ts │ │ │ └── extract-index-field-names.ts │ │ ├── refdoc │ │ │ └── build-index-refdoc.ts │ │ ├── types │ │ │ └── index.types.ts │ │ └── view │ │ │ ├── build-map-view-index-fn.ts │ │ │ ├── build-view-index-query.ts │ │ │ ├── ensure-view-indexes.ts │ │ │ └── view-index-options.ts │ ├── interfaces │ │ ├── create-model.interface.ts │ │ ├── find.interface.ts │ │ ├── model-metadata.interface.ts │ │ └── update-many.interface.ts │ ├── model.ts │ ├── model.types.ts │ ├── populate.types.ts │ └── utils │ │ ├── array-diff.ts │ │ ├── get-model-ref-keys.ts │ │ ├── model.utils.ts │ │ ├── remove-life-cycle.ts │ │ ├── store-life-cycle.ts │ │ └── update-refdoc-indexes.ts ├── ottoman │ └── ottoman.ts ├── plugins │ ├── global-plugin-handler-error.ts │ └── global-plugin-handler.ts ├── query │ ├── base-query.ts │ ├── exceptions.ts │ ├── helpers │ │ ├── builders.ts │ │ ├── dictionary.ts │ │ ├── index.ts │ │ └── reservedWords.ts │ ├── index.ts │ ├── interface │ │ └── query.types.ts │ ├── query.ts │ └── utils.ts ├── schema │ ├── errors │ │ ├── build-schema-error.ts │ │ ├── index.ts │ │ └── validation-error.ts │ ├── helpers │ │ ├── date-minmax.ts │ │ ├── fn-schema.ts │ │ ├── index.ts │ │ ├── is-ottoman-type.ts │ │ ├── number-minmax.ts │ │ └── validator.ts │ ├── index.ts │ ├── interfaces │ │ └── schema.types.ts │ ├── schema.ts │ └── types │ │ ├── array-type.ts │ │ ├── boolean-type.ts │ │ ├── core-type.ts │ │ ├── date-type.ts │ │ ├── embed-type.ts │ │ ├── index.ts │ │ ├── mixed-type.ts │ │ ├── number-type.ts │ │ ├── reference-type.ts │ │ └── string-type.ts └── utils │ ├── cast-strategy.ts │ ├── constants.ts │ ├── environment-set.ts │ ├── extract-connection-string.ts │ ├── extract-data-from-model.ts │ ├── generate-uuid.ts │ ├── getValueByPath.ts │ ├── hooks.ts │ ├── index.ts │ ├── is-debug-mode.ts │ ├── is-metadata.ts │ ├── is-model.ts │ ├── is-not-found.ts │ ├── is-type.ts │ ├── jp-parse.ts │ ├── merge.ts │ ├── noenumarable.ts │ ├── parse-errors.ts │ ├── path-to-n1ql.ts │ ├── pipe.ts │ ├── populate │ ├── can-be-populated.ts │ └── is-populate-object.ts │ ├── query │ ├── extract-populate.ts │ └── extract-select.ts │ ├── schema.utils.ts │ ├── search-consistency.ts │ ├── setValueByPath.ts │ ├── type-helpers.ts │ └── validation.strategy.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: 'module', // Allows for the use of imports 6 | }, 7 | extends: [ 8 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | 'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 10 | 'plugin:prettier/recommended', 11 | ], 12 | rules: { 13 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 14 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 15 | 'no-unused-labels': 2, 16 | 'no-unused-vars': 0, 17 | '@typescript-eslint/ban-ts-comment': 0, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/issue.yaml: -------------------------------------------------------------------------------- 1 | # name: Issue Opened 2 | 3 | # on: 4 | # issues: 5 | # types: [opened, reopened] 6 | 7 | # env: 8 | # ISSUE_URL: ${{ github.event.issue.html_url }} 9 | # ISSUE_TITLE: ${{ github.event.issue.title }} 10 | # ISSUE_BODY: ${{ github.event.issue.body }} 11 | # jobs: 12 | # new_issue: 13 | # name: New Issue 14 | # runs-on: ubuntu-latest 15 | # steps: 16 | # - uses: actions/checkout@v2 17 | # - name: Send Issues to slack 18 | # run: | 19 | # echo "Newly opened issue at " 20 | # # ISSUE_TITLE=${{ github.event.issue.title }} 21 | # # ISSUE_BODY=${{ github.event.issue.body }} 22 | # # ISSUE_URL=${{ github.event.issue.issue_url }} 23 | # TIME_STAMP="`date +%s`" 24 | # cat < /tmp/slack_message.json 25 | # { 26 | # "type": "mrkdwn", 27 | # "text": " Issue Opened on Node Ottoman ", 28 | # "attachments": [ 29 | # { 30 | # "fallback": " Node Ottoman Issue", 31 | # "color": "#36a64f", 32 | # "pretext": " Opened Issue : $ISSUE_TITLE \n \n $ISSUE_BODY \n \n For more details about issue: $ISSUE_URL ", 33 | # "footer_icon": "https://www.couchbase.com/webfiles/1629373386042/images/favicon.ico", 34 | # "ts": ${TIME_STAMP} 35 | # } 36 | # ] 37 | # } 38 | # EOT 39 | # cat /tmp/slack_message.json 40 | # SLACK_WEBHOOK_URL="${{ secrets.SLACK_WEBHOOK_URL_DA_ALERTS_SUCCESS }}" 41 | # curl -X POST -H 'Content-type: application/json' --data @/tmp/slack_message.json ${SLACK_WEBHOOK_URL} 42 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | services: 10 | couchbase: 11 | image: couchbase/server-sandbox:7.1.3 12 | ports: 13 | - 8091-8094:8091-8094 14 | - 11210:11210 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | matrix: 20 | os: [ ubuntu-latest ] 21 | node_version: [ '20' ] 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Set up Node.js 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{ matrix.node_version }} 30 | 31 | - name: Install Dependencies 32 | run: yarn install --immutable 33 | 34 | - name: Build 35 | run: yarn build 36 | 37 | - name: Test 38 | run: yarn test:coverage 39 | 40 | - name: Upload coverage to Codecov 41 | uses: codecov/codecov-action@v1 42 | 43 | - name: Create .env file 44 | uses: SpicyPizza/create-envfile@v1.3.0 45 | with: 46 | envKey_ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} 47 | envKey_ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} 48 | directory: docusaurus/ 49 | 50 | - name: Generate docs 51 | run: yarn docs 52 | 53 | - name: Publish docs 54 | uses: peaceiris/actions-gh-pages@v3 55 | with: 56 | github_token: ${{ secrets.GITHUB_TOKEN }} 57 | publish_dir: ./docs 58 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | services: 9 | couchbase: 10 | image: couchbase/server-sandbox:7.1.3 11 | ports: 12 | - 8091-8094:8091-8094 13 | - 11210:11210 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | node_version: [ '20' ] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - name: Set up Node.js 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node_version }} 29 | 30 | - name: Install Dependencies 31 | run: yarn install --immutable 32 | 33 | - name: Build 34 | run: yarn build 35 | 36 | - name: Test 37 | run: yarn test:coverage 38 | 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v1 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | .pnp.* 44 | .yarn/* 45 | !.yarn/patches 46 | !.yarn/plugins 47 | !.yarn/releases 48 | !.yarn/sdks 49 | !.yarn/versions 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # Next.js build output 86 | .next 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | .idea 113 | lib 114 | /docusaurus/docs/api/ 115 | 116 | # Misc 117 | .DS_Store 118 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint 5 | yarn build 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | .idea 106 | /src 107 | .vuepress 108 | vuepress 109 | api 110 | __test__ 111 | .github 112 | docs -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | trailingComma: 'all', 4 | tabWidth: 2, 5 | singleQuote: true, 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach", 9 | "port": 9229, 10 | "request": "attach", 11 | "skipFiles": ["/**"], 12 | "type": "pwa-node", 13 | "env": { "OTTOMAN_LEGACY_TEST": 1 } 14 | }, 15 | 16 | { 17 | "type": "pwa-chrome", 18 | "request": "launch", 19 | "name": "Launch Chrome against localhost", 20 | "url": "http://localhost:8080", 21 | "webRoot": "${workspaceFolder}" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.2.0](http://github.com/couchbaselabs/node-ottoman/compare/v2.1.0...v2.2.0) (2022-03-29) 2 | 3 | 4 | ### Features 5 | 6 | * **hooks:** trigger embed schema hooks ([9389dfe](http://github.com/couchbaselabs/node-ottoman/commit/9389dfefe9ea49cb9f302152abeb24da9f391c80)) 7 | * **ottoman:** add support for nested modelKey ([6bcb9fc](http://github.com/couchbaselabs/node-ottoman/commit/6bcb9fc9dee7084ff259b2db8660326cb946a8a7)), closes [#638](http://github.com/couchbaselabs/node-ottoman/issues/638) 8 | -------------------------------------------------------------------------------- /__test__/create-many.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, IManyQueryResponse, model } from '../src'; 2 | import { ManyQueryResponse, StatusExecution } from '../src/handler'; 3 | import { consistency, startInTest } from './testData'; 4 | 5 | test('Test Create Many', async () => { 6 | const Box = model('Box', { name: String, price: Number }); 7 | await startInTest(getDefaultInstance()); 8 | const docs = [{ name: 'Xbox' }, { name: 'Yellow Box' }]; 9 | interface Doc { 10 | id?: string; 11 | name: string; 12 | price?: number; 13 | } 14 | const queryResult = await Box.createMany(docs); 15 | const boxs = await Box.find({}, consistency); 16 | const cleanUp = async () => await Box.removeMany({ _type: 'Box' }); 17 | await cleanUp(); 18 | expect(queryResult.message.success).toBe(docs.length); 19 | expect(boxs.rows.length).toBeGreaterThanOrEqual(2); 20 | expect(queryResult.message.data.length).toStrictEqual(docs.length); 21 | expect(queryResult.message.data[0].id).toBeDefined(); 22 | }); 23 | 24 | test('Test Create Many with find limit 0', async () => { 25 | const Box = model('Box', { name: String, price: Number }); 26 | await startInTest(getDefaultInstance()); 27 | const docs = [{ name: 'Xbox' }, { name: 'Yellow Box' }]; 28 | const queryResult: IManyQueryResponse = await Box.createMany(docs); 29 | const boxs = await Box.find({}, { ...consistency, limit: 0 }); 30 | const cleanUp = async () => await Box.removeMany({ _type: 'Box' }); 31 | await cleanUp(); 32 | expect(queryResult.message.success).toBe(docs.length); 33 | expect(boxs.rows.length).toBe(0); 34 | }); 35 | 36 | test('Test Create Many Errors ', async () => { 37 | const Box = model('Box', { name: String, price: { required: true, type: Number } }); 38 | await startInTest(getDefaultInstance()); 39 | const docs = [{ name: 'Xbox', price: 10 }, { name: 'Yellow Box' }]; 40 | const queryResult: IManyQueryResponse = await Box.createMany(docs); 41 | const cleanUp = async () => await Box.removeMany({ _type: 'Box' }); 42 | await cleanUp(); 43 | expect(queryResult.message.success).toBe(1); 44 | expect(queryResult.message.errors.length).toBe(1); 45 | expect(queryResult.message.errors[0].exception).toBe('ValidationError'); 46 | }); 47 | 48 | test('Test Create Many Errors JSON Strict ', async () => { 49 | const Box = model('Box', { name: String, price: { required: true, type: Number } }); 50 | await startInTest(getDefaultInstance()); 51 | const docs = [{ name: 'Xbox', price: 10 }, { name: 'Yellow Box' }]; 52 | const queryResult: IManyQueryResponse = await Box.createMany(docs); 53 | 54 | const queryResultJson: IManyQueryResponse = new ManyQueryResponse('FAILURE', { 55 | data: [], 56 | success: 1, 57 | match_number: 2, 58 | errors: [new StatusExecution({ name: 'Yellow Box' }, 'FAILURE', 'ValidationError', `Property 'price' is required`)], 59 | }); 60 | const cleanUp = async () => await Box.removeMany({ _type: 'Box' }); 61 | await cleanUp(); 62 | expect(queryResult.status).toBe('FAILURE'); 63 | expect(queryResult.message.success).toBe(1); 64 | expect(queryResult.message.match_number).toBe(2); 65 | expect(JSON.stringify(queryResult.message.errors)).toStrictEqual(JSON.stringify(queryResultJson.message.errors)); 66 | }); 67 | 68 | test('Test Create Many Errors Class Strict ', async () => { 69 | const Box = model('Box', { name: String, price: { required: true, type: Number } }); 70 | await startInTest(getDefaultInstance()); 71 | const docs = [{ name: 'Xbox', price: 10 }, { name: 'Yellow Box' }]; 72 | const queryResult: IManyQueryResponse = await Box.createMany(docs); 73 | 74 | const queryResultClass = new ManyQueryResponse('FAILURE', { 75 | success: 1, 76 | match_number: 2, 77 | data: [], 78 | errors: [ 79 | new StatusExecution( 80 | { 81 | name: 'Yellow Box', 82 | }, 83 | 'FAILURE', 84 | 'ValidationError', 85 | `Property 'price' is required`, 86 | ), 87 | ], 88 | }); 89 | 90 | const cleanUp = async () => await Box.removeMany({ _type: 'Box' }); 91 | await cleanUp(); 92 | expect(queryResult.status).toBe('FAILURE'); 93 | expect(queryResult.message.success).toBe(1); 94 | expect(queryResult.message.match_number).toBe(2); 95 | expect(queryResult.message.errors).toStrictEqual(queryResultClass.message.errors); 96 | }); 97 | -------------------------------------------------------------------------------- /__test__/errors-name.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, model, Schema, ValidationError } from '../src'; 2 | import { BuildIndexQueryError } from '../src/exceptions/ottoman-errors'; 3 | import { cast, CAST_STRATEGY } from '../src/utils/cast-strategy'; 4 | import { startInTest } from './testData'; 5 | 6 | const CardSchemaBase = new Schema({ 7 | cardNumber: { type: String }, 8 | zipCode: String, 9 | }); 10 | 11 | const cardInfo = { 12 | cardNumber: '5678 5678 5678 5678', 13 | zipCode: '56789', 14 | }; 15 | 16 | describe('Errors Name', () => { 17 | test('-> Indexes -> BuildIndexQueryError', async () => { 18 | const CardSchema = new Schema(CardSchemaBase); 19 | CardSchema.index.findByCardNumber = { 20 | by: 'number', 21 | type: 'view', 22 | }; 23 | const Card = model('Card', CardSchema); 24 | await startInTest(getDefaultInstance()); 25 | const result = await Card.create(cardInfo); 26 | try { 27 | await Card.findByCardNumber(); 28 | } catch (e) { 29 | const { message } = e as Error; 30 | expect(e).toBeInstanceOf(BuildIndexQueryError); 31 | expect(message).toBe(`Function 'findByCardNumber' received wrong argument value, 'undefined' wasn't expected`); 32 | } 33 | try { 34 | await Card.findByCardNumber([]); 35 | } catch (e) { 36 | const { message } = e as Error; 37 | expect(e).toBeInstanceOf(BuildIndexQueryError); 38 | expect(message).toBe( 39 | `Function 'findByCardNumber' received wrong number of arguments, '1:[number]' argument(s) was expected and '0:[]' were received`, 40 | ); 41 | } 42 | await Card.removeById(result.id); 43 | }); 44 | test('-> Indexes -> BuildIndexQueryError -> wrong index type', async () => { 45 | const CardSchema = new Schema(CardSchemaBase); 46 | CardSchema.index.findByCardNumber = { 47 | by: 'number', 48 | type: 'dummyIndexType' as unknown as undefined, 49 | }; 50 | try { 51 | model('Card', CardSchema); 52 | } catch (e) { 53 | const { message } = e as Error; 54 | expect(e).toBeInstanceOf(BuildIndexQueryError); 55 | expect(message).toBe( 56 | `Unexpected index type 'dummyIndexType' in index 'findByCardNumber', was expected 'refdoc', 'n1ql' or 'view'`, 57 | ); 58 | } 59 | 60 | // await Card.removeById(result.id); 61 | }); 62 | 63 | test('-> Indexes -> BuildIndexQueryError -> wrong number of arguments for n1ql/undefined type', async () => { 64 | const CardSchema = new Schema(CardSchemaBase); 65 | CardSchema.index.findByCardNumber = { 66 | by: 'cardNumber', 67 | type: 'n1ql', 68 | }; 69 | const Card = model('Card', CardSchema); 70 | try { 71 | await Card.findByCardNumber(['cardNumber', 'zipCode']); 72 | } catch (e) { 73 | const { message } = e as Error; 74 | expect(e).toBeInstanceOf(BuildIndexQueryError); 75 | expect(message).toBe( 76 | `Function 'findByCardNumber' received wrong number of arguments, '1:[cardNumber]' argument(s) was expected and '2:[cardNumber,zipCode]' were received`, 77 | ); 78 | } 79 | }); 80 | test('-> Cast', () => { 81 | const schema = new Schema({ name: String, age: Number }); 82 | try { 83 | cast({ name: 'jane', age: 3 }, schema, { strategy: CAST_STRATEGY.KEEP, strict: true }); 84 | } catch (e) { 85 | const { message } = e as Error; 86 | expect(e).toBeInstanceOf(ValidationError); 87 | expect(message).toBe(`Cast Strategy 'keep' or 'defaultOrKeep' isn't support when strict is set to true.`); 88 | } 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /__test__/exceptions.spec.ts: -------------------------------------------------------------------------------- 1 | import * as couchbase from 'couchbase'; 2 | import { DocumentNotFoundError, IndexExistsError } from '../src'; 3 | 4 | describe('exceptions test', () => { 5 | test('DocumentNotFoundError Exception Error', () => { 6 | expect(couchbase.DocumentNotFoundError.name).toBe(DocumentNotFoundError.name); 7 | expect(new DocumentNotFoundError()).toBeInstanceOf(couchbase.DocumentNotFoundError); 8 | }); 9 | 10 | test('KeyValueErrorContext Exception Context', () => { 11 | expect(couchbase.IndexExistsError.name).toBe(IndexExistsError.name); 12 | expect(new IndexExistsError()).toBeInstanceOf(couchbase.IndexExistsError); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /__test__/fts.spec.ts: -------------------------------------------------------------------------------- 1 | import { searchQuery, SearchQuery } from '../src'; 2 | 3 | const maybe = process.env.CI ? test.skip : test; 4 | 5 | maybe('fts match results', async () => { 6 | const result = await searchQuery('hotels', SearchQuery.match('Gillingham'), { limit: 5 }); 7 | expect(result).toBeDefined(); 8 | expect(result.rows.length).toBeGreaterThanOrEqual(1); 9 | expect(result.rows[0].id).toBeDefined(); 10 | }); 11 | 12 | maybe('fts matchPhrase basic', async () => { 13 | const result = await searchQuery('hotels', SearchQuery.matchPhrase('Medway Youth Hostel'), { limit: 5 }); 14 | expect(result).toBeDefined(); 15 | expect(result.rows.length).toBeGreaterThanOrEqual(1); 16 | expect(result.rows[0].id).toBe('hotel_10025'); 17 | }); 18 | 19 | maybe('fts conjuncts results', async () => { 20 | const query = SearchQuery.conjuncts(SearchQuery.match('Berkeley'), SearchQuery.matchPhrase('luxury hotel')); 21 | const result = await searchQuery('hotels', query); 22 | expect(result).toBeDefined(); 23 | expect(result.rows.length).toBeGreaterThanOrEqual(1); 24 | expect(result.rows[0].id).toBeDefined(); 25 | }); 26 | 27 | maybe('fts disjunction results', async () => { 28 | const query = SearchQuery.disjuncts(SearchQuery.match('Louvre'), SearchQuery.match('Eiffel')); 29 | const result = await searchQuery('hotels', query); 30 | expect(result).toBeDefined(); 31 | expect(result.rows.length).toBeGreaterThanOrEqual(1); 32 | expect(result.rows[0].id).toBeDefined(); 33 | }); 34 | -------------------------------------------------------------------------------- /__test__/global-plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { registerGlobalPlugin, Schema, model, getDefaultInstance } from '../src'; 2 | import { __plugins, getGlobalPlugins } from '../src/plugins/global-plugin-handler'; 3 | import { GlobalPluginHandlerError } from '../src/plugins/global-plugin-handler-error'; 4 | import { startInTest } from './testData'; 5 | 6 | describe('Global Plugin', () => { 7 | test('-> test add global plugin', () => { 8 | const plugins = [ 9 | function plugin1() { 10 | console.log('plugin 1'); 11 | }, 12 | function plugin2() { 13 | console.log('plugin 2'); 14 | }, 15 | ]; 16 | 17 | const plugin3 = () => { 18 | console.log('plugin 3'); 19 | }; 20 | 21 | const plugin4 = () => { 22 | console.log('plugin 4'); 23 | }; 24 | 25 | const plugin5 = () => { 26 | console.log('plugin 5'); 27 | }; 28 | 29 | registerGlobalPlugin(...plugins); 30 | registerGlobalPlugin(plugin3, plugin4); 31 | registerGlobalPlugin(plugin5); 32 | const globalPlugins = getGlobalPlugins(); 33 | expect(globalPlugins.length).toStrictEqual(5); 34 | expect(__plugins.length).toStrictEqual(5); 35 | expect(globalPlugins[0]).toBeInstanceOf(Function); 36 | expect(globalPlugins[1]).toBeInstanceOf(Function); 37 | expect(globalPlugins[2]).toBeInstanceOf(Function); 38 | expect(globalPlugins[3]).toBeInstanceOf(Function); 39 | expect(globalPlugins[4]).toBeInstanceOf(Function); 40 | expect(globalPlugins[0].name).toBe(plugins[0].name); 41 | expect(globalPlugins[1].name).toBe(plugins[1].name); 42 | expect(globalPlugins[2].name).toBe(plugin3.name); 43 | expect(globalPlugins[3].name).toBe(plugin4.name); 44 | expect(globalPlugins[4].name).toBe(plugin5.name); 45 | }); 46 | 47 | test('-> should throw a GlobalPluginHandlerError', () => { 48 | expect(() => registerGlobalPlugin([null])).toThrow(GlobalPluginHandlerError); 49 | }); 50 | 51 | test('global plugin with hook to modify document', async () => { 52 | const pluginLog = (pSchema: any) => { 53 | pSchema.pre('save', function (doc: any) { 54 | doc.operational = false; 55 | return doc; 56 | }); 57 | }; 58 | 59 | const pluginLog2 = (pSchema: any) => { 60 | pSchema.pre('save', function (doc: any) { 61 | doc.plugin = 'registered from plugin 2!'; 62 | return doc; 63 | }); 64 | }; 65 | 66 | registerGlobalPlugin(pluginLog); 67 | registerGlobalPlugin(pluginLog2); 68 | 69 | const schema = new Schema({ 70 | name: String, 71 | operational: Boolean, 72 | }); 73 | 74 | const schemaData = { 75 | name: 'hello', 76 | operational: true, 77 | }; 78 | 79 | const GlobalPlugins = model('globalPlugins', schema); 80 | 81 | await startInTest(getDefaultInstance()); 82 | 83 | const doc = await GlobalPlugins.create(schemaData); 84 | expect(doc.operational).toBe(false); // plugin 85 | expect(doc.plugin).toBe('registered from plugin 2!'); // plugin2 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /__test__/handler-remove-many.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, getModelMetadata, model, Schema, DocumentNotFoundError } from '../src'; 2 | import { batchProcessQueue, chunkArray, IManyQueryResponse, removeCallback, StatusExecution } from '../src/handler'; 3 | import { ModelMetadata } from '../src/model/interfaces/model-metadata.interface'; 4 | import { consistency, startInTest } from './testData'; 5 | 6 | describe('Test Document Remove Many', () => { 7 | test('Test Process Query Stack Function', async () => { 8 | const removeCallback = async (id: string) => { 9 | if (id.indexOf('9') !== -1) { 10 | return Promise.resolve(new StatusExecution(id, 'SUCCESS')); 11 | } else { 12 | return Promise.reject(new StatusExecution(id, 'FAILURE')); 13 | } 14 | }; 15 | const stack = Array(205) 16 | .fill(null) 17 | .map((u, i) => i.toString()); 18 | // @ts-ignore 19 | const items = await batchProcessQueue({ collection: null } as ModelMetadata)(stack, removeCallback, 100); 20 | expect(items.message.success).toBe(38); 21 | expect(items.message.errors.length).toBe(167); 22 | }); 23 | test('Test ChunkArray Function', () => { 24 | const stack = Array(50) 25 | .fill(null) 26 | .map((u, i) => i.toString()); 27 | const result = chunkArray(stack, 10); 28 | expect(result.length).toBe(5); 29 | }); 30 | 31 | test('Test Remove Many Function', async () => { 32 | const CatSchema = new Schema({ 33 | name: String, 34 | age: Number, 35 | }); 36 | const Cat = model('Cat', CatSchema); 37 | await startInTest(getDefaultInstance()); 38 | 39 | const batchCreate = async () => { 40 | await Cat.create({ name: 'Cat0', age: 27 }); 41 | await Cat.create({ name: 'Cat1', age: 28 }); 42 | await Cat.create({ name: 'Cat2', age: 29 }); 43 | await Cat.create({ name: 'Cat3', age: 30 }); 44 | }; 45 | await batchCreate(); 46 | const response: IManyQueryResponse = await Cat.removeMany({ name: { $like: '%Cat%' } }, consistency); 47 | expect(response.message.success).toBe(4); 48 | expect(response.message.data?.length).toBe(4); 49 | expect(response.message.match_number).toBe(4); 50 | }); 51 | 52 | test('Test Remove Many Function Document Not Found Error', async () => { 53 | const CatSchema = new Schema({ 54 | name: String, 55 | age: Number, 56 | }); 57 | const Cat = model('Cat', CatSchema); 58 | await startInTest(getDefaultInstance()); 59 | const response: IManyQueryResponse = await Cat.removeMany({ name: { $like: 'DummyCatName91' } }); 60 | expect(response.message.success).toBe(0); 61 | expect(response.message.match_number).toBe(0); 62 | expect(response.message.errors).toEqual([]); 63 | }); 64 | 65 | test('Update Many Response Errors', async () => { 66 | const CatSchema = new Schema({ 67 | name: String, 68 | age: Number, 69 | }); 70 | const Cat = model('Cat', CatSchema); 71 | const metadata = getModelMetadata(Cat); 72 | try { 73 | await removeCallback('dummy_id', metadata, {}, {}); 74 | } catch (err) { 75 | const error = err as StatusExecution; 76 | const dnf = new DocumentNotFoundError(); 77 | const cleanUp = async () => await Cat.removeMany({ _type: 'Cat' }); 78 | await cleanUp(); 79 | expect(error.exception).toBe(dnf.constructor.name); 80 | expect(error.message).toBe(dnf.message); 81 | expect(error.status).toBe('FAILURE'); 82 | } 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /__test__/hooks.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, isDocumentNotFoundError, model, Schema } from '../src'; 2 | import { startInTest } from './testData'; 3 | 4 | const accessDoc2 = { 5 | type: 'hooks', 6 | isActive: false, 7 | name: 'Ottoman Hooks', 8 | }; 9 | 10 | const removeDoc = { 11 | isActive: true, 12 | name: 'Remove', 13 | }; 14 | 15 | const validateDoc = { 16 | isActive: true, 17 | name: 'Validate Doc', 18 | }; 19 | 20 | const schema = { 21 | type: String, 22 | isActive: Boolean, 23 | name: String, 24 | }; 25 | 26 | test('Hook.pre.save', async () => { 27 | const UserSchema = new Schema(schema); 28 | UserSchema.pre('save', async (document) => { 29 | document.name = 'async pre save'; 30 | }); 31 | 32 | const UserModel = model('User', UserSchema); 33 | await startInTest(getDefaultInstance()); 34 | const result = await UserModel.create(accessDoc2); 35 | const userSaved = await UserModel.findById(result.id); 36 | expect(userSaved.name).toBe('async pre save'); 37 | }); 38 | 39 | test('Hook.post.save', async () => { 40 | const UserSchema = new Schema(schema); 41 | UserSchema.post('save', (document) => { 42 | document.name = 'async post save'; 43 | }); 44 | 45 | const UserModel = model('User', UserSchema); 46 | 47 | await startInTest(getDefaultInstance()); 48 | 49 | const result = await UserModel.create(accessDoc2); 50 | expect(result).toBeDefined(); 51 | expect(result.name).toBe('async post save'); 52 | }); 53 | 54 | test('Hook update', async () => { 55 | const UserSchema = new Schema(schema); 56 | UserSchema.pre('update', (document) => { 57 | document.name = 'async pre update'; 58 | }); 59 | 60 | UserSchema.post('update', (document) => { 61 | document.document = document; 62 | }); 63 | 64 | const UserModel = model('User', UserSchema); 65 | 66 | await startInTest(getDefaultInstance()); 67 | 68 | const user = new UserModel(accessDoc2); 69 | await user.save(); 70 | expect(user.id).toBeDefined(); 71 | expect(user.name).toBe(accessDoc2.name); 72 | await user.save(); 73 | expect(user.name).toBe('async pre update'); 74 | const userUpdated = await UserModel.findById(user.id); 75 | expect(userUpdated.name).toBe('async pre update'); 76 | expect(user.document).toBeDefined(); 77 | expect(user.document.name).toBe('async pre update'); 78 | }); 79 | 80 | test('Hook.pre.remove function', async () => { 81 | const UserSchema = new Schema(schema); 82 | 83 | UserSchema.pre('remove', (document) => { 84 | document.name = 'async pre remove'; 85 | }); 86 | 87 | const UserModel = model('User', UserSchema); 88 | 89 | await startInTest(getDefaultInstance()); 90 | 91 | const user = new UserModel(removeDoc); 92 | await user.save(); 93 | const userSaved = await UserModel.findById(user.id); 94 | expect(userSaved.id).toBeDefined(); 95 | await user.remove(); 96 | expect(user.name).toBe('async pre remove'); 97 | try { 98 | await UserModel.findById(user.id); 99 | } catch (e) { 100 | expect(isDocumentNotFoundError(e)).toBe(true); 101 | } 102 | }); 103 | 104 | test('Hook.pre.validate function', async () => { 105 | const UserSchema = new Schema(schema); 106 | 107 | UserSchema.pre('validate', (document) => { 108 | if (document.name === validateDoc.name) { 109 | const message = `Username '${validateDoc.name}' not allowed`; 110 | throw new Error(message); 111 | } 112 | }); 113 | 114 | const UserModel = model('User', UserSchema); 115 | 116 | await startInTest(getDefaultInstance()); 117 | 118 | const user = new UserModel(validateDoc); 119 | try { 120 | await user.save(); 121 | } catch (e) { 122 | expect((e as Error).message).toBe(`Username '${validateDoc.name}' not allowed`); 123 | } 124 | }); 125 | -------------------------------------------------------------------------------- /__test__/indexes-query.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, model, Schema } from '../src'; 2 | import { delay, startInTest } from './testData'; 3 | 4 | const generateMockData = async () => { 5 | const userSchema = new Schema({ 6 | name: String, 7 | }); 8 | userSchema.queries = { 9 | myPosts: { 10 | of: 'Post', 11 | by: 'user', 12 | }, 13 | }; 14 | const User = model('User', userSchema); 15 | const postSchema = new Schema({ 16 | user: { type: userSchema, ref: 'User' }, 17 | title: String, 18 | body: String, 19 | }); 20 | const Post = model('Post', postSchema); 21 | 22 | await startInTest(getDefaultInstance()); 23 | 24 | const user = new User({ name: 'ottoman-user' }); 25 | await user.save(); 26 | const post = new Post({ 27 | title: 'The new post', 28 | body: 'This is my new post', 29 | user: user.id, 30 | }); 31 | await post.save(); 32 | return { user, post }; 33 | }; 34 | 35 | describe('Mode Query Index Tests', () => { 36 | test('Test schema query structure', async () => { 37 | const { user } = await generateMockData(); 38 | await delay(500); 39 | const results = await user.myPosts(); 40 | expect(results.rows).toBeDefined(); 41 | expect(results.rows.length > 0).toBeTruthy(); 42 | expect(results.rows[0].user).toBe(user.id); 43 | }); 44 | 45 | test('Test schema query: check wrong structure', async () => { 46 | const run = () => { 47 | const userSchema = new Schema({ 48 | name: String, 49 | }); 50 | userSchema.queries = { 51 | myPosts: { 52 | // @ts-ignore 53 | off: 'Post', 54 | by: 'user', 55 | }, 56 | }; 57 | const User = model('User', userSchema); 58 | new User({ name: 'ottoman-user' }); 59 | }; 60 | expect(run).toThrow('The "by" and "of" properties are required to build the queries.'); 61 | }); 62 | 63 | test('Test schema query: not registered model exception', async () => { 64 | const run = () => { 65 | const userSchema = new Schema({ 66 | name: String, 67 | }); 68 | userSchema.queries = { 69 | myPosts: { 70 | of: 'News', 71 | by: 'user', 72 | }, 73 | }; 74 | const User = model('User', userSchema); 75 | new User({ name: 'ottoman-user-test' }); 76 | }; 77 | expect(run).toThrow(`Collection 'News' does not exist.`); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /__test__/indexes.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, getDefaultInstance, ViewIndexOptions } from '../src'; 2 | import { delay, startInTest } from './testData'; 3 | import { BuildIndexQueryError } from '../src/exceptions/ottoman-errors'; 4 | 5 | describe('Indexes', () => { 6 | const UserSchema = new Schema({ 7 | name: String, 8 | email: String, 9 | card: { 10 | cardNumber: String, 11 | zipCode: String, 12 | }, 13 | roles: [{ name: String }], 14 | }); 15 | 16 | UserSchema.index.findN1qlByName = { by: 'name', options: { limit: 4, select: 'name' } }; 17 | UserSchema.index.findN1qlByCardNumber = { by: 'card.cardNumber', type: 'n1ql' }; 18 | UserSchema.index.findN1qlByRoles = { by: 'roles[*].name', type: 'n1ql' }; 19 | 20 | UserSchema.index.findN1qlByNameandEmail = { 21 | by: ['name', 'email'], 22 | options: { limit: 4, select: 'name, email' }, 23 | type: 'n1ql', 24 | }; 25 | UserSchema.index.findByEmail = { by: 'email', type: 'n1ql' }; 26 | UserSchema.index.findByName = { by: 'name', type: 'view' }; 27 | UserSchema.index.findRefName = { by: 'name', type: 'refdoc' }; 28 | 29 | test('Testing indexes', async () => { 30 | const User = model('User', UserSchema); 31 | await startInTest(getDefaultInstance()); 32 | 33 | const userData = { 34 | name: `index`, 35 | email: 'index@email.com', 36 | card: { cardNumber: '424242425252', zipCode: '42424' }, 37 | roles: [{ name: 'admin' }], 38 | }; 39 | const user = new User(userData); 40 | await user.save(); 41 | 42 | await delay(2500); 43 | 44 | const usersN1ql = await User.findN1qlByName(userData.name); 45 | expect(usersN1ql.rows[0].name).toBe(userData.name); 46 | 47 | const usersN1qlByCard = await User.findN1qlByCardNumber(userData.card.cardNumber); 48 | expect(usersN1qlByCard.rows[0].card.cardNumber).toBe(userData.card.cardNumber); 49 | 50 | const usersN1qlByNameAndEmail = await User.findN1qlByNameandEmail([userData.name, userData.email]); 51 | expect(usersN1qlByNameAndEmail.rows[0].name).toBe(userData.name); 52 | expect(usersN1qlByNameAndEmail.rows[0].email).toBe(userData.email); 53 | 54 | const usersRolesN1ql = await User.findN1qlByRoles(userData.roles[0].name); 55 | expect(usersRolesN1ql.rows[0].roles[0].name).toBe(userData.roles[0].name); 56 | 57 | try { 58 | await User.findByName(); 59 | } catch (e) { 60 | expect((e as Error).message).toBe( 61 | `Function 'findByName' received wrong argument value, 'undefined' wasn't expected`, 62 | ); 63 | } 64 | 65 | try { 66 | await User.findByName(['must', 'fail']); 67 | } catch (e) { 68 | expect((e as Error).message).toBe( 69 | `Function 'findByName' received wrong number of arguments, '1:[name]' argument(s) was expected and '2:[must,fail]' were received`, 70 | ); 71 | } 72 | 73 | const viewIndexOptions = new ViewIndexOptions({ limit: 1 }); 74 | const usersView = await User.findByName(userData.name, viewIndexOptions); 75 | expect(usersView).toBeDefined(); 76 | 77 | const userRefdoc = await User.findRefName(userData.name); 78 | expect(userRefdoc.name).toBe(userData.name); 79 | }); 80 | test('Testing indexes -> should throw a BuildIndexQueryError -> more than one wildcard in path ', async () => { 81 | UserSchema.index.findN1qlByRolesException = { by: 'roles[*].name[*].first', type: 'n1ql' }; 82 | try { 83 | const User = model('User', UserSchema); 84 | const userData = { 85 | name: `index`, 86 | email: 'index@email.com', 87 | card: { cardNumber: '424242425252', zipCode: '42424' }, 88 | roles: [{ name: 'admin' }], 89 | }; 90 | const user = new User(userData); 91 | await startInTest(getDefaultInstance()); 92 | await user.save(); 93 | } catch (e) { 94 | const { message } = e as Error; 95 | expect(e).toBeInstanceOf(BuildIndexQueryError); 96 | expect(message).toBe('Cannot create an index with more than one wildcard in path'); 97 | } 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /__test__/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import { getOttomanInstances, Ottoman } from '../src'; 2 | import { bucketName, connectionString, password, username } from './testData'; 3 | 4 | beforeEach(async () => { 5 | let options = {}; 6 | if (process.env.OTTOMAN_LEGACY_TEST) { 7 | options = { collectionName: '_default' }; 8 | } 9 | 10 | const ottoman = new Ottoman(options); 11 | await ottoman.connect({ 12 | password, 13 | username, 14 | connectionString, 15 | bucketName, 16 | }); 17 | }); 18 | 19 | afterEach(async () => { 20 | for (const instance of getOttomanInstances()) { 21 | await instance.close(); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /__test__/let-clause.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, LetExprType, LogicalWhereExpr, Query } from '../src'; 2 | import { startInTest } from './testData'; 3 | import { QueryScanConsistency } from 'couchbase'; 4 | 5 | describe('LET clause', () => { 6 | const selectExpr = 't1.airportname, t1.geo.lat, t1.geo.lon, t1.city, t1.type'; 7 | 8 | const letExpr: LetExprType = { 9 | min_lat: 71, 10 | max_lat: 'ABS(t1.geo.lon)*4+1', 11 | place: `(SELECT RAW t2.country FROM \`travel-sample\` t2 WHERE t2.type = "landmark")`, 12 | }; 13 | const whereExpr: LogicalWhereExpr = { 14 | $and: [ 15 | { 't1.type': 'airport' }, 16 | { 't1.geo.lat': { $gt: { $field: 'min_lat' } } }, 17 | { 't1.geo.lat': { $lt: { $field: 'max_lat' } } }, 18 | { 't1.country': { $in: { $field: 'place' } } }, 19 | ], 20 | }; 21 | 22 | /** 23 | * @example https://docs.couchbase.com/server/current/n1ql/n1ql-language-reference/let.html#examples_section 24 | **/ 25 | test(`-> query builder with LET clause`, () => { 26 | const query = new Query({}, 'travel-sample t1').select(selectExpr).let(letExpr).where(whereExpr).build(); 27 | 28 | expect(query).toBe( 29 | 'SELECT t1.airportname, t1.geo.lat, t1.geo.lon, t1.city, t1.type FROM `travel-sample` t1 LET min_lat=71,max_lat=ABS(t1.geo.lon)*4+1,place=(SELECT RAW t2.country FROM `travel-sample` t2 WHERE t2.type = "landmark") WHERE (t1.type="airport" AND t1.geo.lat>min_lat AND t1.geo.lat query execution with LET clause`, async () => { 33 | const query = new Query({}, 'travel-sample t1').select(selectExpr).let(letExpr).where(whereExpr).build(); 34 | const ottoman = getDefaultInstance(); 35 | await startInTest(ottoman); 36 | 37 | const response = await ottoman.query(query, { scanConsistency: QueryScanConsistency.RequestPlus }); 38 | 39 | expect(response.rows).toStrictEqual([ 40 | { 41 | airportname: 'Wiley Post Will Rogers Mem', 42 | city: 'Barrow', 43 | lat: 71.285446, 44 | lon: -156.766003, 45 | type: 'airport', 46 | }, 47 | { 48 | airportname: 'Dillant Hopkins Airport', 49 | city: 'Keene', 50 | lat: 72.270833, 51 | lon: 42.898333, 52 | type: 'airport', 53 | }, 54 | ]); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /__test__/max-expiry.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, model, Schema, DocumentNotFoundError } from '../src'; 2 | import { delay, startInTest } from './testData'; 3 | 4 | test('Collection with maxExpiry', async () => { 5 | const schema = new Schema({ 6 | name: String, 7 | }); 8 | const collectionName = 'maxExpiryCollection'; 9 | const MaxExpiryModel = model<{ name: string }>(collectionName, schema, { maxExpiry: 5 }); 10 | 11 | await startInTest(getDefaultInstance()); 12 | 13 | const doc = new MaxExpiryModel({ name: 'Max Expiry' }); 14 | await doc.save(); 15 | 16 | expect(doc.id).toBeDefined(); 17 | await delay(6000); 18 | try { 19 | await MaxExpiryModel.findById(doc.id); 20 | } catch (e) { 21 | expect(e).toBeInstanceOf(DocumentNotFoundError); 22 | } 23 | }); 24 | 25 | test('Document with maxExpiry, not setted in collection', async () => { 26 | const schema = new Schema({ 27 | name: String, 28 | }); 29 | const collectionName = 'maxExpiryDocument'; 30 | const MaxExpiryModel = model<{ name: string }>(collectionName, schema); 31 | 32 | await startInTest(getDefaultInstance()); 33 | 34 | const doc = new MaxExpiryModel({ name: 'Max Expiry Document Fixed' }); 35 | await doc.save(false, { maxExpiry: 5 }); 36 | 37 | expect(doc.id).toBeDefined(); 38 | await delay(7000); 39 | try { 40 | await MaxExpiryModel.findById(doc.id); 41 | } catch (e) { 42 | expect(e).toBeInstanceOf(DocumentNotFoundError); 43 | } 44 | }); 45 | 46 | /** 47 | * maxExpiry in Document and Collection only work as expected if Document value is lower than Collection 48 | * If Collection maxExpiry value is lower than the Document maxExpiry, 49 | * then the document will be removed by the Collection maxExpiry clean up routine 50 | **/ 51 | test('Collection and Document with maxExpiry', async () => { 52 | const schema = new Schema({ 53 | name: String, 54 | }); 55 | const collectionName = 'maxExpiryCollectionAndDocument'; 56 | const MaxExpiryModel = model<{ name: string; id?: string }>(collectionName, schema, { maxExpiry: 15 }); 57 | 58 | await startInTest(getDefaultInstance()); 59 | 60 | const doc = new MaxExpiryModel({ name: 'Max Expiry' }); 61 | await doc.save(false, { maxExpiry: 5 }); 62 | expect(doc.id).toBeDefined(); 63 | await delay(6000); 64 | try { 65 | await MaxExpiryModel.findById(doc.id!); 66 | } catch (e) { 67 | expect(e).toBeInstanceOf(DocumentNotFoundError); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /__test__/model-schema-ref-check.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, model, Schema, SearchConsistency } from '../src'; 2 | import { startInTest } from './testData'; 3 | import { InvalidModelReferenceError } from '../src/exceptions/ottoman-errors'; 4 | 5 | const accessDoc = { 6 | type: 'enforceRefCheck', 7 | isActive: false, 8 | name: 'check', 9 | }; 10 | 11 | const CardSchema = new Schema({ 12 | cardNumber: String, 13 | zipCode: String, 14 | }); 15 | 16 | test('save with schema enforceRefCheck option', async () => { 17 | model('Card', CardSchema); 18 | 19 | const schema = new Schema( 20 | { 21 | type: String, 22 | isActive: Boolean, 23 | name: String, 24 | card: { type: CardSchema, ref: 'Card' }, 25 | }, 26 | { enforceRefCheck: 'throw' }, 27 | ); 28 | const User = model('User', schema); 29 | 30 | await startInTest(getDefaultInstance()); 31 | 32 | const user = new User(accessDoc); 33 | user.card = 'no-existing-ID'; 34 | try { 35 | await user.save(); 36 | } catch (e) { 37 | expect(e).toBeInstanceOf(InvalidModelReferenceError); 38 | } 39 | }); 40 | 41 | test('findById with schema enforceRefCheck option', async () => { 42 | model('Card', CardSchema); 43 | 44 | const schema = new Schema({ 45 | type: String, 46 | isActive: Boolean, 47 | name: String, 48 | card: { type: CardSchema, ref: 'Card' }, 49 | }); 50 | const User = model('User', schema); 51 | 52 | await startInTest(getDefaultInstance()); 53 | 54 | const user = new User(accessDoc); 55 | user.card = 'findById-no-existing-ID'; 56 | await user.save(); 57 | try { 58 | await User.findById(user.id, { populate: '*', enforceRefCheck: 'throw' }); 59 | } catch (e) { 60 | expect(e).toBeInstanceOf(InvalidModelReferenceError); 61 | } 62 | }); 63 | 64 | test('find with schema enforceRefCheck option', async () => { 65 | model('Card', CardSchema); 66 | 67 | const schema = new Schema({ 68 | type: String, 69 | isActive: Boolean, 70 | name: String, 71 | card: { type: CardSchema, ref: 'Card' }, 72 | }); 73 | const User = model('User', schema); 74 | 75 | await startInTest(getDefaultInstance()); 76 | 77 | const user = new User(accessDoc); 78 | user.card = 'find-no-existing-ID'; 79 | await user.save(); 80 | try { 81 | await User.find({ id: user.id }, { populate: '*', enforceRefCheck: 'throw' }); 82 | } catch (e) { 83 | expect(e).toBeInstanceOf(InvalidModelReferenceError); 84 | } 85 | }); 86 | 87 | test('find with schema enforceRefCheck option set to true', async () => { 88 | model('Card', CardSchema); 89 | 90 | const schema = new Schema({ 91 | type: String, 92 | isActive: Boolean, 93 | name: String, 94 | card: { type: CardSchema, ref: 'Card' }, 95 | }); 96 | 97 | const User = model('User', schema); 98 | await startInTest(getDefaultInstance()); 99 | 100 | const user = new User(accessDoc); 101 | user.card = 'find-no-existing-ID-true'; 102 | await user.save(); 103 | jest.spyOn(console, 'warn').mockImplementation(); 104 | await User.find({ id: user.id }, { populate: '*', enforceRefCheck: true, consistency: SearchConsistency.LOCAL }); 105 | expect(console.warn).toHaveBeenCalledWith( 106 | expect.stringContaining( 107 | `Reference to 'card' can't be populated cause document with id 'find-no-existing-ID-true' not found!`, 108 | ), 109 | ); 110 | }); 111 | -------------------------------------------------------------------------------- /__test__/model-utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { model } from '../src'; 2 | import { arrayDiff } from '../src/model/utils/array-diff'; 3 | 4 | describe('Test Model utilities methods', () => { 5 | test('model.toObject', () => { 6 | const Cat = model('Cat', { name: String }); 7 | const catObject = { name: 'figaro' }; 8 | const cat = new Cat(catObject); 9 | expect(JSON.stringify(cat.toObject())).toStrictEqual(JSON.stringify(catObject)); 10 | }); 11 | 12 | test('model.toJson', async () => { 13 | const Cat = model('Cat', { name: String }); 14 | const catObject = { name: 'figaro' }; 15 | const cat = new Cat(catObject); 16 | expect(JSON.stringify(cat)).toBe(JSON.stringify(catObject)); 17 | }); 18 | 19 | test('arrayDiff', () => { 20 | const arr1 = [1, 2, 3]; 21 | const arr2 = [3, 4]; 22 | expect(arrayDiff(arr1, arr2)).toStrictEqual([1, 2]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__test__/nested-model-key.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, model } from '../src'; 2 | import { startInTest } from './testData'; 3 | 4 | const accessDoc = { 5 | type: 'airlineR', 6 | isActive: false, 7 | name: 'Ottoman Access', 8 | }; 9 | 10 | const accessDoc2 = { 11 | type: 'airlineNested', 12 | isActive: false, 13 | name: 'Ottoman Access Nested', 14 | }; 15 | 16 | const updateDoc = { 17 | isActive: true, 18 | }; 19 | 20 | const replaceDoc = { 21 | type: 'airlineNested Replace', 22 | isActive: false, 23 | }; 24 | 25 | const schema = { 26 | type: String, 27 | isActive: Boolean, 28 | name: String, 29 | }; 30 | 31 | interface IUser { 32 | type: string; 33 | name?: string; 34 | isActive?: boolean; 35 | } 36 | 37 | describe('nested model key', function () { 38 | test('UserModel.create Creating a document', async () => { 39 | const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); 40 | await startInTest(getDefaultInstance()); 41 | const result = await UserModel.create(accessDoc); 42 | expect(result.id).toBeDefined(); 43 | }); 44 | 45 | test('UserModel.findById Get a document', async () => { 46 | const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); 47 | await startInTest(getDefaultInstance()); 48 | const result = await UserModel.create(accessDoc); 49 | const user = await UserModel.findById(result.id); 50 | expect(user.name).toBeDefined(); 51 | }); 52 | 53 | test('UserModel.update -> Update a document', async () => { 54 | const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); 55 | await startInTest(getDefaultInstance()); 56 | const result = await UserModel.create(accessDoc); 57 | await UserModel.updateById(result.id, updateDoc); 58 | const user = await UserModel.findById(result.id); 59 | expect(user.isActive).toBe(true); 60 | }); 61 | 62 | test('UserModel.replace Replace a document', async () => { 63 | const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); 64 | await startInTest(getDefaultInstance()); 65 | const result = await UserModel.create(accessDoc); 66 | await UserModel.replaceById(result.id, replaceDoc); 67 | const user = await UserModel.findById(result.id); 68 | expect(user.type).toBe('airlineNested Replace'); 69 | expect(user.name).toBeUndefined(); 70 | }); 71 | 72 | test('Document.save Save and update a document', async () => { 73 | const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); 74 | await startInTest(getDefaultInstance()); 75 | const user = new UserModel(accessDoc2); 76 | const result = await user.save(); 77 | expect(user.id).toBeDefined(); 78 | user.name = 'Instance Edited'; 79 | user.id = result.id; 80 | const updated = await user.save(); 81 | expect(updated.name).toBe('Instance Edited'); 82 | }); 83 | 84 | test('Remove saved document from Model instance', async () => { 85 | const UserModel = model('UserNested', schema, { modelKey: 'metadata.doc_type' }); 86 | await startInTest(getDefaultInstance()); 87 | const user = new UserModel(accessDoc2); 88 | await user.save(); 89 | const removed = await user.remove(); 90 | expect(user.id).toBeDefined(); 91 | expect(removed.cas).toBeDefined(); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /__test__/ottoman-instances.spec.ts: -------------------------------------------------------------------------------- 1 | import { getModelMetadata, Ottoman } from '../src'; 2 | import { connectionString, username, connectUri, bucketName, password } from './testData'; 3 | import { isModel } from '../src/utils/is-model'; 4 | import { OttomanError } from '../src/exceptions/ottoman-errors'; 5 | 6 | describe('Test ottoman instances', () => { 7 | test('Multiple instances with string param', async () => { 8 | const instance2 = new Ottoman(); 9 | await instance2.connect(connectUri); 10 | expect(instance2.bucket).toBeDefined(); 11 | await instance2.close(); 12 | }); 13 | 14 | test('Multiple instances with object param', async () => { 15 | const instance3 = new Ottoman(); 16 | await instance3.connect({ 17 | bucketName, 18 | password, 19 | connectionString, 20 | username, 21 | }); 22 | expect(instance3.bucket).toBeDefined(); 23 | instance3.model('Dog', { name: String }); 24 | const ModelDog = instance3.getModel('Dog'); 25 | await instance3.close(); 26 | expect(isModel(ModelDog)).toBe(true); 27 | }); 28 | 29 | test('Get default collection', async () => { 30 | const instance = new Ottoman(); 31 | await instance.connect(connectUri); 32 | const defaultCollection = instance.getCollection(); 33 | expect(defaultCollection.name).toBe(''); 34 | await instance.close(); 35 | }); 36 | 37 | test('Get collection by name', async () => { 38 | const instance = new Ottoman(); 39 | await instance.connect(connectUri); 40 | const collectionName = 'test'; 41 | const testCollection = instance.getCollection(collectionName); 42 | expect(testCollection.name).toBe(collectionName); 43 | await instance.close(); 44 | }); 45 | 46 | test('Get cluster -> throw error', () => { 47 | const instance = new Ottoman(); 48 | try { 49 | instance.cluster; 50 | } catch (e) { 51 | const { message } = e as Error; 52 | expect(e).toBeInstanceOf(OttomanError); 53 | expect(message).toBe('No active connection detected, please try to connect.'); 54 | } 55 | }); 56 | 57 | test('model -> throw error already been registered', () => { 58 | const instance = new Ottoman(); 59 | instance.model('User', { name: String }); 60 | 61 | try { 62 | instance.model('User', { lastname: String }); 63 | } catch (e) { 64 | const { message } = e as Error; 65 | expect(e).toBeInstanceOf(OttomanError); 66 | expect(message).toBe(`A model with name 'User' has already been registered.`); 67 | } 68 | }); 69 | 70 | test('Change idKey at global level', async () => { 71 | const idKey = '_id'; 72 | const instance = new Ottoman({ idKey }); 73 | await instance.connect({ 74 | bucketName, 75 | password, 76 | connectionString, 77 | username, 78 | }); 79 | expect(instance.bucket).toBeDefined(); 80 | instance.model('Dog', { name: String }); 81 | const ModelDog = instance.getModel('Dog'); 82 | const metadata = getModelMetadata(ModelDog); 83 | expect(metadata.ID_KEY).toBe(idKey); 84 | await instance.close(); 85 | expect(isModel(ModelDog)).toBe(true); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /__test__/query-array.spec.ts: -------------------------------------------------------------------------------- 1 | import { model, Schema, SearchConsistency, getDefaultInstance, set, Query } from '../src'; 2 | import { startInTest } from './testData'; 3 | 4 | describe('Query Builder Array', () => { 5 | const houseSchema = new Schema({ 6 | title: String, 7 | address: { 8 | line1: String, 9 | line2: String, 10 | state: String, 11 | }, 12 | numbers: [Number], 13 | }); 14 | 15 | test('Basics', async () => { 16 | const House = model('House', houseSchema); 17 | await startInTest(getDefaultInstance()); 18 | 19 | const house = new House({ 20 | title: 'Beach House', 21 | address: { line1: 'miami beach', state: 'FL' }, 22 | numbers: [1, 5, 10, 156788], 23 | }); 24 | await house.save(); 25 | 26 | const house2 = new House({ 27 | title: 'City House', 28 | address: { line1: 'city house', state: 'FL' }, 29 | numbers: [1, 5], 30 | }); 31 | await house2.save(); 32 | 33 | const house3 = new House({ 34 | title: 'Highway House', 35 | address: { line1: 'Highway house', state: 'FL' }, 36 | numbers: [1, 5, 4000000], 37 | }); 38 | await house3.save(); 39 | 40 | const filter = { 41 | $any: { 42 | $expr: [{ search: { $in: 'numbers' } }], 43 | $satisfies: { search: { $gte: 5, $lte: 15 } }, 44 | }, 45 | }; 46 | 47 | const results = await House.find(filter, { sort: { 'numbers[-1]': 'DESC' }, consistency: SearchConsistency.LOCAL }); 48 | expect(results.rows.length).toBeGreaterThanOrEqual(1); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /__test__/query-builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildIndexExpr, 3 | buildSelectExpr, 4 | buildWhereClauseExpr, 5 | IIndexOnParams, 6 | IIndexWithParams, 7 | ISelectType, 8 | LogicalWhereExpr, 9 | QueryOperatorNotFoundException, 10 | SelectClauseException, 11 | WhereClauseException, 12 | } from '../src'; 13 | import { CollectionInWithinExceptions } from '../src/query/exceptions'; 14 | 15 | describe('Test Query Builder functions', () => { 16 | test('Check select clause parameter types', async () => { 17 | const dist: ISelectType = { 18 | $distinct: { 19 | $raw: { 20 | $count: { 21 | $field: { 22 | name: 'ottoman', 23 | }, 24 | as: 'odm', 25 | }, 26 | }, 27 | }, 28 | }; 29 | 30 | expect(buildSelectExpr('', dist)).toStrictEqual('DISTINCT RAW COUNT(ottoman) AS odm'); 31 | }); 32 | test('Verify the exception throwing, if there is an error in the SELECT expression.', async () => { 33 | const dist: ISelectType = { 34 | // @ts-ignore 35 | $raw1: { 36 | $count: { 37 | $field: { 38 | name: 'ottoman', 39 | }, 40 | as: 'odm', 41 | }, 42 | }, 43 | }; 44 | const run = () => buildSelectExpr('', dist); 45 | expect(run).toThrow(SelectClauseException); 46 | }); 47 | test('Check the exception WHERE with an operator not found', async () => { 48 | const expr_where: LogicalWhereExpr = { address: { $nill: '%57-59%' } }; 49 | 50 | const run = () => buildWhereClauseExpr('', expr_where); 51 | expect(run).toThrow(QueryOperatorNotFoundException); 52 | }); 53 | 54 | test('Check the exception WHERE', async () => { 55 | const expr_where: LogicalWhereExpr = { 56 | // @ts-ignore 57 | $not: { address: { $like: '%57-59%' }, free_breakfast: true, free_lunch: [1] }, 58 | }; 59 | 60 | const run = () => buildWhereClauseExpr('', expr_where); 61 | expect(run).toThrow(WhereClauseException); 62 | }); 63 | 64 | test('Check WHERE clause parameter types', async () => { 65 | const where: LogicalWhereExpr = { 66 | $or: [{ price: { $gt: 1.99, $isNotNull: true } }, { auto: { $gt: 10 } }, { amount: 10 }], 67 | $and: [ 68 | { price2: { $gt: 1.99, $isNotNull: true } }, 69 | { $or: [{ price3: { $gt: 1.99, $isNotNull: true } }, { id: '20' }] }, 70 | ], 71 | }; 72 | expect(buildWhereClauseExpr('', where)).toStrictEqual( 73 | '((price>1.99 AND price IS NOT NULL) OR auto>10 OR amount=10) AND ((price2>1.99 AND price2 IS NOT NULL) AND ((price3>1.99 AND price3 IS NOT NULL) OR id="20"))', 74 | ); 75 | }); 76 | 77 | test('Check WHERE NOT operator', async () => { 78 | const where: LogicalWhereExpr = { 79 | $and: [ 80 | { 81 | $not: [ 82 | { price: { $gt: 1.99 } }, 83 | { auto: { $gt: 10 } }, 84 | { amount: 10 }, 85 | { $or: [{ type: 'hotel' }, { type: 'landmark' }, { $not: [{ price: 10 }] }] }, 86 | ], 87 | }, 88 | { id: 8000 }, 89 | ], 90 | }; 91 | expect(buildWhereClauseExpr('', where)).toStrictEqual( 92 | '(NOT (price>1.99 AND auto>10 AND amount=10 AND (type="hotel" OR type="landmark" OR NOT (price=10))) AND id=8000)', 93 | ); 94 | }); 95 | test('Check the parameters of the INDEX clause', async () => { 96 | const expr_where: LogicalWhereExpr = { 'travel-sample.callsign': { $like: '%57-59%' } }; 97 | 98 | const on: IIndexOnParams[] = [{ name: 'travel-sample.callsing', sort: 'ASC' }]; 99 | 100 | const withExpr: IIndexWithParams = { 101 | nodes: [], 102 | defer_build: true, 103 | num_replica: 0, 104 | }; 105 | 106 | const index = buildIndexExpr('travel-sample', 'CREATE', 'travel_sample_id_test', on, expr_where, true, withExpr); 107 | 108 | expect(index).toStrictEqual( 109 | 'CREATE INDEX `travel_sample_id_test` ON `travel-sample`(`travel-sample`.callsing["ASC"]) WHERE `travel-sample`.callsign LIKE "%57-59%" USING GSI WITH {"nodes": [],"defer_build": true,"num_replica": 0}', 110 | ); 111 | }); 112 | 113 | test('buildWhereClauseExpr -> should throw a CollectionInWithInExceptions', async () => { 114 | const where: LogicalWhereExpr = { 115 | $any: { 116 | $expr: [{ $dummyIn: { search_expr: 'search', target_expr: 'address' } }], 117 | $satisfies: { address: '10' }, 118 | }, 119 | }; 120 | expect(() => buildWhereClauseExpr('', where)).toThrow(CollectionInWithinExceptions); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /__test__/query-index.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LogicalWhereExpr, 3 | IIndexOnParams, 4 | IIndexWithParams, 5 | Query, 6 | MultipleQueryTypesException, 7 | IndexParamsOnExceptions, 8 | IndexParamsUsingGSIExceptions, 9 | } from '../src'; 10 | 11 | describe('Test Query Builder INDEX clause', () => { 12 | test('Check the INDEX clause of the query builder', async () => { 13 | const expr_where: LogicalWhereExpr = { 'travel-sample.callsign': { $like: '%57-59%' } }; 14 | 15 | const on: IIndexOnParams[] = [{ name: 'travel-sample.callsing' }]; 16 | 17 | const withExpr: IIndexWithParams = { 18 | nodes: ['192.168.1.1:8078', '192.168.1.1:8079'], 19 | defer_build: true, 20 | num_replica: 2, 21 | }; 22 | 23 | const query = new Query({}, 'travel-sample') 24 | .index('CREATE', 'travel_sample_id_test') 25 | .on(on) 26 | .where(expr_where) 27 | .usingGSI() 28 | .with(withExpr) 29 | .build(); 30 | 31 | expect(query).toStrictEqual( 32 | 'CREATE INDEX `travel_sample_id_test` ON `travel-sample`(`travel-sample`.callsing) WHERE `travel-sample`.callsign LIKE "%57-59%" USING GSI WITH {"nodes": ["192.168.1.1:8078","192.168.1.1:8079"],"defer_build": true,"num_replica": 2}', 33 | ); 34 | }); 35 | 36 | test('Check Multiple Query Exceptions with select', () => { 37 | const run = () => new Query({}, 'travel-sample').index('CREATE', 'travel_index').select('*'); 38 | expect(run).toThrow(MultipleQueryTypesException); 39 | }); 40 | 41 | test('Check the DROP INDEX clause of the query builder', async () => { 42 | const query = new Query({}, 'travel-sample').index('DROP', 'travel_sample_id_test').usingGSI().build(); 43 | 44 | expect(query).toStrictEqual('DROP INDEX `travel-sample`.`travel_sample_id_test` USING GSI'); 45 | }); 46 | 47 | test('Check the exception Index Params On Exception', async () => { 48 | const run = () => new Query({}, 'travel-sample').select().on([{ name: 'test', sort: 'DESC' }]); 49 | expect(run).toThrow(IndexParamsOnExceptions); 50 | }); 51 | 52 | test('Check the exception Invalid index name', async () => { 53 | const run = () => new Query({}, 'travel-sample').index('CREATE', 'index-*&'); 54 | expect(run).toThrow( 55 | 'Valid GSI index names can contain any of the following characters: A-Z a-z 0-9 # _, and must start with a letter, [A-Z a-z]', 56 | ); 57 | }); 58 | test('Check Multiple Query Exceptions with index', () => { 59 | const run = () => new Query({}, 'travel-sample').select('*').index('CREATE', 'travel_index'); 60 | expect(run).toThrow(MultipleQueryTypesException); 61 | }); 62 | 63 | test('Check the exception IndexParamsUsingGSIExceptions', () => { 64 | const run = () => new Query({}, 'travel-sample').select('*').usingGSI(); 65 | expect(run).toThrow(IndexParamsUsingGSIExceptions); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /__test__/query-utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseStringSelectExpr, getProjectionFields, Query, escapeReservedWords } from '../src'; 2 | import { escapeFromClause } from '../src/query/utils'; 3 | 4 | describe('Test Query Builder Utils', () => { 5 | test('Test the conversion of select expression into an Array of selection keys', async () => { 6 | const expr = "DISTINCT (RAW (COUNT('address') as addr, address))"; 7 | 8 | const result = parseStringSelectExpr(expr); 9 | 10 | expect(result).toStrictEqual(['addr', 'address']); 11 | }); 12 | 13 | test('Test the conversion of simple string select into an Array of selection keys', async () => { 14 | const expr = 'address'; 15 | 16 | const result = parseStringSelectExpr(expr); 17 | 18 | expect(result).toStrictEqual(['address']); 19 | }); 20 | 21 | test('Test get Projections fields with an empty select', () => { 22 | const result = getProjectionFields('travel-sample', ''); 23 | expect(result.fields).toStrictEqual([]); 24 | expect(result.projection).toBe(`\`travel-sample\`.*`); 25 | }); 26 | 27 | test('Test get Projections fields with select parameters', () => { 28 | const result = getProjectionFields('travel-sample', 'address, type'); 29 | expect(result.fields).toStrictEqual(['address', 'type']); 30 | expect(result.projection).toBe(`address, type,_type`); 31 | }); 32 | 33 | test('Test get Projections fields with an array in the select field', () => { 34 | const result = getProjectionFields('travel-sample', ['address', 'type']); 35 | expect(result.fields).toStrictEqual(['address', 'type']); 36 | expect(result.projection).toBe(`address,type,_type`); 37 | }); 38 | 39 | test('Test get Projections fields using an expression in the select field', () => { 40 | const result = getProjectionFields('travel-sample', [{ $field: 'address' }, { $field: 'type' }]); 41 | expect(result.fields).toStrictEqual(['address', 'type']); 42 | expect(result.projection).toBe(`address,type,_type`); 43 | }); 44 | 45 | test('Test get Projections fields with select using the Query Builder', () => { 46 | const result = getProjectionFields('travel-sample'); 47 | const query = new Query({}, 'travel-sample').select(result.projection).limit(10).build(); 48 | expect(query).toBe(`SELECT \`travel-sample\`.* FROM \`travel-sample\` LIMIT 10`); 49 | }); 50 | 51 | test('Test escape reserved words function', () => { 52 | const expr = escapeReservedWords('travel-sample[0].user.name.permissions[0].name-aux'); 53 | const expr2 = escapeReservedWords('roles.name.permissions-admin'); 54 | const expr3 = escapeReservedWords('travel-sample'); 55 | expect(expr).toStrictEqual('`travel-sample`[0].`user`.name.permissions[0].`name-aux`'); 56 | expect(expr2).toStrictEqual('roles.name.`permissions-admin`'); 57 | expect(expr3).toStrictEqual('`travel-sample`'); 58 | }); 59 | 60 | test('escape fromClause', () => { 61 | const escaped = escapeFromClause('travel-sample'); 62 | expect(escaped).toBe('`travel-sample`'); 63 | 64 | const escaped2 = escapeFromClause('`travel-sample`'); 65 | expect(escaped2).toBe('`travel-sample`'); 66 | 67 | const escapeAlias = escapeFromClause('travel-sample t'); 68 | expect(escapeAlias).toBe('`travel-sample` t'); 69 | 70 | const escapeAlias2 = escapeFromClause('`travel-sample` t'); 71 | expect(escapeAlias2).toBe('`travel-sample` t'); 72 | 73 | const escapedCollection = escapeFromClause('travel-sample._default.users'); 74 | expect(escapedCollection).toBe('`travel-sample`.`_default`.`users`'); 75 | 76 | const escapedCollectionAlias = escapeFromClause('travel-sample._default.users users'); 77 | expect(escapedCollectionAlias).toBe('`travel-sample`.`_default`.`users` users'); 78 | 79 | const escapedCollection2 = escapeFromClause('`travel-sample`._default.users'); 80 | expect(escapedCollection2).toBe('`travel-sample`.`_default`.`users`'); 81 | 82 | const escapedCollection3 = escapeFromClause('`travel-sample`.`_default`.`users` users'); 83 | expect(escapedCollection3).toBe('`travel-sample`.`_default`.`users` users'); 84 | 85 | const escapedCollectionFixExtraBacksticks = escapeFromClause('`travel-sample._default.users users`'); 86 | expect(escapedCollectionFixExtraBacksticks).toBe('`travel-sample`.`_default`.`users` users'); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /__test__/schema-cast.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultInstance, is, model, Schema } from '../src'; 2 | import { ArrayType } from '../src/schema/types'; 3 | import { cast, CAST_STRATEGY } from '../src/utils/cast-strategy'; 4 | import { consistency, startInTest } from './testData'; 5 | 6 | test('cast schema', () => { 7 | const childSchema = new Schema({ name: String, age: Number }); 8 | const schema = new Schema({ 9 | name: String, 10 | age: Number, 11 | isActive: Boolean, 12 | createdAt: Date, 13 | prices: [Number], 14 | child: childSchema, 15 | children: [childSchema], 16 | }); 17 | 18 | const john = cast({ age: 23, createdAt: new Date(), prices: [2, 3.14], child: { name: 'jane', age: 3 } }, schema); 19 | expect(is(john.age, Number)).toBe(true); 20 | expect(is(john.createdAt, Date)).toBe(true); 21 | expect(john.prices[0]).toBe(2); 22 | expect(john.prices[1]).toBe(3.14); 23 | expect(john.child.age).toBe(3); 24 | 25 | const xavier = cast( 26 | { age: '32', createdAt: new Date().toISOString(), prices: ['2', '3.14'], child: { name: 'jane', age: '3' } }, 27 | schema, 28 | ); 29 | expect(is(xavier.age, Number)).toBe(true); 30 | expect(xavier.age).toBe(32); 31 | expect(is(xavier.createdAt, Date)).toBe(true); 32 | expect(xavier.prices[0]).toBe(2); 33 | expect(xavier.prices[1]).toBe(3.14); 34 | expect(xavier.child.age).toBe(3); 35 | 36 | const unCasteableData = { age: 'true', createdAt: 'Invalid Date' }; 37 | const drEmpty = cast(unCasteableData, schema); 38 | expect(drEmpty.age).toBe(undefined); 39 | expect(drEmpty.createdAt).toBe(undefined); 40 | 41 | const drError = cast(unCasteableData, schema, { strategy: CAST_STRATEGY.KEEP }); 42 | expect(drError.age).toBe(unCasteableData.age); 43 | expect(drError.createdAt).toBe(unCasteableData.createdAt); 44 | }); 45 | 46 | test('support array of primitive types', () => { 47 | const schema = new Schema({ 48 | coordinates: { 49 | type: [Number], 50 | required: true, 51 | default: [0], 52 | }, 53 | articles: { default: () => [], type: [{ type: String, ref: 'Article' }] }, 54 | }); 55 | const fields = schema.fields; 56 | expect(fields.coordinates.typeName).toBe(Array.name); 57 | expect((fields.articles as ArrayType).options).toBeDefined(); 58 | expect((fields.articles as ArrayType).options!.default).toBeInstanceOf(Function); 59 | expect((fields.coordinates as ArrayType).itemType.typeName).toBe(Number.name); 60 | }); 61 | 62 | test('test strict schema', () => { 63 | const schema = new Schema({ 64 | name: String, 65 | age: { type: Number }, 66 | }); 67 | 68 | const user = schema.validate({ name: 'testing', age: '45', score: 99 }); 69 | expect(user.age).toBeDefined(); 70 | expect(user.score).toBe(undefined); 71 | }); 72 | 73 | test('test strict schema using default', () => { 74 | const schema = new Schema({ 75 | name: String, 76 | age: { type: Number, default: 15 }, 77 | }); 78 | 79 | const user = schema.validate({ name: 'testing', score: 99 }, { strict: true }); 80 | expect(user.age).toBe(15); 81 | expect(user.score).toBe(undefined); 82 | }); 83 | 84 | test('test strict schema model create', async () => { 85 | const schema = new Schema({ 86 | name: String, 87 | }); 88 | const Model = model('strictSchema', schema); 89 | await startInTest(getDefaultInstance()); 90 | const name = `name-${Date.now()}`; 91 | const doc = new Model({ name, notInSchema: true }); 92 | await doc.save(); 93 | 94 | expect(doc.name).toBe(name); 95 | expect(doc.notInSchema).toBe(undefined); 96 | 97 | const docs = await Model.find({ name }, consistency); 98 | const findDoc = docs.rows[0]; 99 | expect(findDoc.name).toBe(name); 100 | expect(findDoc.notInSchema).toBe(undefined); 101 | }); 102 | 103 | test('test strict false schema model create', async () => { 104 | const schema = new Schema( 105 | { 106 | name: String, 107 | }, 108 | { strict: false }, 109 | ); 110 | const Model = model('strictSchema', schema); 111 | await startInTest(getDefaultInstance()); 112 | const name = `name-strict-${Date.now()}`; 113 | const doc = new Model({ name, notInSchema: true }); 114 | await doc.save(); 115 | 116 | const docs = await Model.find({ name }, consistency); 117 | const findDoc = docs.rows[0]; 118 | await Model.removeMany({ name: { $like: 'name-strict-%' } }); 119 | expect(doc.name).toBe(name); 120 | expect(doc.notInSchema).toBe(true); 121 | expect(findDoc.name).toBe(name); 122 | expect(findDoc.notInSchema).toBe(true); 123 | }); 124 | -------------------------------------------------------------------------------- /__test__/schema.helpers.spec.ts: -------------------------------------------------------------------------------- 1 | import { applyValidator } from '../src/schema/helpers'; 2 | import { ValidationError } from '../src'; 3 | 4 | describe('Schema Helpers', () => { 5 | test('should return void when using ValidatorOption in the validator and the value is valid', () => { 6 | expect(applyValidator('Sample value', { message: 'Only letters', regexp: new RegExp('\\w') })).toBeUndefined(); 7 | }); 8 | test("should return a message when using ValidatorOption in the validator and the value isn't valid", () => { 9 | expect(() => applyValidator('Sample value', { message: 'Only numbers', regexp: new RegExp('\\d') })).toThrow( 10 | new ValidationError('Only numbers'), 11 | ); 12 | }); 13 | test('should return a message when receiving a String as result of the validator function', () => { 14 | const validator = (val) => { 15 | throw new Error(`Value "${val}" not allowed`); 16 | }; 17 | expect(() => applyValidator('Sample value', validator)).toThrow('Value "Sample value" not allowed'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__test__/schema.spec.ts: -------------------------------------------------------------------------------- 1 | import { BuildSchemaError, model, registerGlobalPlugin, Schema } from '../src'; 2 | 3 | describe('Schema', () => { 4 | const schema = new Schema({ name: String }); 5 | describe('Pre-Hooks', () => { 6 | test('should add pre-hooks to action validate', () => { 7 | schema.pre('validate', () => console.log('Yes')); 8 | expect(schema.preHooks.validate).toBeDefined(); 9 | expect(schema.preHooks.validate).toBeInstanceOf(Array); 10 | expect(schema.preHooks.validate[0]).toBeInstanceOf(Function); 11 | }); 12 | test('should add pre-hooks to action save', () => { 13 | schema.pre('save', () => console.log('Yes')); 14 | expect(schema.preHooks.save).toBeDefined(); 15 | expect(schema.preHooks.save).toBeInstanceOf(Array); 16 | expect(schema.preHooks.save[0]).toBeInstanceOf(Function); 17 | }); 18 | test('should add pre-hooks to action remove', () => { 19 | schema.pre('remove', () => console.log('Yes')); 20 | expect(schema.preHooks.remove).toBeDefined(); 21 | expect(schema.preHooks.remove).toBeInstanceOf(Array); 22 | expect(schema.preHooks.remove[0]).toBeInstanceOf(Function); 23 | }); 24 | test('should throw an error when adding the pre-hook to an action not allowed', () => { 25 | // @ts-ignore 26 | expect(() => schema.pre('other', () => console.log('Yes'))).toThrow(BuildSchemaError); 27 | }); 28 | }); 29 | 30 | describe('Post-Hooks', () => { 31 | test('should add post-hooks to action validate', () => { 32 | schema.post('validate', () => console.log('Yes')); 33 | expect(schema.postHooks.validate).toBeDefined(); 34 | expect(schema.postHooks.validate).toBeInstanceOf(Array); 35 | expect(schema.postHooks.validate[0]).toBeInstanceOf(Function); 36 | }); 37 | test('should add post-hooks to action save', () => { 38 | schema.post('save', () => console.log('Yes')); 39 | expect(schema.postHooks.save).toBeDefined(); 40 | expect(schema.postHooks.save).toBeInstanceOf(Array); 41 | expect(schema.postHooks.save[0]).toBeInstanceOf(Function); 42 | }); 43 | test('should add post-hooks to action remove', () => { 44 | schema.post('remove', () => console.log('Yes')); 45 | expect(schema.postHooks.remove).toBeDefined(); 46 | expect(schema.postHooks.remove).toBeInstanceOf(Array); 47 | expect(schema.postHooks.remove[0]).toBeInstanceOf(Function); 48 | }); 49 | test('should throw an error when adding the post-hook to an action not allowed', () => { 50 | // @ts-ignore 51 | expect(() => schema.post('other', () => console.log('Yes'))).toThrow(BuildSchemaError); 52 | }); 53 | }); 54 | describe('Plugins', () => { 55 | test('should apply a plugin', () => { 56 | const fnPlugin = (s) => expect(s).toEqual(schema); 57 | schema.plugin(fnPlugin); 58 | }); 59 | test('Apply global plugin', async () => { 60 | let log1 = false; 61 | let log2 = false; 62 | const logPluginPre1 = (schema) => 63 | schema.pre('save', () => { 64 | log1 = true; 65 | }); 66 | const logPluginPre2 = (schema) => 67 | schema.pre('save', () => { 68 | log2 = true; 69 | }); 70 | registerGlobalPlugin(...[logPluginPre1, logPluginPre2]); 71 | const userSchema = new Schema({ name: String }); 72 | const User = model('User', userSchema); 73 | const user = new User({ name: 'John' }); 74 | await user.save(); 75 | await user.remove(); 76 | expect(log1).toEqual(true); 77 | expect(log2).toEqual(true); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /__test__/schema.validators.spec.ts: -------------------------------------------------------------------------------- 1 | import { addValidators, validate, Schema, BuildSchemaError } from '../src'; 2 | 3 | describe('schema custom validators', () => { 4 | beforeAll(() => { 5 | // Add custom validators 6 | addValidators({ 7 | string: (value) => { 8 | if (typeof value !== 'string') { 9 | throw new Error('Not a string!'); 10 | } 11 | }, 12 | integer: (value) => { 13 | // Numbers are converted to strings before entering validator functions 14 | const intRegex = /^\+?(0|[1-9]\d*)$/; 15 | if (!intRegex.test(String(value))) { 16 | throw new Error('Not an integer!'); 17 | } 18 | }, 19 | email: (val: any) => { 20 | const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 21 | 22 | if (!emailRegex.test(val)) { 23 | // email is not correct 24 | throw new Error('invalid email'); 25 | } 26 | 27 | return true; 28 | }, 29 | }); 30 | }); 31 | test('should validate the data with custom validators', () => { 32 | const TestSchema = new Schema({ 33 | name: { 34 | type: String, 35 | validator: 'string', // reference the customer validator by name 36 | }, 37 | score: { 38 | type: Number, 39 | validator: Schema.validators.integer, // direct ref to validator func 40 | }, 41 | }); 42 | 43 | const data = { name: 'Joseph', score: '1234' }; 44 | expect(validate(data, TestSchema)).toEqual({ name: 'Joseph', score: 1234 }); 45 | }); 46 | 47 | test('should fail to validate bad data with custom validators', () => { 48 | const TestSchema = new Schema({ 49 | name: { 50 | type: String, 51 | validator: 'string', // reference the customer validator by name 52 | }, 53 | score: { 54 | type: Number, 55 | validator: Schema.validators.integer, // direct ref to validator func 56 | }, 57 | }); 58 | 59 | const data = { name: 'Joseph', score: 56.9 }; 60 | expect(() => validate(data, TestSchema)).toThrow(new Error('Not an integer!')); 61 | }); 62 | 63 | test("should throw an error if custom validator doesn't exist", () => { 64 | const schemaDef = { 65 | name: { 66 | type: String, 67 | validator: 'fake', 68 | }, 69 | }; 70 | expect(() => validate({ name: 'John' }, schemaDef)).toThrow( 71 | new Error(`Validator 'fake' for field 'name' does not exist.`), 72 | ); 73 | }); 74 | test("should throw an errors if validators aren't object", () => { 75 | // @ts-ignore 76 | expect(() => addValidators([])).toThrow(new BuildSchemaError('Validators must be an object.')); 77 | }); 78 | 79 | test("should throw an errors if any validator aren't function", () => { 80 | expect(() => 81 | addValidators({ 82 | name: (val) => { 83 | console.log(val); 84 | }, 85 | // @ts-ignore 86 | fails: 'Not valid', 87 | }), 88 | ).toThrow(new BuildSchemaError('Validator object properties must be functions.')); 89 | }); 90 | 91 | test('email validation optional succeed', async () => { 92 | const UserSchema = new Schema({ 93 | firstName: { type: String, required: true }, 94 | lastName: { type: String, required: true }, 95 | email: { type: String, required: true, validator: 'email' }, 96 | altEmail: { type: String, validator: Schema.validators.email }, 97 | }); 98 | 99 | const jane = { 100 | firstName: 'Jane', 101 | lastName: 'Smith', 102 | email: 'user@user.com', 103 | }; 104 | expect(() => validate(jane, UserSchema)).not.toThrow(); 105 | }); 106 | 107 | test('email validation fails', async () => { 108 | const UserSchema = new Schema({ 109 | firstName: { type: String, required: true }, 110 | lastName: { type: String, required: true }, 111 | email: { type: String, required: true, validator: 'email' }, 112 | altEmail: { type: String, validator: Schema.validators.email }, 113 | }); 114 | 115 | const jane = { 116 | firstName: 'Jane', 117 | lastName: 'Smith', 118 | email: 'user@user.com', 119 | altEmail: 'abc', 120 | }; 121 | expect(() => validate(jane, UserSchema)).toThrow(); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /__test__/testData.ts: -------------------------------------------------------------------------------- 1 | import { LogicalWhereExpr, Ottoman, SearchConsistency, FindOptions, ModelTypes } from '../src'; 2 | 3 | export const bucketName = 'travel-sample'; 4 | export const username = 'Administrator'; 5 | export const password = 'password'; 6 | export const connectionString = 'couchbase://127.0.0.1'; 7 | export const connectUri = `${connectionString}/${bucketName}@${username}:${password}`; 8 | 9 | export const delay = (timems: number): Promise => 10 | new Promise((resolve) => { 11 | setTimeout(() => { 12 | resolve(true); 13 | }, timems); 14 | }); 15 | 16 | export const startInTest = async (ottoman: Ottoman): Promise => { 17 | await ottoman.start(); 18 | return true; 19 | }; 20 | 21 | export const cleanUp = ( 22 | model: ModelTypes, 23 | query: LogicalWhereExpr = { _type: model.collectionName }, 24 | options: FindOptions = { consistency: SearchConsistency.LOCAL }, 25 | ) => { 26 | return model.removeMany(query, options); 27 | }; 28 | 29 | export const consistency = { consistency: SearchConsistency.LOCAL }; 30 | -------------------------------------------------------------------------------- /__test__/transaction-indexes.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, getDefaultInstance, SearchConsistency } from '../src'; 2 | import { delay, startInTest } from './testData'; 3 | 4 | test('Testing indexes', async () => { 5 | const UserSchema = new Schema({ 6 | name: String, 7 | email: String, 8 | card: { 9 | cardNumber: String, 10 | zipCode: String, 11 | }, 12 | roles: [{ name: String }], 13 | }); 14 | 15 | UserSchema.index.findN1qlByName = { by: 'name', options: { limit: 4, select: 'name' } }; 16 | 17 | const User = model('TransactionUser7', UserSchema); 18 | const ottoman = getDefaultInstance(); 19 | await startInTest(ottoman); 20 | 21 | const userData = { 22 | name: `index`, 23 | email: 'index@email.com', 24 | card: { cardNumber: '424242425252', zipCode: '42424' }, 25 | roles: [{ name: 'admin' }], 26 | }; 27 | 28 | try { 29 | await ottoman.$transactions(async (ctx) => { 30 | await User.create(userData, { transactionContext: ctx }); 31 | 32 | await delay(2500); 33 | 34 | const usersN1ql = await User.findN1qlByName(userData.name, { transactionContext: ctx }); 35 | expect(usersN1ql.rows[0].name).toBe(userData.name); 36 | }); 37 | } catch (e) { 38 | console.log(e); 39 | } 40 | 41 | const usersN1ql = await User.findN1qlByName(userData.name, { consistency: SearchConsistency.LOCAL }); 42 | expect(usersN1ql.rows[0].name).toBe(userData.name); 43 | }); 44 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 60..90 3 | round: down 4 | precision: 2 5 | status: 6 | project: 7 | default: 8 | informational: true 9 | patch: 10 | default: 11 | informational: true 12 | codecov: 13 | allow_pseudo_compare: False -------------------------------------------------------------------------------- /docusaurus/.env.example: -------------------------------------------------------------------------------- 1 | ALGOLIA_APP_ID= 2 | ALGOLIA_API_KEY= 3 | -------------------------------------------------------------------------------- /docusaurus/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | .env 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /docusaurus/CNAME: -------------------------------------------------------------------------------- 1 | ottomanjs.com -------------------------------------------------------------------------------- /docusaurus/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | #### Environment Variables 20 | The `.env.example` file shows the required environment variables that must be replaced in order for Algolia DocSearch to work locally. 21 | 22 | ### Build 23 | 24 | ``` 25 | $ yarn build 26 | ``` 27 | 28 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 29 | 30 | ### Deployment 31 | 32 | Using SSH: 33 | 34 | ``` 35 | $ USE_SSH=true yarn deploy 36 | ``` 37 | 38 | Not using SSH: 39 | 40 | ``` 41 | $ GIT_USER= yarn deploy 42 | ``` 43 | 44 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 45 | -------------------------------------------------------------------------------- /docusaurus/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docusaurus/docs/advanced/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Advanced" 2 | position: -2 -------------------------------------------------------------------------------- /docusaurus/docs/advanced/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/advanced/create.png -------------------------------------------------------------------------------- /docusaurus/docs/advanced/findById.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/advanced/findById.png -------------------------------------------------------------------------------- /docusaurus/docs/advanced/mongodb-to-couchbase.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/advanced/mongodb-to-couchbase.jpg -------------------------------------------------------------------------------- /docusaurus/docs/advanced/ottoman-couchbase-node-sdk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/advanced/ottoman-couchbase-node-sdk.jpg -------------------------------------------------------------------------------- /docusaurus/docs/basic/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Basic" 2 | position: -3 -------------------------------------------------------------------------------- /docusaurus/docs/basic/connection-anatomy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/basic/connection-anatomy.png -------------------------------------------------------------------------------- /docusaurus/docs/basic/how-to-use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/basic/how-to-use.png -------------------------------------------------------------------------------- /docusaurus/docs/basic/howToUse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/basic/howToUse.jpg -------------------------------------------------------------------------------- /docusaurus/docs/basic/install-screen-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/basic/install-screen-sm.png -------------------------------------------------------------------------------- /docusaurus/docs/cli/_category_.yml: -------------------------------------------------------------------------------- 1 | label: "Command Line Tool" 2 | position: -1 -------------------------------------------------------------------------------- /docusaurus/docs/cli/cli.md: -------------------------------------------------------------------------------- 1 | # Ottoman CLI 2 | 3 | ## Goals: 4 | - Provide some useful tools while working with Ottoman. 5 | - Speed up the Ottoman setup process. 6 | - Help keep databases in sync across Development and Production environments. 7 | - Automate Ottoman's setup process. 8 | 9 | ## Install 10 | ```shell 11 | npm install -g ottoman-cli 12 | ``` 13 | 14 | ## Usage: 15 | ```shell 16 | ottoman-cli generate 17 | ``` 18 | or 19 | ```shell 20 | npx ottoman-cli generate 21 | ``` 22 | 23 | ## Commands: 24 | - `generate` 25 | - `migrate` 26 | 27 | ### Generate command 28 | 29 | The Ottoman CLI's `generate` command will bootstrap an Ottoman app for you in no time! 30 | Simply follow the steps in the wizard. Happy Coding! 31 | 32 | ```shell 33 | ottoman-cli generate 34 | ``` 35 | 36 | ![generate.png](generate.png) 37 | 38 | ### Migrate command 39 | 40 | ```shell 41 | ottoman-cli migrate 42 | ``` 43 | 44 | The Ottoman CLI's `migrate` command will sync your database with the information took from your models. 45 | Scopes, Collections and Indexes will be created automatically after run the `migrate` command. 46 | 47 | Constraints: 48 | 1. You should build your app before run the `migrate` command 49 | ```shell 50 | npm run build 51 | ottoman-cli migrate 52 | ``` 53 | 2. env variables should point to the built code 54 | 55 | Correct 56 | ```dotenv 57 | OTTOMAN_CLI_MODEL_PATTERN="dist/**/*.model.js" 58 | ``` 59 | Incorrect 60 | ```dotenv 61 | OTTOMAN_CLI_MODEL_PATTERN="src/**/*.model.ts" 62 | ``` 63 | 3. OTTOMAN_CLI_ENTRY="dist/config/ottoman.js" entry point file should export an `ottoman` instance 64 | ```ts 65 | import { Ottoman } from "ottoman"; 66 | 67 | const ottoman = new Ottoman(...); 68 | 69 | export { ottoman }; 70 | ``` 71 | -------------------------------------------------------------------------------- /docusaurus/docs/cli/generate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/docs/cli/generate.png -------------------------------------------------------------------------------- /docusaurus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ottoman-documentation", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "generate": "yarn install && yarn build && cp -R build ../docs && cp CNAME ../docs", 14 | "write-translations": "docusaurus write-translations", 15 | "write-heading-ids": "docusaurus write-heading-ids" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.4.1", 19 | "@docusaurus/preset-classic": "2.4.1", 20 | "@mdx-js/react": "^1.6.22", 21 | "clsx": "^1.2.1", 22 | "got": "^14.0.0", 23 | "prism-react-renderer": "^1.3.5", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "trim": "^1.0.1" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/module-type-aliases": "2.2.0", 30 | "docusaurus-plugin-typedoc": "^0.17.5", 31 | "dotenv": "^16.1.4", 32 | "typedoc": "^0.23.21", 33 | "typedoc-plugin-markdown": "^3.13.6" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.5%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "engines": { 48 | "node": ">=16.14" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docusaurus/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /docusaurus/src/components/HomepageFeatures/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import styles from './styles.module.css'; 5 | import GenerateImg from '@site/static/img/generate.png'; 6 | 7 | const FeatureList = [ 8 | { 9 | title: 'Powered by Couchbase', 10 | Svg: require('@site/static/img/logo.svg').default, 11 | description: ( 12 | <> 13 | Couchbase is JSON database that excels in high volume transactions, making Ottoman a powerful tool to handle 14 | your data. 15 | 16 | ), 17 | }, 18 | { 19 | title: 'Fast', 20 | Svg: require('@site/static/img/fast.svg').default, 21 | description: ( 22 | <> 23 | As Couchbase has a built-in managed cache to enable a memory-first architecture. Read and write operations run 24 | at the speed of RAM. 25 | 26 | ), 27 | }, 28 | { 29 | title: 'Familiar', 30 | Svg: require('@site/static/img/familiar.svg').default, 31 | description: <>Ottoman brings together the best of NoSQL document databases and relational databases., 32 | }, 33 | ]; 34 | 35 | function Feature({ Svg, title, description }) { 36 | return ( 37 | <> 38 |
39 |
40 | 41 |
42 |
43 |

{title}

44 |

{description}

45 |
46 |
47 | 48 | ); 49 | } 50 | 51 | export default function HomepageFeatures() { 52 | return ( 53 | <> 54 |
55 |
56 |
57 |

58 | Spin up your project in a few steps using the CLI 59 |

60 |
61 |
62 |
63 |
64 |             $ npx ottoman-cli generate
65 |           
66 |
67 |
68 | 69 |
70 |
71 |
72 |
73 |
74 | {FeatureList.map((props, idx) => ( 75 | 76 | ))} 77 |
78 |
79 |
80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /docusaurus/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | margin-top: 32px; 7 | } 8 | 9 | .featureSvg { 10 | height: 200px; 11 | width: 200px; 12 | } 13 | 14 | .cliIntructions { 15 | padding: 8px 16px; 16 | background-color: silver; 17 | border-radius: 5px; 18 | } 19 | -------------------------------------------------------------------------------- /docusaurus/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /docusaurus/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import Layout from '@theme/Layout'; 6 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 |

{siteConfig.title}

16 |

{siteConfig.tagline}

17 |
18 | 19 | Get started 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export default function Home() { 28 | const { siteConfig } = useDocusaurusContext(); 29 | return ( 30 | 31 | 32 |
33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /docusaurus/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 2rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | background-color: #303846 !important; 12 | } 13 | 14 | @media screen and (max-width: 996px) { 15 | .heroBanner { 16 | padding: 2rem; 17 | } 18 | } 19 | 20 | .buttons { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | -------------------------------------------------------------------------------- /docusaurus/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docusaurus/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/static/.nojekyll -------------------------------------------------------------------------------- /docusaurus/static/img/couchbase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/static/img/couchbase.png -------------------------------------------------------------------------------- /docusaurus/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/static/img/favicon.ico -------------------------------------------------------------------------------- /docusaurus/static/img/generate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/node-ottoman/f205b2c60146abe04165e78659c16ce0d15a4164/docusaurus/static/img/generate.png -------------------------------------------------------------------------------- /docusaurus/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | setupFilesAfterEnv: ['./__test__/jest.setup.ts'], 5 | collectCoverageFrom: ['src/**/*.ts', '!src/**/*.interface.ts', '!src/**/*.types.ts', '!**/node_modules/**'], 6 | testTimeout: 80000, 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ottoman", 3 | "version": "2.5.2", 4 | "main": "lib/index.js", 5 | "types": "lib/types/index.d.ts", 6 | "description": "Ottoman Couchbase ODM", 7 | "keywords": [ 8 | "couchbase", 9 | "odm", 10 | "nosql", 11 | "json", 12 | "document", 13 | "model", 14 | "schema", 15 | "database" 16 | ], 17 | "license": "Apache-2.0", 18 | "scripts": { 19 | "build": "rimraf ./lib && tsc", 20 | "build:test": "yarn build && yarn test", 21 | "dev": "tsc --watch --incremental", 22 | "docs": "cd docusaurus && yarn generate", 23 | "docs:start": "cd docusaurus && yarn start", 24 | "is:ready": "yarn lint && yarn build && yarn test:ready", 25 | "lint": "eslint '*/**/*.ts' --ignore-pattern '/lib/*' --quiet --fix", 26 | "test": "jest --clearCache && jest -i", 27 | "test:legacy": "jest --clearCache && OTTOMAN_LEGACY_TEST=1 jest -i", 28 | "test:coverage": "jest --clearCache && jest -i --coverage", 29 | "test:dev": "jest --watch", 30 | "test:ready": "jest --clearCache && jest -i --coverage", 31 | "prepare": "husky install" 32 | }, 33 | "engines": { 34 | "node": ">=8.0.0" 35 | }, 36 | "husky": { 37 | "hooks": { 38 | "pre-commit": "lint-staged" 39 | } 40 | }, 41 | "lint-staged": { 42 | "*.{js,ts,tsx}": [ 43 | "eslint --fix" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@types/jest": "29.2.4", 48 | "@types/node": "16.4.14", 49 | "@typescript-eslint/eslint-plugin": "5.45.1", 50 | "@typescript-eslint/parser": "5.45.1", 51 | "eslint": "8.29.0", 52 | "eslint-config-prettier": "8.5.0", 53 | "eslint-plugin-prettier": "4.2.1", 54 | "husky": "8.0.2", 55 | "jest": "29.3.1", 56 | "lint-staged": "13.1.0", 57 | "prettier": "2.8.0", 58 | "pretty-quick": "3.1.3", 59 | "rimraf": "3.0.2", 60 | "ts-jest": "29.0.3", 61 | "typescript": "4.9.3" 62 | }, 63 | "dependencies": { 64 | "couchbase": "4.4.6", 65 | "jsonpath": "1.1.1", 66 | "lodash": "4.17.21", 67 | "uuid": "9.0.0" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "http://github.com/couchbaselabs/node-ottoman.git" 72 | }, 73 | "config": { 74 | "commitizen": { 75 | "path": "./node_modules/cz-conventional-changelog" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['master'], 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | '@semantic-release/changelog', 7 | [ 8 | '@semantic-release/git', 9 | { 10 | assets: ['CHANGELOG.md'], 11 | }, 12 | ], 13 | '@semantic-release/github', 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /src/couchbase.ts: -------------------------------------------------------------------------------- 1 | import * as Couchbase from 'couchbase'; 2 | export { Couchbase }; 3 | 4 | export { 5 | BucketSettings, 6 | CreateBucketOptions, 7 | CertificateAuthenticator, 8 | ICreateBucketSettings, 9 | BucketManager, 10 | CollectionManager, 11 | NodeCallback, 12 | Bucket, 13 | QueryIndexManager, 14 | Cluster, 15 | DropCollectionOptions, 16 | CreateScopeOptions, 17 | CreateCollectionOptions, 18 | DropScopeOptions, 19 | DropBucketOptions, 20 | Collection, 21 | QueryOptions, 22 | QueryProfileMode, 23 | MutationState, 24 | QueryScanConsistency, 25 | SearchQuery, 26 | SearchScanConsistency, 27 | TermSearchFacet, 28 | TransactionAttemptContext, 29 | } from 'couchbase'; 30 | -------------------------------------------------------------------------------- /src/exceptions/exceptions.ts: -------------------------------------------------------------------------------- 1 | export { 2 | CouchbaseError, 3 | TimeoutError, 4 | RequestCanceledError, 5 | InvalidArgumentError, 6 | ServiceNotAvailableError, 7 | InternalServerFailureError, 8 | AuthenticationFailureError, 9 | TemporaryFailureError, 10 | ParsingFailureError, 11 | CasMismatchError, 12 | BucketNotFoundError, 13 | CollectionNotFoundError, 14 | EncodingFailureError, 15 | DecodingFailureError, 16 | UnsupportedOperationError, 17 | AmbiguousTimeoutError, 18 | UnambiguousTimeoutError, 19 | FeatureNotAvailableError, 20 | ScopeNotFoundError, 21 | IndexNotFoundError, 22 | IndexExistsError, 23 | DocumentNotFoundError, 24 | DocumentUnretrievableError, 25 | DocumentLockedError, 26 | ValueTooLargeError, 27 | DocumentExistsError, 28 | ValueNotJsonError, 29 | DurabilityLevelNotAvailableError, 30 | DurabilityImpossibleError, 31 | DurabilityAmbiguousError, 32 | DurableWriteInProgressError, 33 | DurableWriteReCommitInProgressError, 34 | MutationLostError, 35 | PathNotFoundError, 36 | PathMismatchError, 37 | PathInvalidError, 38 | PathTooBigError, 39 | PathTooDeepError, 40 | ValueTooDeepError, 41 | ValueInvalidError, 42 | DocumentNotJsonError, 43 | NumberTooBigError, 44 | DeltaInvalidError, 45 | PathExistsError, 46 | PlanningFailureError, 47 | IndexFailureError, 48 | CompilationFailureError, 49 | JobQueueFullError, 50 | DatasetNotFoundError, 51 | DataverseNotFoundError, 52 | DatasetExistsError, 53 | DataverseExistsError, 54 | LinkNotFoundError, 55 | ViewNotFoundError, 56 | DesignDocumentNotFoundError, 57 | CollectionExistsError, 58 | ScopeExistsError, 59 | UserNotFoundError, 60 | GroupNotFoundError, 61 | BucketExistsError, 62 | UserExistsError, 63 | BucketNotFlushableError, 64 | KeyValueErrorContext, 65 | ViewErrorContext, 66 | QueryErrorContext, 67 | SearchErrorContext, 68 | AnalyticsErrorContext, 69 | } from 'couchbase'; 70 | -------------------------------------------------------------------------------- /src/exceptions/ottoman-errors.ts: -------------------------------------------------------------------------------- 1 | export class OttomanError extends Error { 2 | constructor(message?: string) { 3 | super(message); 4 | this.name = this.constructor.name; 5 | } 6 | } 7 | 8 | export class PathN1qlError extends OttomanError {} 9 | export class BuildSchemaError extends OttomanError {} 10 | export class ImmutableError extends OttomanError {} 11 | export class BuildIndexQueryError extends OttomanError {} 12 | export class BuildQueryError extends OttomanError {} 13 | export class BadKeyGeneratorDelimiterError extends OttomanError { 14 | name = 'BadKeyGeneratorDelimiter'; 15 | } 16 | export class InvalidModelReferenceError extends OttomanError { 17 | name = 'InvalidModelReferenceError'; 18 | } 19 | -------------------------------------------------------------------------------- /src/handler/create-many.ts: -------------------------------------------------------------------------------- 1 | import { ModelMetadata } from '../model/interfaces/model-metadata.interface'; 2 | import { batchProcessQueue } from './utils'; 3 | import { StatusExecution } from './types'; 4 | import { ModelTypes, saveOptions } from '../model/model.types'; 5 | 6 | /** 7 | * Async Function: Create many documents at once 8 | * 9 | * @param documents List of documents to create 10 | * 11 | * @return (ManyQueryResponse)[(/classes/queryresponse.html)] 12 | */ 13 | export const createMany = 14 | (metadata: ModelMetadata) => 15 | async (documents: unknown[], options: saveOptions = {}) => { 16 | return await batchProcessQueue(metadata)(documents, createManyCallback, {}, options, 100); 17 | }; 18 | 19 | /** 20 | * @ignore 21 | */ 22 | export const createManyCallback = ( 23 | document: ModelTypes, 24 | metadata: ModelMetadata, 25 | extra: any, 26 | options: saveOptions = {}, 27 | ): Promise => { 28 | const Model = metadata.ottoman.getModel(metadata.modelName); 29 | return Model.create(document, options) 30 | .then((created) => { 31 | return Promise.resolve(new StatusExecution(created, 'SUCCESS')); 32 | }) 33 | .catch((error) => { 34 | /* istanbul ignore next */ 35 | return Promise.reject(new StatusExecution(document, 'FAILURE', error.constructor.name, error.message)); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/handler/find/find-by-id-options.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAttemptContext } from 'couchbase'; 2 | 3 | export class FindByIdOptions { 4 | transactionContext?: TransactionAttemptContext; 5 | select?: string | string[]; 6 | populate?: string | string[]; 7 | withExpiry?: boolean; 8 | transcoder?: any; 9 | timeout?: number; 10 | populateMaxDeep?: number; 11 | /** 12 | * Documents returned from queries with the `lean` option enabled are plain javascript objects, not Ottoman Documents. They have no save methods, hooks or other Ottoman Document's features. 13 | * @example 14 | * ```ts 15 | * const document = await UserModel.findById(id, { lean: true }); 16 | * document instanceof Document; // false 17 | * ``` 18 | **/ 19 | lean?: boolean; 20 | enforceRefCheck?: boolean | 'throw'; 21 | constructor(data: FindByIdOptions) { 22 | for (const key in data) { 23 | this[key] = data[key]; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/handler/find/find-options.ts: -------------------------------------------------------------------------------- 1 | import { PopulateFieldsType } from '../../model/populate.types'; 2 | import { ISelectType, SortType } from '../../query'; 3 | import { SearchConsistency } from '../../utils/search-consistency'; 4 | import { TransactionAttemptContext } from 'couchbase'; 5 | 6 | export class FindOptions implements IFindOptions { 7 | skip?: number; 8 | limit?: number; 9 | sort?: Record; 10 | populate?: PopulateFieldsType; 11 | populateMaxDeep?: number; 12 | select?: ISelectType[] | string | string[]; 13 | consistency?: SearchConsistency; 14 | noCollection?: boolean; 15 | /** 16 | * Documents returned from queries with the `lean` option enabled are plain javascript objects, not Ottoman Documents. They have no save methods, hooks or other Ottoman Document's features. 17 | * @example 18 | * ```ts 19 | * const document = await UserModel.findById(id, { lean: true }); 20 | * document instanceof Document; // false 21 | * ``` 22 | **/ 23 | lean?: boolean; 24 | ignoreCase?: boolean; 25 | enforceRefCheck?: boolean | 'throw'; 26 | transactionContext?: TransactionAttemptContext; 27 | constructor(data: FindOptions) { 28 | for (const key in data) { 29 | this[key] = data[key]; 30 | } 31 | } 32 | } 33 | 34 | export interface IFindOptions { 35 | skip?: number; 36 | limit?: number; 37 | sort?: Record; 38 | populate?: PopulateFieldsType; 39 | populateMaxDeep?: number; 40 | select?: ISelectType[] | string | string[]; 41 | consistency?: SearchConsistency; 42 | noCollection?: boolean; 43 | lean?: boolean; 44 | ignoreCase?: boolean; 45 | } 46 | -------------------------------------------------------------------------------- /src/handler/index.ts: -------------------------------------------------------------------------------- 1 | export * from './find/find'; 2 | export * from './find/find-by-id-options'; 3 | export * from './find/find-options'; 4 | export * from './remove'; 5 | export * from './store'; 6 | export * from './types'; 7 | export * from './remove-many'; 8 | export * from './update_many'; 9 | export * from './create-many'; 10 | export * from './utils'; 11 | -------------------------------------------------------------------------------- /src/handler/remove-many.ts: -------------------------------------------------------------------------------- 1 | import { ModelMetadata } from '../model/interfaces/model-metadata.interface'; 2 | import { batchProcessQueue } from './utils'; 3 | import { ManyQueryResponse, StatusExecution } from './types'; 4 | 5 | /** 6 | * Async Function: Deletes all of the documents that match conditions from the collection. 7 | * Allows use of filters and options. 8 | * 9 | * @param ids List of documents to delete 10 | * 11 | * @return (ManyQueryResponse)[(/classes/queryresponse.html)] 12 | */ 13 | export const removeMany = 14 | (metadata: ModelMetadata) => 15 | async (ids, options = {}): Promise => { 16 | return await batchProcessQueue(metadata)(ids, removeCallback, {}, options, 100); 17 | }; 18 | 19 | /** 20 | * @ignore 21 | */ 22 | export const removeCallback = (id: string, metadata: ModelMetadata, extra, options): Promise => { 23 | const model = metadata.ottoman.getModel(metadata.modelName); 24 | 25 | return model 26 | .removeById(id, options) 27 | .then(() => { 28 | return Promise.resolve(new StatusExecution(id, 'SUCCESS')); 29 | }) 30 | .catch((error) => { 31 | return Promise.reject(new StatusExecution(id, 'FAILURE', error.constructor.name, error.message)); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/handler/remove.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAttemptContext } from 'couchbase'; 2 | 3 | interface RemoveOptions { 4 | timeout?: number; 5 | transactionContext?: TransactionAttemptContext; 6 | } 7 | 8 | /** 9 | * Removes a document by id from a given collection. 10 | */ 11 | export const remove = async (id, collection, options?: RemoveOptions): Promise => { 12 | const { transactionContext } = options || {}; 13 | if (transactionContext) { 14 | const doc = await transactionContext.get(collection, id); 15 | return transactionContext.remove(doc); 16 | } 17 | return collection.remove(id, options); 18 | }; 19 | -------------------------------------------------------------------------------- /src/handler/store.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAttemptContext } from 'couchbase'; 2 | 3 | interface StoreOptions { 4 | cas?: string; 5 | transcoder?: any; 6 | timeout?: number; 7 | maxExpiry?: number; 8 | expiry?: number; 9 | transactionContext?: TransactionAttemptContext; 10 | } 11 | 12 | /** 13 | * Stores a Document: Updates a document if CAS value is defined, otherwise it inserts a new document. 14 | * CAS is a value representing the current state of an item/document in the Couchbase Server. Each modification of the document changes it's CAS value. 15 | */ 16 | export const store = async (key, data, options: StoreOptions, collection): Promise => { 17 | let storePromise; 18 | const { transactionContext } = options || {}; 19 | if (options.maxExpiry !== undefined) { 20 | options.expiry = options.maxExpiry; 21 | delete options.maxExpiry; 22 | } 23 | if (options.cas) { 24 | if (transactionContext) { 25 | const doc = await transactionContext.get(collection, key); 26 | storePromise = transactionContext.replace(doc, data); 27 | } else { 28 | storePromise = collection.replace(key, data, options); 29 | } 30 | } else { 31 | if (options.transactionContext) { 32 | storePromise = options.transactionContext.insert(collection, key, data); 33 | } else { 34 | storePromise = collection.insert(key, data, options); 35 | } 36 | } 37 | return storePromise; 38 | }; 39 | -------------------------------------------------------------------------------- /src/handler/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Status of Query Response. 3 | * */ 4 | export type Status = 'SUCCESS' | 'FAILURE'; 5 | 6 | /** 7 | * Generic class of Query Response 8 | * */ 9 | export class QueryResponse { 10 | status: Status; 11 | message: M; 12 | constructor(status: Status, message: M) { 13 | this.status = status; 14 | this.message = message; 15 | } 16 | } 17 | 18 | export interface IStatusExecution { 19 | payload: string | Record; 20 | status: Status; 21 | exception?: string; 22 | message?: string; 23 | } 24 | 25 | /** 26 | * Status of a Query Execution. 27 | * */ 28 | export class StatusExecution implements IStatusExecution { 29 | payload: string | Record; 30 | status: Status; 31 | exception?: string; 32 | message?: string; 33 | /** 34 | * @param payload Receive id when updateMany or removeMany is used, receive object in case of createMany 35 | * @param status Status of Execution ('SUCCESS' | 'FAILURE') 36 | * @param exception Couchbase exception 37 | * @param message Couchbase exception message 38 | * */ 39 | constructor(payload: string | Record, status: Status, exception = '', message = '') { 40 | this.payload = payload; 41 | this.status = status; 42 | this.exception = exception; 43 | this.message = message; 44 | } 45 | } 46 | 47 | /** 48 | * Message of a Many Query Response. 49 | * 50 | * @field match_number Number of items that matched the filter, in case of createMany represent the number of documents to create. 51 | * @field success Number of successful operations 52 | * @field errors List of errors thrown in the execution 53 | * */ 54 | export interface ManyResponse { 55 | data: T[]; 56 | match_number: number; 57 | success: number; 58 | errors: StatusExecution[]; 59 | } 60 | 61 | export interface IManyQueryResponse { 62 | status: Status; 63 | message: ManyResponse; 64 | } 65 | 66 | /** 67 | * Response class for bulk operations. 68 | * */ 69 | export class ManyQueryResponse extends QueryResponse> implements IManyQueryResponse { 70 | /** 71 | * @param status Status of Execution ('SUCCESS' | 'FAILURE') 72 | * @param message: Message of Response see [ManyResponse](/interfaces/manyresponse.html) 73 | * 74 | * The response status will be **SUCCESS** as long as no error occurs, otherwise it will be **FAILURE**. 75 | * */ 76 | constructor(status: Status, message: ManyResponse) { 77 | super(status, message); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/handler/update_many.ts: -------------------------------------------------------------------------------- 1 | import { ModelMetadata } from '../model/interfaces/model-metadata.interface'; 2 | import { batchProcessQueue } from './utils'; 3 | import { ManyQueryResponse, StatusExecution } from './types'; 4 | import { ModelTypes } from '../model/model.types'; 5 | import { MutationFunctionOptions } from '../utils/cast-strategy'; 6 | 7 | /** 8 | * Async Function: Update all of the documents that match conditions from the collection. 9 | * Allows use of filters and options. 10 | * 11 | * @param documents List of documents to update 12 | * @param doc Fields to update. 13 | * 14 | * @return (ManyQueryResponse)[(/classes/queryresponse.html)] 15 | */ 16 | export const updateMany = 17 | (metadata: ModelMetadata) => 18 | async ( 19 | documents: ModelTypes[], 20 | doc: Partial, 21 | options: MutationFunctionOptions, 22 | ): Promise => { 23 | async function cb(document: ModelTypes, metadata: ModelMetadata, extra: Record) { 24 | return updateCallback(document, metadata, extra, options); 25 | } 26 | return await batchProcessQueue(metadata)(documents, cb, doc, options, 100); 27 | }; 28 | 29 | /** 30 | * @ignore 31 | */ 32 | export const updateCallback = ( 33 | document: ModelTypes, 34 | metadata: ModelMetadata, 35 | extra: Record, 36 | options: MutationFunctionOptions, 37 | ): Promise => { 38 | const model = metadata.ottoman.getModel(metadata.modelName); 39 | return model 40 | .updateById(document[metadata.ID_KEY], { ...document, ...extra }, options) 41 | .then((updated) => { 42 | return Promise.resolve(new StatusExecution(updated, 'SUCCESS')); 43 | }) 44 | .catch((error) => { 45 | return Promise.reject( 46 | new StatusExecution(document[metadata.ID_KEY], 'FAILURE', error.constructor.name, error.message), 47 | ); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/handler/utils.ts: -------------------------------------------------------------------------------- 1 | import { ModelMetadata } from '../model/interfaces/model-metadata.interface'; 2 | import { ManyQueryResponse, ManyResponse, StatusExecution } from './types'; 3 | 4 | /** 5 | * @ignore 6 | */ 7 | export const chunkArray = (list, size) => { 8 | const clonedList = [...list]; 9 | const results: any = []; 10 | while (clonedList.length) { 11 | results.push(clonedList.splice(0, size)); 12 | } 13 | return results; 14 | }; 15 | 16 | /** 17 | * @ignore 18 | */ 19 | function* processBatch(items, fn, metadata, extra, options): IterableIterator> { 20 | const clonedItems = [...items]; 21 | for (const items of clonedItems) { 22 | yield fn(items, metadata, extra, options) 23 | .then((result) => result) 24 | .catch((error) => error); 25 | } 26 | } 27 | 28 | /** 29 | * @ignore 30 | */ 31 | export const batchProcessQueue = 32 | (metadata: ModelMetadata) => 33 | async (items: unknown[], fn: unknown, extra: Record = {}, options: any = {}, throttle = 100) => { 34 | const chunks = chunkArray([...items], throttle); 35 | const result: ManyResponse = { success: 0, match_number: items.length, errors: [], data: [] }; 36 | for (const chunk of chunks) { 37 | const promises: Promise[] = []; 38 | for (const promise of processBatch(chunk, fn, metadata, extra, options)) { 39 | promises.push(promise); 40 | } 41 | const batchResults = await Promise.all(promises); 42 | for (const r of batchResults) { 43 | if (r.status === 'FAILURE') { 44 | result.errors.push(r); 45 | } else { 46 | result.success += 1; 47 | if (r.payload) { 48 | result.data?.push(r.payload as T); 49 | } 50 | } 51 | } 52 | } 53 | return new ManyQueryResponse(result.errors.length > 0 ? 'FAILURE' : 'SUCCESS', result); 54 | }; 55 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | connect, 3 | getCollection, 4 | model, 5 | start, 6 | close, 7 | getDefaultInstance, 8 | getOttomanInstances, 9 | getModel, 10 | Ottoman, 11 | searchQuery, 12 | } from './ottoman/ottoman'; 13 | export { Model, IModel } from './model/model'; 14 | export { Document, IDocument } from './model/document'; 15 | export { ModelOptions, CreateModelOptions } from './model/interfaces/create-model.interface'; 16 | export { FindOneAndUpdateOption } from './model/interfaces/find.interface'; 17 | export { UpdateManyOptions } from './model/interfaces/update-many.interface'; 18 | export { ModelTypes, saveOptions } from './model/model.types'; 19 | export { getModelMetadata } from './model/utils/model.utils'; 20 | export { ViewIndexOptions } from './model/index/view/view-index-options'; 21 | export { 22 | validate, 23 | applyDefaultValue, 24 | registerType, 25 | addValidators, 26 | IOttomanType, 27 | Schema, 28 | ValidationError, 29 | BuildSchemaError, 30 | EmbedType, 31 | StringType, 32 | ArrayType, 33 | BooleanType, 34 | NumberType, 35 | DateType, 36 | CoreType, 37 | ReferenceType, 38 | MixedType, 39 | ValidatorOption, 40 | } from './schema'; 41 | export { FindByIdOptions, FindOptions, IManyQueryResponse, ManyQueryResponse, IStatusExecution } from './handler'; 42 | export { registerGlobalPlugin } from './plugins/global-plugin-handler'; 43 | export * from './utils'; 44 | export * from './couchbase'; 45 | export { getProjectionFields } from './utils/query/extract-select'; 46 | export { SearchConsistency } from './utils/search-consistency'; 47 | export * from './exceptions/exceptions'; 48 | export { MutationFunctionOptions } from './utils/cast-strategy'; 49 | export { 50 | ReturnResultDict, 51 | ResultExprDict, 52 | AggDict, 53 | selectBuilder, 54 | buildSelectExpr, 55 | buildWhereClauseExpr, 56 | buildIndexExpr, 57 | SelectClauseException, 58 | MultipleQueryTypesException, 59 | WhereClauseException, 60 | QueryOperatorNotFoundException, 61 | QueryGroupByParamsException, 62 | IndexParamsUsingGSIExceptions, 63 | IndexParamsOnExceptions, 64 | Query, 65 | LetExprType, 66 | SortType, 67 | ISelectType, 68 | LogicalWhereExpr, 69 | ComparisonWhereExpr, 70 | IField, 71 | IIndexOnParams, 72 | IIndexWithParams, 73 | IndexType, 74 | ISelectAggType, 75 | AggType, 76 | ComparisonEmptyOperatorType, 77 | ComparisonMultipleOperatorType, 78 | ComparisonSingleStringOperatorType, 79 | ComparisonSingleOperatorType, 80 | LogicalOperatorType, 81 | ResultExprType, 82 | ReturnResultType, 83 | parseStringSelectExpr, 84 | escapeReservedWords, 85 | IConditionExpr, 86 | } from './query'; 87 | -------------------------------------------------------------------------------- /src/model/hooks/exec-hooks.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from '../../utils/pipe'; 2 | import { EmbedType } from '../../schema'; 3 | 4 | /** 5 | * Executes hooks in a chain, 6 | * passing previous result to the next hook in chain. 7 | */ 8 | export const execHooks = async (schema, hookType: 'preHooks' | 'postHooks', hookAction, document?): Promise => { 9 | for (const key in schema.fields) { 10 | const field = schema.fields[key]; 11 | if (field.typeName === 'Embed' && (field as EmbedType).schema) { 12 | await execHooks((field as EmbedType).schema, hookType, hookAction, document[field.name]); 13 | } 14 | } 15 | if (schema[hookType] && schema[hookType][hookAction]) { 16 | const hooks = schema[hookType][hookAction]; 17 | const hooksFn = pipe(...hooks); 18 | await hooksFn(document); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/model/index/helpers/index-field-names.ts: -------------------------------------------------------------------------------- 1 | export const indexFieldsName = (fields: string[]): string => { 2 | const fieldKeys: string[] = []; 3 | for (let i = 0; i < fields.length; ++i) { 4 | fieldKeys.push(fields[i].replace(/\./g, '::')); 5 | } 6 | return `${fieldKeys.join('$')}`; 7 | }; 8 | -------------------------------------------------------------------------------- /src/model/index/n1ql/build-index-query.ts: -------------------------------------------------------------------------------- 1 | import { BuildIndexQueryError } from '../../../exceptions/ottoman-errors'; 2 | import { FindOptions } from '../../../handler'; 3 | 4 | /** 5 | * View index function factory. 6 | */ 7 | export const buildIndexQuery = 8 | (Model, fields, indexFnName, indexOptions = {}) => 9 | (values: any | any[], options: FindOptions = {}) => { 10 | values = Array.isArray(values) ? values : typeof values === 'string' ? [values] : values; 11 | let filter = values; 12 | const n1qlOptions = { ...indexOptions, ...options }; 13 | if (Array.isArray(values)) { 14 | if (values.length !== fields.length) { 15 | throw new BuildIndexQueryError( 16 | `Function '${indexFnName}' received wrong number of arguments, '${fields.length}:[${fields}]' argument(s) was expected and '${values.length}:[${values}]' were received`, 17 | ); 18 | } 19 | filter = {}; 20 | for (let i = 0; i < fields.length; i++) { 21 | const field = fields[i]; 22 | if (field.includes('[*]')) { 23 | const [target, targetField] = field.split('[*].'); 24 | filter['$any'] = { 25 | $expr: [{ x: { $in: target } }], 26 | $satisfies: { [`x.${targetField}`]: values[i] }, 27 | }; 28 | } else { 29 | filter[field] = values[i]; 30 | } 31 | } 32 | } 33 | return Model.find(filter, n1qlOptions); 34 | }; 35 | -------------------------------------------------------------------------------- /src/model/index/n1ql/extract-index-field-names.ts: -------------------------------------------------------------------------------- 1 | import { jpParse } from '../../../utils/jp-parse'; 2 | import { pathToN1QL } from '../../../utils/path-to-n1ql'; 3 | import { BuildIndexQueryError } from '../../../exceptions/ottoman-errors'; 4 | 5 | export const extractIndexFieldNames = (gsi: { fields: string[] }) => { 6 | const fieldNames: string[] = []; 7 | for (let j = 0; j < gsi.fields.length; ++j) { 8 | const path = jpParse(gsi.fields[j]); 9 | let wildCardAt = -1; 10 | for (let k = 0; k < path.length; ++k) { 11 | if (path[k].operation === 'subscript' && path[k].expression.type === 'wildcard') { 12 | if (wildCardAt !== -1) { 13 | throw new BuildIndexQueryError('Cannot create an index with more than one wildcard in path'); 14 | } 15 | wildCardAt = k; 16 | } 17 | } 18 | 19 | if (wildCardAt === -1) { 20 | fieldNames.push(pathToN1QL(path)); 21 | } else { 22 | const pathBefore = path.slice(0, wildCardAt); 23 | const pathAfter = path.slice(wildCardAt + 1); 24 | 25 | let objTarget = pathToN1QL(pathAfter); 26 | if (objTarget !== '') { 27 | objTarget = 'v.' + objTarget; 28 | } else { 29 | objTarget = 'v'; 30 | } 31 | 32 | const arrTarget = pathToN1QL(pathBefore); 33 | 34 | fieldNames.push(`DISTINCT ARRAY ${objTarget} FOR v IN ${arrTarget} END`); 35 | } 36 | } 37 | return fieldNames; 38 | }; 39 | -------------------------------------------------------------------------------- /src/model/index/refdoc/build-index-refdoc.ts: -------------------------------------------------------------------------------- 1 | import { indexFieldsName } from '../helpers/index-field-names'; 2 | import { ModelMetadata } from '../../interfaces/model-metadata.interface'; 3 | import { TransactionAttemptContext } from 'couchbase'; 4 | 5 | export const buildViewRefdoc = 6 | (metadata: ModelMetadata, Model, fields, prefix) => 7 | async (values, options: { transactionContext?: TransactionAttemptContext } = {}) => { 8 | values = Array.isArray(values) ? values : [values]; 9 | const key = buildRefKey(fields, values, prefix); 10 | const { collection } = metadata; 11 | const c = collection(); 12 | if (options.transactionContext) { 13 | const { transactionContext } = options; 14 | const result = await transactionContext.get(c, key); 15 | if (result?.content) { 16 | return Model.findById(result.content, { transactionContext: transactionContext }); 17 | } 18 | } else { 19 | const result = await c.get(key, options); 20 | if (result?.value) { 21 | return Model.findById(result.value); 22 | } 23 | } 24 | }; 25 | 26 | export const buildFieldsRefKey = (fields: string[], prefix: string): string => { 27 | const fieldsKey: string = indexFieldsName(fields); 28 | return `${prefix}$${fieldsKey}`; 29 | }; 30 | 31 | export const buildRefKey = (fields: string[], values: string[], prefix: string): string => { 32 | const fieldsRefKey: string = buildFieldsRefKey(fields, prefix); 33 | const valuesKey: string = values.join('|'); 34 | return `$${fieldsRefKey}.${valuesKey}`; 35 | }; 36 | -------------------------------------------------------------------------------- /src/model/index/types/index.types.ts: -------------------------------------------------------------------------------- 1 | import { FindOptions } from '../../../handler'; 2 | 3 | export type IndexType = 'refdoc' | 'n1ql' | 'view'; 4 | export type SchemaIndex = Record< 5 | string, 6 | { by: string | string[]; ref?: string; options?: FindOptions; type?: IndexType } 7 | >; 8 | export type SchemaQuery = Record; 9 | -------------------------------------------------------------------------------- /src/model/index/view/build-map-view-index-fn.ts: -------------------------------------------------------------------------------- 1 | import { ModelMetadata } from '../../interfaces/model-metadata.interface'; 2 | 3 | export const buildMapViewIndexFn = (metadata: ModelMetadata, fields) => { 4 | const { modelKey, modelName } = metadata; 5 | const docFields = fields.map((field) => `doc.${field}`); 6 | return `function (doc, meta) { 7 | if (doc.${modelKey} == "${modelName}") { 8 | emit([${docFields.join(',')}], null); 9 | } 10 | }`; 11 | }; 12 | -------------------------------------------------------------------------------- /src/model/index/view/build-view-index-query.ts: -------------------------------------------------------------------------------- 1 | import { ViewQueryOptions } from 'couchbase'; 2 | import { Ottoman } from '../../../ottoman/ottoman'; 3 | import { BuildIndexQueryError } from '../../../exceptions/ottoman-errors'; 4 | 5 | /** 6 | * Index function factory. 7 | */ 8 | export const buildViewIndexQuery = 9 | (ottoman: Ottoman, designDocName, indexName, fields, Model) => 10 | async (values: any | any[], options: ViewQueryOptions = {}) => { 11 | if (!values) { 12 | throw new BuildIndexQueryError( 13 | `Function '${indexName}' received wrong argument value, '${values}' wasn't expected`, 14 | ); 15 | } 16 | const arrayValues = Array.isArray(values) ? values : [values]; 17 | const fieldsLength = fields.length; 18 | const valuesLength = arrayValues.length; 19 | 20 | if (valuesLength !== fieldsLength) { 21 | throw new BuildIndexQueryError( 22 | `Function '${indexName}' received wrong number of arguments, '${fieldsLength}:[${fields}]' argument(s) was expected and '${valuesLength}:[${arrayValues}]' were received`, 23 | ); 24 | } 25 | options.keys = arrayValues; 26 | const result = await ottoman.bucket!.viewQuery(designDocName, indexName, options); 27 | const populatedResults: any[] = []; 28 | for (const row of result.rows) { 29 | const populatedDocument = await Model.findById(row.id, options); 30 | populatedResults.push(populatedDocument); 31 | } 32 | return { rows: populatedResults, meta: result.meta }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/model/index/view/ensure-view-indexes.ts: -------------------------------------------------------------------------------- 1 | import { DesignDocument } from 'couchbase'; 2 | import { Ottoman } from '../../../ottoman/ottoman'; 3 | 4 | export const ensureViewIndexes = async (ottoman: Ottoman, indexes) => { 5 | const designDocs: { name: string; data: any }[] = []; 6 | for (const key in indexes) { 7 | if (indexes.hasOwnProperty(key)) { 8 | designDocs.push({ 9 | name: key, 10 | data: indexes[key], 11 | }); 12 | } 13 | } 14 | 15 | if (designDocs.length === 0) { 16 | return; 17 | } 18 | 19 | for (const designDoc of designDocs) { 20 | const doc = DesignDocument._fromNsData(designDoc.name, designDoc.data); 21 | await ottoman.viewIndexManager.upsertDesignDocument(doc); 22 | } 23 | return true; 24 | }; 25 | -------------------------------------------------------------------------------- /src/model/index/view/view-index-options.ts: -------------------------------------------------------------------------------- 1 | export class ViewIndexOptions { 2 | skip?: number; 3 | limit?: number; 4 | stale?: unknown; 5 | order?: unknown; 6 | reduce?: unknown; 7 | group?: boolean; 8 | groupLevel?: number; 9 | key?: string; 10 | keys?: string[]; 11 | range?: { start: string | string[]; end: string | string[]; inclusiveEnd: boolean }; 12 | idRange?: string[] | { start: string; end: string }; 13 | fullSet?: boolean; 14 | onError?: unknown; 15 | timeout?: number; 16 | 17 | constructor(data: ViewIndexOptions) { 18 | for (const key in data) { 19 | this[key] = data[key]; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/model/interfaces/create-model.interface.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../../schema'; 2 | import { ModelMetadata } from './model-metadata.interface'; 3 | import { Ottoman } from '../../ottoman/ottoman'; 4 | 5 | export interface ModelOptions { 6 | collectionName?: string; 7 | scopeName?: string; 8 | idKey?: string; 9 | modelKey?: string; 10 | maxExpiry?: number; 11 | keyGenerator?: (params: { metadata: ModelMetadata }) => string; 12 | keyGeneratorDelimiter?: string; 13 | } 14 | 15 | export interface CreateModelOptions { 16 | collectionName: string; 17 | scopeName: string; 18 | idKey: string; 19 | modelKey: string; 20 | maxExpiry?: number; 21 | keyGenerator?: (params: { metadata: ModelMetadata }) => string; 22 | keyGeneratorDelimiter?: string; 23 | } 24 | 25 | export interface CreateModel { 26 | name: string; 27 | schemaDraft: Schema | Record; 28 | options: CreateModelOptions; 29 | ottoman: Ottoman; 30 | } 31 | -------------------------------------------------------------------------------- /src/model/interfaces/find.interface.ts: -------------------------------------------------------------------------------- 1 | import { IFindOptions } from '../../handler'; 2 | import { MutationFunctionOptions } from '../../utils/cast-strategy'; 3 | import { TransactionAttemptContext } from '../../couchbase'; 4 | 5 | /** 6 | * Find One and Update Option parameter. 7 | * */ 8 | export interface FindOneAndUpdateOption extends IFindOptions, MutationFunctionOptions { 9 | /** Default: false 10 | * if true, and no documents found, insert a new document. 11 | * */ 12 | upsert?: boolean; 13 | /** Default: false 14 | * if true, return a document after update otherwise return the document before update. 15 | * */ 16 | new?: boolean; 17 | maxExpiry?: number; 18 | /** 19 | * Default: false 20 | * enforceRefCheck will check if the referenced document exists 21 | * set to true: will log a warning message. 22 | * set to 'throw': will throw an exception. 23 | */ 24 | enforceRefCheck?: boolean | 'throw'; 25 | transactionContext?: TransactionAttemptContext; 26 | } 27 | -------------------------------------------------------------------------------- /src/model/interfaces/model-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../../schema'; 2 | import { Ottoman } from '../../ottoman/ottoman'; 3 | 4 | export interface ModelMetadata { 5 | modelName: string; 6 | collectionName: string; 7 | scopeName: string; 8 | schema: Schema; 9 | collection: any; 10 | ID_KEY: string; 11 | ottoman: Ottoman; 12 | modelKey: string; 13 | maxExpiry?: number; 14 | keyGenerator?: (params: { metadata: ModelMetadata }) => string; 15 | keyGeneratorDelimiter?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/model/interfaces/update-many.interface.ts: -------------------------------------------------------------------------------- 1 | import { IFindOptions } from '../../handler'; 2 | import { MutationFunctionOptions } from '../../utils/cast-strategy'; 3 | 4 | /** 5 | * Update Many Options parameter 6 | * */ 7 | export interface UpdateManyOptions extends IFindOptions, MutationFunctionOptions { 8 | /** Default: false 9 | * if true, and no documents found, insert a new document. 10 | * */ 11 | upsert?: boolean; 12 | 13 | /** 14 | * Default: false 15 | * enforceRefCheck will check if the referenced document exists 16 | * set to true: will log a warning message. 17 | * set to 'throw': will throw an exception. 18 | */ 19 | enforceRefCheck?: boolean | 'throw'; 20 | } 21 | -------------------------------------------------------------------------------- /src/model/model.types.ts: -------------------------------------------------------------------------------- 1 | import { IModel } from './model'; 2 | import { IDocument } from './document'; 3 | import { CastOptions } from '../utils/cast-strategy'; 4 | import { TransactionAttemptContext } from 'couchbase'; 5 | 6 | type WhateverTypes = { [key: string]: any }; 7 | 8 | export interface saveOptions { 9 | maxExpiry?: number; 10 | 11 | /** 12 | * Default: false 13 | * enforceRefCheck will check if the referenced document exists 14 | * set to true: will log a warning message. 15 | * set to 'throw': will throw an exception. 16 | */ 17 | enforceRefCheck?: boolean | 'throw'; 18 | transactionContext?: TransactionAttemptContext; 19 | } 20 | 21 | /** 22 | * Represents the options for counting records. 23 | */ 24 | export interface CountOptions { 25 | transactionContext?: TransactionAttemptContext; 26 | } 27 | 28 | /** 29 | * Interface for the removeOptions class. 30 | */ 31 | export interface removeOptions { 32 | transactionContext?: TransactionAttemptContext; 33 | } 34 | 35 | export type ModelTypes = WhateverTypes & 36 | IModel & { 37 | new (data: T, options?: CastOptions): IDocument; 38 | }; 39 | -------------------------------------------------------------------------------- /src/model/populate.types.ts: -------------------------------------------------------------------------------- 1 | import { TransactionAttemptContext } from 'couchbase'; 2 | 3 | export type FieldsBaseType = string | string[]; 4 | export type PopulateSelectBaseType = { select?: FieldsBaseType; populate?: PopulateFieldsType }; 5 | export type PopulateSelectType = { [K: string]: FieldsBaseType | PopulateSelectBaseType }; 6 | export type PopulateFieldsType = FieldsBaseType | PopulateSelectType; 7 | 8 | export type PopulateOptionsType = { 9 | deep?: number; 10 | lean?: boolean; 11 | enforceRefCheck?: boolean | 'throw'; 12 | transactionContext?: TransactionAttemptContext; 13 | }; 14 | -------------------------------------------------------------------------------- /src/model/utils/array-diff.ts: -------------------------------------------------------------------------------- 1 | // Returns the elements of arr1 that do not exist in arr2 2 | export const arrayDiff = (arr1, arr2) => arr1.filter((item) => !arr2.find((ref) => ref === item)); 3 | -------------------------------------------------------------------------------- /src/model/utils/get-model-ref-keys.ts: -------------------------------------------------------------------------------- 1 | import { buildRefKey } from '../index/refdoc/build-index-refdoc'; 2 | 3 | export const getModelRefKeys = (data, prefix, ottoman) => { 4 | const refdocKeys: string[] = []; 5 | const refdocs = ottoman.getRefdocIndexByKey(prefix); 6 | if (refdocs) { 7 | for (const refdoc of refdocs) { 8 | const { fields } = refdoc; 9 | const values: string[] = []; 10 | for (const field of fields) { 11 | if (data.hasOwnProperty(field) && data[field] !== undefined && data[field] !== null) { 12 | values.push(data[field]); 13 | } 14 | } 15 | refdocKeys.push(buildRefKey(fields, values, prefix)); 16 | } 17 | } 18 | return refdocKeys; 19 | }; 20 | -------------------------------------------------------------------------------- /src/model/utils/remove-life-cycle.ts: -------------------------------------------------------------------------------- 1 | import { HOOKS } from '../../utils/hooks'; 2 | import { execHooks } from '../hooks/exec-hooks'; 3 | import { remove } from '../../handler'; 4 | import { updateRefdocIndexes } from './update-refdoc-indexes'; 5 | 6 | /** 7 | * Remove lifecycle including hooks. 8 | * @ignore 9 | */ 10 | export const removeLifeCycle = async ({ id, options, metadata, refKeys, data }) => { 11 | const { schema, collection } = metadata; 12 | const document = data; 13 | await execHooks(schema, 'preHooks', HOOKS.REMOVE, document); 14 | 15 | const _collection = collection(); 16 | const result = await remove(id, _collection, options); 17 | 18 | // After store document update index refdocs 19 | await updateRefdocIndexes(refKeys, null, _collection, options.transactionContext); 20 | 21 | await execHooks(schema, 'preHooks', HOOKS.REMOVE, { document, result }); 22 | 23 | return { result, document }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/model/utils/store-life-cycle.ts: -------------------------------------------------------------------------------- 1 | import { HOOKS } from '../../utils/hooks'; 2 | import { execHooks } from '../hooks/exec-hooks'; 3 | import { ReferenceType, validate } from '../../schema'; 4 | import { store } from '../../handler'; 5 | import { updateRefdocIndexes } from './update-refdoc-indexes'; 6 | import { CAST_STRATEGY } from '../../utils/cast-strategy'; 7 | import { DocumentNotFoundError } from 'couchbase'; 8 | import { InvalidModelReferenceError } from '../../exceptions/ottoman-errors'; 9 | 10 | /** 11 | * Store lifecycle including hooks and validations. 12 | * @ignore 13 | */ 14 | export const storeLifeCycle = async ({ key, id, data, options, metadata, refKeys }) => { 15 | const { schema, collection, modelKey, ID_KEY, ottoman } = metadata; 16 | let document = data; 17 | const _colleciton = collection(); 18 | await execHooks(schema, 'preHooks', HOOKS.VALIDATE, document); 19 | 20 | document = validate(document, schema, { 21 | strict: schema.options.strict, 22 | strategy: CAST_STRATEGY.THROW, 23 | skip: [modelKey.split('.')[0], ID_KEY], 24 | }); 25 | 26 | // enforceRefCheck logic 27 | let enforceRefCheck = schema.options.enforceRefCheck; 28 | if (options.hasOwnProperty('enforceRefCheck')) { 29 | enforceRefCheck = options.enforceRefCheck; 30 | } 31 | if (enforceRefCheck) { 32 | for (const key in schema.fields) { 33 | const fieldType = schema.fields[key]; 34 | if (fieldType instanceof ReferenceType) { 35 | const RefModel = ottoman.getModel(fieldType.refModel); 36 | try { 37 | await RefModel.findById(document[fieldType.name], { transactionContext: options.transactionContext }); 38 | } catch (e) { 39 | if (e instanceof DocumentNotFoundError) { 40 | switch (enforceRefCheck) { 41 | case true: 42 | console.warn( 43 | `Reference to '${fieldType.name}' field with value = '${document[fieldType.name]}' was not found!`, 44 | ); 45 | break; 46 | case 'throw': 47 | throw new InvalidModelReferenceError( 48 | `Reference to '${fieldType.name}' field with value = '${document[fieldType.name]}' was not found!`, 49 | ); 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | await execHooks(schema, 'postHooks', HOOKS.VALIDATE, document); 58 | 59 | if (options.cas) { 60 | await execHooks(schema, 'preHooks', HOOKS.UPDATE, document); 61 | } else { 62 | await execHooks(schema, 'preHooks', HOOKS.SAVE, document); 63 | } 64 | 65 | const result = await store(key, document, options, _colleciton); 66 | 67 | // After storing the document update the index refdocs 68 | await updateRefdocIndexes(refKeys, id, _colleciton, options.transactionContext); 69 | 70 | if (options.cas) { 71 | await execHooks(schema, 'postHooks', HOOKS.UPDATE, document); 72 | } else { 73 | await execHooks(schema, 'postHooks', HOOKS.SAVE, document); 74 | } 75 | 76 | return { result, document }; 77 | }; 78 | -------------------------------------------------------------------------------- /src/model/utils/update-refdoc-indexes.ts: -------------------------------------------------------------------------------- 1 | import { isDebugMode } from '../../utils/is-debug-mode'; 2 | import { Collection, TransactionAttemptContext } from 'couchbase'; 3 | import { TransactionGetResult, MutationResult } from 'couchbase'; 4 | 5 | export const updateRefdocIndexes = async ( 6 | refKeys: { add: string[]; remove: string[] }, 7 | key: string | null, 8 | collection: Collection, 9 | transactionContext?: TransactionAttemptContext, 10 | ) => { 11 | for await (const ref of addRefKeys(refKeys.add, collection, key, transactionContext)) { 12 | if (isDebugMode()) { 13 | console.log('Adding refdoc index:', ref); 14 | } 15 | } 16 | 17 | for await (const ref of removeRefKeys(refKeys.remove, collection, transactionContext)) { 18 | if (isDebugMode()) { 19 | console.log('Removing refdoc index:', ref); 20 | } 21 | } 22 | }; 23 | 24 | async function* addRefKeys(refKeys, collection, key, transactionContext?: TransactionAttemptContext) { 25 | for (const ref of refKeys) { 26 | if (ref.length < 250) { 27 | let promise: Promise; 28 | if (transactionContext) { 29 | promise = transactionContext.insert(collection, ref, key); 30 | } else { 31 | promise = collection.insert(ref, key); 32 | } 33 | yield promise.catch((e) => { 34 | if (isDebugMode()) { 35 | console.warn(e); 36 | } 37 | }); 38 | } else { 39 | console.log(`Unable to store refdoc index for ${ref}, Maximum key size is 250 bytes`); 40 | yield false; 41 | } 42 | } 43 | } 44 | 45 | async function* removeRefKeys(refKeys, collection, transactionContext?: TransactionAttemptContext) { 46 | for (const ref of refKeys) { 47 | let promise: Promise; 48 | if (transactionContext) { 49 | const doc = await transactionContext.get(collection, ref); 50 | promise = transactionContext.remove(doc); 51 | } else { 52 | promise = collection.remove(ref); 53 | } 54 | yield promise.catch((e) => { 55 | if (isDebugMode()) { 56 | console.warn(e); 57 | } 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/plugins/global-plugin-handler-error.ts: -------------------------------------------------------------------------------- 1 | import { OttomanError } from '../exceptions/ottoman-errors'; 2 | export class GlobalPluginHandlerError extends OttomanError {} 3 | -------------------------------------------------------------------------------- /src/plugins/global-plugin-handler.ts: -------------------------------------------------------------------------------- 1 | import { GlobalPluginHandlerError } from './global-plugin-handler-error'; 2 | 3 | type PluginFunctions = () => void; 4 | 5 | /** 6 | * Store global plugins. 7 | */ 8 | export const __plugins: PluginFunctions[] = []; 9 | 10 | /** 11 | * Register a global plugin. 12 | */ 13 | export const registerGlobalPlugin = (...plugins) => { 14 | for (const plugin of plugins) { 15 | if (plugin && typeof plugin === 'function') { 16 | __plugins.push(plugin); 17 | } else { 18 | throw new GlobalPluginHandlerError('Unable to register the global plugin, only functions are allowed'); 19 | } 20 | } 21 | }; 22 | 23 | /** 24 | * Return all global plugins 25 | */ 26 | export const getGlobalPlugins = () => __plugins; 27 | -------------------------------------------------------------------------------- /src/query/base-query.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IConditionExpr, 3 | ISelectType, 4 | LetExprType, 5 | LogicalWhereExpr, 6 | QueryBuildOptionsType, 7 | SortType, 8 | } from './interface/query.types'; 9 | 10 | /** 11 | * Basic definition of Query class. 12 | * */ 13 | export abstract class BaseQuery { 14 | protected constructor(protected _conditions: IConditionExpr, protected _collection: string) {} 15 | 16 | abstract select(value?: ISelectType[] | string | undefined): BaseQuery; 17 | abstract where(value: LogicalWhereExpr): BaseQuery; 18 | abstract orderBy(value: Record): BaseQuery; 19 | abstract limit(value: number): BaseQuery; 20 | abstract offset(value: number): BaseQuery; 21 | abstract let(value: LetExprType): BaseQuery; 22 | abstract useKeys(value: [string]): BaseQuery; 23 | abstract build(options: QueryBuildOptionsType): string; 24 | } 25 | -------------------------------------------------------------------------------- /src/query/exceptions.ts: -------------------------------------------------------------------------------- 1 | import { OttomanError } from '../exceptions/ottoman-errors'; 2 | 3 | export class SelectClauseException extends OttomanError { 4 | constructor() { 5 | // todo: update message 6 | super('The SELECT clause does not have the proper structure'); 7 | } 8 | } 9 | 10 | export class WhereClauseException extends OttomanError { 11 | constructor(message = '') { 12 | super(`The WHERE clause does not have the proper structure. ${message}`); 13 | } 14 | } 15 | 16 | export class CollectionInWithinExceptions extends WhereClauseException { 17 | constructor( 18 | message = 'The Collection Operator needs to have the following clauses declared (IN | WITHIN) and SATISFIES.', 19 | ) { 20 | super(); 21 | this.message = message; 22 | } 23 | } 24 | export class MultipleQueryTypesException extends OttomanError { 25 | constructor(type1: string, type2: string) { 26 | super(`Cannot combine multiple query types (ex: ${type1} with ${type2}`); 27 | } 28 | } 29 | 30 | export class QueryOperatorNotFoundException extends WhereClauseException { 31 | constructor(operator: string) { 32 | super(); 33 | this.message = `Operator not found: ${operator}`; 34 | } 35 | } 36 | 37 | export class QueryGroupByParamsException extends WhereClauseException { 38 | constructor() { 39 | super(); 40 | this.message = `The GROUP BY clause must be defined to use the LETTING and HAVING clauses`; 41 | } 42 | } 43 | 44 | export class IndexParamsOnExceptions extends OttomanError { 45 | constructor(clause: string[]) { 46 | super(`The ON parameter can only be applied in the following clauses: ${JSON.stringify(clause)}`); 47 | } 48 | } 49 | 50 | export class IndexParamsUsingGSIExceptions extends OttomanError { 51 | constructor(clause: string[]) { 52 | super(`The USING GSI parameter can only be applied in the following clauses: ${JSON.stringify(clause)}`); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/query/helpers/dictionary.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AggType, 3 | CollectionRangePredicateOperatorType, 4 | CollectionDeepSearchOperatorType, 5 | ComparisonEmptyOperatorType, 6 | ComparisonMultipleOperatorType, 7 | ComparisonSingleOperatorType, 8 | ComparisonSingleStringOperatorType, 9 | LogicalOperatorType, 10 | ResultExprType, 11 | ReturnResultType, 12 | CollectionSatisfiesOperatorType, 13 | } from '../interface/query.types'; 14 | 15 | /** 16 | * Dictionary for handling aggregation functions. 17 | * */ 18 | export const AggDict: Record = { 19 | $arrayAgg: 'ARRAY_AGG', 20 | $avg: 'AVG', 21 | $mean: 'MEAN', 22 | $count: 'COUNT', 23 | $countn: 'COUNTN', 24 | $max: 'MAX', 25 | $median: 'MEDIAN', 26 | $min: 'MIN', 27 | $stddev: 'STDDEV', 28 | $stddevPop: 'STDDEV_POP', 29 | $stddevSamp: 'STDDEV_SAMP', 30 | $sum: 'SUM', 31 | $variance: 'VARIANCE', 32 | $variancePop: 'VARIANCE_POP', 33 | $varianceSamp: 'VARIANCE_SAMP', 34 | $varPop: 'VAR_POP', 35 | $varSamp: 'VAR_SAMP', 36 | }; 37 | 38 | /** 39 | * Dictionary for handling result expressions of RAW | ELEMENT | VALUE type. 40 | * */ 41 | export const ResultExprDict: Record = { $raw: 'RAW', $element: 'ELEMENT', $value: 'VALUE' }; 42 | 43 | /** 44 | * Dictionary for handling result expressions of ALL | DISTINCT type. 45 | * */ 46 | export const ReturnResultDict: Record = { $all: 'ALL', $distinct: 'DISTINCT' }; 47 | 48 | /** 49 | * Dictionary for handling Boolean comparison operators. 50 | * */ 51 | export const ComparisonEmptyOperatorDict: Record = { 52 | $isNull: 'IS NULL', 53 | $isNotNull: 'IS NOT NULL', 54 | $isMissing: 'IS MISSING', 55 | $isNotMissing: 'IS NOT MISSING', 56 | $isValued: 'IS VALUED', 57 | $isNotValued: 'IS NOT VALUED', 58 | }; 59 | 60 | /** 61 | * Dictionary for handling Numeric comparison operators. 62 | * */ 63 | export const ComparisonSingleOperatorDict: Record = { 64 | $eq: '=', 65 | $neq: '!=', 66 | $gt: '>', 67 | $gte: '>=', 68 | $lt: '<', 69 | $lte: '<=', 70 | }; 71 | 72 | /** 73 | * Dictionary for handling String comparison operators. 74 | * */ 75 | export const ComparisonSingleStringOperatorDict: Record = { 76 | $like: 'LIKE', 77 | $notLike: 'NOT LIKE', 78 | }; 79 | 80 | /** 81 | * Dictionary for handling Range comparison operators. 82 | * */ 83 | export const ComparisonMultipleOperatorDict: Record = { 84 | $btw: 'BETWEEN', 85 | $notBtw: 'NOT BETWEEN', 86 | }; 87 | 88 | /** 89 | * Dictionary for handling Logical operators. 90 | * */ 91 | export const LogicalOperatorDict: Record = { 92 | $and: 'AND', 93 | $or: 'OR', 94 | $not: 'NOT', 95 | }; 96 | 97 | /** 98 | * Dictionary for handling collection of range predicates operators. 99 | * */ 100 | export const CollectionRangePredicateOperatorDict: Record = { 101 | $any: 'ANY', 102 | $every: 'EVERY', 103 | }; 104 | 105 | /** 106 | * Dictionary for handling collection ( [NOT] IN | WITHIN ) operators. 107 | * */ 108 | export const CollectionDeepSearchOperatorDict: Record = { 109 | $in: 'IN', 110 | $within: 'WITHIN', 111 | $notIn: 'NOT IN', 112 | $notWithin: 'NOT WITHIN', 113 | }; 114 | 115 | /** 116 | * Dictionary for handling collection Satisfies operators. 117 | * */ 118 | export const CollectionSatisfiesOperatorDict: Record = { 119 | $satisfies: 'SATISFIES', 120 | }; 121 | -------------------------------------------------------------------------------- /src/query/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { selectBuilder, buildSelectExpr, buildWhereClauseExpr, buildIndexExpr, buildSelectArrayExpr } from './builders'; 2 | export { AggDict, ResultExprDict, ReturnResultDict } from './dictionary'; 3 | export { n1qlReservedWords } from './reservedWords'; 4 | -------------------------------------------------------------------------------- /src/query/helpers/reservedWords.ts: -------------------------------------------------------------------------------- 1 | export const n1qlReservedWords: string[] = [ 2 | 'ADVISE', 3 | 'ALL', 4 | 'ALTER', 5 | 'ANALYZE', 6 | 'AND', 7 | 'ANY', 8 | 'ARRAY', 9 | 'AS', 10 | 'ASC', 11 | 'BEGIN', 12 | 'BETWEEN', 13 | 'BINARY', 14 | 'BOOLEAN', 15 | 'BREAK', 16 | 'BUCKET', 17 | 'BUILD', 18 | 'BY', 19 | 'CALL', 20 | 'CASE', 21 | 'CAST', 22 | 'CLUSTER', 23 | 'COLLATE', 24 | 'COLLECTION', 25 | 'COMMIT', 26 | 'CONNECT', 27 | 'CONTINUE', 28 | 'CORRELATED', 29 | 'COVER', 30 | 'CREATE', 31 | 'CURRENT', 32 | 'DATABASE', 33 | 'DATASET', 34 | 'DATASTORE', 35 | 'DECLARE', 36 | 'DECREMENT', 37 | 'DELETE', 38 | 'DERIVED', 39 | 'DESC', 40 | 'DESCRIBE', 41 | 'DISTINCT', 42 | 'DO', 43 | 'DROP', 44 | 'EACH', 45 | 'ELEMENT', 46 | 'ELSE', 47 | 'END', 48 | 'EVERY', 49 | 'EXCEPT', 50 | 'EXCLUDE', 51 | 'EXECUTE', 52 | 'EXISTS', 53 | 'EXPLAIN', 54 | 'FALSE', 55 | 'FETCH', 56 | 'FIRST', 57 | 'FLATTEN', 58 | 'FOLLOWING', 59 | 'FOR', 60 | 'FORCE', 61 | 'FROM', 62 | 'FTS', 63 | 'FUNCTION', 64 | 'GOLANG', 65 | 'GRANT', 66 | 'GROUP', 67 | 'GROUPS', 68 | 'GSI', 69 | 'HASH', 70 | 'HAVING', 71 | 'IF', 72 | 'IGNORE', 73 | 'ILIKE', 74 | 'IN', 75 | 'INCLUDE', 76 | 'INCREMENT', 77 | 'INDEX', 78 | 'INFER', 79 | 'INLINE', 80 | 'INNER', 81 | 'INSERT', 82 | 'INTERSECT', 83 | 'INTO', 84 | 'IS', 85 | 'JAVASCRIPT', 86 | 'JOIN', 87 | 'KEY', 88 | 'KEYS', 89 | 'KEYSPACE', 90 | 'KNOWN', 91 | 'LANGUAGE', 92 | 'LAST', 93 | 'LEFT', 94 | 'LET', 95 | 'LETTING', 96 | 'LIKE', 97 | 'LIMIT', 98 | 'LSM', 99 | 'MAP', 100 | 'MAPPING', 101 | 'MATCHED', 102 | 'MATERIALIZED', 103 | 'MERGE', 104 | 'MINUS', 105 | 'MISSING', 106 | 'NAMESPACE', 107 | 'NEST', 108 | 'NL', 109 | 'NO', 110 | 'NOT', 111 | 'NTH_VALUE', 112 | 'NULL', 113 | 'NULLS', 114 | 'NUMBER', 115 | 'OBJECT', 116 | 'OFFSET', 117 | 'ON', 118 | 'OPTION', 119 | 'OPTIONS [1]', 120 | 'OR', 121 | 'ORDER', 122 | 'OTHERS', 123 | 'OUTER', 124 | 'OVER', 125 | 'PARSE', 126 | 'PARTITION', 127 | 'PASSWORD', 128 | 'PATH', 129 | 'POOL', 130 | 'PRECEDING', 131 | 'PREPARE', 132 | 'PRIMARY', 133 | 'PRIVATE', 134 | 'PRIVILEGE', 135 | 'PROBE', 136 | 'PROCEDURE', 137 | 'PUBLIC', 138 | 'RANGE', 139 | 'RAW', 140 | 'REALM', 141 | 'REDUCE', 142 | 'RENAME', 143 | 'RESPECT', 144 | 'RETURN', 145 | 'RETURNING', 146 | 'REVOKE', 147 | 'RIGHT', 148 | 'ROLE', 149 | 'ROLLBACK', 150 | 'ROW', 151 | 'ROWS', 152 | 'SATISFIES', 153 | 'SCHEMA', 154 | 'SELECT', 155 | 'SELF', 156 | 'SEMI', 157 | 'SET', 158 | 'SHOW', 159 | 'SOME', 160 | 'START', 161 | 'STATISTICS', 162 | 'STRING', 163 | 'SYSTEM', 164 | 'THEN', 165 | 'TIES', 166 | 'TO', 167 | 'TRANSACTION', 168 | 'TRIGGER', 169 | 'TRUE', 170 | 'TRUNCATE', 171 | 'UNBOUNDED', 172 | 'UNDER', 173 | 'UNION', 174 | 'UNIQUE', 175 | 'UNKNOWN', 176 | 'UNNEST', 177 | 'UNSET', 178 | 'UPDATE', 179 | 'UPSERT', 180 | 'USE', 181 | 'USER', 182 | 'USING', 183 | 'VALIDATE', 184 | 'VALUE', 185 | 'VALUED', 186 | 'VALUES', 187 | 'VIA', 188 | 'VIEW', 189 | 'WHEN', 190 | 'WHERE', 191 | 'WHILE', 192 | 'WITH', 193 | 'WITHIN', 194 | 'WORK', 195 | 'XOR', 196 | ]; 197 | -------------------------------------------------------------------------------- /src/query/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | selectBuilder, 3 | buildSelectExpr, 4 | buildWhereClauseExpr, 5 | buildIndexExpr, 6 | buildSelectArrayExpr, 7 | AggDict, 8 | ResultExprDict, 9 | ReturnResultDict, 10 | } from './helpers'; 11 | export { 12 | SelectClauseException, 13 | MultipleQueryTypesException, 14 | WhereClauseException, 15 | QueryOperatorNotFoundException, 16 | IndexParamsUsingGSIExceptions, 17 | QueryGroupByParamsException, 18 | IndexParamsOnExceptions, 19 | } from './exceptions'; 20 | export { Query } from './query'; 21 | export { 22 | LetExprType, 23 | SortType, 24 | ISelectType, 25 | LogicalWhereExpr, 26 | ComparisonWhereExpr, 27 | IField, 28 | IIndexOnParams, 29 | IIndexWithParams, 30 | IndexType, 31 | ISelectAggType, 32 | AggType, 33 | ComparisonEmptyOperatorType, 34 | ComparisonMultipleOperatorType, 35 | ComparisonSingleStringOperatorType, 36 | ComparisonSingleOperatorType, 37 | LogicalOperatorType, 38 | ResultExprType, 39 | ReturnResultType, 40 | IConditionExpr, 41 | } from './interface/query.types'; 42 | export { parseStringSelectExpr, escapeReservedWords } from './utils'; 43 | -------------------------------------------------------------------------------- /src/query/utils.ts: -------------------------------------------------------------------------------- 1 | import { n1qlReservedWords } from './helpers'; 2 | 3 | const replaceList = ['ALL', 'DISTINCT', 'RAW', 'ELEMENT', 'VALUE']; 4 | 5 | export const escapeFromClause = (str: string) => { 6 | const trimStr = str.trim(); 7 | const [collection, ...rest] = trimStr.split(' '); 8 | const parts = collection.split('.'); 9 | const result: string[] = []; 10 | for (const part of parts) { 11 | const cleanBacksticks = part.replace(/`/g, ''); 12 | result.push(`\`${cleanBacksticks}\``); 13 | } 14 | return `${result.join('.')}${rest.length > 0 ? ` ${rest.map((item) => item.replace(/`/g, '')).join(' ')}` : ''}`; 15 | }; 16 | 17 | /** 18 | * Convert select expression into an Array of selection keys 19 | * */ 20 | export const parseStringSelectExpr = (expr: string): string[] => { 21 | if (expr.indexOf(',') === -1 && expr.indexOf(' as ') === -1) { 22 | return [expr]; 23 | } 24 | let resultExpr = expr.replace(/[()]/g, ''); 25 | replaceList.forEach((value: string) => { 26 | resultExpr = resultExpr.replace(new RegExp(`/[${value}]/`, 'g'), ''); 27 | }); 28 | return resultExpr.split(',').map((v: string) => { 29 | return extractAsValue(v); 30 | }); 31 | }; 32 | 33 | /** 34 | * @ignore 35 | * */ 36 | const extractAsValue = (expr: string): string => { 37 | const result = expr.toLowerCase().split(' as '); 38 | if (result.length === 2) { 39 | return result[1].trim(); 40 | } 41 | return expr.trim(); 42 | }; 43 | 44 | /** 45 | * @ignore 46 | */ 47 | export const escapeReservedWords = (field: string) => { 48 | if (n1qlReservedWords.includes(field.toUpperCase())) { 49 | return `\`${field}\``; 50 | } 51 | if (field.match(/(\-)|(\.)|(\[\d+\])/g)) { 52 | let expr = field; 53 | expr = expr 54 | .split('.') 55 | .map((value) => { 56 | if (n1qlReservedWords.includes(value.toUpperCase())) { 57 | return `\`${value}\``; 58 | } 59 | return value; 60 | }) 61 | .join('.'); 62 | expr = expr.replace(/([a-z0-9]*\-[a-z0-9]*)/g, '`$&`'); 63 | return expr; 64 | } 65 | return field; 66 | }; 67 | -------------------------------------------------------------------------------- /src/schema/errors/build-schema-error.ts: -------------------------------------------------------------------------------- 1 | import { OttomanError } from '../../exceptions/ottoman-errors'; 2 | 3 | export class BuildSchemaError extends OttomanError {} 4 | -------------------------------------------------------------------------------- /src/schema/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { BuildSchemaError } from './build-schema-error'; 2 | export { ValidationError } from './validation-error'; 3 | -------------------------------------------------------------------------------- /src/schema/errors/validation-error.ts: -------------------------------------------------------------------------------- 1 | import { OttomanError } from '../../exceptions/ottoman-errors'; 2 | 3 | export class ValidationError extends OttomanError {} 4 | -------------------------------------------------------------------------------- /src/schema/helpers/date-minmax.ts: -------------------------------------------------------------------------------- 1 | export interface DateOption { 2 | val: Date; 3 | message: string; 4 | } 5 | export type DateFunction = () => Date | DateOption; 6 | 7 | export const validateMinDate = (value: Date, min: Date | DateOption, property: string): string | void => { 8 | const _minDate: Date = (min as DateOption).val !== undefined ? (min as DateOption).val : (min as Date); 9 | if (_minDate > value) { 10 | return (min as DateOption).message !== undefined 11 | ? (min as DateOption).message 12 | : `Property ${property} cannot allow dates before ${_minDate.toISOString()}`; 13 | } 14 | }; 15 | 16 | export const validateMaxDate = (value: Date, max: Date | DateOption, property: string): string | void => { 17 | const _maxDate: Date = (max as DateOption).val !== undefined ? (max as DateOption).val : (max as Date); 18 | if (_maxDate < value) { 19 | return (max as DateOption).message !== undefined 20 | ? (max as DateOption).message 21 | : `Property ${property} cannot allow dates after ${_maxDate.toISOString()}`; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/schema/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { applyValidator } from './validator'; 2 | export { MinmaxOption, NumberFunction, validateMinLimit, validateMaxLimit } from './number-minmax'; 3 | export { DateOption, DateFunction, validateMinDate, validateMaxDate } from './date-minmax'; 4 | export { buildFields, validate, applyDefaultValue, registerType, addValidators } from './fn-schema'; 5 | export { isOttomanType } from './is-ottoman-type'; 6 | -------------------------------------------------------------------------------- /src/schema/helpers/is-ottoman-type.ts: -------------------------------------------------------------------------------- 1 | export const isOttomanType = (object): boolean => 'name' in object && 'typeName' in object && 'cast' in object; 2 | -------------------------------------------------------------------------------- /src/schema/helpers/number-minmax.ts: -------------------------------------------------------------------------------- 1 | export interface MinmaxOption { 2 | message: string; 3 | val: number; 4 | } 5 | 6 | export type NumberFunction = () => number | MinmaxOption; 7 | 8 | export const validateMinLimit = ( 9 | val: number, 10 | min: number | MinmaxOption | undefined, 11 | property: string, 12 | ): string | void => { 13 | if (typeof min === 'number' && min > val) { 14 | return `Property '${property}' is less than the minimum allowed value of '${min}'`; 15 | } 16 | if (typeof min !== 'undefined') { 17 | const _obj = min as MinmaxOption; 18 | if (_obj.val > val) { 19 | return _obj.message; 20 | } 21 | } 22 | }; 23 | 24 | export const validateMaxLimit = ( 25 | val: number, 26 | max: number | MinmaxOption | undefined, 27 | property: string, 28 | ): string | void => { 29 | if (typeof max === 'number' && max < val) { 30 | return `Property '${property}' is more than the maximum allowed value of '${max}'`; 31 | } 32 | if (typeof max !== 'undefined') { 33 | const _obj = max as MinmaxOption; 34 | if (_obj.val < val) { 35 | return _obj.message; 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/schema/helpers/validator.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../schema'; 2 | import { BuildSchemaError, ValidationError } from '../errors'; 3 | import { ValidatorFunction, ValidatorOption } from '../interfaces/schema.types'; 4 | 5 | export const applyValidator = ( 6 | val: unknown, 7 | validator: ValidatorOption | ValidatorFunction | string | undefined, 8 | name?: string, 9 | ): string | void => { 10 | let _validator: ValidatorFunction | undefined = undefined; 11 | if (validator !== undefined) { 12 | switch (typeof validator) { 13 | case 'string': 14 | if (typeof Schema.validators[validator] === 'undefined') { 15 | throw new BuildSchemaError(`Validator '${validator}' for field '${name}' does not exist.`); 16 | } 17 | _validator = Schema.validators[validator]; 18 | break; 19 | case 'function': 20 | _validator = validator; 21 | break; 22 | case 'object': 23 | if (!validator.regexp !== undefined) { 24 | _validator = (value) => { 25 | if (!validator.regexp.test(String(value))) { 26 | throw new ValidationError(validator.message); 27 | } 28 | }; 29 | } 30 | } 31 | if (_validator !== undefined) { 32 | _validator(val); 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export { validate, applyDefaultValue, buildFields, registerType, addValidators } from './helpers'; 2 | export { ValidationError, BuildSchemaError } from './errors'; 3 | export { 4 | ReferenceType, 5 | MixedType, 6 | EmbedType, 7 | StringType, 8 | ArrayType, 9 | BooleanType, 10 | NumberType, 11 | DateType, 12 | CoreType, 13 | } from './types'; 14 | export { Schema } from './schema'; 15 | export { IOttomanType, ValidatorOption } from './interfaces/schema.types'; 16 | -------------------------------------------------------------------------------- /src/schema/interfaces/schema.types.ts: -------------------------------------------------------------------------------- 1 | import { HOOKS } from '../../utils/hooks'; 2 | import { CoreType } from '../types'; 3 | import { CAST_STRATEGY } from '../../utils/cast-strategy'; 4 | 5 | export type SchemaDef = Record; 6 | export type FieldMap = { [key: string]: IOttomanType }; 7 | export type PluginConstructor = (Schema) => void; 8 | export type FactoryFunction = (name, options) => IOttomanType; 9 | /** 10 | * Should throw all errors detected. 11 | */ 12 | export type OttomanSchemaTypes = 'String' | 'Boolean' | 'Number' | 'Date' | 'Array' | 'Reference' | 'Embed' | 'Mixed'; 13 | export type ValidatorFunction = (value: unknown) => void; 14 | export type AutoFunction = () => unknown; 15 | export type SupportFactoryTypes = { [key in OttomanSchemaTypes]: FactoryFunction }; 16 | export type SupportTypes = { [key in OttomanSchemaTypes]: CoreType }; 17 | export type CustomValidations = { [key: string]: ValidatorFunction }; 18 | export type RequiredFunction = () => boolean | RequiredOption; 19 | export type HookHandler = (IDocument) => void; 20 | export type Hook = { 21 | [key in HOOKS]?: HookHandler[] | HookHandler; 22 | }; 23 | 24 | export interface ValidatorOption { 25 | regexp: RegExp; 26 | message: string; 27 | } 28 | 29 | export interface RequiredOption { 30 | val: boolean; 31 | message: string; 32 | } 33 | 34 | interface SchemaTimestampsConfig { 35 | createdAt?: boolean | string; 36 | updatedAt?: boolean | string; 37 | currentTime?: () => Date | number; 38 | } 39 | 40 | export interface CoreTypeOptions { 41 | required?: boolean | RequiredOption | RequiredFunction; 42 | /** 43 | * If truthy, Ottoman will disallow changes to this path once the document is saved to the database for the first time. 44 | **/ 45 | immutable?: boolean; 46 | default?: unknown; 47 | validator?: ValidatorOption | ValidatorFunction | string; 48 | } 49 | 50 | export abstract class IOttomanType { 51 | protected constructor(public name: string, public typeName: string) {} 52 | abstract cast(value: unknown, strategy?: CAST_STRATEGY): unknown; 53 | abstract validate(value: unknown, strict?: boolean): unknown; 54 | } 55 | 56 | export interface SchemaOptions { 57 | strict?: boolean; 58 | preHooks?: Hook; 59 | postHooks?: Hook; 60 | /** 61 | * The timestamps option tells Ottoman to assign createdAt and updatedAt fields to your schema. The type 62 | * assigned is `Date`. By default, the names of the fields are createdAt and updatedAt. Customize the 63 | * field names by setting timestamps.createdAt and timestamps.updatedAt. 64 | */ 65 | timestamps?: boolean | SchemaTimestampsConfig; 66 | enforceRefCheck?: boolean | 'throw'; 67 | } 68 | -------------------------------------------------------------------------------- /src/schema/types/array-type.ts: -------------------------------------------------------------------------------- 1 | import { CoreType } from './core-type'; 2 | import { is } from '../../utils'; 3 | import { ValidationError } from '../errors'; 4 | import { CoreTypeOptions, IOttomanType } from '../interfaces/schema.types'; 5 | import { CAST_STRATEGY, checkCastStrategy, ensureArrayItemsType } from '../../utils/cast-strategy'; 6 | 7 | /** 8 | * Array type supports arrays of [SchemaTypes](/docs/api/classes/schema.html) and arrays of subdocuments. 9 | * 10 | * ## Options 11 | * 12 | * - **required** flag to define if the field is mandatory 13 | * - **validator** that will be applied to the field, allowed function, object or string with the name of the custom validator 14 | * - **default** that will define the initial value of the field, allowed and value or function to generate it 15 | * - **immutable** that will define this field as immutable. Ottoman prevents you from changing immutable fields if the schema as configure like strict 16 | * 17 | * @example 18 | * ```typescript 19 | * const postSchemaDef = new Schema({ 20 | * postTags: [String], 21 | * comments: [{ type: commentSchema, ref: 'Comment' }], 22 | * }); 23 | * ``` 24 | */ 25 | export class ArrayType extends CoreType { 26 | constructor(name: string, public itemType: IOttomanType, options?: CoreTypeOptions) { 27 | super(name, ArrayType.sName, options); 28 | } 29 | 30 | static sName = Array.name; 31 | 32 | cast(value: unknown, strategy = CAST_STRATEGY.DEFAULT_OR_DROP): unknown { 33 | if (is(value, Array)) { 34 | return ensureArrayItemsType(value, this.itemType, strategy); 35 | } 36 | return checkCastStrategy(value, strategy, this); 37 | } 38 | 39 | validate(value: unknown, strict = true) { 40 | value = super.validate(value, strict); 41 | if (this.isEmpty(value)) return value; 42 | if (!is(value, Array)) { 43 | throw new ValidationError(`Property '${this.name}' must be of type '${this.typeName}'`); 44 | } 45 | const _value = value as unknown[]; 46 | const _valueResult: unknown[] = []; 47 | this.checkValidator(_value); 48 | for (const key in _value) { 49 | _valueResult.push(this.itemType.validate(_value[key], strict)); 50 | } 51 | return _valueResult; 52 | } 53 | } 54 | 55 | export const arrayTypeFactory = (name: string, item: CoreType, options?: CoreTypeOptions): ArrayType => 56 | new ArrayType(name, item, options); 57 | -------------------------------------------------------------------------------- /src/schema/types/boolean-type.ts: -------------------------------------------------------------------------------- 1 | import { CoreType } from './core-type'; 2 | import { CoreTypeOptions } from '../interfaces/schema.types'; 3 | import { CAST_STRATEGY, checkCastStrategy } from '../../utils/cast-strategy'; 4 | 5 | /** 6 | * `Boolean` are plain JavaScript Boolean. 7 | * 8 | * ## Options 9 | * 10 | * - **required** flag to define if the field is mandatory 11 | * - **validator** that will be applied to the field a validation function, validation object or string with the name of the custom validator 12 | * - **default** that will define the initial value of the field, this option allows a value or a function 13 | * - **immutable** that will define this field as immutable. Ottoman prevents you from changing immutable fields if the schema as configure like strict 14 | * 15 | * 16 | * @example 17 | * ```typescript 18 | * const userSchema = new Schema({ 19 | * isActive: Boolean, 20 | * isSomething: Schema.Types.Boolean 21 | * }) 22 | * ``` 23 | * 24 | * ### Ottoman will cast the following values to true: 25 | * * true 26 | * * 'true' 27 | * * 1 28 | * * '1' 29 | * * 'yes' 30 | * 31 | * ### Ottoman will cast the following values to false: 32 | * * false 33 | * * 'false' 34 | * * 0 35 | * * '0' 36 | * * 'no' 37 | */ 38 | export class BooleanType extends CoreType { 39 | constructor(name: string, options?: CoreTypeOptions) { 40 | super(name, BooleanType.sName, options); 41 | } 42 | static sName = Boolean.name; 43 | 44 | static convertToTrue = new Set([true, 'true', 1, '1', 'yes']); 45 | static convertToFalse = new Set([false, 'false', 0, '0', 'no']); 46 | 47 | cast(value, strategy = CAST_STRATEGY.DEFAULT_OR_DROP) { 48 | if (BooleanType.convertToTrue.has(value)) { 49 | return true; 50 | } else if (BooleanType.convertToFalse.has(value)) { 51 | return false; 52 | } else { 53 | return checkCastStrategy(value, strategy, this); 54 | } 55 | } 56 | 57 | validate(value, strategy) { 58 | value = super.validate(value, strategy); 59 | const _value = this.cast(value, strategy); 60 | if (_value === undefined || _value === null) return value; 61 | this.checkValidator(value); 62 | return _value; 63 | } 64 | 65 | isEmpty(value: boolean): boolean { 66 | return value === undefined || value === null; 67 | } 68 | } 69 | 70 | export const booleanTypeFactory = (key: string, opts: CoreTypeOptions): BooleanType => new BooleanType(key, opts); 71 | -------------------------------------------------------------------------------- /src/schema/types/core-type.ts: -------------------------------------------------------------------------------- 1 | import { applyValidator } from '../helpers'; 2 | import { ValidationError } from '../errors'; 3 | import { 4 | CoreTypeOptions, 5 | IOttomanType, 6 | RequiredFunction, 7 | RequiredOption, 8 | ValidatorFunction, 9 | ValidatorOption, 10 | } from '../interfaces/schema.types'; 11 | import { VALIDATION_STRATEGY } from '../../utils'; 12 | 13 | /** 14 | * @param name of field in schema 15 | * @param typeName name of type 16 | * @param options 17 | * @param options.required flag to define if the field is mandatory 18 | * @param options.validator that will be applied to the field, allowed function, object or string with the name of the custom validator 19 | * @param options.default that will define the initial value of the field, this option allows a value or a function 20 | * @param options.immutable that will define this field as immutable. Ottoman prevents you from changing immutable fields if the schema as configure like strict 21 | */ 22 | export abstract class CoreType extends IOttomanType { 23 | protected constructor(name: string, typeName: string, public options?: CoreTypeOptions) { 24 | super(name, typeName); 25 | this.name = name; 26 | this.typeName = typeName; 27 | } 28 | 29 | static sName; 30 | 31 | get required(): boolean | RequiredOption | RequiredFunction { 32 | return this.options?.required || false; 33 | } 34 | 35 | get validator(): ValidatorOption | ValidatorFunction | string | undefined { 36 | return this.options?.validator; 37 | } 38 | 39 | get default(): unknown { 40 | return this.options?.default; 41 | } 42 | 43 | buildDefault(): unknown { 44 | if (typeof this.default === 'function') { 45 | return this.default(); 46 | } else { 47 | return this.default; 48 | } 49 | } 50 | 51 | // eslint-disable-next-line no-unused-vars 52 | validate(value: unknown, strict = true): unknown { 53 | if (this.isEmpty(value)) { 54 | const _required = this.checkRequired() || ''; 55 | if (_required.length > 0) { 56 | throw new ValidationError(_required); 57 | } 58 | } 59 | return value; 60 | } 61 | 62 | checkRequired(): string | void { 63 | const _required = (typeof this.required === 'function' ? this.required() : this.required) as RequiredOption; 64 | if (typeof _required.val !== 'undefined' && _required.val) { 65 | return _required.message; 66 | } else if (!!_required) { 67 | return `Property '${this.name}' is required`; 68 | } 69 | } 70 | 71 | checkValidator(value: unknown): void { 72 | applyValidator(value, this.validator, this.name); 73 | } 74 | 75 | isEmpty(value: unknown): boolean { 76 | return value === undefined || value === null; 77 | } 78 | 79 | isStrictStrategy(strategy: VALIDATION_STRATEGY): boolean { 80 | return strategy == VALIDATION_STRATEGY.STRICT; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/schema/types/date-type.ts: -------------------------------------------------------------------------------- 1 | import { CoreType } from './core-type'; 2 | import { DateFunction, DateOption, validateMaxDate, validateMinDate } from '../helpers'; 3 | import { ValidationError } from '../errors'; 4 | import { CoreTypeOptions } from '../interfaces/schema.types'; 5 | import { is } from '../../utils'; 6 | import { CAST_STRATEGY, checkCastStrategy } from '../../utils/cast-strategy'; 7 | import { isDateValid } from '../../utils/type-helpers'; 8 | 9 | /** 10 | * @field `min` date value that will be accepted 11 | * @field `max` date value that will be accepted 12 | * */ 13 | interface DateTypeOptions { 14 | min?: Date | DateOption | DateFunction | string; 15 | max?: Date | DateOption | DateFunction | string; 16 | } 17 | 18 | /** 19 | * `Date` are plain JavaScript Date. 20 | * 21 | * ## Options 22 | * 23 | * - **required** flag to define if the field is mandatory 24 | * - **validator** that will be applied to the field a validation function, validation object or string with the name of the custom validator 25 | * - **default** that will define the initial value of the field, this option allows a value or a function 26 | * - **immutable** that will define this field as immutable. Ottoman prevents you from changing immutable fields if the schema as configure like strict 27 | * - **min** minimum date value that will be accepted 28 | * - **max** maximum date value that will be accepted 29 | * 30 | * @example 31 | * ```typescript 32 | * const userSchema = new Schema({ 33 | * birthday: { type: Date, min: '1990-12-31', max: new Date() }, 34 | * hired: Schema.Types.Date 35 | * }) 36 | * ``` 37 | */ 38 | export class DateType extends CoreType { 39 | constructor(name: string, options?: DateTypeOptions & CoreTypeOptions) { 40 | super(name, DateType.sName, options); 41 | } 42 | 43 | static sName = Date.name; 44 | 45 | get min(): Date | DateOption | DateFunction | undefined { 46 | const _min = (this.options as DateTypeOptions).min; 47 | if (typeof _min === 'string') { 48 | return new Date(String(_min)); 49 | } 50 | return _min; 51 | } 52 | 53 | get max(): Date | DateOption | DateFunction | undefined { 54 | const _max = (this.options as DateTypeOptions).max; 55 | if (typeof _max === 'string') { 56 | return new Date(String(_max)); 57 | } 58 | return _max; 59 | } 60 | 61 | buildDefault(): Date | undefined { 62 | const result: any = super.buildDefault(); 63 | if (result) { 64 | return !(result instanceof Date) ? new Date(String(result)) : (result as Date); 65 | } 66 | return result; 67 | } 68 | 69 | cast(value: any, strategy = CAST_STRATEGY.DEFAULT_OR_DROP) { 70 | if (isDateValid(value)) { 71 | return new Date(value); 72 | } else { 73 | return checkCastStrategy(value, strategy, this); 74 | } 75 | } 76 | 77 | validate(value: unknown, strategy) { 78 | value = super.validate(value, strategy); 79 | if (this.isEmpty(value)) return value; 80 | const _value = this.isStrictStrategy(strategy) 81 | ? is(value, Date) 82 | ? (value as Date) 83 | : undefined 84 | : is(value, Date) 85 | ? (value as Date) 86 | : is(value, String) 87 | ? new Date(String(value)) 88 | : is(value, Number) 89 | ? new Date(Number(value)) 90 | : undefined; 91 | if (_value === undefined) { 92 | throw new ValidationError(`Property '${this.name}' must be of type '${this.typeName}'`); 93 | } 94 | this.checkValidator(_value); 95 | let errors: string[] = []; 96 | errors.push(this._checkMinDate(_value)); 97 | errors.push(this._checkMaxDate(_value)); 98 | errors = errors.filter((e) => e !== ''); 99 | if (errors.length > 0) { 100 | throw new ValidationError(errors.join('\n')); 101 | } 102 | return _value; 103 | } 104 | 105 | private _checkMinDate(val: Date): string { 106 | const _min = typeof this.min === 'function' ? this.min() : this.min; 107 | if (_min === undefined) { 108 | return ''; 109 | } 110 | return validateMinDate(val, _min, this.name) || ''; 111 | } 112 | 113 | private _checkMaxDate(val: Date): string { 114 | const _max = typeof this.max === 'function' ? this.max() : this.max; 115 | if (_max === undefined) { 116 | return ''; 117 | } 118 | return validateMaxDate(val, _max, this.name) || ''; 119 | } 120 | } 121 | 122 | export const dateTypeFactory = (name: string, opts: DateTypeOptions & CoreTypeOptions): DateType => 123 | new DateType(name, opts); 124 | -------------------------------------------------------------------------------- /src/schema/types/embed-type.ts: -------------------------------------------------------------------------------- 1 | import { CoreType } from './core-type'; 2 | import { Schema } from '../schema'; 3 | import { isModel } from '../../utils/is-model'; 4 | import { is } from '../../utils'; 5 | import { ValidationError } from '../errors'; 6 | import { CoreTypeOptions } from '../interfaces/schema.types'; 7 | import { cast, CAST_STRATEGY, checkCastStrategy } from '../../utils/cast-strategy'; 8 | 9 | /** 10 | * `EmbedType` will allow declaration of path as another schema, set type to the sub-schema's instance. 11 | * 12 | * ## Options 13 | * 14 | * - **required** flag to define if the field is mandatory 15 | * - **validator** that will be applied to the field a validation function, validation object or string with the name of the custom validator 16 | * - **default** that will define the initial value of the field, this option allows a value or a function 17 | * - **immutable** that will define this field as immutable. Ottoman prevents you from changing immutable fields if the schema as configure like strict 18 | * 19 | * @example 20 | * ```javascript 21 | * const userSchema = new Schema({ 22 | * name: String, 23 | * email: Schema.Types.String, 24 | * createAt: Date, 25 | * }); 26 | * 27 | * const schema = new Schema({ 28 | * ... 29 | * user: userSchema 30 | * }); 31 | * ``` 32 | * You can also use a JavaScript plain Object as value for an `EmbedType`. 33 | * Therefore the below example will behave the same as the example above. 34 | * 35 | * ```javascript 36 | * const schema = new Schema({ 37 | * ... 38 | * user: { 39 | * name: String, 40 | * email: String, 41 | * createAt: Date, 42 | * } 43 | * }); 44 | * ``` 45 | * @tip 46 | * `EmbedType` will allow you to easily reuse existing schemas into new ones using composition. 47 | */ 48 | export class EmbedType extends CoreType { 49 | constructor(name: string, public schema: Schema, options?: CoreTypeOptions) { 50 | super(name, EmbedType.sName, options); 51 | } 52 | static sName = 'Embed'; 53 | 54 | cast(value: unknown, strategy = CAST_STRATEGY.DEFAULT_OR_DROP): unknown { 55 | if (!is(value, Object) && !isModel(value)) { 56 | return checkCastStrategy(value, strategy, this); 57 | } else { 58 | return cast(value, this.schema, { strategy }); 59 | } 60 | } 61 | 62 | validate(value: unknown, strategy) { 63 | value = super.validate(value, strategy); 64 | if (this.isEmpty(value)) return value; 65 | if (!is(value, Object) && !isModel(value)) { 66 | throw new ValidationError(`Property '${this.name}' must be of type '${this.typeName}'`); 67 | } 68 | this.checkValidator(value); 69 | return this.schema.validate(value); 70 | } 71 | } 72 | 73 | export const embedTypeFactory = (name: string, schema: Schema): EmbedType => new EmbedType(name, schema); 74 | -------------------------------------------------------------------------------- /src/schema/types/index.ts: -------------------------------------------------------------------------------- 1 | export { CoreType } from './core-type'; 2 | export { stringTypeFactory, StringType } from './string-type'; 3 | export { booleanTypeFactory, BooleanType } from './boolean-type'; 4 | export { numberTypeFactory, NumberType } from './number-type'; 5 | export { dateTypeFactory, DateType } from './date-type'; 6 | export { arrayTypeFactory, ArrayType } from './array-type'; 7 | export { embedTypeFactory, EmbedType } from './embed-type'; 8 | export { referenceTypeFactory, ReferenceType } from './reference-type'; 9 | export { mixedTypeFactory, MixedType } from './mixed-type'; 10 | -------------------------------------------------------------------------------- /src/schema/types/mixed-type.ts: -------------------------------------------------------------------------------- 1 | import { CoreType } from './core-type'; 2 | import { CoreTypeOptions } from '../interfaces/schema.types'; 3 | 4 | /** 5 | * `Mixed` type supports any `Object` types, you can change the value to anything else, it can be represented in the following ways: 6 | * 7 | * ## Options 8 | * 9 | * - **required** flag to define if the field is mandatory 10 | * - **validator** that will be applied to the field a validation function, validation object or string with the name of the custom validator 11 | * - **default** that will define the initial value of the field, this option allows a value or a function 12 | * - **immutable** that will define this field as immutable. Ottoman prevents you from changing immutable fields if the schema as configure like strict 13 | * 14 | * @example 15 | * ```typescript 16 | * const schema = new Schema({ 17 | * inline: Schema.Types.Mixed, 18 | * types: { type: Schema.Types.Mixed, required: true }, 19 | * obj: Object, 20 | * empty: {}, 21 | * }); 22 | * 23 | * 24 | * schema.fields.inline instanceof MixedType; // true 25 | * schema.fields.types instanceof MixedType; // true 26 | * schema.fields.obj instanceof MixedType; // true 27 | * schema.fields.empty instanceof MixedType; // true 28 | * 29 | * const data = { 30 | * inline: { name: 'george' }, 31 | * types: { 32 | * email: 'george@gmail.com', 33 | * }, 34 | * obj: 'Hello', 35 | * empty: { hello: 'hello' }, 36 | * }; 37 | * 38 | * const result = validate(data, schema); 39 | * console.log(result); 40 | * 41 | * // Output!!! 42 | * // { 43 | * // "inline": { 44 | * // "name": "george" 45 | * // }, 46 | * // "types": { 47 | * // "email": "george@gmail.com" 48 | * // }, 49 | * // "obj": "Hello", 50 | * // "empty": { 51 | * // "hello": "hello" 52 | * // } 53 | * // } 54 | * ``` 55 | */ 56 | export class MixedType extends CoreType { 57 | constructor(name: string, options?: CoreTypeOptions) { 58 | super(name, MixedType.sName, options); 59 | } 60 | 61 | static sName = 'Mixed'; 62 | 63 | cast(value: unknown, strategy) { 64 | value = super.validate(value, strategy); 65 | this.checkValidator(value); 66 | return value; 67 | } 68 | } 69 | 70 | export const mixedTypeFactory = (name: string, otps: CoreTypeOptions): MixedType => new MixedType(name, otps); 71 | -------------------------------------------------------------------------------- /src/schema/types/number-type.ts: -------------------------------------------------------------------------------- 1 | import { CoreType } from './core-type'; 2 | import { MinmaxOption, NumberFunction, validateMaxLimit, validateMinLimit } from '../helpers'; 3 | import { ValidationError } from '../errors'; 4 | import { CoreTypeOptions } from '../interfaces/schema.types.js'; 5 | import { is } from '../../utils'; 6 | import { CAST_STRATEGY, checkCastStrategy } from '../../utils/cast-strategy'; 7 | import { isNumber } from '../../utils/type-helpers'; 8 | 9 | /** 10 | * @field `intVal` flag that will allow only integer values 11 | * @field `min` numeric value that will be accepted 12 | * @field `max` numeric value that will be accepted 13 | * */ 14 | interface NumberTypeOptions { 15 | intVal?: boolean; 16 | min?: number | NumberFunction | MinmaxOption; 17 | max?: number | NumberFunction | MinmaxOption; 18 | } 19 | /** 20 | * `Number` are plain JavaScript Number. 21 | * 22 | * ## Options 23 | * 24 | * - **required** flag to define if the field is mandatory 25 | * - **validator** that will be applied to the field a validation function, validation object or string with the name of the custom validator 26 | * - **default** that will define the initial value of the field, this option allows a value or a function 27 | * - **immutable** that will define this field as immutable. Ottoman prevents you from changing immutable fields if the schema as configure like strict 28 | * - **intVal** flag that will allow only integer values 29 | * - **min** minimum numerical value value that will be accepted 30 | * - **max** maximum numeric value that will be accepted 31 | * 32 | * @example 33 | * ```typescript 34 | * const userSchema = new Schema({ 35 | * age: Number, 36 | * }) 37 | * ``` 38 | */ 39 | export class NumberType extends CoreType { 40 | constructor(name: string, options?: CoreTypeOptions & NumberTypeOptions) { 41 | super(name, NumberType.sName, options); 42 | } 43 | static sName = Number.name; 44 | 45 | get max(): number | NumberFunction | MinmaxOption | undefined { 46 | const _options = this.options as NumberTypeOptions; 47 | return _options.max; 48 | } 49 | 50 | get min(): number | NumberFunction | MinmaxOption | undefined { 51 | const _options = this.options as NumberTypeOptions; 52 | return _options.min; 53 | } 54 | 55 | get intVal(): boolean { 56 | const _options = this.options as NumberTypeOptions; 57 | return typeof _options.intVal === 'undefined' ? false : _options.intVal; 58 | } 59 | 60 | cast(value: unknown, strategy = CAST_STRATEGY.DEFAULT_OR_DROP): unknown { 61 | const castedValue = Number(value); 62 | if (isNumber(castedValue)) { 63 | return castedValue; 64 | } else { 65 | return checkCastStrategy(value, strategy, this); 66 | } 67 | } 68 | 69 | validate(value: unknown, strategy) { 70 | value = super.validate(value, strategy); 71 | if (this.isEmpty(value)) return value; 72 | const _value = Number(value); 73 | let errors: string[] = []; 74 | const _wrongType = this.isStrictStrategy(strategy) ? !is(value, Number) : isNaN(_value); 75 | if (_wrongType) { 76 | throw new ValidationError(`Property '${this.name}' must be of type '${this.typeName}'`); 77 | } 78 | 79 | if (this.intVal && _value % 1 !== 0) { 80 | errors.push(`Property ${this.name} only allows Integer values`); 81 | } 82 | this.checkValidator(_value); 83 | errors.push(this._checkMin(_value)); 84 | errors.push(this._checkMax(_value)); 85 | errors = errors.filter((e) => e !== ''); 86 | if (errors.length > 0) { 87 | throw new ValidationError(errors.join('\n')); 88 | } 89 | 90 | return _value; 91 | } 92 | private _checkMin(val: number): string { 93 | const _min = typeof this.min === 'function' ? this.min() : this.min; 94 | return validateMinLimit(val, _min, this.name) || ''; 95 | } 96 | 97 | private _checkMax(val: number): string { 98 | const _max = typeof this.max === 'function' ? this.max() : this.max; 99 | return validateMaxLimit(val, _max, this.name) || ''; 100 | } 101 | } 102 | 103 | export const numberTypeFactory = (name: string, otps: CoreTypeOptions & NumberTypeOptions): NumberType => 104 | new NumberType(name, otps); 105 | -------------------------------------------------------------------------------- /src/schema/types/reference-type.ts: -------------------------------------------------------------------------------- 1 | import { CoreType } from './core-type'; 2 | import { Schema } from '../schema'; 3 | import { is } from '../../utils'; 4 | import { isModel } from '../../utils/is-model'; 5 | import { ValidationError } from '../errors'; 6 | import { CoreTypeOptions } from '../interfaces/schema.types'; 7 | 8 | interface ReferenceOptions { 9 | schema: Schema; 10 | refModel: string; 11 | } 12 | 13 | /** 14 | * The `Reference` type creates a relationship between two schemas. 15 | * 16 | * ## Options 17 | * 18 | * - **required** flag to define if the field is mandatory 19 | * - **validator** that will be applied to the field a validation function, validation object or string with the name of the custom validator 20 | * - **default** that will define the initial value of the field, this option allows a value or a function 21 | * - **immutable** that will define this field as immutable. Ottoman prevents you from changing immutable fields if the schema as configure like strict 22 | * 23 | * @example 24 | * ```typescript 25 | * const Cat = model('Cat', {name: String}); 26 | * const schema = new Schema({ 27 | * type: String, 28 | * isActive: Boolean, 29 | * name: String, 30 | * cats: [{ type: CatSchema, ref: 'Cat' }], 31 | * }); 32 | * const User = model('User', schema); 33 | * ``` 34 | */ 35 | export class ReferenceType extends CoreType { 36 | constructor(name: string, public schema: Schema, public refModel: string, options?: CoreTypeOptions) { 37 | super(name, ReferenceType.sName, options); 38 | } 39 | static sName = 'Reference'; 40 | 41 | cast(value) { 42 | return value; 43 | } 44 | 45 | validate(value: unknown, strategy) { 46 | super.validate(value, strategy); 47 | if (this.isEmpty(value)) return value; 48 | if (is(value, String)) { 49 | return String(value); 50 | } 51 | if (!is(value, Object) && !isModel(value)) { 52 | throw new ValidationError(`Property '${this.name}' must be of type '${this.typeName}'`); 53 | } 54 | this.checkValidator(value); 55 | return this.schema.validate(value); 56 | } 57 | } 58 | 59 | export const referenceTypeFactory = (name: string, opts: ReferenceOptions): ReferenceType => 60 | new ReferenceType(name, opts.schema, opts.refModel); 61 | -------------------------------------------------------------------------------- /src/utils/cast-strategy.ts: -------------------------------------------------------------------------------- 1 | import { applyDefaultValue, CoreType, IOttomanType, Schema, ValidationError } from '../schema'; 2 | import { TransactionAttemptContext } from 'couchbase'; 3 | 4 | /** 5 | * Cast Strategies 6 | * 'keep' | 'drop' | 'throw' | 'defaultOrDrop' | 'defaultOrKeep' 7 | * 8 | * @desc 9 | * When trying to cast a value to a given type if it fail Cast Strategy will behave this ways for: 10 | * + 'keep' -> The original value will be returned 11 | * + 'drop' -> Return undefined and the object field will be removed. 12 | * + 'throw' -> Throw exception 'Value couldn't be casted to given type' 13 | * + 'defaultOrDrop' -> Try to return default value if exists, if no default value was provided for the current field then it will be removed 14 | * + 'defaultOrDrop' -> Try to return default value if exists, if no default value was provided for the current field then it will keep original value. 15 | */ 16 | export enum CAST_STRATEGY { 17 | KEEP = 'keep', 18 | DROP = 'drop', 19 | THROW = 'throw', 20 | DEFAULT_OR_DROP = 'defaultOrDrop', 21 | DEFAULT_OR_KEEP = 'defaultOrKeep', 22 | } 23 | 24 | export const ensureArrayItemsType = (array, field, strategy) => array.map((item) => ensureTypes(item, field, strategy)); 25 | 26 | export const ensureTypes = (item, field: IOttomanType, strategy?: CAST_STRATEGY) => { 27 | if (field) { 28 | return field.cast(item, strategy); 29 | } 30 | }; 31 | 32 | export interface CastOptions { 33 | strategy?: CAST_STRATEGY; 34 | strict?: boolean; 35 | skip?: string[]; 36 | } 37 | 38 | /** 39 | * @desc 40 | * Used when trying to apply a value to a given immutable property: 41 | * + **false** -> Allow apply the new value 42 | * + **true** -> Don't allow apply the new value 43 | * + **'throw'** -> Throw exception "ImmutableError: Field 'field_name' is immutable and current cast strategy is set to 'throw'" 44 | */ 45 | export type ApplyStrategy = boolean | CAST_STRATEGY.THROW; 46 | 47 | /** 48 | * @desc 49 | * Used by the mutation functions to apply the defined strategy. 50 | */ 51 | export type MutationFunctionOptions = { 52 | strict?: ApplyStrategy; 53 | maxExpiry?: number; 54 | enforceRefCheck?: boolean | 'throw'; 55 | transactionContext?: TransactionAttemptContext; 56 | }; 57 | 58 | export const cast = ( 59 | data, 60 | schema, 61 | options: CastOptions = { 62 | strategy: CAST_STRATEGY.DEFAULT_OR_DROP, 63 | strict: true, 64 | skip: [], 65 | }, 66 | ) => { 67 | const skip = options.skip || []; 68 | const strategy = options.strategy || CAST_STRATEGY.DEFAULT_OR_DROP; 69 | const strict = options.strict || false; 70 | if (strict && (strategy === CAST_STRATEGY.KEEP || strategy === CAST_STRATEGY.DEFAULT_OR_KEEP)) { 71 | throw new ValidationError(`Cast Strategy 'keep' or 'defaultOrKeep' isn't support when strict is set to true.`); 72 | } 73 | const result: any = {}; 74 | let _data = { ...data }; 75 | const _schema = schema instanceof Schema ? schema : new Schema(schema); 76 | 77 | if ( 78 | strategy === CAST_STRATEGY.DEFAULT_OR_DROP || 79 | strategy === CAST_STRATEGY.DEFAULT_OR_KEEP || 80 | strategy === CAST_STRATEGY.THROW 81 | ) { 82 | _data = applyDefaultValue(_data, _schema); 83 | } 84 | for (const key in _data) { 85 | if (_data.hasOwnProperty(key)) { 86 | if (_schema.fields[key] && !skip.includes(key)) { 87 | const field: IOttomanType = _schema.fields[key]; 88 | const value = ensureTypes(_data[key], field, strategy); 89 | switch (strategy) { 90 | case CAST_STRATEGY.KEEP: 91 | result[key] = value; 92 | break; 93 | case CAST_STRATEGY.DROP: 94 | case CAST_STRATEGY.DEFAULT_OR_DROP: 95 | default: 96 | if (value !== undefined) { 97 | result[key] = value; 98 | } 99 | } 100 | } else { 101 | if (!strict || skip.includes(key)) { 102 | result[key] = _data[key]; 103 | } 104 | } 105 | } 106 | } 107 | return result; 108 | }; 109 | 110 | export const checkCastStrategy = (value: unknown, strategy: CAST_STRATEGY, type: CoreType | IOttomanType) => { 111 | switch (strategy) { 112 | case CAST_STRATEGY.KEEP: 113 | case CAST_STRATEGY.DEFAULT_OR_KEEP: 114 | return value; 115 | case CAST_STRATEGY.THROW: 116 | throw new ValidationError(`Property '${type.name}' must be of type '${type.typeName}'`); 117 | case CAST_STRATEGY.DROP: 118 | case CAST_STRATEGY.DEFAULT_OR_DROP: 119 | default: 120 | return undefined; 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { BadKeyGeneratorDelimiterError } from '../exceptions/ottoman-errors'; 2 | 3 | /** 4 | * Default value for metadata key, to keep collection tracking 5 | * eg. This model new Model('User', schema, options), will generate documents -> document._type = 'User' 6 | */ 7 | export const MODEL_KEY = '_type'; 8 | 9 | /** 10 | * Key to add document metadata identifier 11 | * This key will store the value of the key, 12 | * key -> document, document.id === key 13 | */ 14 | export const DEFAULT_ID_KEY = 'id'; 15 | 16 | /** 17 | * Default scope name. 18 | */ 19 | export const DEFAULT_SCOPE = '_default'; 20 | 21 | /** 22 | * Default collection name. 23 | */ 24 | export const DEFAULT_COLLECTION = '_default'; 25 | 26 | /** 27 | * Default KeyGenerator function. 28 | */ 29 | export const KEY_GENERATOR = ({ metadata }) => `${metadata.modelName}`; 30 | 31 | /** 32 | * Default KeyGeneratorDelimiter value. 33 | */ 34 | export const KEY_GENERATOR_DELIMITER = '::'; 35 | 36 | /** 37 | * Internal KeyGenerator function. 38 | * @param keyGen 39 | * @param metadata 40 | * @param id 41 | */ 42 | export const _keyGenerator = (keyGen, { metadata, id }, delimeter = KEY_GENERATOR_DELIMITER) => { 43 | const prefix = keyGen({ metadata }); 44 | const prefixDelimeter = `${prefix || ''}${delimeter || ''}`; 45 | if (prefixDelimeter && id.toString().startsWith(prefixDelimeter)) { 46 | return id; 47 | } 48 | return `${prefixDelimeter}${id}`; 49 | }; 50 | 51 | function isValidDelimiter(str) { 52 | return /[~!#$%&*_\-:<>?|]/g.test(str); 53 | } 54 | 55 | export const validateDelimiter = (delimiter: string) => { 56 | if (delimiter.length > 2) { 57 | throw new BadKeyGeneratorDelimiterError(`keyGeneratorDelimiter only support up to 2 characters`); 58 | } 59 | 60 | if (!isValidDelimiter(delimiter)) { 61 | throw new BadKeyGeneratorDelimiterError( 62 | `Invalid keyGeneratorDelimiter value, the supported characters ~!#$%&*_-:<>?`, 63 | ); 64 | } 65 | }; 66 | 67 | export const DEFAULT_MAX_EXPIRY = 0; 68 | -------------------------------------------------------------------------------- /src/utils/environment-set.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from '../schema'; 2 | 3 | export const set = (key: string, value: number | string | boolean) => { 4 | const valueType = typeof value; 5 | if (typeof value === 'string' && key) { 6 | process.env[key] = value; 7 | } else if ((valueType === 'boolean' || valueType === 'number') && key) { 8 | process.env[key] = String(value); 9 | } else { 10 | if (!key) { 11 | throw new ValidationError('set first argument required a valid string value'); 12 | } 13 | throw new ValidationError('set second argument must be number | string | boolean value'); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/extract-connection-string.ts: -------------------------------------------------------------------------------- 1 | import { ConnectOptions } from '../ottoman/ottoman'; 2 | 3 | export const extractConnectionString = (connectOptions: string): ConnectOptions => { 4 | const [connUrl, credentials] = connectOptions.split('@'); 5 | const [username, password] = credentials.split(':'); 6 | const lastIndex = connUrl.lastIndexOf('/'); 7 | const connectionString = connUrl.substring(0, lastIndex); 8 | const bucketName = connUrl.substring(lastIndex + 1); 9 | return { connectionString, username, password, bucketName }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/extract-data-from-model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get a Model instance and return a JavaScript Object. 3 | */ 4 | export const extractDataFromModel = (document) => { 5 | document._depopulate(); 6 | return { ...document }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/generate-uuid.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | export const generateUUID = () => { 4 | return uuidv4(); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/getValueByPath.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export const getValueByPath = >(obj: T, path: string): any => _.get(obj, path); 4 | -------------------------------------------------------------------------------- /src/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Hooks available 3 | * 'validate' | 'save' | 'update' | 'remove' 4 | */ 5 | export enum HOOKS { 6 | VALIDATE = 'validate', 7 | SAVE = 'save', 8 | UPDATE = 'update', 9 | REMOVE = 'remove', 10 | } 11 | 12 | export type HookTypes = HOOKS | 'validate' | 'save' | 'update' | 'remove'; 13 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { isDocumentNotFoundError } from './is-not-found'; 2 | export { VALIDATION_STRATEGY } from './validation.strategy'; 3 | export { is, isSchemaTypeSupported, isSchemaFactoryType } from './is-type'; 4 | export { set } from './environment-set'; 5 | export { setValueByPath } from './setValueByPath'; 6 | export { getValueByPath } from './getValueByPath'; 7 | -------------------------------------------------------------------------------- /src/utils/is-debug-mode.ts: -------------------------------------------------------------------------------- 1 | export const isDebugMode = (): boolean => !!process.env.DEBUG; 2 | -------------------------------------------------------------------------------- /src/utils/is-metadata.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if key is metadata. 3 | * @param key 4 | */ 5 | import { MODEL_KEY, DEFAULT_ID_KEY } from './constants'; 6 | 7 | export const isMetadataKey = (key: string): boolean => key === DEFAULT_ID_KEY || key === MODEL_KEY; 8 | -------------------------------------------------------------------------------- /src/utils/is-model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Determine if a given parameter is 3 | * a Model 4 | * or a document built with a Model. 5 | */ 6 | import { Document } from '../model/document'; 7 | 8 | export const isModel = (model): boolean => { 9 | if (!model) { 10 | return false; 11 | } 12 | return model instanceof Document || model.prototype instanceof Document; 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/is-not-found.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNotFoundError } from 'couchbase'; 2 | 3 | /** 4 | * Return a boolean value if an exception is DocumentNotFoundError. 5 | */ 6 | export const isDocumentNotFoundError = (exception: any): boolean => exception instanceof DocumentNotFoundError; 7 | -------------------------------------------------------------------------------- /src/utils/is-type.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../schema'; 2 | 3 | /** 4 | * Checking if a value is a specific type or constructor 5 | * @param val 6 | * @param type 7 | */ 8 | export const is = (val, type): boolean => 9 | ![, null].includes(val) && (val.name === type.name || val.constructor.name === type.name); 10 | 11 | /** 12 | * Checking if a value is a type supported by Ottoman. 13 | * @param val 14 | */ 15 | export const isSchemaTypeSupported = (val): boolean => { 16 | return val.constructor['sName'] in Schema.Types; 17 | }; 18 | 19 | /** 20 | * Checking if a value is a factory supported by Ottoman. 21 | * @param value 22 | * @param factory List of Schema Factories 23 | */ 24 | export const isSchemaFactoryType = (value, factory): boolean => { 25 | return value.name in factory; 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/jp-parse.ts: -------------------------------------------------------------------------------- 1 | import jsonpath from 'jsonpath'; 2 | 3 | export const jpParse = (pathStr: string) => { 4 | // Temporary fix until we have our own parser. 5 | const dollarKey = 'SUPERWACKYDOLLARKEY'; 6 | 7 | const path = jsonpath.parse(pathStr.replace('$', dollarKey)); 8 | for (let i = 0; i < path.length; ++i) { 9 | const pathPart = path[i]; 10 | 11 | if (pathPart.scope !== 'child') { 12 | throw new Error('Expected child selectors only.'); 13 | } 14 | if (pathPart.operation !== 'member' && pathPart.operation !== 'subscript') { 15 | throw new Error('Expected member and subscript selectors only.'); 16 | } 17 | if (pathPart.expression.type === 'wildcard') { 18 | delete pathPart.expression.value; 19 | } 20 | 21 | if (pathPart.expression.value) { 22 | pathPart.expression.value = pathPart.expression.value.replace(dollarKey, '$'); 23 | } 24 | 25 | delete pathPart.scope; 26 | } 27 | return path; 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/merge.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | function customizer(objValue, srcValue) { 4 | if (_.isArray(objValue)) { 5 | return srcValue; 6 | } 7 | } 8 | 9 | const mergeDoc = (dest, source) => { 10 | return _.mergeWith({}, dest, source, customizer); 11 | }; 12 | 13 | export { mergeDoc }; 14 | -------------------------------------------------------------------------------- /src/utils/noenumarable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Set a given property of the target to be non-enumerable 3 | * Will not be listed on Object.keys and will be excluded by spread operator 4 | */ 5 | export const nonenumerable = (target: any, propertyKey: string) => { 6 | const descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || {}; 7 | if (descriptor.enumerable !== false) { 8 | descriptor.enumerable = false; 9 | descriptor.writable = true; 10 | Object.defineProperty(target, propertyKey, descriptor); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/parse-errors.ts: -------------------------------------------------------------------------------- 1 | import { CouchbaseError, BucketNotFoundError, CollectionNotFoundError, ScopeNotFoundError } from 'couchbase'; 2 | 3 | export function parseError(e: CouchbaseError, info: any): void { 4 | const { message, cause } = e; 5 | const response: string = (cause as any)?.response ?? ''; 6 | 7 | if (response.includes('scope_not_found') || e instanceof ScopeNotFoundError) { 8 | throw new ScopeNotFoundError( 9 | new Error( 10 | `${message}${message.includes('drop collection') ? ` '${info.collectionName}'` : ''}, scope '${ 11 | info.scopeName 12 | }' not found`, 13 | ), 14 | ); 15 | } 16 | if (response.includes('collection_not_found') || e instanceof CollectionNotFoundError) { 17 | throw new CollectionNotFoundError( 18 | new Error(`${message}, in scope '${info.scopeName}' collection '${info.collectionName}' not found`), 19 | ); 20 | } 21 | if (e instanceof BucketNotFoundError) { 22 | throw new BucketNotFoundError(new Error(`failed to drop bucket, bucket '${info.bucketName}' not found`)); 23 | } 24 | throw e; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/path-to-n1ql.ts: -------------------------------------------------------------------------------- 1 | import { PathN1qlError } from '../exceptions/ottoman-errors'; 2 | 3 | type PathToN1QLOperationType = 'member' | 'subscript'; 4 | type PathToN1QLExpressionType = 'identifier' | 'string_literal'; 5 | 6 | export type PathToN1QLItemType = { 7 | operation: PathToN1QLOperationType; 8 | expression: { 9 | type: PathToN1QLExpressionType; 10 | value: string; 11 | }; 12 | }; 13 | 14 | export const pathToN1QL = (path: PathToN1QLItemType[]): string => { 15 | const fields: string[] = []; 16 | for (const { operation, expression } of path) { 17 | const { type: eType, value: eValue } = expression; 18 | switch (operation) { 19 | case 'member': { 20 | if (eType !== 'identifier') throw new PathN1qlError(`Unexpected member expression type '${eType}'.`); 21 | break; 22 | } 23 | case 'subscript': { 24 | if (eType !== 'string_literal') throw new PathN1qlError(`Unexpected subscript expression type '${eType}'.`); 25 | break; 26 | } 27 | default: 28 | throw new PathN1qlError(`Unexpected path operation type '${operation}'.`); 29 | } 30 | fields.push(`\`${eValue}\``); 31 | } 32 | return fields.join('.'); 33 | }; 34 | -------------------------------------------------------------------------------- /src/utils/pipe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Performs left-to-right function composition for asynchronous functions. 3 | */ 4 | export const pipe = 5 | (...fns) => 6 | async (arg) => { 7 | for (const fn of fns) { 8 | await fn(arg); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/populate/can-be-populated.ts: -------------------------------------------------------------------------------- 1 | import { isDebugMode } from '../is-debug-mode'; 2 | 3 | /** 4 | * Determine if a given field can be populated. 5 | */ 6 | export const canBePopulated = (populate: string, fields: string[]): boolean => { 7 | if (populate === '*' || fields.some((field) => populate === field || field === `\`${populate}\``)) { 8 | return true; 9 | } 10 | 11 | if (isDebugMode()) { 12 | console.warn(`Unable to populate field "${populate}", it is not available on select clause [${fields.join(', ')}]`); 13 | } 14 | return false; 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/populate/is-populate-object.ts: -------------------------------------------------------------------------------- 1 | import { PopulateFieldsType } from '../../model/populate.types'; 2 | 3 | export const isPopulateAnObject = (fieldsName?: PopulateFieldsType): boolean => { 4 | return !!fieldsName && typeof fieldsName === 'object' && !Array.isArray(fieldsName); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/query/extract-populate.ts: -------------------------------------------------------------------------------- 1 | import { FindOptions } from '../../handler'; 2 | 3 | /** 4 | * Extract the values to be populated. 5 | */ 6 | export const extractPopulate = (populate: FindOptions['populate']): string[] => { 7 | let populateFields: string[] = Array.isArray(populate) ? populate : []; 8 | 9 | if (populate && typeof populate === 'string') { 10 | populateFields = populate.trim().split(/\s*,\s*/g); 11 | } 12 | 13 | return [...new Set(populateFields)]; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/query/extract-select.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extract the values to be used on select clause. 3 | */ 4 | import { buildSelectArrayExpr, ISelectType, parseStringSelectExpr } from '../../query'; 5 | import { MODEL_KEY } from '../constants'; 6 | 7 | export const extractSelect = ( 8 | select: string | string[], 9 | options: { noCollection?: boolean } = {}, 10 | excludeMeta = false, 11 | modelKey: string, 12 | ): string[] => { 13 | const { noCollection } = options; 14 | let selectProjection: string[] = []; 15 | 16 | if (select) { 17 | selectProjection = Array.isArray(select) ? select : parseStringSelectExpr(select); 18 | } 19 | const metadata = excludeMeta ? [] : getMetadata(noCollection, modelKey); 20 | return [...new Set([...selectProjection, ...metadata])]; 21 | }; 22 | 23 | export const getProjectionFields = ( 24 | selectDot: string, 25 | select: ISelectType[] | string | string[] = '', 26 | options: { noCollection?: boolean } = {}, 27 | modelKey: string = MODEL_KEY, 28 | ): { projection: string; fields: string[] } => { 29 | let fields: string[] = []; 30 | let projection = ''; 31 | const metadata = getMetadata(options.noCollection, modelKey); 32 | if (typeof select === 'string') { 33 | if (!select) { 34 | projection = [`\`${selectDot}\`.*`].join(','); 35 | } else { 36 | projection = [select, ...metadata].join(','); 37 | } 38 | 39 | fields = extractSelect(select, { noCollection: options.noCollection }, true, modelKey); 40 | } else if (Array.isArray(select) && select.length > 0) { 41 | if (typeof select[0] === 'string') { 42 | projection = [select, ...metadata].join(','); 43 | fields = extractSelect( 44 | select as string[], 45 | { 46 | noCollection: options.noCollection, 47 | }, 48 | true, 49 | modelKey, 50 | ); 51 | } else { 52 | const selectExpr = buildSelectArrayExpr(select as ISelectType[]); 53 | projection = [selectExpr, ...metadata].join(','); 54 | fields = extractSelect( 55 | selectExpr.replace(/`/g, ''), 56 | { 57 | noCollection: options.noCollection, 58 | }, 59 | true, 60 | modelKey, 61 | ); 62 | } 63 | } 64 | 65 | return { 66 | projection, 67 | fields, 68 | }; 69 | }; 70 | 71 | const getMetadata = (noCollection?: boolean, modelKey?: string) => { 72 | const metadataSelect: string[] = []; 73 | if (!noCollection && modelKey) { 74 | metadataSelect.push(modelKey); 75 | } 76 | return metadataSelect; 77 | }; 78 | -------------------------------------------------------------------------------- /src/utils/schema.utils.ts: -------------------------------------------------------------------------------- 1 | import { PopulateSelectBaseType } from '../model/populate.types'; 2 | import { ReferenceType, Schema } from '../schema'; 3 | import { extractPopulate } from './query/extract-populate'; 4 | 5 | export const extractSchemaReferencesFields = (schema: Schema) => { 6 | const { fields } = schema; 7 | const populableFields: any = {}; 8 | for (const fieldName in fields) { 9 | if (fields.hasOwnProperty(fieldName)) { 10 | const field = getSchemaType(fieldName, schema); 11 | if ((field as any).refModel) { 12 | populableFields[fieldName] = field; 13 | } 14 | } 15 | } 16 | return populableFields; 17 | }; 18 | 19 | export const extractSchemaReferencesFromGivenFields = (fields, schema: Schema): Record => { 20 | const toPopulate = extractPopulate(fields); 21 | const fieldsToPopulate: any = {}; 22 | for (const fieldName of toPopulate) { 23 | fieldsToPopulate[fieldName] = getSchemaType(fieldName, schema); 24 | } 25 | return fieldsToPopulate; 26 | }; 27 | 28 | export const getSchemaType = (fieldName, schema) => { 29 | let field = schema.path(fieldName); 30 | if ((field as any).typeName === 'Array') { 31 | field = (field as any).itemType; 32 | } 33 | return field; 34 | }; 35 | 36 | export const extractPopulateFieldsFromObject = ({ select, populate }: PopulateSelectBaseType): string[] => { 37 | let toPopulate: string[] = extractPopulate(select); 38 | 39 | if (populate) { 40 | toPopulate = [ 41 | ...new Set([ 42 | ...toPopulate, 43 | ...(typeof populate === 'object' && !Array.isArray(populate) 44 | ? Object.keys(populate) 45 | : extractPopulate(populate)), 46 | ]), 47 | ]; 48 | } 49 | return toPopulate; 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils/search-consistency.ts: -------------------------------------------------------------------------------- 1 | export enum SearchConsistency { 2 | /** 3 | * No degree consistency required. 4 | * Called as NOT_BOUNDED in Couchbase Server. 5 | */ 6 | NONE, 7 | 8 | /** 9 | * Operations performed by this ottoman instance will be reflected 10 | * in queries performed by this particular ottoman instance. This 11 | * type of consistency will be slower than no consistency, but faster 12 | * than GLOBAL as the index state is tracked internally rather than 13 | * requested from the server. 14 | * Called as AT_PLUS in Couchbase Server 15 | */ 16 | LOCAL, 17 | 18 | /** 19 | * Operations performed by any client of the Couchbase Server up to the 20 | * time of the queries dispatch will be reflected in any index results. 21 | * This is the slowest of all consistency levels as it requires that the 22 | * server synchronize its indexes to the current key-value state prior to 23 | * execution of the query. 24 | * Called as REQUEST_PLUST in Couchbase Server 25 | */ 26 | GLOBAL, 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/setValueByPath.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export const setValueByPath = >(object: T, path: string, value: any) => { 4 | _.set(object as any, path, value); 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/type-helpers.ts: -------------------------------------------------------------------------------- 1 | export const isNumber = (val: unknown) => typeof val === 'number' && val === val; 2 | export const isDateValid = (val) => !Number.isNaN(new Date(val).valueOf()); 3 | -------------------------------------------------------------------------------- /src/utils/validation.strategy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Hooks available 3 | * 'strict' | 'equal' 4 | */ 5 | export enum VALIDATION_STRATEGY { 6 | STRICT = 'strict', 7 | EQUAL = 'equal', 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 5 | "outDir": "./lib/", 6 | "declarationDir": "./lib/types/", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "removeComments": false, 10 | "noImplicitAny": false, 11 | "strict": true, 12 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 13 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 14 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 15 | }, 16 | "include": ["./src/**/*"], 17 | "exclude": ["node_modules", "**/*.spec.ts"] 18 | } 19 | --------------------------------------------------------------------------------