@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.
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/lib/errors.d.ts +39 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +2 -0
- package/lib/esm/errors.d.ts +39 -0
- package/lib/esm/errors.d.ts.map +1 -0
- package/lib/esm/errors.js +2 -0
- package/lib/esm/index.d.ts +7 -0
- package/lib/esm/index.d.ts.map +1 -0
- package/lib/esm/index.js +5 -0
- package/lib/esm/mutate.d.ts +6 -0
- package/lib/esm/mutate.d.ts.map +1 -0
- package/lib/esm/mutate.js +88 -0
- package/lib/esm/parse.d.ts +3 -0
- package/lib/esm/parse.d.ts.map +1 -0
- package/lib/esm/parse.js +164 -0
- package/lib/esm/schema.d.ts +5 -0
- package/lib/esm/schema.d.ts.map +1 -0
- package/lib/esm/schema.js +55 -0
- package/lib/esm/stringify.d.ts +3 -0
- package/lib/esm/stringify.d.ts.map +1 -0
- package/lib/esm/stringify.js +50 -0
- package/lib/esm/types.d.ts +43 -0
- package/lib/esm/types.d.ts.map +1 -0
- package/lib/esm/types.js +2 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +5 -0
- package/lib/mutate.d.ts +6 -0
- package/lib/mutate.d.ts.map +1 -0
- package/lib/mutate.js +88 -0
- package/lib/parse.d.ts +3 -0
- package/lib/parse.d.ts.map +1 -0
- package/lib/parse.js +164 -0
- package/lib/schema.d.ts +5 -0
- package/lib/schema.d.ts.map +1 -0
- package/lib/schema.js +55 -0
- package/lib/stringify.d.ts +3 -0
- package/lib/stringify.d.ts.map +1 -0
- package/lib/stringify.js +50 -0
- package/lib/tsconfig.esm.tsbuildinfo +1 -0
- package/lib/types/errors.d.ts +39 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/index.d.ts +7 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/mutate.d.ts +6 -0
- package/lib/types/mutate.d.ts.map +1 -0
- package/lib/types/parse.d.ts +3 -0
- package/lib/types/parse.d.ts.map +1 -0
- package/lib/types/schema.d.ts +5 -0
- package/lib/types/schema.d.ts.map +1 -0
- package/lib/types/stringify.d.ts +3 -0
- package/lib/types/stringify.d.ts.map +1 -0
- package/lib/types/types.d.ts +43 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types.d.ts +43 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/package.json +43 -0
- package/src/__tests__/additional-coverage.test.ts +82 -0
- package/src/__tests__/list-edge.test.ts +26 -0
- package/src/__tests__/mutate.test.ts +33 -0
- package/src/__tests__/parse-stringify.test.ts +104 -0
- package/src/__tests__/remove-filter.test.ts +46 -0
- package/src/__tests__/schema-edge.test.ts +46 -0
- package/src/__tests__/schema-infer.test-d.ts +24 -0
- package/src/__tests__/stability-and-diagnostics.test.ts +40 -0
- package/src/errors.ts +46 -0
- package/src/index.ts +6 -0
- package/src/mutate.ts +132 -0
- package/src/parse.ts +221 -0
- package/src/schema.ts +81 -0
- package/src/stringify.ts +60 -0
- package/src/types.ts +88 -0
- package/tools/build-package.sh +5 -0
- package/tools/test-build-package.sh +4 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.esm.json +7 -0
- package/tsconfig.json +8 -0
- package/tsconfig.types.json +9 -0
- 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":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,MAAM,cAAc,GACtB,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,UAAU,GACV,IAAI,GACJ,IAAI,CAAC;AACT,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AACxC,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,eAAe,GAAG,YAAY,CAAC;AAEvE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEhD,MAAM,WAAW,mBAAmB,CAAC,KAAK,SAAS,aAAa;IAC9D,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CACjD;AAED,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,QAAQ,CAAC,GAAG;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAChD,CAAC;AACF,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,QAAQ,CAAC,GAAG;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAE5E,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;AAG/D,KAAK,YAAY,CAAC,CAAC,SAAS,eAAe,IAAI,OAAO,CACpD,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,EACtB,cAAc,CACf,CAAC;AACF,KAAK,SAAS,CAAC,CAAC,SAAS,eAAe,IACtC,CAAC,SAAS,mBAAmB,CAAC,QAAQ,CAAC,GACnC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,GAC3C,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAElD,KAAK,UAAU,CAAC,CAAC,SAAS,eAAe,IACvC,SAAS,SAAS,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,GACpC;IACE,OAAO,CAAC,EAAE,CAAC,SAAS,mBAAmB,CAAC,QAAQ,CAAC,GAC7C,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GACzB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B,GACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAE3B,KAAK,OAAO,CAAC,CAAC,SAAS,eAAe,IAAI,OAAO,CAC/C,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,EACtB,YAAY,CACb,CAAC;AACF,KAAK,OAAO,CAAC,CAAC,SAAS,eAAe,IAAI,CAAC,IAAI,SAAS,OAAO,CAAC,CAAC,CAAC,GAC9D;IACE,EAAE,CAAC,EAAE,CAAC,SAAS,mBAAmB,CAAC,QAAQ,CAAC,GACxC,SAAS,MAAM,EAAE,GACjB,SAAS,MAAM,EAAE,CAAC;CACvB,GACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,GACvB,CAAC,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,GACrB;IACE,GAAG,CAAC,EAAE,CAAC,SAAS,mBAAmB,CAAC,QAAQ,CAAC,GACzC,SAAS,MAAM,EAAE,GACjB,SAAS,MAAM,EAAE,CAAC;CACvB,GACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAE5B,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,eAAe,IAAI,OAAO,CACzD,SAAS,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAC1C,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,IAAI;KAC1C,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM;IAC3C,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC;CACpC"}
|
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":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,MAAM,cAAc,GACtB,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,UAAU,GACV,IAAI,GACJ,IAAI,CAAC;AACT,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC;AACxC,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AACxC,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,eAAe,GAAG,YAAY,CAAC;AAEvE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEhD,MAAM,WAAW,mBAAmB,CAAC,KAAK,SAAS,aAAa;IAC9D,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CACjD;AAED,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,QAAQ,CAAC,GAAG;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAChD,CAAC;AACF,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,QAAQ,CAAC,GAAG;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAE5E,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;AAG/D,KAAK,YAAY,CAAC,CAAC,SAAS,eAAe,IAAI,OAAO,CACpD,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,EACtB,cAAc,CACf,CAAC;AACF,KAAK,SAAS,CAAC,CAAC,SAAS,eAAe,IACtC,CAAC,SAAS,mBAAmB,CAAC,QAAQ,CAAC,GACnC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,GAC3C,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAElD,KAAK,UAAU,CAAC,CAAC,SAAS,eAAe,IACvC,SAAS,SAAS,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,GACpC;IACE,OAAO,CAAC,EAAE,CAAC,SAAS,mBAAmB,CAAC,QAAQ,CAAC,GAC7C,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GACzB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B,GACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAE3B,KAAK,OAAO,CAAC,CAAC,SAAS,eAAe,IAAI,OAAO,CAC/C,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,EACtB,YAAY,CACb,CAAC;AACF,KAAK,OAAO,CAAC,CAAC,SAAS,eAAe,IAAI,CAAC,IAAI,SAAS,OAAO,CAAC,CAAC,CAAC,GAC9D;IACE,EAAE,CAAC,EAAE,CAAC,SAAS,mBAAmB,CAAC,QAAQ,CAAC,GACxC,SAAS,MAAM,EAAE,GACjB,SAAS,MAAM,EAAE,CAAC;CACvB,GACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,GACvB,CAAC,KAAK,SAAS,OAAO,CAAC,CAAC,CAAC,GACrB;IACE,GAAG,CAAC,EAAE,CAAC,SAAS,mBAAmB,CAAC,QAAQ,CAAC,GACzC,SAAS,MAAM,EAAE,GACjB,SAAS,MAAM,EAAE,CAAC;CACvB,GACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAE5B,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,eAAe,IAAI,OAAO,CACzD,SAAS,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAC1C,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,IAAI;KAC1C,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,MAAM;IAC3C,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,UAAU,EAAE,CAAC;CACpC"}
|
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
|
+
}
|