├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.config.ts ├── bun.lockb ├── docs ├── .vitepress │ ├── config.ts │ └── theme │ │ ├── index.ts │ │ └── style.css ├── api-reference │ ├── filter-match-mode.md │ ├── query-filter.md │ ├── query-function.md │ └── query-params.md ├── features │ ├── filtering.md │ ├── pagination.md │ ├── searching.md │ ├── sorting.md │ └── type-safety.md ├── filter-match-modes │ ├── array-length.md │ ├── between.md │ ├── contains.md │ ├── equals.md │ ├── exists.md │ ├── greater-than-or-equal.md │ ├── greater-than.md │ ├── less-than-or-equal.md │ ├── less-than.md │ ├── not-equals.md │ ├── object-match.md │ └── regex.md ├── getting-started │ ├── installation.md │ └── introduction.md ├── index.md └── public │ ├── favicon.ico │ └── images │ └── logo.svg ├── eslint.config.js ├── package.json ├── pnpm-workspace.yaml ├── src ├── index.ts ├── query.ts ├── types.ts └── utils.ts ├── test ├── fixtures │ ├── filtering.fixture.json │ ├── pagination.fixture.json │ ├── search.fixture.json │ └── sorting.fixture.json └── index.test.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [antfu] 2 | opencollective: antfu 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install Bun 19 | uses: oven-sh/setup-bun@v1 20 | 21 | - name: Install dependencies 22 | run: bun install 23 | 24 | - name: Lint 25 | run: bun run lint 26 | 27 | typecheck: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Install Bun 33 | uses: oven-sh/setup-bun@v1 34 | 35 | - name: Install dependencies 36 | run: bun install 37 | 38 | - name: Typecheck 39 | run: bun run typecheck 40 | 41 | test: 42 | runs-on: ${{ matrix.os }} 43 | 44 | strategy: 45 | matrix: 46 | os: [ubuntu-latest, windows-latest, macos-latest] 47 | fail-fast: false 48 | 49 | steps: 50 | - uses: actions/checkout@v4 51 | 52 | - name: Install Bun 53 | uses: oven-sh/setup-bun@v1 54 | 55 | - name: Install dependencies 56 | run: bun install 57 | 58 | - name: Build 59 | run: bun run build 60 | 61 | - name: Test 62 | run: bun run test 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | playground/.nuxt 13 | docs/.vitepress/dist 14 | docs/.vitepress/cache 15 | .vercel 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shell-emulator=true 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off" }, 15 | { "rule": "*-indent", "severity": "off" }, 16 | { "rule": "*-spacing", "severity": "off" }, 17 | { "rule": "*-spaces", "severity": "off" }, 18 | { "rule": "*-order", "severity": "off" }, 19 | { "rule": "*-dangle", "severity": "off" }, 20 | { "rule": "*-newline", "severity": "off" }, 21 | { "rule": "*quotes", "severity": "off" }, 22 | { "rule": "*semi", "severity": "off" } 23 | ], 24 | 25 | // Enable eslint for all supported languages 26 | "eslint.validate": [ 27 | "javascript", 28 | "javascriptreact", 29 | "typescript", 30 | "typescriptreact", 31 | "vue", 32 | "html", 33 | "markdown", 34 | "json", 35 | "jsonc", 36 | "yaml" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/antfu/contribute 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Array-Query 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | > ### **Query JavaScript arrays with ORM-like syntax, benefiting from excellent type-safety & developer experience.** 10 | 11 | ## Key Features: 12 | 13 | - 🛠 **Type-safe Querying:** Design your queries with strong typing (typed sort / search / filter keys & output) 14 | - 📄 **Pagination:** Easily paginate through large datasets with built-in support. 15 | - 🔎 **Full-text Search:** Perform comprehensive searches across multiple fields in your data. 16 | - 🧭 **Advanced Filtering:** Apply complex filters with various match modes for precise data retrieval. Supports logical grouping, nested conditions, and array matching. 17 | - 🔢 **Flexible Sorting:** Order results based on any field, with support for multiple sort criteria. 18 | - 🚀 **Lightweight and Fast:** Queries stay super fast, even with large datasets. 19 | - 🧩 **Zero Dependencies**: Completely self-contained with no external dependencies, ensuring a small bundle cost 20 | 21 | ## INSTALLATION 22 | 23 | ```bash 24 | // npm 25 | npm install @chronicstone/array-query 26 | 27 | // yarn 28 | yarn add @chronicstone/array-query 29 | 30 | // pnpm 31 | pnpm add @chronicstone/array-query 32 | 33 | // bun 34 | bun add @chronicstone/array-query 35 | ``` 36 | 37 | ## USAGE EXAMPLE 38 | 39 | ```ts 40 | import { query } from '@chronicstone/array-query' 41 | 42 | const users = [ 43 | { id: 1, fullName: 'John Doe', email: 'john@example.com', age: 30, status: 'active', roles: ['admin'], createdAt: '2023-01-01' }, 44 | { id: 2, fullName: 'Jane Smith', email: 'jane@example.com', age: 28, status: 'inactive', roles: ['user'], createdAt: '2023-02-15' }, 45 | { id: 3, fullName: 'Bob Johnson', email: 'bob@example.com', age: 35, status: 'active', roles: ['user', 'manager'], createdAt: '2023-03-20' }, 46 | // ... more users 47 | ] 48 | 49 | const { totalRows, totalPages, rows } = query(users, { 50 | // Pagination 51 | page: 1, 52 | limit: 10, 53 | 54 | // Sorting 55 | sort: [ 56 | { key: 'age', dir: 'desc' }, 57 | { key: 'fullName', dir: 'asc' } 58 | ], 59 | 60 | // Searching 61 | search: { 62 | value: 'john', 63 | keys: ['fullName', 'email'] 64 | }, 65 | 66 | // Filtering 67 | filter: [ 68 | { key: 'status', matchMode: 'equals', value: 'active' }, 69 | { key: 'age', matchMode: 'greaterThan', value: 25 }, 70 | ] 71 | }) 72 | ``` 73 | 74 | This example demonstrates pagination, multi-field sorting, full-text searching, and complex filtering with nested conditions and array field matching. 75 | 76 | ## License 77 | 78 | [MIT](./LICENSE) License © 2023-PRESENT [Cyprien THAO](https://github.com/ChronicStone) 79 | 80 | 81 | 82 | [npm-version-src]: https://img.shields.io/npm/v/@chronicstone/array-query?style=flat&colorA=080f12&colorB=1fa669 83 | [npm-version-href]: https://npmjs.com/package/@chronicstone/array-query 84 | [npm-downloads-src]: https://img.shields.io/npm/dm/@chronicstone/array-query?style=flat&colorA=080f12&colorB=1fa669 85 | [npm-downloads-href]: https://npmjs.com/package/@chronicstone/array-query 86 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/@chronicstone/array-query?style=flat&colorA=080f12&colorB=1fa669&label=minzip 87 | [bundle-href]: https://bundlephobia.com/result?p=@chronicstone/array-query 88 | [license-src]: https://img.shields.io/github/license/ChronicStone/array-ql.svg?style=flat&colorA=080f12&colorB=1fa669 89 | [license-href]: https://github.com/ChronicStone/array-ql/blob/main/LICENSE 90 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 91 | [jsdocs-href]: https://www.jsdocs.io/package/@chronicstone/array-query 92 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | ], 7 | declaration: true, 8 | clean: true, 9 | rollup: { 10 | emitCJS: true, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChronicStone/array-ql/fea673b4ee20e8ad83b896a2fb1d19be4e8c358c/bun.lockb -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { transformerTwoslash } from '@shikijs/vitepress-twoslash' 3 | import Unocss from 'unocss/vite' 4 | 5 | // https://vitepress.dev/reference/site-config 6 | export default defineConfig({ 7 | title: 'Array-Query', 8 | description: 'Documentation of Array-Query library', 9 | sitemap: { 10 | hostname: 'https://typed-xlsx.vercel.app', 11 | }, 12 | markdown: { 13 | theme: { 14 | light: 'github-light', 15 | dark: 'github-dark', 16 | }, 17 | 18 | codeTransformers: [ 19 | transformerTwoslash(), 20 | ], 21 | }, 22 | lastUpdated: true, 23 | ignoreDeadLinks: true, 24 | cleanUrls: true, 25 | router: { 26 | prefetchLinks: true, 27 | }, 28 | titleTemplate: 'Array-Query | :title', 29 | themeConfig: { 30 | logo: '/images/logo.svg', 31 | editLink: { 32 | pattern: 'https://github.com/ChronicStone/array-ql/edit/main/docs/:path', 33 | text: 'Edit this page on GitHub', 34 | }, 35 | search: { provider: 'local' }, 36 | nav: [ 37 | { text: 'Home', link: '/' }, 38 | { text: 'Documentation', link: '/getting-started/introduction' }, 39 | ], 40 | socialLinks: [ 41 | { icon: 'github', link: 'https://github.com/ChronicStone/array-ql' }, 42 | ], 43 | footer: { 44 | message: 'Released under the MIT License.', 45 | copyright: 'Copyright © 2024-present Cyprien THAO', 46 | }, 47 | sidebar: [ 48 | { 49 | text: 'Getting Started', 50 | items: [ 51 | { text: 'Introduction', link: '/getting-started/introduction' }, 52 | { text: 'Installation', link: '/getting-started/installation' }, 53 | ], 54 | }, 55 | { 56 | text: 'Features', 57 | items: [ 58 | { text: 'Type-Safe Queries', link: '/features/type-safety' }, 59 | { text: 'Pagination', link: '/features/pagination' }, 60 | { text: 'Sorting', link: '/features/sorting' }, 61 | { text: 'Searching', link: '/features/searching' }, 62 | { text: 'Filtering', link: '/features/filtering' }, 63 | ], 64 | }, 65 | { 66 | text: 'Filter Match Modes', 67 | items: [ 68 | { text: 'contains', link: '/filter-match-modes/contains' }, 69 | { text: 'between', link: '/filter-match-modes/between' }, 70 | { text: 'equals', link: '/filter-match-modes/equals' }, 71 | { text: 'notEquals', link: '/filter-match-modes/not-equals' }, 72 | { text: 'greaterThan', link: '/filter-match-modes/greater-than' }, 73 | { text: 'greaterThanOrEqual', link: '/filter-match-modes/greater-than-or-equal' }, 74 | { text: 'lessThan', link: '/filter-match-modes/less-than' }, 75 | { text: 'lessThanOrEqual', link: '/filter-match-modes/less-than-or-equal' }, 76 | { text: 'exists', link: '/filter-match-modes/exists' }, 77 | { text: 'arrayLength', link: '/filter-match-modes/array-length' }, 78 | { text: 'objectMatch', link: '/filter-match-modes/object-match' }, 79 | ], 80 | }, 81 | // { 82 | // text: 'API Reference', 83 | // items: [ 84 | // { text: 'Query Function', link: '/api-reference/query-function' }, 85 | // { text: 'QueryParams', link: '/api-reference/query-params' }, 86 | // { text: 'QueryFilter', link: '/api-reference/query-filter' }, 87 | // { text: 'FilterMatchMode', link: '/api-reference/filter-match-mode' }, 88 | // ], 89 | // }, 90 | ], 91 | 92 | }, 93 | vite: { 94 | plugins: [ 95 | Unocss({}), 96 | ], 97 | }, 98 | }) 99 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/guide/custom-theme 2 | import { h } from 'vue' 3 | import type { Theme } from 'vitepress' 4 | import DefaultTheme from 'vitepress/theme' 5 | import './style.css' 6 | import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' 7 | import '@shikijs/vitepress-twoslash/style.css' 8 | import vitepressNprogress from 'vitepress-plugin-nprogress' 9 | import 'vitepress-plugin-nprogress/lib/css/index.css' 10 | import 'uno.css' 11 | 12 | export default { 13 | extends: DefaultTheme, 14 | Layout: () => { 15 | return h(DefaultTheme.Layout, null, { 16 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 17 | }) 18 | }, 19 | enhanceApp(ctx) { 20 | ctx.app.use(TwoslashFloatingVue) 21 | vitepressNprogress(ctx) 22 | }, 23 | } satisfies Theme 24 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-white: #ffffff; 3 | --vp-c-black: #000000; 4 | 5 | --vp-c-gray: #8e8e93; 6 | 7 | --vp-c-text-light-1: #000000; 8 | --vp-c-text-light-2: #454545; 9 | --vp-c-text-light-3: #959595; 10 | 11 | --vp-c-text-dark-1: rgba(255, 255, 245, 0.86); 12 | --vp-c-text-dark-2: rgba(235, 235, 245, 0.6); 13 | --vp-c-text-dark-3: rgba(235, 235, 245, 0.38); 14 | 15 | --vp-c-blue: #0384fc; /* primary brand color */ 16 | --vp-c-blue-soft: rgba(3, 132, 252, 0.1); 17 | --vp-c-blue-light: #35a0fd; /* lighter version */ 18 | --vp-c-blue-lighter: #67bbfe; /* even lighter version */ 19 | --vp-c-blue-dark: #026ac9; /* darker version */ 20 | --vp-c-blue-darker: #015196; /* even darker version */ 21 | } 22 | 23 | /** 24 | * Colors Theme 25 | * -------------------------------------------------------------------------- */ 26 | 27 | :root { 28 | --vp-c-bg: #ffffff; 29 | 30 | --vp-c-bg-elv: #ffffff; 31 | --vp-c-bg-elv-up: #ffffff; 32 | --vp-c-bg-elv-down: #f6f6f7; 33 | --vp-c-bg-elv-mute: #f6f6f7; 34 | 35 | --vp-c-bg-soft: #f6f6f7; 36 | --vp-c-bg-soft-up: #ffffff; 37 | --vp-c-bg-soft-down: #e3e3e5; 38 | --vp-c-bg-soft-mute: #e3e3e5; 39 | 40 | --vp-c-bg-alt: #f6f6f7; 41 | 42 | --vp-c-border: rgba(60, 60, 67, 0.29); 43 | --vp-c-divider: rgba(60, 60, 67, 0.12); 44 | --vp-c-gutter: rgba(60, 60, 67, 0.12); 45 | 46 | --vp-c-neutral: var(--vp-c-black); 47 | --vp-c-neutral-inverse: var(--vp-c-white); 48 | 49 | --vp-c-text-1: var(--vp-c-text-light-1); 50 | --vp-c-text-2: var(--vp-c-text-light-2); 51 | --vp-c-text-3: var(--vp-c-text-light-3); 52 | 53 | --vp-c-text-inverse-1: var(--vp-c-text-dark-1); 54 | --vp-c-text-inverse-2: var(--vp-c-text-dark-2); 55 | --vp-c-text-inverse-3: var(--vp-c-text-dark-3); 56 | 57 | --vp-c-text-code: #4a4a4a; 58 | 59 | --vp-c-brand: var(--vp-c-blue); 60 | --vp-c-brand-light: var(--vp-c-blue-light); 61 | --vp-c-brand-lighter: var(--vp-c-blue-lighter); 62 | --vp-c-brand-dark: var(--vp-c-blue-dark); 63 | --vp-c-brand-darker: var(--vp-c-blue-darker); 64 | 65 | --vp-c-mute: #f6f6f7; 66 | --vp-c-mute-light: #f9f9fc; 67 | --vp-c-mute-lighter: #ffffff; 68 | --vp-c-mute-dark: #e3e3e5; 69 | --vp-c-mute-darker: #d7d7d9; 70 | 71 | --vp-c-bg-alt: #ffffff; 72 | --vp-sidebar-bg-color: var(--vp-c-bg-alt); 73 | --vp-code-copy-code-bg: var(--vp-code-block-bg); 74 | --vp-code-copy-code-hover-bg: var(--vp-code-block-bg); 75 | } 76 | 77 | :root { 78 | --vp-font-family-base: -apple-system, BlinkMacSystemFont, segoe ui, Helvetica, Arial, sans-serif, apple color emoji, segoe ui emoji; 79 | --vp-font-family-mono: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; 80 | } 81 | 82 | 83 | /** 84 | * Colors: Function 85 | * 86 | * -------------------------------------------------------------------------- */ 87 | 88 | :root { 89 | --vp-c-brand-1: var(--vp-c-blue); 90 | --vp-c-brand-2: var(--vp-c-blue-light); 91 | --vp-c-brand-3: var(--vp-c-blue-lighter); 92 | --vp-c-brand-soft: var(--vp-c-blue-soft); 93 | --vp-code-color: var(--vp-c-text-light-2); 94 | } 95 | 96 | /** 97 | * Component: Code 98 | * -------------------------------------------------------------------------- */ 99 | :root { 100 | --vp-code-block-color: var(--vp-c-text-1); 101 | --vp-code-block-bg: rgb(246, 248, 250); 102 | 103 | --vp-code-tab-divider: var(--vp-c-text-light-3); 104 | --vp-code-tab-text-color: var(--vp-c-text-light-2); 105 | --vp-code-tab-bg: var(--vp-code-block-bg); 106 | --vp-code-tab-hover-text-color: var(--vp-c-text-light-1); 107 | --vp-code-tab-active-text-color: var(--vp-c-text-light-1); 108 | --vp-code-tab-active-bar-color: var(--vp-c-brand); 109 | 110 | --vp-code-line-highlight-color: rgba(0, 0, 0, 0.1); 111 | } 112 | 113 | 114 | .dark { 115 | --vp-c-bg: #1e1e20; 116 | 117 | --vp-c-bg-elv: #252529; 118 | --vp-c-bg-elv-up: #313136; 119 | --vp-c-bg-elv-down: #1e1e20; 120 | --vp-c-bg-elv-mute: #313136; 121 | 122 | --vp-c-bg-soft: #252529; 123 | --vp-c-bg-soft-up: #313136; 124 | --vp-c-bg-soft-down: #1e1e20; 125 | --vp-c-bg-soft-mute: #313136; 126 | 127 | --vp-c-bg-alt: #161618; 128 | 129 | --vp-c-border: rgba(82, 82, 89, 0.68); 130 | --vp-c-divider: rgba(82, 82, 89, 0.32); 131 | --vp-c-gutter: #000000; 132 | 133 | --vp-c-neutral: var(--vp-c-white); 134 | --vp-c-neutral-inverse: var(--vp-c-black); 135 | 136 | --vp-c-text-1: var(--vp-c-text-dark-1); 137 | --vp-c-text-2: var(--vp-c-text-dark-2); 138 | --vp-c-text-3: var(--vp-c-text-dark-3); 139 | 140 | --vp-c-text-inverse-1: var(--vp-c-text-light-1); 141 | --vp-c-text-inverse-2: var(--vp-c-text-light-2); 142 | --vp-c-text-inverse-3: var(--vp-c-text-light-3); 143 | 144 | --vp-c-mute: #313136; 145 | --vp-c-mute-light: #3a3a3c; 146 | --vp-c-mute-lighter: #505053; 147 | --vp-c-mute-dark: #2c2c30; 148 | --vp-c-mute-darker: #252529; 149 | 150 | --vp-code-block-bg: #2a2a2a; 151 | --vp-code-tab-hover-text-color: var(--vp-c-text-dark-2); 152 | --vp-code-tab-active-text-color: var(--vp-c-text-dark-1); 153 | 154 | --vp-c-text-code: #e8e8e8; 155 | 156 | --vp-code-color: var(--vp-c-text-dark-1); 157 | --vp-code-tab-text-color: var(--vp-c-text-dark-2); 158 | } 159 | 160 | .vp-doc h1, 161 | .vp-doc h2, 162 | .vp-doc h3, 163 | .vp-doc h4, 164 | .vp-doc h5, 165 | .vp-doc h6 { 166 | font-weight: 700; 167 | } 168 | 169 | .title { 170 | font-weight: 700 !important; 171 | } 172 | 173 | .tagline { 174 | max-width: 500px !important; 175 | } 176 | 177 | .DocSearch-Button { 178 | background-color: var(--vp-c-bg-soft); 179 | } 180 | 181 | body { 182 | -webkit-font-smoothing: subpixel-antialiased; 183 | } 184 | 185 | .VPImage.image-src { 186 | max-width: 640px; 187 | } 188 | 189 | @media screen and (max-width: 960px) { 190 | .VPHero.has-image .image { 191 | display: none; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /docs/api-reference/filter-match-mode.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChronicStone/array-ql/fea673b4ee20e8ad83b896a2fb1d19be4e8c358c/docs/api-reference/filter-match-mode.md -------------------------------------------------------------------------------- /docs/api-reference/query-filter.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChronicStone/array-ql/fea673b4ee20e8ad83b896a2fb1d19be4e8c358c/docs/api-reference/query-filter.md -------------------------------------------------------------------------------- /docs/api-reference/query-function.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChronicStone/array-ql/fea673b4ee20e8ad83b896a2fb1d19be4e8c358c/docs/api-reference/query-function.md -------------------------------------------------------------------------------- /docs/api-reference/query-params.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChronicStone/array-ql/fea673b4ee20e8ad83b896a2fb1d19be4e8c358c/docs/api-reference/query-params.md -------------------------------------------------------------------------------- /docs/features/filtering.md: -------------------------------------------------------------------------------- 1 | # Filtering 2 | 3 | ArrayQuery provides a powerful and flexible filtering engine that allows you to apply complex conditions to your data. The filtering system supports a wide range of match modes, nested conditions, and logical grouping with built-in array handling. 4 | 5 | ## Filter Structure 6 | 7 | Filters in ArrayQuery can be either an array of individual filter conditions or an array of filter groups. Here's the updated filter structure: 8 | 9 | ```ts 10 | export type FilterOptions = Array | Array 11 | 12 | interface QueryFilter { 13 | key: string 14 | value: any 15 | operator?: Operator 16 | matchMode: FilterMatchMode 17 | params?: ComparatorParams | ObjectMapFilterParams 18 | } 19 | 20 | interface QueryFilterGroup { 21 | operator: Operator 22 | filters: QueryFilter[] 23 | } 24 | 25 | type Operator = 'AND' | 'OR' | (() => 'AND' | 'OR') 26 | ``` 27 | 28 | ## Basic Usage 29 | 30 | Here's a simple example of how to use the filter feature with ArrayQuery: 31 | 32 | ```ts twoslash 33 | import { query } from '@chronicstone/array-query' 34 | 35 | const users = [ 36 | { id: 1, name: 'John Doe', age: 30, roles: ['admin', 'user'] }, 37 | { id: 2, name: 'Jane Smith', age: 25, roles: ['user'] }, 38 | { id: 3, name: 'Bob Johnson', age: 35, roles: ['manager', 'user'] } 39 | ] 40 | 41 | const result = query(users, { 42 | filter: [ 43 | { key: 'age', matchMode: 'greaterThan', value: 25 } 44 | ] 45 | }) 46 | ``` 47 | 48 | This query will return users older than 25. 49 | 50 | ## Advanced Filtering 51 | 52 | ArrayQuery supports complex filtering scenarios, including logical groups and built-in array handling. 53 | 54 | ### Multiple Conditions with Regular Filters 55 | 56 | When you use an array of regular filters, all conditions must be met for a row to be included in the result: 57 | 58 | ```ts twoslash 59 | import { query } from '@chronicstone/array-query' 60 | 61 | const users = [ 62 | { id: 1, name: 'John Doe', age: 30, roles: ['admin', 'user'] }, 63 | { id: 2, name: 'Jane Smith', age: 25, roles: ['user'] }, 64 | { id: 3, name: 'Bob Johnson', age: 35, roles: ['manager', 'user'] } 65 | ] 66 | 67 | const result = query(users, { 68 | filter: [ 69 | { key: 'age', matchMode: 'greaterThan', value: 25 }, 70 | { key: 'name', matchMode: 'contains', value: 'John' } 71 | ] 72 | }) 73 | ``` 74 | 75 | This query will return users who are older than 25 AND have 'John' in their name. 76 | 77 | ### Logical Groups 78 | 79 | You can use filter groups to create more complex conditions. When using filter groups, a row will be included if it matches at least one of the groups: 80 | 81 | ```ts twoslash 82 | import { query } from '@chronicstone/array-query' 83 | 84 | const users = [ 85 | { id: 1, name: 'John Doe', age: 30, roles: ['admin', 'user'] }, 86 | { id: 2, name: 'Jane Smith', age: 25, roles: ['user'] }, 87 | { id: 3, name: 'Bob Johnson', age: 35, roles: ['manager', 'user'] }, 88 | { id: 4, name: 'Alice Williams', age: 65, roles: ['user'] } 89 | ] 90 | 91 | const result = query(users, { 92 | filter: [ 93 | { 94 | operator: 'AND', 95 | filters: [ 96 | { key: 'age', matchMode: 'greaterThan', value: 18 }, 97 | { key: 'age', matchMode: 'lessThan', value: 45 } 98 | ] 99 | }, 100 | { 101 | operator: 'AND', 102 | filters: [ 103 | { key: 'age', matchMode: 'greaterThan', value: 60 }, 104 | { key: 'age', matchMode: 'lessThan', value: 90 } 105 | ] 106 | } 107 | ] 108 | }) 109 | ``` 110 | 111 | This example filters users who are either between 18 and 45 years old, OR between 60 and 90 years old. The row will be included if it matches either of these groups. 112 | 113 | ### Built-in Array Handling 114 | 115 | ArrayQuery automatically handles array values in the data. When a filter is applied to a field that contains an array, the filter condition is checked against each element of the array: 116 | 117 | ```ts twoslash 118 | import { query } from '@chronicstone/array-query' 119 | 120 | const users = [ 121 | { id: 1, name: 'John Doe', age: 30, roles: ['admin', 'user'] }, 122 | { id: 2, name: 'Jane Smith', age: 25, roles: ['user'] }, 123 | { id: 3, name: 'Bob Johnson', age: 35, roles: ['manager', 'user'] } 124 | ] 125 | 126 | const result = query(users, { 127 | filter: [ 128 | { 129 | key: 'roles', 130 | matchMode: 'equals', 131 | value: 'admin', 132 | operator: 'OR' 133 | } 134 | ] 135 | }) 136 | ``` 137 | 138 | In this example: 139 | - For a user with `roles: ['admin', 'manager']`, the filter will return true because at least one element ("admin") matches the condition. 140 | - The `operator: "OR"` means that if any element in the array matches, the condition is considered true. 141 | - If `operator: "AND"` was used, all elements in the array would need to match the condition. 142 | 143 | ### Nested Object Filtering 144 | 145 | You can filter based on nested object properties using dot notation: 146 | 147 | ```ts twoslash 148 | import { query } from '@chronicstone/array-query' 149 | 150 | const users = [ 151 | { id: 1, name: 'John Doe', age: 30, address: { city: 'New York', country: 'USA' } }, 152 | { id: 2, name: 'Jane Smith', age: 25, address: { city: 'London', country: 'UK' } }, 153 | { id: 3, name: 'Bob Johnson', age: 35, address: { city: 'Paris', country: 'France' } } 154 | ] 155 | 156 | const result = query(users, { 157 | filter: [ 158 | { key: 'address.city', matchMode: 'equals', value: 'New York' } 159 | ] 160 | }) 161 | ``` 162 | 163 | ## Match Modes 164 | 165 | ArrayQuery supports various match modes for different types of comparisons. Each match mode has its own dedicated documentation page for detailed usage. The available match modes include: 166 | 167 | - 'contains' 168 | - 'between' 169 | - 'equals' 170 | - 'notEquals' 171 | - 'greaterThan' 172 | - 'greaterThanOrEqual' 173 | - 'lessThan' 174 | - 'lessThanOrEqual' 175 | - 'exists' 176 | - 'arrayLength' 177 | - 'regex' 178 | - 'objectMatch' 179 | -------------------------------------------------------------------------------- /docs/features/pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | Pagination is a crucial feature when dealing with large datasets. ArrayQuery provides an easy-to-use pagination system that allows you to efficiently retrieve subsets of your data. 4 | 5 | ## How Pagination Works 6 | 7 | ArrayQuery's pagination system operates based on two main parameters: 8 | 9 | 1. `page`: The current page number (1-indexed) 10 | 2. `limit`: The number of items per page 11 | 12 | When you apply pagination to your query, ArrayQuery will return the specified subset of data along with metadata about the pagination results. 13 | 14 | ## Basic Usage 15 | 16 | Here's a simple example of how to use pagination with ArrayQuery: 17 | 18 | ```ts twoslash 19 | import { query } from '@chronicstone/array-query' 20 | 21 | const data = [ 22 | { id: 1, name: 'Item 1' }, 23 | { id: 2, name: 'Item 2' }, 24 | { id: 3, name: 'Item 3' }, 25 | { id: 4, name: 'Item 4' }, 26 | { id: 5, name: 'Item 5' }, 27 | { id: 6, name: 'Item 6' }, 28 | { id: 7, name: 'Item 7' }, 29 | { id: 8, name: 'Item 8' }, 30 | { id: 9, name: 'Item 9' }, 31 | { id: 10, name: 'Item 10' }, 32 | ] 33 | 34 | const result = query(data, { 35 | page: 1, 36 | limit: 5 37 | }) 38 | ``` 39 | 40 | This query will return the following result: 41 | 42 | ## Understanding the Result 43 | 44 | The outout of query is different with and without pagination. When pagination is applied, the result will be an object with the following properties: 45 | 46 | - `rows: T[]`: An array containing the items for the requested page 47 | - `unpaginatedRows: T[]`: An array containing the items for the entire dataset, with filter / sort .. without pagination 48 | - `totalPages`: The total number of pages based on the dataset size and the limit 49 | - `totalRows`: The total number of rows in the entire dataset 50 | 51 | ```ts twoslash 52 | import { query } from '@chronicstone/array-query' 53 | 54 | const data = [ 55 | { id: 1, name: 'Item 1' }, 56 | { id: 2, name: 'Item 2' }, 57 | { id: 3, name: 'Item 3' }, 58 | { id: 4, name: 'Item 4' }, 59 | { id: 5, name: 'Item 5' }, 60 | ] 61 | 62 | const result = query(data, { 63 | page: 2, 64 | limit: 2 65 | }) 66 | 67 | console.log(result) 68 | // HOVER result TO SEE OUTPUT TYPE 69 | ``` 70 | 71 | When pagination is not applied, the result will be an an object with the following properties: 72 | 73 | - `rows: T[]`: An array containing the items for the entire dataset 74 | 75 | ```ts twoslash 76 | import { query } from '@chronicstone/array-query' 77 | 78 | const data = [ 79 | { id: 1, name: 'Item 1' }, 80 | { id: 2, name: 'Item 2' }, 81 | { id: 3, name: 'Item 3' }, 82 | { id: 4, name: 'Item 4' }, 83 | { id: 5, name: 'Item 5' }, 84 | ] 85 | 86 | const result = query(data, {}) 87 | // HOVER result TO SEE OUTPUT TYPE 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/features/searching.md: -------------------------------------------------------------------------------- 1 | # Searching 2 | 3 | ArrayQuery provides a powerful searching capability that allows you to perform full-text searches across multiple fields in your data, including deeply nested fields and array fields. 4 | 5 | ## How Searching Works 6 | 7 | The search feature in ArrayQuery operates based on the `SearchOptions` interface: 8 | 9 | ```ts twoslash 10 | interface SearchOptions { 11 | value: string 12 | keys: string[] | Array<{ key: string, caseSensitive?: boolean }> 13 | caseSensitive?: boolean 14 | } 15 | ``` 16 | 17 | - `value`: The search term or phrase 18 | - `keys`: An array of field names to search within, or an array of objects specifying the key and case sensitivity for each field 19 | - `caseSensitive`: A global flag to set case sensitivity for all fields (optional) 20 | 21 | When you apply a search to your query, ArrayQuery will return all items where the specified fields contain the search value. 22 | 23 | ## Basic Usage 24 | 25 | Here's a simple example of how to use the search feature with ArrayQuery: 26 | 27 | ```ts twoslash 28 | import { query } from '@chronicstone/array-query' 29 | 30 | const data = [ 31 | { id: 1, title: 'iPhone 12', description: 'Latest Apple smartphone' }, 32 | { id: 2, title: 'Samsung Galaxy S21', description: 'Flagship Android device' }, 33 | { id: 3, title: 'Google Pixel 5', description: 'Pure Android experience' }, 34 | { id: 4, title: 'OnePlus 9', description: 'Flagship killer smartphone' } 35 | ] 36 | 37 | const result = query(data, { 38 | search: { 39 | value: 'android', 40 | keys: ['title', 'description'] 41 | } 42 | }) 43 | ``` 44 | 45 | This query will return the following result: 46 | 47 | ```ts 48 | [ 49 | { id: 2, title: 'Samsung Galaxy S21', description: 'Flagship Android device' }, 50 | { id: 3, title: 'Google Pixel 5', description: 'Pure Android experience' } 51 | ] 52 | ``` 53 | 54 | ## Case Sensitivity 55 | 56 | You can control case sensitivity in two ways: 57 | 58 | 1. Globally for all fields: 59 | 60 | ```ts twoslash 61 | import { query } from '@chronicstone/array-query' 62 | 63 | const data = [ 64 | { id: 1, title: 'iPhone 12', description: 'Latest Apple smartphone' }, 65 | { id: 2, title: 'Samsung Galaxy S21', description: 'Flagship Android device' }, 66 | { id: 3, title: 'Google Pixel 5', description: 'Pure Android experience' }, 67 | { id: 4, title: 'OnePlus 9', description: 'Flagship killer smartphone' } 68 | ] 69 | 70 | const result = query(data, { 71 | search: { 72 | value: 'Android', 73 | keys: ['title', 'description'], 74 | caseSensitive: true 75 | } 76 | }) 77 | ``` 78 | 79 | 2. Per field: 80 | 81 | ```ts twoslash 82 | import { query } from '@chronicstone/array-query' 83 | 84 | const data = [ 85 | { id: 1, title: 'iPhone 12', description: 'Latest Apple smartphone' }, 86 | { id: 2, title: 'Samsung Galaxy S21', description: 'Flagship Android device' }, 87 | { id: 3, title: 'Google Pixel 5', description: 'Pure Android experience' }, 88 | { id: 4, title: 'OnePlus 9', description: 'Flagship killer smartphone' } 89 | ] 90 | 91 | const result = query(data, { 92 | search: { 93 | value: 'Android', 94 | keys: [ 95 | { key: 'title', caseSensitive: true }, 96 | { key: 'description' } 97 | ] 98 | } 99 | }) 100 | ``` 101 | 102 | In the second example, the search will be case-sensitive for the 'title' field but case-insensitive for the 'description' field. 103 | 104 | ## Searching Deeply Nested Fields 105 | 106 | ArrayQuery supports searching in deeply nested fields using dot notation: 107 | 108 | ```ts twoslash 109 | import { query } from '@chronicstone/array-query' 110 | 111 | const data = [ 112 | { 113 | id: 1, 114 | name: 'John Doe', 115 | contact: { 116 | email: 'john@example.com', 117 | address: { 118 | city: 'New York', 119 | country: 'USA' 120 | } 121 | } 122 | }, 123 | { 124 | id: 2, 125 | name: 'Jane Smith', 126 | contact: { 127 | email: 'jane@example.com', 128 | address: { 129 | city: 'London', 130 | country: 'UK' 131 | } 132 | } 133 | } 134 | ] 135 | 136 | const result = query(data, { 137 | search: { 138 | value: 'New York', 139 | keys: ['contact.address.city'] 140 | } 141 | }) 142 | ``` 143 | 144 | This will return all items where the city in the nested address object matches 'New York'. 145 | 146 | ## Searching Array Fields 147 | 148 | ArrayQuery can also search within array fields: 149 | 150 | ```ts twoslash 151 | import { query } from '@chronicstone/array-query' 152 | 153 | const data = [ 154 | { id: 1, name: 'John Doe', tags: ['developer', 'javascript', 'typescript'] }, 155 | { id: 2, name: 'Jane Smith', tags: ['designer', 'ui', 'ux'] }, 156 | { id: 3, name: 'Bob Johnson', tags: ['manager', 'agile', 'scrum'] } 157 | ] 158 | 159 | const result = query(data, { 160 | search: { 161 | value: 'script', 162 | keys: ['tags'] 163 | } 164 | }) 165 | ``` 166 | 167 | This will return all items where any of the tags contain 'script', matching both 'javascript' and 'typescript' in this case. 168 | -------------------------------------------------------------------------------- /docs/features/sorting.md: -------------------------------------------------------------------------------- 1 | # Sorting 2 | 3 | ArrayQuery provides a flexible sorting capability that allows you to order your data based on one or multiple fields. This feature is crucial for organizing and presenting data in a meaningful way. 4 | 5 | ## How Sorting Works 6 | 7 | The sort feature in ArrayQuery operates based on the `SortOptions` interface: 8 | 9 | ```ts twoslash 10 | interface SortOptions { 11 | key: string 12 | dir: 'asc' | 'desc' 13 | } 14 | ``` 15 | 16 | - `key`: The field name to sort by 17 | - `dir`: The sort direction ('asc' for ascending, 'desc' for descending) 18 | 19 | You can provide either a single `SortOptions` object or an array of `SortOptions` to sort by multiple fields. 20 | 21 | ## Basic Usage 22 | 23 | Here's a simple example of how to use the sort feature with ArrayQuery: 24 | 25 | ```ts twoslash 26 | import { query } from '@chronicstone/array-query' 27 | 28 | const users = [ 29 | { id: 1, name: 'John Doe', age: 30 }, 30 | { id: 2, name: 'Jane Smith', age: 25 }, 31 | { id: 3, name: 'Bob Johnson', age: 35 } 32 | ] 33 | 34 | const result = query(users, { 35 | sort: { key: 'age', dir: 'asc' } 36 | }) 37 | ``` 38 | 39 | This query will return the following result: 40 | 41 | ```ts twoslash 42 | [ 43 | { id: 2, name: 'Jane Smith', age: 25 }, 44 | { id: 1, name: 'John Doe', age: 30 }, 45 | { id: 3, name: 'Bob Johnson', age: 35 } 46 | ] 47 | ``` 48 | 49 | ## Multi-Field Sorting 50 | 51 | ArrayQuery supports sorting by multiple fields. The sort options are applied in the order they are provided: 52 | 53 | ```ts twoslash 54 | import { query } from '@chronicstone/array-query' 55 | 56 | const users = [ 57 | { id: 1, name: 'John Doe', age: 30 }, 58 | { id: 2, name: 'Jane Smith', age: 25 }, 59 | { id: 3, name: 'Martin Sam', age: 35 }, 60 | { id: 4, name: 'Bob Johnson', age: 35 }, 61 | { id: 5, name: 'Robert John', age: 28 }, 62 | { id: 6, name: 'Alice Williams', age: 28 }, 63 | { id: 7, name: 'Emma Jones', age: 32 } 64 | ] 65 | 66 | const result = query(users, { 67 | sort: [ 68 | { key: 'age', dir: 'asc' }, 69 | { key: 'name', dir: 'asc' } 70 | ] 71 | }) 72 | ``` 73 | 74 | This will return: 75 | 76 | ```ts twoslash 77 | [ 78 | { id: 2, name: 'Jane Smith', age: 25 }, 79 | { id: 6, name: 'Alice Williams', age: 28 }, 80 | { id: 5, name: 'Robert John', age: 28 }, 81 | { id: 1, name: 'John Doe', age: 30 }, 82 | { id: 7, name: 'Emma Jones', age: 32 }, 83 | { id: 4, name: 'Bob Johnson', age: 35 }, 84 | { id: 3, name: 'Martin Sam', age: 35 } 85 | ] 86 | ``` 87 | 88 | In this example, the data is first sorted by age in ascending order. For entries with the same age, it then sorts by name in ascending order. 89 | 90 | ## Sorting Nested Fields 91 | 92 | You can sort by nested fields using dot notation: 93 | 94 | ```ts twoslash 95 | import { query } from '@chronicstone/array-query' 96 | 97 | const data = [ 98 | { id: 1, name: 'John', scores: { math: 85, science: 92 } }, 99 | { id: 2, name: 'Jane', scores: { math: 90, science: 88 } }, 100 | { id: 3, name: 'Bob', scores: { math: 78, science: 95 } } 101 | ] 102 | 103 | const result = query(data, { 104 | sort: { key: 'scores.math', dir: 'desc' } 105 | }) 106 | ``` 107 | 108 | This will sort the data based on the math scores in descending order. 109 | 110 | ## Advanced Sorting with Value Parsing 111 | 112 | The `SortOptions` interface now includes a `parser` option, which allows you to prepare and format values before sorting. This is particularly useful when dealing with mixed data types or when you need to apply custom sorting logic. 113 | 114 | ```typescript 115 | export interface SortOptions { 116 | key: string 117 | dir?: 'asc' | 'desc' 118 | parser?: 'number' | 'boolean' | 'string' | ((value: any) => string | number | boolean | null | undefined) 119 | } 120 | ``` 121 | 122 | The `parser` option can be one of the following: 123 | 124 | - A string value: `'number'`, `'boolean'`, or `'string'` for basic type conversion 125 | - A custom function that takes the value and returns a parsed version 126 | 127 | ### Examples 128 | 129 | #### Using Built-in Parsers 130 | 131 | 1. Sorting mixed string and number values as numbers: 132 | 133 | ```ts twoslash 134 | import { query } from '@chronicstone/array-query' 135 | 136 | const data = [ 137 | { id: 1, value: '10' }, 138 | { id: 2, value: 5 }, 139 | { id: 3, value: '15' }, 140 | { id: 4, value: '3' } 141 | ] 142 | 143 | const result = query(data, { 144 | sort: { key: 'value', dir: 'asc', parser: 'number' } 145 | }) 146 | ``` 147 | 148 | Result: 149 | ```ts twoslash 150 | [ 151 | { id: 4, value: '3' }, 152 | { id: 2, value: 5 }, 153 | { id: 1, value: '10' }, 154 | { id: 3, value: '15' } 155 | ] 156 | ``` 157 | 158 | 2. Sorting boolean values: 159 | 160 | ```ts twoslash 161 | import { query } from '@chronicstone/array-query' 162 | 163 | const data = [ 164 | { id: 1, isActive: 'true' }, 165 | { id: 2, isActive: false }, 166 | { id: 3, isActive: 'false' }, 167 | { id: 4, isActive: true } 168 | ] 169 | 170 | const result = query(data, { 171 | sort: { key: 'isActive', dir: 'desc', parser: 'boolean' } 172 | }) 173 | ``` 174 | 175 | Result: 176 | ```ts twoslash 177 | [ 178 | { id: 1, isActive: 'true' }, 179 | { id: 4, isActive: true }, 180 | { id: 2, isActive: false }, 181 | { id: 3, isActive: 'false' } 182 | ] 183 | ``` 184 | 185 | #### Using Custom Parser Functions 186 | 187 | 3. Sorting by date strings: 188 | 189 | ```ts twoslash 190 | import { query } from '@chronicstone/array-query' 191 | 192 | const data = [ 193 | { id: 1, date: '2023-05-15' }, 194 | { id: 2, date: '2023-01-10' }, 195 | { id: 3, date: '2023-12-01' } 196 | ] 197 | 198 | const result = query(data, { 199 | sort: { 200 | key: 'date', 201 | dir: 'asc', 202 | parser: value => new Date(value).getTime() 203 | } 204 | }) 205 | ``` 206 | 207 | Result: 208 | ```ts twoslash 209 | [ 210 | { id: 2, date: '2023-01-10' }, 211 | { id: 1, date: '2023-05-15' }, 212 | { id: 3, date: '2023-12-01' } 213 | ] 214 | ``` 215 | -------------------------------------------------------------------------------- /docs/features/type-safety.md: -------------------------------------------------------------------------------- 1 | # Type-Safe Queries 2 | 3 | ArrayQuery provides type safety for query parameters, ensuring that you can only use valid keys for sorting, searching, and filtering. This feature helps catch errors at compile-time and provides excellent autocompletion support in your IDE. 4 | 5 | ## Sorting 6 | 7 | When specifying the `key` for sorting, you'll get type-safe suggestions based on the properties of your data: 8 | 9 | ```ts twoslash 10 | // @noErrors 11 | import { query } from '@chronicstone/array-query' 12 | 13 | const users = [ 14 | { id: 1, name: 'Alice', age: 30, email: 'alice@example.com' }, 15 | { id: 2, name: 'Bob', age: 25, email: 'bob@example.com' }, 16 | ] 17 | 18 | const result = query(users, { 19 | sort: { key: '' } 20 | // ^| 21 | }) 22 | ``` 23 | 24 | Hovering over the empty string will show a popover with valid options: `"id" | "name" | "age" | "email"`. 25 | 26 | ## Searching 27 | 28 | The `keys` array in the search options is also type-safe: 29 | 30 | ```ts twoslash 31 | // @noErrors 32 | import { query } from '@chronicstone/array-query' 33 | 34 | const products = [ 35 | { id: 1, name: 'Laptop', price: 1000, description: 'Powerful laptop' }, 36 | { id: 2, name: 'Phone', price: 500, description: 'Smart phone' }, 37 | ] 38 | 39 | const result = query(products, { 40 | search: { 41 | value: 'laptop', 42 | keys: [''] 43 | // ^| 44 | } 45 | }) 46 | ``` 47 | 48 | The popover for the empty string will show: `"id" | "name" | "price" | "description"`. 49 | 50 | ## Filtering 51 | 52 | Filter keys are also type-safe, including for nested properties: 53 | 54 | ```ts twoslash 55 | // @noErrors 56 | import { query } from '@chronicstone/array-query' 57 | 58 | const employees = [ 59 | { id: 1, name: 'Alice', department: { name: 'IT', location: 'New York' } }, 60 | { id: 2, name: 'Bob', department: { name: 'HR', location: 'London' } }, 61 | ] 62 | 63 | const result = query(employees, { 64 | filter: [ 65 | { key: '' } 66 | // ^| 67 | ] 68 | }) 69 | ``` 70 | 71 | ## Complex Nested Structures 72 | 73 | ArrayQuery handles complex nested structures with ease: 74 | 75 | ```typescript twoslash 76 | // @noErrors 77 | import { query } from '@chronicstone/array-query' 78 | 79 | const data = [ 80 | { 81 | id: 1, 82 | info: { 83 | personal: { name: 'Alice', age: 30 }, 84 | professional: { title: 'Developer', skills: ['JavaScript', 'TypeScript'] } 85 | }, 86 | contacts: [ 87 | { type: 'email', value: 'alice@example.com' }, 88 | { type: 'phone', value: '123-456-7890' } 89 | ] 90 | } 91 | ] 92 | 93 | const result = query(data, { 94 | sort: { key: '', dir: 'asc' }, 95 | // ^| 96 | }) 97 | ``` 98 | 99 | These type-safe queries ensure that you're always using valid property paths, reducing errors and improving the developer experience when working with ArrayQuery. 100 | -------------------------------------------------------------------------------- /docs/filter-match-modes/array-length.md: -------------------------------------------------------------------------------- 1 | # Array Length Match Mode 2 | 3 | The 'arrayLength' match mode checks the length of an array field against a specified value. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', skills: ['JavaScript', 'TypeScript', 'React'] }, 10 | { id: 2, name: 'Jane', skills: ['Python', 'Django'] }, 11 | { id: 3, name: 'Bob', skills: ['Java', 'Spring', 'Hibernate', 'SQL'] } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'skills', matchMode: 'arrayLength', value: 3 } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 1, name: 'John', skills: ['JavaScript', 'TypeScript', 'React'] } 25 | ] 26 | ``` 27 | 28 | This filter returns all items where the 'skills' array has exactly 3 elements. 29 | 30 | You can also use comparison operators with 'arrayLength': 31 | 32 | ```ts twoslash 33 | import { query } from '@chronicstone/array-query' 34 | 35 | const data = [ 36 | { id: 1, name: 'John', skills: ['JavaScript', 'TypeScript', 'React'] }, 37 | { id: 2, name: 'Jane', skills: ['Python', 'Django'] }, 38 | { id: 3, name: 'Bob', skills: ['Java', 'Spring', 'Hibernate', 'SQL'] } 39 | ] 40 | 41 | const result = query(data, { 42 | filter: [ 43 | { key: 'skills', matchMode: 'arrayLength', value: 2 } 44 | ] 45 | }) 46 | ``` 47 | 48 | Output: 49 | ```ts twoslash 50 | [ 51 | { id: 2, name: 'Jane', skills: ['Python', 'Django'] }, 52 | ] 53 | ``` 54 | 55 | This filter returns all items where the 'skills' array has more than 2 elements. 56 | -------------------------------------------------------------------------------- /docs/filter-match-modes/between.md: -------------------------------------------------------------------------------- 1 | # Between Match Mode 2 | 3 | The 'between' match mode checks if a numeric or date value is within a specified range. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', age: 25 }, 10 | { id: 2, name: 'Jane', age: 30 }, 11 | { id: 3, name: 'Bob', age: 35 } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'age', matchMode: 'between', value: [28, 32] } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 2, name: 'Jane', age: 30 } 25 | ] 26 | ``` 27 | 28 | This filter returns all items where the 'age' is between 28 and 32 (inclusive). 29 | 30 | For date comparisons, you can use the `dateMode` parameter: 31 | 32 | ```ts twoslash 33 | import { query } from '@chronicstone/array-query' 34 | 35 | const data = [ 36 | { id: 1, name: 'John', joinDate: '2023-01-15' }, 37 | { id: 2, name: 'Jane', joinDate: '2023-06-20' }, 38 | { id: 3, name: 'Bob', joinDate: '2023-12-05' } 39 | ] 40 | 41 | const result = query(data, { 42 | filter: [ 43 | { 44 | key: 'joinDate', 45 | matchMode: 'between', 46 | value: ['2023-05-01', '2023-08-31'], 47 | params: { dateMode: true } 48 | } 49 | ] 50 | }) 51 | ``` 52 | 53 | Output: 54 | ```ts twoslash 55 | [ 56 | { id: 2, name: 'Jane', joinDate: '2023-06-20' } 57 | ] 58 | ``` 59 | 60 | This filter returns all items where the 'joinDate' is between May 1, 2023 and August 31, 2023. 61 | -------------------------------------------------------------------------------- /docs/filter-match-modes/contains.md: -------------------------------------------------------------------------------- 1 | # Contains Match Mode 2 | 3 | The 'contains' match mode checks if a string value contains the specified substring. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John Doe', email: 'john@example.com' }, 10 | { id: 2, name: 'Jane Smith', email: 'jane@example.com' }, 11 | { id: 3, name: 'Bob Johnson', email: 'bob@example.com' } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'name', matchMode: 'contains', value: 'oh' } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 1, name: 'John Doe', email: 'john@example.com' } 25 | ] 26 | ``` 27 | 28 | In this example, the filter returns all items where the 'name' field contains the substring 'oh'. 29 | -------------------------------------------------------------------------------- /docs/filter-match-modes/equals.md: -------------------------------------------------------------------------------- 1 | # Equals Match Mode 2 | 3 | The 'equals' match mode checks for exact equality between the field value and the specified value. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', status: 'active' }, 10 | { id: 2, name: 'Jane', status: 'inactive' }, 11 | { id: 3, name: 'Bob', status: 'active' } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'status', matchMode: 'equals', value: 'active' } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 1, name: 'John', status: 'active' }, 25 | { id: 3, name: 'Bob', status: 'active' } 26 | ] 27 | ``` 28 | 29 | This filter returns all items where the 'status' is exactly equal to 'active'. 30 | -------------------------------------------------------------------------------- /docs/filter-match-modes/exists.md: -------------------------------------------------------------------------------- 1 | # Exists Match Mode 2 | 3 | The 'exists' match mode checks if a specified field exists (or doesn't exist) in the data objects. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', age: 25 }, 10 | { id: 2, name: 'Jane' }, 11 | { id: 3, name: 'Bob', age: 35 } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'age', matchMode: 'exists', value: true } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 1, name: 'John', age: 25 }, 25 | { id: 3, name: 'Bob', age: 35 } 26 | ] 27 | ``` 28 | 29 | This filter returns all items where the 'age' field exists. 30 | 31 | To find items where a field doesn't exist: 32 | 33 | ```ts twoslash 34 | import { query } from '@chronicstone/array-query' 35 | 36 | const data = [ 37 | { id: 1, name: 'John', age: 25 }, 38 | { id: 2, name: 'Jane' }, 39 | { id: 3, name: 'Bob', age: 35 } 40 | ] 41 | 42 | const result = query(data, { 43 | filter: [ 44 | { key: 'age', matchMode: 'exists', value: false } 45 | ] 46 | }) 47 | ``` 48 | 49 | Output: 50 | ```ts twoslash 51 | [ 52 | { id: 2, name: 'Jane' } 53 | ] 54 | ``` 55 | 56 | This filter returns all items where the 'age' field does not exist. 57 | -------------------------------------------------------------------------------- /docs/filter-match-modes/greater-than-or-equal.md: -------------------------------------------------------------------------------- 1 | # Greater Than or Equal Match Mode 2 | 3 | The 'greaterThanOrEqual' match mode checks if a numeric or date value is greater than or equal to the specified value. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', age: 25 }, 10 | { id: 2, name: 'Jane', age: 30 }, 11 | { id: 3, name: 'Bob', age: 35 } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'age', matchMode: 'greaterThanOrEqual', value: 30 } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 2, name: 'Jane', age: 30 }, 25 | { id: 3, name: 'Bob', age: 35 } 26 | ] 27 | ``` 28 | 29 | This filter returns all items where the 'age' is greater than or equal to 30. 30 | 31 | For date comparisons, you can use the `dateMode` parameter: 32 | 33 | ```ts twoslash 34 | import { query } from '@chronicstone/array-query' 35 | 36 | const data = [ 37 | { id: 1, name: 'John', joinDate: '2023-01-15' }, 38 | { id: 2, name: 'Jane', joinDate: '2023-06-20' }, 39 | { id: 3, name: 'Bob', joinDate: '2023-12-05' } 40 | ] 41 | 42 | const result = query(data, { 43 | filter: [ 44 | { 45 | key: 'joinDate', 46 | matchMode: 'greaterThanOrEqual', 47 | value: '2023-06-20', 48 | params: { dateMode: true } 49 | } 50 | ] 51 | }) 52 | ``` 53 | 54 | Output: 55 | ```ts twoslash 56 | [ 57 | { id: 2, name: 'Jane', joinDate: '2023-06-20' }, 58 | { id: 3, name: 'Bob', joinDate: '2023-12-05' } 59 | ] 60 | ``` 61 | 62 | This filter returns all items where the 'joinDate' is on or after June 20, 2023. 63 | -------------------------------------------------------------------------------- /docs/filter-match-modes/greater-than.md: -------------------------------------------------------------------------------- 1 | # Greater Than Match Mode 2 | 3 | The 'greaterThan' match mode checks if a numeric or date value is greater than the specified value. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', age: 25 }, 10 | { id: 2, name: 'Jane', age: 30 }, 11 | { id: 3, name: 'Bob', age: 35 } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'age', matchMode: 'greaterThan', value: 28 } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 2, name: 'Jane', age: 30 }, 25 | { id: 3, name: 'Bob', age: 35 } 26 | ] 27 | ``` 28 | 29 | This filter returns all items where the 'age' is greater than 28. 30 | 31 | For date comparisons, you can use the `dateMode` parameter: 32 | 33 | ```ts twoslash 34 | import { query } from '@chronicstone/array-query' 35 | 36 | const data = [ 37 | { id: 1, name: 'John', joinDate: '2023-01-15' }, 38 | { id: 2, name: 'Jane', joinDate: '2023-06-20' }, 39 | { id: 3, name: 'Bob', joinDate: '2023-12-05' } 40 | ] 41 | 42 | const result = query(data, { 43 | filter: [ 44 | { 45 | key: 'joinDate', 46 | matchMode: 'greaterThan', 47 | value: '2023-06-01', 48 | params: { dateMode: true } 49 | } 50 | ] 51 | }) 52 | ``` 53 | 54 | Output: 55 | ```ts twoslash 56 | [ 57 | { id: 2, name: 'Jane', joinDate: '2023-06-20' }, 58 | { id: 3, name: 'Bob', joinDate: '2023-12-05' } 59 | ] 60 | ``` 61 | 62 | This filter returns all items where the 'joinDate' is after June 1, 2023. 63 | -------------------------------------------------------------------------------- /docs/filter-match-modes/less-than-or-equal.md: -------------------------------------------------------------------------------- 1 | # Less Than or Equal Match Mode 2 | 3 | The 'lessThanOrEqual' match mode checks if a numeric or date value is less than or equal to the specified value. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', age: 25 }, 10 | { id: 2, name: 'Jane', age: 30 }, 11 | { id: 3, name: 'Bob', age: 35 } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'age', matchMode: 'lessThanOrEqual', value: 30 } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 1, name: 'John', age: 25 }, 25 | { id: 2, name: 'Jane', age: 30 } 26 | ] 27 | ``` 28 | 29 | This filter returns all items where the 'age' is less than or equal to 30. 30 | 31 | For date comparisons, you can use the `dateMode` parameter: 32 | 33 | ```ts twoslash 34 | import { query } from '@chronicstone/array-query' 35 | 36 | const data = [ 37 | { id: 1, name: 'John', joinDate: '2023-01-15' }, 38 | { id: 2, name: 'Jane', joinDate: '2023-06-20' }, 39 | { id: 3, name: 'Bob', joinDate: '2023-12-05' } 40 | ] 41 | 42 | const result = query(data, { 43 | filter: [ 44 | { 45 | key: 'joinDate', 46 | matchMode: 'lessThanOrEqual', 47 | value: '2023-06-20', 48 | params: { dateMode: true } 49 | } 50 | ] 51 | }) 52 | ``` 53 | 54 | Output: 55 | ```ts twoslash 56 | [ 57 | { id: 1, name: 'John', joinDate: '2023-01-15' }, 58 | { id: 2, name: 'Jane', joinDate: '2023-06-20' } 59 | ] 60 | ``` 61 | 62 | This filter returns all items where the 'joinDate' is on or before June 20, 2023. 63 | -------------------------------------------------------------------------------- /docs/filter-match-modes/less-than.md: -------------------------------------------------------------------------------- 1 | # Less Than Match Mode 2 | 3 | The 'lessThan' match mode checks if a numeric or date value is less than the specified value. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', age: 25 }, 10 | { id: 2, name: 'Jane', age: 30 }, 11 | { id: 3, name: 'Bob', age: 35 } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'age', matchMode: 'lessThan', value: 30 } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 1, name: 'John', age: 25 } 25 | ] 26 | ``` 27 | 28 | This filter returns all items where the 'age' is less than 30. 29 | 30 | For date comparisons, you can use the `dateMode` parameter: 31 | 32 | ```ts twoslash 33 | import { query } from '@chronicstone/array-query' 34 | 35 | const data = [ 36 | { id: 1, name: 'John', joinDate: '2023-01-15' }, 37 | { id: 2, name: 'Jane', joinDate: '2023-06-20' }, 38 | { id: 3, name: 'Bob', joinDate: '2023-12-05' } 39 | ] 40 | 41 | const result = query(data, { 42 | filter: [ 43 | { 44 | key: 'joinDate', 45 | matchMode: 'lessThan', 46 | value: '2023-06-01', 47 | params: { dateMode: true } 48 | } 49 | ] 50 | }) 51 | ``` 52 | 53 | Output: 54 | ```ts twoslash 55 | [ 56 | { id: 1, name: 'John', joinDate: '2023-01-15' } 57 | ] 58 | ``` 59 | 60 | This filter returns all items where the 'joinDate' is before June 1, 2023. 61 | -------------------------------------------------------------------------------- /docs/filter-match-modes/not-equals.md: -------------------------------------------------------------------------------- 1 | # Not Equals Match Mode 2 | 3 | The 'notEquals' match mode checks for inequality between the field value and the specified value. 4 | 5 | ```ts twoslash 6 | import { query } from '@chronicstone/array-query' 7 | 8 | const data = [ 9 | { id: 1, name: 'John', status: 'active' }, 10 | { id: 2, name: 'Jane', status: 'inactive' }, 11 | { id: 3, name: 'Bob', status: 'active' } 12 | ] 13 | 14 | const result = query(data, { 15 | filter: [ 16 | { key: 'status', matchMode: 'notEquals', value: 'active' } 17 | ] 18 | }) 19 | ``` 20 | 21 | Output: 22 | ```ts twoslash 23 | [ 24 | { id: 2, name: 'Jane', status: 'inactive' } 25 | ] 26 | ``` 27 | 28 | This filter returns all items where the 'status' is not equal to 'active'. 29 | -------------------------------------------------------------------------------- /docs/filter-match-modes/object-match.md: -------------------------------------------------------------------------------- 1 | # Object Match Mode 2 | 3 | The 'objectMatch' mode provides powerful nested object filtering capabilities, including the ability to filter arrays of objects. 4 | 5 | ## Basic Usage 6 | 7 | ```ts twoslash 8 | import { query } from '@chronicstone/array-query' 9 | 10 | const data = [ 11 | { id: 1, name: 'John', account: { type: 'premium', status: 'active', details: { level: 3 } } }, 12 | { id: 2, name: 'Jane', account: { type: 'basic', status: 'inactive', details: { level: 1 } } }, 13 | { id: 3, name: 'Bob', account: { type: 'premium', status: 'inactive', details: { level: 2 } } } 14 | ] 15 | 16 | const result = query(data, { 17 | filter: [ 18 | { 19 | key: 'account', 20 | matchMode: 'objectMatch', 21 | value: { type: 'premium', status: 'active' }, 22 | params: { 23 | operator: 'AND', 24 | properties: [ 25 | { key: 'type', matchMode: 'equals' }, 26 | { key: 'status', matchMode: 'equals' } 27 | ] 28 | } 29 | } 30 | ] 31 | }) 32 | ``` 33 | 34 | Output: 35 | ```ts twoslash 36 | [ 37 | { id: 1, name: 'John', account: { type: 'premium', status: 'active', details: { level: 3 } } } 38 | ] 39 | ``` 40 | 41 | This example demonstrates the basic usage. It filters objects where the 'account' has both 'type' equal to 'premium' AND 'status' equal to 'active'. 42 | 43 | ## Using Different Match Modes 44 | 45 | You can use different match modes for each property: 46 | 47 | ```ts twoslash 48 | import { query } from '@chronicstone/array-query' 49 | 50 | const data = [ 51 | { id: 1, name: 'John', account: { type: 'premium', status: 'active', details: { level: 3 } } }, 52 | { id: 2, name: 'Jane', account: { type: 'basic', status: 'inactive', details: { level: 1 } } }, 53 | { id: 3, name: 'Bob', account: { type: 'premium', status: 'inactive', details: { level: 2 } } } 54 | ] 55 | 56 | const result = query(data, { 57 | filter: [ 58 | { 59 | key: 'account', 60 | matchMode: 'objectMatch', 61 | value: { type: 'premium', details: { level: 2 } }, 62 | params: { 63 | operator: 'AND', 64 | properties: [ 65 | { key: 'type', matchMode: 'equals' }, 66 | { key: 'details.level', matchMode: 'greaterThan' } 67 | ] 68 | } 69 | } 70 | ] 71 | }) 72 | ``` 73 | 74 | Output: 75 | ```ts twoslash 76 | [ 77 | { id: 1, name: 'John', account: { type: 'premium', status: 'active', details: { level: 3 } } } 78 | ] 79 | ``` 80 | 81 | This example uses 'equals' for 'type' and 'greaterThan' for 'details.level'. 82 | 83 | ## Filtering Arrays of Objects 84 | 85 | ### Case 1: ALL objects in the array match ALL conditions 86 | 87 | ```ts twoslash 88 | import { query } from '@chronicstone/array-query' 89 | 90 | const data = [ 91 | { id: 1, name: 'John', orders: [{ id: 1, total: 100 }, { id: 2, total: 200 }] }, 92 | { id: 2, name: 'Jane', orders: [{ id: 3, total: 150 }, { id: 4, total: 300 }] }, 93 | { id: 3, name: 'Bob', orders: [{ id: 5, total: 50 }, { id: 6, total: 250 }] } 94 | ] 95 | 96 | const result = query(data, { 97 | filter: [ 98 | { 99 | key: 'orders', 100 | matchMode: 'objectMatch', 101 | value: { total: 50 }, 102 | operator: 'AND', 103 | params: { 104 | operator: 'AND', 105 | properties: [ 106 | { key: 'total', matchMode: 'greaterThan' } 107 | ] 108 | } 109 | } 110 | ] 111 | }) 112 | ``` 113 | 114 | Output: 115 | ```ts twoslash 116 | [ 117 | { id: 1, name: 'John', orders: [{ id: 1, total: 100 }, { id: 2, total: 200 }] }, 118 | { id: 2, name: 'Jane', orders: [{ id: 3, total: 150 }, { id: 4, total: 300 }] } 119 | ] 120 | ``` 121 | 122 | This example returns items where ALL orders have a total greater than 50. 123 | 124 | ### Case 2: ALL objects in the array match SOME of the conditions 125 | 126 | ```ts twoslash 127 | import { query } from '@chronicstone/array-query' 128 | 129 | const data = [ 130 | { id: 1, name: 'John', orders: [{ id: 1, total: 100, status: 'completed' }, { id: 2, total: 200, status: 'pending' }] }, 131 | { id: 2, name: 'Jane', orders: [{ id: 3, total: 150, status: 'completed' }, { id: 4, total: 300, status: 'completed' }] }, 132 | { id: 3, name: 'Bob', orders: [{ id: 5, total: 50, status: 'pending' }, { id: 6, total: 250, status: 'completed' }] } 133 | ] 134 | 135 | const result = query(data, { 136 | filter: [ 137 | { 138 | key: 'orders', 139 | matchMode: 'objectMatch', 140 | value: { total: 100, status: 'completed' }, 141 | operator: 'AND', 142 | params: { 143 | operator: 'OR', 144 | properties: [ 145 | { key: 'total', matchMode: 'greaterThan' }, 146 | { key: 'status', matchMode: 'equals' } 147 | ] 148 | } 149 | } 150 | ] 151 | }) 152 | ``` 153 | 154 | Output: 155 | ```ts twoslash 156 | [ 157 | { id: 1, name: 'John', orders: [{ id: 1, total: 100, status: 'completed' }, { id: 2, total: 200, status: 'pending' }] }, 158 | { id: 2, name: 'Jane', orders: [{ id: 3, total: 150, status: 'completed' }, { id: 4, total: 300, status: 'completed' }] } 159 | ] 160 | ``` 161 | 162 | This example returns items where ALL orders either have a total greater than 100 OR have a status of 'completed'. 163 | 164 | ### Case 3: SOME objects in the array match ALL conditions 165 | 166 | ```ts twoslash 167 | import { query } from '@chronicstone/array-query' 168 | 169 | const data = [ 170 | { id: 1, name: 'John', orders: [{ id: 1, total: 100, status: 'completed' }, { id: 2, total: 200, status: 'pending' }] }, 171 | { id: 2, name: 'Jane', orders: [{ id: 3, total: 150, status: 'completed' }, { id: 4, total: 300, status: 'completed' }] }, 172 | { id: 3, name: 'Bob', orders: [{ id: 5, total: 50, status: 'pending' }, { id: 6, total: 250, status: 'completed' }] } 173 | ] 174 | 175 | const result = query(data, { 176 | filter: [ 177 | { 178 | key: 'orders', 179 | matchMode: 'objectMatch', 180 | value: { total: 200, status: 'completed' }, 181 | operator: 'OR', 182 | params: { 183 | operator: 'AND', 184 | properties: [ 185 | { key: 'total', matchMode: 'greaterThan' }, 186 | { key: 'status', matchMode: 'equals' } 187 | ] 188 | } 189 | } 190 | ] 191 | }) 192 | ``` 193 | 194 | Output: 195 | ```ts twoslash 196 | [ 197 | { id: 2, name: 'Jane', orders: [{ id: 3, total: 150, status: 'completed' }, { id: 4, total: 300, status: 'completed' }] } 198 | ] 199 | ``` 200 | 201 | This example returns items where at least one order has a total greater than 200 AND a status of 'completed'. 202 | 203 | ## Using applyAtRoot 204 | 205 | The `applyAtRoot` option allows you to apply the filter to the root object instead of a nested property: 206 | 207 | ```ts twoslash 208 | // @errors: 2322 209 | import { query } from '@chronicstone/array-query' 210 | 211 | const data = [ 212 | { id: 1, name: 'John', age: 30, status: 'active' }, 213 | { id: 2, name: 'Jane', age: 25, status: 'inactive' }, 214 | { id: 3, name: 'Bob', age: 35, status: 'active' } 215 | ] 216 | 217 | const result = query(data, { 218 | filter: [ 219 | { 220 | key: '', 221 | matchMode: 'objectMatch', 222 | value: { age: 30, status: 'active' }, 223 | params: { 224 | operator: 'AND', 225 | properties: [ 226 | { key: 'age', matchMode: 'greaterThanOrEqual' }, 227 | { key: 'status', matchMode: 'equals' } 228 | ], 229 | applyAtRoot: true 230 | } 231 | } 232 | ] 233 | }) 234 | ``` 235 | 236 | Output: 237 | ```ts twoslash 238 | [ 239 | { id: 1, name: 'John', age: 30, status: 'active' }, 240 | { id: 3, name: 'Bob', age: 35, status: 'active' } 241 | ] 242 | ``` 243 | 244 | This example applies the filter directly to the root objects in the array, matching those with age >= 30 and status 'active'. 245 | -------------------------------------------------------------------------------- /docs/filter-match-modes/regex.md: -------------------------------------------------------------------------------- 1 | # Regex Match Mode 2 | 3 | The regex match mode allows you to filter data using regular expressions. This powerful feature enables complex pattern matching across your dataset. 4 | 5 | ## Basic Usage 6 | 7 | To use the regex match mode, set the `matchMode` to `'regex'` in your filter configuration: 8 | 9 | ```ts twoslash 10 | import { query } from '@chronicstone/array-query' 11 | 12 | const data = [ 13 | { id: 1, email: 'john@example.com' }, 14 | { id: 2, email: 'jane@test.com' }, 15 | { id: 3, email: 'bob@example.org' } 16 | ] 17 | 18 | const result = query(data, { 19 | filter: [ 20 | { key: 'email', matchMode: 'regex', value: '@example\\.(com|org)' } 21 | ] 22 | }) 23 | ``` 24 | 25 | This query will return all items where the email matches the pattern `@example.com` or `@example.org`. 26 | 27 | ## Flags 28 | 29 | You can modify the behavior of the regex matching by providing flags. Flags are specified using the `params` option: 30 | 31 | ```ts twoslash 32 | import { query } from '@chronicstone/array-query' 33 | 34 | const data = [ 35 | { id: 1, name: 'John Doe' }, 36 | { id: 2, name: 'jane smith' }, 37 | { id: 3, name: 'Bob Johnson' } 38 | ] 39 | 40 | const result = query(data, { 41 | filter: [ 42 | { 43 | key: 'name', 44 | matchMode: 'regex', 45 | value: '^j.*', 46 | params: { flags: 'i' } 47 | } 48 | ] 49 | }) 50 | ``` 51 | 52 | ## Raw RegExp 53 | 54 | Instead of defining the regex pattern as a string, you can also pass a regular expression object directly: 55 | 56 | ```ts twoslash 57 | import { query } from '@chronicstone/array-query' 58 | 59 | const data = [ 60 | { id: 1, name: 'John Doe' }, 61 | { id: 2, name: 'jane smith' }, 62 | { id: 3, name: 'Bob Johnson' } 63 | ] 64 | 65 | const result = query(data, { 66 | filter: [ 67 | { 68 | key: 'name', 69 | matchMode: 'regex', 70 | value: /^j.*/i 71 | } 72 | ] 73 | }) 74 | ``` 75 | 76 | For a detailed explanation of available flags and their usage, please refer to the [MDN documentation on Regular Expression Flags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#advanced_searching_with_flags). 77 | 78 | ## Examples 79 | 80 | ### Case-insensitive Matching 81 | 82 | ```ts twoslash 83 | import { query } from '@chronicstone/array-query' 84 | 85 | const data = [ 86 | { id: 1, name: 'John Doe' }, 87 | { id: 2, name: 'jane smith' }, 88 | { id: 3, name: 'Bob Johnson' } 89 | ] 90 | 91 | const result = query(data, { 92 | filter: [ 93 | { 94 | key: 'name', 95 | matchMode: 'regex', 96 | value: '^j.*', 97 | params: { flags: 'i' } 98 | } 99 | ] 100 | }) 101 | ``` 102 | 103 | This will match both "John Doe" and "jane smith". 104 | 105 | ### Multi-line Matching 106 | 107 | ```ts twoslash 108 | import { query } from '@chronicstone/array-query' 109 | 110 | const data = [ 111 | { id: 1, description: 'First line\nSecond line' }, 112 | { id: 2, description: 'One line only' }, 113 | { id: 3, description: 'First line\nLast line' } 114 | ] 115 | 116 | const result = query(data, { 117 | filter: [ 118 | { 119 | key: 'description', 120 | matchMode: 'regex', 121 | value: '^Last', 122 | params: { flags: 'm' } 123 | } 124 | ] 125 | }) 126 | ``` 127 | 128 | This will match the item with id 3, where "Last" appears at the start of a line. 129 | 130 | ## Combining with Other Filters 131 | 132 | You can combine regex filters with other filter types: 133 | 134 | ```typescript 135 | const result = query(data, { 136 | filter: [ 137 | { 138 | key: 'name', 139 | matchMode: 'regex', 140 | value: '^j.*', 141 | params: { flags: 'i' } 142 | }, 143 | { key: 'age', matchMode: 'greaterThan', value: 28 } 144 | ] 145 | }) 146 | ``` 147 | 148 | This will find items where the name starts with 'j' (case-insensitive) AND the age is greater than 28. 149 | 150 | ## Performance Considerations 151 | 152 | While regex matching is powerful, it can be computationally expensive, especially on large datasets or with complex patterns. Use it judiciously and consider performance implications in your use case. 153 | 154 | ## Escaping Special Characters 155 | 156 | Remember to properly escape special regex characters in your pattern strings. For example, to match a literal period, use `\\.` instead of `.`. 157 | 158 | ## Further Reading 159 | 160 | For more information on JavaScript regular expressions, including pattern syntax and usage, refer to the [MDN Regular Expressions Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions). 161 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Installing ArrayQuery in your project is straightforward. You can use your preferred package manager to add it to your dependencies. 4 | 5 | ## Installing ArrayQuery 6 | 7 | Choose your preferred package manager from the options below: 8 | 9 | ::: code-group 10 | ```sh [npm] 11 | npm install @chronicstone/array-query 12 | ``` 13 | 14 | ```sh [yarn] 15 | yarn add @chronicstone/array-query 16 | ``` 17 | 18 | ```sh [pnpm] 19 | pnpm add @chronicstone/array-query 20 | ``` 21 | 22 | ```sh [bun] 23 | bun add @chronicstone/array-query 24 | ``` 25 | ::: 26 | 27 | ## Importing ArrayQuery 28 | 29 | Once installed, you can import the `query` function from ArrayQuery in your TypeScript or JavaScript files: 30 | 31 | ```ts twoslash 32 | import { query } from '@chronicstone/array-query' 33 | ``` 34 | 35 | The package also exports all its types : 36 | 37 | ```ts twoslash 38 | import type { FilterMatchMode, QueryFilter, QueryParams } from '@chronicstone/array-query' 39 | ``` 40 | 41 | ## Use the library 42 | 43 | You can now use the `query` function to perform various operations on your array data. 44 | 45 | ```ts twoslash 46 | import { query } from '@chronicstone/array-query' 47 | 48 | const data = [ 49 | { id: 1, name: 'John Doe', age: 30 }, 50 | { id: 2, name: 'Jane Smith', age: 25 }, 51 | { id: 3, name: 'Bob Johnson', age: 35 } 52 | ] 53 | 54 | const result = query(data, { 55 | limit: 10, 56 | sort: { key: 'age', order: 'asc' }, 57 | }) 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/getting-started/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction to ArrayQuery 2 | 3 | ArrayQuery is a powerful TypeScript library designed to bring Type-Safe ORM-like querying capabilities to local arrays. It provides an intuitive and efficient way to manipulate and retrieve data from arrays, offering functionality similar to database querying but for in-memory JavaScript/TypeScript arrays. 4 | 5 | ## What is ArrayQuery? 6 | 7 | ArrayQuery is a lightweight, yet feature-rich library that allows developers to perform complex operations on arrays of objects with ease. It's particularly useful when working with large datasets in memory, or when you need to implement advanced filtering, sorting, and pagination capabilities in front-end applications. 8 | 9 | ## Key Features 10 | 11 | ArrayQuery offers a range of powerful features to enhance your data manipulation capabilities: 12 | 13 | 1. **Pagination**: Easily paginate through large datasets, specifying the number of items per page and the current page number. 14 | 15 | 2. **Full-Text Searching**: Perform comprehensive searches across multiple fields in your data, allowing for flexible and powerful search functionality. 16 | 17 | 3. **Advanced Filtering**: Apply complex filters with various match modes for precise data retrieval. From simple equality checks to complex object matching, ArrayQuery provides a wide range of filtering options. 18 | 19 | 4. **Flexible Sorting**: Order your results based on any field, with support for both ascending and descending orders. 20 | 21 | 5. **Type-Safe Operations**: Leverage TypeScript for type-safe querying and manipulation of your data, reducing errors and improving developer experience. 22 | 23 | 6. **Complex Object Filtering**: Filter nested objects and arrays with ease using advanced match modes, allowing for intricate data querying. 24 | 25 | 8. **Lightweight and Fast**: Optimized for performance, ArrayQuery adds minimal overhead to your applications while providing powerful functionality. 26 | 27 | ## When to Use ArrayQuery 28 | 29 | ArrayQuery is ideal for scenarios where you need to: 30 | 31 | - Implement complex filtering and sorting in client-side applications 32 | - Paginate large datasets efficiently 33 | - Perform full-text searches across multiple fields 34 | - Handle complex filtering and sorting 35 | - Implement data table functionalities in web applications 36 | 37 | Whether you're building a data-heavy dashboard, implementing an advanced search feature, or simply need more control over your array manipulations, ArrayQuery provides the tools you need to work with your data effectively and efficiently. 38 | 39 | In the following sections, we'll dive deeper into each feature and provide examples of how to use ArrayQuery in your projects. 40 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: Type-Safe ORM-like Querying for Local Arrays 5 | hero: 6 | name: "ArrayQuery" 7 | text: "Type-Safe ORM-like Querying for Arrays" 8 | tagline: "Manipulate and retrieve data from arrays with ease using a familiar and intuitive API." 9 | image: 10 | src: /images/logo.svg 11 | alt: ArrayQuery 12 | style: 13 | margin-left: 50px 14 | actions: 15 | - theme: brand 16 | text: Get Started 17 | link: /getting-started/introduction 18 | - theme: alt 19 | text: View on GitHub 20 | link: https://github.com/ChronicStone/array-ql 21 | 22 | features: 23 | - title: Type-Safe Querying 24 | details: Enjoy 100% type-safe query configurations with keys inferred from your dataset, enhancing developer experience and reducing errors. 25 | icon: "🛡️" 26 | - title: Powerful Pagination 27 | details: Effortlessly navigate through large datasets with built-in, performant pagination support. 28 | icon: "📄" 29 | - title: Advanced Filtering 30 | details: Apply complex, multi-level filters with various match modes for precise and flexible data retrieval. 31 | icon: "🧭" 32 | - title: Full-Text Search 33 | details: Perform lightning-fast, comprehensive searches across multiple fields in your data. 34 | icon: "🔎" 35 | - title: Zero Dependencies 36 | details: Benefit from a lightweight, bloat-free library that won't burden your project with unnecessary code. 37 | icon: "🪶" 38 | - title: Flexible Sorting 39 | details: Order results based on multiple fields, with support for custom sorting logic and directional control. 40 | icon: "🔢" 41 | - title: Optimized Performance 42 | details: Process millions of rows in milliseconds, with speed that scales for even the largest datasets. 43 | icon: "⚡" 44 | - title: Intuitive API 45 | details: Enjoy a clean, elegant API that makes complex data operations simple and readable. 46 | icon: "🧩" 47 | 48 | footer: 49 | message: "Licensed under the MIT License. Created by [Your Name]. Extensible and customizable for developers." 50 | --- 51 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChronicStone/array-ql/fea673b4ee20e8ad83b896a2fb1d19be4e8c358c/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 10 | 12 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu() 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chronicstone/array-query", 3 | "type": "module", 4 | "version": "1.0.5", 5 | "description": "A simple and lightweight array query library", 6 | "author": "Cyprien THAO ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/chronicstone/array-ql#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/chronicstone/array-ql.git" 12 | }, 13 | "bugs": "https://github.com/chronicstone/array-ql/issues", 14 | "keywords": [], 15 | "sideEffects": false, 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "import": "./dist/index.mjs", 20 | "require": "./dist/index.cjs" 21 | } 22 | }, 23 | "main": "./dist/index.mjs", 24 | "module": "./dist/index.mjs", 25 | "types": "./dist/index.d.ts", 26 | "typesVersions": { 27 | "*": { 28 | "*": [ 29 | "./dist/*", 30 | "./dist/index.d.ts" 31 | ] 32 | } 33 | }, 34 | "files": [ 35 | "dist" 36 | ], 37 | "scripts": { 38 | "build": "unbuild", 39 | "dev": "unbuild --stub", 40 | "lint": "eslint .", 41 | "prepublishOnly": "nr build", 42 | "release": "bumpp && npm publish", 43 | "start": "esno src/index.ts", 44 | "test": "vitest", 45 | "typecheck": "tsc --noEmit", 46 | "prepare": "simple-git-hooks", 47 | "docs:dev": "vitepress dev docs", 48 | "docs:build": "vitepress build docs", 49 | "docs:preview": "vitepress preview docs" 50 | }, 51 | "devDependencies": { 52 | "@antfu/eslint-config": "^2.22.0-beta.2", 53 | "@antfu/ni": "^0.21.12", 54 | "@antfu/utils": "^0.7.10", 55 | "@chronicstone/array-query": "0.2.6", 56 | "@faker-js/faker": "^8.4.1", 57 | "@shikijs/vitepress-twoslash": "^1.10.3", 58 | "@types/node": "^20.14.10", 59 | "bumpp": "^9.4.1", 60 | "eslint": "^9.6.0", 61 | "esno": "^4.7.0", 62 | "lint-staged": "^15.2.7", 63 | "markdown-it-container": "^4.0.0", 64 | "pnpm": "^9.4.0", 65 | "rimraf": "^5.0.8", 66 | "simple-git-hooks": "^2.11.1", 67 | "typescript": "^5.5.3", 68 | "unbuild": "^2.0.0", 69 | "unocss": "^0.61.5", 70 | "vite": "^5.3.3", 71 | "vitepress": "^1.3.1", 72 | "vitepress-plugin-nprogress": "^0.0.4", 73 | "vitest": "^1.6.0" 74 | }, 75 | "simple-git-hooks": { 76 | "pre-commit": "pnpm lint-staged" 77 | }, 78 | "lint-staged": { 79 | "*": "eslint --fix" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - docs 4 | - packages/* 5 | - examples/* 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type * from './types' 2 | export { query } from './query' 3 | -------------------------------------------------------------------------------- /src/query.ts: -------------------------------------------------------------------------------- 1 | import type { FilterOptions, GenericObject, QueryFilter, QueryFilterGroup, QueryParams, QueryResult } from './types' 2 | import { getObjectProperty, getOperator, processFilterWithLookup, processSearchQuery } from './utils' 3 | 4 | export function query>( 5 | data: T[], 6 | params: P, 7 | ): QueryResult { 8 | let result = lazyQuery(data, params) 9 | result = lazySortedQuery(result, params.sort) 10 | return paginateQuery(result, params) 11 | } 12 | 13 | function* lazyQuery(data: T[], params: QueryParams): Generator { 14 | for (const item of data) { 15 | if (matchesSearchAndFilters(item, params)) { 16 | yield item 17 | } 18 | } 19 | } 20 | 21 | function matchesSearchAndFilters(item: T, params: QueryParams): boolean { 22 | return matchesSearch(item, params.search) && matchesFilters(item, params.filter) 23 | } 24 | 25 | function matchesSearch(item: T, search?: QueryParams['search']): boolean { 26 | if (!search || !search.value) 27 | return true 28 | 29 | return search.keys.some((key) => { 30 | const field = typeof key === 'string' ? key : key.key 31 | const caseSensitive = typeof key === 'string' ? (search.caseSensitive ?? false) : key.caseSensitive ?? false 32 | return processSearchQuery({ key: field, caseSensitive, object: item, value: search.value }) 33 | }) 34 | } 35 | 36 | function matchesFilters(item: T, filters?: FilterOptions): boolean { 37 | const _filters = ((typeof filters === 'object' && ('groups' in filters) ? filters.groups : filters) ?? []) 38 | .filter(filter => filter.condition?.() ?? true) 39 | if (!_filters || _filters.length === 0) 40 | return true 41 | const isGroup = _filters.every(filter => 'filters' in filter) 42 | const groupOperator = getOperator(typeof filters === 'object' && 'operator' in filters ? filters.operator : 'OR') 43 | const method = isGroup ? (groupOperator === 'AND' ? 'every' : 'some') : 'every' 44 | return _filters[method]((group: QueryFilter | QueryFilterGroup) => { 45 | const groupFilters = ('filters' in group ? group.filters : [group]) 46 | .filter(filter => filter.condition?.() ?? true) 47 | if (groupFilters.length === 0) 48 | return true 49 | 50 | const op = 'filters' in group ? group.operator : 'OR' 51 | return groupFilters[op === 'AND' ? 'every' : 'some']((filter: QueryFilter) => { 52 | const value = getObjectProperty(item, filter.key) 53 | const operator = getOperator(filter.operator) 54 | const params = (!('params' in filter) ? null : typeof filter.params === 'function' ? filter.params(filter.value) : filter.params) ?? null 55 | return processFilterWithLookup({ 56 | type: filter.matchMode, 57 | params, 58 | operator, 59 | value, 60 | filter: filter.value, 61 | }) 62 | }) 63 | }) 64 | } 65 | 66 | function* lazySortedQuery( 67 | data: Iterable, 68 | sortOptions?: QueryParams['sort'], 69 | ): Generator { 70 | if (!sortOptions) { 71 | yield * data 72 | return 73 | } 74 | 75 | const sortArray = Array.isArray(sortOptions) ? sortOptions : [sortOptions] 76 | const buffer: T[] = [] 77 | const compare = (a: T, b: T) => { 78 | for (const { key, dir, parser } of sortArray) { 79 | const parserHandler = typeof parser === 'function' 80 | ? parser 81 | : (v: any) => 82 | parser === 'number' 83 | ? Number(v) 84 | : parser === 'boolean' 85 | ? Boolean(v) 86 | : parser === 'string' ? String(v) : v 87 | const aParsed = parserHandler(getObjectProperty(a, key)) ?? null 88 | const bParsed = parserHandler(getObjectProperty(b, key)) ?? null 89 | if (aParsed !== bParsed) { 90 | const comparison = (aParsed < bParsed) ? -1 : 1 91 | return dir === 'asc' ? comparison : -comparison 92 | } 93 | } 94 | return 0 95 | } 96 | 97 | for (const item of data) { 98 | buffer.push(item) 99 | if (buffer.length >= 1000) 100 | break 101 | } 102 | 103 | buffer.sort(compare) 104 | 105 | while (buffer.length > 0) { 106 | yield buffer[0] 107 | buffer.shift() 108 | 109 | for (const item of data) { 110 | let insertIndex = buffer.findIndex(bufferItem => compare(item, bufferItem) < 0) 111 | if (insertIndex === -1) { 112 | insertIndex = buffer.length 113 | } 114 | buffer.splice(insertIndex, 0, item) 115 | if (buffer.length >= 1000) 116 | break 117 | } 118 | } 119 | } 120 | 121 | function paginateQuery>(data: Iterable, params: P): QueryResult { 122 | if (typeof params.limit === 'undefined') { 123 | return Array.from(data) as QueryResult 124 | } 125 | 126 | else { 127 | let rows = Array.from(data) 128 | const totalRows = rows.length 129 | const totalPages = Math.ceil(totalRows / params.limit) 130 | const start = ((params?.page ?? 1) - 1) * params.limit 131 | const end = start + params.limit 132 | rows = rows.slice(start, end) 133 | 134 | return { 135 | totalRows, 136 | totalPages, 137 | rows, 138 | get unpaginatedRows() { 139 | return Array.from(data) 140 | }, 141 | } as QueryResult 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Primitive = string | number | symbol 2 | export type GenericObject = Record 3 | 4 | export type NestedPaths = T extends Array 5 | ? `${NestedPaths}` 6 | : T extends object 7 | ? { 8 | [K in keyof T & (string | number)]: K extends string 9 | ? `${K}` | `${K}.${NestedPaths}` 10 | : never; 11 | }[keyof T & (string | number)] 12 | : never 13 | 14 | export type NestedPathsForType = T extends Array 15 | ? NestedPathsForType 16 | : T extends object 17 | ? { 18 | [K in keyof T & (string | number)]: K extends string 19 | ? T[K] extends P 20 | ? `${K}` | `${K}.${NestedPathsForType}` 21 | : T[K] extends object 22 | ? `${K}.${NestedPathsForType}` 23 | : string 24 | : string; 25 | }[keyof T & (string | number)] 26 | : string 27 | 28 | export type Operator = 'AND' | 'OR' | (() => 'AND' | 'OR') 29 | 30 | export type FilterMatchMode = 31 | | 'contains' 32 | | 'between' 33 | | 'equals' 34 | | 'notEquals' 35 | | 'greaterThan' 36 | | 'greaterThanOrEqual' 37 | | 'lessThan' 38 | | 'lessThanOrEqual' 39 | | 'exists' 40 | | 'regex' 41 | | 'arrayLength' 42 | | 'objectMatch' 43 | 44 | export type NonObjectMatchMode = Exclude 45 | export type ComparatorMatchMode = Extract 46 | export type RegexMatchMode = Extract 47 | export interface ComparatorParams { 48 | dateMode?: boolean 49 | } 50 | 51 | export interface RegexParams { 52 | flags?: string 53 | } 54 | export interface ObjectMapFilterParams { 55 | operator: 'AND' | 'OR' 56 | properties: Array<{ 57 | key: string 58 | matchMode: Exclude 59 | }> 60 | transformFilterValue?: (value: any) => any 61 | matchPropertyAtIndex?: boolean 62 | applyAtRoot?: boolean 63 | } 64 | 65 | export type MatchModeCore = ({ 66 | matchMode: Exclude 67 | } | { 68 | matchMode: ComparatorMatchMode 69 | params?: ComparatorParams 70 | } | { 71 | matchMode: 'regex' 72 | params?: RegexParams 73 | } | { 74 | matchMode: 'objectMatch' 75 | params: ObjectMapFilterParams | ((value: any) => ObjectMapFilterParams) 76 | }) 77 | 78 | export type QueryFilter = { 79 | key: Paths 80 | value: any 81 | operator?: Operator 82 | condition?: () => boolean 83 | } & MatchModeCore 84 | 85 | export interface QueryFilterGroup { 86 | operator: Operator 87 | filters: QueryFilter[] 88 | condition?: () => boolean 89 | } 90 | 91 | export type FilterOptions = Array> | Array> | { 92 | groups: Array> 93 | operator: Operator 94 | } 95 | 96 | export interface SearchOptions { 97 | value: string 98 | keys: Paths[] | Array<{ key: Paths, caseSensitive?: boolean }> 99 | caseSensitive?: boolean 100 | } 101 | 102 | export interface SortOption { 103 | key: Paths 104 | dir?: 'asc' | 'desc' 105 | parser?: 'number' | 'boolean' | 'string' | ((value: any) => string | number | boolean | null | undefined) 106 | } 107 | 108 | export interface QueryParams< 109 | T extends GenericObject = GenericObject, 110 | Paths extends NestedPaths = NestedPaths, 111 | PrimitivePath extends string = NestedPathsForType, 112 | > { 113 | sort?: SortOption | Array> 114 | search?: SearchOptions 115 | filter?: FilterOptions 116 | limit?: number 117 | page?: number 118 | } 119 | 120 | export type QueryResult> = P extends { limit: number } ? { totalRows: number, totalPages: number, rows: T[], unpaginatedRows: T[] } : T[] 121 | 122 | export interface MatchModeProcessorMap { 123 | equals: (f: { value: any, filter: any }) => boolean 124 | notEquals: (f: { value: any, filter: any }) => boolean 125 | exists: (f: { value: any, filter: any }) => boolean 126 | contains: (f: { value: any, filter: any }) => boolean 127 | greaterThan: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean 128 | greaterThanOrEqual: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean 129 | lessThan: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean 130 | lessThanOrEqual: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean 131 | between: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean 132 | arrayLength: (f: { value: any, filter: any }) => boolean 133 | objectMatch: (f: { value: any, filter: any, params: ObjectMapFilterParams, index?: number }) => boolean 134 | regex: (f: { value: any, filter: any, params?: RegexParams }) => boolean 135 | } 136 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { FilterMatchMode, GenericObject, MatchModeProcessorMap, Operator } from './types' 2 | 3 | export function getObjectProperty(object: Record, key: string) { 4 | return key.split('.').reduce((o, i) => o?.[i], object) 5 | } 6 | 7 | export const ValueChecker = { 8 | string: (value: any): value is string => typeof value === 'string', 9 | number: (value: any): value is number => typeof value === 'number', 10 | boolean: (value: any): value is boolean => typeof value === 'boolean', 11 | strNum: (value: any): value is string | number => typeof value === 'string' || typeof value === 'number', 12 | strNumBool: (value: any): value is string | number | boolean => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean', 13 | strNumBoolNull: (value: any): value is string | number | boolean | null => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null, 14 | object: (value: any): value is GenericObject => { 15 | return typeof value === 'object' && value !== null 16 | }, 17 | } 18 | 19 | export const MatchModeProcessor: MatchModeProcessorMap = { 20 | equals: ({ value, filter }) => 21 | ValueChecker.strNumBoolNull(filter) 22 | && ValueChecker.strNumBoolNull(value) 23 | && filter === value, 24 | notEquals: ({ value, filter }) => 25 | ValueChecker.strNumBoolNull(filter) 26 | && ValueChecker.strNumBoolNull(value) 27 | && filter !== value, 28 | exists: ({ value, filter }) => filter ? typeof value !== 'undefined' : typeof value === 'undefined', 29 | contains: ({ value, filter }) => value?.includes(filter), 30 | greaterThan: ({ value, filter, params }) => params?.dateMode ? new Date(value) > new Date(filter) : value > filter, 31 | greaterThanOrEqual: ({ value, filter, params }) => { 32 | return params?.dateMode ? new Date(value) >= new Date(filter) : value >= filter 33 | }, 34 | lessThan: ({ value, filter, params }) => params?.dateMode ? new Date(value) < new Date(filter) : value < filter, 35 | lessThanOrEqual: ({ value, filter, params }) => { 36 | return params?.dateMode ? new Date(value) <= new Date(filter) : value <= filter 37 | }, 38 | between: ({ value, filter, params }) => { 39 | return params?.dateMode ? new Date(value) >= new Date(filter[0]) && new Date(value) <= new Date(filter[1]) : value >= filter[0] && value <= filter[1] 40 | }, 41 | regex: ({ value, filter, params }) => 42 | typeof value === 'string' && new RegExp(filter, params?.flags ?? '').test(value), 43 | arrayLength: ({ value, filter }) => Array.isArray(value) && value.length === filter, 44 | objectMatch: ({ value, filter, params, index }) => { 45 | const properties = typeof index !== 'undefined' && params.matchPropertyAtIndex ? [params.properties[index]] : params.properties 46 | 47 | return properties[params.operator === 'AND' ? 'every' : 'some' as const](property => MatchModeProcessor[property.matchMode]({ 48 | value: getObjectProperty(value, property.key), 49 | filter: getObjectProperty(filter, property.key), 50 | params: {} as any, 51 | })) 52 | }, 53 | } 54 | 55 | export function validateBetweenPayload(payload: any) { 56 | return Array.isArray(payload) && payload.length === 2 && payload.every((i: any) => !Array.isArray(i)) 57 | } 58 | 59 | export function parseSearchValue(value: any, caseSensitive: boolean): string { 60 | return (caseSensitive ? value?.toString() : value?.toString()?.toLowerCase?.()) ?? '' 61 | } 62 | 63 | export function processSearchQuery(params: { key: string, object: Record, value: string, caseSensitive: boolean }): boolean { 64 | const { key, object, value, caseSensitive } = params 65 | const keys = key.split('.') 66 | 67 | let current: any = object 68 | for (let i = 0; i < keys.length; i++) { 69 | if (Array.isArray(current)) 70 | return current.some(item => processSearchQuery({ key: keys.slice(i).join('.'), object: item, value, caseSensitive })) 71 | 72 | else if (current && Object.prototype.hasOwnProperty.call(current, keys[i])) 73 | current = current[keys[i]] 74 | 75 | else 76 | return false 77 | } 78 | 79 | if (Array.isArray(current)) 80 | return current.some(element => parseSearchValue(element, caseSensitive).includes(parseSearchValue(value, caseSensitive))) 81 | 82 | else 83 | return parseSearchValue(current, caseSensitive).includes(parseSearchValue(value, caseSensitive)) ?? false 84 | } 85 | 86 | export function processFilterWithLookup< 87 | T extends FilterMatchMode, 88 | P = Parameters[0], 89 | >(params: { 90 | type: FilterMatchMode 91 | operator: 'AND' | 'OR' 92 | value: any 93 | filter: any 94 | params: P extends { params: infer U } ? U : P extends { params?: infer U } ? U : null 95 | lookupFrom?: 'value' | 'filter' 96 | }) { 97 | if (!Array.isArray(params.filter) || (params.type === 'between' && validateBetweenPayload(params.filter))) { 98 | return Array.isArray(params.value) 99 | ? params.value.some(value => 100 | MatchModeProcessor[params.type]({ 101 | params: params.params as any, 102 | value, 103 | filter: params.filter, 104 | }), 105 | ) 106 | : MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter: params.filter }) 107 | } 108 | 109 | else if (params.operator === 'AND') { 110 | return Array.isArray(params.filter) && params.filter.every((filter, index) => { 111 | if (Array.isArray(params.value)) { 112 | return params.value.some(value => 113 | MatchModeProcessor[params.type]({ 114 | params: params.params as any, 115 | value, 116 | filter, 117 | index, 118 | }), 119 | ) 120 | } 121 | else { 122 | return MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter, index }) 123 | } 124 | }) 125 | } 126 | 127 | else if (params.operator === 'OR') { 128 | return Array.isArray(params.filter) && params.filter.some((filter, index) => 129 | Array.isArray(params.value) 130 | ? params.value.some(value => 131 | MatchModeProcessor[params.type]({ 132 | params: params.params as any, 133 | value, 134 | filter, 135 | index, 136 | }), 137 | ) 138 | : MatchModeProcessor[params.type]({ params: params.params as any, value: params.value, filter, index }), 139 | ) 140 | } 141 | 142 | return false 143 | } 144 | 145 | export function getOperator(operator?: Operator) { 146 | return (typeof operator === 'function' ? operator() : operator) ?? 'OR' 147 | } 148 | 149 | export function omit>(object: T, keys: K) { 150 | const _result = { ...object } 151 | for (const key of keys) delete _result[key] 152 | return _result as Omit 153 | } 154 | -------------------------------------------------------------------------------- /test/fixtures/filtering.fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Contains match mode", 4 | "data": [ 5 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 6 | { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, 7 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 8 | ], 9 | "query": { 10 | "filter": [ 11 | { "key": "name", "matchMode": "contains", "value": "oh" } 12 | ] 13 | }, 14 | "result": { 15 | "rows": [ 16 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 17 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 18 | 19 | ] 20 | } 21 | }, 22 | { 23 | "title": "Between match mode with numbers", 24 | "data": [ 25 | { "id": 1, "name": "John", "age": 25 }, 26 | { "id": 2, "name": "Jane", "age": 30 }, 27 | { "id": 3, "name": "Bob", "age": 35 } 28 | ], 29 | "query": { 30 | "filter": [ 31 | { "key": "age", "matchMode": "between", "value": [28, 32] } 32 | ] 33 | }, 34 | "result": { 35 | "rows": [ 36 | { "id": 2, "name": "Jane", "age": 30 } 37 | ] 38 | } 39 | }, 40 | { 41 | "title": "Between match mode with dates", 42 | "data": [ 43 | { "id": 1, "name": "John", "joinDate": "2023-01-15" }, 44 | { "id": 2, "name": "Jane", "joinDate": "2023-06-20" }, 45 | { "id": 3, "name": "Bob", "joinDate": "2023-12-05" } 46 | ], 47 | "query": { 48 | "filter": [ 49 | { 50 | "key": "joinDate", 51 | "matchMode": "between", 52 | "value": ["2023-05-01", "2023-08-31"], 53 | "params": { "dateMode": true } 54 | } 55 | ] 56 | }, 57 | "result": { 58 | "rows": [ 59 | { "id": 2, "name": "Jane", "joinDate": "2023-06-20" } 60 | ] 61 | } 62 | }, 63 | { 64 | "title": "Equals match mode", 65 | "data": [ 66 | { "id": 1, "name": "John", "status": "active" }, 67 | { "id": 2, "name": "Jane", "status": "inactive" }, 68 | { "id": 3, "name": "Bob", "status": "active" } 69 | ], 70 | "query": { 71 | "filter": [ 72 | { "key": "status", "matchMode": "equals", "value": "active" } 73 | ] 74 | }, 75 | "result": { 76 | "rows": [ 77 | { "id": 1, "name": "John", "status": "active" }, 78 | { "id": 3, "name": "Bob", "status": "active" } 79 | ] 80 | } 81 | }, 82 | { 83 | "title": "Not Equals match mode", 84 | "data": [ 85 | { "id": 1, "name": "John", "status": "active" }, 86 | { "id": 2, "name": "Jane", "status": "inactive" }, 87 | { "id": 3, "name": "Bob", "status": "active" } 88 | ], 89 | "query": { 90 | "filter": [ 91 | { "key": "status", "matchMode": "notEquals", "value": "active" } 92 | ] 93 | }, 94 | "result": { 95 | "rows": [ 96 | { "id": 2, "name": "Jane", "status": "inactive" } 97 | ] 98 | } 99 | }, 100 | { 101 | "title": "Greater Than match mode", 102 | "data": [ 103 | { "id": 1, "name": "John", "age": 25 }, 104 | { "id": 2, "name": "Jane", "age": 30 }, 105 | { "id": 3, "name": "Bob", "age": 35 } 106 | ], 107 | "query": { 108 | "filter": [ 109 | { "key": "age", "matchMode": "greaterThan", "value": 28 } 110 | ] 111 | }, 112 | "result": { 113 | "rows": [ 114 | { "id": 2, "name": "Jane", "age": 30 }, 115 | { "id": 3, "name": "Bob", "age": 35 } 116 | ] 117 | } 118 | }, 119 | { 120 | "title": "Greater Than or Equal match mode", 121 | "data": [ 122 | { "id": 1, "name": "John", "age": 25 }, 123 | { "id": 2, "name": "Jane", "age": 30 }, 124 | { "id": 3, "name": "Bob", "age": 35 } 125 | ], 126 | "query": { 127 | "filter": [ 128 | { "key": "age", "matchMode": "greaterThanOrEqual", "value": 30 } 129 | ] 130 | }, 131 | "result": { 132 | "rows": [ 133 | { "id": 2, "name": "Jane", "age": 30 }, 134 | { "id": 3, "name": "Bob", "age": 35 } 135 | ] 136 | } 137 | }, 138 | { 139 | "title": "Less Than match mode", 140 | "data": [ 141 | { "id": 1, "name": "John", "age": 25 }, 142 | { "id": 2, "name": "Jane", "age": 30 }, 143 | { "id": 3, "name": "Bob", "age": 35 } 144 | ], 145 | "query": { 146 | "filter": [ 147 | { "key": "age", "matchMode": "lessThan", "value": 30 } 148 | ] 149 | }, 150 | "result": { 151 | "rows": [ 152 | { "id": 1, "name": "John", "age": 25 } 153 | ] 154 | } 155 | }, 156 | { 157 | "title": "Less Than or Equal match mode", 158 | "data": [ 159 | { "id": 1, "name": "John", "age": 25 }, 160 | { "id": 2, "name": "Jane", "age": 30 }, 161 | { "id": 3, "name": "Bob", "age": 35 } 162 | ], 163 | "query": { 164 | "filter": [ 165 | { "key": "age", "matchMode": "lessThanOrEqual", "value": 30 } 166 | ] 167 | }, 168 | "result": { 169 | "rows": [ 170 | { "id": 1, "name": "John", "age": 25 }, 171 | { "id": 2, "name": "Jane", "age": 30 } 172 | ] 173 | } 174 | }, 175 | { 176 | "title": "Exists match mode", 177 | "data": [ 178 | { "id": 1, "name": "John", "age": 25 }, 179 | { "id": 2, "name": "Jane" }, 180 | { "id": 3, "name": "Bob", "age": 35 } 181 | ], 182 | "query": { 183 | "filter": [ 184 | { "key": "age", "matchMode": "exists", "value": true } 185 | ] 186 | }, 187 | "result": { 188 | "rows": [ 189 | { "id": 1, "name": "John", "age": 25 }, 190 | { "id": 3, "name": "Bob", "age": 35 } 191 | ] 192 | } 193 | }, 194 | { 195 | "title": "Array Length match mode", 196 | "data": [ 197 | { "id": 1, "name": "John", "skills": ["JavaScript", "TypeScript", "React"] }, 198 | { "id": 2, "name": "Jane", "skills": ["Python", "Django"] }, 199 | { "id": 3, "name": "Bob", "skills": ["Java", "Spring", "Hibernate", "SQL"] } 200 | ], 201 | "query": { 202 | "filter": [ 203 | { "key": "skills", "matchMode": "arrayLength", "value": 3 } 204 | ] 205 | }, 206 | "result": { 207 | "rows": [ 208 | { "id": 1, "name": "John", "skills": ["JavaScript", "TypeScript", "React"] } 209 | ] 210 | } 211 | }, 212 | { 213 | "title": "Regex match mode - basic matching", 214 | "data": [ 215 | { "id": 1, "email": "john@example.com" }, 216 | { "id": 2, "email": "jane@test.com" }, 217 | { "id": 3, "email": "bob@example.org" } 218 | ], 219 | "query": { 220 | "filter": [ 221 | { "key": "email", "matchMode": "regex", "value": "@example\\.(com|org)" } 222 | ] 223 | }, 224 | "result": { 225 | "rows": [ 226 | { "id": 1, "email": "john@example.com" }, 227 | { "id": 3, "email": "bob@example.org" } 228 | ] 229 | } 230 | }, 231 | { 232 | "title": "Regex match mode - case insensitive", 233 | "data": [ 234 | { "id": 1, "name": "John Doe" }, 235 | { "id": 2, "name": "jane smith" }, 236 | { "id": 3, "name": "Bob Johnson" } 237 | ], 238 | "query": { 239 | "filter": [ 240 | { 241 | "key": "name", 242 | "matchMode": "regex", 243 | "value": "^j.*", 244 | "params": { "flags": "i" } 245 | } 246 | ] 247 | }, 248 | "result": { 249 | "rows": [ 250 | { "id": 1, "name": "John Doe" }, 251 | { "id": 2, "name": "jane smith" } 252 | ] 253 | } 254 | }, 255 | { 256 | "title": "Regex match mode - case sensitive", 257 | "data": [ 258 | { "id": 1, "name": "John Doe" }, 259 | { "id": 2, "name": "jane smith" }, 260 | { "id": 3, "name": "Bob Johnson" } 261 | ], 262 | "query": { 263 | "filter": [ 264 | { 265 | "key": "name", 266 | "matchMode": "regex", 267 | "value": "^j.*" 268 | } 269 | ] 270 | }, 271 | "result": { 272 | "rows": [ 273 | { "id": 2, "name": "jane smith" } 274 | ] 275 | } 276 | }, 277 | { 278 | "title": "Regex match mode - global flag", 279 | "data": [ 280 | { "id": 1, "text": "The quick brown fox" }, 281 | { "id": 2, "text": "The lazy dog" }, 282 | { "id": 3, "text": "Quick foxes are quick" } 283 | ], 284 | "query": { 285 | "filter": [ 286 | { 287 | "key": "text", 288 | "matchMode": "regex", 289 | "value": "quick", 290 | "params": { "flags": "g" } 291 | } 292 | ] 293 | }, 294 | "result": { 295 | "rows": [ 296 | { "id": 1, "text": "The quick brown fox" }, 297 | { "id": 3, "text": "Quick foxes are quick" } 298 | ] 299 | } 300 | }, 301 | { 302 | "title": "Regex match mode - multiline flag", 303 | "data": [ 304 | { "id": 1, "description": "First line\nSecond line" }, 305 | { "id": 2, "description": "One line only" }, 306 | { "id": 3, "description": "First line\nLast line" } 307 | ], 308 | "query": { 309 | "filter": [ 310 | { 311 | "key": "description", 312 | "matchMode": "regex", 313 | "value": "^Last", 314 | "params": { "flags": "m" } 315 | } 316 | ] 317 | }, 318 | "result": { 319 | "rows": [ 320 | { "id": 3, "description": "First line\nLast line" } 321 | ] 322 | } 323 | }, 324 | { 325 | "title": "Regex match mode - combined flags", 326 | "data": [ 327 | { "id": 1, "content": "HELLO\nWorld" }, 328 | { "id": 2, "content": "hello\nWORLD" }, 329 | { "id": 3, "content": "Hi\nthere" } 330 | ], 331 | "query": { 332 | "filter": [ 333 | { 334 | "key": "content", 335 | "matchMode": "regex", 336 | "value": "^world", 337 | "params": { "flags": "im" } 338 | } 339 | ] 340 | }, 341 | "result": { 342 | "rows": [ 343 | { "id": 1, "content": "HELLO\nWorld" }, 344 | { "id": 2, "content": "hello\nWORLD" } 345 | ] 346 | } 347 | }, 348 | { 349 | "title": "Regex match mode - with other filters", 350 | "data": [ 351 | { "id": 1, "name": "John Doe", "age": 30 }, 352 | { "id": 2, "name": "Jane Smith", "age": 25 }, 353 | { "id": 3, "name": "Bob Johnson", "age": 35 } 354 | ], 355 | "query": { 356 | "filter": [ 357 | { 358 | "key": "name", 359 | "matchMode": "regex", 360 | "value": "^j.*", 361 | "params": { "flags": "i" } 362 | }, 363 | { "key": "age", "matchMode": "greaterThan", "value": 28 } 364 | ] 365 | }, 366 | "result": { 367 | "rows": [ 368 | { "id": 1, "name": "John Doe", "age": 30 } 369 | ] 370 | } 371 | }, 372 | { 373 | "title": "Object Match mode", 374 | "data": [ 375 | { "id": 1, "name": "John", "account": { "type": "premium", "status": "active" } }, 376 | { "id": 2, "name": "Jane", "account": { "type": "basic", "status": "inactive" } }, 377 | { "id": 3, "name": "Bob", "account": { "type": "premium", "status": "inactive" } } 378 | ], 379 | "query": { 380 | "filter": [ 381 | { 382 | "key": "account", 383 | "matchMode": "objectMatch", 384 | "value": { "type": "premium", "status": "active" }, 385 | "params": { 386 | "operator": "AND", 387 | "properties": [ 388 | { "key": "type", "matchMode": "equals" }, 389 | { "key": "status", "matchMode": "equals" } 390 | ] 391 | } 392 | } 393 | ] 394 | }, 395 | "result": { 396 | "rows": [ 397 | { "id": 1, "name": "John", "account": { "type": "premium", "status": "active" } } 398 | ] 399 | } 400 | }, 401 | { 402 | "title": "Object Match - ALL objects in the array match ALL conditions", 403 | "data": [ 404 | { "id": 1, "name": "John", "orders": [{ "id": 1, "total": 100 }, { "id": 2, "total": 200 }] }, 405 | { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150 }, { "id": 4, "total": 300 }] }, 406 | { "id": 3, "name": "Bob", "orders": [{ "id": 5, "total": 50 }, { "id": 6, "total": 250 }] } 407 | ], 408 | "query": { 409 | "filter": [ 410 | { 411 | "key": "orders", 412 | "matchMode": "objectMatch", 413 | "value": { "total": 50 }, 414 | "operator": "AND", 415 | "params": { 416 | "operator": "AND", 417 | "properties": [ 418 | { "key": "total", "matchMode": "greaterThan" } 419 | ] 420 | } 421 | } 422 | ] 423 | }, 424 | "result": { 425 | "rows": [ 426 | { "id": 1, "name": "John", "orders": [{ "id": 1, "total": 100 }, { "id": 2, "total": 200 }] }, 427 | { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150 }, { "id": 4, "total": 300 }] } 428 | ] 429 | } 430 | }, 431 | { 432 | "title": "Object Match - ALL objects in the array match SOME of the conditions", 433 | "data": [ 434 | { "id": 1, "name": "John", "orders": [{ "id": 1, "total": 100, "status": "completed" }, { "id": 2, "total": 200, "status": "pending" }] }, 435 | { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150, "status": "completed" }, { "id": 4, "total": 300, "status": "completed" }] }, 436 | { "id": 3, "name": "Bob", "orders": [{ "id": 5, "total": 50, "status": "pending" }, { "id": 6, "total": 250, "status": "completed" }] } 437 | ], 438 | "query": { 439 | "filter": [ 440 | { 441 | "key": "orders", 442 | "matchMode": "objectMatch", 443 | "value": { "total": 100, "status": "completed" }, 444 | "operator": "AND", 445 | "params": { 446 | "operator": "OR", 447 | "properties": [ 448 | { "key": "total", "matchMode": "greaterThan" }, 449 | { "key": "status", "matchMode": "equals" } 450 | ] 451 | } 452 | } 453 | ] 454 | }, 455 | "result": { 456 | "rows": [ 457 | { "id": 1, "name": "John", "orders": [{ "id": 1, "total": 100, "status": "completed" }, { "id": 2, "total": 200, "status": "pending" }] }, 458 | { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150, "status": "completed" }, { "id": 4, "total": 300, "status": "completed" }] } 459 | ] 460 | } 461 | }, 462 | { 463 | "title": "Object Match - SOME objects in the array match ALL conditions", 464 | "data": [ 465 | { "id": 1, "name": "John", "orders": [{ "id": 1, "total": 100, "status": "completed" }, { "id": 2, "total": 200, "status": "pending" }] }, 466 | { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150, "status": "completed" }, { "id": 4, "total": 300, "status": "completed" }] }, 467 | { "id": 3, "name": "Bob", "orders": [{ "id": 5, "total": 50, "status": "pending" }, { "id": 6, "total": 250, "status": "completed" }] } 468 | ], 469 | "query": { 470 | "filter": [ 471 | { 472 | "key": "orders", 473 | "matchMode": "objectMatch", 474 | "value": { "total": 200, "status": "completed" }, 475 | "operator": "OR", 476 | "params": { 477 | "operator": "AND", 478 | "properties": [ 479 | { "key": "total", "matchMode": "greaterThan" }, 480 | { "key": "status", "matchMode": "equals" } 481 | ] 482 | } 483 | } 484 | ] 485 | }, 486 | "result": { 487 | "rows": [ 488 | { "id": 2, "name": "Jane", "orders": [{ "id": 3, "total": 150, "status": "completed" }, { "id": 4, "total": 300, "status": "completed" }] } 489 | ] 490 | } 491 | }, 492 | { 493 | "title": "Combined filters with AND logic", 494 | "data": [ 495 | { "id": 1, "name": "John", "age": 30, "status": "active" }, 496 | { "id": 2, "name": "Jane", "age": 25, "status": "inactive" }, 497 | { "id": 3, "name": "Bob", "age": 35, "status": "active" } 498 | ], 499 | "query": { 500 | "filter": [ 501 | { "key": "age", "matchMode": "greaterThan", "value": 25 }, 502 | { "key": "status", "matchMode": "equals", "value": "active" } 503 | ] 504 | }, 505 | "result": { 506 | "rows": [ 507 | { "id": 1, "name": "John", "age": 30, "status": "active" }, 508 | { "id": 3, "name": "Bob", "age": 35, "status": "active" } 509 | ] 510 | } 511 | }, 512 | { 513 | "title": "Combined filters with OR logic", 514 | "data": [ 515 | { "id": 1, "name": "John", "age": 30, "status": "active" }, 516 | { "id": 2, "name": "Jane", "age": 25, "status": "inactive" }, 517 | { "id": 3, "name": "Bob", "age": 35, "status": "active" } 518 | ], 519 | "query": { 520 | "filter": [ 521 | { 522 | "operator": "OR", 523 | "filters": [ 524 | { "key": "age", "matchMode": "lessThan", "value": 30 }, 525 | { "key": "status", "matchMode": "equals", "value": "active" } 526 | ] 527 | } 528 | ] 529 | }, 530 | "result": { 531 | "rows": [ 532 | { "id": 1, "name": "John", "age": 30, "status": "active" }, 533 | { "id": 2, "name": "Jane", "age": 25, "status": "inactive" }, 534 | { "id": 3, "name": "Bob", "age": 35, "status": "active" } 535 | ] 536 | } 537 | } 538 | ] 539 | -------------------------------------------------------------------------------- /test/fixtures/pagination.fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Basic pagination - first page", 4 | "data": [ 5 | { "id": 1, "name": "Item 1" }, 6 | { "id": 2, "name": "Item 2" }, 7 | { "id": 3, "name": "Item 3" }, 8 | { "id": 4, "name": "Item 4" }, 9 | { "id": 5, "name": "Item 5" } 10 | ], 11 | "query": { 12 | "page": 1, 13 | "limit": 2 14 | }, 15 | "result": { 16 | "rows": [ 17 | { "id": 1, "name": "Item 1" }, 18 | { "id": 2, "name": "Item 2" } 19 | ], 20 | "totalPages": 3, 21 | "totalRows": 5 22 | } 23 | }, 24 | { 25 | "title": "Pagination - middle page", 26 | "data": [ 27 | { "id": 1, "name": "Item 1" }, 28 | { "id": 2, "name": "Item 2" }, 29 | { "id": 3, "name": "Item 3" }, 30 | { "id": 4, "name": "Item 4" }, 31 | { "id": 5, "name": "Item 5" } 32 | ], 33 | "query": { 34 | "page": 2, 35 | "limit": 2 36 | }, 37 | "result": { 38 | "rows": [ 39 | { "id": 3, "name": "Item 3" }, 40 | { "id": 4, "name": "Item 4" } 41 | ], 42 | "totalPages": 3, 43 | "totalRows": 5 44 | } 45 | }, 46 | { 47 | "title": "Pagination - last page", 48 | "data": [ 49 | { "id": 1, "name": "Item 1" }, 50 | { "id": 2, "name": "Item 2" }, 51 | { "id": 3, "name": "Item 3" }, 52 | { "id": 4, "name": "Item 4" }, 53 | { "id": 5, "name": "Item 5" } 54 | ], 55 | "query": { 56 | "page": 3, 57 | "limit": 2 58 | }, 59 | "result": { 60 | "rows": [ 61 | { "id": 5, "name": "Item 5" } 62 | ], 63 | "totalPages": 3, 64 | "totalRows": 5 65 | } 66 | }, 67 | { 68 | "title": "Pagination - page out of range (too high)", 69 | "data": [ 70 | { "id": 1, "name": "Item 1" }, 71 | { "id": 2, "name": "Item 2" }, 72 | { "id": 3, "name": "Item 3" } 73 | ], 74 | "query": { 75 | "page": 5, 76 | "limit": 2 77 | }, 78 | "result": { 79 | "rows": [], 80 | "totalPages": 2, 81 | "totalRows": 3 82 | } 83 | }, 84 | { 85 | "title": "Pagination - limit larger than dataset", 86 | "data": [ 87 | { "id": 1, "name": "Item 1" }, 88 | { "id": 2, "name": "Item 2" }, 89 | { "id": 3, "name": "Item 3" } 90 | ], 91 | "query": { 92 | "page": 1, 93 | "limit": 10 94 | }, 95 | "result": { 96 | "rows": [ 97 | { "id": 1, "name": "Item 1" }, 98 | { "id": 2, "name": "Item 2" }, 99 | { "id": 3, "name": "Item 3" } 100 | ], 101 | "totalPages": 1, 102 | "totalRows": 3 103 | } 104 | }, 105 | { 106 | "title": "Pagination - empty dataset", 107 | "data": [], 108 | "query": { 109 | "page": 1, 110 | "limit": 5 111 | }, 112 | "result": { 113 | "rows": [], 114 | "totalPages": 0, 115 | "totalRows": 0 116 | } 117 | }, 118 | { 119 | "title": "Pagination - limit of 1", 120 | "data": [ 121 | { "id": 1, "name": "Item 1" }, 122 | { "id": 2, "name": "Item 2" }, 123 | { "id": 3, "name": "Item 3" } 124 | ], 125 | "query": { 126 | "page": 2, 127 | "limit": 1 128 | }, 129 | "result": { 130 | "rows": [ 131 | { "id": 2, "name": "Item 2" } 132 | ], 133 | "totalPages": 3, 134 | "totalRows": 3 135 | } 136 | } 137 | ] 138 | -------------------------------------------------------------------------------- /test/fixtures/search.fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Basic single field search", 4 | "data": [ 5 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 6 | { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, 7 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 8 | ], 9 | "query": { 10 | "search": { 11 | "value": "john", 12 | "keys": ["name"] 13 | } 14 | }, 15 | "result": { 16 | "rows": [ 17 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 18 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 19 | 20 | ] 21 | } 22 | }, 23 | { 24 | "title": "Multi-field search", 25 | "data": [ 26 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 27 | { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, 28 | { "id": 3, "name": "Bob Thomas", "email": "bobjohn@example.com" } 29 | ], 30 | "query": { 31 | "search": { 32 | "value": "john", 33 | "keys": ["name", "email"] 34 | } 35 | }, 36 | "result": { 37 | "rows": [ 38 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 39 | { "id": 3, "name": "Bob Thomas", "email": "bobjohn@example.com" } 40 | ] 41 | } 42 | }, 43 | { 44 | "title": "Case-sensitive search", 45 | "data": [ 46 | { "id": 1, "name": "JOHN Doe", "email": "JOHN@example.com" }, 47 | { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, 48 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 49 | ], 50 | "query": { 51 | "search": { 52 | "value": "JOHN", 53 | "keys": ["name"], 54 | "caseSensitive": true 55 | } 56 | }, 57 | "result": { 58 | "rows": [ 59 | { "id": 1, "name": "JOHN Doe", "email": "JOHN@example.com" } 60 | ] 61 | } 62 | }, 63 | { 64 | "title": "Partial match search", 65 | "data": [ 66 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 67 | { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, 68 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 69 | ], 70 | "query": { 71 | "search": { 72 | "value": "oh", 73 | "keys": ["name", "email"] 74 | } 75 | }, 76 | "result": { 77 | "rows": [ 78 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 79 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 80 | 81 | ] 82 | } 83 | }, 84 | { 85 | "title": "Search with no results", 86 | "data": [ 87 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 88 | { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, 89 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 90 | ], 91 | "query": { 92 | "search": { 93 | "value": "xyz", 94 | "keys": ["name", "email"] 95 | } 96 | }, 97 | "result": { 98 | "rows": [] 99 | } 100 | }, 101 | { 102 | "title": "Search in nested fields", 103 | "data": [ 104 | { "id": 1, "name": "John Doe", "contact": { "email": "john@example.com" } }, 105 | { "id": 2, "name": "Jane Smith", "contact": { "email": "jane@example.com" } }, 106 | { "id": 3, "name": "Bob Johnson", "contact": { "email": "bob@example.com" } } 107 | ], 108 | "query": { 109 | "search": { 110 | "value": "john", 111 | "keys": ["contact.email"] 112 | } 113 | }, 114 | "result": { 115 | "rows": [ 116 | { "id": 1, "name": "John Doe", "contact": { "email": "john@example.com" } } 117 | ] 118 | } 119 | }, 120 | { 121 | "title": "Search with empty value", 122 | "data": [ 123 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 124 | { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, 125 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 126 | ], 127 | "query": { 128 | "search": { 129 | "value": "", 130 | "keys": ["name", "email"] 131 | } 132 | }, 133 | "result": { 134 | "rows": [ 135 | { "id": 1, "name": "John Doe", "email": "john@example.com" }, 136 | { "id": 2, "name": "Jane Smith", "email": "jane@example.com" }, 137 | { "id": 3, "name": "Bob Johnson", "email": "bob@example.com" } 138 | ] 139 | } 140 | }, 141 | { 142 | "title": "Search with number fields", 143 | "data": [ 144 | { "id": 1, "name": "John Doe", "age": 30 }, 145 | { "id": 2, "name": "Jane Smith", "age": 25 }, 146 | { "id": 3, "name": "Bob Johnson", "age": 35 } 147 | ], 148 | "query": { 149 | "search": { 150 | "value": "30", 151 | "keys": ["name", "age"] 152 | } 153 | }, 154 | "result": { 155 | "rows": [ 156 | { "id": 1, "name": "John Doe", "age": 30 } 157 | ] 158 | } 159 | }, 160 | { 161 | "title": "Search with boolean fields", 162 | "data": [ 163 | { "id": 1, "name": "John Doe", "isActive": true }, 164 | { "id": 2, "name": "Jane Smith", "isActive": false }, 165 | { "id": 3, "name": "Bob Johnson", "isActive": true } 166 | ], 167 | "query": { 168 | "search": { 169 | "value": "true", 170 | "keys": ["name", "isActive"] 171 | } 172 | }, 173 | "result": { 174 | "rows": [ 175 | { "id": 1, "name": "John Doe", "isActive": true }, 176 | { "id": 3, "name": "Bob Johnson", "isActive": true } 177 | ] 178 | } 179 | }, 180 | { 181 | "title": "Search with array fields", 182 | "data": [ 183 | { "id": 1, "name": "John Doe", "tags": ["developer", "javascript"] }, 184 | { "id": 2, "name": "Jane Smith", "tags": ["designer", "ui"] }, 185 | { "id": 3, "name": "Bob Johnson", "tags": ["manager", "agile"] } 186 | ], 187 | "query": { 188 | "search": { 189 | "value": "javascript", 190 | "keys": ["name", "tags"] 191 | } 192 | }, 193 | "result": { 194 | "rows": [ 195 | { "id": 1, "name": "John Doe", "tags": ["developer", "javascript"] } 196 | ] 197 | } 198 | } 199 | ] 200 | -------------------------------------------------------------------------------- /test/fixtures/sorting.fixture.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Sort by single field ascending", 4 | "data": [ 5 | { "id": 3, "name": "Charlie", "age": 30 }, 6 | { "id": 1, "name": "Alice", "age": 25 }, 7 | { "id": 2, "name": "Bob", "age": 35 } 8 | ], 9 | "query": { 10 | "sort": { "key": "name", "dir": "asc" } 11 | }, 12 | "result": { 13 | "rows": [ 14 | { "id": 1, "name": "Alice", "age": 25 }, 15 | { "id": 2, "name": "Bob", "age": 35 }, 16 | { "id": 3, "name": "Charlie", "age": 30 } 17 | ] 18 | } 19 | }, 20 | { 21 | "title": "Sort by single field descending", 22 | "data": [ 23 | { "id": 3, "name": "Charlie", "age": 30 }, 24 | { "id": 1, "name": "Alice", "age": 25 }, 25 | { "id": 2, "name": "Bob", "age": 35 } 26 | ], 27 | "query": { 28 | "sort": { "key": "age", "dir": "desc" } 29 | }, 30 | "result": { 31 | "rows": [ 32 | { "id": 2, "name": "Bob", "age": 35 }, 33 | { "id": 3, "name": "Charlie", "age": 30 }, 34 | { "id": 1, "name": "Alice", "age": 25 } 35 | ] 36 | } 37 | }, 38 | { 39 | "title": "Sort by multiple fields", 40 | "data": [ 41 | { "id": 1, "name": "Alice", "age": 30 }, 42 | { "id": 2, "name": "Bob", "age": 25 }, 43 | { "id": 3, "name": "Charlie", "age": 30 }, 44 | { "id": 4, "name": "David", "age": 25 } 45 | ], 46 | "query": { 47 | "sort": [ 48 | { "key": "age", "dir": "asc" }, 49 | { "key": "name", "dir": "desc" } 50 | ] 51 | }, 52 | "result": { 53 | "rows": [ 54 | { "id": 4, "name": "David", "age": 25 }, 55 | { "id": 2, "name": "Bob", "age": 25 }, 56 | { "id": 3, "name": "Charlie", "age": 30 }, 57 | { "id": 1, "name": "Alice", "age": 30 } 58 | ] 59 | } 60 | }, 61 | { 62 | "title": "Sort by nested field", 63 | "data": [ 64 | { "id": 1, "name": "John", "scores": { "math": 85, "science": 92 } }, 65 | { "id": 2, "name": "Jane", "scores": { "math": 90, "science": 88 } }, 66 | { "id": 3, "name": "Bob", "scores": { "math": 78, "science": 95 } } 67 | ], 68 | "query": { 69 | "sort": { "key": "scores.math", "dir": "desc" } 70 | }, 71 | "result": { 72 | "rows": [ 73 | { "id": 2, "name": "Jane", "scores": { "math": 90, "science": 88 } }, 74 | { "id": 1, "name": "John", "scores": { "math": 85, "science": 92 } }, 75 | { "id": 3, "name": "Bob", "scores": { "math": 78, "science": 95 } } 76 | ] 77 | } 78 | }, 79 | { 80 | "title": "Sort with null values - asc (nulls first)", 81 | "data": [ 82 | { "id": 1, "name": "Alice", "age": 30 }, 83 | { "id": 2, "name": "Bob", "age": null }, 84 | { "id": 3, "name": "Charlie", "age": 25 } 85 | ], 86 | "query": { 87 | "sort": { "key": "age", "dir": "asc" } 88 | }, 89 | "result": { 90 | "rows": [ 91 | { "id": 2, "name": "Bob", "age": null }, 92 | { "id": 3, "name": "Charlie", "age": 25 }, 93 | { "id": 1, "name": "Alice", "age": 30 } 94 | ] 95 | } 96 | }, 97 | { 98 | "title": "Sort with undefined values - desc (undefined last)", 99 | "data": [ 100 | { "id": 1, "name": "Alice", "age": 30 }, 101 | { "id": 2, "name": "Bob" }, 102 | { "id": 3, "name": "Charlie", "age": 25 } 103 | ], 104 | "query": { 105 | "sort": { "key": "age", "dir": "desc" } 106 | }, 107 | "result": { 108 | "rows": [ 109 | { "id": 1, "name": "Alice", "age": 30 }, 110 | { "id": 3, "name": "Charlie", "age": 25 }, 111 | { "id": 2, "name": "Bob" } 112 | ] 113 | } 114 | }, 115 | { 116 | "title": "Sort with mix of types (numbers and strings)", 117 | "data": [ 118 | { "id": 1, "value": 5 }, 119 | { "id": 2, "value": "10" }, 120 | { "id": 3, "value": "3" }, 121 | { "id": 4, "value": 15 } 122 | ], 123 | "query": { 124 | "sort": { "key": "value", "dir": "asc", "parser": "number" } 125 | }, 126 | "result": { 127 | "rows": [ 128 | { "id": 3, "value": "3" }, 129 | { "id": 1, "value": 5 }, 130 | { "id": 2, "value": "10" }, 131 | { "id": 4, "value": 15 } 132 | ] 133 | } 134 | } 135 | 136 | ] 137 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import { describe, expect, it } from 'vitest' 3 | import { query } from '../src' 4 | import { omit } from '../src/utils' 5 | import PaginationFixtures from './fixtures/pagination.fixture.json' 6 | import SortingFixtures from './fixtures/sorting.fixture.json' 7 | import FilteringFixtures from './fixtures/filtering.fixture.json' 8 | import SearchFixtures from './fixtures/search.fixture.json' 9 | 10 | const fixtures = [ 11 | { 12 | key: 'pagination', 13 | tests: PaginationFixtures, 14 | }, 15 | { 16 | key: 'sorting', 17 | tests: SortingFixtures, 18 | }, 19 | { 20 | key: 'search', 21 | tests: SearchFixtures, 22 | }, 23 | { 24 | key: 'filtering', 25 | tests: FilteringFixtures, 26 | }, 27 | ] 28 | 29 | for (const fixture of fixtures) { 30 | describe(`${fixture.key} tests`, () => { 31 | for (const test of fixture.tests) { 32 | it(test.title, () => { 33 | const result = (query as any)(test.data, test.query) 34 | const matchResult = Array.isArray(result) ? { rows: result } : omit(result, ['unpaginatedRows']) 35 | console.log('expected', JSON.stringify(test.result, null, 2)) 36 | console.log('actual', JSON.stringify(matchResult, null, 2)) 37 | expect(matchResult).toEqual(test.result) 38 | }) 39 | } 40 | }) 41 | } 42 | 43 | describe('performance check', () => { 44 | const getValue = () => Math.floor(Math.random() * 100) 45 | const startItems = performance.now() 46 | const items = Array.from({ length: 1000000 }, (_, i) => ({ 47 | id: i, 48 | name: `Item ${i}`, 49 | value: getValue(), 50 | other: [], 51 | address: { city: 'New York', country: 'USA' }, 52 | age: Math.floor(Math.random() * 100), 53 | })) 54 | const endItems = performance.now() 55 | it('query 1M rows - paginate + sort + search + filter in less than 50ms', () => { 56 | console.info('Time taken to generate 1M items:', endItems - startItems) 57 | const start = performance.now() 58 | query(items, { 59 | limit: 100, 60 | sort: [ 61 | { key: 'age', dir: 'asc' }, 62 | { key: 'name', dir: 'asc' }, 63 | ], 64 | search: { 65 | value: 'Item 1', 66 | keys: ['name'], 67 | }, 68 | filter: [{ key: 'value', matchMode: 'greaterThan', value: 50 }], 69 | }) 70 | 71 | const end = performance.now() 72 | console.info('Time taken to query 1M items:', end - start) 73 | expect(end - start).toBeLessThan(50) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "skipDefaultLibCheck": true, 13 | "skipLibCheck": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------