@plumile/filter-query 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/lib/errors.d.ts +39 -0
  4. package/lib/errors.d.ts.map +1 -0
  5. package/lib/errors.js +2 -0
  6. package/lib/esm/errors.d.ts +39 -0
  7. package/lib/esm/errors.d.ts.map +1 -0
  8. package/lib/esm/errors.js +2 -0
  9. package/lib/esm/index.d.ts +7 -0
  10. package/lib/esm/index.d.ts.map +1 -0
  11. package/lib/esm/index.js +5 -0
  12. package/lib/esm/mutate.d.ts +6 -0
  13. package/lib/esm/mutate.d.ts.map +1 -0
  14. package/lib/esm/mutate.js +88 -0
  15. package/lib/esm/parse.d.ts +3 -0
  16. package/lib/esm/parse.d.ts.map +1 -0
  17. package/lib/esm/parse.js +164 -0
  18. package/lib/esm/schema.d.ts +5 -0
  19. package/lib/esm/schema.d.ts.map +1 -0
  20. package/lib/esm/schema.js +55 -0
  21. package/lib/esm/stringify.d.ts +3 -0
  22. package/lib/esm/stringify.d.ts.map +1 -0
  23. package/lib/esm/stringify.js +50 -0
  24. package/lib/esm/types.d.ts +43 -0
  25. package/lib/esm/types.d.ts.map +1 -0
  26. package/lib/esm/types.js +2 -0
  27. package/lib/index.d.ts +7 -0
  28. package/lib/index.d.ts.map +1 -0
  29. package/lib/index.js +5 -0
  30. package/lib/mutate.d.ts +6 -0
  31. package/lib/mutate.d.ts.map +1 -0
  32. package/lib/mutate.js +88 -0
  33. package/lib/parse.d.ts +3 -0
  34. package/lib/parse.d.ts.map +1 -0
  35. package/lib/parse.js +164 -0
  36. package/lib/schema.d.ts +5 -0
  37. package/lib/schema.d.ts.map +1 -0
  38. package/lib/schema.js +55 -0
  39. package/lib/stringify.d.ts +3 -0
  40. package/lib/stringify.d.ts.map +1 -0
  41. package/lib/stringify.js +50 -0
  42. package/lib/tsconfig.esm.tsbuildinfo +1 -0
  43. package/lib/types/errors.d.ts +39 -0
  44. package/lib/types/errors.d.ts.map +1 -0
  45. package/lib/types/index.d.ts +7 -0
  46. package/lib/types/index.d.ts.map +1 -0
  47. package/lib/types/mutate.d.ts +6 -0
  48. package/lib/types/mutate.d.ts.map +1 -0
  49. package/lib/types/parse.d.ts +3 -0
  50. package/lib/types/parse.d.ts.map +1 -0
  51. package/lib/types/schema.d.ts +5 -0
  52. package/lib/types/schema.d.ts.map +1 -0
  53. package/lib/types/stringify.d.ts +3 -0
  54. package/lib/types/stringify.d.ts.map +1 -0
  55. package/lib/types/types.d.ts +43 -0
  56. package/lib/types/types.d.ts.map +1 -0
  57. package/lib/types.d.ts +43 -0
  58. package/lib/types.d.ts.map +1 -0
  59. package/lib/types.js +2 -0
  60. package/package.json +43 -0
  61. package/src/__tests__/additional-coverage.test.ts +82 -0
  62. package/src/__tests__/list-edge.test.ts +26 -0
  63. package/src/__tests__/mutate.test.ts +33 -0
  64. package/src/__tests__/parse-stringify.test.ts +104 -0
  65. package/src/__tests__/remove-filter.test.ts +46 -0
  66. package/src/__tests__/schema-edge.test.ts +46 -0
  67. package/src/__tests__/schema-infer.test-d.ts +24 -0
  68. package/src/__tests__/stability-and-diagnostics.test.ts +40 -0
  69. package/src/errors.ts +46 -0
  70. package/src/index.ts +6 -0
  71. package/src/mutate.ts +132 -0
  72. package/src/parse.ts +221 -0
  73. package/src/schema.ts +81 -0
  74. package/src/stringify.ts +60 -0
  75. package/src/types.ts +88 -0
  76. package/tools/build-package.sh +5 -0
  77. package/tools/test-build-package.sh +4 -0
  78. package/tsconfig.build.json +8 -0
  79. package/tsconfig.esm.json +7 -0
  80. package/tsconfig.json +8 -0
  81. package/tsconfig.types.json +9 -0
  82. package/vitest.config.ts +7 -0
@@ -0,0 +1,43 @@
1
+ import type { Diagnostic } from './errors.js';
2
+ export type ScalarOperator = 'gt' | 'gte' | 'lt' | 'lte' | 'eq' | 'neq' | 'contains' | 'sw' | 'ew';
3
+ export type BetweenOperator = 'between';
4
+ export type ListOperator = 'in' | 'nin';
5
+ export type Operator = ScalarOperator | BetweenOperator | ListOperator;
6
+ export type PrimitiveKind = 'number' | 'string';
7
+ export interface FieldDescriptorBase<TKind extends PrimitiveKind> {
8
+ readonly kind: TKind;
9
+ readonly operators: readonly Operator[];
10
+ readonly parse?: (raw: string) => unknown;
11
+ readonly serialize?: (value: unknown) => string;
12
+ }
13
+ export type NumberFieldDescriptor = FieldDescriptorBase<'number'> & {
14
+ readonly parse?: (raw: string) => number | undefined;
15
+ readonly serialize?: (value: number) => string;
16
+ };
17
+ export type StringFieldDescriptor = FieldDescriptorBase<'string'> & {
18
+ readonly parse?: (raw: string) => string | undefined;
19
+ readonly serialize?: (value: string) => string;
20
+ };
21
+ export type FieldDescriptor = NumberFieldDescriptor | StringFieldDescriptor;
22
+ export type Schema = Readonly<Record<string, FieldDescriptor>>;
23
+ type OperatorKeys<D extends FieldDescriptor> = Extract<D['operators'][number], ScalarOperator>;
24
+ type ScalarMap<D extends FieldDescriptor> = D extends FieldDescriptorBase<'number'> ? Record<OperatorKeys<D>, number | undefined> : Record<OperatorKeys<D>, string | undefined>;
25
+ type BetweenMap<D extends FieldDescriptor> = 'between' extends D['operators'][number] ? {
26
+ between?: D extends FieldDescriptorBase<'number'> ? readonly [number, number] : readonly [string, string];
27
+ } : Record<never, never>;
28
+ type ListOps<D extends FieldDescriptor> = Extract<D['operators'][number], ListOperator>;
29
+ type ListMap<D extends FieldDescriptor> = ('in' extends ListOps<D> ? {
30
+ in?: D extends FieldDescriptorBase<'number'> ? readonly number[] : readonly string[];
31
+ } : Record<never, never>) & ('nin' extends ListOps<D> ? {
32
+ nin?: D extends FieldDescriptorBase<'number'> ? readonly number[] : readonly string[];
33
+ } : Record<never, never>);
34
+ export type InferField<D extends FieldDescriptor> = Partial<ScalarMap<D> & BetweenMap<D> & ListMap<D>>;
35
+ export type InferFilters<S extends Schema> = {
36
+ [K in keyof S]?: InferField<S[K]>;
37
+ };
38
+ export interface ParseResult<S extends Schema> {
39
+ readonly filters: InferFilters<S>;
40
+ readonly diagnostics: Diagnostic[];
41
+ }
42
+ export {};
43
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
package/lib/types.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { Diagnostic } from './errors.js';
2
+ export type ScalarOperator = 'gt' | 'gte' | 'lt' | 'lte' | 'eq' | 'neq' | 'contains' | 'sw' | 'ew';
3
+ export type BetweenOperator = 'between';
4
+ export type ListOperator = 'in' | 'nin';
5
+ export type Operator = ScalarOperator | BetweenOperator | ListOperator;
6
+ export type PrimitiveKind = 'number' | 'string';
7
+ export interface FieldDescriptorBase<TKind extends PrimitiveKind> {
8
+ readonly kind: TKind;
9
+ readonly operators: readonly Operator[];
10
+ readonly parse?: (raw: string) => unknown;
11
+ readonly serialize?: (value: unknown) => string;
12
+ }
13
+ export type NumberFieldDescriptor = FieldDescriptorBase<'number'> & {
14
+ readonly parse?: (raw: string) => number | undefined;
15
+ readonly serialize?: (value: number) => string;
16
+ };
17
+ export type StringFieldDescriptor = FieldDescriptorBase<'string'> & {
18
+ readonly parse?: (raw: string) => string | undefined;
19
+ readonly serialize?: (value: string) => string;
20
+ };
21
+ export type FieldDescriptor = NumberFieldDescriptor | StringFieldDescriptor;
22
+ export type Schema = Readonly<Record<string, FieldDescriptor>>;
23
+ type OperatorKeys<D extends FieldDescriptor> = Extract<D['operators'][number], ScalarOperator>;
24
+ type ScalarMap<D extends FieldDescriptor> = D extends FieldDescriptorBase<'number'> ? Record<OperatorKeys<D>, number | undefined> : Record<OperatorKeys<D>, string | undefined>;
25
+ type BetweenMap<D extends FieldDescriptor> = 'between' extends D['operators'][number] ? {
26
+ between?: D extends FieldDescriptorBase<'number'> ? readonly [number, number] : readonly [string, string];
27
+ } : Record<never, never>;
28
+ type ListOps<D extends FieldDescriptor> = Extract<D['operators'][number], ListOperator>;
29
+ type ListMap<D extends FieldDescriptor> = ('in' extends ListOps<D> ? {
30
+ in?: D extends FieldDescriptorBase<'number'> ? readonly number[] : readonly string[];
31
+ } : Record<never, never>) & ('nin' extends ListOps<D> ? {
32
+ nin?: D extends FieldDescriptorBase<'number'> ? readonly number[] : readonly string[];
33
+ } : Record<never, never>);
34
+ export type InferField<D extends FieldDescriptor> = Partial<ScalarMap<D> & BetweenMap<D> & ListMap<D>>;
35
+ export type InferFilters<S extends Schema> = {
36
+ [K in keyof S]?: InferField<S[K]>;
37
+ };
38
+ export interface ParseResult<S extends Schema> {
39
+ readonly filters: InferFilters<S>;
40
+ readonly diagnostics: Diagnostic[];
41
+ }
42
+ export {};
43
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB0eXBlIHsgRGlhZ25vc3RpYyB9IGZyb20gJy4vZXJyb3JzLmpzJztcblxuZXhwb3J0IHR5cGUgU2NhbGFyT3BlcmF0b3IgPVxuICB8ICdndCdcbiAgfCAnZ3RlJ1xuICB8ICdsdCdcbiAgfCAnbHRlJ1xuICB8ICdlcSdcbiAgfCAnbmVxJ1xuICB8ICdjb250YWlucydcbiAgfCAnc3cnXG4gIHwgJ2V3JztcbmV4cG9ydCB0eXBlIEJldHdlZW5PcGVyYXRvciA9ICdiZXR3ZWVuJztcbmV4cG9ydCB0eXBlIExpc3RPcGVyYXRvciA9ICdpbicgfCAnbmluJztcbmV4cG9ydCB0eXBlIE9wZXJhdG9yID0gU2NhbGFyT3BlcmF0b3IgfCBCZXR3ZWVuT3BlcmF0b3IgfCBMaXN0T3BlcmF0b3I7XG5cbmV4cG9ydCB0eXBlIFByaW1pdGl2ZUtpbmQgPSAnbnVtYmVyJyB8ICdzdHJpbmcnO1xuXG5leHBvcnQgaW50ZXJmYWNlIEZpZWxkRGVzY3JpcHRvckJhc2U8VEtpbmQgZXh0ZW5kcyBQcmltaXRpdmVLaW5kPiB7XG4gIHJlYWRvbmx5IGtpbmQ6IFRLaW5kO1xuICByZWFkb25seSBvcGVyYXRvcnM6IHJlYWRvbmx5IE9wZXJhdG9yW107XG4gIHJlYWRvbmx5IHBhcnNlPzogKHJhdzogc3RyaW5nKSA9PiB1bmtub3duO1xuICByZWFkb25seSBzZXJpYWxpemU/OiAodmFsdWU6IHVua25vd24pID0+IHN0cmluZztcbn1cblxuZXhwb3J0IHR5cGUgTnVtYmVyRmllbGREZXNjcmlwdG9yID0gRmllbGREZXNjcmlwdG9yQmFzZTwnbnVtYmVyJz4gJiB7XG4gIHJlYWRvbmx5IHBhcnNlPzogKHJhdzogc3RyaW5nKSA9PiBudW1iZXIgfCB1bmRlZmluZWQ7XG4gIHJlYWRvbmx5IHNlcmlhbGl6ZT86ICh2YWx1ZTogbnVtYmVyKSA9PiBzdHJpbmc7XG59O1xuZXhwb3J0IHR5cGUgU3RyaW5nRmllbGREZXNjcmlwdG9yID0gRmllbGREZXNjcmlwdG9yQmFzZTwnc3RyaW5nJz4gJiB7XG4gIHJlYWRvbmx5IHBhcnNlPzogKHJhdzogc3RyaW5nKSA9PiBzdHJpbmcgfCB1bmRlZmluZWQ7XG4gIHJlYWRvbmx5IHNlcmlhbGl6ZT86ICh2YWx1ZTogc3RyaW5nKSA9PiBzdHJpbmc7XG59O1xuXG5leHBvcnQgdHlwZSBGaWVsZERlc2NyaXB0b3IgPSBOdW1iZXJGaWVsZERlc2NyaXB0b3IgfCBTdHJpbmdGaWVsZERlc2NyaXB0b3I7XG5cbmV4cG9ydCB0eXBlIFNjaGVtYSA9IFJlYWRvbmx5PFJlY29yZDxzdHJpbmcsIEZpZWxkRGVzY3JpcHRvcj4+O1xuXG4vLyBJbmZlciBvcGVyYXRvciBtYXBwaW5nIGZvciBhIHNpbmdsZSBmaWVsZCBkZXNjcmlwdG9yXG50eXBlIE9wZXJhdG9yS2V5czxEIGV4dGVuZHMgRmllbGREZXNjcmlwdG9yPiA9IEV4dHJhY3Q8XG4gIERbJ29wZXJhdG9ycyddW251bWJlcl0sXG4gIFNjYWxhck9wZXJhdG9yXG4+O1xudHlwZSBTY2FsYXJNYXA8RCBleHRlbmRzIEZpZWxkRGVzY3JpcHRvcj4gPVxuICBEIGV4dGVuZHMgRmllbGREZXNjcmlwdG9yQmFzZTwnbnVtYmVyJz5cbiAgICA/IFJlY29yZDxPcGVyYXRvcktleXM8RD4sIG51bWJlciB8IHVuZGVmaW5lZD5cbiAgICA6IFJlY29yZDxPcGVyYXRvcktleXM8RD4sIHN0cmluZyB8IHVuZGVmaW5lZD47XG5cbnR5cGUgQmV0d2Vlbk1hcDxEIGV4dGVuZHMgRmllbGREZXNjcmlwdG9yPiA9XG4gICdiZXR3ZWVuJyBleHRlbmRzIERbJ29wZXJhdG9ycyddW251bWJlcl1cbiAgICA/IHtcbiAgICAgICAgYmV0d2Vlbj86IEQgZXh0ZW5kcyBGaWVsZERlc2NyaXB0b3JCYXNlPCdudW1iZXInPlxuICAgICAgICAgID8gcmVhZG9ubHkgW251bWJlciwgbnVtYmVyXVxuICAgICAgICAgIDogcmVhZG9ubHkgW3N0cmluZywgc3RyaW5nXTtcbiAgICAgIH1cbiAgICA6IFJlY29yZDxuZXZlciwgbmV2ZXI+O1xuXG50eXBlIExpc3RPcHM8RCBleHRlbmRzIEZpZWxkRGVzY3JpcHRvcj4gPSBFeHRyYWN0PFxuICBEWydvcGVyYXRvcnMnXVtudW1iZXJdLFxuICBMaXN0T3BlcmF0b3Jcbj47XG50eXBlIExpc3RNYXA8RCBleHRlbmRzIEZpZWxkRGVzY3JpcHRvcj4gPSAoJ2luJyBleHRlbmRzIExpc3RPcHM8RD5cbiAgPyB7XG4gICAgICBpbj86IEQgZXh0ZW5kcyBGaWVsZERlc2NyaXB0b3JCYXNlPCdudW1iZXInPlxuICAgICAgICA/IHJlYWRvbmx5IG51bWJlcltdXG4gICAgICAgIDogcmVhZG9ubHkgc3RyaW5nW107XG4gICAgfVxuICA6IFJlY29yZDxuZXZlciwgbmV2ZXI+KSAmXG4gICgnbmluJyBleHRlbmRzIExpc3RPcHM8RD5cbiAgICA/IHtcbiAgICAgICAgbmluPzogRCBleHRlbmRzIEZpZWxkRGVzY3JpcHRvckJhc2U8J251bWJlcic+XG4gICAgICAgICAgPyByZWFkb25seSBudW1iZXJbXVxuICAgICAgICAgIDogcmVhZG9ubHkgc3RyaW5nW107XG4gICAgICB9XG4gICAgOiBSZWNvcmQ8bmV2ZXIsIG5ldmVyPik7XG5cbmV4cG9ydCB0eXBlIEluZmVyRmllbGQ8RCBleHRlbmRzIEZpZWxkRGVzY3JpcHRvcj4gPSBQYXJ0aWFsPFxuICBTY2FsYXJNYXA8RD4gJiBCZXR3ZWVuTWFwPEQ+ICYgTGlzdE1hcDxEPlxuPjtcblxuZXhwb3J0IHR5cGUgSW5mZXJGaWx0ZXJzPFMgZXh0ZW5kcyBTY2hlbWE+ID0ge1xuICBbSyBpbiBrZXlvZiBTXT86IEluZmVyRmllbGQ8U1tLXT47XG59O1xuXG5leHBvcnQgaW50ZXJmYWNlIFBhcnNlUmVzdWx0PFMgZXh0ZW5kcyBTY2hlbWE+IHtcbiAgcmVhZG9ubHkgZmlsdGVyczogSW5mZXJGaWx0ZXJzPFM+O1xuICByZWFkb25seSBkaWFnbm9zdGljczogRGlhZ25vc3RpY1tdO1xufVxuIl19
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@plumile/filter-query",
3
+ "version": "0.1.17",
4
+ "description": "Typed filter query string parser and serializer for Plumile ecosystem",
5
+ "type": "module",
6
+ "main": "lib/esm/index.js",
7
+ "module": "lib/esm/index.js",
8
+ "types": "lib/types/index.d.ts",
9
+ "typings": "lib/types/index.d.ts",
10
+ "exports": "./lib/esm/index.js",
11
+ "sideEffects": false,
12
+ "author": "Olivier Hardy <olivier@plumile.com>",
13
+ "scripts": {
14
+ "build:package": "./tools/build-package.sh",
15
+ "build:package:esm": "tsc --build tsconfig.esm.json",
16
+ "build:package:types": "tsc --build tsconfig.types.json",
17
+ "check:build-package": "tsc --noEmit",
18
+ "clean": "rimraf lib && rimraf *.tsbuildinfo",
19
+ "test:build-package": "./tools/test-build-package.sh"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://gitlab.com/plumile/js"
24
+ },
25
+ "bugs": {
26
+ "url": "https://gitlab.com/plumile/js/-/issues"
27
+ },
28
+ "engines": {
29
+ "node": ">=21.0.0",
30
+ "npm": ">=8.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "rimraf": "6.0.1",
34
+ "typescript": "5.9.2"
35
+ },
36
+ "peerDependencies": {
37
+ "tslib": ">=2.8.1"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "gitHead": "9b42a2afd74c8161c475436751ae90d0240c22a1"
43
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineSchema, numberField, stringField } from '../schema.js';
3
+ import { parse } from '../parse.js';
4
+ import { removeFilter, mergeFilters } from '../mutate.js';
5
+ import { stringify } from '../stringify.js';
6
+
7
+ describe('additional coverage', () => {
8
+ const baseSchema = defineSchema({
9
+ price: numberField(['gt', 'lte', 'between', 'in']),
10
+ title: stringField(['contains', 'eq']),
11
+ });
12
+
13
+ it('removeFilter(operator) prunes field entirely when last operator removed', () => {
14
+ const onlyGt = parse('price.gt=5', baseSchema).filters;
15
+ const removed = removeFilter(onlyGt, 'price', 'gt');
16
+ expect(removed.price).toBeUndefined();
17
+ });
18
+
19
+ it('removeFilter on missing field returns same reference', () => {
20
+ const empty = parse('', baseSchema).filters;
21
+ const result = removeFilter(empty, 'price');
22
+ expect(result).toBe(empty);
23
+ });
24
+
25
+ it('mergeFilters ignores undefined in patch (no change)', () => {
26
+ const a = parse('price.gt=10', baseSchema).filters;
27
+ const patch = { price: undefined } as any; // should be ignored
28
+ const merged = mergeFilters(a, patch);
29
+ expect(merged).toBe(a);
30
+ });
31
+
32
+ it('stringify respects operator order declared in descriptor (custom order)', () => {
33
+ const custom = defineSchema({
34
+ price: numberField(['lte', 'gt', 'in']), // intentionally non-sorted
35
+ });
36
+ const { filters } = parse('price.gt=5&price.lte=10&price.in=1,2', custom);
37
+ const out = stringify(filters, custom);
38
+ // Expect order: lte then gt then in
39
+ expect(out.indexOf('price.lte=10')).toBeLessThan(out.indexOf('price.gt=5'));
40
+ expect(out.indexOf('price.gt=5')).toBeLessThan(out.indexOf('price.in=1,2'));
41
+ });
42
+
43
+ it('parse keeps valid operator despite unknown operator earlier for same field', () => {
44
+ const { filters, diagnostics } = parse(
45
+ 'price.xyz=9&price.gt=3',
46
+ baseSchema,
47
+ );
48
+ expect(filters.price?.gt).toBe(3);
49
+ expect(
50
+ diagnostics.some((d) => {
51
+ return d.kind === 'UnknownOperator' && d.operator === 'xyz';
52
+ }),
53
+ ).toBe(true);
54
+ });
55
+
56
+ it('between with invalid numeric value produces InvalidValue diagnostic', () => {
57
+ const { filters, diagnostics } = parse('price.between=1,abc', baseSchema);
58
+ expect(filters.price?.between).toBeUndefined();
59
+ expect(
60
+ diagnostics.some((d) => {
61
+ return d.kind === 'InvalidValue' && d.operator === 'between';
62
+ }),
63
+ ).toBe(true);
64
+ });
65
+
66
+ it('leading question mark in search string is handled', () => {
67
+ const { filters } = parse('?price.gt=42&title.contains=foo', baseSchema);
68
+ expect(filters.price?.gt).toBe(42);
69
+ expect(filters.title?.contains).toBe('foo');
70
+ });
71
+
72
+ it('ignores segments without equal sign', () => {
73
+ const { filters, diagnostics } = parse(
74
+ 'price.gt=5&bogus&title.eq=abc',
75
+ baseSchema,
76
+ );
77
+ expect(filters.price?.gt).toBe(5);
78
+ expect(filters.title?.eq).toBe('abc');
79
+ // no diagnostics mandated for segments without '='; implementation ignores them silently
80
+ expect(diagnostics.length).toBeGreaterThanOrEqual(0);
81
+ });
82
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineSchema, numberField } from '../schema.js';
3
+ import { parse } from '../parse.js';
4
+
5
+ const schema = defineSchema({ price: numberField(['in']) });
6
+
7
+ describe('list operator edge cases', () => {
8
+ it('empty list value produces no filter entry and an InvalidValue diagnostic', () => {
9
+ const { filters, diagnostics } = parse('price.in=', schema);
10
+ expect(filters.price).toBeUndefined();
11
+ const invalids = diagnostics.filter((d) => {
12
+ return d.kind === 'InvalidValue';
13
+ });
14
+ expect(invalids.length).toBe(1);
15
+ });
16
+
17
+ it('decode error in list value yields DecodeError diagnostic and no filter change', () => {
18
+ const { filters, diagnostics } = parse('price.in=1%ZZ', schema);
19
+ expect(filters).toEqual({});
20
+ expect(
21
+ diagnostics.some((d) => {
22
+ return d.kind === 'DecodeError' && d.operator === 'in';
23
+ }),
24
+ ).toBe(true);
25
+ });
26
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineSchema, numberField, stringField } from '../schema.js';
3
+ import { setFilter, mergeFilters } from '../mutate.js';
4
+ import { parse } from '../parse.js';
5
+
6
+ const schema = defineSchema({
7
+ price: numberField(),
8
+ title: stringField(),
9
+ });
10
+
11
+ describe('mutations', () => {
12
+ it('setFilter adds and removes properly', () => {
13
+ const base = parse('price.gt=10', schema).filters;
14
+ const withBetween = setFilter(base, schema, 'price', 'between', [1, 2]);
15
+ expect(withBetween).not.toBe(base);
16
+ expect(withBetween.price?.between).toEqual([1, 2]);
17
+ const removed = setFilter(
18
+ withBetween,
19
+ schema,
20
+ 'price',
21
+ 'between',
22
+ undefined,
23
+ );
24
+ expect(removed.price?.between).toBeUndefined();
25
+ });
26
+
27
+ it('mergeFilters preserves reference when identical', () => {
28
+ const a = parse('price.gt=10&title.contains=foo', schema).filters;
29
+ const b = parse('price.gt=10', schema).filters;
30
+ const merged = mergeFilters(a, b);
31
+ expect(merged).toBe(a); // no change because b brings nothing new
32
+ });
33
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineSchema, numberField, stringField } from '../schema.js';
3
+ import { parse } from '../parse.js';
4
+ import { stringify } from '../stringify.js';
5
+
6
+ const schema = defineSchema({
7
+ price: numberField(),
8
+ title: stringField(),
9
+ });
10
+
11
+ describe('parse & stringify', () => {
12
+ it('parses scalar and list operators and round-trips', () => {
13
+ const q =
14
+ 'price.gt=10&price.lte=100&price.in=10,20&title.contains=foo%20bar&title.in=a&title.in=b';
15
+ const { filters, diagnostics } = parse(q, schema);
16
+ expect(diagnostics.length).toBe(0);
17
+ expect(filters.price?.gt).toBe(10);
18
+ expect(filters.price?.lte).toBe(100);
19
+ expect(filters.price?.in).toEqual([10, 20]);
20
+ expect(filters.title?.contains).toBe('foo bar');
21
+ expect(filters.title?.in).toEqual(['a', 'b']);
22
+ const out = stringify(filters, schema);
23
+ // canonical order: price first (decl), then title
24
+ expect(out).toContain('price.gt=10');
25
+ expect(out).toContain('price.lte=100');
26
+ expect(out).toContain('price.in=10,20');
27
+ expect(out).toContain('title.contains=foo%20bar');
28
+ expect(out).toContain('title.in=a,b');
29
+ });
30
+
31
+ it('handles between and duplicate between diagnostic', () => {
32
+ const q = 'price.between=1,5&price.between=2,6';
33
+ const { filters, diagnostics } = parse(q, schema);
34
+ expect(filters.price?.between).toEqual([1, 5]);
35
+ expect(
36
+ diagnostics.some((d) => {
37
+ return d.kind === 'DuplicateBetween';
38
+ }),
39
+ ).toBe(true);
40
+ });
41
+
42
+ it('round-trips canonical ordering', () => {
43
+ const q = 'title.ew=bar&price.in=2,1&price.gt=3&title.sw=foo';
44
+ const { filters } = parse(q, schema);
45
+ const out = stringify(filters, schema);
46
+ // price declared before title so should start with price.
47
+ expect(out.startsWith('price.')).toBe(true);
48
+ // parsing the output again should yield equivalent filters
49
+ const again = parse(out, schema).filters;
50
+ expect(again).toEqual(filters);
51
+ });
52
+
53
+ it('produces diagnostics for invalid number and invalid between arity', () => {
54
+ const q = 'price.gt=xx&price.between=1,2,3&title.contains=x%ZZ';
55
+ const { diagnostics } = parse(q, schema);
56
+ expect(
57
+ diagnostics.find((d) => {
58
+ return d.kind === 'InvalidValue';
59
+ }),
60
+ ).toBeTruthy();
61
+ expect(
62
+ diagnostics.find((d) => {
63
+ return d.kind === 'InvalidArity';
64
+ }),
65
+ ).toBeTruthy();
66
+ expect(
67
+ diagnostics.find((d) => {
68
+ return d.kind === 'DecodeError';
69
+ }),
70
+ ).toBeTruthy();
71
+ });
72
+
73
+ it('merges multiple nin occurrences preserving order', () => {
74
+ const q = 'price.nin=1,2&price.nin=3&price.nin=4,5';
75
+ const { filters } = parse(q, schema);
76
+ expect(filters.price?.nin).toEqual([1, 2, 3, 4, 5]);
77
+ const s = stringify(filters, schema);
78
+ expect(s).toContain('price.nin=1,2,3,4,5');
79
+ });
80
+
81
+ it('supports eq / neq operators and overrides last-write', () => {
82
+ const q = 'price.eq=10&price.eq=11&title.eq=foo&title.neq=bar';
83
+ const { filters } = parse(q, schema);
84
+ expect(filters.price?.eq).toBe(11); // last write wins
85
+ expect(filters.title?.eq).toBe('foo');
86
+ expect(filters.title?.neq).toBe('bar');
87
+ const s = stringify(filters, schema);
88
+ expect(s).toContain('price.eq=11');
89
+ expect(s).toMatch(/title\.eq=foo/);
90
+ expect(s).toMatch(/title\.neq=bar/);
91
+ });
92
+
93
+ it('supports sw / ew / contains textual operators', () => {
94
+ const q = 'title.sw=Hello&title.ew=World&title.contains=lo%20Wo';
95
+ const { filters } = parse(q, schema);
96
+ expect(filters.title?.sw).toBe('Hello');
97
+ expect(filters.title?.ew).toBe('World');
98
+ expect(filters.title?.contains).toBe('lo Wo');
99
+ const s = stringify(filters, schema);
100
+ expect(s).toContain('title.sw=Hello');
101
+ expect(s).toContain('title.ew=World');
102
+ expect(s).toContain('title.contains=lo%20Wo');
103
+ });
104
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineSchema, numberField, stringField } from '../schema.js';
3
+ import { parse } from '../parse.js';
4
+ import { setFilter, removeFilter, isEmpty, mergeFilters } from '../mutate.js';
5
+
6
+ const schema = defineSchema({
7
+ price: numberField(['gt', 'lte', 'in']),
8
+ title: stringField(['contains', 'in']),
9
+ });
10
+
11
+ describe('removeFilter & isEmpty', () => {
12
+ it('removeFilter(operator) removes only that operator keeping field if others remain', () => {
13
+ const base = parse('price.gt=10&price.lte=20', schema).filters;
14
+ const removed = removeFilter(base, 'price', 'gt');
15
+ expect(removed.price?.gt).toBeUndefined();
16
+ expect(removed.price?.lte).toBe(20);
17
+ // field object replaced (immutability) but root cloned
18
+ expect(removed).not.toBe(base);
19
+ });
20
+
21
+ it('removeFilter(field) removes entire field object when no operator specified', () => {
22
+ const base = parse('price.gt=10&title.contains=foo', schema).filters;
23
+ const withoutPrice = removeFilter(base, 'price');
24
+ expect(withoutPrice.price).toBeUndefined();
25
+ expect(withoutPrice.title?.contains).toBe('foo');
26
+ });
27
+
28
+ it('setFilter(undefined) removes operator and prunes empty field', () => {
29
+ let parsed = parse('price.gt=10', schema).filters;
30
+ parsed = setFilter(parsed, schema, 'price', 'gt', undefined);
31
+ expect(parsed.price).toBeUndefined();
32
+ expect(isEmpty(parsed)).toBe(true);
33
+ });
34
+
35
+ it('mergeFilters applies changes and preserves identity when patch is noop after removals', () => {
36
+ const a = parse('price.gt=10&price.lte=20', schema).filters;
37
+ const b = removeFilter(a, 'price', 'lte');
38
+ // patching with remaining price field should keep reference
39
+ const merged = mergeFilters(b, { price: b.price } as any);
40
+ expect(merged).toBe(b);
41
+ // adding new info changes reference
42
+ const merged2 = mergeFilters(b, { title: { contains: 'x' } } as any);
43
+ expect(merged2).not.toBe(b);
44
+ expect(merged2.title?.contains).toBe('x');
45
+ });
46
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineSchema, numberField, stringField } from '../schema.js';
3
+ import { parse } from '../parse.js';
4
+
5
+ describe('schema & parse edge cases', () => {
6
+ it('defineSchema returns a frozen object', () => {
7
+ const schema = defineSchema({ price: numberField(['gt']) });
8
+ expect(Object.isFrozen(schema)).toBe(true);
9
+ });
10
+
11
+ it('restricted operator whitelist rejects others (UnknownOperator)', () => {
12
+ const schema = defineSchema({ price: numberField(['gt']) });
13
+ const { filters, diagnostics } = parse('price.gt=5&price.lte=10', schema);
14
+ expect(filters.price?.gt).toBe(5);
15
+ expect(
16
+ diagnostics.some((d) => {
17
+ return d.kind === 'UnknownOperator' && d.operator === 'lte';
18
+ }),
19
+ ).toBe(true);
20
+ });
21
+
22
+ it('invalid key shapes are ignored silently', () => {
23
+ const schema = defineSchema({ price: numberField(['gt']) });
24
+ const { filters, diagnostics } = parse('pricegt=5&price.=6&.gt=7', schema);
25
+ expect(filters).toEqual({});
26
+ expect(diagnostics.length).toBe(0);
27
+ });
28
+
29
+ it('list operator keeps only valid numeric items and reports diagnostics for invalid + empty ones', () => {
30
+ const schema = defineSchema({ price: numberField(['in']) });
31
+ const { filters, diagnostics } = parse('price.in=1,abc,2,', schema);
32
+ expect(filters.price?.in).toEqual([1, 2]);
33
+ expect(
34
+ diagnostics.filter((d) => {
35
+ return d.kind === 'InvalidValue' && d.operator === 'in';
36
+ }).length,
37
+ ).toBeGreaterThanOrEqual(2); // 'abc' and '' both invalid
38
+ });
39
+
40
+ it('empty search yields empty filters & no diagnostics', () => {
41
+ const schema = defineSchema({ title: stringField(['contains']) });
42
+ const { filters, diagnostics } = parse('', schema);
43
+ expect(filters).toEqual({});
44
+ expect(diagnostics).toEqual([]);
45
+ });
46
+ });
@@ -0,0 +1,24 @@
1
+ import { defineSchema, numberField, stringField } from '../schema.js';
2
+ import type { InferFilters } from '../types.js';
3
+
4
+ export const schema = defineSchema({
5
+ price: numberField(['gt', 'between']),
6
+ title: stringField(['contains', 'in']),
7
+ });
8
+
9
+ type F = InferFilters<typeof schema>;
10
+
11
+ // valid usages
12
+ const a: F = { price: { gt: 1 }, title: { contains: 'x' } };
13
+ const b: F = { price: { between: [1, 2] as const } }; // ok
14
+
15
+ const c: F = { price: { in: [1, 2] } };
16
+ // @ts-expect-error wrong tuple arity
17
+ const d: F = { price: { between: [1, 2, 3] } };
18
+ // @ts-expect-error wrong type
19
+ const e: F = { price: { gt: 'nope' } };
20
+ // @ts-expect-error missing array for list operator
21
+ const f: F = { title: { in: 'x' } };
22
+
23
+ // force usage to avoid unused variable errors
24
+ export const _examples = { a, b, c, d, e, f };
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineSchema, numberField, stringField } from '../schema.js';
3
+ import { parse } from '../parse.js';
4
+ import { setFilter, mergeFilters } from '../mutate.js';
5
+
6
+ const schema = defineSchema({
7
+ price: numberField(),
8
+ title: stringField(),
9
+ });
10
+
11
+ describe('reference stability & combined diagnostics', () => {
12
+ it('setFilter preserves reference when setting identical value again', () => {
13
+ const empty = parse('', schema).filters;
14
+ const f1 = setFilter(empty, schema, 'price', 'gt', 10);
15
+ expect(f1).not.toBe(empty);
16
+ const f2 = setFilter(f1, schema, 'price', 'gt', 10);
17
+ expect(f2).toBe(f1); // identical value no change
18
+ const f3 = setFilter(f2, schema, 'price', 'lte', 20);
19
+ expect(f3).not.toBe(f2);
20
+ // mergeFilters with empty patch returns same reference
21
+ const m1 = mergeFilters(f3, {} as any); // patch brings nothing
22
+ expect(m1).toBe(f3);
23
+ // mergeFilters with identical field object also preserves root
24
+ const patchSame = { price: f3.price } as any;
25
+ const m2 = mergeFilters(f3, patchSame);
26
+ expect(m2).toBe(f3);
27
+ });
28
+
29
+ it('reports UnknownField and UnknownOperator in same parse plus other diagnostics', () => {
30
+ const q = 'unknown.gt=5&price.xyz=10&price.gte=abc';
31
+ const { diagnostics } = parse(q, schema);
32
+ // collect kinds
33
+ const kinds = diagnostics.map((d) => {
34
+ return d.kind;
35
+ });
36
+ expect(kinds).toContain('UnknownField');
37
+ expect(kinds).toContain('UnknownOperator');
38
+ expect(kinds).toContain('InvalidValue'); // abc invalid number
39
+ });
40
+ });
package/src/errors.ts ADDED
@@ -0,0 +1,46 @@
1
+ export type Diagnostic =
2
+ | UnknownFieldDiagnostic
3
+ | UnknownOperatorDiagnostic
4
+ | InvalidValueDiagnostic
5
+ | InvalidArityDiagnostic
6
+ | DuplicateBetweenDiagnostic
7
+ | DecodeErrorDiagnostic;
8
+
9
+ export interface BaseDiagnostic {
10
+ readonly kind: string;
11
+ readonly field?: string;
12
+ readonly operator?: string;
13
+ readonly detail?: string;
14
+ }
15
+
16
+ export interface UnknownFieldDiagnostic extends BaseDiagnostic {
17
+ kind: 'UnknownField';
18
+ field: string;
19
+ }
20
+ export interface UnknownOperatorDiagnostic extends BaseDiagnostic {
21
+ kind: 'UnknownOperator';
22
+ field: string;
23
+ operator: string;
24
+ }
25
+ export interface InvalidValueDiagnostic extends BaseDiagnostic {
26
+ kind: 'InvalidValue';
27
+ field: string;
28
+ operator: string;
29
+ detail?: string;
30
+ }
31
+ export interface InvalidArityDiagnostic extends BaseDiagnostic {
32
+ kind: 'InvalidArity';
33
+ field: string;
34
+ operator: string;
35
+ detail: string; // expected arity info
36
+ }
37
+ export interface DuplicateBetweenDiagnostic extends BaseDiagnostic {
38
+ kind: 'DuplicateBetween';
39
+ field: string;
40
+ }
41
+ export interface DecodeErrorDiagnostic extends BaseDiagnostic {
42
+ kind: 'DecodeError';
43
+ field: string;
44
+ operator: string;
45
+ detail: string;
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type * from './types.js';
2
+ export * from './schema.js';
3
+ export type * from './errors.js';
4
+ export * from './parse.js';
5
+ export * from './stringify.js';
6
+ export * from './mutate.js';