├── .github ├── release-drafter-config.yml └── workflows │ ├── ci.yml │ └── release-drafter.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG ├── LICENSE ├── README.md ├── docker ├── .gitignore ├── README.md ├── data │ └── .gitkeep └── docker-compose.yml ├── docs ├── .nojekyll ├── README.md └── classes │ ├── AbstractSearch.md │ ├── ArrayHashInput.md │ ├── Circle.md │ ├── Client.md │ ├── Field.md │ ├── FieldNotInSchema.md │ ├── InvalidHashInput.md │ ├── InvalidHashValue.md │ ├── InvalidInput.md │ ├── InvalidJsonInput.md │ ├── InvalidJsonValue.md │ ├── InvalidSchema.md │ ├── InvalidValue.md │ ├── NestedHashInput.md │ ├── NullJsonInput.md │ ├── NullJsonValue.md │ ├── PointOutOfRange.md │ ├── RawSearch.md │ ├── RedisOmError.md │ ├── Repository.md │ ├── Schema.md │ ├── Search.md │ ├── SearchError.md │ ├── SemanticSearchError.md │ ├── Where.md │ └── WhereField.md ├── lib ├── client │ ├── client.ts │ └── index.ts ├── entity │ ├── entity.ts │ └── index.ts ├── error │ ├── index.ts │ ├── invalid-input.ts │ ├── invalid-schema.ts │ ├── invalid-value.ts │ ├── point-out-of-range.ts │ ├── redis-om-error.ts │ └── search-error.ts ├── index.ts ├── indexer │ ├── index-builder.ts │ └── index.ts ├── repository │ ├── index.ts │ └── repository.ts ├── schema │ ├── definitions.ts │ ├── field.ts │ ├── index.ts │ ├── options.ts │ └── schema.ts ├── search │ ├── index.ts │ ├── results-converter.ts │ ├── search.ts │ ├── where-and.ts │ ├── where-boolean.ts │ ├── where-date.ts │ ├── where-field.ts │ ├── where-number.ts │ ├── where-or.ts │ ├── where-point.ts │ ├── where-string-array.ts │ ├── where-string.ts │ ├── where-text.ts │ └── where.ts └── transformer │ ├── from-hash-transformer.ts │ ├── from-json-transformer.ts │ ├── index.ts │ ├── to-hash-transformer.ts │ ├── to-json-transformer.ts │ └── transformer-common.ts ├── logo.svg ├── package-lock.json ├── package.json ├── spec ├── functional │ ├── core │ │ ├── fetch-hash.spec.ts │ │ ├── fetch-json.spec.ts │ │ ├── remove-hash.spec.ts │ │ ├── remove-json.spec.ts │ │ ├── save-hash.spec.ts │ │ ├── save-json.spec.ts │ │ ├── update-hash.spec.ts │ │ └── update-json.spec.ts │ ├── demo.spec.ts │ ├── helpers │ │ ├── data-helper.ts │ │ ├── hash-example-data.ts │ │ ├── json-example-data.ts │ │ └── redis-helper.ts │ └── search │ │ ├── create-and-drop-index-on-hash.spec.ts │ │ ├── create-and-drop-index-on-json.spec.ts │ │ ├── drop-missing-index.spec.ts │ │ ├── search-hash.spec.ts │ │ └── search-json.spec.ts ├── helpers │ ├── custom-matchers.ts │ └── example-data.ts └── unit │ ├── client │ ├── client-close.spec.ts │ ├── client-create-index.spec.ts │ ├── client-drop-index.spec.ts │ ├── client-expire.spec.ts │ ├── client-expireAt.spec.ts │ ├── client-fetch-repository.spec.ts │ ├── client-get-set.spec.ts │ ├── client-hgetall.spec.ts │ ├── client-hsetall.spec.ts │ ├── client-jsonget.spec.ts │ ├── client-jsonset.spec.ts │ ├── client-open.spec.ts │ ├── client-search.spec.ts │ ├── client-unlink.spec.ts │ └── client-use.spec.ts │ ├── error │ └── errors.spec.ts │ ├── helpers │ ├── mock-client.ts │ ├── mock-indexer.ts │ ├── mock-redis.ts │ ├── search-helpers.ts │ └── test-entity-and-schema.ts │ ├── indexer │ ├── boolean-hash-fields.spec.ts │ ├── boolean-json-fields.spec.ts │ ├── date-hash-fields.spec.ts │ ├── date-json-fields.spec.ts │ ├── index-builder.spec.ts │ ├── number-array-json-fields.spec.ts │ ├── number-hash-fields.spec.ts │ ├── number-json-fields.spec.ts │ ├── point-hash-fields.spec.ts │ ├── point-json-fields.spec.ts │ ├── string-array-hash-fields.spec.ts │ ├── string-array-json-fields.spec.ts │ ├── string-hash-fields.spec.ts │ ├── string-json-fields.spec.ts │ ├── text-hash-fields.spec.ts │ └── text-json-fields.spec.ts │ ├── repository │ ├── repository-constructor.spec.ts │ ├── repository-create-index.spec.ts │ ├── repository-drop-index.spec.ts │ ├── repository-expire.spec.ts │ ├── repository-expireAt.spec.ts │ ├── repository-fetch.spec.ts │ ├── repository-remove.spec.ts │ ├── repository-save.spec.ts │ └── repository-search.spec.ts │ ├── schema │ ├── field.spec.ts │ ├── schema-index-hash.spec.ts │ └── schema.spec.ts │ ├── search │ ├── raw-search-query.spec.ts │ ├── search-by-boolean.spec.ts │ ├── search-by-date.spec.ts │ ├── search-by-number-array.spec.ts │ ├── search-by-number.spec.ts │ ├── search-by-point.spec.ts │ ├── search-by-string-array.spec.ts │ ├── search-by-string.spec.ts │ ├── search-by-text.spec.ts │ ├── search-query-with-escaped-fields.spec.ts │ ├── search-query.spec.ts │ ├── search-return-all-ids.spec.ts │ ├── search-return-all-keys.spec.ts │ ├── search-return-all.spec.ts │ ├── search-return-count.spec.ts │ ├── search-return-first-id.spec.ts │ ├── search-return-first-key.spec.ts │ ├── search-return-first.spec.ts │ ├── search-return-max-id.spec.ts │ ├── search-return-max-key.spec.ts │ ├── search-return-max.spec.ts │ ├── search-return-min-id.spec.ts │ ├── search-return-min-key.spec.ts │ ├── search-return-min.spec.ts │ ├── search-return-page-of-ids.spec.ts │ ├── search-return-page-of-keys.spec.ts │ ├── search-return-page.spec.ts │ └── search-sort-by.spec.ts │ └── transformer │ ├── from-redis-hash.spec.ts │ ├── from-redis-json.spec.ts │ ├── to-redis-hash.spec.ts │ └── to-redis-json.spec.ts ├── tsconfig.json └── vitest.config.js /.github/release-drafter-config.yml: -------------------------------------------------------------------------------- 1 | name-template: 'Version $NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | autolabeler: 4 | - label: 'maintenance' 5 | files: 6 | - '*.md' 7 | - '.github/*' 8 | - label: 'bug' 9 | branch: 10 | - '/bug-.+' 11 | - label: 'maintenance' 12 | branch: 13 | - '/maintenance-.+' 14 | - label: 'feature' 15 | branch: 16 | - '/feature-.+' 17 | categories: 18 | - title: '🔥 Breaking Changes' 19 | labels: 20 | - 'breakingchange' 21 | - title: '🚀 New Features' 22 | labels: 23 | - 'feature' 24 | - 'enhancement' 25 | - title: '🐛 Bug Fixes' 26 | labels: 27 | - 'fix' 28 | - 'bugfix' 29 | - 'bug' 30 | - title: '🧰 Maintenance' 31 | label: 'maintenance' 32 | change-template: '- $TITLE (#$NUMBER)' 33 | exclude-labels: 34 | - 'skip-changelog' 35 | template: | 36 | ## Changes 37 | 38 | $CHANGES 39 | 40 | ## Contributors 41 | We'd like to thank all the contributors who worked on this release! 42 | 43 | $CONTRIBUTORS 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test-all: 12 | name: Test All 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | node-version: [18, 20] 18 | services: 19 | redis: 20 | image: redis/redis-stack-server:latest 21 | ports: 22 | - 6379:6379 23 | options: >- 24 | --health-cmd "redis-cli PING" 25 | --health-interval 10s 26 | --health-timeout 5s 27 | --health-retries 5 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | 37 | - name: Update npm 38 | run: npm install --global npm 39 | if: ${{ matrix.node-version <= 14 }} 40 | 41 | - name: Install packages 42 | run: npm ci 43 | 44 | - name: Run all tests 45 | run: npm run test:coverage 46 | 47 | - name: Upload to Codecov 48 | uses: codecov/codecov-action@v3 49 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | with: 16 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 17 | config-name: release-drafter-config.yml 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .DS_Store -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Redis Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | data/** 2 | !data/.gitkeep 3 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # redismod-docker-compose 2 | 3 | This folder contains a Docker Compose file configured to use the RedisLabs redismod image; a Docker image with selected Redis Labs modules. 4 | 5 | ## Pre-requisites 6 | 7 | - Docker: https://docs.docker.com/get-docker/ 8 | - Docker Compose: https://docs.docker.com/compose/install/ 9 | 10 | This image runs Redis on the default port 6379 which you can access as if it were a local install of Redis. Just ensure that you shut down any other Redis instances that might be on port 6379 before starting this one. 11 | 12 | The image stores the data file under the ./data directory. 13 | 14 | To launch Redis simply enter: 15 | 16 | ``` 17 | docker compose up 18 | ``` 19 | -------------------------------------------------------------------------------- /docker/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis/redis-om-node/fe1eea9072fad7faed69a98b1885f906c46045d6/docker/data/.gitkeep -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | redis: 4 | image: "redis/redis-stack:latest" 5 | ports: 6 | - "6379:6379" 7 | volumes: 8 | - ./data:/data 9 | deploy: 10 | replicas: 1 11 | restart_policy: 12 | condition: on-failure 13 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/classes/InvalidSchema.md: -------------------------------------------------------------------------------- 1 | [redis-om](../README.md) / InvalidSchema 2 | 3 | # Class: InvalidSchema 4 | 5 | ## Hierarchy 6 | 7 | - [`RedisOmError`](RedisOmError.md) 8 | 9 | ↳ **`InvalidSchema`** 10 | 11 | ## Table of contents 12 | 13 | ### Constructors 14 | 15 | - [constructor](InvalidSchema.md#constructor) 16 | 17 | ### Properties 18 | 19 | - [cause](InvalidSchema.md#cause) 20 | - [message](InvalidSchema.md#message) 21 | - [name](InvalidSchema.md#name) 22 | - [stack](InvalidSchema.md#stack) 23 | - [prepareStackTrace](InvalidSchema.md#preparestacktrace) 24 | - [stackTraceLimit](InvalidSchema.md#stacktracelimit) 25 | 26 | ### Methods 27 | 28 | - [captureStackTrace](InvalidSchema.md#capturestacktrace) 29 | 30 | ## Constructors 31 | 32 | ### constructor 33 | 34 | • **new InvalidSchema**(`message?`) 35 | 36 | #### Parameters 37 | 38 | | Name | Type | 39 | | :------ | :------ | 40 | | `message?` | `string` | 41 | 42 | #### Inherited from 43 | 44 | [RedisOmError](RedisOmError.md).[constructor](RedisOmError.md#constructor) 45 | 46 | #### Defined in 47 | 48 | node_modules/typescript/lib/lib.es5.d.ts:1059 49 | 50 | • **new InvalidSchema**(`message?`, `options?`) 51 | 52 | #### Parameters 53 | 54 | | Name | Type | 55 | | :------ | :------ | 56 | | `message?` | `string` | 57 | | `options?` | `ErrorOptions` | 58 | 59 | #### Inherited from 60 | 61 | [RedisOmError](RedisOmError.md).[constructor](RedisOmError.md#constructor) 62 | 63 | #### Defined in 64 | 65 | node_modules/typescript/lib/lib.es2022.error.d.ts:30 66 | 67 | ## Properties 68 | 69 | ### cause 70 | 71 | • `Optional` **cause**: `unknown` 72 | 73 | #### Inherited from 74 | 75 | [RedisOmError](RedisOmError.md).[cause](RedisOmError.md#cause) 76 | 77 | #### Defined in 78 | 79 | node_modules/typescript/lib/lib.es2022.error.d.ts:26 80 | 81 | ___ 82 | 83 | ### message 84 | 85 | • **message**: `string` 86 | 87 | #### Inherited from 88 | 89 | [RedisOmError](RedisOmError.md).[message](RedisOmError.md#message) 90 | 91 | #### Defined in 92 | 93 | node_modules/typescript/lib/lib.es5.d.ts:1054 94 | 95 | ___ 96 | 97 | ### name 98 | 99 | • **name**: `string` 100 | 101 | #### Inherited from 102 | 103 | [RedisOmError](RedisOmError.md).[name](RedisOmError.md#name) 104 | 105 | #### Defined in 106 | 107 | node_modules/typescript/lib/lib.es5.d.ts:1053 108 | 109 | ___ 110 | 111 | ### stack 112 | 113 | • `Optional` **stack**: `string` 114 | 115 | #### Inherited from 116 | 117 | [RedisOmError](RedisOmError.md).[stack](RedisOmError.md#stack) 118 | 119 | #### Defined in 120 | 121 | node_modules/typescript/lib/lib.es5.d.ts:1055 122 | 123 | ___ 124 | 125 | ### prepareStackTrace 126 | 127 | ▪ `Static` `Optional` **prepareStackTrace**: (`err`: `Error`, `stackTraces`: `CallSite`[]) => `any` 128 | 129 | #### Type declaration 130 | 131 | ▸ (`err`, `stackTraces`): `any` 132 | 133 | Optional override for formatting stack traces 134 | 135 | **`See`** 136 | 137 | https://v8.dev/docs/stack-trace-api#customizing-stack-traces 138 | 139 | ##### Parameters 140 | 141 | | Name | Type | 142 | | :------ | :------ | 143 | | `err` | `Error` | 144 | | `stackTraces` | `CallSite`[] | 145 | 146 | ##### Returns 147 | 148 | `any` 149 | 150 | #### Inherited from 151 | 152 | [RedisOmError](RedisOmError.md).[prepareStackTrace](RedisOmError.md#preparestacktrace) 153 | 154 | #### Defined in 155 | 156 | node_modules/@types/node/globals.d.ts:11 157 | 158 | ___ 159 | 160 | ### stackTraceLimit 161 | 162 | ▪ `Static` **stackTraceLimit**: `number` 163 | 164 | #### Inherited from 165 | 166 | [RedisOmError](RedisOmError.md).[stackTraceLimit](RedisOmError.md#stacktracelimit) 167 | 168 | #### Defined in 169 | 170 | node_modules/@types/node/globals.d.ts:13 171 | 172 | ## Methods 173 | 174 | ### captureStackTrace 175 | 176 | ▸ `Static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` 177 | 178 | Create .stack property on a target object 179 | 180 | #### Parameters 181 | 182 | | Name | Type | 183 | | :------ | :------ | 184 | | `targetObject` | `object` | 185 | | `constructorOpt?` | `Function` | 186 | 187 | #### Returns 188 | 189 | `void` 190 | 191 | #### Inherited from 192 | 193 | [RedisOmError](RedisOmError.md).[captureStackTrace](RedisOmError.md#capturestacktrace) 194 | 195 | #### Defined in 196 | 197 | node_modules/@types/node/globals.d.ts:4 198 | -------------------------------------------------------------------------------- /docs/classes/RedisOmError.md: -------------------------------------------------------------------------------- 1 | [redis-om](../README.md) / RedisOmError 2 | 3 | # Class: RedisOmError 4 | 5 | ## Hierarchy 6 | 7 | - `Error` 8 | 9 | ↳ **`RedisOmError`** 10 | 11 | ↳↳ [`InvalidInput`](InvalidInput.md) 12 | 13 | ↳↳ [`InvalidSchema`](InvalidSchema.md) 14 | 15 | ↳↳ [`InvalidValue`](InvalidValue.md) 16 | 17 | ↳↳ [`PointOutOfRange`](PointOutOfRange.md) 18 | 19 | ↳↳ [`SearchError`](SearchError.md) 20 | 21 | ## Table of contents 22 | 23 | ### Constructors 24 | 25 | - [constructor](RedisOmError.md#constructor) 26 | 27 | ### Properties 28 | 29 | - [cause](RedisOmError.md#cause) 30 | - [message](RedisOmError.md#message) 31 | - [name](RedisOmError.md#name) 32 | - [stack](RedisOmError.md#stack) 33 | - [prepareStackTrace](RedisOmError.md#preparestacktrace) 34 | - [stackTraceLimit](RedisOmError.md#stacktracelimit) 35 | 36 | ### Methods 37 | 38 | - [captureStackTrace](RedisOmError.md#capturestacktrace) 39 | 40 | ## Constructors 41 | 42 | ### constructor 43 | 44 | • **new RedisOmError**(`message?`) 45 | 46 | #### Parameters 47 | 48 | | Name | Type | 49 | | :------ | :------ | 50 | | `message?` | `string` | 51 | 52 | #### Inherited from 53 | 54 | Error.constructor 55 | 56 | #### Defined in 57 | 58 | node_modules/typescript/lib/lib.es5.d.ts:1059 59 | 60 | • **new RedisOmError**(`message?`, `options?`) 61 | 62 | #### Parameters 63 | 64 | | Name | Type | 65 | | :------ | :------ | 66 | | `message?` | `string` | 67 | | `options?` | `ErrorOptions` | 68 | 69 | #### Inherited from 70 | 71 | Error.constructor 72 | 73 | #### Defined in 74 | 75 | node_modules/typescript/lib/lib.es2022.error.d.ts:30 76 | 77 | ## Properties 78 | 79 | ### cause 80 | 81 | • `Optional` **cause**: `unknown` 82 | 83 | #### Inherited from 84 | 85 | Error.cause 86 | 87 | #### Defined in 88 | 89 | node_modules/typescript/lib/lib.es2022.error.d.ts:26 90 | 91 | ___ 92 | 93 | ### message 94 | 95 | • **message**: `string` 96 | 97 | #### Inherited from 98 | 99 | Error.message 100 | 101 | #### Defined in 102 | 103 | node_modules/typescript/lib/lib.es5.d.ts:1054 104 | 105 | ___ 106 | 107 | ### name 108 | 109 | • **name**: `string` 110 | 111 | #### Inherited from 112 | 113 | Error.name 114 | 115 | #### Defined in 116 | 117 | node_modules/typescript/lib/lib.es5.d.ts:1053 118 | 119 | ___ 120 | 121 | ### stack 122 | 123 | • `Optional` **stack**: `string` 124 | 125 | #### Inherited from 126 | 127 | Error.stack 128 | 129 | #### Defined in 130 | 131 | node_modules/typescript/lib/lib.es5.d.ts:1055 132 | 133 | ___ 134 | 135 | ### prepareStackTrace 136 | 137 | ▪ `Static` `Optional` **prepareStackTrace**: (`err`: `Error`, `stackTraces`: `CallSite`[]) => `any` 138 | 139 | #### Type declaration 140 | 141 | ▸ (`err`, `stackTraces`): `any` 142 | 143 | Optional override for formatting stack traces 144 | 145 | **`See`** 146 | 147 | https://v8.dev/docs/stack-trace-api#customizing-stack-traces 148 | 149 | ##### Parameters 150 | 151 | | Name | Type | 152 | | :------ | :------ | 153 | | `err` | `Error` | 154 | | `stackTraces` | `CallSite`[] | 155 | 156 | ##### Returns 157 | 158 | `any` 159 | 160 | #### Inherited from 161 | 162 | Error.prepareStackTrace 163 | 164 | #### Defined in 165 | 166 | node_modules/@types/node/globals.d.ts:11 167 | 168 | ___ 169 | 170 | ### stackTraceLimit 171 | 172 | ▪ `Static` **stackTraceLimit**: `number` 173 | 174 | #### Inherited from 175 | 176 | Error.stackTraceLimit 177 | 178 | #### Defined in 179 | 180 | node_modules/@types/node/globals.d.ts:13 181 | 182 | ## Methods 183 | 184 | ### captureStackTrace 185 | 186 | ▸ `Static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` 187 | 188 | Create .stack property on a target object 189 | 190 | #### Parameters 191 | 192 | | Name | Type | 193 | | :------ | :------ | 194 | | `targetObject` | `object` | 195 | | `constructorOpt?` | `Function` | 196 | 197 | #### Returns 198 | 199 | `void` 200 | 201 | #### Inherited from 202 | 203 | Error.captureStackTrace 204 | 205 | #### Defined in 206 | 207 | node_modules/@types/node/globals.d.ts:4 208 | -------------------------------------------------------------------------------- /docs/classes/Where.md: -------------------------------------------------------------------------------- 1 | [redis-om](../README.md) / Where 2 | 3 | # Class: Where 4 | 5 | Abstract base class used extensively with [Search](Search.md). 6 | 7 | ## Hierarchy 8 | 9 | - **`Where`** 10 | 11 | ↳ [`WhereField`](WhereField.md) 12 | 13 | ## Table of contents 14 | 15 | ### Constructors 16 | 17 | - [constructor](Where.md#constructor) 18 | 19 | ### Methods 20 | 21 | - [toString](Where.md#tostring) 22 | 23 | ## Constructors 24 | 25 | ### constructor 26 | 27 | • **new Where**() 28 | 29 | ## Methods 30 | 31 | ### toString 32 | 33 | ▸ `Abstract` **toString**(): `string` 34 | 35 | Converts this [Where](Where.md) into a portion of a RediSearch query. 36 | 37 | #### Returns 38 | 39 | `string` 40 | 41 | #### Defined in 42 | 43 | [lib/search/where.ts:8](https://github.com/redis/redis-om-node/blob/1acd1cf/lib/search/where.ts#L8) 44 | -------------------------------------------------------------------------------- /lib/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client' 2 | -------------------------------------------------------------------------------- /lib/entity/entity.ts: -------------------------------------------------------------------------------- 1 | /** The Symbol used to access the entity ID of an {@link Entity}. */ 2 | export const EntityId = Symbol('entityId') 3 | 4 | /** The Symbol used to access the keyname of an {@link Entity}. */ 5 | export const EntityKeyName = Symbol('entityKeyName') 6 | 7 | export type EntityInternal = { 8 | /** The unique ID of the {@link Entity}. Access using the {@link EntityId} Symbol. */ 9 | [EntityId]?: string 10 | 11 | /** The key the {@link Entity} is stored under inside of Redis. Access using the {@link EntityKeyName} Symbol. */ 12 | [EntityKeyName]?: string 13 | } 14 | 15 | /** Defines the objects returned from calls to {@link Repository | repositories }. */ 16 | export type Entity = EntityData & EntityInternal 17 | export type EntityKeys = Exclude; 18 | 19 | /** The free-form data associated with an {@link Entity}. */ 20 | export type EntityData = { 21 | [key: string]: EntityDataValue | EntityData | Array 22 | } 23 | 24 | /** Valid types for values in an {@link Entity}. */ 25 | export type EntityDataValue = string | number | boolean | Date | Point | null | undefined | Array 26 | 27 | /** Defines a point on the globe using longitude and latitude. */ 28 | export type Point = { 29 | /** The longitude of the point. */ 30 | longitude: number 31 | /** The latitude of the point. */ 32 | latitude: number 33 | } 34 | -------------------------------------------------------------------------------- /lib/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entity' 2 | -------------------------------------------------------------------------------- /lib/error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './invalid-input' 2 | export * from './invalid-schema' 3 | export * from './invalid-value' 4 | export * from './point-out-of-range' 5 | export * from './redis-om-error' 6 | export * from './search-error' 7 | -------------------------------------------------------------------------------- /lib/error/invalid-input.ts: -------------------------------------------------------------------------------- 1 | import { RedisOmError } from './redis-om-error' 2 | import { Field } from '../schema' 3 | 4 | export class InvalidInput extends RedisOmError {} 5 | 6 | export class NullJsonInput extends InvalidInput { 7 | 8 | #field 9 | 10 | constructor(field: Field) { 11 | const message = `Null or undefined found in field '${field.name}' of type '${field.type}' in JSON at '${field.jsonPath}'.` 12 | super(message) 13 | this.#field = field 14 | } 15 | 16 | get fieldName() { return this.#field.name } 17 | get fieldType() { return this.#field.type } 18 | get jsonPath() { return this.#field.jsonPath } 19 | } 20 | 21 | export class InvalidJsonInput extends InvalidInput { 22 | 23 | #field 24 | 25 | constructor(field: Field) { 26 | const message = `Unexpected value for field '${field.name}' of type '${field.type}' in JSON at '${field.jsonPath}'.` 27 | super(message) 28 | this.#field = field 29 | } 30 | 31 | get fieldName() { return this.#field.name } 32 | get fieldType() { return this.#field.type } 33 | get jsonPath() { return this.#field.jsonPath } 34 | } 35 | 36 | export class InvalidHashInput extends InvalidInput { 37 | 38 | #field 39 | 40 | constructor(field: Field) { 41 | const message = `Unexpected value for field '${field.name}' of type '${field.type}' in Hash.` 42 | super(message) 43 | this.#field = field 44 | } 45 | 46 | get fieldName() { return this.#field.name } 47 | get fieldType() { return this.#field.type } 48 | } 49 | 50 | export class NestedHashInput extends InvalidInput { 51 | 52 | #property 53 | 54 | constructor(property: string) { 55 | const message = `Unexpected object in Hash at property '${property}'. You can not store a nested object in a Redis Hash.` 56 | super(message) 57 | this.#property = property 58 | } 59 | 60 | get field() { return this.#property } 61 | } 62 | 63 | export class ArrayHashInput extends InvalidInput { 64 | 65 | #property 66 | 67 | constructor(property: string) { 68 | const message = `Unexpected array in Hash at property '${property}'. You can not store an array in a Redis Hash without defining it in the Schema.` 69 | super(message) 70 | this.#property = property 71 | } 72 | 73 | get field() { return this.#property } 74 | } 75 | -------------------------------------------------------------------------------- /lib/error/invalid-schema.ts: -------------------------------------------------------------------------------- 1 | import { RedisOmError } from './redis-om-error' 2 | 3 | export class InvalidSchema extends RedisOmError {} 4 | -------------------------------------------------------------------------------- /lib/error/invalid-value.ts: -------------------------------------------------------------------------------- 1 | import { RedisOmError } from './redis-om-error' 2 | import { Field } from '../schema' 3 | 4 | export class InvalidValue extends RedisOmError {} 5 | 6 | export class NullJsonValue extends InvalidValue { 7 | 8 | #field 9 | 10 | constructor(field: Field) { 11 | const message = `Null or undefined found in field '${field.name}' of type '${field.type}' from JSON path '${field.jsonPath}' in Redis.` 12 | super(message) 13 | this.#field = field 14 | } 15 | 16 | get fieldName() { return this.#field.name } 17 | get fieldType() { return this.#field.type } 18 | get jsonPath() { return this.#field.jsonPath } 19 | } 20 | 21 | export class InvalidJsonValue extends InvalidValue { 22 | 23 | #field 24 | 25 | constructor(field: Field) { 26 | const message = `Unexpected value for field '${field.name}' of type '${field.type}' from JSON path '${field.jsonPath}' in Redis.` 27 | super(message) 28 | this.#field = field 29 | } 30 | 31 | get fieldName() { return this.#field.name } 32 | get fieldType() { return this.#field.type } 33 | get jsonPath() { return this.#field.jsonPath } 34 | } 35 | 36 | export class InvalidHashValue extends InvalidValue { 37 | 38 | #field 39 | 40 | constructor(field: Field) { 41 | const message = `Unexpected value for field '${field.name}' of type '${field.type}' from Hash field '${field.hashField}' read from Redis.` 42 | super(message) 43 | this.#field = field 44 | } 45 | 46 | get fieldName() { return this.#field.name } 47 | get fieldType() { return this.#field.type } 48 | get hashField() { return this.#field.hashField } 49 | } 50 | -------------------------------------------------------------------------------- /lib/error/point-out-of-range.ts: -------------------------------------------------------------------------------- 1 | import { RedisOmError } from './redis-om-error' 2 | import { Point } from "../entity" 3 | 4 | export class PointOutOfRange extends RedisOmError { 5 | 6 | #latitude 7 | #longitude 8 | 9 | constructor(point: Point) { 10 | super("Points must be between ±85.05112878 latitude and ±180 longitude.") 11 | this.#longitude = point.longitude 12 | this.#latitude = point.latitude 13 | } 14 | 15 | get point() { 16 | return { longitude: this.#longitude, latitude: this.#latitude } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/error/redis-om-error.ts: -------------------------------------------------------------------------------- 1 | export class RedisOmError extends Error {} 2 | -------------------------------------------------------------------------------- /lib/error/search-error.ts: -------------------------------------------------------------------------------- 1 | import { RedisOmError } from './redis-om-error' 2 | 3 | export class SearchError extends RedisOmError {} 4 | 5 | export class SemanticSearchError extends SearchError {} 6 | 7 | export class FieldNotInSchema extends SearchError { 8 | 9 | #field 10 | 11 | constructor(fieldName: string) { 12 | super(`The field '${fieldName}' is not part of the schema and thus cannot be used to search.`) 13 | this.#field = fieldName 14 | } 15 | 16 | get field() { 17 | return this.#field 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export { Client, RedisClientConnection, RedisConnection, RedisClusterConnection } from './client' 2 | export { Entity, EntityData, EntityDataValue, EntityId, EntityInternal, EntityKeyName, EntityKeys, Point } from './entity' 3 | export * from './error' 4 | export { Field } from './schema/field' 5 | export { Schema, InferSchema } from './schema/schema' 6 | export { DataStructure, IdStrategy, SchemaOptions, StopWordOptions } from './schema/options' 7 | export { AllFieldDefinition, BooleanFieldDefinition, CommonFieldDefinition, DateFieldDefinition, FieldDefinition, 8 | FieldType, NumberArrayFieldDefinition, NumberFieldDefinition, PointFieldDefinition, SchemaDefinition, StringArrayFieldDefinition, 9 | StringFieldDefinition, TextFieldDefinition } from './schema/definitions' 10 | export { AbstractSearch, Circle, CircleFunction, RawSearch, Search, SubSearchFunction, Where, WhereField } from './search' 11 | export { Repository } from './repository' 12 | -------------------------------------------------------------------------------- /lib/indexer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './index-builder' 2 | -------------------------------------------------------------------------------- /lib/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from './repository' 2 | -------------------------------------------------------------------------------- /lib/schema/field.ts: -------------------------------------------------------------------------------- 1 | import { AllFieldDefinition, FieldDefinition, FieldType } from './definitions' 2 | 3 | /** 4 | * Describes a field in a {@link Schema}. 5 | */ 6 | export class Field { 7 | 8 | readonly #name: string 9 | #definition: AllFieldDefinition 10 | 11 | /** 12 | * Creates a Field. 13 | * 14 | * @param name The name of the Field. 15 | * @param definition The underlying {@link FieldDefinition}. 16 | */ 17 | constructor(name: string, definition: FieldDefinition) { 18 | this.#name = name 19 | this.#definition = definition 20 | } 21 | 22 | /** The name of the field. */ 23 | get name(): string { 24 | return this.#name 25 | } 26 | 27 | /** The {@link FieldType | type} of the field. */ 28 | get type(): FieldType { 29 | return this.#definition.type 30 | } 31 | 32 | /** The field name used to store this {@link Field} in a Hash. */ 33 | get hashField(): string { 34 | return this.#definition.field ?? this.#definition.alias ?? this.name 35 | } 36 | 37 | /** The JSONPath used to store this {@link Field} in a JSON document. */ 38 | get jsonPath(): string { 39 | if (this.#definition.path) return this.#definition.path 40 | const alias = (this.#definition.alias ?? this.name).replace(/"/g, '\\"') 41 | return this.isArray ? `$["${alias}"][*]` : `$["${alias}"]` 42 | } 43 | 44 | /** The separator for string[] fields when stored in Hashes. */ 45 | get separator(): string { 46 | return this.#definition.separator ?? '|' 47 | } 48 | 49 | /** Indicates that the field as sortable. */ 50 | get sortable(): boolean { 51 | return this.#definition.sortable ?? false 52 | } 53 | 54 | /** The case-sensitivity of the field. */ 55 | get caseSensitive(): boolean { 56 | return this.#definition.caseSensitive ?? false 57 | } 58 | 59 | /** Indicates the field as being indexed—and thus queryable—by RediSearch. */ 60 | get indexed(): boolean { 61 | return this.#definition.indexed ?? true 62 | } 63 | 64 | /** Indicates that the field as indexed with stemming support. */ 65 | get stemming(): boolean { 66 | return this.#definition.stemming ?? true 67 | } 68 | 69 | /** Indicates that the field is normalized. Ignored if sortable is false. */ 70 | get normalized(): boolean { 71 | return this.#definition.normalized ?? true 72 | } 73 | 74 | /** The search weight of the field. */ 75 | get weight(): number | null { 76 | return this.#definition.weight ?? null 77 | } 78 | 79 | /** The phonetic matcher for the field. */ 80 | get matcher(): string | null { 81 | return this.#definition.matcher ?? null 82 | } 83 | 84 | /** Is this type an array or not. */ 85 | get isArray(): boolean { 86 | return this.type.endsWith('[]') 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definitions' 2 | export * from './field' 3 | export * from './options' 4 | export * from './schema' 5 | -------------------------------------------------------------------------------- /lib/schema/options.ts: -------------------------------------------------------------------------------- 1 | /** The type of data structure in Redis to map objects to. */ 2 | export type DataStructure = 'HASH' | 'JSON'; 3 | 4 | /** A function that generates random entityIds. */ 5 | export type IdStrategy = () => Promise; 6 | 7 | /** Valid values for how to use stop words for a given {@link Schema}. */ 8 | export type StopWordOptions = 'OFF' | 'DEFAULT' | 'CUSTOM'; 9 | 10 | /** Configuration options for a {@link Schema}. */ 11 | export type SchemaOptions = { 12 | 13 | /** 14 | * The name used by RediSearch to store the index for this {@link Schema}. Defaults 15 | * to prefix followed by `:index`. So, for a prefix of `Foo`, it would use `Foo:index`. 16 | */ 17 | indexName?: string 18 | 19 | /** 20 | * The name used by Redis OM to store the hash of the index for this {@link Schema}. 21 | * Defaults to prefix followed by `:index:hash`. So, for a prefix of `Foo`, it would 22 | * use `Foo:index:hash`. 23 | */ 24 | indexHashName?: string 25 | 26 | /** The data structure used to store the {@link Entity} in Redis. Can be set 27 | * to either `JSON` or `HASH`. Defaults to JSON. 28 | */ 29 | dataStructure?: DataStructure 30 | 31 | /** 32 | * A function that generates a random entityId. Defaults to a function that generates 33 | * [ULIDs](https://github.com/ulid/spec). Combined with prefix to generate a Redis key. 34 | * If prefix is `Foo` and idStratgey returns `12345` then the generated key would be 35 | * `Foo:12345`. 36 | */ 37 | idStrategy?: IdStrategy 38 | 39 | /** 40 | * Configures the usage of stop words. Valid values are `OFF`, `DEFAULT`, and `CUSTOM`. 41 | * Setting this to `OFF` disables all stop words. Setting this to `DEFAULT` uses the 42 | * stop words intrinsic to RediSearch. Setting this to `CUSTOM` tells RediSearch to 43 | * use the stop words in `stopWords`. 44 | */ 45 | useStopWords?: StopWordOptions 46 | 47 | /** 48 | * Stop words to be used by this schema. If `useStopWords` is 49 | * anything other than `CUSTOM`, this option is ignored. 50 | */ 51 | stopWords?: Array 52 | } 53 | -------------------------------------------------------------------------------- /lib/search/index.ts: -------------------------------------------------------------------------------- 1 | export * from './results-converter' 2 | export * from './search' 3 | export * from './where-and' 4 | export * from './where-boolean' 5 | export * from './where-date' 6 | export * from './where-field' 7 | export * from './where-number' 8 | export * from './where-or' 9 | export * from './where-point' 10 | export * from './where-string-array' 11 | export * from './where-string' 12 | export * from './where-text' 13 | export * from './where' 14 | -------------------------------------------------------------------------------- /lib/search/results-converter.ts: -------------------------------------------------------------------------------- 1 | import { RedisHashData, RedisJsonData, SearchDocument, SearchResults } from "../client" 2 | import { Entity, EntityData, EntityId, EntityKeyName } from "../entity" 3 | import { Schema } from "../schema" 4 | import { fromRedisHash, fromRedisJson } from "../transformer" 5 | 6 | export function extractCountFromSearchResults(results: SearchResults): number { 7 | return results.total 8 | } 9 | 10 | export function extractKeyNamesFromSearchResults(results: SearchResults): string[] { 11 | return results.documents.map(document => document.id) 12 | } 13 | 14 | export function extractEntityIdsFromSearchResults(schema: Schema, results: SearchResults): string[] { 15 | const keyNames = extractKeyNamesFromSearchResults(results) 16 | return keyNamesToEntityIds(schema.schemaName, keyNames) 17 | } 18 | 19 | export function extractEntitiesFromSearchResults(schema: Schema, results: SearchResults): T[] { 20 | if (schema.dataStructure === 'HASH') { 21 | return results.documents.map(document => hashDocumentToEntity(schema, document)) 22 | } else { 23 | return results.documents.map(document => jsonDocumentToEntity(schema, document)) 24 | } 25 | } 26 | 27 | function hashDocumentToEntity(schema: Schema, document: SearchDocument): T { 28 | const keyName: string = document.id 29 | const hashData: RedisHashData = document.value 30 | 31 | const entityData = fromRedisHash(schema, hashData) 32 | return enrichEntityData(schema.schemaName, keyName, entityData) 33 | } 34 | 35 | function jsonDocumentToEntity(schema: Schema, document: SearchDocument): T { 36 | const keyName: string = document.id 37 | const jsonData: RedisJsonData = document.value['$'] ?? false ? JSON.parse(document.value['$']) : document.value 38 | 39 | const entityData = fromRedisJson(schema, jsonData) 40 | return enrichEntityData(schema.schemaName, keyName, entityData) 41 | } 42 | 43 | function enrichEntityData(keyPrefix: string, keyName: string, entityData: EntityData): T { 44 | const entityId = keyNameToEntityId(keyPrefix, keyName) 45 | return {...entityData, [EntityId]: entityId, [EntityKeyName]: keyName} as T 46 | } 47 | 48 | function keyNamesToEntityIds(keyPrefix: string, keyNames: string[]): string[] { 49 | return keyNames.map(keyName => keyNameToEntityId(keyPrefix, keyName)) 50 | } 51 | 52 | function keyNameToEntityId(keyPrefix: string, keyName: string): string { 53 | const escapedPrefix = keyPrefix.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') 54 | const regex = new RegExp(`^${escapedPrefix}:`) 55 | return keyName.replace(regex, "") 56 | } 57 | -------------------------------------------------------------------------------- /lib/search/where-and.ts: -------------------------------------------------------------------------------- 1 | import { Where } from "./where" 2 | 3 | export class WhereAnd extends Where { 4 | private left: Where 5 | private right: Where 6 | 7 | constructor(left: Where, right: Where) { 8 | super() 9 | this.left = left 10 | this.right = right 11 | } 12 | 13 | toString(): string { 14 | return `( ${this.left.toString()} ${this.right.toString()} )` 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/search/where-boolean.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity" 2 | import { Search } from "./search" 3 | import { WhereField } from "./where-field" 4 | 5 | export abstract class WhereBoolean extends WhereField { 6 | protected value!: boolean 7 | 8 | eq(value: boolean): Search { 9 | this.value = value 10 | return this.search 11 | } 12 | 13 | equal(value: boolean): Search { return this.eq(value) } 14 | equals(value: boolean): Search { return this.eq(value) } 15 | equalTo(value: boolean): Search { return this.eq(value) } 16 | 17 | true(): Search { return this.eq(true) } 18 | false(): Search { return this.eq(false) } 19 | 20 | abstract toString(): string 21 | } 22 | 23 | export class WhereHashBoolean extends WhereBoolean { 24 | toString(): string { 25 | return this.buildQuery(`{${this.value ? '1' : '0'}}`) 26 | } 27 | } 28 | 29 | export class WhereJsonBoolean extends WhereBoolean { 30 | toString(): string { 31 | return this.buildQuery(`{${this.value}}`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/search/where-date.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity" 2 | import { Search } from "./search" 3 | import { WhereField } from "./where-field" 4 | 5 | export class WhereDate extends WhereField { 6 | private lower: number = Number.NEGATIVE_INFINITY 7 | private upper: number = Number.POSITIVE_INFINITY 8 | private lowerExclusive: boolean = false 9 | private upperExclusive: boolean = false 10 | 11 | eq(value: Date | number | string): Search { 12 | const epoch = this.coerceDateToEpoch(value) 13 | this.lower = epoch 14 | this.upper = epoch 15 | return this.search 16 | } 17 | 18 | gt(value: Date | number | string): Search { 19 | const epoch = this.coerceDateToEpoch(value) 20 | this.lower = epoch 21 | this.lowerExclusive = true 22 | return this.search 23 | } 24 | 25 | gte(value: Date | number | string): Search { 26 | this.lower = this.coerceDateToEpoch(value) 27 | return this.search 28 | } 29 | 30 | lt(value: Date | number | string): Search { 31 | this.upper = this.coerceDateToEpoch(value) 32 | this.upperExclusive = true 33 | return this.search 34 | } 35 | 36 | lte(value: Date | number | string): Search { 37 | this.upper = this.coerceDateToEpoch(value) 38 | return this.search 39 | } 40 | 41 | between(lower: Date | number | string, upper: Date | number | string): Search { 42 | this.lower = this.coerceDateToEpoch(lower) 43 | this.upper = this.coerceDateToEpoch(upper) 44 | return this.search 45 | } 46 | 47 | equal(value: Date | number | string): Search { return this.eq(value) } 48 | equals(value: Date | number | string): Search { return this.eq(value) } 49 | equalTo(value: Date | number | string): Search { return this.eq(value) } 50 | 51 | greaterThan(value: Date | number | string): Search { return this.gt(value) } 52 | greaterThanOrEqualTo(value: Date | number | string): Search { return this.gte(value) } 53 | lessThan(value: Date | number | string): Search { return this.lt(value) } 54 | lessThanOrEqualTo(value: Date | number | string): Search { return this.lte(value) } 55 | 56 | on(value: Date | number | string): Search { return this.eq(value) } 57 | after(value: Date | number | string): Search { return this.gt(value) } 58 | before(value: Date | number | string): Search { return this.lt(value) } 59 | onOrAfter(value: Date | number | string): Search { return this.gte(value) } 60 | onOrBefore(value: Date | number | string): Search { return this.lte(value) } 61 | 62 | toString(): string { 63 | const lower = this.makeLowerString() 64 | const upper = this.makeUpperString() 65 | return this.buildQuery(`[${lower} ${upper}]`) 66 | } 67 | 68 | private makeLowerString() { 69 | if (this.lower === Number.NEGATIVE_INFINITY) return '-inf' 70 | if (this.lowerExclusive) return `(${this.lower}` 71 | return this.lower.toString() 72 | } 73 | 74 | private makeUpperString() { 75 | if (this.upper === Number.POSITIVE_INFINITY) return '+inf' 76 | if (this.upperExclusive) return `(${this.upper}` 77 | return this.upper.toString() 78 | } 79 | 80 | private coerceDateToEpoch(value: Date | number | string) { 81 | if (value instanceof Date) return value.getTime() / 1000 82 | if (typeof value === 'string') return new Date(value).getTime() / 1000 83 | return value 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/search/where-number.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity" 2 | import { Search } from "./search" 3 | import { WhereField } from "./where-field" 4 | 5 | export class WhereNumber extends WhereField { 6 | private lower: number = Number.NEGATIVE_INFINITY 7 | private upper: number = Number.POSITIVE_INFINITY 8 | private lowerExclusive: boolean = false 9 | private upperExclusive: boolean = false 10 | 11 | eq(value: number): Search { 12 | this.lower = value 13 | this.upper = value 14 | return this.search 15 | } 16 | 17 | gt(value: number): Search { 18 | this.lower = value 19 | this.lowerExclusive = true 20 | return this.search 21 | } 22 | 23 | gte(value: number): Search { 24 | this.lower = value 25 | return this.search 26 | } 27 | 28 | lt(value: number): Search { 29 | this.upper = value 30 | this.upperExclusive = true 31 | return this.search 32 | } 33 | 34 | lte(value: number): Search { 35 | this.upper = value 36 | return this.search 37 | } 38 | 39 | between(lower: number, upper: number): Search { 40 | this.lower = lower 41 | this.upper = upper 42 | return this.search 43 | } 44 | 45 | equal(value: number): Search { return this.eq(value) } 46 | equals(value: number): Search { return this.eq(value) } 47 | equalTo(value: number): Search { return this.eq(value) } 48 | 49 | greaterThan(value: number): Search { return this.gt(value) } 50 | greaterThanOrEqualTo(value: number): Search { return this.gte(value) } 51 | lessThan(value: number): Search { return this.lt(value) } 52 | lessThanOrEqualTo(value: number): Search { return this.lte(value) } 53 | 54 | toString(): string { 55 | const lower = this.makeLowerString() 56 | const upper = this.makeUpperString() 57 | return this.buildQuery(`[${lower} ${upper}]`) 58 | } 59 | 60 | private makeLowerString() { 61 | if (this.lower === Number.NEGATIVE_INFINITY) return '-inf' 62 | if (this.lowerExclusive) return `(${this.lower}` 63 | return this.lower.toString() 64 | } 65 | 66 | private makeUpperString() { 67 | if (this.upper === Number.POSITIVE_INFINITY) return '+inf' 68 | if (this.upperExclusive) return `(${this.upper}` 69 | return this.upper.toString() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/search/where-or.ts: -------------------------------------------------------------------------------- 1 | import { Where } from "./where" 2 | 3 | export class WhereOr extends Where { 4 | private left: Where 5 | private right: Where 6 | 7 | constructor(left: Where, right: Where) { 8 | super() 9 | this.left = left 10 | this.right = right 11 | } 12 | 13 | toString(): string { 14 | return `( ${this.left.toString()} | ${this.right.toString()} )` 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/search/where-string-array.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity" 2 | import { Search } from "./search" 3 | import { WhereField } from "./where-field" 4 | 5 | export class WhereStringArray extends WhereField { 6 | private value!: Array 7 | 8 | contain(value: string): Search { 9 | this.value = [value] 10 | return this.search 11 | } 12 | 13 | contains(value: string): Search { return this.contain(value) } 14 | 15 | containsOneOf(...value: Array): Search { 16 | this.value = value 17 | return this.search 18 | } 19 | 20 | containOneOf(...value: Array): Search { return this.containsOneOf(...value) } 21 | 22 | toString(): string { 23 | const escapedValue = this.value.map(s => this.escapePunctuationAndSpaces(s)).join('|') 24 | return this.buildQuery(`{${escapedValue}}`) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/search/where-string.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity" 2 | import { Search } from "./search" 3 | import { WhereField } from "./where-field" 4 | import { SemanticSearchError } from "../error" 5 | 6 | export class WhereString extends WhereField { 7 | private value!: string 8 | 9 | eq(value: string | number | boolean): Search { 10 | this.value = value.toString() 11 | return this.search 12 | } 13 | 14 | equal(value: string | number | boolean): Search { return this.eq(value) } 15 | equals(value: string | number | boolean): Search { return this.eq(value) } 16 | equalTo(value: string | number | boolean): Search { return this.eq(value) } 17 | 18 | match(_: string | number | boolean): Search { return this.throwMatchExcpetion() } 19 | matches(_: string | number | boolean): Search { return this.throwMatchExcpetion() } 20 | matchExact(_: string | number | boolean): Search { return this.throwMatchExcpetion() } 21 | matchExactly(_: string | number | boolean): Search { return this.throwMatchExcpetion() } 22 | matchesExactly(_: string | number | boolean): Search { return this.throwMatchExcpetion() } 23 | 24 | get exact() { return this.throwMatchExcpetionReturningThis() } 25 | get exactly() { return this.throwMatchExcpetionReturningThis() } 26 | 27 | toString(): string { 28 | const escapedValue = this.escapePunctuationAndSpaces(this.value) 29 | return this.buildQuery(`{${escapedValue}}`) 30 | } 31 | 32 | private throwMatchExcpetion(): Search { 33 | throw new SemanticSearchError("Cannot perform full-text search operations like .match on field of type 'string'. If full-text search is needed on this field, change the type to 'text' in the Schema.") 34 | } 35 | 36 | private throwMatchExcpetionReturningThis(): WhereString { 37 | throw new SemanticSearchError("Cannot perform full-text search operations like .match on field of type 'string'. If full-text search is needed on this field, change the type to 'text' in the Schema.") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/search/where-text.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../entity" 2 | import { Search } from "./search" 3 | import { WhereField } from "./where-field" 4 | 5 | import { SemanticSearchError } from "../error" 6 | 7 | export class WhereText extends WhereField { 8 | private value!: string 9 | private exactValue = false 10 | private fuzzyMatching!: boolean 11 | private levenshteinDistance!: number 12 | 13 | match( 14 | value: string | number | boolean, 15 | options: { fuzzyMatching?: boolean; levenshteinDistance?: 1 | 2 | 3 } = { 16 | fuzzyMatching: false, 17 | levenshteinDistance: 1, 18 | } 19 | ): Search { 20 | this.value = value.toString() 21 | this.fuzzyMatching = options.fuzzyMatching ?? false 22 | this.levenshteinDistance = options.levenshteinDistance ?? 1 23 | return this.search 24 | } 25 | 26 | matchExact(value: string | number | boolean): Search { 27 | this.exact.value = value.toString() 28 | return this.search 29 | } 30 | 31 | matches( 32 | value: string | number | boolean, 33 | options: { fuzzyMatching?: boolean; levenshteinDistance?: 1 | 2 | 3 } = { 34 | fuzzyMatching: false, 35 | levenshteinDistance: 1, 36 | } 37 | ): Search { return this.match(value, options) } 38 | matchExactly(value: string | number | boolean): Search { return this.matchExact(value) } 39 | matchesExactly(value: string | number | boolean): Search { return this.matchExact(value) } 40 | 41 | get exact() { 42 | this.exactValue = true 43 | return this 44 | } 45 | 46 | get exactly() { 47 | return this.exact 48 | } 49 | 50 | eq(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } 51 | equal(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } 52 | equals(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } 53 | equalTo(_: string | number | boolean): Search { return this.throwEqualsExcpetion() } 54 | 55 | toString(): string { 56 | const escapedValue = this.escapePunctuation(this.value) 57 | 58 | if (this.exactValue) { 59 | return this.buildQuery(`"${escapedValue}"`) 60 | } else if (this.fuzzyMatching) { 61 | return this.buildQuery(`${"%".repeat(this.levenshteinDistance)}${escapedValue}${"%".repeat(this.levenshteinDistance)}`); 62 | } else { 63 | return this.buildQuery(`'${escapedValue}'`) 64 | } 65 | } 66 | 67 | private throwEqualsExcpetion(): Search { 68 | throw new SemanticSearchError("Cannot call .equals on a field of type 'text', either use .match to perform full-text search or change the type to 'string' in the Schema.") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/search/where.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract base class used extensively with {@link Search}. 3 | */ 4 | export abstract class Where { 5 | /** 6 | * Converts this {@link Where} into a portion of a RediSearch query. 7 | */ 8 | abstract toString(): string 9 | } 10 | -------------------------------------------------------------------------------- /lib/transformer/from-hash-transformer.ts: -------------------------------------------------------------------------------- 1 | import { Field, Schema } from "../schema" 2 | import { RedisHashData } from "../client" 3 | import { convertEpochStringToDate, convertStringToNumber, convertStringToPoint, isNotNullish, isNullish, isNumberString, isPointString, stringifyError } from "./transformer-common" 4 | import { EntityData } from "../entity" 5 | import { InvalidHashValue } from "../error" 6 | 7 | export function fromRedisHash(schema: Schema, hashData: RedisHashData): EntityData { 8 | const data: { [key: string]: any } = { ...hashData } 9 | schema.fields.forEach(field => { 10 | if (field.hashField) delete data[field.hashField] 11 | const value = hashData[field.hashField] 12 | if (isNotNullish(value)) { 13 | data[field.name] = convertKnownValueFromString(field, value!) 14 | } else if (isNullish(value) && field.type === 'string[]') { 15 | data[field.name] = [] 16 | } 17 | }) 18 | return data 19 | } 20 | 21 | function convertKnownValueFromString(field: Field, value: string): any { 22 | switch (field.type) { 23 | case 'boolean': 24 | if (value === '1') return true 25 | if (value === '0') return false 26 | throw new InvalidHashValue(field) 27 | case 'number': 28 | if (isNumberString(value)) return convertStringToNumber(value) 29 | throw new InvalidHashValue(field) 30 | case 'date': 31 | if (isNumberString(value)) return convertEpochStringToDate(value) 32 | throw new InvalidHashValue(field) 33 | case 'point': 34 | if (isPointString(value)) return convertStringToPoint(value) 35 | throw new InvalidHashValue(field) 36 | case 'string': 37 | case 'text': 38 | return value 39 | case 'string[]': 40 | return convertStringToStringArray(value, field.separator) 41 | } 42 | } 43 | 44 | const convertStringToStringArray = (value: string, separator: string): string[] => value.split(separator) 45 | -------------------------------------------------------------------------------- /lib/transformer/from-json-transformer.ts: -------------------------------------------------------------------------------- 1 | import { JSONPath } from 'jsonpath-plus' 2 | import clone from 'just-clone' 3 | 4 | import { Field, Schema } from "../schema" 5 | import { RedisJsonData } from "../client" 6 | 7 | import { convertEpochToDate, convertKnownValueToString, convertStringToPoint, isArray, isBoolean, isNull, isNumber, isPointString, isString, stringifyError } from "./transformer-common" 8 | import { EntityData } from '../entity' 9 | import { InvalidJsonValue, NullJsonValue } from '../error' 10 | 11 | 12 | export function fromRedisJson(schema: Schema, json: RedisJsonData): EntityData { 13 | const data: EntityData = clone(json) 14 | convertFromRedisJsonKnown(schema, data) 15 | return data 16 | } 17 | 18 | function convertFromRedisJsonKnown(schema: Schema, data: EntityData) { 19 | schema.fields.forEach(field => { 20 | 21 | const path = field.jsonPath 22 | const results = JSONPath({ resultType: 'all', path, json: data }) 23 | 24 | if (field.isArray) { 25 | convertKnownResultsFromJson(field, results) 26 | return 27 | } 28 | 29 | if (results.length === 1) { 30 | convertKnownResultFromJson(field, results[0]) 31 | return 32 | } 33 | 34 | if (results.length > 1) throw new InvalidJsonValue(field) 35 | }) 36 | } 37 | 38 | function convertKnownResultFromJson(field: Field, result: any): any { 39 | const { value, parent, parentProperty } = result 40 | parent[parentProperty] = convertKnownValueFromJson(field, value) 41 | } 42 | 43 | function convertKnownResultsFromJson(field: Field, results: any[]): any { 44 | results.forEach((result: any) => { 45 | const { value, parent, parentProperty } = result 46 | parent[parentProperty] = convertKnownArrayValueFromJson(field, value) 47 | }) 48 | } 49 | 50 | function convertKnownValueFromJson(field: Field, value: any): any { 51 | if (isNull(value)) return value 52 | 53 | switch (field.type) { 54 | case 'boolean': 55 | if (isBoolean(value)) return value 56 | throw new InvalidJsonValue(field) 57 | case 'number': 58 | if (isNumber(value)) return value 59 | throw new InvalidJsonValue(field) 60 | case 'date': 61 | if (isNumber(value)) return convertEpochToDate(value) 62 | throw new InvalidJsonValue(field) 63 | case 'point': 64 | if (isPointString(value)) return convertStringToPoint(value) 65 | throw new InvalidJsonValue(field) 66 | case 'string': 67 | case 'text': 68 | if (isBoolean(value)) return value.toString() 69 | if (isNumber(value)) return value.toString() 70 | if (isString(value)) return value 71 | throw new InvalidJsonValue(field) 72 | } 73 | } 74 | 75 | function convertKnownArrayValueFromJson(field: Field, value: any) { 76 | 77 | if (isNull(value)) throw new NullJsonValue(field) 78 | 79 | switch (field.type) { 80 | case 'string[]': 81 | if (isBoolean(value)) return value.toString() 82 | if (isNumber(value)) return value.toString() 83 | if (isString(value)) return value 84 | throw new InvalidJsonValue(field) 85 | case 'number[]': 86 | if (isNumber(value)) return value 87 | throw new InvalidJsonValue(field) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/transformer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './from-hash-transformer' 2 | export * from './from-json-transformer' 3 | export * from './to-hash-transformer' 4 | export * from './to-json-transformer' 5 | -------------------------------------------------------------------------------- /lib/transformer/to-hash-transformer.ts: -------------------------------------------------------------------------------- 1 | import { Field, Schema } from "../schema" 2 | import { RedisHashData } from "../client" 3 | import { convertBooleanToString, convertDateToString, convertEpochDateToString, convertIsoDateToString, convertNumberToString, convertPointToString, isArray, isBoolean, isDate, isDateString, isNotNullish, isNumber, isObject, isPoint, isString, stringifyError } from "./transformer-common" 4 | import { EntityData } from "../entity" 5 | import { InvalidHashInput, NestedHashInput, RedisOmError, ArrayHashInput } from "../error" 6 | 7 | export function toRedisHash(schema: Schema, data: EntityData): RedisHashData { 8 | const hashData: RedisHashData = {} 9 | Object.entries(data).forEach(([key, value]) => { 10 | if (isNotNullish(value)) { 11 | const field = schema.fieldByName(key) 12 | const hashField = field ? field.hashField : key 13 | if (field && field.type === 'string[]' && isArray(value) && (value as string[]).length === 0) { 14 | // no-op 15 | } else { 16 | hashData[hashField] = field ? convertKnownValueToString(field, value) : convertUnknownValueToString(key, value) 17 | } 18 | } 19 | }) 20 | return hashData 21 | } 22 | 23 | function convertKnownValueToString(field: Field, value: any): string { 24 | switch (field.type) { 25 | case 'boolean': 26 | if (isBoolean(value)) return convertBooleanToString(value) 27 | throw new InvalidHashInput(field) 28 | case 'number': 29 | if (isNumber(value)) return convertNumberToString(value) 30 | throw new InvalidHashInput(field) 31 | case 'date': 32 | if (isNumber(value)) return convertEpochDateToString(value) 33 | if (isDate(value)) return convertDateToString(value) 34 | if (isDateString(value)) return convertIsoDateToString(value) 35 | throw new InvalidHashInput(field) 36 | case 'point': 37 | if (isPoint(value)) return convertPointToString(value) 38 | throw new InvalidHashInput(field) 39 | case 'string': 40 | case 'text': 41 | if (isBoolean(value)) return value.toString() 42 | if (isNumber(value)) return value.toString() 43 | if (isString(value)) return value 44 | throw new InvalidHashInput(field) 45 | case 'string[]': 46 | if (isArray(value)) return convertStringArrayToString(value, field.separator) 47 | throw new InvalidHashInput(field) 48 | default: 49 | throw new RedisOmError(`Expected a valid field type but received: ${field.type}`) 50 | } 51 | } 52 | 53 | function convertUnknownValueToString(key: string, value: any): string { 54 | if (isBoolean(value)) return convertBooleanToString(value) 55 | if (isNumber(value)) return convertNumberToString(value) 56 | if (isDate(value)) return convertDateToString(value) 57 | if (isPoint(value)) return convertPointToString(value) 58 | if (isArray(value)) throw new ArrayHashInput(key) 59 | if (isObject(value)) throw new NestedHashInput(key) 60 | return value.toString() 61 | } 62 | 63 | const convertStringArrayToString = (value: string[], separator: string): string => value.join(separator) 64 | -------------------------------------------------------------------------------- /lib/transformer/to-json-transformer.ts: -------------------------------------------------------------------------------- 1 | import { JSONPath } from 'jsonpath-plus' 2 | import clone from 'just-clone' 3 | 4 | import { Field, Schema } from "../schema" 5 | import { RedisJsonData } from "../client" 6 | 7 | import { convertDateToEpoch, convertIsoDateToEpoch, convertPointToString, isArray, isBoolean, isDate, isDateString, isDefined, isNull, isNumber, isObject, isPoint, isString, isUndefined, stringifyError } from "./transformer-common" 8 | import { EntityData } from '../entity' 9 | import { InvalidJsonInput, NullJsonInput } from '../error' 10 | 11 | export function toRedisJson(schema: Schema, data: EntityData): RedisJsonData { 12 | let json: RedisJsonData = clone(data) 13 | convertToRedisJsonKnown(schema, json) 14 | return convertToRedisJsonUnknown(json) 15 | } 16 | 17 | function convertToRedisJsonKnown(schema: Schema, json: RedisJsonData) { 18 | schema.fields.forEach(field => { 19 | 20 | const results = JSONPath({ resultType: 'all', path: field.jsonPath, json }) 21 | 22 | if (field.isArray) { 23 | convertKnownResultsToJson(field, results) 24 | return 25 | } 26 | 27 | if (results.length === 0) return 28 | 29 | if (results.length === 1) { 30 | convertKnownResultToJson(field, results[0]) 31 | return 32 | } 33 | 34 | throw new InvalidJsonInput(field) 35 | }) 36 | } 37 | 38 | function convertToRedisJsonUnknown(json: RedisJsonData) { 39 | Object.entries(json).forEach(([key, value]) => { 40 | if (isUndefined(value)) { 41 | delete json[key] 42 | } else if (isObject(value)) { 43 | json[key] = convertToRedisJsonUnknown(value) 44 | } else { 45 | json[key] = convertUnknownValueToJson(value) 46 | } 47 | }) 48 | return json 49 | } 50 | 51 | function convertKnownResultToJson(field: Field, result: any): any { 52 | const { value, parent, parentProperty } = result 53 | if (isDefined(value)) parent[parentProperty] = convertKnownValueToJson(field, value) 54 | } 55 | 56 | function convertKnownResultsToJson(field: Field, results: any[]): any { 57 | results.forEach((result: any) => { 58 | const { value, parent, parentProperty } = result 59 | if (isNull(value)) throw new NullJsonInput(field) 60 | if (isUndefined(value) && isArray(parent)) throw new NullJsonInput(field) 61 | if (isDefined(value)) parent[parentProperty] = convertKnownArrayValueToJson(field, value) 62 | }) 63 | } 64 | 65 | function convertKnownValueToJson(field: Field, value: any): any { 66 | 67 | if (isNull(value)) return value 68 | 69 | switch (field.type) { 70 | case 'boolean': 71 | if (isBoolean(value)) return value 72 | throw new InvalidJsonInput(field) 73 | case 'number': 74 | if (isNumber(value)) return value 75 | throw new InvalidJsonInput(field) 76 | case 'date': 77 | if (isNumber(value)) return value 78 | if (isDate(value)) return convertDateToEpoch(value) 79 | if (isDateString(value)) return convertIsoDateToEpoch(value) 80 | throw new InvalidJsonInput(field) 81 | case 'point': 82 | if (isPoint(value)) return convertPointToString(value) 83 | throw new InvalidJsonInput(field) 84 | case 'string': 85 | case 'text': 86 | if (isBoolean(value)) return value.toString() 87 | if (isNumber(value)) return value.toString() 88 | if (isString(value)) return value 89 | throw new InvalidJsonInput(field) 90 | } 91 | } 92 | 93 | function convertKnownArrayValueToJson(field: Field, value: any) { 94 | switch (field.type) { 95 | case 'string[]': 96 | if (isBoolean(value)) return value.toString() 97 | if (isNumber(value)) return value.toString() 98 | if (isString(value)) return value 99 | throw new InvalidJsonInput(field) 100 | case 'number[]': 101 | if (isNumber(value)) return value 102 | throw new InvalidJsonInput(field) 103 | } 104 | } 105 | 106 | function convertUnknownValueToJson(value: any): any { 107 | if (isDate(value)) return convertDateToEpoch(value) 108 | return value 109 | } 110 | -------------------------------------------------------------------------------- /lib/transformer/transformer-common.ts: -------------------------------------------------------------------------------- 1 | import { PointOutOfRange, RedisOmError } from "../error" 2 | import { Point } from "../entity" 3 | 4 | export const isNull = (value: any): boolean => value === null 5 | export const isDefined = (value: any): boolean => value !== undefined 6 | export const isUndefined = (value: any): boolean => value === undefined 7 | export const isNullish = (value: any): boolean => value === undefined || value === null 8 | export const isNotNullish = (value: any): boolean => value !== undefined && value !== null 9 | export const isBoolean = (value: any): boolean => typeof value === 'boolean' 10 | export const isNumber = (value: any): boolean => typeof value === 'number' 11 | export const isString = (value: any): boolean => typeof value === 'string' 12 | export const isDate = (value: any): boolean => value instanceof Date 13 | export const isDateString = (value: any): boolean => isString(value) && !isNaN(new Date(value).getTime()) 14 | export const isArray = (value: any): boolean => Array.isArray(value) 15 | export const isObject = (value: any): boolean => value !== null && typeof value === 'object' && !isArray(value) && !isDate(value) 16 | 17 | export const isPoint = (value: any): boolean => 18 | isObject(value) && 19 | Object.keys(value).length === 2 && 20 | typeof value.latitude === 'number' && 21 | typeof value.longitude === 'number' 22 | 23 | export const isNumberString = (value: string): boolean => !isNaN(Number(value)) 24 | export const isPointString = (value: string): boolean => /^-?\d+(\.\d*)?,-?\d+(\.\d*)?$/.test(value) 25 | 26 | // As per https://redis.io/commands/geoadd/ and local testing 27 | // Valid latitudes are from -85.05112878 to 85.05112878 degrees (*NOT* -90 to +90) 28 | const isValidPoint = (value: any) => 29 | Math.abs(value.latitude) <= 85.05112878 && 30 | Math.abs(value.longitude) <= 180 31 | 32 | export const convertBooleanToString = (value: boolean) => value ? '1' : '0' 33 | 34 | export const convertNumberToString = (value: number) => value.toString() 35 | export const convertStringToNumber = (value: string): number => Number.parseFloat(value) 36 | 37 | export const convertDateToEpoch = (value: Date) => (value.getTime() / 1000) 38 | export const convertDateToString = (value: Date) => convertDateToEpoch(value).toString() 39 | export const convertEpochDateToString = (value: number) => convertNumberToString(value) 40 | 41 | export const convertIsoDateToEpoch = (value: string) => convertDateToEpoch(new Date(value)) 42 | export const convertIsoDateToString = (value: string) => convertDateToString(new Date(value)) 43 | 44 | export const convertEpochStringToDate = (value: string): Date => new Date(convertEpochToDate(convertStringToNumber(value))) 45 | export const convertEpochToDate = (value: number): Date => new Date(value * 1000) 46 | 47 | export const convertPointToString = (value: Point) => { 48 | if (isValidPoint(value)) return `${value.longitude},${value.latitude}` 49 | throw new PointOutOfRange(value) 50 | } 51 | 52 | export const convertStringToPoint = (value: string): Point => { 53 | const [ longitude, latitude ] = value.split(',').map(convertStringToNumber) 54 | return { longitude: longitude!, latitude: latitude! } 55 | } 56 | 57 | export function convertKnownValueToString(value: any) { 58 | if (isBoolean(value)) return value.toString() 59 | if (isNumber(value)) return value.toString() 60 | if (isString(value)) return value 61 | throw new RedisOmError(`Expected a string but received a non-string`) 62 | } 63 | 64 | export const stringifyError = (value: any) => JSON.stringify(value, null, 1) 65 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-om", 3 | "version": "0.4.7", 4 | "description": "Object mapping, and more, for Redis and Node.js. Written in TypeScript.", 5 | "main": "dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "files": [ 8 | "README.md", 9 | "CHANGELOG", 10 | "LICENSE", 11 | "logo.svg", 12 | "dist", 13 | "docs" 14 | ], 15 | "scripts": { 16 | "build": "tsup", 17 | "docs": "rm -rf ./docs && typedoc", 18 | "test": "vitest", 19 | "test:coverage": "vitest run --coverage", 20 | "test:ui": "vitest --ui", 21 | "test:unit": "vitest --dir ./spec/unit", 22 | "test:functional": "vitest --dir ./spec/functional" 23 | }, 24 | "tsup": { 25 | "entry": [ 26 | "lib/index.ts" 27 | ], 28 | "clean": true, 29 | "dts": true, 30 | "noExternal": [ 31 | "ulid" 32 | ] 33 | }, 34 | "repository": "github:redis/redis-om-node", 35 | "homepage": "https://github.com/redis/redis-om-node#readme", 36 | "keywords": [ 37 | "redis", 38 | "orm" 39 | ], 40 | "license": "MIT", 41 | "author": "Guy Royse (http://guyroyse.com/)", 42 | "engines": { 43 | "node": ">= 14" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "^18.0.0", 47 | "@vitest/ui": "^0.17.1", 48 | "c8": "^7.11.3", 49 | "tsup": "^6.1.2", 50 | "typedoc": "^0.23.2", 51 | "typedoc-plugin-markdown": "^3.13.1", 52 | "typescript": "^4.7.2", 53 | "vitest": "^0.20.0" 54 | }, 55 | "dependencies": { 56 | "jsonpath-plus": "^10.1.0", 57 | "just-clone": "^6.1.1", 58 | "redis": "^4.6.4", 59 | "ulid": "^2.3.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /spec/functional/core/fetch-hash.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { EntityKeyName, RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createHashEntitySchema } from '../helpers/data-helper' 6 | import { removeKeys, saveHash } from '../helpers/redis-helper' 7 | 8 | import { ANOTHER_ENTITY, ANOTHER_HASH, AN_EMPTY_ENTITY, AN_ENTITY, A_HASH, A_THIRD_ENTITY, A_THIRD_HASH } from '../helpers/hash-example-data' 9 | 10 | describe("fetch hash", () => { 11 | 12 | let redis: RedisConnection 13 | let repository: Repository 14 | let schema: Schema 15 | 16 | beforeAll(async () => { 17 | redis = createClient() 18 | await redis.connect() 19 | await removeKeys(redis, 'fetch-hash:1', 'fetch-hash:2', 'fetch-hash:3') 20 | await saveHash(redis, 'fetch-hash:1', A_HASH) 21 | await saveHash(redis, 'fetch-hash:2', ANOTHER_HASH) 22 | await saveHash(redis, 'fetch-hash:3', A_THIRD_HASH) 23 | 24 | schema = createHashEntitySchema('fetch-hash') 25 | repository = new Repository(schema, redis) 26 | }) 27 | 28 | afterAll(async () => { 29 | await removeKeys(redis, 'fetch-hash:1', 'fetch-hash:2', 'fetch-hash:3') 30 | await redis.quit() 31 | }) 32 | 33 | it("fetches a single entity from Redis", async () => 34 | expect(repository.fetch('1')).resolves.toEqual({ [EntityKeyName]: 'fetch-hash:1', ...AN_ENTITY })) 35 | 36 | it("fetches an empty entity from Redis", async () => 37 | expect(repository.fetch('empty')).resolves.toEqual({ [EntityKeyName]: 'fetch-hash:empty', someStrings: [], ...AN_EMPTY_ENTITY })) 38 | 39 | it("fetches multiple entities from Redis with discrete arguments", async () => { 40 | let entities = await repository.fetch('1', '2', '3') 41 | expect(entities).toEqual(expect.arrayContaining([ 42 | { [EntityKeyName]: 'fetch-hash:1', ...AN_ENTITY }, 43 | { [EntityKeyName]: 'fetch-hash:2', ...ANOTHER_ENTITY }, 44 | { [EntityKeyName]: 'fetch-hash:3', ...A_THIRD_ENTITY } 45 | ])) 46 | }) 47 | 48 | it("fetches multiple entities from Redis with an array", async () => { 49 | let entities = await repository.fetch(['1', '2', '3']) 50 | expect(entities).toEqual(expect.arrayContaining([ 51 | { [EntityKeyName]: 'fetch-hash:1', ...AN_ENTITY }, 52 | { [EntityKeyName]: 'fetch-hash:2', ...ANOTHER_ENTITY }, 53 | { [EntityKeyName]: 'fetch-hash:3', ...A_THIRD_ENTITY } 54 | ])) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /spec/functional/core/fetch-json.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { EntityId, EntityKeyName, RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createJsonEntitySchema } from '../helpers/data-helper' 6 | import { removeKeys, saveJson } from '../helpers/redis-helper' 7 | 8 | import { ANOTHER_ENTITY, ANOTHER_JSON, AN_EMPTY_ENTITY, AN_ENTITY, A_JSON, A_THIRD_ENTITY, A_THIRD_JSON } from '../helpers/json-example-data' 9 | 10 | describe("fetch JSON", () => { 11 | 12 | let redis: RedisConnection 13 | let repository: Repository 14 | let schema: Schema 15 | 16 | beforeAll(async () => { 17 | redis = createClient() 18 | await redis.connect() 19 | await removeKeys(redis, 'fetch-json:1', 'fetch-json:2', 'fetch-json:3') 20 | await saveJson(redis, 'fetch-json:1', A_JSON) 21 | await saveJson(redis, 'fetch-json:2', ANOTHER_JSON) 22 | await saveJson(redis, 'fetch-json:3', A_THIRD_JSON) 23 | 24 | schema = createJsonEntitySchema('fetch-json') 25 | repository = new Repository(schema, redis) 26 | }) 27 | 28 | afterAll(async () => { 29 | await removeKeys(redis, 'fetch-json:1', 'fetch-json:2', 'fetch-json:3') 30 | await redis.quit() 31 | }) 32 | 33 | it("fetches a single entity from Redis", async () => 34 | expect(repository.fetch('1')).resolves.toEqual({ [EntityKeyName]: 'fetch-json:1', ...AN_ENTITY })) 35 | 36 | it("fetches an empty entity from Redis", async () => { 37 | const entity = await repository.fetch('empty') 38 | expect(entity).toEqual({ [EntityKeyName]: 'fetch-json:empty', ...AN_EMPTY_ENTITY }) 39 | }) 40 | 41 | it("fetches a missing entity from Redis", async () => { 42 | const entity = await repository.fetch('missing') 43 | expect(entity).toEqual({ [EntityId]: 'missing', [EntityKeyName]: 'fetch-json:missing' }) 44 | }) 45 | 46 | it("fetches all the entities from Redis with discrete arguments", async () => { 47 | let entities = await repository.fetch('1', '2', '3') 48 | expect(entities).toEqual(expect.arrayContaining([ 49 | { [EntityKeyName]: 'fetch-json:1', ...AN_ENTITY }, 50 | { [EntityKeyName]: 'fetch-json:2', ...ANOTHER_ENTITY }, 51 | { [EntityKeyName]: 'fetch-json:3', ...A_THIRD_ENTITY } 52 | ])) 53 | }) 54 | }) -------------------------------------------------------------------------------- /spec/functional/core/remove-hash.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createHashEntitySchema } from '../helpers/data-helper' 6 | import { keyExists, removeKeys, saveHash } from '../helpers/redis-helper' 7 | 8 | import { ANOTHER_HASH, A_HASH, A_THIRD_HASH } from '../helpers/hash-example-data' 9 | 10 | 11 | describe("remove hash", () => { 12 | 13 | let redis: RedisConnection 14 | let repository: Repository 15 | let schema: Schema 16 | 17 | beforeAll(async () => { 18 | redis = createClient() 19 | await redis.connect() 20 | schema = createHashEntitySchema('remove-hash') 21 | repository = new Repository(schema, redis) 22 | }) 23 | 24 | beforeEach(async () => { 25 | await removeKeys(redis, 'remove-hash:1', 'remove-hash:2', 'remove-hash:3') 26 | await saveHash(redis, 'remove-hash:1', A_HASH) 27 | await saveHash(redis, 'remove-hash:2', ANOTHER_HASH) 28 | await saveHash(redis, 'remove-hash:3', A_THIRD_HASH) 29 | }) 30 | 31 | afterAll(async () => { 32 | await removeKeys(redis, 'remove-hash:1', 'remove-hash:2', 'remove-hash:3') 33 | await redis.quit() 34 | }) 35 | 36 | it("removes a single entity", async () => { 37 | expect(keyExists(redis, 'remove-hash:1')).resolves.toBe(true) 38 | await repository.remove('1') 39 | expect(keyExists(redis, 'remove-hash:1')).resolves.toBe(false) 40 | }) 41 | 42 | it("removes multiple entities with discrete arguments", async () => { 43 | expect(keyExists(redis, 'remove-hash:1')).resolves.toBe(true) 44 | expect(keyExists(redis, 'remove-hash:2')).resolves.toBe(true) 45 | expect(keyExists(redis, 'remove-hash:3')).resolves.toBe(true) 46 | 47 | await repository.remove('1', '2', '3') 48 | 49 | expect(keyExists(redis, 'remove-hash:1')).resolves.toBe(false) 50 | expect(keyExists(redis, 'remove-hash:2')).resolves.toBe(false) 51 | expect(keyExists(redis, 'remove-hash:full')).resolves.toBe(false) 52 | }) 53 | 54 | it("removes multiple entities with an array", async () => { 55 | expect(keyExists(redis, 'remove-hash:1')).resolves.toBe(true) 56 | expect(keyExists(redis, 'remove-hash:2')).resolves.toBe(true) 57 | expect(keyExists(redis, 'remove-hash:3')).resolves.toBe(true) 58 | await repository.remove([ '1', '2', '3' ]) 59 | 60 | expect(keyExists(redis, 'remove-hash:1')).resolves.toBe(false) 61 | expect(keyExists(redis, 'remove-hash:2')).resolves.toBe(false) 62 | expect(keyExists(redis, 'remove-hash:3')).resolves.toBe(false) 63 | }) 64 | 65 | it("removes a non-existing entity", async () => { 66 | expect(keyExists(redis, 'remove-hash:empty')).resolves.toBe(false) 67 | await repository.remove('empty') 68 | expect(keyExists(redis, 'remove-hash:empty')).resolves.toBe(false) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /spec/functional/core/remove-json.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createJsonEntitySchema } from '../helpers/data-helper' 6 | import { keyExists, removeKeys, saveJson } from '../helpers/redis-helper' 7 | 8 | import { ANOTHER_JSON, A_JSON, A_THIRD_JSON } from '../helpers/json-example-data' 9 | 10 | describe("remove JSON", () => { 11 | 12 | let redis: RedisConnection 13 | let repository: Repository 14 | let schema: Schema 15 | 16 | beforeAll(async () => { 17 | redis = createClient() 18 | await redis.connect() 19 | schema = createJsonEntitySchema('remove-json') 20 | repository = new Repository(schema, redis) 21 | }) 22 | 23 | beforeEach(async () => { 24 | await removeKeys(redis, 'remove-json:1', 'remove-json:2', 'remove-json:3') 25 | await saveJson(redis, 'remove-json:1', A_JSON) 26 | await saveJson(redis, 'remove-json:2', ANOTHER_JSON) 27 | await saveJson(redis, 'remove-json:3', A_THIRD_JSON) 28 | }) 29 | 30 | afterAll(async () => { 31 | await removeKeys(redis, 'remove-json:1', 'remove-json:2', 'remove-json:3') 32 | await redis.quit() 33 | }) 34 | 35 | it("removes a single entity", async () => { 36 | expect(keyExists(redis, 'remove-json:1')).resolves.toBe(true) 37 | await repository.remove('1') 38 | expect(keyExists(redis, 'remove-json:1')).resolves.toBe(false) 39 | }) 40 | 41 | it("removes multiple entities with discrete arguments", async () => { 42 | expect(keyExists(redis, 'remove-json:1')).resolves.toBe(true) 43 | expect(keyExists(redis, 'remove-json:2')).resolves.toBe(true) 44 | expect(keyExists(redis, 'remove-json:3')).resolves.toBe(true) 45 | 46 | await repository.remove('1', '2', '3') 47 | 48 | expect(keyExists(redis, 'remove-json:1')).resolves.toBe(false) 49 | expect(keyExists(redis, 'remove-json:2')).resolves.toBe(false) 50 | expect(keyExists(redis, 'remove-json:3')).resolves.toBe(false) 51 | }) 52 | 53 | it("removes multiple entities with an array", async () => { 54 | expect(keyExists(redis, 'remove-json:1')).resolves.toBe(true) 55 | expect(keyExists(redis, 'remove-json:2')).resolves.toBe(true) 56 | expect(keyExists(redis, 'remove-json:3')).resolves.toBe(true) 57 | 58 | await repository.remove([ '1', '2', '3' ]) 59 | 60 | expect(keyExists(redis, 'remove-json:1')).resolves.toBe(false) 61 | expect(keyExists(redis, 'remove-json:2')).resolves.toBe(false) 62 | expect(keyExists(redis, 'remove-json:3')).resolves.toBe(false) 63 | }) 64 | 65 | it("removes a non-existing entity", async () => { 66 | expect(keyExists(redis, 'remove-json:empty')).resolves.toBe(false) 67 | await repository.remove('empty') 68 | expect(keyExists(redis, 'remove-json:empty')).resolves.toBe(false) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /spec/functional/core/save-hash.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { Entity, EntityId, EntityKeyName, RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createHashEntitySchema } from '../helpers/data-helper' 6 | import { fetchHashData, keyExists, removeKeys } from '../helpers/redis-helper' 7 | 8 | import { AN_EMPTY_ENTITY, AN_EMPTY_HASH, AN_ENTITY, A_HASH } from '../helpers/hash-example-data' 9 | 10 | describe("save hash", () => { 11 | 12 | let redis: RedisConnection 13 | let repository: Repository 14 | let schema: Schema 15 | let entity: Entity 16 | 17 | beforeAll(async () => { 18 | redis = createClient() 19 | await redis.connect() 20 | 21 | schema = createHashEntitySchema('save-hash') 22 | repository = new Repository(schema, redis) 23 | }) 24 | 25 | beforeEach(async () => await removeKeys(redis, 'save-hash:1', 'save-hash:empty')) 26 | afterAll(async () => { 27 | await removeKeys(redis, 'save-hash:1', 'save-hash:empty') 28 | await redis.quit() 29 | }) 30 | 31 | describe("when saving an entity to redis", () => { 32 | beforeEach(async () => { entity = await repository.save(AN_ENTITY) }) 33 | 34 | it('returns the expected entity', () => expect(entity).toEqual({ 35 | ...AN_ENTITY, 36 | [EntityId]: '1', 37 | [EntityKeyName]: 'save-hash:1' 38 | })) 39 | 40 | it('saves the expected Hash in Redis', async () => expect(fetchHashData(redis, 'save-hash:1')).resolves.toEqual(A_HASH)) 41 | }) 42 | 43 | describe("when saving an empty entity to redis", () => { 44 | beforeEach(async () => { entity = await repository.save(AN_EMPTY_ENTITY) }) 45 | 46 | it('returns the expected entity', () => expect(entity).toEqual({ 47 | ...AN_EMPTY_ENTITY, 48 | [EntityId]: 'empty', 49 | [EntityKeyName]: 'save-hash:empty' 50 | })) 51 | 52 | it('saves an empty Hash in Redis', async () => expect(fetchHashData(redis, 'save-hash:empty')).resolves.toEqual(AN_EMPTY_HASH)) 53 | 54 | it("stores no Hash at all", async () => expect(keyExists(redis, 'save-hash:empty')).resolves.toBe(false)) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /spec/functional/core/save-json.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { Entity, EntityKeyName, EntityId, RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createJsonEntitySchema } from '../helpers/data-helper' 6 | import { fetchJsonData, keyExists, removeKeys } from '../helpers/redis-helper' 7 | 8 | import { AN_EMPTY_ENTITY, AN_EMPTY_JSON, AN_ENTITY, A_JSON } from '../helpers/json-example-data' 9 | 10 | describe("save JSON", () => { 11 | 12 | let redis: RedisConnection 13 | let repository: Repository 14 | let schema: Schema 15 | let entity: Entity 16 | 17 | beforeAll(async () => { 18 | redis = createClient() 19 | await redis.connect() 20 | 21 | schema = createJsonEntitySchema('save-json') 22 | repository = new Repository(schema, redis) 23 | }) 24 | 25 | beforeEach(async () => await removeKeys(redis, 'save-json:1', 'save-json:empty')) 26 | afterAll(async () => { 27 | await removeKeys(redis, 'save-json:1', 'save-json:empty') 28 | await redis.quit() 29 | }) 30 | 31 | describe("when saving an entity to redis", () => { 32 | beforeEach(async () => { entity = await repository.save(AN_ENTITY) }) 33 | 34 | it('returns the expected entity', () => expect(entity).toEqual({ 35 | ...AN_ENTITY, 36 | [EntityId]: '1', 37 | [EntityKeyName]: 'save-json:1' 38 | })) 39 | 40 | it('saves the expected JSON in Redis', async () => expect(fetchJsonData(redis, 'save-json:1')).resolves.toEqual(A_JSON)) 41 | }) 42 | 43 | describe("when saving an empty entity to redis", () => { 44 | beforeEach(async () => { entity = await repository.save(AN_EMPTY_ENTITY) }) 45 | 46 | it('returns the expected entity', () => expect(entity).toEqual({ 47 | ...AN_EMPTY_ENTITY, 48 | [EntityId]: 'empty', 49 | [EntityKeyName]: 'save-json:empty' 50 | })) 51 | 52 | it('saves an empty JSON in Redis', async () => expect(fetchJsonData(redis, 'save-json:empty')).resolves.toEqual(AN_EMPTY_JSON)) 53 | it("stores an empty key", async () => expect(keyExists(redis, 'save-json:empty')).resolves.toBe(true)) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /spec/functional/core/update-hash.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { Entity, EntityId, EntityKeyName, RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createHashEntitySchema } from '../helpers/data-helper' 6 | import { keyExists, removeKeys, saveHash, fetchHashData } from '../helpers/redis-helper' 7 | 8 | import { ANOTHER_ENTITY, ANOTHER_HASH, AN_EMPTY_HASH, A_HASH } from '../helpers/hash-example-data' 9 | 10 | describe("update hash", () => { 11 | 12 | let redis: RedisConnection 13 | let repository: Repository 14 | let schema: Schema 15 | let entity: Entity 16 | let returnedEntity: Entity 17 | 18 | beforeAll(async () => { 19 | redis = createClient() 20 | await redis.connect() 21 | 22 | schema = createHashEntitySchema('update-hash') 23 | repository = new Repository(schema, redis) 24 | }) 25 | 26 | beforeEach(async () => { 27 | await removeKeys(redis, 'update-hash:1') 28 | await saveHash(redis, 'update-hash:1', A_HASH) 29 | }) 30 | 31 | afterAll(async () => { 32 | await removeKeys(redis, 'update-hash:1') 33 | await redis.quit() 34 | }) 35 | 36 | describe("when updating an Entity to Redis", () => { 37 | beforeEach(async () => { 38 | entity = await repository.fetch('1') 39 | entity.aString = ANOTHER_ENTITY.aString 40 | entity.anotherString = ANOTHER_ENTITY.anotherString 41 | entity.someText = ANOTHER_ENTITY.someText 42 | entity.someOtherText = ANOTHER_ENTITY.someOtherText 43 | entity.aNumber = ANOTHER_ENTITY.aNumber 44 | entity.anotherNumber = ANOTHER_ENTITY.anotherNumber 45 | entity.aBoolean = ANOTHER_ENTITY.aBoolean 46 | entity.anotherBoolean = ANOTHER_ENTITY.anotherBoolean 47 | entity.aPoint = ANOTHER_ENTITY.aPoint 48 | entity.anotherPoint = ANOTHER_ENTITY.anotherPoint 49 | entity.aDate = ANOTHER_ENTITY.aDate 50 | entity.anotherDate = ANOTHER_ENTITY.anotherDate 51 | entity.someStrings = ANOTHER_ENTITY.someStrings 52 | entity.someOtherStrings = ANOTHER_ENTITY.someOtherStrings 53 | returnedEntity = await repository.save(entity) 54 | }) 55 | 56 | it("returns the expected entity", () => expect(returnedEntity).toEqual({ 57 | ...ANOTHER_ENTITY, 58 | [EntityId]: '1', 59 | [EntityKeyName]: 'update-hash:1' 60 | })) 61 | 62 | it('saves the expected Hash in Redis', async () => expect(fetchHashData(redis, 'update-hash:1')).resolves.toEqual(ANOTHER_HASH)) 63 | }) 64 | 65 | describe("when updating an entity to be completely empty", () => { 66 | beforeEach(async () => { 67 | entity = await repository.fetch('1') 68 | entity.aString = null 69 | entity.anotherString = null 70 | entity.someText = null 71 | entity.someOtherText = null 72 | entity.aNumber = null 73 | entity.anotherNumber = undefined 74 | entity.aBoolean = undefined 75 | entity.anotherBoolean = undefined 76 | entity.aPoint = undefined 77 | entity.anotherPoint = undefined 78 | delete entity.aDate 79 | delete entity.anotherDate 80 | delete entity.someStrings 81 | delete entity.someOtherStrings 82 | returnedEntity = await repository.save(entity) 83 | }) 84 | 85 | it("returns the expected entity", () => expect(returnedEntity).toEqual({ 86 | aString: null, 87 | anotherString: null, 88 | someText: null, 89 | someOtherText: null, 90 | aNumber: null, 91 | anotherNumber: undefined, 92 | aBoolean: undefined, 93 | anotherBoolean: undefined, 94 | aPoint: undefined, 95 | anotherPoint: undefined, 96 | [EntityId]: '1', 97 | [EntityKeyName]: 'update-hash:1' 98 | })) 99 | 100 | it('saves an empty Hash in Redis', async () => expect(fetchHashData(redis, 'update-hash:1')).resolves.toEqual(AN_EMPTY_HASH)) 101 | it("removes the Hash from Redis", async () => expect(keyExists(redis, 'update-hash:1')).resolves.toBe(false)) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /spec/functional/core/update-json.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { Entity, EntityId, EntityKeyName, RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createJsonEntitySchema } from '../helpers/data-helper' 6 | import { fetchJsonData, keyExists, removeKeys, saveJson } from '../helpers/redis-helper' 7 | 8 | import { ANOTHER_ENTITY, ANOTHER_JSON, AN_EMPTY_JSON, A_JSON } from '../helpers/json-example-data' 9 | 10 | describe("update JSON", () => { 11 | 12 | let redis: RedisConnection 13 | let repository: Repository 14 | let schema: Schema 15 | let entity: Entity 16 | let returnedEntity: Entity 17 | 18 | beforeAll(async () => { 19 | redis = createClient() 20 | await redis.connect() 21 | 22 | schema = createJsonEntitySchema('update-json') 23 | repository = new Repository(schema, redis) 24 | }) 25 | 26 | beforeEach(async () => { 27 | await removeKeys(redis, 'update-json:1') 28 | await saveJson(redis, 'update-json:1', A_JSON) 29 | }) 30 | 31 | afterAll(async () => { 32 | await removeKeys(redis, 'update-json:1') 33 | await redis.quit() 34 | }) 35 | 36 | describe("when updating an Entity to Redis", () => { 37 | beforeEach(async () => { 38 | entity = await repository.fetch('1') 39 | entity.root = ANOTHER_ENTITY.root 40 | entity.anotherString = ANOTHER_ENTITY.anotherString 41 | entity.someOtherText = ANOTHER_ENTITY.someOtherText 42 | entity.anotherNumber = ANOTHER_ENTITY.anotherNumber 43 | entity.anotherBoolean = ANOTHER_ENTITY.anotherBoolean 44 | entity.anotherPoint = ANOTHER_ENTITY.anotherPoint 45 | entity.anotherDate = ANOTHER_ENTITY.anotherDate 46 | entity.someOtherStrings = ANOTHER_ENTITY.someOtherStrings 47 | entity.someOtherNumbers = ANOTHER_ENTITY.someOtherNumbers 48 | returnedEntity = await repository.save(entity) 49 | }) 50 | 51 | it("returns the expected entity", () => expect(returnedEntity).toEqual({ 52 | ...ANOTHER_ENTITY, 53 | [EntityId]: '1', 54 | [EntityKeyName]: 'update-json:1' 55 | })) 56 | 57 | it('create the expected JSON in Redis', async () => expect(fetchJsonData(redis, 'update-json:1')).resolves.toEqual(ANOTHER_JSON)) 58 | }) 59 | 60 | describe("when updating an empty entity to Redis", () => { 61 | beforeEach(async () => { 62 | entity = await repository.fetch('1') 63 | entity.root = undefined 64 | entity.anotherString = undefined 65 | entity.someOtherText = undefined 66 | entity.anotherNumber = undefined 67 | delete entity.anotherBoolean 68 | delete entity.anotherPoint 69 | delete entity.anotherDate 70 | delete entity.someOtherStrings 71 | delete entity.someOtherNumbers 72 | returnedEntity = await repository.save(entity) 73 | }) 74 | 75 | it("returns the expected entity", () => expect(returnedEntity).toEqual({ 76 | root: undefined, 77 | anotherString: undefined, 78 | someOtherText: undefined, 79 | anotherNumber: undefined, 80 | [EntityId]: '1', 81 | [EntityKeyName]: 'update-json:1' 82 | })) 83 | 84 | it("creates the expected JSON", async () => expect(fetchJsonData(redis, 'update-json:1')).resolves.toEqual(AN_EMPTY_JSON)) 85 | it("stores an empty key", async () => expect(keyExists(redis, 'update-json:1')).resolves.toBe(true)) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /spec/functional/helpers/data-helper.ts: -------------------------------------------------------------------------------- 1 | import { saveJson } from './redis-helper' 2 | 3 | import { EntityData, RedisConnection, Schema } from "$lib/index" 4 | 5 | export function createHashEntitySchema(prefix: string): Schema { 6 | return new Schema(prefix, { 7 | aString: { type: 'string', field: 'root_aString' }, 8 | someText: { type: 'text', field: 'root_someText', sortable: true }, 9 | aNumber: { type: 'number', field: 'root_aNumber', sortable: true }, 10 | aBoolean: { type: 'boolean', field: 'root_aBoolean' }, 11 | aPoint: { type: 'point', field: 'root_aPoint' }, 12 | aDate: { type: 'date', field: 'root_aDate', sortable: true }, 13 | someStrings: { type: 'string[]', field: 'root_someStrings', } 14 | }, { 15 | dataStructure: 'HASH' 16 | }) 17 | } 18 | 19 | export function createJsonEntitySchema(prefix: string): Schema { 20 | return new Schema(prefix, { 21 | aString: { type: 'string', path: '$.root.aString' }, 22 | someText: { type: 'text', path: '$.root.someText', sortable: true }, 23 | aNumber: { type: 'number', path: '$.root.aNumber', sortable: true }, 24 | aBoolean: { type: 'boolean', path: '$.root.aBoolean' }, 25 | aPoint: { type: 'point', path: '$.root.aPoint' }, 26 | aDate: { type: 'date', path: '$.root.aDate', sortable: true }, 27 | someStrings: { type: 'string[]', path: '$.root.someStrings[*]' }, 28 | someNumbers: { type: 'number[]', path: '$.root.someNumbers[*]' } 29 | }, { 30 | dataStructure: 'JSON' 31 | }) 32 | } 33 | 34 | export async function loadTestJson(redis: RedisConnection, key: string, data: EntityData) { 35 | 36 | const json: any = {} 37 | 38 | Object.keys(data).forEach(field => { 39 | const value = (data as any)[field] 40 | if (value !== null) { 41 | if (value instanceof Date) json[field] = value.getTime() / 1000 42 | else if (typeof value === 'object' && !Array.isArray(value)) json[field] = `${value.longitude},${value.latitude}` 43 | else json[field] = value 44 | } 45 | }) 46 | 47 | await saveJson(redis, key, json) 48 | } 49 | -------------------------------------------------------------------------------- /spec/functional/helpers/redis-helper.ts: -------------------------------------------------------------------------------- 1 | import { RedisConnection, RedisHashData, RedisJsonData } from "$lib/client" 2 | 3 | export async function sleep(ms: number) { 4 | return new Promise(resolve => setTimeout(resolve, ms)); 5 | } 6 | 7 | export async function removeKeys(redis: RedisConnection, ...keys: string[]) { 8 | for (const key of keys) await redis.del(key) 9 | } 10 | 11 | export async function saveHash(redis: RedisConnection, key: string, fieldsAndValues: RedisHashData) { 12 | await redis.hSet(key, fieldsAndValues) 13 | } 14 | 15 | export async function saveJson(redis: RedisConnection, key: string, json: RedisJsonData) { 16 | await redis.json.set(key, '$', json) 17 | } 18 | 19 | export async function keyExists(redis: RedisConnection, key: string): Promise { 20 | const exists = await redis.exists(key) 21 | return !!exists 22 | } 23 | 24 | export async function fetchJsonData(redis: RedisConnection, key: string): Promise { 25 | const results = await redis.json.get(key, { path: '$' }) 26 | return results === null ? null : (results as RedisJsonData)[0] 27 | } 28 | 29 | export async function fetchHashKeys(redis: RedisConnection, key: string): Promise { 30 | return await redis.hKeys(key) 31 | } 32 | 33 | export async function fetchHashFields(redis: RedisConnection, key: string, ...fields: string[]): Promise { 34 | return await redis.hmGet(key, fields) 35 | } 36 | 37 | export async function fetchHashData(redis: RedisConnection, key: string): Promise { 38 | return await redis.hGetAll(key) 39 | } 40 | 41 | export async function fetchIndexInfo(redis: RedisConnection, indexName: string): Promise { 42 | return await redis.ft.info(indexName) 43 | } 44 | 45 | export async function fetchIndexHash(redis: RedisConnection, indexHashName: string): Promise { 46 | return await redis.get(indexHashName) 47 | } 48 | -------------------------------------------------------------------------------- /spec/functional/search/drop-missing-index.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis' 2 | 3 | import { RedisConnection, Repository, Schema } from '$lib/index' 4 | 5 | import { createHashEntitySchema } from '../helpers/data-helper' 6 | import { fetchIndexInfo } from '../helpers/redis-helper' 7 | 8 | describe("drop missing index on hash", () => { 9 | 10 | let redis: RedisConnection 11 | let repository: Repository 12 | let schema: Schema 13 | 14 | beforeAll(async () => { 15 | redis = createClient() 16 | await redis.connect() 17 | 18 | schema = createHashEntitySchema('drop-missing') 19 | repository = new Repository(schema, redis) 20 | }) 21 | 22 | afterAll(async () => { 23 | await redis.quit() 24 | }) 25 | 26 | describe("when the index is dropped", () => { 27 | beforeEach(async () => { 28 | await repository.dropIndex() 29 | }) 30 | 31 | it("the index still doesn't exists", () => { 32 | expect(async () => await fetchIndexInfo(redis, 'drop-missing:index')) 33 | .rejects.toThrow("Unknown index name") 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /spec/helpers/custom-matchers.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare global { 4 | namespace jest { 5 | interface AsymmetricMatchers { 6 | toThrowErrorOfType(expectedType: any, expectedMessage: string): void 7 | } 8 | interface Matchers { 9 | toThrowErrorOfType(expectedType: any, expectedMessage: string): R 10 | } 11 | } 12 | } 13 | 14 | expect.extend({ 15 | toThrowErrorOfType(fnOrError, expectedType, expectedMessage) { 16 | 17 | let pass: boolean, actual: any 18 | 19 | if (fnOrError instanceof Function) { 20 | try { 21 | fnOrError() 22 | } catch (error: any) { 23 | actual = error 24 | } 25 | } else { 26 | actual = fnOrError 27 | } 28 | 29 | pass = actual.constructor.name === expectedType.name && actual.message === expectedMessage 30 | 31 | return { 32 | pass, 33 | message: () => 34 | `Expected${ pass ? ' not ' : ' ' }to throw ${expectedType.name} with message "${expectedMessage}".\n` + 35 | `Received: ${actual.constructor.name} with message "${actual.message}"` 36 | } 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /spec/helpers/example-data.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "$lib/entity" 2 | 3 | export const A_STRING = 'foo' 4 | export const ANOTHER_STRING = 'bar' 5 | export const A_THIRD_STRING = 'baz' 6 | 7 | export const SOME_TEXT = "The quick brown fox jumped over the lazy dog." 8 | export const SOME_OTHER_TEXT = "The five boxing wizards jump quickly." 9 | export const SOME_MORE_TEXT = "How vexingly quick daft zebras jump!" 10 | 11 | export const A_NUMBER = 42 12 | export const A_NUMBER_STRING = A_NUMBER.toString() 13 | 14 | export const ANOTHER_NUMBER = 23 15 | export const ANOTHER_NUMBER_STRING = ANOTHER_NUMBER.toString() 16 | 17 | export const A_THIRD_NUMBER = 13 18 | export const A_THIRD_NUMBER_STRING = A_THIRD_NUMBER.toString() 19 | 20 | export const A_DATE: Date = new Date('1997-07-04T16:56:55.456Z') 21 | export const A_DATE_ISO: string = A_DATE.toISOString() 22 | export const A_DATE_EPOCH: number = A_DATE.getTime() / 1000 23 | export const A_DATE_EPOCH_STRING: string = A_DATE_EPOCH.toString() 24 | 25 | export const ANOTHER_DATE: Date = new Date('1969-07-20T20:17:40.000Z') 26 | export const ANOTHER_DATE_ISO: string = ANOTHER_DATE.toISOString() 27 | export const ANOTHER_DATE_EPOCH: number = ANOTHER_DATE.getTime() / 1000 28 | export const ANOTHER_DATE_EPOCH_STRING: string = ANOTHER_DATE_EPOCH.toString() 29 | 30 | export const A_THIRD_DATE: Date = new Date('2015-07-14T11:49:57.000Z') 31 | export const A_THIRD_DATE_ISO: string = A_THIRD_DATE.toISOString() 32 | export const A_THIRD_DATE_EPOCH: number = A_THIRD_DATE.getTime() / 1000 33 | export const A_THIRD_DATE_EPOCH_STRING: string = A_THIRD_DATE_EPOCH.toString() 34 | 35 | export const A_POINT: Point = { longitude: 12.34, latitude: 56.78 } 36 | export const A_POINT_JSON: string = JSON.stringify(A_POINT) 37 | export const A_POINT_STRING: string = `${A_POINT.longitude},${A_POINT.latitude}` 38 | export const A_POINT_PRETTY_JSON: string = JSON.stringify(A_POINT, null, 1) 39 | 40 | export const ANOTHER_POINT: Point = { longitude: -23.45, latitude: 67.89 } 41 | export const ANOTHER_POINT_JSON: string = JSON.stringify(ANOTHER_POINT) 42 | export const ANOTHER_POINT_STRING: string = `${ANOTHER_POINT.longitude},${ANOTHER_POINT.latitude}` 43 | 44 | export const A_THIRD_POINT: Point = { longitude: -34.56, latitude: 78.90 } 45 | export const A_THIRD_POINT_JSON: string = JSON.stringify(A_THIRD_POINT) 46 | export const A_THIRD_POINT_STRING: string = `${A_THIRD_POINT.longitude},${A_THIRD_POINT.latitude}` 47 | 48 | export const AN_INVALID_POINT: Point = { longitude: 123.4, latitude: 85.05112879 } 49 | export const AN_INVALID_POINT_STRING: string = `${AN_INVALID_POINT.longitude},${AN_INVALID_POINT.latitude}` 50 | 51 | export const A_PARITAL_POINT = { latitude: 85.05112879 } 52 | 53 | export const SOME_STRINGS: Array = ['alfa', 'bravo', 'charlie'] 54 | export const SOME_STRINGS_JSON: string = JSON.stringify(SOME_STRINGS) 55 | export const SOME_STRINGS_JOINED: string = SOME_STRINGS.join('|') 56 | 57 | export const SOME_OTHER_STRINGS: Array = ['bravo', 'charlie', 'delta'] 58 | export const SOME_OTHER_STRINGS_JSON: string = JSON.stringify(SOME_OTHER_STRINGS) 59 | export const SOME_OTHER_STRINGS_JOINED: string = SOME_OTHER_STRINGS.join('|') 60 | 61 | export const SOME_MORE_STRINGS: Array = ['charlie', 'delta', 'echo'] 62 | export const SOME_MORE_STRINGS_JSON: string = JSON.stringify(SOME_MORE_STRINGS) 63 | export const SOME_MORE_STRINGS_JOINED: string = SOME_MORE_STRINGS.join('|') 64 | 65 | export const SOME_NUMBERS: Array = [42, 23, 13] 66 | export const SOME_OTHER_NUMBERS: Array = [23, 13, 72] 67 | export const SOME_MORE_NUMBERS: Array = [13, 72, 94] 68 | -------------------------------------------------------------------------------- /spec/unit/client/client-close.spec.ts: -------------------------------------------------------------------------------- 1 | import { redis } from '../helpers/mock-redis' 2 | 3 | import { Client } from '$lib/client' 4 | 5 | describe("Client", () => { 6 | 7 | let client: Client 8 | 9 | beforeEach(() => { client = new Client() }) 10 | 11 | describe("#close", () => { 12 | describe("when called on an open client", () => { 13 | beforeEach(async () => { 14 | await client.open() 15 | await client.close() 16 | }) 17 | 18 | it("closes the Redis connection", async () => expect(redis.quit).toHaveBeenCalled()) 19 | it("is no longer open", async () => expect(client.isOpen()).toBe(false)) 20 | }) 21 | 22 | describe("when called on an already closed client", () => { 23 | beforeEach(async () => { 24 | await client.open() 25 | await client.close() 26 | }) 27 | 28 | it("happily closes it anyways", async () => { 29 | await client.close() 30 | expect(client.isOpen()).toBe(false) 31 | }) 32 | }) 33 | 34 | it("happily closes an unopened client", async () => { 35 | await client.close() 36 | expect(client.isOpen()).toBe(false) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /spec/unit/client/client-create-index.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { RediSearchSchema, SchemaFieldTypes } from 'redis' 4 | 5 | import { redis } from '../helpers/mock-redis' 6 | import { Client, CreateOptions } from '$lib/client' 7 | import { RedisOmError } from '$lib/error' 8 | 9 | 10 | 11 | const schema: RediSearchSchema = { 12 | foo: { type: SchemaFieldTypes.TAG }, 13 | bar: { type: SchemaFieldTypes.TEXT } 14 | } 15 | 16 | const options: CreateOptions = { 17 | ON: 'HASH', 18 | PREFIX: 'prefix', 19 | STOPWORDS: ['bar', 'baz', 'qux'] 20 | } 21 | 22 | describe("Client", () => { 23 | 24 | let client: Client 25 | 26 | beforeEach(() => { client = new Client() }) 27 | 28 | describe("#createIndex", () => { 29 | describe("when called on an open client", () => { 30 | beforeEach(async () => { 31 | await client.open() 32 | }) 33 | 34 | it("passes a command to redis", async () => { 35 | await client.createIndex('index', schema, options) 36 | expect(redis.ft.create).toHaveBeenCalledWith('index', schema, options) 37 | }) 38 | }) 39 | 40 | describe("when called on a closed client", () => { 41 | beforeEach(async () => { 42 | await client.open() 43 | await client.close() 44 | }) 45 | 46 | it("errors when called on a closed client", () => 47 | expect(async () => await client.createIndex('index', schema, options)) 48 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 49 | }) 50 | 51 | it("errors when called on a new client", async () => 52 | expect(async () => await client.createIndex('index', schema, options)) 53 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /spec/unit/client/client-drop-index.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { ft } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | 9 | describe("Client", () => { 10 | 11 | let client: Client 12 | 13 | beforeEach(() => { client = new Client() }) 14 | 15 | describe("#dropIndex", () => { 16 | describe("when called on an open client", () => { 17 | beforeEach(async () => { 18 | await client.open() 19 | }) 20 | 21 | it("passes the command to redis", async () => { 22 | await client.dropIndex('index') 23 | expect(ft.dropIndex).toHaveBeenCalledWith('index') 24 | }) 25 | }) 26 | 27 | describe("when called on a closed client", () => { 28 | beforeEach(async () => { 29 | await client.open() 30 | await client.close() 31 | }) 32 | 33 | it("errors when called on a closed client", () => 34 | expect(async () => await client.dropIndex('index')) 35 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 36 | }) 37 | 38 | it("errors when called on a new client", async () => 39 | expect(async () => await client.dropIndex('index')) 40 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /spec/unit/client/client-expire.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { redis } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | describe("Client", () => { 9 | 10 | let client: Client 11 | 12 | beforeEach(() => { client = new Client() }) 13 | 14 | describe("#expire", () => { 15 | describe("when called on an open client", () => { 16 | beforeEach(async () => { 17 | await client.open() 18 | }) 19 | 20 | it("passes the command to redis", async () => { 21 | await client.expire('foo', 60) 22 | expect(redis.expire).toHaveBeenCalledWith('foo', 60) 23 | }) 24 | }) 25 | 26 | describe("when called on a closed client", () => { 27 | beforeEach(async () => { 28 | await client.open() 29 | await client.close() 30 | }) 31 | 32 | it("errors when called on a closed client", () => 33 | expect(async () => await client.expire('foo', 60)) 34 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 35 | }) 36 | 37 | it("errors when called on a new client", async () => 38 | expect(async () => await client.expire('foo', 60)) 39 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /spec/unit/client/client-expireAt.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { redis } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | const today = new Date(); 9 | const tomorrow = new Date(today); 10 | tomorrow.setDate(tomorrow.getDate() + 1); 11 | const yesterday = new Date(today); 12 | yesterday.setDate(yesterday.getDate() - 1); 13 | 14 | describe("Client", () => { 15 | 16 | let client: Client 17 | 18 | beforeEach(() => { client = new Client() }) 19 | 20 | describe("#expire", () => { 21 | describe("when called on an open client", () => { 22 | beforeEach(async () => { 23 | await client.open() 24 | }) 25 | 26 | it("passes the command to redis", async () => { 27 | await client.expireAt('foo', tomorrow) 28 | expect(redis.expireAt).toHaveBeenCalledWith('foo', tomorrow) 29 | }) 30 | }) 31 | 32 | describe("when called on a closed client", () => { 33 | beforeEach(async () => { 34 | await client.open() 35 | await client.close() 36 | }) 37 | 38 | it("errors when called on a closed client", () => 39 | expect(async () => await client.expireAt('foo', tomorrow)) 40 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 41 | }) 42 | 43 | it("errors when called on a new client", async () => 44 | expect(async () => await client.expireAt('foo', tomorrow)) 45 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /spec/unit/client/client-fetch-repository.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { Client } from '$lib/client' 4 | import { RedisOmError } from '$lib/error' 5 | import { Repository } from '$lib/repository' 6 | import { Schema } from '$lib/schema/schema' 7 | 8 | vi.mock('$lib/repository') 9 | 10 | describe("Client", () => { 11 | 12 | let client: Client 13 | 14 | beforeEach(() => { client = new Client() }) 15 | afterEach(() => { client.close() }) 16 | 17 | it("passes", () => expect(true).toBe(true)) 18 | 19 | describe("#fetchRepository", () => { 20 | 21 | let repository: Repository 22 | let schema: Schema 23 | 24 | describe("when fetching a Repository", () => { 25 | beforeAll(() => { 26 | schema = new Schema("TestEntity", {}) 27 | }) 28 | 29 | describe("when called on an open client", () => { 30 | 31 | beforeEach(async () => { 32 | await client.open() 33 | repository = client.fetchRepository(schema) 34 | }) 35 | 36 | it("creates a repository with the schema and client", () => { 37 | expect(Repository).toHaveBeenCalledWith(schema, client) 38 | }) 39 | 40 | it("returns a repository", async () => { 41 | expect(repository).toBeInstanceOf(Repository) 42 | }) 43 | }) 44 | 45 | describe("when called on a closed client", () => { 46 | beforeEach(async () => { 47 | await client.open() 48 | await client.close() 49 | }) 50 | 51 | it("errors when called on a closed client", () => 52 | expect(() => client.fetchRepository(schema)) 53 | .toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 54 | }) 55 | 56 | it("errors when called on a new client", () => 57 | expect(() => client.fetchRepository(schema)) 58 | .toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /spec/unit/client/client-get-set.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { redis } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | describe("Client", () => { 9 | 10 | let client: Client 11 | let result: string | null 12 | 13 | beforeEach(() => { client = new Client() }) 14 | 15 | describe("#get", () => { 16 | describe("when called on an open client", () => { 17 | beforeEach(async () => { 18 | await client.open() 19 | }) 20 | 21 | describe("and the result is a string", () => { 22 | beforeEach(async () => { 23 | redis.get.mockReturnValue('bar') 24 | result = await client.get('foo') 25 | }) 26 | 27 | it("passes the command to redis", async () => { 28 | expect(redis.get).toHaveBeenCalledWith('foo') 29 | }) 30 | 31 | it("returns the result", async () => expect(result).toBe('bar')) 32 | }) 33 | 34 | describe("and the result is null", () => { 35 | beforeEach(async () => { 36 | redis.get.mockResolvedValue(null) 37 | result = await client.get('foo') 38 | }) 39 | 40 | it("passes the command to redis", async () => { 41 | expect(redis.get).toHaveBeenCalledWith('foo') 42 | }) 43 | 44 | it("returns the result", async () => expect(result).toBeNull()) 45 | }) 46 | }) 47 | 48 | describe("when called on a closed client", () => { 49 | beforeEach(async () => { 50 | await client.open() 51 | await client.close() 52 | }) 53 | 54 | it("errors when called on a closed client", () => 55 | expect(async () => await client.get('foo')) 56 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 57 | }) 58 | 59 | it("errors when called on a new client", async () => 60 | expect(async () => await client.get('foo')) 61 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 62 | }) 63 | 64 | describe("#set", () => { 65 | describe("when called on an open client", () => { 66 | beforeEach(async () => { 67 | await client.open() 68 | await client.set('foo', 'bar') 69 | }) 70 | 71 | it("passes the command to redis", async () => { 72 | expect(redis.set).toHaveBeenCalledWith('foo', 'bar') 73 | }) 74 | }) 75 | 76 | describe("when called on a closed client", () => { 77 | beforeEach(async () => { 78 | await client.open() 79 | await client.close() 80 | }) 81 | 82 | it("errors when called on a closed client", () => 83 | expect(async () => await client.set('foo', 'bar')) 84 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 85 | }) 86 | 87 | it("errors when called on a new client", async () => 88 | expect(async () => await client.set('foo', 'bar')) 89 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 90 | }) 91 | 92 | }) 93 | -------------------------------------------------------------------------------- /spec/unit/client/client-hgetall.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { redis } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | describe("Client", () => { 9 | 10 | let client: Client 11 | let result: { [key: string]: string } 12 | 13 | beforeEach(() => { client = new Client() }) 14 | 15 | describe("#hgetall", () => { 16 | describe("when called on an open client", () => { 17 | beforeEach(async () => { 18 | await client.open() 19 | redis.hGetAll.mockResolvedValue({ foo: 'bar', baz: 'qux' }) 20 | result = await client.hgetall('foo') 21 | }) 22 | 23 | it("passes the command to redis", async () => { 24 | expect(redis.hGetAll).toHaveBeenCalledWith('foo') 25 | }) 26 | 27 | it("returns the value from redis", async () => { 28 | expect(result).toEqual({ foo: 'bar', baz: 'qux' }) 29 | }) 30 | }) 31 | 32 | describe("when called on a closed client", () => { 33 | beforeEach(async () => { 34 | await client.open() 35 | await client.close() 36 | }) 37 | 38 | it("errors when called on a closed client", () => 39 | expect(async () => await client.hgetall('foo')) 40 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 41 | }) 42 | 43 | it("errors when called on a new client", async () => 44 | expect(async () => await client.hgetall('foo')) 45 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /spec/unit/client/client-hsetall.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { redis, multi } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | describe("Client", () => { 9 | 10 | let client: Client 11 | 12 | beforeEach(() => { client = new Client() }) 13 | 14 | describe("#hsetall", () => { 15 | describe("when called on an open client", () => { 16 | beforeEach(async () => { 17 | await client.open() 18 | }) 19 | 20 | it("passes the command to redis", async () => { 21 | // it's a bit of an ugly mirror but will do 22 | await client.hsetall('foo', { foo: 'bar', baz: 'qux' }) 23 | expect(redis.multi).toHaveBeenCalled() 24 | expect(multi.unlink).toHaveBeenCalledWith('foo') 25 | expect(multi.hSet).toHaveBeenCalledWith('foo', { foo: 'bar', baz: 'qux' }) 26 | expect(multi.exec).toHaveBeenCalled() 27 | }) 28 | }) 29 | 30 | describe("when called on a closed client", () => { 31 | beforeEach(async () => { 32 | await client.open() 33 | await client.close() 34 | }) 35 | 36 | it("errors when called on a closed client", () => 37 | expect(async () => await client.hsetall('foo', { foo: 'bar', baz: 'qux' })) 38 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 39 | }) 40 | 41 | it("errors when called on a new client", async () => 42 | expect(async () => await client.hsetall('foo', { foo: 'bar', baz: 'qux' })) 43 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /spec/unit/client/client-jsonget.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { json } from '../helpers/mock-redis' 4 | 5 | import { Client, RedisJsonData } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | describe("Client", () => { 9 | 10 | let client: Client 11 | let result: RedisJsonData | null 12 | 13 | beforeEach(() => { client = new Client() }) 14 | 15 | describe("#jsonget", () => { 16 | describe("when called on an open client", () => { 17 | beforeEach(async () => { 18 | await client.open() 19 | json.get.mockResolvedValue([ { "foo": "bar", "bar": 42, "baz": true, "qux": null } ]) 20 | result = await client.jsonget('foo') 21 | }) 22 | 23 | it("passes the command to redis", async () => { 24 | expect(json.get).toHaveBeenCalledWith('foo', { path: '$' }) 25 | }) 26 | 27 | it("returns the JSON", async () => { 28 | expect(result).toEqual({ foo: 'bar', bar: 42, baz: true, qux: null }) 29 | }) 30 | }) 31 | 32 | describe("when called on a closed client", () => { 33 | beforeEach(async () => { 34 | await client.open() 35 | await client.close() 36 | }) 37 | 38 | it("errors when called on a closed client", () => 39 | expect(async () => await client.jsonget('foo')) 40 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 41 | }) 42 | 43 | it("errors when called on a new client", async () => 44 | expect(async () => await client.jsonget('foo')) 45 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /spec/unit/client/client-jsonset.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { json } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | describe("Client", () => { 9 | 10 | let client: Client 11 | 12 | beforeEach(() => { client = new Client() }) 13 | 14 | describe("#jsonset", () => { 15 | describe("when called on an open client", () => { 16 | beforeEach(async () => { 17 | await client.open() 18 | await client.jsonset('foo', { foo: 'bar', bar: 42, baz: true, qux: null }) 19 | }) 20 | 21 | it("passes the command to redis", async () => { 22 | expect(json.set).toHaveBeenCalledWith('foo', '$', { foo: 'bar', bar: 42, baz: true, qux: null } ) 23 | }) 24 | }) 25 | 26 | describe("when called on a closed client", () => { 27 | beforeEach(async () => { 28 | await client.open() 29 | await client.close() 30 | }) 31 | 32 | it("errors when called on a closed client", () => 33 | expect(async () => await client.jsonget('foo')) 34 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 35 | }) 36 | 37 | it("errors when called on a new client", async () => 38 | expect(async () => await client.jsonget('foo')) 39 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /spec/unit/client/client-open.spec.ts: -------------------------------------------------------------------------------- 1 | import { redis, createClient } from '../helpers/mock-redis' 2 | import { Client } from '$lib/client' 3 | 4 | describe("Client", () => { 5 | 6 | let client: Client, self: Client 7 | 8 | beforeEach(() => { client = new Client() }) 9 | 10 | describe("#open", () => { 11 | describe("when not called", () => { 12 | it("is not open", () => { 13 | expect(client.isOpen()).toBe(false) 14 | }) 15 | }) 16 | 17 | describe("when called without a url", () => { 18 | beforeEach(async () => {self = await client.open()}) 19 | 20 | it("creates a redis client with the default url", () => { 21 | expect(createClient).toHaveBeenCalledWith({ url: 'redis://localhost:6379' }) 22 | }) 23 | 24 | it("connects to redis", async () => { 25 | expect(redis.connect).toHaveBeenCalled() 26 | }) 27 | 28 | it("is open", () => { 29 | expect(client.isOpen()).toBe(true) 30 | }) 31 | 32 | it("returns itself", async () => { 33 | expect(self).toBe(client) 34 | }) 35 | 36 | describe("when trying to call it again", () => { 37 | beforeEach(async () => {self = await client.open()}) 38 | 39 | it("doesn't re-create a redis client", () => { 40 | expect(createClient).toBeCalledTimes(1) 41 | }) 42 | 43 | it("doesn't open redis again", async () => { 44 | expect(redis.connect).toBeCalledTimes(1) 45 | }) 46 | 47 | it("returns itself", async () => { 48 | expect(self).toBe(client) 49 | }) 50 | }) 51 | }) 52 | 53 | describe("when called with a url", () => { 54 | beforeEach(async () => {self = await client.open('foo')}) 55 | 56 | it("creates a new redis client with the provided url", () => { 57 | expect(createClient).toHaveBeenCalledWith({ url: 'foo' }) 58 | }) 59 | 60 | it("connects to redis", async () => { 61 | expect(redis.connect).toHaveBeenCalled() 62 | }) 63 | 64 | it("is open", () => { 65 | expect(client.isOpen()).toBe(true) 66 | }) 67 | 68 | it("returns itself", async () => { 69 | expect(self).toBe(client) 70 | }) 71 | 72 | describe("when trying to call it again", () => { 73 | beforeEach(async () => {self = await client.open('foo')}) 74 | 75 | it("doesn't re-create a redis client", () => { 76 | expect(createClient).toBeCalledTimes(1) 77 | }) 78 | 79 | it("doesn't open redis again", async () => { 80 | expect(redis.connect).toBeCalledTimes(1) 81 | }) 82 | 83 | it("returns itself", async () => { 84 | expect(self).toBe(client) 85 | }) 86 | }) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /spec/unit/client/client-search.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { redis } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | describe("Client", () => { 9 | 10 | let client: Client 11 | 12 | beforeEach(() => { client = new Client() }) 13 | 14 | describe("#search", () => { 15 | describe("when called on an open client", () => { 16 | beforeEach(async () => { 17 | await client.open() 18 | }) 19 | 20 | it("sends the expected command when given minimal options", async () => { 21 | await client.search('index', 'query') 22 | expect(redis.ft.search).toHaveBeenCalledWith('index', 'query') 23 | }) 24 | 25 | it("sends the expected command when given some options", async () => { 26 | await client.search('index', 'query', { LIMIT: { from: 0, size: 5 } }) 27 | expect(redis.ft.search).toHaveBeenCalledWith('index', 'query', { LIMIT: { from: 0, size: 5 } }) 28 | }) 29 | }) 30 | 31 | describe("when called on an unopened client", () => { 32 | it("throws an error", async () => expect(async () => await client.search('index', 'query')) 33 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 34 | }) 35 | 36 | describe("when called on a closed client", () => { 37 | beforeEach(async () => { 38 | await client.open() 39 | await client.close() 40 | }) 41 | 42 | it("throws an error", () => expect(async () => await client.search('index', 'query')) 43 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /spec/unit/client/client-unlink.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { redis } from '../helpers/mock-redis' 4 | 5 | import { Client } from '$lib/client' 6 | import { RedisOmError } from '$lib/error' 7 | 8 | describe("Client", () => { 9 | 10 | let client: Client 11 | 12 | beforeEach(() => { client = new Client() }) 13 | 14 | describe("#unlink", () => { 15 | describe("when called on an open client", () => { 16 | beforeEach(async () => { 17 | await client.open() 18 | }) 19 | 20 | it("doesn't call redis when passed no keys", async () => { 21 | await client.unlink() 22 | expect(redis.unlink).not.toHaveBeenCalled() 23 | }) 24 | 25 | it("passes a single key to redis", async () => { 26 | await client.unlink('foo') 27 | expect(redis.unlink).toHaveBeenCalledWith(expect.arrayContaining(['foo'])) 28 | }) 29 | 30 | it("passes multiple keys to redis", async () => { 31 | await client.unlink('foo', 'bar', 'baz') 32 | expect(redis.unlink).toHaveBeenCalledWith(expect.arrayContaining(['foo', 'bar', 'baz'])) 33 | }) 34 | }) 35 | 36 | describe("when called on a closed client", () => { 37 | beforeEach(async () => { 38 | await client.open() 39 | await client.close() 40 | }) 41 | 42 | it("errors when called on a closed client", () => 43 | expect(async () => await client.unlink('foo')) 44 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 45 | }) 46 | 47 | it("errors when called on a new client", async () => 48 | expect(async () => await client.unlink('foo')) 49 | .rejects.toThrowErrorOfType(RedisOmError, "Redis connection needs to be open.")) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /spec/unit/client/client-use.spec.ts: -------------------------------------------------------------------------------- 1 | import { redis, createClient } from '../helpers/mock-redis' 2 | import { Client } from '$lib/client' 3 | 4 | const BOGUS_CONNECTION = { THIS_IS_NOT: 'a real connection' } 5 | 6 | 7 | describe("Client", () => { 8 | 9 | let client: Client, self: Client 10 | 11 | beforeEach(() => { client = new Client() }) 12 | 13 | describe("#use", () => { 14 | describe("when not called", () => { 15 | it("is not open", () => { 16 | expect(client.isOpen()).toBe(false) 17 | }) 18 | }) 19 | 20 | describe("when called", () => { 21 | beforeEach(async () => { 22 | // @ts-ignore: no way to call createClient without actually connecting to Redis 23 | self = await client.use(BOGUS_CONNECTION) 24 | }) 25 | 26 | it("creates a redis client with the connection", () => { 27 | expect(createClient).not.toHaveBeenCalled() 28 | }) 29 | 30 | it("is open", () => { 31 | expect(client.isOpen()).toBe(true) 32 | }) 33 | 34 | it("returns itself", async () => { 35 | expect(self).toBe(client) 36 | }) 37 | }) 38 | 39 | describe("when called on an open connection", () => { 40 | beforeEach(async () => { 41 | await client.open() 42 | // @ts-ignore: no way to call createClient without actually connecting to Redis 43 | self = await client.use(BOGUS_CONNECTION) 44 | }) 45 | 46 | it("closes the existing redis connection", () => { 47 | expect(redis.quit).toHaveBeenCalled() 48 | }) 49 | 50 | it("doesn't create a new redis client", () => { 51 | expect(createClient).not.toHaveBeenCalledWith() 52 | }) 53 | 54 | it("is open", () => { 55 | expect(client.isOpen()).toBe(true) 56 | }) 57 | 58 | it("returns itself", async () => { 59 | expect(self).toBe(client) 60 | }) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /spec/unit/error/errors.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArrayHashInput, FieldNotInSchema, InvalidHashInput, InvalidHashValue, InvalidInput, InvalidJsonInput, InvalidJsonValue, InvalidSchema, InvalidValue, NestedHashInput, NullJsonInput, NullJsonValue, PointOutOfRange, RedisOmError, SearchError, SemanticSearchError } from '$lib/error' 2 | import { Field } from '$lib/schema' 3 | 4 | describe("Errors", () => { 5 | 6 | let error: any 7 | 8 | describe.each([ 9 | ["RedisOmError", RedisOmError], 10 | ["InvalidInput", InvalidInput], 11 | ["InvalidSchema", InvalidSchema], 12 | ["InvalidValue", InvalidValue], 13 | ["SearchError", SearchError], 14 | ["SemanticSearchError", SemanticSearchError] 15 | ])("%s", (_, errorClass) => { 16 | beforeEach(() => { error = new errorClass("An error has occured.") }) 17 | it("has the expected message", () => expect(error.message).toBe(`An error has occured.`)) 18 | }) 19 | 20 | describe.each([ 21 | ["NullJsonInput", NullJsonInput, `Null or undefined found in field 'aString' of type 'string' in JSON at '$["aString"]'.`], 22 | ["InvalidJsonInput", InvalidJsonInput, `Unexpected value for field 'aString' of type 'string' in JSON at '$["aString"]'.`], 23 | ["NullJsonValue", NullJsonValue, `Null or undefined found in field 'aString' of type 'string' from JSON path '$["aString"]' in Redis.`], 24 | ["InvalidJsonValue", InvalidJsonValue, `Unexpected value for field 'aString' of type 'string' from JSON path '$["aString"]' in Redis.`] 25 | ])("%s", (_, errorClass, expectedMessage) => { 26 | beforeEach(() => { error = new errorClass(new Field('aString', { type: 'string' })) }) 27 | it("has the expected message", () => expect(error.message).toBe(expectedMessage)) 28 | it("has the expected field name", () => expect(error.fieldName).toBe('aString')) 29 | it("has the expected field type", () => expect(error.fieldType).toBe('string')) 30 | it("has the expected JSON path", () => expect(error.jsonPath).toBe('$["aString"]')) 31 | }) 32 | 33 | describe.each([ 34 | ["NestedHashInput", NestedHashInput, `Unexpected object in Hash at property 'aString'. You can not store a nested object in a Redis Hash.`], 35 | ["ArrayHashInput", ArrayHashInput, `Unexpected array in Hash at property 'aString'. You can not store an array in a Redis Hash without defining it in the Schema.`], 36 | ["FieldNotInSchema", FieldNotInSchema, `The field 'aString' is not part of the schema and thus cannot be used to search.`] 37 | ])("%s", (_, errorClass, expectedMessage) => { 38 | beforeEach(() => { error = new errorClass('aString') }) 39 | it("has the expected message", () => expect(error.message).toBe(expectedMessage)) 40 | it("has the expected field", () => expect(error.field).toBe('aString')) 41 | }) 42 | 43 | describe("InvalidHashInput", () => { 44 | beforeEach(() => { error = new InvalidHashInput(new Field('aString', { type: 'string' })) }) 45 | it("has the expected message", () => expect(error.message).toBe(`Unexpected value for field 'aString' of type 'string' in Hash.`)) 46 | it("has the expected field name", () => expect(error.fieldName).toBe('aString')) 47 | it("has the expected field type", () => expect(error.fieldType).toBe('string')) 48 | }) 49 | 50 | describe("InvalidHashValue", () => { 51 | beforeEach(() => { error = new InvalidHashValue(new Field('aString', { type: 'string' })) }) 52 | it("has the expected message", () => expect(error.message).toBe(`Unexpected value for field 'aString' of type 'string' from Hash field 'aString' read from Redis.`)) 53 | it("has the expected field name", () => expect(error.fieldName).toBe('aString')) 54 | it("has the expected field type", () => expect(error.fieldType).toBe('string')) 55 | it("has the expected Hash field", () => expect(error.hashField).toBe('aString')) 56 | }) 57 | 58 | describe("PointOutOfRange", () => { 59 | beforeEach(() => { error = new PointOutOfRange({ longitude: 4242, latitude: 2323 }) }) 60 | it("has the expected message", () => expect(error.message).toBe(`Points must be between ±85.05112878 latitude and ±180 longitude.`)) 61 | it("has the expected point", () => expect(error.point).toEqual({ longitude: 4242, latitude: 2323 })) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /spec/unit/helpers/mock-client.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | 3 | import { Client } from '$lib/client' 4 | 5 | export const client = Object.create(Client.prototype) 6 | 7 | client.use = vi.fn() 8 | client.useNoClose = vi.fn() 9 | client.open = vi.fn() 10 | client.close = vi.fn() 11 | client.execute = vi.fn() 12 | client.fetchRepository = vi.fn() 13 | client.createIndex = vi.fn() 14 | client.dropIndex = vi.fn() 15 | client.search = vi.fn() 16 | client.unlink = vi.fn() 17 | client.expire = vi.fn() 18 | client.expireAt = vi.fn() 19 | client.get = vi.fn() 20 | client.set = vi.fn() 21 | client.hgetall = vi.fn() 22 | client.hsetall = vi.fn() 23 | client.jsonget = vi.fn() 24 | client.jsonset = vi.fn() 25 | 26 | vi.mock('$lib/client', () => ({ 27 | Client: vi.fn(() => client) 28 | })) 29 | -------------------------------------------------------------------------------- /spec/unit/helpers/mock-indexer.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | 3 | vi.mock('$lib/indexer', () => ({ 4 | buildRediSearchSchema: vi.fn() 5 | })) 6 | 7 | -------------------------------------------------------------------------------- /spec/unit/helpers/mock-redis.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | 3 | export const ft = { 4 | create: vi.fn(), 5 | search: vi.fn(), 6 | dropIndex: vi.fn() 7 | } 8 | 9 | export const json = { 10 | get: vi.fn(), 11 | set: vi.fn() 12 | } 13 | 14 | export const redis = { 15 | ft, 16 | json, 17 | connect: vi.fn(), 18 | quit: vi.fn(), 19 | get: vi.fn(), 20 | set: vi.fn(), 21 | hGetAll: vi.fn(), 22 | expire: vi.fn(), 23 | expireAt: vi.fn(), 24 | sendCommand: vi.fn(), 25 | unlink: vi.fn(), 26 | multi: vi.fn().mockImplementation(() => multi) 27 | } 28 | 29 | export const multi = { 30 | unlink: vi.fn().mockImplementation(() => multi), 31 | hSet: vi.fn().mockImplementation(() => multi), 32 | exec: vi.fn().mockImplementation(() => redis) 33 | } 34 | 35 | export const createClient = vi.fn(() => redis) 36 | 37 | vi.mock('redis', () => ({ createClient })) 38 | -------------------------------------------------------------------------------- /spec/unit/helpers/test-entity-and-schema.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '$lib/schema/schema'; 2 | 3 | export const simpleSchema = new Schema("SimpleEntity", { 4 | aString: { type: 'string' }, 5 | someText: { type: 'text' }, 6 | aNumber: { type: 'number' }, 7 | aBoolean: { type: 'boolean' }, 8 | aPoint: { type: 'point' }, 9 | aDate: { type: 'date' }, 10 | someStrings: { type: 'string[]' }, 11 | someNumbers: { type: 'number[]' } 12 | }); 13 | 14 | export const simpleHashSchema = new Schema("SimpleHashEntity", { 15 | aString: { type: 'string' }, 16 | someText: { type: 'text' }, 17 | aNumber: { type: 'number' }, 18 | aBoolean: { type: 'boolean' }, 19 | aPoint: { type: 'point' }, 20 | aDate: { type: 'date' }, 21 | someStrings: { type: 'string[]' } 22 | }, { 23 | dataStructure: 'HASH' 24 | }); 25 | 26 | export const simpleSortableHashSchema = new Schema("SimpleHashEntity", { 27 | aString: { type: 'string', sortable: true }, 28 | someText: { type: 'text', sortable: true }, 29 | aNumber: { type: 'number', sortable: true }, 30 | aBoolean: { type: 'boolean', sortable: true }, 31 | aPoint: { type: 'point' }, 32 | aDate: { type: 'date', sortable: true }, 33 | someStrings: { type: 'string[]' } 34 | }, { 35 | dataStructure: 'HASH' 36 | }); 37 | 38 | export const simpleJsonSchema = new Schema("SimpleJsonEntity", { 39 | aString: { type: 'string' }, 40 | someText: { type: 'text' }, 41 | aNumber: { type: 'number' }, 42 | aBoolean: { type: 'boolean' }, 43 | aPoint: { type: 'point' }, 44 | aDate: { type: 'date' }, 45 | someStrings: { type: 'string[]' }, 46 | someNumbers: { type: 'number[]' } 47 | }, { 48 | dataStructure: 'JSON' 49 | }); 50 | 51 | export const simpleSortableJsonSchema = new Schema("SimpleJsonEntity", { 52 | aString: { type: 'string', sortable: true }, 53 | someText: { type: 'text', sortable: true }, 54 | aNumber: { type: 'number', sortable: true }, 55 | aBoolean: { type: 'boolean', sortable: true }, 56 | aPoint: { type: 'point' }, 57 | aDate: { type: 'date', sortable: true }, 58 | someStrings: { type: 'string[]' }, 59 | someNumbers: { type: 'number[]' } 60 | }, { 61 | dataStructure: 'JSON' 62 | }); 63 | 64 | export const aliasedSchema = new Schema("AliasedEntity", { 65 | aString: { type: 'string', alias: 'anotherString' }, 66 | someText: { type: 'text', alias: 'someOtherText' }, 67 | aNumber: { type: 'number', alias: 'anotherNumber' }, 68 | aBoolean: { type: 'boolean', alias: 'anotherBoolean' }, 69 | aPoint: { type: 'point', alias: 'anotherPoint' }, 70 | aDate: { type: 'date', alias: 'anotherDate' }, 71 | someStrings: { type: 'string[]', alias: 'someOtherStrings' }, 72 | someNumbers: { type: 'number[]', alias: 'someOtherNumbers' } 73 | }); 74 | 75 | export const stopWordsOffSchema = new Schema("StopWordsOffEntity", { 76 | someText: { type: 'text' } 77 | }, { 78 | useStopWords: 'OFF' 79 | }); 80 | 81 | export const customStopWordsSchema = new Schema("CustomStopWordsEntity", { 82 | someText: { type: 'text' } 83 | }, { 84 | useStopWords: 'CUSTOM', 85 | stopWords: ['foo', 'bar', 'baz'] 86 | }); 87 | 88 | export const escapedFieldsSchema = new Schema("EscapedFieldsEntity", { 89 | "a,field": { type: 'string' }, 90 | "a.field": { type: 'string' }, 91 | "afield": { type: 'string' }, 93 | "a{field": { type: 'string' }, 94 | "a}field": { type: 'string' }, 95 | "a[field": { type: 'string' }, 96 | "a]field": { type: 'string' }, 97 | "a\"field": { type: 'string' }, 98 | "a'field": { type: 'string' }, 99 | "a:field": { type: 'string' }, 100 | "a;field": { type: 'string' }, 101 | "a!field": { type: 'string' }, 102 | "a@field": { type: 'string' }, 103 | "a#field": { type: 'string' }, 104 | "a$field": { type: 'string' }, 105 | "a%field": { type: 'string' }, 106 | "a^field": { type: 'string' }, 107 | "a&field": { type: 'string' }, 108 | "a(field": { type: 'string' }, 109 | "a)field": { type: 'string' }, 110 | "a-field": { type: 'string' }, 111 | "a+field": { type: 'string' }, 112 | "a=field": { type: 'string' }, 113 | "a~field": { type: 'string' }, 114 | "a|field": { type: 'string' }, 115 | "a/field": { type: 'string' }, 116 | "a\\field": { type: 'string' }, 117 | "a field": { type: 'string' } 118 | }); 119 | -------------------------------------------------------------------------------- /spec/unit/indexer/boolean-hash-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured boolean for a HASH", { 9 | schemaDef: { aField: { type: 'boolean' } } as SchemaDefinition, 10 | dataStructure: 'HASH', 11 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField' } } 12 | }], 13 | 14 | ["that defines an aliased boolean for a HASH", { 15 | schemaDef: { aField: { type: 'boolean', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'HASH', 17 | expectedRedisSchema: { anotherField: { type: 'TAG', AS: 'aField' } } 18 | }], 19 | 20 | ["that defines a fielded boolean for a HASH", { 21 | schemaDef: { aField: { type: 'boolean', field: 'anotherField' } } as SchemaDefinition, 22 | dataStructure: 'HASH', 23 | expectedRedisSchema: { anotherField: { type: 'TAG', AS: 'aField' } } 24 | }], 25 | 26 | ["that defines a sorted boolean for a HASH", { 27 | schemaDef: { aField: { type: 'boolean', sortable: true } } as SchemaDefinition, 28 | dataStructure: 'HASH', 29 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', SORTABLE: true } } 30 | }], 31 | 32 | ["that defines an unsorted boolean for a HASH", { 33 | schemaDef: { aField: { type: 'boolean', sortable: false } } as SchemaDefinition, 34 | dataStructure: 'HASH', 35 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField' } } 36 | }], 37 | 38 | ["that defines an indexed boolean for a HASH", { 39 | schemaDef: { aField: { type: 'boolean', indexed: true } } as SchemaDefinition, 40 | dataStructure: 'HASH', 41 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField' } } 42 | }], 43 | 44 | ["that defines an unindexed boolean for a HASH", { 45 | schemaDef: { aField: { type: 'boolean', indexed: false } } as SchemaDefinition, 46 | dataStructure: 'HASH', 47 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', NOINDEX: true } } 48 | }], 49 | 50 | ["that defines a fully configured boolean for a HASH", { 51 | schemaDef: { aField: { type: 'boolean', alias: 'ignoredField', field: 'anotherField', sortable: true, indexed: false } } as SchemaDefinition, 52 | dataStructure: 'HASH', 53 | expectedRedisSchema: { anotherField: { type: 'TAG', AS: 'aField', SORTABLE: true, NOINDEX: true } } 54 | }] 55 | 56 | ])("%s", (_, data) => { 57 | it("generates a Redis schema for the field", () => { 58 | let schemaDef = data.schemaDef 59 | let dataStructure = data.dataStructure as DataStructure 60 | let expectedRedisSchema = data.expectedRedisSchema 61 | 62 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 63 | let actual = buildRediSearchSchema(schema) 64 | expect(actual).toEqual(expectedRedisSchema) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /spec/unit/indexer/boolean-json-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { RediSearchSchema } from 'redis' 2 | 3 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 4 | import { buildRediSearchSchema } from '$lib/indexer' 5 | 6 | 7 | const warnSpy = vi.spyOn(global.console, 'warn').mockImplementation(() => {}) 8 | 9 | describe("#buildRediSearchSchema", () => { 10 | describe.each([ 11 | 12 | ["that defines an unconfigured boolean for a JSON", { 13 | schemaDef: { aField: { type: 'boolean' } } as SchemaDefinition, 14 | dataStructure: 'JSON', 15 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'TAG' } }, 16 | expectedWarning: null 17 | }], 18 | 19 | ["that defines an aliased boolean for a JSON", { 20 | schemaDef: { aField: { type: 'boolean', alias: 'anotherField' } } as SchemaDefinition, 21 | dataStructure: 'JSON', 22 | expectedRedisSchema: { '$["anotherField"]': { AS: 'aField', type: 'TAG' } }, 23 | expectedWarning: null 24 | }], 25 | 26 | ["that defines a pathed boolean for a JSON", { 27 | schemaDef: { aField: { type: 'boolean', path: '$.anotherField' } } as SchemaDefinition, 28 | dataStructure: 'JSON', 29 | expectedRedisSchema: { '$.anotherField': { AS: 'aField', type: 'TAG' } }, 30 | expectedWarning: null 31 | }], 32 | 33 | ["that defines a sorted boolean for a JSON", { 34 | schemaDef: { aField: { type: 'boolean', sortable: true } } as SchemaDefinition, 35 | dataStructure: 'JSON', 36 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'TAG' } }, 37 | expectedWarning: "You have marked a boolean field as sortable but RediSearch doesn't support the SORTABLE argument on a TAG for JSON. Ignored." 38 | }], 39 | 40 | ["that defines an unsorted boolean for a JSON", { 41 | schemaDef: { aField: { type: 'boolean', sortable: false } } as SchemaDefinition, 42 | dataStructure: 'JSON', 43 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'TAG' } }, 44 | expectedWarning: null 45 | }], 46 | 47 | ["that defines an indexed boolean for a JSON", { 48 | schemaDef: { aField: { type: 'boolean', indexed: true } } as SchemaDefinition, 49 | dataStructure: 'JSON', 50 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'TAG' } }, 51 | expectedWarning: null 52 | }], 53 | 54 | ["that defines an unidexed boolean for a JSON", { 55 | schemaDef: { aField: { type: 'boolean', indexed: false } } as SchemaDefinition, 56 | dataStructure: 'JSON', 57 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'TAG', NOINDEX: true } }, 58 | expectedWarning: null 59 | }], 60 | 61 | ["that defines a fully configured boolean for a JSON", { 62 | schemaDef: { aField: { type: 'boolean', alias: 'ignoredField', path: '$.anotherField', sortable: true, indexed: false } } as SchemaDefinition, 63 | dataStructure: 'JSON', 64 | expectedRedisSchema: { '$.anotherField': { AS: 'aField', type: 'TAG', NOINDEX: true } }, 65 | expectedWarning: "You have marked a boolean field as sortable but RediSearch doesn't support the SORTABLE argument on a TAG for JSON. Ignored." 66 | }] 67 | 68 | ])("%s", (_, data) => { 69 | 70 | let redisSchema: RediSearchSchema 71 | let schemaDef = data.schemaDef 72 | let dataStructure = data.dataStructure as DataStructure 73 | let expectedRedisSchema = data.expectedRedisSchema 74 | let expectedWarning = data.expectedWarning 75 | 76 | beforeEach(() => { 77 | warnSpy.mockReset() 78 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 79 | redisSchema = buildRediSearchSchema(schema) 80 | }) 81 | 82 | it("generates a Redis schema for the field", () => { 83 | expect(redisSchema).toEqual(expectedRedisSchema) 84 | }) 85 | 86 | if (expectedWarning) { 87 | it("generates the expected warning", () => { 88 | expect(warnSpy).toHaveBeenCalledWith(expectedWarning) 89 | }) 90 | } else { 91 | it("does not generate a warning", () => { 92 | expect(warnSpy).not.toHaveBeenCalled() 93 | }) 94 | } 95 | 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /spec/unit/indexer/date-hash-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured date for a HASH", { 9 | schemaDef: { aField: { type: 'date' } } as SchemaDefinition, 10 | dataStructure: 'HASH', 11 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField' } } 12 | }], 13 | 14 | ["that defines an aliased date for a HASH", { 15 | schemaDef: { aField: { type: 'date', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'HASH', 17 | expectedRedisSchema: { anotherField: { type: 'NUMERIC', AS: 'aField' } } 18 | }], 19 | 20 | ["that defines a fielded date for a HASH", { 21 | schemaDef: { aField: { type: 'date', field: 'anotherField' } } as SchemaDefinition, 22 | dataStructure: 'HASH', 23 | expectedRedisSchema: { anotherField: { type: 'NUMERIC', AS: 'aField' } } 24 | }], 25 | 26 | ["that defines a sorted date for a HASH", { 27 | schemaDef: { aField: { type: 'date', sortable: true } } as SchemaDefinition, 28 | dataStructure: 'HASH', 29 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField', SORTABLE: true } } 30 | }], 31 | 32 | ["that defines an unsorted date for a HASH", { 33 | schemaDef: { aField: { type: 'date', sortable: false } } as SchemaDefinition, 34 | dataStructure: 'HASH', 35 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField' } } 36 | }], 37 | 38 | ["that defines an indexed date for a HASH", { 39 | schemaDef: { aField: { type: 'date', indexed: true } } as SchemaDefinition, 40 | dataStructure: 'HASH', 41 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField' } } 42 | }], 43 | 44 | ["that defines an unindexed date for a HASH", { 45 | schemaDef: { aField: { type: 'date', indexed: false } } as SchemaDefinition, 46 | dataStructure: 'HASH', 47 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField', NOINDEX: true } } 48 | }], 49 | 50 | ["that defines a fully configured date for a HASH", { 51 | schemaDef: { aField: { type: 'date', alias: 'ignoredField', field: 'anotherField', sortable: true, indexed: false } } as SchemaDefinition, 52 | dataStructure: 'HASH', 53 | expectedRedisSchema: { anotherField: { type: 'NUMERIC', AS: 'aField', SORTABLE: true, NOINDEX: true } } 54 | }] 55 | 56 | ])("%s", (_, data) => { 57 | it("generates a Redis schema for the field", () => { 58 | let schemaDef = data.schemaDef 59 | let dataStructure = data.dataStructure as DataStructure 60 | let expectedRedisSchema = data.expectedRedisSchema 61 | 62 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 63 | let actual = buildRediSearchSchema(schema) 64 | expect(actual).toEqual(expectedRedisSchema) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /spec/unit/indexer/date-json-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured date for a JSON", { 9 | schemaDef: { aField: { type: 'date' } } as SchemaDefinition, 10 | dataStructure: 'JSON', 11 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'NUMERIC' } } 12 | }], 13 | 14 | ["that defines an aliased date for a JSON", { 15 | schemaDef: { aField: { type: 'date', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'JSON', 17 | expectedRedisSchema: { '$["anotherField"]': { AS: 'aField', type: 'NUMERIC' } } 18 | }], 19 | 20 | ["that defines an pathed date for a JSON", { 21 | schemaDef: { aField: { type: 'date', path: '$.anotherField' } } as SchemaDefinition, 22 | dataStructure: 'JSON', 23 | expectedRedisSchema: { '$.anotherField': { AS: 'aField', type: 'NUMERIC' } } 24 | }], 25 | 26 | ["that defines a sorted date for a JSON", { 27 | schemaDef: { aField: { type: 'date', sortable: true } } as SchemaDefinition, 28 | dataStructure: 'JSON', 29 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'NUMERIC', SORTABLE: true } } 30 | }], 31 | 32 | ["that defines an unsorted date for a JSON", { 33 | schemaDef: { aField: { type: 'date', sortable: false } } as SchemaDefinition, 34 | dataStructure: 'JSON', 35 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'NUMERIC' } } 36 | }], 37 | 38 | ["that defines an indexed date for a JSON", { 39 | schemaDef: { aField: { type: 'date', indexed: true } } as SchemaDefinition, 40 | dataStructure: 'JSON', 41 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'NUMERIC' } } 42 | }], 43 | 44 | ["that defines an indexed date for a JSON", { 45 | schemaDef: { aField: { type: 'date', indexed: false } } as SchemaDefinition, 46 | dataStructure: 'JSON', 47 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'NUMERIC', NOINDEX: true } } 48 | }], 49 | 50 | ["that defines a fully configured date for a JSON", { 51 | schemaDef: { aField: { type: 'date', alias: 'ignoredField', path: '$.anotherField', sortable: true, indexed: false } } as SchemaDefinition, 52 | dataStructure: 'JSON', 53 | expectedRedisSchema: { '$.anotherField': { AS: 'aField', type: 'NUMERIC', SORTABLE: true, NOINDEX: true } } 54 | }] 55 | 56 | ])("%s", (_, data) => { 57 | it("generates a Redis schema for the field", () => { 58 | let schemaDef = data.schemaDef 59 | let dataStructure = data.dataStructure as DataStructure 60 | let expectedRedisSchema = data.expectedRedisSchema 61 | 62 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 63 | let actual = buildRediSearchSchema(schema) 64 | expect(actual).toEqual(expectedRedisSchema) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /spec/unit/indexer/number-array-json-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured number[] for a JSON", { 9 | schemaDef: { aField: { type: 'number[]' } } as SchemaDefinition, 10 | dataStructure: 'JSON', 11 | expectedRedisSchema: { '$["aField"][*]' : { AS: 'aField', type: 'NUMERIC' } } 12 | }], 13 | 14 | ["that defines an aliased number[] for a JSON", { 15 | schemaDef: { aField: { type: 'number[]', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'JSON', 17 | expectedRedisSchema: { '$["anotherField"][*]' : { AS: 'aField', type: 'NUMERIC' } } 18 | }], 19 | 20 | ["that defines an pathed number[] for a JSON", { 21 | schemaDef: { aField: { type: 'number[]', path: '$.anotherField[*]' } } as SchemaDefinition, 22 | dataStructure: 'JSON', 23 | expectedRedisSchema: { '$.anotherField[*]' : { AS: 'aField', type: 'NUMERIC' } } 24 | }], 25 | 26 | ["that defines a sorted number[] for a JSON", { 27 | schemaDef: { aField: { type: 'number[]', sortable: true } } as SchemaDefinition, 28 | dataStructure: 'JSON', 29 | expectedRedisSchema: { '$["aField"][*]' : { AS: 'aField', type: 'NUMERIC', SORTABLE: true } } 30 | }], 31 | 32 | ["that defines an unsorted number[] for a JSON", { 33 | schemaDef: { aField: { type: 'number[]', sortable: false } } as SchemaDefinition, 34 | dataStructure: 'JSON', 35 | expectedRedisSchema: { '$["aField"][*]' : { AS: 'aField', type: 'NUMERIC' } } 36 | }], 37 | 38 | ["that defines an indexed number[] for a JSON", { 39 | schemaDef: { aField: { type: 'number[]', indexed: true } } as SchemaDefinition, 40 | dataStructure: 'JSON', 41 | expectedRedisSchema: { '$["aField"][*]' : { AS: 'aField', type: 'NUMERIC' } } 42 | }], 43 | 44 | ["that defines an unindexed number[] for a JSON", { 45 | schemaDef: { aField: { type: 'number[]', indexed: false } } as SchemaDefinition, 46 | dataStructure: 'JSON', 47 | expectedRedisSchema: { '$["aField"][*]' : { AS: 'aField', type: 'NUMERIC', NOINDEX: true } } 48 | }], 49 | 50 | ["that defines a fully-configured number[] for a JSON", { 51 | schemaDef: { aField: { type: 'number[]', alias: 'ignoredField', path: '$.anotherField[*]', sortable: true, indexed: false } } as SchemaDefinition, 52 | dataStructure: 'JSON', 53 | expectedRedisSchema: { '$.anotherField[*]': { AS: 'aField', type: 'NUMERIC', SORTABLE: true, NOINDEX: true } } 54 | }] 55 | 56 | ])("%s", (_, data) => { 57 | it("generates a Redis schema for the field", () => { 58 | let schemaDef = data.schemaDef 59 | let dataStructure = data.dataStructure as DataStructure 60 | let expectedRedisSchema = data.expectedRedisSchema 61 | 62 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 63 | let actual = buildRediSearchSchema(schema) 64 | expect(actual).toEqual(expectedRedisSchema) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /spec/unit/indexer/number-hash-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured number for a HASH", { 9 | schemaDef: { aField: { type: 'number' } } as SchemaDefinition, 10 | dataStructure: 'HASH', 11 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField' } } 12 | }], 13 | 14 | ["that defines an aliased number for a HASH", { 15 | schemaDef: { aField: { type: 'number', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'HASH', 17 | expectedRedisSchema: { anotherField: { type: 'NUMERIC', AS: 'aField' } } 18 | }], 19 | 20 | ["that defines a fielded number for a HASH", { 21 | schemaDef: { aField: { type: 'number', field: 'anotherField' } } as SchemaDefinition, 22 | dataStructure: 'HASH', 23 | expectedRedisSchema: { anotherField: { type: 'NUMERIC', AS: 'aField' } } 24 | }], 25 | 26 | ["that defines a sorted number for a HASH", { 27 | schemaDef: { aField: { type: 'number', sortable: true } } as SchemaDefinition, 28 | dataStructure: 'HASH', 29 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField', SORTABLE: true } } 30 | }], 31 | 32 | ["that defines an unsorted number for a HASH", { 33 | schemaDef: { aField: { type: 'number', sortable: false } } as SchemaDefinition, 34 | dataStructure: 'HASH', 35 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField' } } 36 | }], 37 | 38 | ["that defines an indexed number for a HASH", { 39 | schemaDef: { aField: { type: 'number', indexed: true } } as SchemaDefinition, 40 | dataStructure: 'HASH', 41 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField' } } 42 | }], 43 | 44 | ["that defines an unindexed number for a HASH", { 45 | schemaDef: { aField: { type: 'number', indexed: false } } as SchemaDefinition, 46 | dataStructure: 'HASH', 47 | expectedRedisSchema: { aField: { type: 'NUMERIC', AS: 'aField', NOINDEX: true } } 48 | }], 49 | 50 | ["that defines a fully-configured number for a HASH", { 51 | schemaDef: { aField: { type: 'number', alias: 'ignoredField', field: 'anotherField', sortable: true, indexed: false } } as SchemaDefinition, 52 | dataStructure: 'HASH', 53 | expectedRedisSchema: { anotherField: { type: 'NUMERIC', AS: 'aField', SORTABLE: true, NOINDEX: true } } 54 | }] 55 | 56 | ])("%s", (_, data) => { 57 | it("generates a Redis schema for the field", () => { 58 | let schemaDef = data.schemaDef 59 | let dataStructure = data.dataStructure as DataStructure 60 | let expectedRedisSchema = data.expectedRedisSchema 61 | 62 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 63 | let actual = buildRediSearchSchema(schema) 64 | expect(actual).toEqual(expectedRedisSchema) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /spec/unit/indexer/number-json-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured number for a JSON", { 9 | schemaDef: { aField: { type: 'number' } } as SchemaDefinition, 10 | dataStructure: 'JSON', 11 | expectedRedisSchema: { '$["aField"]' : { AS: 'aField', type: 'NUMERIC' } } 12 | }], 13 | 14 | ["that defines an aliased number for a JSON", { 15 | schemaDef: { aField: { type: 'number', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'JSON', 17 | expectedRedisSchema: { '$["anotherField"]' : { AS: 'aField', type: 'NUMERIC' } } 18 | }], 19 | 20 | ["that defines an pathed number for a JSON", { 21 | schemaDef: { aField: { type: 'number', path: '$.anotherField' } } as SchemaDefinition, 22 | dataStructure: 'JSON', 23 | expectedRedisSchema: { '$.anotherField' : { AS: 'aField', type: 'NUMERIC' } } 24 | }], 25 | 26 | ["that defines a sorted number for a JSON", { 27 | schemaDef: { aField: { type: 'number', sortable: true } } as SchemaDefinition, 28 | dataStructure: 'JSON', 29 | expectedRedisSchema: { '$["aField"]' : { AS: 'aField', type: 'NUMERIC', SORTABLE: true } } 30 | }], 31 | 32 | ["that defines an unsorted number for a JSON", { 33 | schemaDef: { aField: { type: 'number', sortable: false } } as SchemaDefinition, 34 | dataStructure: 'JSON', 35 | expectedRedisSchema: { '$["aField"]' : { AS: 'aField', type: 'NUMERIC' } } 36 | }], 37 | 38 | ["that defines an indexed number for a JSON", { 39 | schemaDef: { aField: { type: 'number', indexed: true } } as SchemaDefinition, 40 | dataStructure: 'JSON', 41 | expectedRedisSchema: { '$["aField"]' : { AS: 'aField', type: 'NUMERIC' } } 42 | }], 43 | 44 | ["that defines an unindexed number for a JSON", { 45 | schemaDef: { aField: { type: 'number', indexed: false } } as SchemaDefinition, 46 | dataStructure: 'JSON', 47 | expectedRedisSchema: { '$["aField"]' : { AS: 'aField', type: 'NUMERIC', NOINDEX: true } } 48 | }], 49 | 50 | ["that defines a fully-configured number for a JSON", { 51 | schemaDef: { aField: { type: 'number', alias: 'ignoredField', path: '$.anotherField', sortable: true, indexed: false } } as SchemaDefinition, 52 | dataStructure: 'JSON', 53 | expectedRedisSchema: { '$.anotherField': { AS: 'aField', type: 'NUMERIC', SORTABLE: true, NOINDEX: true } } 54 | }] 55 | 56 | ])("%s", (_, data) => { 57 | it("generates a Redis schema for the field", () => { 58 | let schemaDef = data.schemaDef 59 | let dataStructure = data.dataStructure as DataStructure 60 | let expectedRedisSchema = data.expectedRedisSchema 61 | 62 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 63 | let actual = buildRediSearchSchema(schema) 64 | expect(actual).toEqual(expectedRedisSchema) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /spec/unit/indexer/point-hash-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured point for a HASH", { 9 | schemaDef: { aField: { type: 'point' } } as SchemaDefinition, 10 | dataStructure: 'HASH', 11 | expectedRedisSchema: { aField: { type: 'GEO', AS: 'aField' } } 12 | }], 13 | 14 | ["that defines an aliased point for a HASH", { 15 | schemaDef: { aField: { type: 'point', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'HASH', 17 | expectedRedisSchema: { anotherField: { type: 'GEO', AS: 'aField' } } 18 | }], 19 | 20 | ["that defines a fielded point for a HASH", { 21 | schemaDef: { aField: { type: 'point', field: 'anotherField' } } as SchemaDefinition, 22 | dataStructure: 'HASH', 23 | expectedRedisSchema: { anotherField: { type: 'GEO', AS: 'aField' } } 24 | }], 25 | 26 | ["that defines an indexed point for a HASH", { 27 | schemaDef: { aField: { type: 'point', indexed: true } } as SchemaDefinition, 28 | dataStructure: 'HASH', 29 | expectedRedisSchema: { aField: { type: 'GEO', AS: 'aField' } } 30 | }], 31 | 32 | ["that defines an unindexed point for a HASH", { 33 | schemaDef: { aField: { type: 'point', indexed: false } } as SchemaDefinition, 34 | dataStructure: 'HASH', 35 | expectedRedisSchema: { aField: { type: 'GEO', AS: 'aField', NOINDEX: true } } 36 | }], 37 | 38 | ["that defines a fully-configured point for a HASH", { 39 | schemaDef: { aField: { type: 'point', alias: 'ignoredField', field: 'anotherField', indexed: false } } as SchemaDefinition, 40 | dataStructure: 'HASH', 41 | expectedRedisSchema: { anotherField: { type: 'GEO', AS: 'aField', NOINDEX: true } } 42 | }] 43 | 44 | ])("%s", (_, data) => { 45 | it("generates a Redis schema for the field", () => { 46 | let schemaDef = data.schemaDef 47 | let dataStructure = data.dataStructure as DataStructure 48 | let expectedRedisSchema = data.expectedRedisSchema 49 | 50 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 51 | let actual = buildRediSearchSchema(schema) 52 | expect(actual).toEqual(expectedRedisSchema) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /spec/unit/indexer/point-json-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured point for a JSON", { 9 | schemaDef: { aField: { type: 'point' } } as SchemaDefinition, 10 | dataStructure: 'JSON', 11 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'GEO' } } 12 | }], 13 | 14 | ["that defines an aliased point for a JSON", { 15 | schemaDef: { aField: { type: 'point', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'JSON', 17 | expectedRedisSchema: { '$["anotherField"]': { AS: 'aField', type: 'GEO' } } 18 | }], 19 | 20 | ["that defines a pathed point for a JSON", { 21 | schemaDef: { aField: { type: 'point', path: '$.anotherField' } } as SchemaDefinition, 22 | dataStructure: 'JSON', 23 | expectedRedisSchema: { '$.anotherField': { AS: 'aField', type: 'GEO' } } 24 | }], 25 | 26 | ["that defines an indexed point for a JSON", { 27 | schemaDef: { aField: { type: 'point', indexed: true } } as SchemaDefinition, 28 | dataStructure: 'JSON', 29 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'GEO' } } 30 | }], 31 | 32 | ["that defines an unindexed point for a JSON", { 33 | schemaDef: { aField: { type: 'point', indexed: false } } as SchemaDefinition, 34 | dataStructure: 'JSON', 35 | expectedRedisSchema: { '$["aField"]': { AS: 'aField', type: 'GEO', NOINDEX: true } } 36 | }], 37 | 38 | ["that defines a fully-configured point for a JSON", { 39 | schemaDef: { aField: { type: 'point', alias: 'ignoredField', path: '$.anotherField', indexed: false } } as SchemaDefinition, 40 | dataStructure: 'JSON', 41 | expectedRedisSchema: { '$.anotherField': { AS: 'aField', type: 'GEO', NOINDEX: true } } 42 | }] 43 | 44 | 45 | ])("%s", (_, data) => { 46 | it("generates a Redis schema for the field", () => { 47 | let schemaDef = data.schemaDef 48 | let dataStructure = data.dataStructure as DataStructure 49 | let expectedRedisSchema = data.expectedRedisSchema 50 | 51 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 52 | let actual = buildRediSearchSchema(schema) 53 | expect(actual).toEqual(expectedRedisSchema) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /spec/unit/indexer/string-array-hash-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured array for a HASH", { 9 | schemaDef: { aField: { type: 'string[]' } } as SchemaDefinition, 10 | dataStructure: 'HASH', 11 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TAG', SEPARATOR: '|' } } 12 | }], 13 | 14 | ["that defines an aliased array for a HASH", { 15 | schemaDef: { aField: { type: 'string[]', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'HASH', 17 | expectedRedisSchema: { anotherField: { AS: 'aField', type: 'TAG', SEPARATOR: '|' } } 18 | }], 19 | 20 | ["that defines a fielded array for a HASH", { 21 | schemaDef: { aField: { type: 'string[]', alias: 'anotherField' } } as SchemaDefinition, 22 | dataStructure: 'HASH', 23 | expectedRedisSchema: { anotherField: { AS: 'aField', type: 'TAG', SEPARATOR: '|' } } 24 | }], 25 | 26 | ["that defines a separated array for a HASH", { 27 | schemaDef: { aField: { type: 'string[]', separator: ',' } } as SchemaDefinition, 28 | dataStructure: 'HASH', 29 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TAG', SEPARATOR: ',' } } 30 | }], 31 | 32 | ["that defines an indexed array for a HASH", { 33 | schemaDef: { aField: { type: 'string[]', indexed: true } } as SchemaDefinition, 34 | dataStructure: 'HASH', 35 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TAG', SEPARATOR: '|' } } 36 | }], 37 | 38 | ["that defines an unindexed array for a HASH", { 39 | schemaDef: { aField: { type: 'string[]', indexed: false } } as SchemaDefinition, 40 | dataStructure: 'HASH', 41 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TAG', SEPARATOR: '|', NOINDEX: true } } 42 | }], 43 | 44 | ["that defines a fully-configured array for a HASH", { 45 | schemaDef: { aField: { type: 'string[]', alias: 'ignoredField', field: 'anotherField', separator: ',', indexed: false } } as SchemaDefinition, 46 | dataStructure: 'HASH', 47 | expectedRedisSchema: { anotherField: { AS: 'aField', type: 'TAG', SEPARATOR: ',', NOINDEX: true } } 48 | }] 49 | 50 | ])("%s", (_, data) => { 51 | it("generates a Redis schema for the field", () => { 52 | let schemaDef = data.schemaDef 53 | let dataStructure = data.dataStructure as DataStructure 54 | let expectedRedisSchema = data.expectedRedisSchema 55 | 56 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 57 | let actual = buildRediSearchSchema(schema) 58 | expect(actual).toEqual(expectedRedisSchema) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /spec/unit/indexer/string-array-json-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured array for a JSON", { 9 | schemaDef: { aField: { type: 'string[]' } } as SchemaDefinition, 10 | dataStructure: 'JSON', 11 | expectedRedisSchema: { '$["aField"][*]': { AS: 'aField', type: 'TAG', SEPARATOR: '|' } } 12 | }], 13 | 14 | ["that defines an aliased array for a JSON", { 15 | schemaDef: { aField: { type: 'string[]', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'JSON', 17 | expectedRedisSchema: { '$["anotherField"][*]': { AS: 'aField', type: 'TAG', SEPARATOR: '|' } } 18 | }], 19 | 20 | ["that defines a pathed array for a JSON", { 21 | schemaDef: { aField: { type: 'string[]', path: '$.anotherField[*]' } } as SchemaDefinition, 22 | dataStructure: 'JSON', 23 | expectedRedisSchema: { '$.anotherField[*]': { AS: 'aField', type: 'TAG', SEPARATOR: '|' } } 24 | }], 25 | 26 | ["that defines an indexed array for a JSON", { 27 | schemaDef: { aField: { type: 'string[]', indexed: true } } as SchemaDefinition, 28 | dataStructure: 'JSON', 29 | expectedRedisSchema: { '$["aField"][*]': { AS: 'aField', type: 'TAG', SEPARATOR: '|' } } 30 | }], 31 | 32 | ["that defines an unindexed array for a JSON", { 33 | schemaDef: { aField: { type: 'string[]', indexed: false } } as SchemaDefinition, 34 | dataStructure: 'JSON', 35 | expectedRedisSchema: { '$["aField"][*]': { AS: 'aField', type: 'TAG', SEPARATOR: '|', NOINDEX: true } } 36 | }], 37 | 38 | ["that defines a fully-configured array for a JSON", { 39 | schemaDef: { aField: { type: 'string[]', alias: 'ignoredField', path: '$.anotherField[*]', indexed: false } } as SchemaDefinition, 40 | dataStructure: 'JSON', 41 | expectedRedisSchema: { '$.anotherField[*]': { AS: 'aField', type: 'TAG', SEPARATOR: '|', NOINDEX: true } } 42 | }] 43 | 44 | ])("%s", (_, data) => { 45 | it("generates a Redis schema for the field", () => { 46 | let schemaDef = data.schemaDef 47 | let dataStructure = data.dataStructure as DataStructure 48 | let expectedRedisSchema = data.expectedRedisSchema 49 | 50 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 51 | let actual = buildRediSearchSchema(schema) 52 | expect(actual).toEqual(expectedRedisSchema) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /spec/unit/indexer/string-hash-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured string for a HASH", { 9 | schemaDef: { aField: { type: 'string' } } as SchemaDefinition, 10 | dataStructure: 'HASH', 11 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', SEPARATOR: '|' } } 12 | }], 13 | 14 | ["that defines an aliased string for a HASH", { 15 | schemaDef: { aField: { type: 'string', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'HASH', 17 | expectedRedisSchema: { anotherField: { type: 'TAG', AS: 'aField', SEPARATOR: '|' } } 18 | }], 19 | 20 | ["that defines a fielded string for a HASH", { 21 | schemaDef: { aField: { type: 'string', field: 'anotherField' } } as SchemaDefinition, 22 | dataStructure: 'HASH', 23 | expectedRedisSchema: { anotherField: { type: 'TAG', AS: 'aField', SEPARATOR: '|' } } 24 | }], 25 | 26 | ["that defines an unsorted string for a HASH", { 27 | schemaDef: { aField: { type: 'string', sortable: false } } as SchemaDefinition, 28 | dataStructure: 'HASH', 29 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', SEPARATOR: '|' } } 30 | }], 31 | 32 | ["that defines a sorted string for a HASH", { 33 | schemaDef: { aField: { type: 'string', sortable: true } } as SchemaDefinition, 34 | dataStructure: 'HASH', 35 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', SEPARATOR: '|', SORTABLE: true } } 36 | }], 37 | 38 | ["that defines a separated string for a HASH", { 39 | schemaDef: { aField: { type: 'string', separator: ',' } } as SchemaDefinition, 40 | dataStructure: 'HASH', 41 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', SEPARATOR: ',' } } 42 | }], 43 | 44 | ["that defines an indexed string for a HASH", { 45 | schemaDef: { aField: { type: 'string', indexed: true } } as SchemaDefinition, 46 | dataStructure: 'HASH', 47 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', SEPARATOR: '|' } } 48 | }], 49 | 50 | ["that defines an unindexed string for a HASH", { 51 | schemaDef: { aField: { type: 'string', indexed: false } } as SchemaDefinition, 52 | dataStructure: 'HASH', 53 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', SEPARATOR: '|', NOINDEX: true } } 54 | }], 55 | 56 | ["that defines a caseSensitive string for a HASH", { 57 | schemaDef: { aField: { type: 'string', caseSensitive: true } } as SchemaDefinition, 58 | dataStructure: 'HASH', 59 | expectedRedisSchema: { aField: { type: 'TAG', AS: 'aField', SEPARATOR: '|', CASESENSITIVE: true } } 60 | }], 61 | 62 | ["that defines a fully configured string for a HASH", { 63 | schemaDef: { aField: { type: 'string', alias: 'ignoredField', field: 'anotherField', sortable: true, separator: ',', indexed: false, caseSensitive: true } } as SchemaDefinition, 64 | dataStructure: 'HASH', 65 | expectedRedisSchema: { anotherField: { type: 'TAG', AS: 'aField', SEPARATOR: ',', SORTABLE: true, NOINDEX: true, CASESENSITIVE: true } } 66 | }] 67 | 68 | ])("%s", (_, data) => { 69 | it("generates a Redis schema for the field", () => { 70 | let schemaDef = data.schemaDef 71 | let dataStructure = data.dataStructure as DataStructure 72 | let expectedRedisSchema = data.expectedRedisSchema 73 | 74 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 75 | let actual = buildRediSearchSchema(schema) 76 | expect(actual).toEqual(expectedRedisSchema) 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /spec/unit/indexer/text-hash-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, DataStructure } from '$lib/schema' 2 | import { buildRediSearchSchema } from '$lib/indexer' 3 | 4 | 5 | describe("#buildRediSearchSchema", () => { 6 | describe.each([ 7 | 8 | ["that defines an unconfigured text for a HASH", { 9 | schemaDef: { aField: { type: 'text' } } as SchemaDefinition, 10 | dataStructure: 'HASH', 11 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT' } } 12 | }], 13 | 14 | ["that defines an aliased text for a HASH", { 15 | schemaDef: { aField: { type: 'text', alias: 'anotherField' } } as SchemaDefinition, 16 | dataStructure: 'HASH', 17 | expectedRedisSchema: { anotherField: { AS: 'aField', type: 'TEXT' } } 18 | }], 19 | 20 | ["that defines a fielded text for a HASH", { 21 | schemaDef: { aField: { type: 'text', field: 'anotherField' } } as SchemaDefinition, 22 | dataStructure: 'HASH', 23 | expectedRedisSchema: { anotherField: { AS: 'aField', type: 'TEXT' } } 24 | }], 25 | 26 | ["that defines a sorted text for a HASH", { 27 | schemaDef: { aField: { type: 'text', sortable: true } } as SchemaDefinition, 28 | dataStructure: 'HASH', 29 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT', SORTABLE: true } } 30 | }], 31 | 32 | ["that defines an unsorted text for a HASH", { 33 | schemaDef: { aField: { type: 'text', sortable: false } } as SchemaDefinition, 34 | dataStructure: 'HASH', 35 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT' } } 36 | }], 37 | 38 | ["that defines an indexed text for a HASH", { 39 | schemaDef: { aField: { type: 'text', indexed: true } } as SchemaDefinition, 40 | dataStructure: 'HASH', 41 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT' } } 42 | }], 43 | 44 | ["that defines an unindexed text for a HASH", { 45 | schemaDef: { aField: { type: 'text', indexed: false } } as SchemaDefinition, 46 | dataStructure: 'HASH', 47 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT', NOINDEX: true } } 48 | }], 49 | 50 | ["that defines a phonetic matcher text for a HASH", { 51 | schemaDef: { aField: { type: 'text', matcher: 'dm:en' } } as SchemaDefinition, 52 | dataStructure: 'HASH', 53 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT', PHONETIC: 'dm:en' } } 54 | }], 55 | 56 | ["that defines an unstemmed text for a HASH", { 57 | schemaDef: { aField: { type: 'text', stemming: false } } as SchemaDefinition, 58 | dataStructure: 'HASH', 59 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT', NOSTEM: true } } 60 | }], 61 | 62 | ["that defines an unnormalized text for a HASH", { 63 | schemaDef: { aField: { type: 'text', normalized: false } } as SchemaDefinition, 64 | dataStructure: 'HASH', 65 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT', SORTABLE: 'UNF' } } 66 | }], 67 | 68 | ["that defines a weighted text for a HASH", { 69 | schemaDef: { aField: { type: 'text', weight: 2 } } as SchemaDefinition, 70 | dataStructure: 'HASH', 71 | expectedRedisSchema: { aField: { AS: 'aField', type: 'TEXT', WEIGHT: 2 } } 72 | }], 73 | 74 | ["that defines a fully configured text for a HASH", { 75 | schemaDef: { aField: { type: 'text', alias: 'ignoredField', field: 'anotherField', sortable: true, indexed: false, matcher: 'dm:en', stemming: false, normalized: false, weight: 2 } } as SchemaDefinition, 76 | dataStructure: 'HASH', 77 | expectedRedisSchema: { anotherField: { AS: 'aField', type: 'TEXT', SORTABLE: 'UNF', NOINDEX: true, PHONETIC: 'dm:en', NOSTEM: true, WEIGHT: 2 } } 78 | }] 79 | 80 | ])("%s", (_, data) => { 81 | it("generates a Redis schema for the field", () => { 82 | let schemaDef = data.schemaDef 83 | let dataStructure = data.dataStructure as DataStructure 84 | let expectedRedisSchema = data.expectedRedisSchema 85 | 86 | let schema = new Schema('TestEntity', schemaDef, { dataStructure }) 87 | let actual = buildRediSearchSchema(schema) 88 | expect(actual).toEqual(expectedRedisSchema) 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /spec/unit/repository/repository-constructor.spec.ts: -------------------------------------------------------------------------------- 1 | import '../helpers/mock-client' 2 | import '../helpers/mock-redis' 3 | 4 | import { createClient } from 'redis' 5 | 6 | import { Client, RedisConnection } from '$lib/client' 7 | import { Repository } from '$lib/repository' 8 | 9 | import { simpleSchema } from '../helpers/test-entity-and-schema' 10 | 11 | 12 | describe("Repository", () => { 13 | 14 | let client: Client 15 | let connection: RedisConnection 16 | let repository: Repository 17 | 18 | describe("when created with a Client", () => { 19 | beforeEach(() => { 20 | client = new Client() 21 | repository = new Repository(simpleSchema, client) 22 | }) 23 | 24 | it("uses the client it is given", () => { 25 | // @ts-ignore 26 | expect(repository.client).toBe(client) 27 | }) 28 | }) 29 | 30 | describe("when created with a RedisConnection", () => { 31 | beforeEach(() => { 32 | connection = createClient() 33 | repository = new Repository(simpleSchema, connection) 34 | }) 35 | 36 | it("creates a new client", () => { 37 | expect(Client).toHaveBeenCalled() 38 | }) 39 | 40 | it("uses the RedisConnection", () => { 41 | // @ts-ignore 42 | expect(repository.client.useNoClose).toHaveBeenCalledWith(connection) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /spec/unit/repository/repository-drop-index.spec.ts: -------------------------------------------------------------------------------- 1 | import '../helpers/mock-client' 2 | import '../../helpers/custom-matchers' 3 | 4 | import { Client } from '$lib/client' 5 | import { Repository } from '$lib/repository' 6 | import { Schema } from '$lib/schema' 7 | 8 | 9 | const simpleSchema = new Schema("SimpleEntity", {}, { dataStructure: 'HASH' }) 10 | 11 | describe("Repository", () => { 12 | 13 | let client: Client 14 | let repository: Repository 15 | 16 | describe("#dropIndex", () => { 17 | 18 | beforeAll(() => { client = new Client() }) 19 | beforeEach(() => { repository = new Repository(simpleSchema, client) }) 20 | 21 | describe("when the index exists", () => { 22 | beforeEach(async () => await repository.dropIndex()) 23 | 24 | it("asks the client to drop the index", async () => 25 | expect(client.dropIndex).toHaveBeenCalledWith(simpleSchema.indexName)) 26 | 27 | it("asks the client to remove the index hash", async () => 28 | expect(client.unlink).toHaveBeenCalledWith(simpleSchema.indexHashName)) 29 | }) 30 | 31 | describe("when the index doesn't exist", () => { 32 | beforeEach(async () => { 33 | vi.mocked(client.dropIndex).mockRejectedValue(Error("Unknown index name")) 34 | }) 35 | 36 | it("eats the exception", async () => await repository.dropIndex()) // it doesn't throw an exception 37 | }) 38 | 39 | describe("when the index doesn't exist for newer versions of Redis", () => { 40 | beforeEach(async () => { 41 | vi.mocked(client.dropIndex).mockRejectedValue(Error("Unknown index name")) 42 | }) 43 | 44 | it("eats the exception", async () => await repository.dropIndex()) // it doesn't throw an exception 45 | }) 46 | 47 | describe("when dropping the index throws some other Redis exception", () => { 48 | beforeEach(async () => { 49 | vi.mocked(client.dropIndex).mockRejectedValue(Error("Some other error")) 50 | }) 51 | 52 | it("propogates the exception", async () => 53 | expect(async () => await repository.dropIndex()).rejects.toThrowErrorOfType(Error, "Some other error")) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /spec/unit/repository/repository-expire.spec.ts: -------------------------------------------------------------------------------- 1 | import '../helpers/mock-client' 2 | 3 | import { Client } from '$lib/client' 4 | import { Repository } from '$lib/repository' 5 | import { Schema } from '$lib/schema' 6 | 7 | const simpleSchema = new Schema("SimpleEntity", {}, { dataStructure: 'HASH' }) 8 | 9 | describe("Repository", () => { 10 | describe("#expire", () => { 11 | 12 | let client: Client 13 | let repository: Repository 14 | 15 | beforeAll(() => { client = new Client() }) 16 | beforeEach(() => { repository = new Repository(simpleSchema, client) }) 17 | 18 | it("expires a single entity", async () => { 19 | await repository.expire('foo', 60) 20 | expect(client.expire).toHaveBeenCalledWith('SimpleEntity:foo', 60) 21 | }) 22 | 23 | it("expires a multiple entities", async () => { 24 | await repository.expire(['foo', 'bar', 'baz'], 60) 25 | expect(client.expire).toHaveBeenNthCalledWith(1, 'SimpleEntity:foo', 60) 26 | expect(client.expire).toHaveBeenNthCalledWith(2, 'SimpleEntity:bar', 60) 27 | expect(client.expire).toHaveBeenNthCalledWith(3, 'SimpleEntity:baz', 60) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /spec/unit/repository/repository-expireAt.spec.ts: -------------------------------------------------------------------------------- 1 | import '../helpers/mock-client'; 2 | 3 | import { Client } from '$lib/client'; 4 | import { Repository } from '$lib/repository'; 5 | import { Schema } from '$lib/schema'; 6 | 7 | const simpleSchema = new Schema('SimpleEntity', {}, { dataStructure: 'HASH' }); 8 | 9 | const today = new Date(); 10 | const tomorrow = new Date(today); 11 | tomorrow.setDate(tomorrow.getDate() + 1); 12 | const yesterday = new Date(today); 13 | yesterday.setDate(yesterday.getDate() - 1); 14 | 15 | describe('Repository', () => { 16 | describe('#expireAt', () => { 17 | let client: Client; 18 | let repository: Repository; 19 | 20 | beforeAll(() => { 21 | client = new Client(); 22 | }); 23 | beforeEach(() => { 24 | repository = new Repository(simpleSchema, client); 25 | }); 26 | 27 | it('expires a single entity', async () => { 28 | await repository.expireAt('foo', tomorrow); 29 | expect(client.expireAt).toHaveBeenCalledWith( 30 | 'SimpleEntity:foo', 31 | tomorrow 32 | ); 33 | }); 34 | 35 | it('expires multiple entities', async () => { 36 | await repository.expireAt(['foo', 'bar', 'baz'], tomorrow); 37 | expect(client.expireAt).toHaveBeenNthCalledWith( 38 | 1, 39 | 'SimpleEntity:foo', 40 | tomorrow 41 | ); 42 | expect(client.expireAt).toHaveBeenNthCalledWith( 43 | 2, 44 | 'SimpleEntity:bar', 45 | tomorrow 46 | ); 47 | expect(client.expireAt).toHaveBeenNthCalledWith( 48 | 3, 49 | 'SimpleEntity:baz', 50 | tomorrow 51 | ); 52 | }); 53 | 54 | it('throws an error when provided invalid/past date', async () => { 55 | let caughtError: any; 56 | await repository.expireAt('foo', yesterday).catch((error) => { 57 | caughtError = error; 58 | }); 59 | expect(client.expireAt).toHaveBeenCalledTimes(0); 60 | expect(caughtError).toBeDefined(); 61 | expect(caughtError!.message).toEqual( 62 | `${yesterday.toString()} is invalid. Expiration date must be in the future.` 63 | ); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /spec/unit/repository/repository-remove.spec.ts: -------------------------------------------------------------------------------- 1 | import '../helpers/mock-client' 2 | 3 | import { Client } from '$lib/client' 4 | import { Repository } from '$lib/repository' 5 | import { Schema } from '$lib/schema' 6 | 7 | const simpleSchema = new Schema("SimpleEntity", {}, { dataStructure: 'HASH' }) 8 | 9 | describe("Repository", () => { 10 | describe("#remove", () => { 11 | 12 | let client: Client 13 | let repository: Repository 14 | 15 | beforeAll(() => { client = new Client() }) 16 | beforeEach(() => { repository = new Repository(simpleSchema, client) }) 17 | 18 | it("removes no entities", async () => { 19 | await repository.remove() 20 | expect(client.unlink).not.toHaveBeenCalled() 21 | }) 22 | 23 | it("removes a single entity", async () => { 24 | await repository.remove('foo') 25 | expect(client.unlink).toHaveBeenCalledWith('SimpleEntity:foo') 26 | }) 27 | 28 | it("removes multiple entities", async () => { 29 | await repository.remove('foo', 'bar', 'baz') 30 | expect(client.unlink).toHaveBeenCalledWith( 31 | 'SimpleEntity:foo', 'SimpleEntity:bar', 'SimpleEntity:baz' 32 | ) 33 | }) 34 | 35 | it("removes multiple entities discretely", async () => { 36 | await repository.remove(['foo', 'bar', 'baz']) 37 | expect(client.unlink).toHaveBeenCalledWith( 38 | 'SimpleEntity:foo', 'SimpleEntity:bar', 'SimpleEntity:baz' 39 | ) 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /spec/unit/repository/repository-search.spec.ts: -------------------------------------------------------------------------------- 1 | import '../helpers/mock-client' 2 | 3 | import { Client } from '$lib/client' 4 | import { AbstractSearch, Search, RawSearch } from '$lib/search' 5 | import { Repository } from '$lib/repository' 6 | 7 | import { simpleSchema } from '../helpers/test-entity-and-schema' 8 | 9 | describe("Repository", () => { 10 | 11 | let client: Client 12 | let repository: Repository 13 | let search: AbstractSearch 14 | 15 | beforeEach(() => { 16 | client = new Client() 17 | repository = new Repository(simpleSchema, client) 18 | }) 19 | 20 | describe("#searchRaw", () => { 21 | 22 | beforeEach(async () => { 23 | search = repository.searchRaw("NOT A VALID QUERY BUT HEY WHATEVER") 24 | }) 25 | 26 | it("returns a RawSearch instance", () => expect(search).toBeInstanceOf(RawSearch)) 27 | 28 | describe("the RawSearch instance", () => { 29 | it("has the provided schema", () => { 30 | // @ts-ignore: peek inside since I can't mock the constructor 31 | expect(search.schema).toBe(simpleSchema) 32 | }) 33 | 34 | it("has the provided client", () => { 35 | // @ts-ignore: peek inside since I can't mock the constructor 36 | expect(search.client).toBe(client) 37 | }) 38 | 39 | it("has the provided query", () => { 40 | // @ts-ignore: peek inside since I can't mock the constructor 41 | expect(search.rawQuery).toBe("NOT A VALID QUERY BUT HEY WHATEVER") 42 | }) 43 | }) 44 | }) 45 | 46 | describe("#search", () => { 47 | 48 | beforeEach(async () => { 49 | repository = new Repository(simpleSchema, client); 50 | search = repository.search(); 51 | }) 52 | 53 | it("returns a Search instance", () => expect(search).toBeInstanceOf(Search)) 54 | 55 | describe("the Search instance", () => { 56 | it("has the provided schema", () => { 57 | // @ts-ignore: peek inside since I can't mock the constructor 58 | expect(search.schema).toBe(simpleSchema) 59 | }) 60 | 61 | it("has the provided client", () => { 62 | // @ts-ignore: peek inside since I can't mock the constructor 63 | expect(search.client).toBe(client) 64 | }) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /spec/unit/schema/schema-index-hash.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema, SchemaDefinition, SchemaOptions } from '$lib/schema' 2 | 3 | const EMPTY_SCHEMA_DEF: SchemaDefinition = {} 4 | const WELL_POPULATED_SCHEMA_DEF: SchemaDefinition = { 5 | aString: { type: 'string' }, anotherString: { type: 'string' }, 6 | someText: { type: 'text' }, someOtherText: { type: 'text' }, 7 | aNumber: { type: 'number' }, anotherNumber: { type: 'number' }, 8 | aBoolean: { type: 'boolean' }, anotherBoolean: { type: 'boolean' }, 9 | aPoint: { type: 'point' }, anotherPoint: { type: 'point' }, 10 | aDate: { type: 'date' }, anotherDate: { type: 'date' }, 11 | someStrings: { type: 'string[]' }, someOtherStrings: { type: 'string[]' } 12 | } 13 | 14 | const EMPTY_HASH = "9UJTUMAzgvhnE/cOJXT1D3KPGYg=" 15 | const WELL_POPULATED_HASH = "F+GgQDhzmXhvTNhQczPZtCIJ0BA=" 16 | 17 | describe("Schema", () => { 18 | describe("#indexHash", () => { 19 | 20 | describe.each([ 21 | ["that is given a well populated schema", { 22 | schemaDef: WELL_POPULATED_SCHEMA_DEF, 23 | options: {} as SchemaOptions, 24 | expectedHash: WELL_POPULATED_HASH 25 | }], 26 | ["that is given an empty schema", { 27 | schemaDef: EMPTY_SCHEMA_DEF, 28 | options: {} as SchemaOptions, 29 | expectedHash: EMPTY_HASH 30 | }], 31 | ["that is given the default data strucutre of JSON", { 32 | schemaDef: EMPTY_SCHEMA_DEF, 33 | options: { dataStructure: 'JSON' } as SchemaOptions, 34 | expectedHash: EMPTY_HASH 35 | }], 36 | ["that overrides the data structure to be HASH", { 37 | schemaDef: EMPTY_SCHEMA_DEF, 38 | options: { dataStructure: 'HASH' } as SchemaOptions, 39 | expectedHash: "nd4P5YFFLxYr/3glJ6Thvlk+0tg=" 40 | }], 41 | ["that overrides the index name", { 42 | schemaDef: EMPTY_SCHEMA_DEF, 43 | options: { indexName: 'test-index' } as SchemaOptions, 44 | expectedHash: "DgFE5XI/doj0oQNTZQ5yqPHtb6M=" 45 | }], 46 | ["that overrides the index hash name", { 47 | schemaDef: EMPTY_SCHEMA_DEF, 48 | options: { indexHashName: 'test-index-hash' } as SchemaOptions, 49 | expectedHash: "p1OuQrsovszDdUD5pnbxTT5sbpI=" 50 | }], 51 | ["that overrides the id generation strategy", { 52 | schemaDef: EMPTY_SCHEMA_DEF, 53 | options: { idStrategy: async () => '1' } as SchemaOptions, 54 | expectedHash: EMPTY_HASH 55 | }], 56 | ["that disables stop words", { 57 | schemaDef: EMPTY_SCHEMA_DEF, 58 | options: { useStopWords: 'OFF' } as SchemaOptions, 59 | expectedHash: "W7+Ri6W8CIo8GUkRY+uabQgpNVA=" 60 | }], 61 | ["that uses default stop words", { 62 | schemaDef: EMPTY_SCHEMA_DEF, 63 | options: { useStopWords: 'DEFAULT' } as SchemaOptions, 64 | expectedHash: EMPTY_HASH 65 | }], 66 | ["that uses custom stop words", { 67 | schemaDef: EMPTY_SCHEMA_DEF, 68 | options: { useStopWords: 'CUSTOM' } as SchemaOptions, 69 | expectedHash: "nEC4s/DEz7EvTApCOKzQlXFTgSA=" 70 | }], 71 | ["that sets custom stop words", { 72 | schemaDef: EMPTY_SCHEMA_DEF, 73 | options: { stopWords: ['foo', 'bar', 'baz'] } as SchemaOptions, 74 | expectedHash: "odwqZJal1kQTrLjIqu79U4ZHtDs=" 75 | }] 76 | ])("%s", (_, data) => { 77 | it("generates the expected hash", () => { 78 | const schema = new Schema('TestEntity', data.schemaDef, data.options) 79 | expect(schema.indexHash).toBe(data.expectedHash) 80 | }) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /spec/unit/search/raw-search-query.spec.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "$lib/client" 2 | import { RawSearch } from "$lib/search" 3 | 4 | import { simpleHashSchema } from "../helpers/test-entity-and-schema" 5 | 6 | 7 | describe("RawSearch", () => { 8 | describe("#query", () => { 9 | 10 | let client: Client 11 | let search: RawSearch 12 | 13 | beforeAll(() => { 14 | client = new Client() 15 | }) 16 | 17 | describe("when constructed with the default query", () => { 18 | beforeEach(() => { 19 | search = new RawSearch(simpleHashSchema, client) 20 | }) 21 | 22 | it("generates the default query", () => { 23 | expect(search.query).toBe("*") 24 | }) 25 | }) 26 | 27 | describe("when constructed with a specified query", () => { 28 | beforeEach(() => { 29 | search = new RawSearch(simpleHashSchema, client, "SOME BOGUS QUERY") 30 | }) 31 | 32 | it("generates the specific query", () => { 33 | expect(search.query).toBe("SOME BOGUS QUERY") 34 | }) 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /spec/unit/search/search-by-string-array.spec.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "$lib/client" 2 | import { Search, WhereField } from "$lib/search" 3 | 4 | import { A_STRING, ANOTHER_STRING, A_THIRD_STRING } from '../../helpers/example-data' 5 | import { simpleSchema } from "../helpers/test-entity-and-schema" 6 | 7 | 8 | describe("Search", () => { 9 | describe("#query", () => { 10 | 11 | let client: Client 12 | let search: Search 13 | let where: WhereField 14 | 15 | const A_CONTAINS_QUERY = `(@someStrings:{${A_STRING}})` 16 | const A_NEGATED_CONTAINS_QUERY = `(-@someStrings:{${A_STRING}})` 17 | const A_CONTAINS_ONE_QUERY = `(@someStrings:{${A_STRING}|${ANOTHER_STRING}|${A_THIRD_STRING}})` 18 | const A_NEGATED_CONTAINS_ONE_QUERY = `(-@someStrings:{${A_STRING}|${ANOTHER_STRING}|${A_THIRD_STRING}})` 19 | 20 | type ArrayChecker = (search: Search) => void 21 | const expectToBeContainsQuery: ArrayChecker = search => expect(search.query).toBe(A_CONTAINS_QUERY) 22 | const expectToBeNegatedContainsQuery: ArrayChecker = search => expect(search.query).toBe(A_NEGATED_CONTAINS_QUERY) 23 | const expectToBeContainsOneQuery: ArrayChecker = search => expect(search.query).toBe(A_CONTAINS_ONE_QUERY) 24 | const expectToBeNegatedContainsOneQuery: ArrayChecker = search => expect(search.query).toBe(A_NEGATED_CONTAINS_ONE_QUERY) 25 | 26 | beforeAll(() => { 27 | client = new Client() 28 | }) 29 | 30 | beforeEach(() => { 31 | search = new Search(simpleSchema, client) 32 | where = search.where('someStrings') 33 | }) 34 | 35 | describe("when generating for an array", () => { 36 | 37 | it("generates a query with .contains", () => expectToBeContainsQuery(where.contains(A_STRING))) 38 | it("generates a query with .does.contain", () => expectToBeContainsQuery(where.does.contain(A_STRING))) 39 | it("generates a query with .does.not.contain", () => expectToBeNegatedContainsQuery(where.does.not.contain(A_STRING))) 40 | 41 | it("generates a query with .containsOneOf", () => expectToBeContainsOneQuery(where.containsOneOf(A_STRING, ANOTHER_STRING, A_THIRD_STRING))) 42 | it("generates a query with .does.containOneOf", () => expectToBeContainsOneQuery(where.does.containOneOf(A_STRING, ANOTHER_STRING, A_THIRD_STRING))) 43 | it("generates a query with .does.not.containOneOf", () => expectToBeNegatedContainsOneQuery(where.does.not.containOneOf(A_STRING, ANOTHER_STRING, A_THIRD_STRING))) 44 | 45 | it("generates a query with .contains that escapes all punctuation", () => { 46 | let query = where.contains(",.?<>{}[]\"':;|!@#$%^&()-+=~/\\ ").query 47 | expect(query).toBe("(@someStrings:{\\,\\.\\?\\<\\>\\{\\}\\[\\]\\\"\\'\\:\\;\\|\\!\\@\\#\\$\\%\\^\\&\\(\\)\\-\\+\\=\\~\\/\\\\\\ })") 48 | }) 49 | 50 | it("generates a query with .containsOneOf that escapes all punctuation", () => { 51 | let query = where.containsOneOf(",.?<>{}[]\"':;|", "!@#$%^&()-+=~/\\ ").query 52 | expect(query).toBe("(@someStrings:{\\,\\.\\?\\<\\>\\{\\}\\[\\]\\\"\\'\\:\\;\\||\\!\\@\\#\\$\\%\\^\\&\\(\\)\\-\\+\\=\\~\\/\\\\\\ })") 53 | }) 54 | 55 | it("generates a query with .contains with a prefix matching wildcard", () => { 56 | let query = where.contains("foo*").query 57 | expect(query).toBe("(@someStrings:{foo*})") 58 | }) 59 | 60 | it("generates a query with .containsOnOf with a prefix matching wildcard", () => { 61 | let query = where.containsOneOf("foo*").query 62 | expect(query).toBe("(@someStrings:{foo*})") 63 | }) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /spec/unit/search/search-query-with-escaped-fields.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../helpers/custom-matchers' 2 | 3 | import { Client } from "$lib/client" 4 | import { Search } from "$lib/search" 5 | 6 | import { escapedFieldsSchema } from "../helpers/test-entity-and-schema" 7 | import { FieldNotInSchema } from "$lib/error" 8 | 9 | import { A_STRING } from '../../helpers/example-data' 10 | 11 | 12 | const EXPECTED_STRING_QUERY_1 = `@aString:{${A_STRING}}` 13 | 14 | describe("Search", () => { 15 | describe("#query", () => { 16 | 17 | let client: Client 18 | 19 | beforeAll(() => { 20 | client = new Client() 21 | }) 22 | 23 | let search: Search 24 | 25 | beforeEach(() => { 26 | search = new Search(escapedFieldsSchema, client) 27 | }) 28 | 29 | it.each([ 30 | [ "generates a query escaping ','", "a,field", `(@a\\,field:{${A_STRING}})` ], 31 | [ "generates a query escaping '.'", "a.field", `(@a\\.field:{${A_STRING}})` ], 32 | [ "generates a query escaping '<'", "a'", "a>field", `(@a\\>field:{${A_STRING}})` ], 34 | [ "generates a query escaping '{'", "a{field", `(@a\\{field:{${A_STRING}})` ], 35 | [ "generates a query escaping '}'", "a}field", `(@a\\}field:{${A_STRING}})` ], 36 | [ "generates a query escaping '['", "a[field", `(@a\\[field:{${A_STRING}})` ], 37 | [ "generates a query escaping ']'", "a]field", `(@a\\]field:{${A_STRING}})` ], 38 | [ "generates a query escaping '\"'", "a\"field", `(@a\\"field:{${A_STRING}})` ], 39 | [ "generates a query escaping '''", "a'field", `(@a\\'field:{${A_STRING}})` ], 40 | [ "generates a query escaping ':'", "a:field", `(@a\\:field:{${A_STRING}})` ], 41 | [ "generates a query escaping ';'", "a;field", `(@a\\;field:{${A_STRING}})` ], 42 | [ "generates a query escaping '!'", "a!field", `(@a\\!field:{${A_STRING}})` ], 43 | [ "generates a query escaping '@'", "a@field", `(@a\\@field:{${A_STRING}})` ], 44 | [ "generates a query escaping '#'", "a#field", `(@a\\#field:{${A_STRING}})` ], 45 | [ "generates a query escaping '$'", "a$field", `(@a\\$field:{${A_STRING}})` ], 46 | [ "generates a query escaping '%'", "a%field", `(@a\\%field:{${A_STRING}})` ], 47 | [ "generates a query escaping '^'", "a^field", `(@a\\^field:{${A_STRING}})` ], 48 | [ "generates a query escaping '&'", "a&field", `(@a\\&field:{${A_STRING}})` ], 49 | [ "generates a query escaping '('", "a(field", `(@a\\(field:{${A_STRING}})` ], 50 | [ "generates a query escaping ')'", "a)field", `(@a\\)field:{${A_STRING}})` ], 51 | [ "generates a query escaping '-'", "a-field", `(@a\\-field:{${A_STRING}})` ], 52 | [ "generates a query escaping '+'", "a+field", `(@a\\+field:{${A_STRING}})` ], 53 | [ "generates a query escaping '='", "a=field", `(@a\\=field:{${A_STRING}})` ], 54 | [ "generates a query escaping '~'", "a~field", `(@a\\~field:{${A_STRING}})` ], 55 | [ "generates a query escaping '|'", "a|field", `(@a\\|field:{${A_STRING}})` ], 56 | [ "generates a query escaping '/'", "a/field", `(@a\\/field:{${A_STRING}})` ], 57 | [ "generates a query escaping '\\'", "a\\field", `(@a\\\\field:{${A_STRING}})` ], 58 | [ "generates a query escaping ' '", "a field", `(@a\\ field:{${A_STRING}})` ] 59 | ])('%s', (_, field, expectedQuery) => { 60 | let query = search.where(field).eq(A_STRING).query 61 | expect(query).toBe(expectedQuery) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-count.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { Search, RawSearch } from "$lib/search" 4 | 5 | import { simpleHashSchema, simpleJsonSchema } from "../helpers/test-entity-and-schema" 6 | import { mockClientSearchToReturnCountOf } from "../helpers/search-helpers" 7 | 8 | 9 | type HashSearch = Search | RawSearch 10 | type JsonSearch = Search | RawSearch 11 | 12 | describe("Search", () => { 13 | 14 | describe.each([ 15 | [ "FluentSearch", 16 | new Search(simpleHashSchema, new Client()), 17 | new Search(simpleJsonSchema, new Client()) ], 18 | [ "RawSearch", 19 | new RawSearch(simpleHashSchema, new Client()), 20 | new RawSearch(simpleJsonSchema, new Client()) ] 21 | ])("%s", (_, hashSearch: HashSearch, jsonSearch: JsonSearch) => { 22 | 23 | let actualCount: number 24 | 25 | describe("#returnCount", () => { 26 | let query = '*', offset = 0, count = 0 27 | 28 | describe("when counting results from hashes", () => { 29 | 30 | beforeEach(async () => { 31 | mockClientSearchToReturnCountOf(3) 32 | actualCount = await hashSearch.return.count() 33 | }) 34 | 35 | it("askes the client for results", () => { 36 | expect(client.search).toHaveBeenCalledTimes(1) 37 | expect(client.search).toHaveBeenCalledWith('SimpleHashEntity:index', query, { 38 | LIMIT: { from: offset, size: count } }) 39 | }) 40 | 41 | it("returns the expected count", () => expect(actualCount).toBe(3)) 42 | }) 43 | 44 | describe("when running against JSON objects", () => { 45 | beforeEach(async () => { 46 | mockClientSearchToReturnCountOf(3) 47 | actualCount = await jsonSearch.return.count() 48 | }) 49 | 50 | it("askes the client for results", () => { 51 | expect(client.search).toHaveBeenCalledTimes(1) 52 | expect(client.search).toHaveBeenCalledWith('SimpleJsonEntity:index', query, { 53 | LIMIT: { from: offset, size: count }, 54 | RETURN: '$' 55 | }) 56 | }) 57 | 58 | it("returns the expected count", () => expect(actualCount).toBe(3)) 59 | }) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-first-id.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { EntityId } from '$lib/entity' 4 | import { Search, RawSearch } from "$lib/search" 5 | 6 | import { simpleHashSchema,} from "../helpers/test-entity-and-schema" 7 | import { mockClientSearchToReturnNothing, mockClientSearchToReturnSingleKey, 8 | SIMPLE_ENTITY_1 } from '../helpers/search-helpers' 9 | 10 | 11 | type HashSearch = Search | RawSearch 12 | 13 | describe.each([ 14 | [ "FluentSearch", 15 | new Search(simpleHashSchema, new Client()) ], 16 | [ "RawSearch", 17 | new RawSearch(simpleHashSchema, new Client()) ] 18 | ])("%s", (_, search: HashSearch) => { 19 | 20 | describe("#returnFirstId", () => { 21 | let id: string | null 22 | let indexName = 'SimpleHashEntity:index', query = '*' 23 | 24 | describe("when querying no results", () => { 25 | beforeEach( async () => { 26 | mockClientSearchToReturnNothing() 27 | id = await search.return.firstId() 28 | }) 29 | 30 | it("asks the client for the first result of a given repository", () => { 31 | expect(client.search).toHaveBeenCalledTimes(1) 32 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 33 | LIMIT: { from: 0, size: 1 }, 34 | RETURN: [] 35 | }) 36 | }) 37 | 38 | it("return no result", () => expect(id).toBe(null)) 39 | }) 40 | 41 | describe("when getting a result", () => { 42 | beforeEach(async () => { 43 | mockClientSearchToReturnSingleKey() 44 | id = await search.return.firstId() 45 | }) 46 | 47 | it("asks the client for the first result of a given repository", () => { 48 | expect(client.search).toHaveBeenCalledTimes(1) 49 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 50 | LIMIT: { from: 0, size: 1 }, 51 | RETURN: [] 52 | }) 53 | }) 54 | 55 | it("returns the first result of a given repository", () => { 56 | expect(id).toEqual(SIMPLE_ENTITY_1[EntityId]) 57 | }) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-first-key.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { EntityId } from '$lib/entity' 4 | import { Search, RawSearch } from "$lib/search" 5 | 6 | import { simpleHashSchema } from "../helpers/test-entity-and-schema" 7 | import { mockClientSearchToReturnNothing, mockClientSearchToReturnSingleKey, 8 | SIMPLE_ENTITY_1 } from '../helpers/search-helpers' 9 | 10 | 11 | type HashSearch = Search | RawSearch 12 | 13 | describe.each([ 14 | [ "FluentSearch", 15 | new Search(simpleHashSchema, new Client()) ], 16 | [ "RawSearch", 17 | new RawSearch(simpleHashSchema, new Client()) ] 18 | ])("%s", (_, search: HashSearch) => { 19 | 20 | describe("#returnFirstKey", () => { 21 | let id: string | null 22 | let indexName = 'SimpleHashEntity:index', query = '*' 23 | 24 | describe("when querying no results", () => { 25 | beforeEach( async () => { 26 | mockClientSearchToReturnNothing() 27 | id = await search.return.firstKey() 28 | }) 29 | 30 | it("asks the client for the first result of a given repository", () => { 31 | expect(client.search).toHaveBeenCalledTimes(1) 32 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 33 | LIMIT: { from: 0, size: 1 }, 34 | RETURN: [] 35 | }) 36 | }) 37 | 38 | it("return no result", () => expect(id).toBe(null)) 39 | }) 40 | 41 | describe("when getting a result", () => { 42 | beforeEach(async () => { 43 | mockClientSearchToReturnSingleKey() 44 | id = await search.return.firstKey() 45 | }) 46 | 47 | it("asks the client for the first result of a given repository", () => { 48 | expect(client.search).toHaveBeenCalledTimes(1) 49 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 50 | LIMIT: { from: 0, size: 1 }, 51 | RETURN: [] 52 | }) 53 | }) 54 | 55 | it("returns the first result of a given repository", () => { 56 | expect(id).toEqual(`SimpleHashEntity:${SIMPLE_ENTITY_1[EntityId]}`) 57 | }) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-max-id.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { EntityId } from '$lib/entity' 4 | import { Search, RawSearch } from "$lib/search" 5 | 6 | import { simpleHashSchema } from "../helpers/test-entity-and-schema" 7 | import { mockClientSearchToReturnNothing, mockClientSearchToReturnSingleKey, SIMPLE_ENTITY_1 } from '../helpers/search-helpers' 8 | 9 | console.warn = vi.fn() 10 | 11 | 12 | type HashSearch = Search | RawSearch 13 | 14 | 15 | describe.each([ 16 | [ "FluentSearch", 17 | new Search(simpleHashSchema, new Client()) ], 18 | [ "RawSearch", 19 | new RawSearch(simpleHashSchema, new Client()) ] 20 | ])("%s", (_, search: HashSearch) => { 21 | 22 | describe("#returnMaxId", () => { 23 | let id: string | null 24 | let indexName = 'SimpleHashEntity:index', query = '*' 25 | 26 | describe("when querying no results", () => { 27 | beforeEach( async () => { 28 | mockClientSearchToReturnNothing() 29 | id = await search.return.maxId('aNumber') 30 | }) 31 | 32 | it("asks the client for the first result of a given repository", () => { 33 | expect(client.search).toHaveBeenCalledTimes(1) 34 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 35 | LIMIT: { from: 0, size: 1 }, 36 | SORTBY: { BY: 'aNumber', DIRECTION: 'DESC' }, 37 | RETURN: [] 38 | }) 39 | }) 40 | 41 | it("return no result", () => expect(id).toBe(null)) 42 | }) 43 | 44 | describe("when getting a result", () => { 45 | beforeEach(async () => { 46 | mockClientSearchToReturnSingleKey() 47 | id = await search.return.maxId('aNumber') 48 | }) 49 | 50 | it("asks the client for the first result of a given repository", () => { 51 | expect(client.search).toHaveBeenCalledTimes(1) 52 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 53 | LIMIT: { from: 0, size: 1 }, 54 | SORTBY: { BY: 'aNumber', DIRECTION: 'DESC' }, 55 | RETURN: [] 56 | }) 57 | }) 58 | 59 | it("returns the first result of a given repository", () => { 60 | expect(id).toEqual(SIMPLE_ENTITY_1[EntityId]) 61 | }) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-max-key.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { EntityId } from '$lib/entity' 4 | import { Search, RawSearch } from "$lib/search" 5 | 6 | import { simpleHashSchema } from "../helpers/test-entity-and-schema" 7 | import { mockClientSearchToReturnNothing, mockClientSearchToReturnSingleKey, 8 | SIMPLE_ENTITY_1 } from '../helpers/search-helpers' 9 | 10 | console.warn = vi.fn() 11 | 12 | 13 | type HashSearch = Search | RawSearch 14 | 15 | 16 | describe.each([ 17 | [ "FluentSearch", 18 | new Search(simpleHashSchema, new Client()) ], 19 | [ "RawSearch", 20 | new RawSearch(simpleHashSchema, new Client()) ] 21 | ])("%s", (_, search: HashSearch) => { 22 | 23 | describe("#returnMaxKey", () => { 24 | let key: string | null 25 | let indexName = 'SimpleHashEntity:index', query = '*' 26 | 27 | describe("when querying no results", () => { 28 | beforeEach( async () => { 29 | mockClientSearchToReturnNothing() 30 | key = await search.return.maxKey('aNumber') 31 | }) 32 | 33 | it("asks the client for the first result of a given repository", () => { 34 | expect(client.search).toHaveBeenCalledTimes(1) 35 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 36 | LIMIT: { from: 0, size: 1 }, 37 | SORTBY: { BY: 'aNumber', DIRECTION: 'DESC' }, 38 | RETURN: [] 39 | }) 40 | }) 41 | 42 | it("return no result", () => expect(key).toBe(null)) 43 | }) 44 | 45 | describe("when getting a result", () => { 46 | beforeEach(async () => { 47 | mockClientSearchToReturnSingleKey() 48 | key = await search.return.maxKey('aNumber') 49 | }) 50 | 51 | it("asks the client for the first result of a given repository", () => { 52 | expect(client.search).toHaveBeenCalledTimes(1) 53 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 54 | LIMIT: { from: 0, size: 1 }, 55 | SORTBY: { BY: 'aNumber', DIRECTION: 'DESC' }, 56 | RETURN: [] 57 | }) 58 | }) 59 | 60 | it("returns the first result of a given repository", () => { 61 | expect(key).toEqual(`SimpleHashEntity:${SIMPLE_ENTITY_1[EntityId]}`) 62 | }) 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-min-id.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { EntityId } from '$lib/entity' 4 | import { Search, RawSearch } from "$lib/search" 5 | 6 | import { simpleHashSchema } from "../helpers/test-entity-and-schema" 7 | import { mockClientSearchToReturnNothing, mockClientSearchToReturnSingleKey, 8 | SIMPLE_ENTITY_1 } from '../helpers/search-helpers' 9 | 10 | console.warn = vi.fn() 11 | 12 | 13 | type HashSearch = Search | RawSearch 14 | 15 | describe.each([ 16 | [ "FluentSearch", 17 | new Search(simpleHashSchema, new Client()) ], 18 | [ "RawSearch", 19 | new RawSearch(simpleHashSchema, new Client()) ] 20 | ])("%s", (_, search: HashSearch) => { 21 | 22 | describe("#returnMinId", () => { 23 | let id: string | null 24 | let indexName = 'SimpleHashEntity:index', query = '*' 25 | 26 | describe("when querying no results", () => { 27 | beforeEach( async () => { 28 | mockClientSearchToReturnNothing() 29 | id = await search.return.minId('aNumber') 30 | }) 31 | 32 | it("asks the client for the first result of a given repository", () => { 33 | expect(client.search).toHaveBeenCalledTimes(1) 34 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 35 | LIMIT: { from: 0, size: 1 }, 36 | SORTBY: { BY: 'aNumber', DIRECTION: 'ASC' }, 37 | RETURN: [] 38 | }) 39 | }) 40 | 41 | it("return no result", () => expect(id).toBe(null)) 42 | }) 43 | 44 | describe("when getting a result", () => { 45 | beforeEach(async () => { 46 | mockClientSearchToReturnSingleKey() 47 | id = await search.return.minId('aNumber') 48 | }) 49 | 50 | it("asks the client for the first result of a given repository", () => { 51 | expect(client.search).toHaveBeenCalledTimes(1) 52 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 53 | LIMIT: { from: 0, size: 1 }, 54 | SORTBY: { BY: 'aNumber', DIRECTION: 'ASC' }, 55 | RETURN: [] 56 | }) 57 | }) 58 | 59 | it("returns the first result of a given repository", () => { 60 | expect(id).toEqual(SIMPLE_ENTITY_1[EntityId]) 61 | }) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-min-key.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { EntityId } from '$lib/entity' 4 | import { Search, RawSearch } from "$lib/search" 5 | 6 | import { simpleHashSchema } from "../helpers/test-entity-and-schema" 7 | import { mockClientSearchToReturnNothing, mockClientSearchToReturnSingleKey, 8 | SIMPLE_ENTITY_1 } from '../helpers/search-helpers' 9 | 10 | console.warn = vi.fn() 11 | 12 | 13 | type HashSearch = Search | RawSearch 14 | 15 | describe.each([ 16 | [ "FluentSearch", 17 | new Search(simpleHashSchema, new Client()) ], 18 | [ "RawSearch", 19 | new RawSearch(simpleHashSchema, new Client()) ] 20 | ])("%s", (_, search: HashSearch) => { 21 | 22 | describe("#returnMinKey", () => { 23 | let key: string | null 24 | let indexName = 'SimpleHashEntity:index', query = '*' 25 | 26 | describe("when querying no results", () => { 27 | beforeEach( async () => { 28 | mockClientSearchToReturnNothing() 29 | key = await search.return.minKey('aNumber') 30 | }) 31 | 32 | it("asks the client for the first result of a given repository", () => { 33 | expect(client.search).toHaveBeenCalledTimes(1) 34 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 35 | LIMIT: { from: 0, size: 1 }, 36 | SORTBY: { BY: 'aNumber', DIRECTION: 'ASC' }, 37 | RETURN: [] 38 | }) 39 | }) 40 | 41 | it("return no result", () => expect(key).toBe(null)) 42 | }) 43 | 44 | describe("when getting a result", () => { 45 | beforeEach(async () => { 46 | mockClientSearchToReturnSingleKey() 47 | key = await search.return.minKey('aNumber') 48 | }) 49 | 50 | it("asks the client for the first result of a given repository", () => { 51 | expect(client.search).toHaveBeenCalledTimes(1) 52 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 53 | LIMIT: { from: 0, size: 1 }, 54 | SORTBY: { BY: 'aNumber', DIRECTION: 'ASC' }, 55 | RETURN: [] 56 | }) 57 | }) 58 | 59 | it("returns the first result of a given repository", () => { 60 | expect(key).toEqual(`SimpleHashEntity:${SIMPLE_ENTITY_1[EntityId]}`) 61 | }) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-page-of-ids.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { EntityId } from '$lib/entity' 4 | import { Search, RawSearch } from "$lib/search" 5 | 6 | import { simpleHashSchema } from "../helpers/test-entity-and-schema" 7 | import { mockClientSearchToReturnNothing, 8 | mockClientSearchToReturnSingleKey, mockClientSearchToReturnMultipleKeys, 9 | SIMPLE_ENTITY_1, SIMPLE_ENTITY_2, SIMPLE_ENTITY_3 } from '../helpers/search-helpers' 10 | 11 | 12 | type HashSearch = Search | RawSearch 13 | 14 | describe.each([ 15 | [ "FluentSearch", 16 | new Search(simpleHashSchema, new Client()) ], 17 | [ "RawSearch", 18 | new RawSearch(simpleHashSchema, new Client()) ] 19 | ])("%s", (_, search: HashSearch) => { 20 | 21 | describe("#returnPageOfIds", () => { 22 | 23 | let keys: string[] 24 | let indexName = 'SimpleHashEntity:index', query = '*' 25 | 26 | describe("when querying no results", () => { 27 | beforeEach(async () => { 28 | mockClientSearchToReturnNothing() 29 | keys = await search.return.pageOfIds(0, 5) 30 | }) 31 | 32 | it("askes the client for results", () => { 33 | expect(client.search).toHaveBeenCalledTimes(1) 34 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 35 | LIMIT: { from: 0, size: 5 }, 36 | RETURN: [] 37 | }) 38 | }) 39 | 40 | it("returns no results", () => expect(keys).toHaveLength(0)) 41 | }) 42 | 43 | describe("when querying a single result", () => { 44 | beforeEach(async () => { 45 | mockClientSearchToReturnSingleKey() 46 | keys = await search.return.pageOfIds(0, 5) 47 | }) 48 | 49 | it("askes the client for results", () => { 50 | expect(client.search).toHaveBeenCalledTimes(1) 51 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 52 | LIMIT: { from: 0, size: 5 }, 53 | RETURN: [] 54 | }) 55 | }) 56 | 57 | it("returns the expected single result", () => { 58 | expect(keys).toHaveLength(1) 59 | expect(keys).toEqual(expect.arrayContaining([ 60 | SIMPLE_ENTITY_1[EntityId] 61 | ])) 62 | }) 63 | }) 64 | 65 | describe("when querying multiple results", () => { 66 | beforeEach(async () => { 67 | mockClientSearchToReturnMultipleKeys() 68 | keys = await search.return.pageOfIds(0, 5) 69 | }) 70 | 71 | it("askes the client for results", () => { 72 | expect(client.search).toHaveBeenCalledTimes(1) 73 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 74 | LIMIT: { from: 0, size: 5 }, 75 | RETURN: [] 76 | }) 77 | }) 78 | 79 | it("returns all the results", async () => { 80 | expect(keys).toHaveLength(3) 81 | expect(keys).toEqual(expect.arrayContaining([ 82 | SIMPLE_ENTITY_1[EntityId], 83 | SIMPLE_ENTITY_2[EntityId], 84 | SIMPLE_ENTITY_3[EntityId] 85 | ])) 86 | }) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /spec/unit/search/search-return-page-of-keys.spec.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../helpers/mock-client' 2 | import { Client } from "$lib/client" 3 | import { EntityId } from '$lib/entity' 4 | import { Search, RawSearch } from "$lib/search" 5 | 6 | import { simpleHashSchema } from "../helpers/test-entity-and-schema" 7 | import { mockClientSearchToReturnNothing, 8 | mockClientSearchToReturnSingleKey, mockClientSearchToReturnMultipleKeys, 9 | SIMPLE_ENTITY_1, SIMPLE_ENTITY_2, SIMPLE_ENTITY_3 } from '../helpers/search-helpers' 10 | 11 | 12 | type HashSearch = Search | RawSearch 13 | 14 | describe.each([ 15 | [ "FluentSearch", 16 | new Search(simpleHashSchema, new Client()) ], 17 | [ "RawSearch", 18 | new RawSearch(simpleHashSchema, new Client()) ] 19 | ])("%s", (_, search: HashSearch) => { 20 | 21 | describe("#returnPageOfKeys", () => { 22 | 23 | let keys: string[] 24 | let indexName = 'SimpleHashEntity:index', query = '*' 25 | 26 | describe("when querying no results", () => { 27 | beforeEach(async () => { 28 | mockClientSearchToReturnNothing() 29 | keys = await search.return.pageOfKeys(0, 5) 30 | }) 31 | 32 | it("askes the client for results", () => { 33 | expect(client.search).toHaveBeenCalledTimes(1) 34 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 35 | LIMIT: { from: 0, size: 5 }, 36 | RETURN: [] 37 | }) 38 | }) 39 | 40 | it("returns no results", () => expect(keys).toHaveLength(0)) 41 | }) 42 | 43 | describe("when querying a single result", () => { 44 | beforeEach(async () => { 45 | mockClientSearchToReturnSingleKey() 46 | keys = await search.return.pageOfKeys(0, 5) 47 | }) 48 | 49 | it("askes the client for results", () => { 50 | expect(client.search).toHaveBeenCalledTimes(1) 51 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 52 | LIMIT: { from: 0, size: 5 }, 53 | RETURN: [] 54 | }) 55 | }) 56 | 57 | it("returns the expected single result", () => { 58 | expect(keys).toHaveLength(1) 59 | expect(keys).toEqual(expect.arrayContaining([ 60 | `SimpleHashEntity:${SIMPLE_ENTITY_1[EntityId]}` 61 | ])) 62 | }) 63 | }) 64 | 65 | describe("when querying multiple results", () => { 66 | beforeEach(async () => { 67 | mockClientSearchToReturnMultipleKeys() 68 | keys = await search.return.pageOfKeys(0, 5) 69 | }) 70 | 71 | it("askes the client for results", () => { 72 | expect(client.search).toHaveBeenCalledTimes(1) 73 | expect(client.search).toHaveBeenCalledWith(indexName, query, { 74 | LIMIT: { from: 0, size: 5 }, 75 | RETURN: [] 76 | }) 77 | }) 78 | 79 | it("returns all the results", async () => { 80 | expect(keys).toHaveLength(3) 81 | expect(keys).toEqual(expect.arrayContaining([ 82 | `SimpleHashEntity:${SIMPLE_ENTITY_1[EntityId]}`, 83 | `SimpleHashEntity:${SIMPLE_ENTITY_2[EntityId]}`, 84 | `SimpleHashEntity:${SIMPLE_ENTITY_3[EntityId]}` 85 | ])) 86 | }) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "CommonJS", 5 | "rootDir": ".", 6 | "outDir": "./dist", 7 | "declaration": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "types": ["vitest/globals"], 12 | "paths": { 13 | "$lib/*": ["./lib/*"] 14 | }, 15 | "noUncheckedIndexedAccess": true 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ], 20 | "typedocOptions": { 21 | "entryPoints": [ 22 | "lib/index.ts" 23 | ], 24 | "out": "docs", 25 | "excludePrivate": true, 26 | "excludeInternal": true, 27 | "readme": "none" 28 | } 29 | } -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import path from "path" 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | clearMocks: true, 8 | isolate: false, 9 | coverage: { 10 | provider: 'istanbul', 11 | exclude: [ 12 | 'spec/*' 13 | ] 14 | }, 15 | exclude: [ 16 | './node_modules/**', 17 | ] 18 | }, 19 | resolve: { 20 | alias: { 21 | "$lib": path.resolve(__dirname, "./lib"), 22 | }, 23 | }, 24 | }) 25 | --------------------------------------------------------------------------------