@hypequery/protocol 0.24.1 → 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.
@@ -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;AA6ED,gGAAgG;AAChG,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,GAAE,MAAM,GAAG,KAAc,GACxF,eAAe,CAmBjB"}
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
- if (values.some(v => typeof v !== 'string' || !allowed.includes(v))
14
- || new Set(values).size !== values.length)
15
- clientError('HQ_CLIENT_QUERY');
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
- for (const item of filters) {
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, '').operator);
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
- for (const item of having) {
56
- const condition = shape(item, ['measure', 'operator', 'value'], '');
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
- || typeof condition.operator !== 'string'
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
- for (const item of orderBy) {
73
- const order = shape(item, ['field', 'direction'], '');
74
- if (typeof order.field !== 'string' || !orderable.includes(order.field) || ordered.has(order.field)
75
- || (order.direction !== 'asc' && order.direction !== 'desc'))
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
- clientError('HQ_CLIENT_QUERY', error.path);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/protocol",
3
- "version": "0.24.1",
3
+ "version": "0.24.2",
4
4
  "description": "Portable analytics contracts and TypeScript reference implementation for Hypequery",
5
5
  "keywords": [
6
6
  "hypequery",