@hypequery/protocol 0.24.0 → 0.24.2
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/dist/client-contract/errors.d.ts +4 -2
- package/dist/client-contract/errors.d.ts.map +1 -1
- package/dist/client-contract/errors.js +7 -3
- package/dist/client-contract/primitives.d.ts.map +1 -1
- package/dist/client-contract/primitives.js +8 -4
- package/dist/client-contract/query.d.ts.map +1 -1
- package/dist/client-contract/query.js +54 -41
- package/package.json +1 -1
|
@@ -15,7 +15,9 @@ export type ProtocolClientContractErrorCode = keyof typeof CLIENT_ERROR_MESSAGES
|
|
|
15
15
|
export declare class ProtocolClientContractError extends TypeError {
|
|
16
16
|
readonly code: ProtocolClientContractErrorCode;
|
|
17
17
|
readonly path: string;
|
|
18
|
-
|
|
18
|
+
/** For `HQ_CLIENT_UNKNOWN_FIELD`, the first member at `path` this version doesn't know. */
|
|
19
|
+
readonly field?: string;
|
|
20
|
+
constructor(code: ProtocolClientContractErrorCode, path?: string, field?: string);
|
|
19
21
|
}
|
|
20
|
-
export declare function clientError(code: ProtocolClientContractErrorCode, path?: string): never;
|
|
22
|
+
export declare function clientError(code: ProtocolClientContractErrorCode, path?: string, field?: string): never;
|
|
21
23
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/client-contract/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB;;;;;;;;;;;;EAYhC,CAAC;AACH,MAAM,MAAM,+BAA+B,GAAG,MAAM,OAAO,qBAAqB,CAAC;AACjF,qBAAa,2BAA4B,SAAQ,SAAS;IACxD,QAAQ,CAAC,IAAI,EAAE,+BAA+B,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/client-contract/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB;;;;;;;;;;;;EAYhC,CAAC;AACH,MAAM,MAAM,+BAA+B,GAAG,MAAM,OAAO,qBAAqB,CAAC;AACjF,qBAAa,2BAA4B,SAAQ,SAAS;IACxD,QAAQ,CAAC,IAAI,EAAE,+BAA+B,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;gBACZ,IAAI,EAAE,+BAA+B,EAAE,IAAI,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM;CAO7E;AACD,wBAAgB,WAAW,CAAC,IAAI,EAAE,+BAA+B,EAAE,IAAI,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAEnG"}
|
|
@@ -14,13 +14,17 @@ export const CLIENT_ERROR_MESSAGES = Object.freeze({
|
|
|
14
14
|
export class ProtocolClientContractError extends TypeError {
|
|
15
15
|
code;
|
|
16
16
|
path;
|
|
17
|
-
|
|
17
|
+
/** For `HQ_CLIENT_UNKNOWN_FIELD`, the first member at `path` this version doesn't know. */
|
|
18
|
+
field;
|
|
19
|
+
constructor(code, path = '', field) {
|
|
18
20
|
super(CLIENT_ERROR_MESSAGES[code]);
|
|
19
21
|
this.name = 'ProtocolClientContractError';
|
|
20
22
|
this.code = code;
|
|
21
23
|
this.path = path;
|
|
24
|
+
if (field !== undefined)
|
|
25
|
+
this.field = field;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
|
-
export function clientError(code, path = '') {
|
|
25
|
-
throw new ProtocolClientContractError(code, path);
|
|
28
|
+
export function clientError(code, path = '', field) {
|
|
29
|
+
throw new ProtocolClientContractError(code, path, field);
|
|
26
30
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/client-contract/primitives.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,eAAO,MAAM,kBAAkB,uDAAwD,CAAC;AACxF,eAAO,MAAM,aAAa,wEAAyE,CAAC;AACpG,eAAO,MAAM,gBAAgB,UAAmG,CAAC;AACjI,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAElE;AACD,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAGjE;
|
|
1
|
+
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/client-contract/primitives.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,eAAO,MAAM,kBAAkB,uDAAwD,CAAC;AACxF,eAAO,MAAM,aAAa,wEAAyE,CAAC;AACpG,eAAO,MAAM,gBAAgB,UAAmG,CAAC;AACjI,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAElE;AACD,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAGjE;AAMD,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAKjG;AACD,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,CAKzF;AACD,wBAAgB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CAG/F;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAG1D;AACD,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvF;AACD,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,MAAM,CAO5E;AACD,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAOzF;AACD,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED,6FAA6F;AAC7F,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD;AACD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7D"}
|
|
@@ -12,19 +12,23 @@ export function record(input, path) {
|
|
|
12
12
|
clientError('HQ_CLIENT_SHAPE', path);
|
|
13
13
|
return input;
|
|
14
14
|
}
|
|
15
|
+
/** The error keeps the shared fixtures' parent path and names the first unlisted member as `field`. */
|
|
16
|
+
function rejectUnknownField(value, keys, path) {
|
|
17
|
+
const unknown = Object.keys(value).find(key => !keys.includes(key));
|
|
18
|
+
if (unknown !== undefined)
|
|
19
|
+
clientError('HQ_CLIENT_UNKNOWN_FIELD', path, unknown);
|
|
20
|
+
}
|
|
15
21
|
/** Closed object whose listed members are all optional; at least one is required. */
|
|
16
22
|
export function optionalShape(input, keys, path) {
|
|
17
23
|
const value = record(input, path);
|
|
18
|
-
|
|
19
|
-
clientError('HQ_CLIENT_UNKNOWN_FIELD', path);
|
|
24
|
+
rejectUnknownField(value, keys, path);
|
|
20
25
|
if (!keys.some(key => Object.hasOwn(value, key)))
|
|
21
26
|
clientError('HQ_CLIENT_SHAPE', path);
|
|
22
27
|
return value;
|
|
23
28
|
}
|
|
24
29
|
export function shape(input, keys, path) {
|
|
25
30
|
const value = record(input, path);
|
|
26
|
-
|
|
27
|
-
clientError('HQ_CLIENT_UNKNOWN_FIELD', path);
|
|
31
|
+
rejectUnknownField(value, keys, path);
|
|
28
32
|
if (keys.some(key => !Object.hasOwn(value, key)))
|
|
29
33
|
clientError('HQ_CLIENT_SHAPE', path);
|
|
30
34
|
return value;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/client-contract/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAMzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/client-contract/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAMzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACjD,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAoFD,gGAAgG;AAChG,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,GAAE,MAAM,GAAG,KAAc,GACxF,eAAe,CAwBjB"}
|
|
@@ -3,88 +3,97 @@ import { validateProtocolClientContract } from './validate.js';
|
|
|
3
3
|
import { record, shape, integer, timezone } from './primitives.js';
|
|
4
4
|
import { freezeClientData, snapshotClientData } from './safe-data.js';
|
|
5
5
|
import { isProtocolNullCheckOperator } from '../expressions/null-checks.js';
|
|
6
|
-
function array(input, maximum) {
|
|
6
|
+
function array(input, maximum, path) {
|
|
7
7
|
if (!Array.isArray(input) || input.length > maximum)
|
|
8
|
-
clientError('HQ_CLIENT_QUERY');
|
|
8
|
+
clientError('HQ_CLIENT_QUERY', path);
|
|
9
9
|
return input;
|
|
10
10
|
}
|
|
11
|
-
function strings(input, allowed, maximum) {
|
|
12
|
-
const values = array(input, maximum);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
function strings(input, allowed, maximum, path) {
|
|
12
|
+
const values = array(input, maximum, path);
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
values.forEach((value, index) => {
|
|
15
|
+
if (typeof value !== 'string' || !allowed.includes(value) || seen.has(value))
|
|
16
|
+
clientError('HQ_CLIENT_QUERY', `${path}/${index}`);
|
|
17
|
+
seen.add(value);
|
|
18
|
+
});
|
|
16
19
|
return values;
|
|
17
20
|
}
|
|
18
|
-
function predicateValue(value, type, operator, maximum) {
|
|
21
|
+
function predicateValue(value, type, operator, maximum, path) {
|
|
19
22
|
const values = operator === 'between' || operator === 'in' || operator === 'notIn'
|
|
20
|
-
? array(value, operator === 'between' ? 2 : maximum) : [value];
|
|
23
|
+
? array(value, operator === 'between' ? 2 : maximum, path) : [value];
|
|
21
24
|
if (values.length === 0 || (operator === 'between' && values.length !== 2))
|
|
22
|
-
clientError('HQ_CLIENT_QUERY');
|
|
25
|
+
clientError('HQ_CLIENT_QUERY', path);
|
|
23
26
|
for (const item of values) {
|
|
24
27
|
if (type === 'number' ? typeof item !== 'number' || !Number.isFinite(item)
|
|
25
28
|
: type === 'boolean' ? typeof item !== 'boolean' : typeof item !== 'string')
|
|
26
|
-
clientError('HQ_CLIENT_QUERY');
|
|
29
|
+
clientError('HQ_CLIENT_QUERY', path);
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
function bodyFor(endpoint, source, transport) {
|
|
30
33
|
const keys = ['dimensions', 'filters', 'orderBy', 'limit', 'offset', 'includeMeta',
|
|
31
34
|
...(endpoint.kind === 'dataset' ? ['measures', 'having'] : []),
|
|
32
35
|
...(endpoint.time ? ['by', ...(endpoint.time.timezone ? ['timezone'] : [])] : [])];
|
|
36
|
+
// An unknown member is reported at the root: its name is submitted data.
|
|
33
37
|
if (Object.keys(source).some(key => !keys.includes(key)))
|
|
34
38
|
clientError('HQ_CLIENT_QUERY');
|
|
35
39
|
const caps = endpoint.limits;
|
|
36
|
-
const dimensions = strings(source.dimensions === undefined ? [] : source.dimensions, Object.keys(endpoint.dimensions).filter(key => endpoint.dimensions[key].groupable), caps.maxDimensions);
|
|
40
|
+
const dimensions = strings(source.dimensions === undefined ? [] : source.dimensions, Object.keys(endpoint.dimensions).filter(key => endpoint.dimensions[key].groupable), caps.maxDimensions, '/dimensions');
|
|
37
41
|
const selectedMeasures = endpoint.kind === 'metric' ? Object.keys(endpoint.measures)
|
|
38
|
-
: strings(source.measures === undefined ? endpoint.defaultMeasures : source.measures, Object.keys(endpoint.measures), caps.maxMeasures);
|
|
42
|
+
: strings(source.measures === undefined ? endpoint.defaultMeasures : source.measures, Object.keys(endpoint.measures), caps.maxMeasures, '/measures');
|
|
39
43
|
if (!dimensions.length && !selectedMeasures.length && source.by === undefined)
|
|
40
44
|
clientError('HQ_CLIENT_QUERY');
|
|
41
|
-
const filters = array(source.filters === undefined ? [] : source.filters, caps.maxPredicates);
|
|
42
|
-
|
|
45
|
+
const filters = array(source.filters === undefined ? [] : source.filters, caps.maxPredicates, '/filters');
|
|
46
|
+
filters.forEach((item, index) => {
|
|
47
|
+
const path = `/filters/${index}`;
|
|
43
48
|
// A null check names no value (RFC 0023), so one carrying `value` fails as an unknown member.
|
|
44
|
-
const nullCheck = isProtocolNullCheckOperator(record(item,
|
|
45
|
-
const filter = shape(item, nullCheck ? ['field', 'operator'] : ['field', 'operator', 'value'],
|
|
49
|
+
const nullCheck = isProtocolNullCheckOperator(record(item, path).operator);
|
|
50
|
+
const filter = shape(item, nullCheck ? ['field', 'operator'] : ['field', 'operator', 'value'], path);
|
|
46
51
|
if (typeof filter.field !== 'string' || !Object.hasOwn(endpoint.filters, filter.field))
|
|
47
|
-
clientError('HQ_CLIENT_QUERY');
|
|
52
|
+
clientError('HQ_CLIENT_QUERY', `${path}/field`);
|
|
48
53
|
const definition = endpoint.filters[filter.field];
|
|
49
54
|
if (typeof filter.operator !== 'string' || !definition.operators.includes(filter.operator))
|
|
50
|
-
clientError('HQ_CLIENT_QUERY');
|
|
55
|
+
clientError('HQ_CLIENT_QUERY', `${path}/operator`);
|
|
51
56
|
if (!nullCheck)
|
|
52
|
-
predicateValue(filter.value, definition.type, filter.operator, caps.maxFilterValues);
|
|
53
|
-
}
|
|
54
|
-
const having = array(source.having === undefined ? [] : source.having, caps.maxPredicates - filters.length);
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
+
predicateValue(filter.value, definition.type, filter.operator, caps.maxFilterValues, `${path}/value`);
|
|
58
|
+
});
|
|
59
|
+
const having = array(source.having === undefined ? [] : source.having, caps.maxPredicates - filters.length, '/having');
|
|
60
|
+
having.forEach((item, index) => {
|
|
61
|
+
const path = `/having/${index}`;
|
|
62
|
+
const condition = shape(item, ['measure', 'operator', 'value'], path);
|
|
57
63
|
if (typeof condition.measure !== 'string' || !selectedMeasures.includes(condition.measure)
|
|
58
|
-
|| !endpoint.measures[condition.measure].having
|
|
59
|
-
|
|
64
|
+
|| !endpoint.measures[condition.measure].having)
|
|
65
|
+
clientError('HQ_CLIENT_QUERY', `${path}/measure`);
|
|
66
|
+
if (typeof condition.operator !== 'string'
|
|
60
67
|
|| !['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in', 'notIn'].includes(condition.operator))
|
|
61
|
-
clientError('HQ_CLIENT_QUERY');
|
|
62
|
-
predicateValue(condition.value, 'number', condition.operator, caps.maxFilterValues);
|
|
63
|
-
}
|
|
68
|
+
clientError('HQ_CLIENT_QUERY', `${path}/operator`);
|
|
69
|
+
predicateValue(condition.value, 'number', condition.operator, caps.maxFilterValues, `${path}/value`);
|
|
70
|
+
});
|
|
64
71
|
if (source.by !== undefined && (typeof source.by !== 'string' || !endpoint.time?.grains.includes(source.by)))
|
|
65
|
-
clientError('HQ_CLIENT_QUERY');
|
|
72
|
+
clientError('HQ_CLIENT_QUERY', '/by');
|
|
66
73
|
if (source.timezone !== undefined)
|
|
67
74
|
timezone(source.timezone, '/timezone');
|
|
68
|
-
const orderBy = array(source.orderBy === undefined ? [] : source.orderBy, caps.maxOrderBy);
|
|
75
|
+
const orderBy = array(source.orderBy === undefined ? [] : source.orderBy, caps.maxOrderBy, '/orderBy');
|
|
69
76
|
const orderable = [...dimensions.filter(key => endpoint.dimensions[key].orderable),
|
|
70
77
|
...selectedMeasures, ...(source.by === undefined ? [] : ['period'])];
|
|
71
78
|
const ordered = new Set();
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
clientError('HQ_CLIENT_QUERY');
|
|
79
|
+
orderBy.forEach((item, index) => {
|
|
80
|
+
const path = `/orderBy/${index}`;
|
|
81
|
+
const order = shape(item, ['field', 'direction'], path);
|
|
82
|
+
if (typeof order.field !== 'string' || !orderable.includes(order.field) || ordered.has(order.field))
|
|
83
|
+
clientError('HQ_CLIENT_QUERY', `${path}/field`);
|
|
84
|
+
if (order.direction !== 'asc' && order.direction !== 'desc')
|
|
85
|
+
clientError('HQ_CLIENT_QUERY', `${path}/direction`);
|
|
77
86
|
ordered.add(order.field);
|
|
78
|
-
}
|
|
87
|
+
});
|
|
79
88
|
// HTTP clamps a larger limit; an agent is refused one, so it never reads a
|
|
80
89
|
// silently shortened page as the whole answer.
|
|
81
90
|
const effectiveLimit = Math.min(source.limit === undefined ? caps.defaultLimit
|
|
82
91
|
: integer(source.limit, 1, transport === 'mcp' ? caps.maxLimit : Number.MAX_SAFE_INTEGER, '/limit'), caps.maxLimit);
|
|
83
92
|
const offset = source.offset === undefined ? 0 : integer(source.offset, 0, caps.maxOffset, '/offset');
|
|
84
93
|
if (source.includeMeta !== undefined && typeof source.includeMeta !== 'boolean')
|
|
85
|
-
clientError('HQ_CLIENT_QUERY');
|
|
94
|
+
clientError('HQ_CLIENT_QUERY', '/includeMeta');
|
|
86
95
|
if (source.includeMeta === true && !endpoint.metadata)
|
|
87
|
-
clientError('HQ_CLIENT_QUERY');
|
|
96
|
+
clientError('HQ_CLIENT_QUERY', '/includeMeta');
|
|
88
97
|
const body = { dimensions, filters, orderBy, limit: effectiveLimit, offset,
|
|
89
98
|
includeMeta: source.includeMeta ?? false,
|
|
90
99
|
...(endpoint.kind === 'dataset' ? { measures: selectedMeasures, having } : {}),
|
|
@@ -118,8 +127,12 @@ export function validateProtocolClientQuery(contract, endpointId, input, transpo
|
|
|
118
127
|
return bodyFor(endpoint, source, transport);
|
|
119
128
|
}
|
|
120
129
|
catch (error) {
|
|
121
|
-
if (error instanceof ProtocolClientContractError)
|
|
122
|
-
|
|
130
|
+
if (error instanceof ProtocolClientContractError) {
|
|
131
|
+
// An agent names the time grain `grain`; point at the member it sent.
|
|
132
|
+
const path = transport === 'mcp' && (error.path === '/by' || error.path.startsWith('/by/'))
|
|
133
|
+
? `/grain${error.path.slice(3)}` : error.path;
|
|
134
|
+
clientError('HQ_CLIENT_QUERY', path);
|
|
135
|
+
}
|
|
123
136
|
throw error;
|
|
124
137
|
}
|
|
125
138
|
}
|