@plumile/filter-query 0.1.28 → 0.1.30
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/README.md +2 -2
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +1 -1
- package/lib/esm/errors.d.ts.map +1 -1
- package/lib/esm/errors.js +1 -1
- package/lib/esm/mutate.d.ts.map +1 -1
- package/lib/esm/mutate.js +1 -1
- package/lib/esm/parse.d.ts.map +1 -1
- package/lib/esm/parse.js +1 -1
- package/lib/esm/stringify.d.ts.map +1 -1
- package/lib/esm/stringify.js +1 -1
- package/lib/esm/types.d.ts.map +1 -1
- package/lib/esm/types.js +1 -1
- package/lib/mutate.d.ts.map +1 -1
- package/lib/mutate.js +1 -1
- package/lib/parse.d.ts.map +1 -1
- package/lib/parse.js +1 -1
- package/lib/stringify.d.ts.map +1 -1
- package/lib/stringify.js +1 -1
- package/lib/tsconfig.esm.tsbuildinfo +1 -1
- package/lib/types/errors.d.ts.map +1 -1
- package/lib/types/mutate.d.ts.map +1 -1
- package/lib/types/parse.d.ts.map +1 -1
- package/lib/types/stringify.d.ts.map +1 -1
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +1 -1
- package/package.json +2 -2
- package/src/__tests__/mutate.test.ts +61 -9
- package/src/__tests__/parse.test.ts +130 -0
- package/src/__tests__/schema.test.ts +30 -0
- package/src/__tests__/stringify.test.ts +64 -0
- package/src/errors.ts +29 -0
- package/src/mutate.ts +24 -5
- package/src/parse.ts +8 -1
- package/src/stringify.ts +10 -2
- package/src/types.ts +16 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { defineSchema, numberField, stringField } from '../schema.js';
|
|
3
|
+
import { parse } from '../parse.js';
|
|
4
|
+
|
|
5
|
+
const schema = defineSchema({
|
|
6
|
+
title: stringField(),
|
|
7
|
+
price: numberField(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('parse helpers', () => {
|
|
11
|
+
it('parses implicit equality and explicit operators into the same field object', () => {
|
|
12
|
+
const result = parse('?title=foo&title.contains=bar', schema);
|
|
13
|
+
expect(result.filters.title).toEqual({
|
|
14
|
+
eq: 'foo',
|
|
15
|
+
contains: 'bar',
|
|
16
|
+
});
|
|
17
|
+
expect(result.diagnostics).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('handles between operator arity and numeric validation', () => {
|
|
21
|
+
const ok = parse('price.between=1,3', schema);
|
|
22
|
+
expect(ok.filters.price?.between).toEqual([1, 3]);
|
|
23
|
+
expect(ok.diagnostics).toEqual([]);
|
|
24
|
+
|
|
25
|
+
const wrongArity = parse('price.between=1', schema);
|
|
26
|
+
expect(wrongArity.filters.price).toBeUndefined();
|
|
27
|
+
expect(wrongArity.diagnostics).toContainEqual(
|
|
28
|
+
expect.objectContaining({
|
|
29
|
+
kind: 'InvalidArity',
|
|
30
|
+
field: 'price',
|
|
31
|
+
operator: 'between',
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const invalidValue = parse('price.between=1,not-a-number', schema);
|
|
36
|
+
expect(invalidValue.filters.price).toBeUndefined();
|
|
37
|
+
expect(invalidValue.diagnostics).toContainEqual(
|
|
38
|
+
expect.objectContaining({
|
|
39
|
+
kind: 'InvalidValue',
|
|
40
|
+
field: 'price',
|
|
41
|
+
operator: 'between',
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('records duplicate between declarations as diagnostics', () => {
|
|
47
|
+
const result = parse('price.between=1,3&price.between=2,4', schema);
|
|
48
|
+
expect(result.filters.price?.between).toEqual([1, 3]);
|
|
49
|
+
expect(result.diagnostics).toContainEqual(
|
|
50
|
+
expect.objectContaining({
|
|
51
|
+
kind: 'DuplicateBetween',
|
|
52
|
+
field: 'price',
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('aggregates list operators and appends across segments', () => {
|
|
58
|
+
const list = parse('title.in=foo,bar%20baz&title.in=qux', schema);
|
|
59
|
+
expect(list.filters.title?.in).toEqual(['foo', 'bar baz', 'qux']);
|
|
60
|
+
expect(list.diagnostics).toEqual([]);
|
|
61
|
+
|
|
62
|
+
const numericList = parse('price.in=1,NaN,3', schema);
|
|
63
|
+
expect(numericList.filters.price?.in).toEqual([1, 3]);
|
|
64
|
+
expect(numericList.diagnostics).toContainEqual(
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
kind: 'InvalidValue',
|
|
67
|
+
field: 'price',
|
|
68
|
+
operator: 'in',
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('decodes values and reports decode failures', () => {
|
|
74
|
+
const decoded = parse('title.contains=hello%20world', schema);
|
|
75
|
+
expect(decoded.filters.title?.contains).toBe('hello world');
|
|
76
|
+
expect(decoded.diagnostics).toEqual([]);
|
|
77
|
+
|
|
78
|
+
const failure = parse('title.eq=%', schema);
|
|
79
|
+
expect(failure.filters.title).toBeUndefined();
|
|
80
|
+
expect(failure.diagnostics).toContainEqual(
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
kind: 'DecodeError',
|
|
83
|
+
field: 'title',
|
|
84
|
+
operator: 'eq',
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('validates scalar numeric operators and surfaces invalid values', () => {
|
|
90
|
+
const ok = parse('price.eq=42', schema);
|
|
91
|
+
expect(ok.filters.price?.eq).toBe(42);
|
|
92
|
+
|
|
93
|
+
const bad = parse('price.eq=', schema);
|
|
94
|
+
expect(bad.filters.price).toBeUndefined();
|
|
95
|
+
expect(bad.diagnostics).toContainEqual(
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
kind: 'InvalidValue',
|
|
98
|
+
field: 'price',
|
|
99
|
+
operator: 'eq',
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('reports unknown fields and operators', () => {
|
|
105
|
+
const field = parse('unknown.eq=value', schema);
|
|
106
|
+
expect(field.filters.unknown as unknown).toBeUndefined();
|
|
107
|
+
expect(field.diagnostics).toContainEqual(
|
|
108
|
+
expect.objectContaining({
|
|
109
|
+
kind: 'UnknownField',
|
|
110
|
+
field: 'unknown',
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const operator = parse('title.between=foo,bar', schema);
|
|
115
|
+
expect(operator.filters.title).toBeUndefined();
|
|
116
|
+
expect(operator.diagnostics).toContainEqual(
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
kind: 'UnknownOperator',
|
|
119
|
+
field: 'title',
|
|
120
|
+
operator: 'between',
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns empty result for blank input', () => {
|
|
126
|
+
expect(parse('', schema)).toEqual({ filters: {}, diagnostics: [] });
|
|
127
|
+
expect(parse(' ', schema)).toEqual({ filters: {}, diagnostics: [] });
|
|
128
|
+
expect(parse('?', schema)).toEqual({ filters: {}, diagnostics: [] });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { numberField, stringField, defineSchema } from '../schema.js';
|
|
3
|
+
|
|
4
|
+
describe('schema helpers', () => {
|
|
5
|
+
it('numberField parses finite numbers and rejects invalid input', () => {
|
|
6
|
+
const descriptor = numberField();
|
|
7
|
+
expect(descriptor.kind).toBe('number');
|
|
8
|
+
expect(descriptor.operators).toContain('between');
|
|
9
|
+
expect(descriptor.parse('42')).toBe(42);
|
|
10
|
+
expect(descriptor.parse('')).toBeUndefined();
|
|
11
|
+
expect(descriptor.parse('NaN')).toBeUndefined();
|
|
12
|
+
expect(descriptor.serialize(13)).toBe('13');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('stringField returns raw strings and serializes values', () => {
|
|
16
|
+
const descriptor = stringField(['eq', 'contains']);
|
|
17
|
+
expect(descriptor.kind).toBe('string');
|
|
18
|
+
expect(descriptor.operators).toEqual(['eq', 'contains']);
|
|
19
|
+
expect(descriptor.parse('hello')).toBe('hello');
|
|
20
|
+
expect(descriptor.serialize(100)).toBe('100');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('defineSchema freezes the returned schema copy', () => {
|
|
24
|
+
const source = { title: stringField() };
|
|
25
|
+
const schema = defineSchema(source);
|
|
26
|
+
expect(schema).not.toBe(source);
|
|
27
|
+
expect(Object.isFrozen(schema)).toBe(true);
|
|
28
|
+
expect(schema.title).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
import type { InferFilters } from '../types.js';
|
|
6
|
+
|
|
7
|
+
const schema = defineSchema({
|
|
8
|
+
title: stringField(['eq', 'contains', 'in']),
|
|
9
|
+
price: numberField(['eq', 'between', 'in', 'nin']),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('stringify helpers', () => {
|
|
13
|
+
it('omits dot for implicit equality and uses dotted keys for other operators', () => {
|
|
14
|
+
const { filters } = parse('title=hello&title.contains=world', schema);
|
|
15
|
+
const output = stringify(filters, schema);
|
|
16
|
+
const segments = output.split('&');
|
|
17
|
+
expect(segments).toContain('title=hello');
|
|
18
|
+
expect(segments).toContain('title.contains=world');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('serializes operators in schema order including between and list variants', () => {
|
|
22
|
+
const { filters } = parse(
|
|
23
|
+
'title=hello&title.contains=world&price=1&price.between=1,4&price.in=2,3&price.nin=5,6',
|
|
24
|
+
schema,
|
|
25
|
+
);
|
|
26
|
+
const output = stringify(filters, schema);
|
|
27
|
+
expect(output.split('&')).toEqual([
|
|
28
|
+
'title=hello',
|
|
29
|
+
'title.contains=world',
|
|
30
|
+
'price=1',
|
|
31
|
+
'price.between=1,4',
|
|
32
|
+
'price.in=2,3',
|
|
33
|
+
'price.nin=5,6',
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('stringifies non primitive scalars before encoding', () => {
|
|
38
|
+
const customSchema = defineSchema({
|
|
39
|
+
meta: stringField(['eq']),
|
|
40
|
+
});
|
|
41
|
+
type CustomFilters = InferFilters<typeof customSchema>;
|
|
42
|
+
const filters: CustomFilters = {
|
|
43
|
+
meta: {
|
|
44
|
+
eq: { nested: true } as unknown,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const output = stringify(filters, customSchema);
|
|
48
|
+
expect(output).toBe(
|
|
49
|
+
`meta=${encodeURIComponent(JSON.stringify({ nested: true }))}`,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('skips operators with undefined or empty values', () => {
|
|
54
|
+
type Filters = InferFilters<typeof schema>;
|
|
55
|
+
const filters: Filters = {
|
|
56
|
+
title: {
|
|
57
|
+
contains: 'value',
|
|
58
|
+
},
|
|
59
|
+
price: {} as Filters['price'],
|
|
60
|
+
};
|
|
61
|
+
const output = stringify(filters, schema);
|
|
62
|
+
expect(output).toBe('title.contains=value');
|
|
63
|
+
});
|
|
64
|
+
});
|
package/src/errors.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Union of all diagnostic variants that can be produced while parsing a filter query string.
|
|
3
|
+
*/
|
|
1
4
|
export type Diagnostic =
|
|
2
5
|
| UnknownFieldDiagnostic
|
|
3
6
|
| UnknownOperatorDiagnostic
|
|
@@ -6,6 +9,10 @@ export type Diagnostic =
|
|
|
6
9
|
| DuplicateBetweenDiagnostic
|
|
7
10
|
| DecodeErrorDiagnostic;
|
|
8
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Common diagnostic shape.
|
|
14
|
+
* Consumers should refine via the `kind` discriminant before reading additional fields.
|
|
15
|
+
*/
|
|
9
16
|
export interface BaseDiagnostic {
|
|
10
17
|
readonly kind: string;
|
|
11
18
|
readonly field?: string;
|
|
@@ -13,31 +20,53 @@ export interface BaseDiagnostic {
|
|
|
13
20
|
readonly detail?: string;
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Raised when the query references a field that is not present in the schema definition.
|
|
25
|
+
*/
|
|
16
26
|
export interface UnknownFieldDiagnostic extends BaseDiagnostic {
|
|
17
27
|
kind: 'UnknownField';
|
|
18
28
|
field: string;
|
|
19
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Raised when a field is known but an operator that is not whitelisted on the schema
|
|
32
|
+
* is encountered.
|
|
33
|
+
*/
|
|
20
34
|
export interface UnknownOperatorDiagnostic extends BaseDiagnostic {
|
|
21
35
|
kind: 'UnknownOperator';
|
|
22
36
|
field: string;
|
|
23
37
|
operator: string;
|
|
24
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Signals that a value could not be parsed according to the field descriptor
|
|
41
|
+
* (e.g. malformed number).
|
|
42
|
+
*/
|
|
25
43
|
export interface InvalidValueDiagnostic extends BaseDiagnostic {
|
|
26
44
|
kind: 'InvalidValue';
|
|
27
45
|
field: string;
|
|
28
46
|
operator: string;
|
|
29
47
|
detail?: string;
|
|
30
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Emitted when an operator receives an unexpected number of values
|
|
51
|
+
* (e.g. `between` without exactly two items).
|
|
52
|
+
*/
|
|
31
53
|
export interface InvalidArityDiagnostic extends BaseDiagnostic {
|
|
32
54
|
kind: 'InvalidArity';
|
|
33
55
|
field: string;
|
|
34
56
|
operator: string;
|
|
35
57
|
detail: string; // expected arity info
|
|
36
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Indicates that multiple `between` declarations were encountered for the same field;
|
|
61
|
+
* only the first is kept.
|
|
62
|
+
*/
|
|
37
63
|
export interface DuplicateBetweenDiagnostic extends BaseDiagnostic {
|
|
38
64
|
kind: 'DuplicateBetween';
|
|
39
65
|
field: string;
|
|
40
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Raised when a value cannot be decoded from the URL query string (invalid percent-encoding).
|
|
69
|
+
*/
|
|
41
70
|
export interface DecodeErrorDiagnostic extends BaseDiagnostic {
|
|
42
71
|
kind: 'DecodeError';
|
|
43
72
|
field: string;
|
package/src/mutate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { InferFilters, Schema } from './types.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Checks shallow equality between two plain objects (own enumerable string keys only).
|
|
5
5
|
*/
|
|
6
6
|
function shallowEqual(a: unknown, b: unknown): boolean {
|
|
7
7
|
if (a === b) {
|
|
@@ -31,7 +31,14 @@ function shallowEqual(a: unknown, b: unknown): boolean {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Sets or unsets a specific operator value on a field while preserving reference
|
|
35
|
+
* equality when nothing changes.
|
|
36
|
+
*
|
|
37
|
+
* @param filters - Current filters object that will be treated as immutable input.
|
|
38
|
+
* @param _schema - Schema reference (reserved for future validation hooks).
|
|
39
|
+
* @param field - Field key to update.
|
|
40
|
+
* @param operator - Operator name to set or remove.
|
|
41
|
+
* @param value - New operator value; pass `undefined` to remove it.
|
|
35
42
|
*/
|
|
36
43
|
export function setFilter<
|
|
37
44
|
S extends Schema,
|
|
@@ -70,7 +77,11 @@ export function setFilter<
|
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
/**
|
|
73
|
-
*
|
|
80
|
+
* Removes either an entire field or a specific operator from the filters object.
|
|
81
|
+
*
|
|
82
|
+
* @param filters - Current filters object (treated immutably).
|
|
83
|
+
* @param field - Field name to alter.
|
|
84
|
+
* @param operator - Optional operator to remove; omit to drop the whole field.
|
|
74
85
|
*/
|
|
75
86
|
export function removeFilter<S extends Schema, F extends keyof S>(
|
|
76
87
|
filters: Readonly<InferFilters<S>>,
|
|
@@ -97,7 +108,11 @@ export function removeFilter<S extends Schema, F extends keyof S>(
|
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
/**
|
|
100
|
-
*
|
|
111
|
+
* Merges a patch of filters into an existing filters object while preserving reference
|
|
112
|
+
* identity for unchanged fields.
|
|
113
|
+
*
|
|
114
|
+
* @param base - Original filters.
|
|
115
|
+
* @param patch - Filters to overlay.
|
|
101
116
|
*/
|
|
102
117
|
export function mergeFilters<S extends Schema>(
|
|
103
118
|
base: Readonly<InferFilters<S>>,
|
|
@@ -124,7 +139,11 @@ export function mergeFilters<S extends Schema>(
|
|
|
124
139
|
return base as InferFilters<S>;
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Determines if the filters object is empty (no fields present).
|
|
144
|
+
*
|
|
145
|
+
* @param filters - Filters to inspect.
|
|
146
|
+
*/
|
|
128
147
|
export function isEmpty<S extends Schema>(
|
|
129
148
|
filters: Readonly<InferFilters<S>>,
|
|
130
149
|
): boolean {
|
package/src/parse.ts
CHANGED
|
@@ -168,7 +168,14 @@ function handleScalar<S extends Schema>(
|
|
|
168
168
|
scalarTarget[operator] = scalar;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Parses a raw search string (leading `?` optional) according to the provided schema
|
|
173
|
+
* and returns the filters plus diagnostics.
|
|
174
|
+
*
|
|
175
|
+
* @param rawSearch - Query string to parse (with or without the leading question mark).
|
|
176
|
+
* @param schema - Schema describing allowed fields and operators.
|
|
177
|
+
* @returns Parsed filters alongside non-blocking diagnostics.
|
|
178
|
+
*/
|
|
172
179
|
export function parse<S extends Schema>(
|
|
173
180
|
rawSearch: string,
|
|
174
181
|
schema: S,
|
package/src/stringify.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { InferFilters, Schema, Operator } from './types.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Builds the encoded key portion for a field/operator pair (implicit `eq` has no dot suffix).
|
|
5
|
+
*/
|
|
4
6
|
function encodeKey(field: string, op: Operator): string {
|
|
5
7
|
if (op === 'eq') return field; // implicit equality form
|
|
6
8
|
return `${field}.${op}`;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Serializes a filters object into a canonical query string respecting the schema operator ordering.
|
|
13
|
+
*
|
|
14
|
+
* @param filters - Parsed filters to encode.
|
|
15
|
+
* @param schema - Schema used to determine operator ordering and formatting.
|
|
16
|
+
* @returns Query string without leading question mark.
|
|
17
|
+
*/
|
|
10
18
|
export function stringify<S extends Schema>(
|
|
11
19
|
filters: InferFilters<S>,
|
|
12
20
|
schema: S,
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Diagnostic } from './errors.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Operators that take a single scalar value (numbers or strings) in the filter schema.
|
|
5
|
+
*/
|
|
3
6
|
export type ScalarOperator =
|
|
4
7
|
| 'gt'
|
|
5
8
|
| 'gte'
|
|
@@ -10,12 +13,19 @@ export type ScalarOperator =
|
|
|
10
13
|
| 'contains'
|
|
11
14
|
| 'sw'
|
|
12
15
|
| 'ew';
|
|
16
|
+
/** Operator representing a range of two values. */
|
|
13
17
|
export type BetweenOperator = 'between';
|
|
18
|
+
/** Operators whose values are lists. */
|
|
14
19
|
export type ListOperator = 'in' | 'nin';
|
|
20
|
+
/** All supported operator kinds. */
|
|
15
21
|
export type Operator = ScalarOperator | BetweenOperator | ListOperator;
|
|
16
22
|
|
|
23
|
+
/** Primitive field kinds supported by the schema. */
|
|
17
24
|
export type PrimitiveKind = 'number' | 'string';
|
|
18
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Base descriptor shared by number and string fields.
|
|
28
|
+
*/
|
|
19
29
|
export interface FieldDescriptorBase<TKind extends PrimitiveKind> {
|
|
20
30
|
readonly kind: TKind;
|
|
21
31
|
readonly operators: readonly Operator[];
|
|
@@ -23,17 +33,21 @@ export interface FieldDescriptorBase<TKind extends PrimitiveKind> {
|
|
|
23
33
|
readonly serialize?: (value: unknown) => string;
|
|
24
34
|
}
|
|
25
35
|
|
|
36
|
+
/** Descriptor for numeric fields. Provides numeric parsing/serialisation hooks. */
|
|
26
37
|
export type NumberFieldDescriptor = FieldDescriptorBase<'number'> & {
|
|
27
38
|
readonly parse?: (raw: string) => number | undefined;
|
|
28
39
|
readonly serialize?: (value: number) => string;
|
|
29
40
|
};
|
|
41
|
+
/** Descriptor for string fields. Provides string parsing/serialisation hooks. */
|
|
30
42
|
export type StringFieldDescriptor = FieldDescriptorBase<'string'> & {
|
|
31
43
|
readonly parse?: (raw: string) => string | undefined;
|
|
32
44
|
readonly serialize?: (value: string) => string;
|
|
33
45
|
};
|
|
34
46
|
|
|
47
|
+
/** Descriptor accepted by the schema (number or string). */
|
|
35
48
|
export type FieldDescriptor = NumberFieldDescriptor | StringFieldDescriptor;
|
|
36
49
|
|
|
50
|
+
/** Immutable mapping between field keys and their descriptors. */
|
|
37
51
|
export type Schema = Readonly<Record<string, FieldDescriptor>>;
|
|
38
52
|
|
|
39
53
|
// Infer operator mapping for a single field descriptor
|
|
@@ -78,10 +92,12 @@ export type InferField<D extends FieldDescriptor> = Partial<
|
|
|
78
92
|
ScalarMap<D> & BetweenMap<D> & ListMap<D>
|
|
79
93
|
>;
|
|
80
94
|
|
|
95
|
+
/** Filters map inferred from a schema definition. */
|
|
81
96
|
export type InferFilters<S extends Schema> = {
|
|
82
97
|
[K in keyof S]?: InferField<S[K]>;
|
|
83
98
|
};
|
|
84
99
|
|
|
100
|
+
/** Result returned by the `parse` function: parsed filters plus diagnostics. */
|
|
85
101
|
export interface ParseResult<S extends Schema> {
|
|
86
102
|
readonly filters: InferFilters<S>;
|
|
87
103
|
readonly diagnostics: Diagnostic[];
|