├── .husky ├── pre-push └── pre-commit ├── tests ├── tsconfig.json ├── or.spec.ts ├── out-list.spec.ts ├── gt.spec.ts ├── lt.spec.ts ├── ge.spec.ts ├── le.spec.ts ├── eq.spec.ts ├── ne.spec.ts ├── in-list.spec.ts ├── and.spec.ts ├── escape-value.spec.ts ├── comparison.spec.ts ├── custom-operator.spec.ts └── index.spec.ts ├── .npmignore ├── .prettierrc ├── tsconfig.json ├── src ├── escape-value.ts ├── index.ts ├── or.ts ├── operation.ts ├── eq.ts ├── in-list.ts ├── lt.ts ├── ne.ts ├── gt.ts ├── out-list.ts ├── le.ts ├── ge.ts ├── comparison.ts └── and.ts ├── .github ├── dependabot.yml └── workflows │ ├── npm-publish.yml │ └── npm-test.yml ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── .eslintrc /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:fix 5 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["*.spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc 2 | .github 3 | .husky 4 | .nyc_output 5 | .prettierrc 6 | .vscode 7 | *.tgz 8 | coverage 9 | jest.config.js 10 | node_modules 11 | src 12 | tests 13 | tsconfig.json 14 | tslint-to-eslint-config.log 15 | tslint.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "fluid": false, 5 | "jsxBracketSameLine": false, 6 | "printWidth": 120, 7 | "semi": true, 8 | "singleQuote": true, 9 | "tabWidth": 2, 10 | "trailingComma": "none", 11 | "useTabs": false 12 | } 13 | -------------------------------------------------------------------------------- /tests/or.spec.ts: -------------------------------------------------------------------------------- 1 | import { or } from '../src'; 2 | 3 | describe('or()', () => { 4 | it('should return or-expression string', () => { 5 | const operators = ['field1==val1', 'field2==20', 'field3=="escaped value"']; 6 | 7 | expect(or(...operators)).toBe( 8 | 'field1==val1,field2==20,field3=="escaped value"' 9 | ); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/out-list.spec.ts: -------------------------------------------------------------------------------- 1 | import { outList } from '../src/'; 2 | 3 | describe('outList()', () => { 4 | it('should return out-list expression', () => { 5 | expect(outList('string', 'string*with*asterix', 'string with spaces', 999, '"quoted" string').toString()).toBe( 6 | '=out=(string,string*with*asterix,"string with spaces",999,"\\"quoted\\" string")' 7 | ); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es2015", 6 | "declaration": true, 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "outDir": "dist/cjs", 11 | "baseUrl": "." 12 | }, 13 | "include": ["src/*"], 14 | "exclude": ["node_modules/*", "examples/*"] 15 | } 16 | -------------------------------------------------------------------------------- /tests/gt.spec.ts: -------------------------------------------------------------------------------- 1 | import { gt } from '../src'; 2 | 3 | describe('gt()', () => { 4 | it('should return "less-then"-operator', () => { 5 | expect(gt(100).toString()).toBe('>100'); 6 | expect(gt('string').toString()).toBe('>string'); 7 | expect(gt('string with spaces').toString()).toBe('>"string with spaces"'); 8 | expect(gt('"quoted" string').toString()).toBe('>"\\"quoted\\" string"'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/lt.spec.ts: -------------------------------------------------------------------------------- 1 | import { lt } from '../src'; 2 | 3 | describe('lt()', () => { 4 | it('should return "less-then"-operator', () => { 5 | expect(lt(100).toString()).toBe('<100'); 6 | expect(lt('string').toString()).toBe(' { 4 | it('should return "less-then"-operator', () => { 5 | expect(ge(100).toString()).toBe('>=100'); 6 | expect(ge('string').toString()).toBe('>=string'); 7 | expect(ge('string with spaces').toString()).toBe('>="string with spaces"'); 8 | expect(ge('"quoted" string').toString()).toBe('>="\\"quoted\\" string"'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/le.spec.ts: -------------------------------------------------------------------------------- 1 | import { le } from '../src'; 2 | 3 | describe('le()', () => { 4 | it('should return "less-then-or-equal"-operation', () => { 5 | expect(le(100).toString()).toBe('<=100'); 6 | expect(le('string').toString()).toBe('<=string'); 7 | expect(le('string with spaces').toString()).toBe('<="string with spaces"'); 8 | expect(le('"quoted" string').toString()).toBe('<="\\"quoted\\" string"'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/escape-value.ts: -------------------------------------------------------------------------------- 1 | const CHARS_TO_ESCAPE = /["'();,=!~<>\s]/; 2 | 3 | export function escapeValue(value: unknown): string { 4 | if (Array.isArray(value)) { 5 | return `${value.map(escapeValue)}`; 6 | } 7 | 8 | let valueString = value.toString(); 9 | 10 | if (CHARS_TO_ESCAPE.test(valueString) || valueString.length === 0) { 11 | valueString = `"${valueString.replace(/"/g, '\\"')}"`; 12 | } 13 | 14 | return valueString; 15 | } 16 | -------------------------------------------------------------------------------- /tests/eq.spec.ts: -------------------------------------------------------------------------------- 1 | import { eq } from '../src'; 2 | 3 | describe('eq()', () => { 4 | it('should return "less-then"-operator', () => { 5 | expect(eq(100).toString()).toBe('==100'); 6 | expect(eq(true).toString()).toBe('==true'); 7 | expect(eq('string').toString()).toBe('==string'); 8 | expect(eq('string with spaces').toString()).toBe('=="string with spaces"'); 9 | expect(eq('"quoted" string').toString()).toBe('=="\\"quoted\\" string"'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/ne.spec.ts: -------------------------------------------------------------------------------- 1 | import { ne } from '../src'; 2 | 3 | describe('ne()', () => { 4 | it('should return "not-equal"-operator', () => { 5 | expect(ne(100).toString()).toBe('!=100'); 6 | expect(ne(true).toString()).toBe('!=true'); 7 | expect(ne('string').toString()).toBe('!=string'); 8 | expect(ne('string with spaces').toString()).toBe('!="string with spaces"'); 9 | expect(ne('"quoted" string').toString()).toBe('!="\\"quoted\\" string"'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/in-list.spec.ts: -------------------------------------------------------------------------------- 1 | import { inList } from '../src'; 2 | 3 | describe('inList()', () => { 4 | it('should return in-list expression', () => { 5 | expect( 6 | inList( 7 | 'string', 8 | 'string*with*asterix', 9 | 'string with spaces', 10 | 999, 11 | '"quoted" string' 12 | ).toString() 13 | ).toBe( 14 | '=in=(string,string*with*asterix,"string with spaces",999,"\\"quoted\\" string")' 15 | ); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/and.spec.ts: -------------------------------------------------------------------------------- 1 | import { and } from '../src'; 2 | 3 | describe('and()', () => { 4 | it('should return and-expression string', () => { 5 | const operators = [ 6 | 'field1==val1', 7 | 'field2==20', 8 | 'field3=="escaped value"', 9 | 'field4=a,field5=b', 10 | 'field6=in=(a,b,c)' 11 | ]; 12 | 13 | expect(and(...operators)).toBe( 14 | 'field1==val1;field2==20;field3=="escaped value";(field4=a,field5=b);field6=in=(a,b,c)' 15 | ); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { and } from './and.js'; 2 | export { comparison, comparison as cmp, Comparison } from './comparison.js'; 3 | export { eq } from './eq.js'; 4 | export { ge } from './ge.js'; 5 | export { gt } from './gt.js'; 6 | export { inList } from './in-list.js'; 7 | export { le } from './le.js'; 8 | export { lt } from './lt.js'; 9 | export { ne } from './ne.js'; 10 | export { or } from './or.js'; 11 | export { outList } from './out-list.js'; 12 | export { Operation, Operators } from './operation.js'; 13 | export { escapeValue } from './escape-value.js'; 14 | -------------------------------------------------------------------------------- /src/or.ts: -------------------------------------------------------------------------------- 1 | import { Comparison, GroupType } from './comparison.js'; 2 | 3 | /** 4 | * Create "or"-group operation 5 | * 6 | * @param argument Operation argument 7 | * @returns Less-or-equal operation 8 | * 9 | * @example 10 | * import {cmp, eq, ge, or} from 'rsql-builder'; 11 | * 12 | * const op = or( 13 | * cmp('year', ge(1980)), 14 | * cmp('director', eq('*Nolan')) 15 | * ); // 'year>=1980,director==*Nolan 16 | * 17 | */ 18 | export function or(...comparisons: (Comparison | string)[]): string { 19 | return comparisons.join(GroupType.OR); 20 | } 21 | -------------------------------------------------------------------------------- /src/operation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Operators signs 3 | * 4 | */ 5 | export enum Operators { 6 | EQUAL = '==', 7 | NOT_EQUAL = '!=', 8 | LESS_THAN = '<', 9 | LESS_OR_EQUAL = '<=', 10 | GREATER_THAN = '>', 11 | GREATER_OR_EQUAL = '>=', 12 | IN = '=in=', 13 | OUT = '=out=' 14 | } 15 | 16 | export type Argument = number | string | boolean; 17 | 18 | export class Operation { 19 | constructor(private args: Argument, private operator: Operators | string = Operators.EQUAL) {} 20 | 21 | toString(): string { 22 | return `${this.operator}${this.args.toString()}`; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/eq.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Operation, Operators } from './operation.js'; 2 | import { escapeValue } from './escape-value.js'; 3 | 4 | /** 5 | * Create equal operation 6 | * 7 | * @param argument Operation argument 8 | * @returns Equal operation 9 | * 10 | * @example 11 | * import {eq} from 'rsql-builder'; 12 | * 13 | * const op1 = eq(300); // '==300' 14 | * const op2 = eq('Taran*'); // '==Tarant*' 15 | * const op3 = eq('John Travolta'); // '=="John Travolta"' 16 | * 17 | */ 18 | export function eq(argument: Argument): Operation { 19 | return new Operation(escapeValue(argument), Operators.EQUAL); 20 | } 21 | -------------------------------------------------------------------------------- /src/in-list.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Operation, Operators } from './operation.js'; 2 | import { escapeValue } from './escape-value.js'; 3 | 4 | /** 5 | * Create in-list operation 6 | * 7 | * @param args Operation argument 8 | * @returns In-list operation 9 | * 10 | * @example 11 | * import {inList} from 'rsql-builder'; 12 | * 13 | * const op = inList( 14 | * 300, 15 | * 'Taran*', 16 | * 'John Travolta' 17 | * ); // '=in=(300,Taran*,"John Travolta")' 18 | * 19 | */ 20 | export function inList(...args: Argument[]): Operation { 21 | return new Operation(`(${args.map(escapeValue).join(',')})`, Operators.IN); 22 | } 23 | -------------------------------------------------------------------------------- /src/lt.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Operation, Operators } from './operation.js'; 2 | import { escapeValue } from './escape-value.js'; 3 | 4 | /** 5 | * Create less-than operation 6 | * 7 | * @param argument Operation argument 8 | * @returns Less-than operation 9 | * 10 | * @example 11 | * import {lt} from 'rsql-builder'; 12 | * 13 | * const op1 = lt(300); // '<300' 14 | * const op2 = lt('Taran*'); // '300' 14 | * const op2 = gt('Taran*'); // '>Tarant*' 15 | * const op3 = gt('John Travolta'); // '>"John Travolta"' 16 | * 17 | */ 18 | export function gt(argument: Argument): Operation { 19 | return new Operation(escapeValue(argument), Operators.GREATER_THAN); 20 | } 21 | -------------------------------------------------------------------------------- /src/out-list.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Operation, Operators } from './operation.js'; 2 | import { escapeValue } from './escape-value.js'; 3 | 4 | /** 5 | * Create out-list operation 6 | * 7 | * @param args Operation argument 8 | * @returns out-list operation 9 | * 10 | * @example 11 | * import {outList} from 'rsql-builder'; 12 | * 13 | * const op = outList( 14 | * 300, 15 | * 'Taran*', 16 | * 'John Travolta' 17 | * ); // '=out=(300,Taran*,"John Travolta")' 18 | * 19 | */ 20 | export function outList(...args: Argument[]): Operation { 21 | return new Operation(`(${args.map(escapeValue).join(',')})`, Operators.OUT); 22 | } 23 | -------------------------------------------------------------------------------- /src/le.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Operation, Operators } from './operation.js'; 2 | import { escapeValue } from './escape-value.js'; 3 | 4 | /** 5 | * Create less-or-equal operation 6 | * 7 | * @param argument Operation argument 8 | * @returns Less-or-equal operation 9 | * 10 | * @example 11 | * import {le} from 'rsql-builder'; 12 | * 13 | * const op1 = le(300); // '<=300' 14 | * const op2 = le('Taran*'); // '<=Tarant*' 15 | * const op3 = le('John Travolta'); // '<="John Travolta"' 16 | * 17 | */ 18 | export function le(argument: Argument): Operation { 19 | return new Operation(escapeValue(argument), Operators.LESS_OR_EQUAL); 20 | } 21 | -------------------------------------------------------------------------------- /src/ge.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Operation, Operators } from './operation.js'; 2 | import { escapeValue } from './escape-value.js'; 3 | 4 | /** 5 | * Create greater-or-equal operation 6 | * 7 | * @param argument Operation argument 8 | * @returns Greater-or-equal operation 9 | * 10 | * @example 11 | * import {ge} from 'rsql-builder'; 12 | * 13 | * const op1 = ge(300); // '>=300' 14 | * const op2 = ge('Taran*'); // '>=Tarant*' 15 | * const op3 = ge('John Travolta'); // '>="John Travolta"' 16 | * 17 | */ 18 | export function ge(argument: Argument): Operation { 19 | return new Operation(escapeValue(argument), Operators.GREATER_OR_EQUAL); 20 | } 21 | -------------------------------------------------------------------------------- /tests/escape-value.spec.ts: -------------------------------------------------------------------------------- 1 | import { escapeValue } from '../src/escape-value'; 2 | 3 | describe('escapeValue()', () => { 4 | it('should leave value as it is', () => { 5 | expect(escapeValue(200).toString()).toBe('200'); 6 | expect(escapeValue('string').toString()).toBe('string'); 7 | expect(escapeValue('string*with*asterix').toString()).toBe('string*with*asterix'); 8 | }); 9 | 10 | it('should quoted value', () => { 11 | expect(escapeValue('').toString()).toBe('""'); 12 | expect(escapeValue('"quoted"').toString()).toBe('"\\"quoted\\""'); 13 | expect(escapeValue('string with spaces').toString()).toBe('"string with spaces"'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | assignees: 6 | - "RomiC" 7 | commit-message: 8 | prefix: "🆙 🛠️" 9 | directory: "/" 10 | labels: 11 | - "dependencies" 12 | - "dx" 13 | open-pull-requests-limit: 10 14 | reviewers: 15 | - "RomiC" 16 | schedule: 17 | interval: "monthly" 18 | 19 | # Maintain dependencies for npm 20 | - package-ecosystem: "npm" 21 | assignees: 22 | - "RomiC" 23 | commit-message: 24 | prefix: "🆙 📦" 25 | directory: "/" 26 | labels: 27 | - "dependencies" 28 | - "dx" 29 | open-pull-requests-limit: 10 30 | reviewers: 31 | - "RomiC" 32 | schedule: 33 | interval: "monthly" 34 | versioning-strategy: increase -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage* 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | dist/ 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # IDE's 41 | .idea 42 | settings.json 43 | .vscode 44 | 45 | *.tgz 46 | .DS_Store 47 | -------------------------------------------------------------------------------- /src/comparison.ts: -------------------------------------------------------------------------------- 1 | import { Operation } from './operation.js'; 2 | 3 | /** 4 | * Comparison groups delimiters 5 | * 6 | */ 7 | export const enum GroupType { 8 | AND = ';', 9 | OR = ',' 10 | } 11 | 12 | export class Comparison { 13 | constructor(private selector: string, private operation: Operation) {} 14 | 15 | toString(): string { 16 | return `${this.selector}${this.operation.toString()}`; 17 | } 18 | } 19 | 20 | /** 21 | * Create comparison object 22 | * 23 | * @param selector Field name 24 | * @param operation Operation-instance 25 | * @returns Instance of Comparison 26 | * 27 | * @example 28 | * import {comparison, eq} from 'rsql-builder'; 29 | * 30 | * const comp = comparison('field1', eq(200)); // 'field1==200' 31 | * 32 | */ 33 | export function comparison(selector: string, operation: Operation): Comparison { 34 | return new Comparison(selector, operation); 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node: ["20", "22", "24"] 13 | name: Test with node ${{ matrix.node }} 14 | steps: 15 | - uses: actions/checkout@v5 16 | - uses: actions/setup-node@v6 17 | with: 18 | node-version: ${{ matrix.node }} 19 | - run: npm ci 20 | - run: npm test 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v5 27 | - uses: actions/setup-node@v6 28 | with: 29 | node-version: 22 30 | registry-url: https://registry.npmjs.org/ 31 | - run: npm ci 32 | - run: npm run build 33 | - run: npm publish 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /tests/comparison.spec.ts: -------------------------------------------------------------------------------- 1 | import { escapeValue } from '../src/escape-value'; 2 | import { cmp, Operation, Operators } from '../src'; 3 | 4 | describe('cmp()', () => { 5 | it('should return the correct comparison', () => { 6 | expect(cmp('field', new Operation(200, Operators.EQUAL)).toString()).toBe('field==200'); 7 | expect(cmp('field', new Operation('string', Operators.GREATER_OR_EQUAL)).toString()).toBe('field>=string'); 8 | expect(cmp('field', new Operation('string with spaces', Operators.LESS_OR_EQUAL)).toString()).toBe( 9 | 'field<=string with spaces' 10 | ); 11 | expect(cmp('field', new Operation(escapeValue('string with spaces'), Operators.LESS_OR_EQUAL)).toString()).toBe( 12 | 'field<="string with spaces"' 13 | ); 14 | expect(cmp('field', new Operation('"quoted" string', Operators.LESS_THAN)).toString()).toBe( 15 | 'field<"quoted" string' 16 | ); 17 | expect(cmp('field', new Operation(escapeValue('"quoted" string'), Operators.LESS_THAN)).toString()).toBe( 18 | 'field<"\\"quoted\\" string"' 19 | ); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /.github/workflows/npm-test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize] 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node: ["20", "22", "24"] 16 | name: Test with node v${{ matrix.node }} 17 | steps: 18 | - uses: actions/checkout@v5 19 | - uses: actions/setup-node@v6 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - run: npm ci 23 | - run: npm run lint 24 | - run: npm test 25 | - run: npm run build 26 | - uses: coverallsapp/github-action@master 27 | with: 28 | github-token: ${{ secrets.github_token }} 29 | flag-name: node v${{ matrix.node }} 30 | parallel: true 31 | 32 | finish: 33 | needs: test 34 | runs-on: ubuntu-latest 35 | name: Publishing coverage report 36 | steps: 37 | - uses: coverallsapp/github-action@master 38 | with: 39 | github-token: ${{ secrets.github_token }} 40 | parallel-finished: true 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Roman Charugin 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 | -------------------------------------------------------------------------------- /src/and.ts: -------------------------------------------------------------------------------- 1 | import { Comparison, GroupType } from './comparison.js'; 2 | 3 | function hasOrOperation(operation: string): boolean { 4 | let insideBracket = false; 5 | 6 | for (const char of operation) { 7 | switch (char) { 8 | case '(': 9 | insideBracket = true; 10 | break; 11 | 12 | case ')': 13 | insideBracket = false; 14 | break; 15 | 16 | case GroupType.OR: 17 | if (!insideBracket) { 18 | return true; 19 | } 20 | break; 21 | } 22 | } 23 | 24 | return false; 25 | } 26 | 27 | /** 28 | * Generate "and"-group of comparisons 29 | * 30 | * @param comparisons List of comparisons or strings (for comparision group) 31 | * @returns "and"-group string 32 | * 33 | * @example 34 | * import {and, cmp, eq, ge} from 'rsql-builder'; 35 | * 36 | * const op = and( 37 | * cmp('year', ge(1980)), 38 | * comparision('director', eq('Quentin Tarantino')) 39 | * ); // 'year>=1980;director=="Quentin Tarantino" 40 | * 41 | */ 42 | export function and(...comparisons: (Comparison | string)[]): string { 43 | return comparisons 44 | .map((comparison) => 45 | typeof comparison === 'string' && hasOrOperation(comparison) ? `(${comparison})` : comparison 46 | ) 47 | .join(GroupType.AND); 48 | } 49 | -------------------------------------------------------------------------------- /tests/custom-operator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Argument, Operation } from '../src/operation'; 2 | import { escapeValue } from '../src/escape-value'; 3 | 4 | function customOperator(value: Argument): Operation { 5 | return new Operation(escapeValue(value), '=custom='); 6 | } 7 | 8 | function customListOperator(value: Argument[]): Operation { 9 | return new Operation(`(${escapeValue(value)})`, '=customListOperator='); 10 | } 11 | 12 | describe('custom operator', () => { 13 | it('should return custom-expression string when a number is provided', () => { 14 | expect(customOperator(100).toString()).toBe('=custom=100'); 15 | }); 16 | 17 | it('should return custom-expression string when a string is provided', () => { 18 | expect(customOperator('string').toString()).toBe('=custom=string'); 19 | }); 20 | 21 | it('should return custom-expression string when a string with spaces is provided', () => { 22 | expect(customOperator('"quoted" string').toString()).toBe('=custom="\\"quoted\\" string"'); 23 | }); 24 | 25 | it('should return custom-expression string when a string with quotes is provided', () => { 26 | expect(customOperator('"quoted" string').toString()).toBe('=custom="\\"quoted\\" string"'); 27 | }); 28 | }); 29 | 30 | describe('custom list operator', () => { 31 | it('should return custom-expression string when a number is provided', () => { 32 | expect(customListOperator(['first', 'second']).toString()).toBe('=customListOperator=(first,second)'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { Comparison, and, cmp, comparison, eq, escapeValue, ge, gt, inList, le, lt, ne, or, outList } from '../src'; 2 | 3 | describe('Functional tests', () => { 4 | it('should export public functions', () => { 5 | expect(typeof and).toBe('function'); 6 | expect(typeof cmp).toBe('function'); 7 | expect(typeof comparison).toBe('function'); 8 | expect(cmp).toBe(comparison); 9 | expect(typeof eq).toBe('function'); 10 | expect(typeof ge).toBe('function'); 11 | expect(typeof gt).toBe('function'); 12 | expect(typeof inList).toBe('function'); 13 | expect(typeof le).toBe('function'); 14 | expect(typeof lt).toBe('function'); 15 | expect(typeof ne).toBe('function'); 16 | expect(typeof or).toBe('function'); 17 | expect(typeof outList).toBe('function'); 18 | expect(typeof Comparison).toBe('function'); 19 | expect(typeof escapeValue).toBe('function'); 20 | }); 21 | 22 | it('should build the query', () => { 23 | expect(and(cmp('name', eq('')), cmp('year', gt(2003)))).toBe('name=="";year>2003'); 24 | 25 | expect(and(cmp('name', eq('Kill Bill')), cmp('year', gt(2003)))).toBe('name=="Kill Bill";year>2003'); 26 | 27 | expect( 28 | and( 29 | cmp('genres', inList('sci-fi', 'action', 'non fiction')), 30 | or(cmp('director', eq('Christopher Nolan')), cmp('actor', eq('*Bale'))), 31 | cmp('year', ge(2000)) 32 | ) 33 | ).toBe('genres=in=(sci-fi,action,"non fiction");(director=="Christopher Nolan",actor==*Bale);year>=2000'); 34 | 35 | expect(and(cmp('director.lastName', eq('Nolan')), cmp('year', ge(2000)), cmp('year', lt(2010)))).toBe( 36 | 'director.lastName==Nolan;year>=2000;year<2010' 37 | ); 38 | 39 | expect( 40 | or( 41 | and(cmp('genres', inList('sci-fi', 'action')), cmp('genres', outList('romance', 'animated', 'horror'))), 42 | cmp('director', eq('Que*Tarantino')) 43 | ) 44 | ).toBe('genres=in=(sci-fi,action);genres=out=(romance,animated,horror),director==Que*Tarantino'); 45 | 46 | expect(or(cmp('genres', inList('sci-fi', 'action')), cmp('director', eq('Que*Tarantino')))).toBe( 47 | 'genres=in=(sci-fi,action),director==Que*Tarantino' 48 | ); 49 | 50 | expect( 51 | and( 52 | cmp('year', ge(1980)), 53 | or(cmp('genres', inList('sci-fi', 'action')), cmp('year', le(2000))), 54 | cmp('director.lastName', ne('Tarant*')) 55 | ) 56 | ).toBe('year>=1980;(genres=in=(sci-fi,action),year<=2000);director.lastName!=Tarant*'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rsql-builder", 3 | "version": "1.3.0", 4 | "description": "RSQL query builder", 5 | "type": "module", 6 | "main": "./dist/cjs/index.js", 7 | "exports": { 8 | "import": "./dist/esm/index.js", 9 | "require": "./dist/cjs/index.js" 10 | }, 11 | "scripts": { 12 | "build": "rm -rf ./dist && npm run build:cjs && npm run build:esm", 13 | "build:cjs": "tsc -p . && echo '{\"type\":\"commonjs\"}' > ./dist/cjs/package.json", 14 | "build:esm": "tsc --module ES6 --outDir dist/esm -p .", 15 | "lint": "eslint README.md src tests", 16 | "lint:fix": "npm run lint -- --fix", 17 | "precommit": "lint-staged", 18 | "prepare": "husky install", 19 | "prepush": "npm test", 20 | "start": "ts-node --files -P ./tsconfig.json", 21 | "test": "jest" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/RomiC/rsql-builder.git" 26 | }, 27 | "keywords": [ 28 | "rsql", 29 | "query", 30 | "builder" 31 | ], 32 | "engines": { 33 | "node": ">= 20.0.0" 34 | }, 35 | "author": "Roman Charugin ", 36 | "contributors": [ 37 | "Roman Charugin ", 38 | "Mihael Šafarić " 39 | ], 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/RomiC/rsql-builder/issues" 43 | }, 44 | "homepage": "https://github.com/RomiC/rsql-builder#readme", 45 | "devDependencies": { 46 | "@types/jest": "29.5.12", 47 | "@types/node": "^24.9.2", 48 | "@typescript-eslint/eslint-plugin": "7.0.0", 49 | "@typescript-eslint/parser": "6.21.0", 50 | "caniuse-lite": "1.0.30001752", 51 | "eslint": "8.57.0", 52 | "eslint-config-prettier": "10.1.8", 53 | "eslint-plugin-import": "2.32.0", 54 | "eslint-plugin-jsdoc": "61.1.11", 55 | "eslint-plugin-markdown": "3.0.1", 56 | "eslint-plugin-prefer-arrow": "1.2.3", 57 | "eslint-plugin-prettier": "5.5.4", 58 | "husky": "9.1.7", 59 | "jest": "29.7.0", 60 | "lint-staged": "16.2.6", 61 | "prettier": "3.6.2", 62 | "ts-jest": "29.4.5", 63 | "typescript": "5.9.3" 64 | }, 65 | "lint-staged": { 66 | "*.ts": "npm run lint:fix" 67 | }, 68 | "jest": { 69 | "preset": "ts-jest", 70 | "testEnvironment": "node", 71 | "collectCoverage": true, 72 | "collectCoverageFrom": [ 73 | "src/*.ts" 74 | ], 75 | "coverageReporters": [ 76 | "lcov" 77 | ], 78 | "moduleNameMapper": { 79 | "./and.js": "/src/and.ts", 80 | "./comparison.js": "/src/comparison.ts", 81 | "./eq.js": "/src/eq.ts", 82 | "./ge.js": "/src/ge.ts", 83 | "./gt.js": "/src/gt.ts", 84 | "./in-list.js": "/src/in-list.ts", 85 | "./le.js": "/src/le.ts", 86 | "./lt.js": "/src/lt.ts", 87 | "./ne.js": "/src/ne.ts", 88 | "./or.js": "/src/or.ts", 89 | "./out-list.js": "/src/out-list.ts", 90 | "./escape-value.js": "/src/escape-value.ts", 91 | "./operation.js": "/src/operation.ts" 92 | }, 93 | "roots": [ 94 | "/src/", 95 | "/tests/" 96 | ] 97 | } 98 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rsql-builder [![Coverage Status](https://coveralls.io/repos/github/RomiC/rsql-builder/badge.svg)](https://coveralls.io/github/RomiC/rsql-builder) 2 | 3 | Here is the simple rsql-query builder utility. It's as minimal as possible but quite powerful at the same time. 4 | 5 | ```js 6 | import { and, cmp, eq, ge, inList, or } from 'rsql-builder'; // or const { and, cmp, eq, ge, inList, or } = require('rsql-builder') 7 | 8 | const query = and( 9 | cmp('genres', inList('sci-fi', 'action', 'non fiction')), 10 | or(cmp('director', eq('Christopher Nolan')), cmp('actor', eq('*Bale'))), 11 | cmp('year', ge(2000)) 12 | ); // 'genres=in=(sci-fi,action,"non fiction");(director=="Christopher Nolan",actor==*Bale);year>=2000' 13 | ``` 14 | 15 | ## Installation 16 | 17 | ```sh 18 | npm install --save rsql-builder 19 | ``` 20 | 21 | ## Available methods 22 | 23 | ### `and(...comparisons): string` 24 | 25 | Create "and"-group of comparison. 26 | 27 | **Arguments** 28 | 29 | - `comparisons` – list of comparisons, instances of Comparison-class or strings (for other comparison groups) 30 | 31 | **Example** 32 | 33 | ```js 34 | import { and, cmp, eq, ge } from 'rsql-builder'; 35 | 36 | const op = and(cmp('year', ge(1980)), cmp('director', eq('Quentin Tarantino'))); // 'year>=1980;director=="Quentin Tarantino" 37 | ``` 38 | 39 | ### `or(...comparisons): string` 40 | 41 | Create "or"-group of comparison. 42 | 43 | **Arguments** 44 | 45 | - `comparisons` – list of comparisons, instances of Comparison-class or strings (for other comparison groups) 46 | 47 | **Example** 48 | 49 | ```js 50 | import { cmp, eq, ge, or } from 'rsql-builder'; 51 | 52 | const op = or(cmp('year', ge(1980)), cmp('director', eq('Quentin Tarantino'))); // 'year>=1980,director=="Quentin Tarantino" 53 | ``` 54 | 55 | ### `cmp(field, operation): Comparison` or `comparison(field, operation): Comparison` 56 | 57 | Create a new comparison for the field. 58 | 59 | **Arguments** 60 | 61 | - `field {string}` – field name 62 | - `operation {Operation}` - operation 63 | 64 | **Example** 65 | 66 | ```js 67 | import { cmp, eq } from 'rsql-builder'; 68 | 69 | const comp = cmp('field1', eq(200)); // 'field1==200' 70 | ``` 71 | 72 | ### `eq(argument): Operation` 73 | 74 | Create "equal"-operation. 75 | 76 | **Arguments** 77 | 78 | - `argument` – Any kind of value 79 | 80 | **Example** 81 | 82 | ```js 83 | import { eq } from 'rsql-builder'; 84 | 85 | const op1 = eq(300); // '==300' 86 | const op2 = eq('Taran*'); // '==Tarant*' 87 | const op3 = eq('John Travolta'); // '=="John Travolta"' 88 | ``` 89 | 90 | ### `ge(argument): Operation` 91 | 92 | Create greater-or-equal operation 93 | 94 | **Arguments** 95 | 96 | - `argument` – Any kind of value 97 | 98 | **Example** 99 | 100 | ```js 101 | import { ge } from 'rsql-builder'; 102 | 103 | const op1 = ge(300); // '>=300' 104 | const op2 = ge('Taran*'); // '>=Tarant*' 105 | const op3 = ge('John Travolta'); // '>="John Travolta"' 106 | ``` 107 | 108 | ### `gt(argument): Operation` 109 | 110 | Create greater-than operation 111 | 112 | **Arguments** 113 | 114 | - `argument` – Any kind of value 115 | 116 | **Example** 117 | 118 | ```js 119 | import { gt } from 'rsql-builder'; 120 | 121 | const op1 = gt(300); // '>=300' 122 | const op2 = gt('Taran*'); // '>=Tarant*' 123 | const op3 = gt('John Travolta'); // '>="John Travolta"' 124 | ``` 125 | 126 | ### `inList(...args): Operation` 127 | 128 | Create in-list operation 129 | 130 | **Arguments** 131 | 132 | - `args` – List of any values 133 | 134 | **Example** 135 | 136 | ```js 137 | import { inList } from 'rsql-builder'; 138 | 139 | const op = inList(300, 'Taran*', 'John Travolta'); // '=in=(300,Taran*,"John Travolta")' 140 | ``` 141 | 142 | ### `le(argument): Operation` 143 | 144 | Create less-or-equal operation 145 | 146 | **Arguments** 147 | 148 | - `argument` – Any kind of value 149 | 150 | **Example** 151 | 152 | ```js 153 | import { le } from 'rsql-builder'; 154 | 155 | const op1 = le(300); // '<=300' 156 | const op2 = le('Taran*'); // '<=Tarant*' 157 | const op3 = le('John Travolta'); // '<="John Travolta"' 158 | ``` 159 | 160 | ### `lt(argument): Operation` 161 | 162 | Create less-than operation 163 | 164 | **Arguments** 165 | 166 | - `argument` – Any kind of value 167 | 168 | **Example** 169 | 170 | ```js 171 | import { lt } from 'rsql-builder'; 172 | 173 | const op1 = lt(300); // '<300' 174 | const op2 = lt('Taran*'); // ' void`." 28 | }, 29 | "Boolean": { 30 | "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" 31 | }, 32 | "Number": { 33 | "message": "Avoid using the `Number` type. Did you mean `number`?" 34 | }, 35 | "String": { 36 | "message": "Avoid using the `String` type. Did you mean `string`?" 37 | }, 38 | "Symbol": { 39 | "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" 40 | } 41 | } 42 | } 43 | ], 44 | "@typescript-eslint/consistent-type-assertions": "off", 45 | "@typescript-eslint/dot-notation": "off", 46 | "@typescript-eslint/explicit-member-accessibility": [ 47 | "off", 48 | { 49 | "accessibility": "explicit" 50 | } 51 | ], 52 | "@typescript-eslint/indent": ["error", 2], 53 | "@typescript-eslint/naming-convention": "off", 54 | "@typescript-eslint/no-empty-function": "error", 55 | "@typescript-eslint/no-empty-interface": "error", 56 | "@typescript-eslint/no-explicit-any": "off", 57 | "@typescript-eslint/no-misused-new": "error", 58 | "@typescript-eslint/no-namespace": "error", 59 | "@typescript-eslint/no-parameter-properties": "off", 60 | "@typescript-eslint/no-shadow": "error", 61 | "@typescript-eslint/no-unused-expressions": "error", 62 | "@typescript-eslint/no-use-before-define": "off", 63 | "@typescript-eslint/no-var-requires": "error", 64 | "@typescript-eslint/prefer-for-of": "error", 65 | "@typescript-eslint/prefer-function-type": "error", 66 | "@typescript-eslint/prefer-namespace-keyword": "error", 67 | "@typescript-eslint/quotes": [ 68 | "warn", 69 | "single", 70 | { 71 | "avoidEscape": true 72 | } 73 | ], 74 | "@typescript-eslint/triple-slash-reference": [ 75 | "off", 76 | { 77 | "path": "always", 78 | "types": "prefer-import", 79 | "lib": "always" 80 | } 81 | ], 82 | "@typescript-eslint/unified-signatures": "error", 83 | "arrow-parens": ["warn", "always"], 84 | "comma-dangle": "error", 85 | "complexity": "off", 86 | "constructor-super": "error", 87 | "eol-last": "off", 88 | "eqeqeq": ["error", "smart"], 89 | "guard-for-in": "error", 90 | "id-blacklist": "warn", 91 | "id-match": "warn", 92 | "import/newline-after-import": "error", 93 | "import/no-default-export": "error", 94 | "import/order": [ 95 | "error", 96 | { 97 | "alphabetize": { 98 | "order": "desc", 99 | "orderImportKind": "asc", 100 | "caseInsensitive": true 101 | }, 102 | "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], 103 | "newlines-between": "always" 104 | } 105 | ], 106 | "jsdoc/check-alignment": "error", 107 | "jsdoc/check-indentation": "error", 108 | "jsdoc/tag-lines": [ 109 | "error", 110 | "any", 111 | { 112 | "startLines": 1, 113 | "tags": { 114 | "example": { 115 | "lines": "always", 116 | "count": 1 117 | } 118 | } 119 | } 120 | ], 121 | "max-classes-per-file": "off", 122 | "max-len": [ 123 | "error", 124 | { 125 | "code": 120, 126 | "comments": 160, 127 | "ignoreComments": false, 128 | "ignoreTrailingComments": true, 129 | "tabWidth": 2 130 | } 131 | ], 132 | "new-parens": "error", 133 | "no-bitwise": "error", 134 | "no-caller": "error", 135 | "no-cond-assign": "error", 136 | "no-console": "error", 137 | "no-debugger": "error", 138 | "no-empty": "error", 139 | "no-eval": "error", 140 | "no-extra-boolean-cast": ["warn", { "enforceForLogicalOperands": true }], 141 | "no-extra-semi": "warn", 142 | "no-fallthrough": "off", 143 | "no-invalid-this": "off", 144 | "no-multi-spaces": "warn", 145 | "no-new-wrappers": "error", 146 | "no-shadow": "off", 147 | "no-throw-literal": "error", 148 | "no-trailing-spaces": "error", 149 | "no-undef-init": "error", 150 | "no-underscore-dangle": "warn", 151 | "no-unsafe-finally": "error", 152 | "no-unused-labels": "error", 153 | "no-var": "error", 154 | "object-curly-spacing": ["warn", "always"], 155 | "object-shorthand": "error", 156 | "one-var": ["error", "never"], 157 | "prefer-arrow/prefer-arrow-functions": "off", 158 | "prefer-const": "error", 159 | "quotes": ["warn", "single"], 160 | "quote-props": ["warn", "consistent-as-needed"], 161 | "radix": "error", 162 | "space-in-parens": ["warn", "never"], 163 | "spaced-comment": [ 164 | "warn", 165 | "always", 166 | { 167 | "markers": ["/"] 168 | } 169 | ], 170 | "use-isnan": "error", 171 | "valid-typeof": "off" 172 | }, 173 | "overrides": [ 174 | { 175 | "files": ["*.ts"], 176 | "parser": "@typescript-eslint/parser", 177 | "extends": ["plugin:@typescript-eslint/recommended"], 178 | "parserOptions": { 179 | "project": ["tsconfig.json", "tests/tsconfig.json"], 180 | "sourceType": "module" 181 | } 182 | } 183 | ] 184 | } 185 | --------------------------------------------------------------------------------