@hypequery/protocol 0.1.0 → 0.2.1

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 (101) hide show
  1. package/README.md +50 -9
  2. package/dist/deployments/errors.d.ts +8 -0
  3. package/dist/deployments/errors.d.ts.map +1 -0
  4. package/dist/deployments/errors.js +13 -0
  5. package/dist/deployments/index.d.ts +5 -0
  6. package/dist/deployments/index.d.ts.map +1 -0
  7. package/dist/deployments/index.js +3 -0
  8. package/dist/deployments/limits.d.ts +4 -0
  9. package/dist/deployments/limits.d.ts.map +1 -0
  10. package/dist/deployments/limits.js +23 -0
  11. package/dist/deployments/types.d.ts +148 -0
  12. package/dist/deployments/types.d.ts.map +1 -0
  13. package/dist/deployments/types.js +1 -0
  14. package/dist/deployments/validate.d.ts +4 -0
  15. package/dist/deployments/validate.d.ts.map +1 -0
  16. package/dist/deployments/validate.js +537 -0
  17. package/dist/expressions/errors.d.ts +8 -0
  18. package/dist/expressions/errors.d.ts.map +1 -0
  19. package/dist/expressions/errors.js +13 -0
  20. package/dist/expressions/index.d.ts +5 -0
  21. package/dist/expressions/index.d.ts.map +1 -0
  22. package/dist/expressions/index.js +3 -0
  23. package/dist/expressions/limits.d.ts +4 -0
  24. package/dist/expressions/limits.d.ts.map +1 -0
  25. package/dist/expressions/limits.js +17 -0
  26. package/dist/expressions/types.d.ts +77 -0
  27. package/dist/expressions/types.d.ts.map +1 -0
  28. package/dist/expressions/types.js +1 -0
  29. package/dist/expressions/validate.d.ts +4 -0
  30. package/dist/expressions/validate.d.ts.map +1 -0
  31. package/dist/expressions/validate.js +338 -0
  32. package/dist/identifiers/identifiers.d.ts +14 -0
  33. package/dist/identifiers/identifiers.d.ts.map +1 -0
  34. package/dist/identifiers/identifiers.js +87 -0
  35. package/dist/identifiers/index.d.ts +4 -0
  36. package/dist/identifiers/index.d.ts.map +1 -0
  37. package/dist/identifiers/index.js +2 -0
  38. package/dist/identifiers/types.d.ts +16 -0
  39. package/dist/identifiers/types.d.ts.map +1 -0
  40. package/dist/identifiers/types.js +5 -0
  41. package/dist/index.d.ts +12 -8
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +6 -1
  44. package/dist/query-implementations/errors.d.ts +8 -0
  45. package/dist/query-implementations/errors.d.ts.map +1 -0
  46. package/dist/query-implementations/errors.js +13 -0
  47. package/dist/query-implementations/index.d.ts +5 -0
  48. package/dist/query-implementations/index.d.ts.map +1 -0
  49. package/dist/query-implementations/index.js +3 -0
  50. package/dist/query-implementations/limits.d.ts +4 -0
  51. package/dist/query-implementations/limits.d.ts.map +1 -0
  52. package/dist/query-implementations/limits.js +22 -0
  53. package/dist/query-implementations/types.d.ts +57 -0
  54. package/dist/query-implementations/types.d.ts.map +1 -0
  55. package/dist/query-implementations/types.js +1 -0
  56. package/dist/query-implementations/validate.d.ts +4 -0
  57. package/dist/query-implementations/validate.d.ts.map +1 -0
  58. package/dist/query-implementations/validate.js +248 -0
  59. package/dist/schemas/errors.d.ts +8 -0
  60. package/dist/schemas/errors.d.ts.map +1 -0
  61. package/dist/schemas/errors.js +13 -0
  62. package/dist/schemas/index.d.ts +5 -0
  63. package/dist/schemas/index.d.ts.map +1 -0
  64. package/dist/schemas/index.js +3 -0
  65. package/dist/schemas/limits.d.ts +4 -0
  66. package/dist/schemas/limits.d.ts.map +1 -0
  67. package/dist/schemas/limits.js +20 -0
  68. package/dist/schemas/types.d.ts +60 -0
  69. package/dist/schemas/types.d.ts.map +1 -0
  70. package/dist/schemas/types.js +1 -0
  71. package/dist/schemas/validate.d.ts +3 -0
  72. package/dist/schemas/validate.d.ts.map +1 -0
  73. package/dist/schemas/validate.js +391 -0
  74. package/dist/values/codec.d.ts +13 -0
  75. package/dist/values/codec.d.ts.map +1 -0
  76. package/dist/values/codec.js +39 -0
  77. package/dist/values/errors.d.ts +8 -0
  78. package/dist/values/errors.d.ts.map +1 -0
  79. package/dist/values/errors.js +13 -0
  80. package/dist/values/index.d.ts +7 -0
  81. package/dist/values/index.d.ts.map +1 -0
  82. package/dist/values/index.js +4 -0
  83. package/dist/values/jcs.d.ts +2 -0
  84. package/dist/values/jcs.d.ts.map +1 -0
  85. package/dist/values/jcs.js +23 -0
  86. package/dist/values/limits.d.ts +4 -0
  87. package/dist/values/limits.d.ts.map +1 -0
  88. package/dist/values/limits.js +26 -0
  89. package/dist/values/parser.d.ts +3 -0
  90. package/dist/values/parser.d.ts.map +1 -0
  91. package/dist/values/parser.js +254 -0
  92. package/dist/values/snapshot.d.ts +3 -0
  93. package/dist/values/snapshot.d.ts.map +1 -0
  94. package/dist/values/snapshot.js +96 -0
  95. package/dist/values/types.d.ts +78 -0
  96. package/dist/values/types.d.ts.map +1 -0
  97. package/dist/values/types.js +1 -0
  98. package/dist/values/validate.d.ts +7 -0
  99. package/dist/values/validate.d.ts.map +1 -0
  100. package/dist/values/validate.js +332 -0
  101. package/package.json +4 -1
@@ -0,0 +1,537 @@
1
+ import { ProtocolExpressionError, validateProtocolExpression } from '../expressions/index.js';
2
+ import { ProtocolIdentifierError, parseProtocolIdentifier, parseProtocolQualifiedIdentifier, } from '../identifiers/index.js';
3
+ import { ProtocolQueryImplementationError, validateProtocolQueryImplementation, validateProtocolSqlExpression, } from '../query-implementations/index.js';
4
+ import { ProtocolSchemaError, validateProtocolSchema, } from '../schemas/index.js';
5
+ import { deploymentError } from './errors.js';
6
+ import { resolveDeploymentLimits } from './limits.js';
7
+ const textEncoder = new TextEncoder();
8
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/;
9
+ const AGGREGATIONS = new Set([
10
+ 'sum', 'count', 'countDistinct', 'avg', 'min', 'max',
11
+ 'argMax', 'argMin', 'percentile', 'stddev', 'variance',
12
+ ]);
13
+ const OPERATORS = new Set([
14
+ 'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'between', 'like',
15
+ ]);
16
+ const GRAINS = new Set(['day', 'week', 'month', 'quarter', 'year']);
17
+ function requireRecord(input, path) {
18
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
19
+ deploymentError('HQ_DEPLOYMENT_TYPE', path);
20
+ }
21
+ const prototype = Object.getPrototypeOf(input);
22
+ if (prototype !== Object.prototype && prototype !== null) {
23
+ deploymentError('HQ_DEPLOYMENT_UNSAFE_OBJECT', path);
24
+ }
25
+ if (Object.getOwnPropertySymbols(input).length > 0) {
26
+ deploymentError('HQ_DEPLOYMENT_UNSAFE_OBJECT', path);
27
+ }
28
+ for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(input))) {
29
+ if (!descriptor.enumerable || !('value' in descriptor)) {
30
+ deploymentError('HQ_DEPLOYMENT_UNSAFE_OBJECT', path);
31
+ }
32
+ }
33
+ return input;
34
+ }
35
+ function requireArray(input, path, maxItems) {
36
+ if (!Array.isArray(input))
37
+ deploymentError('HQ_DEPLOYMENT_TYPE', path);
38
+ if (Object.getPrototypeOf(input) !== Array.prototype || Object.getOwnPropertySymbols(input).length > 0) {
39
+ deploymentError('HQ_DEPLOYMENT_UNSAFE_OBJECT', path);
40
+ }
41
+ if (input.length > maxItems)
42
+ deploymentError('HQ_DEPLOYMENT_TOO_MANY_ITEMS', path);
43
+ const descriptors = Object.getOwnPropertyDescriptors(input);
44
+ for (let index = 0; index < input.length; index += 1) {
45
+ const descriptor = descriptors[String(index)];
46
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
47
+ deploymentError('HQ_DEPLOYMENT_UNSAFE_OBJECT', `${path}[${index}]`);
48
+ }
49
+ }
50
+ if (Object.keys(input).length !== input.length)
51
+ deploymentError('HQ_DEPLOYMENT_UNSAFE_OBJECT', path);
52
+ return input;
53
+ }
54
+ function exactFields(value, required, optional, path) {
55
+ const allowed = new Set([...required, ...optional]);
56
+ for (const key of Object.keys(value)) {
57
+ if (!allowed.has(key))
58
+ deploymentError('HQ_DEPLOYMENT_UNKNOWN_FIELD', `${path}.${key}`);
59
+ }
60
+ for (const key of required) {
61
+ if (!Object.hasOwn(value, key))
62
+ deploymentError('HQ_DEPLOYMENT_TYPE', `${path}.${key}`);
63
+ }
64
+ }
65
+ function freezeRecord(entries) {
66
+ return Object.freeze(Object.assign(Object.create(null), entries));
67
+ }
68
+ function identifier(value, path, qualified = false) {
69
+ try {
70
+ return qualified ? parseProtocolQualifiedIdentifier(value) : parseProtocolIdentifier(value);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof ProtocolIdentifierError) {
74
+ deploymentError('HQ_DEPLOYMENT_INVALID_IDENTIFIER', path);
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+ function boundedText(value, path, maxBytes) {
80
+ if (typeof value !== 'string')
81
+ deploymentError('HQ_DEPLOYMENT_TYPE', path);
82
+ if (value.trim().length === 0)
83
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', path);
84
+ for (const character of value) {
85
+ const code = character.codePointAt(0);
86
+ if (code <= 0x1f || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
87
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', path);
88
+ }
89
+ }
90
+ if (value.length > maxBytes || textEncoder.encode(value).byteLength > maxBytes) {
91
+ deploymentError('HQ_DEPLOYMENT_TOO_LARGE', path);
92
+ }
93
+ return value;
94
+ }
95
+ function optionalText(value, key, result, path, limits) {
96
+ if (value !== undefined)
97
+ result[key] = boundedText(value, `${path}.${key}`, limits.maxTextBytes);
98
+ }
99
+ function positiveInteger(value, path) {
100
+ if (!Number.isSafeInteger(value) || value < 1) {
101
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', path);
102
+ }
103
+ return value;
104
+ }
105
+ function uniqueStrings(input, path, maxItems, parse) {
106
+ const result = requireArray(input, path, maxItems).map((value, index) => parse(value, `${path}[${index}]`));
107
+ if (new Set(result).size !== result.length)
108
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', path);
109
+ return Object.freeze(result);
110
+ }
111
+ function validateAccess(input, path, limits) {
112
+ const value = requireRecord(input, path);
113
+ if (value.kind === 'public') {
114
+ exactFields(value, ['kind'], [], path);
115
+ return freezeRecord({ kind: 'public' });
116
+ }
117
+ if (value.kind === 'authenticated') {
118
+ exactFields(value, ['kind', 'roles', 'scopes'], [], path);
119
+ const parseClaim = (claim, claimPath) => boundedText(claim, claimPath, limits.maxTextBytes);
120
+ return freezeRecord({
121
+ kind: 'authenticated',
122
+ roles: uniqueStrings(value.roles, `${path}.roles`, limits.maxDatasetItems, parseClaim),
123
+ scopes: uniqueStrings(value.scopes, `${path}.scopes`, limits.maxDatasetItems, parseClaim),
124
+ });
125
+ }
126
+ if (typeof value.kind !== 'string')
127
+ deploymentError('HQ_DEPLOYMENT_TYPE', `${path}.kind`);
128
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.kind`);
129
+ }
130
+ function validateEndpointTenant(input, path, limits) {
131
+ const value = requireRecord(input, path);
132
+ if (value.kind === 'not-required') {
133
+ exactFields(value, ['kind'], [], path);
134
+ return freezeRecord({ kind: 'not-required' });
135
+ }
136
+ if (value.kind === 'required' || value.kind === 'optional') {
137
+ exactFields(value, ['kind', 'mode'], ['column'], path);
138
+ if (value.mode !== 'auto-inject' && value.mode !== 'manual') {
139
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.mode`);
140
+ }
141
+ const result = { kind: value.kind, mode: value.mode };
142
+ if (value.column !== undefined) {
143
+ result.column = boundedText(value.column, `${path}.column`, limits.maxSourceBytes);
144
+ }
145
+ if (value.mode === 'auto-inject' && value.column === undefined) {
146
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.column`);
147
+ }
148
+ return freezeRecord(result);
149
+ }
150
+ if (typeof value.kind !== 'string')
151
+ deploymentError('HQ_DEPLOYMENT_TYPE', `${path}.kind`);
152
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.kind`);
153
+ }
154
+ function validateEndpoint(input, path, limits, transport = false) {
155
+ const value = requireRecord(input, path);
156
+ exactFields(value, transport ? ['access', 'tenant', 'method', 'path'] : ['access', 'tenant'], transport ? ['cacheTtlMs', 'maxLimit'] : ['cacheTtlMs', 'maxLimit', 'path'], path);
157
+ const result = {
158
+ access: validateAccess(value.access, `${path}.access`, limits),
159
+ tenant: validateEndpointTenant(value.tenant, `${path}.tenant`, limits),
160
+ };
161
+ if (value.cacheTtlMs !== undefined)
162
+ result.cacheTtlMs = positiveInteger(value.cacheTtlMs, `${path}.cacheTtlMs`);
163
+ if (value.maxLimit !== undefined)
164
+ result.maxLimit = positiveInteger(value.maxLimit, `${path}.maxLimit`);
165
+ if (!transport && value.path !== undefined) {
166
+ const endpointPath = boundedText(value.path, `${path}.path`, limits.maxPathBytes);
167
+ if (!endpointPath.startsWith('/'))
168
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.path`);
169
+ result.path = endpointPath;
170
+ }
171
+ if (transport) {
172
+ if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].includes(value.method)) {
173
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.method`);
174
+ }
175
+ const endpointPath = boundedText(value.path, `${path}.path`, limits.maxPathBytes);
176
+ if (!endpointPath.startsWith('/'))
177
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.path`);
178
+ result.method = value.method;
179
+ result.path = endpointPath;
180
+ }
181
+ return freezeRecord(result);
182
+ }
183
+ function validateTenant(input, path, limits) {
184
+ const value = requireRecord(input, path);
185
+ if (value.kind === 'not-required') {
186
+ exactFields(value, ['kind'], [], path);
187
+ return freezeRecord({ kind: 'not-required' });
188
+ }
189
+ if (value.kind === 'required') {
190
+ exactFields(value, ['kind', 'field'], [], path);
191
+ return freezeRecord({
192
+ kind: 'required',
193
+ field: boundedText(value.field, `${path}.field`, limits.maxSourceBytes),
194
+ });
195
+ }
196
+ if (typeof value.kind !== 'string')
197
+ deploymentError('HQ_DEPLOYMENT_TYPE', `${path}.kind`);
198
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.kind`);
199
+ }
200
+ function nested(action, path) {
201
+ try {
202
+ return action();
203
+ }
204
+ catch (error) {
205
+ if (error instanceof ProtocolExpressionError
206
+ || error instanceof ProtocolQueryImplementationError
207
+ || error instanceof ProtocolSchemaError) {
208
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', path);
209
+ }
210
+ throw error;
211
+ }
212
+ }
213
+ function validateFieldSource(input, path, limits) {
214
+ const value = requireRecord(input, path);
215
+ if (value.kind === 'column') {
216
+ exactFields(value, ['kind', 'column'], [], path);
217
+ return freezeRecord({
218
+ kind: 'column',
219
+ column: boundedText(value.column, `${path}.column`, limits.maxSourceBytes),
220
+ });
221
+ }
222
+ return nested(() => validateProtocolSqlExpression(value), path);
223
+ }
224
+ function validateDimension(input, path, limits) {
225
+ const value = requireRecord(input, path);
226
+ exactFields(value, ['name', 'type', 'source', 'filterable', 'groupable'], ['label', 'description'], path);
227
+ if (!['string', 'number', 'boolean', 'timestamp'].includes(value.type)) {
228
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.type`);
229
+ }
230
+ if (typeof value.filterable !== 'boolean')
231
+ deploymentError('HQ_DEPLOYMENT_TYPE', `${path}.filterable`);
232
+ if (typeof value.groupable !== 'boolean')
233
+ deploymentError('HQ_DEPLOYMENT_TYPE', `${path}.groupable`);
234
+ const result = {
235
+ name: identifier(value.name, `${path}.name`),
236
+ type: value.type,
237
+ source: validateFieldSource(value.source, `${path}.source`, limits),
238
+ filterable: value.filterable,
239
+ groupable: value.groupable,
240
+ };
241
+ optionalText(value.label, 'label', result, path, limits);
242
+ optionalText(value.description, 'description', result, path, limits);
243
+ return freezeRecord(result);
244
+ }
245
+ function validateMeasure(input, path, limits) {
246
+ const value = requireRecord(input, path);
247
+ exactFields(value, ['name', 'aggregation', 'field', 'filters'], ['argField', 'level', 'sql', 'label', 'description'], path);
248
+ if (typeof value.aggregation !== 'string' || !AGGREGATIONS.has(value.aggregation)) {
249
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.aggregation`);
250
+ }
251
+ const needsArg = value.aggregation === 'argMax' || value.aggregation === 'argMin';
252
+ if (needsArg !== (value.argField !== undefined)) {
253
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.argField`);
254
+ }
255
+ if (value.aggregation === 'percentile') {
256
+ if (typeof value.level !== 'number' || !Number.isFinite(value.level) || value.level < 0 || value.level > 1) {
257
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.level`);
258
+ }
259
+ }
260
+ else if (value.level !== undefined) {
261
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.level`);
262
+ }
263
+ const result = {
264
+ name: identifier(value.name, `${path}.name`),
265
+ aggregation: value.aggregation,
266
+ field: identifier(value.field, `${path}.field`, true),
267
+ filters: Object.freeze(requireArray(value.filters, `${path}.filters`, limits.maxDatasetItems)
268
+ .map((filter, index) => nested(() => validateProtocolExpression(filter), `${path}.filters[${index}]`))),
269
+ };
270
+ if (value.argField !== undefined)
271
+ result.argField = identifier(value.argField, `${path}.argField`, true);
272
+ if (value.level !== undefined)
273
+ result.level = value.level;
274
+ if (value.sql !== undefined)
275
+ result.sql = nested(() => validateProtocolSqlExpression(value.sql), `${path}.sql`);
276
+ optionalText(value.label, 'label', result, path, limits);
277
+ optionalText(value.description, 'description', result, path, limits);
278
+ return freezeRecord(result);
279
+ }
280
+ function validateFilter(input, path, limits) {
281
+ const value = requireRecord(input, path);
282
+ exactFields(value, ['name', 'field', 'operators'], ['label', 'description'], path);
283
+ const operators = uniqueStrings(value.operators, `${path}.operators`, limits.maxDatasetItems, (operator, operatorPath) => {
284
+ if (typeof operator !== 'string' || !OPERATORS.has(operator)) {
285
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', operatorPath);
286
+ }
287
+ return operator;
288
+ });
289
+ if (operators.length === 0)
290
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.operators`);
291
+ const result = {
292
+ name: identifier(value.name, `${path}.name`),
293
+ field: identifier(value.field, `${path}.field`, true),
294
+ operators,
295
+ };
296
+ optionalText(value.label, 'label', result, path, limits);
297
+ optionalText(value.description, 'description', result, path, limits);
298
+ return freezeRecord(result);
299
+ }
300
+ function validateRelationship(input, path) {
301
+ const value = requireRecord(input, path);
302
+ exactFields(value, ['name', 'kind', 'target', 'from', 'to', 'queryable'], [], path);
303
+ if (!['belongsTo', 'hasMany', 'hasOne'].includes(value.kind)) {
304
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.kind`);
305
+ }
306
+ if (typeof value.queryable !== 'boolean')
307
+ deploymentError('HQ_DEPLOYMENT_TYPE', `${path}.queryable`);
308
+ if ((value.kind === 'hasMany') === value.queryable) {
309
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.queryable`);
310
+ }
311
+ return freezeRecord({
312
+ name: identifier(value.name, `${path}.name`),
313
+ kind: value.kind,
314
+ target: identifier(value.target, `${path}.target`),
315
+ from: identifier(value.from, `${path}.from`, true),
316
+ to: identifier(value.to, `${path}.to`, true),
317
+ queryable: value.queryable,
318
+ });
319
+ }
320
+ function validateMetric(input, path, limits) {
321
+ const value = requireRecord(input, path);
322
+ exactFields(value, ['name', 'kind', 'expression', 'dimensions', 'filters', 'grains', 'endpoint'], ['grain', 'label', 'description'], path);
323
+ if (!['metric', 'derived-metric', 'grained-metric'].includes(value.kind)) {
324
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.kind`);
325
+ }
326
+ const parseGrain = (grain, grainPath) => {
327
+ if (typeof grain !== 'string' || !GRAINS.has(grain)) {
328
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', grainPath);
329
+ }
330
+ return grain;
331
+ };
332
+ const grains = uniqueStrings(value.grains, `${path}.grains`, limits.maxDatasetItems, parseGrain);
333
+ if ((value.kind === 'grained-metric') !== (value.grain !== undefined)) {
334
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.grain`);
335
+ }
336
+ const grain = value.grain === undefined
337
+ ? undefined
338
+ : parseGrain(value.grain, `${path}.grain`);
339
+ if (grain !== undefined && grains.length === 0) {
340
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.grains`);
341
+ }
342
+ if (grain !== undefined && !grains.includes(grain)) {
343
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.grain`);
344
+ }
345
+ const result = {
346
+ name: identifier(value.name, `${path}.name`),
347
+ kind: value.kind,
348
+ expression: nested(() => validateProtocolExpression(value.expression), `${path}.expression`),
349
+ dimensions: uniqueStrings(value.dimensions, `${path}.dimensions`, limits.maxDatasetItems, (item, itemPath) => identifier(item, itemPath, true)),
350
+ filters: uniqueStrings(value.filters, `${path}.filters`, limits.maxDatasetItems, (item, itemPath) => identifier(item, itemPath)),
351
+ grains,
352
+ endpoint: validateEndpoint(value.endpoint, `${path}.endpoint`, limits),
353
+ };
354
+ if (grain !== undefined)
355
+ result.grain = grain;
356
+ optionalText(value.label, 'label', result, path, limits);
357
+ optionalText(value.description, 'description', result, path, limits);
358
+ return freezeRecord(result);
359
+ }
360
+ function validateLimits(input, path) {
361
+ const value = requireRecord(input, path);
362
+ exactFields(value, [], ['maxDimensions', 'maxMeasures', 'maxFilters', 'maxResultSize'], path);
363
+ const result = {};
364
+ for (const key of ['maxDimensions', 'maxMeasures', 'maxFilters', 'maxResultSize']) {
365
+ if (value[key] !== undefined)
366
+ result[key] = positiveInteger(value[key], `${path}.${key}`);
367
+ }
368
+ return freezeRecord(result);
369
+ }
370
+ function namedItems(input, path, maxItems, validate) {
371
+ const items = requireArray(input, path, maxItems).map((value, index) => validate(value, `${path}[${index}]`));
372
+ if (new Set(items.map(item => item.name)).size !== items.length) {
373
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', path);
374
+ }
375
+ return Object.freeze(items);
376
+ }
377
+ function validateDataset(input, path, limits) {
378
+ const value = requireRecord(input, path);
379
+ exactFields(value, ['name', 'source', 'tenant', 'dimensions', 'measures', 'filters', 'metrics', 'relationships'], ['timeField', 'limits', 'endpoint'], path);
380
+ const result = {
381
+ name: identifier(value.name, `${path}.name`),
382
+ source: boundedText(value.source, `${path}.source`, limits.maxSourceBytes),
383
+ tenant: validateTenant(value.tenant, `${path}.tenant`, limits),
384
+ dimensions: namedItems(value.dimensions, `${path}.dimensions`, limits.maxDatasetItems, (item, itemPath) => validateDimension(item, itemPath, limits)),
385
+ measures: namedItems(value.measures, `${path}.measures`, limits.maxDatasetItems, (item, itemPath) => validateMeasure(item, itemPath, limits)),
386
+ filters: namedItems(value.filters, `${path}.filters`, limits.maxDatasetItems, (item, itemPath) => validateFilter(item, itemPath, limits)),
387
+ metrics: namedItems(value.metrics, `${path}.metrics`, limits.maxDatasetItems, (item, itemPath) => validateMetric(item, itemPath, limits)),
388
+ relationships: namedItems(value.relationships, `${path}.relationships`, limits.maxDatasetItems, (item, itemPath) => validateRelationship(item, itemPath)),
389
+ };
390
+ if (value.timeField !== undefined)
391
+ result.timeField = identifier(value.timeField, `${path}.timeField`, true);
392
+ if (value.limits !== undefined)
393
+ result.limits = validateLimits(value.limits, `${path}.limits`);
394
+ if (value.endpoint !== undefined)
395
+ result.endpoint = validateEndpoint(value.endpoint, `${path}.endpoint`, limits);
396
+ return freezeRecord(result);
397
+ }
398
+ export function validateProtocolDatasetContract(input, options = {}) {
399
+ return validateDataset(input, '$', resolveDeploymentLimits(options));
400
+ }
401
+ function validateQuery(input, path, limits) {
402
+ const value = requireRecord(input, path);
403
+ exactFields(value, ['name', 'input', 'output', 'implementation', 'endpoint', 'tags'], ['summary', 'description'], path);
404
+ const result = {
405
+ name: identifier(value.name, `${path}.name`),
406
+ input: nested(() => validateProtocolSchema(value.input), `${path}.input`),
407
+ output: nested(() => validateProtocolSchema(value.output), `${path}.output`),
408
+ implementation: nested(() => validateProtocolQueryImplementation(value.implementation), `${path}.implementation`),
409
+ endpoint: validateEndpoint(value.endpoint, `${path}.endpoint`, limits, true),
410
+ tags: uniqueStrings(value.tags, `${path}.tags`, limits.maxDatasetItems, (tag, tagPath) => boundedText(tag, tagPath, limits.maxTextBytes)),
411
+ };
412
+ optionalText(value.summary, 'summary', result, path, limits);
413
+ optionalText(value.description, 'description', result, path, limits);
414
+ return freezeRecord(result);
415
+ }
416
+ function validateArtifact(input, path) {
417
+ const value = requireRecord(input, path);
418
+ exactFields(value, ['runtime', 'artifactSha256'], [], path);
419
+ if (value.runtime !== 'node' && value.runtime !== 'python') {
420
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.runtime`);
421
+ }
422
+ if (typeof value.artifactSha256 !== 'string' || !SHA256_PATTERN.test(value.artifactSha256)) {
423
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', `${path}.artifactSha256`);
424
+ }
425
+ return freezeRecord({
426
+ runtime: value.runtime,
427
+ artifactSha256: value.artifactSha256,
428
+ });
429
+ }
430
+ function validateReferences(contract) {
431
+ const datasets = new Map(contract.datasets.map(dataset => [dataset.name, dataset]));
432
+ for (const [datasetIndex, dataset] of contract.datasets.entries()) {
433
+ for (const [relationshipIndex, relationship] of dataset.relationships.entries()) {
434
+ if (!datasets.has(relationship.target)) {
435
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.datasets[${datasetIndex}].relationships[${relationshipIndex}].target`);
436
+ }
437
+ }
438
+ const filters = new Set(dataset.filters.map(filter => filter.name));
439
+ const hasDimension = (name) => {
440
+ const [head, ...tail] = name.split('.');
441
+ if (tail.length === 0)
442
+ return dataset.dimensions.some(dimension => dimension.name === head);
443
+ const relationship = dataset.relationships.find(candidate => candidate.name === head && candidate.queryable);
444
+ const target = relationship ? datasets.get(relationship.target) : undefined;
445
+ return target?.dimensions.some(dimension => dimension.name === tail.join('.')) ?? false;
446
+ };
447
+ for (const [metricIndex, metric] of dataset.metrics.entries()) {
448
+ if (metric.dimensions.some(dimension => !hasDimension(dimension))
449
+ || metric.filters.some(filter => !filters.has(filter))) {
450
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.datasets[${datasetIndex}].metrics[${metricIndex}]`);
451
+ }
452
+ if ((metric.kind === 'grained-metric' || metric.grains.length > 0)
453
+ && dataset.timeField === undefined) {
454
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.datasets[${datasetIndex}].metrics[${metricIndex}].grains`);
455
+ }
456
+ if (dataset.tenant.kind === 'required' && metric.endpoint.tenant.kind !== 'required') {
457
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.datasets[${datasetIndex}].metrics[${metricIndex}].endpoint.tenant`);
458
+ }
459
+ }
460
+ if (dataset.tenant.kind === 'required'
461
+ && dataset.endpoint !== undefined
462
+ && dataset.endpoint.tenant.kind !== 'required') {
463
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.datasets[${datasetIndex}].endpoint.tenant`);
464
+ }
465
+ }
466
+ const artifacts = new Map(contract.artifacts.map(artifact => [artifact.artifactSha256, artifact.runtime]));
467
+ for (const [queryIndex, query] of contract.queries.entries()) {
468
+ if (query.implementation.kind === 'runtime-reference'
469
+ && artifacts.get(query.implementation.artifactSha256) !== query.implementation.runtime) {
470
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.queries[${queryIndex}].implementation.artifactSha256`);
471
+ }
472
+ if (query.implementation.kind === 'compiled-sql') {
473
+ const implementationRequiresTenant = query.implementation.tenant.kind === 'required';
474
+ const endpointRequiresTenant = query.endpoint.tenant.kind === 'required';
475
+ if (implementationRequiresTenant !== endpointRequiresTenant) {
476
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.queries[${queryIndex}].endpoint.tenant`);
477
+ }
478
+ for (const [parameterIndex, parameter] of query.implementation.parameters.entries()) {
479
+ if (parameter.source.kind === 'input'
480
+ && !schemaContainsPath(query.input, parameter.source.path.split('.'))) {
481
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.queries[${queryIndex}].implementation.parameters[${parameterIndex}].source.path`);
482
+ }
483
+ }
484
+ }
485
+ if (query.implementation.kind === 'semantic-plan') {
486
+ const dataset = datasets.get(query.implementation.query.dataset);
487
+ if (!dataset) {
488
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.queries[${queryIndex}].implementation.query.dataset`);
489
+ }
490
+ if (dataset.tenant.kind === 'required' && query.endpoint.tenant.kind !== 'required') {
491
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', `$.queries[${queryIndex}].endpoint.tenant`);
492
+ }
493
+ }
494
+ }
495
+ }
496
+ function schemaContainsPath(schema, segments) {
497
+ if (segments.length === 0)
498
+ return true;
499
+ if (schema.kind === 'any')
500
+ return true;
501
+ if (schema.kind === 'union') {
502
+ return schema.variants.every(variant => schemaContainsPath(variant, segments));
503
+ }
504
+ if (schema.kind === 'record') {
505
+ return schemaContainsPath(schema.values, segments.slice(1));
506
+ }
507
+ if (schema.kind !== 'object')
508
+ return false;
509
+ const [head, ...tail] = segments;
510
+ const property = schema.properties[head];
511
+ return property !== undefined && schemaContainsPath(property, tail);
512
+ }
513
+ export function validateProtocolDeploymentContract(input, options = {}) {
514
+ const limits = resolveDeploymentLimits(options);
515
+ const value = requireRecord(input, '$');
516
+ exactFields(value, ['kind', 'version', 'datasets', 'queries', 'artifacts'], [], '$');
517
+ if (value.kind !== 'hypequery-deployment')
518
+ deploymentError('HQ_DEPLOYMENT_INVALID_VALUE', '$.kind');
519
+ if (value.version !== 1)
520
+ deploymentError('HQ_DEPLOYMENT_INVALID_VERSION', '$.version');
521
+ const datasets = namedItems(value.datasets, '$.datasets', limits.maxDatasets, (dataset, path) => validateDataset(dataset, path, limits));
522
+ const queries = namedItems(value.queries, '$.queries', limits.maxQueries, (query, path) => validateQuery(query, path, limits));
523
+ const artifacts = Object.freeze(requireArray(value.artifacts, '$.artifacts', limits.maxArtifacts)
524
+ .map((artifact, index) => validateArtifact(artifact, `$.artifacts[${index}]`)));
525
+ if (new Set(artifacts.map(artifact => artifact.artifactSha256)).size !== artifacts.length) {
526
+ deploymentError('HQ_DEPLOYMENT_INVALID_REFERENCE', '$.artifacts');
527
+ }
528
+ const result = freezeRecord({
529
+ kind: 'hypequery-deployment',
530
+ version: 1,
531
+ datasets,
532
+ queries,
533
+ artifacts,
534
+ });
535
+ validateReferences(result);
536
+ return result;
537
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProtocolExpressionErrorCode } from './types.js';
2
+ export declare class ProtocolExpressionError extends TypeError {
3
+ readonly code: ProtocolExpressionErrorCode;
4
+ readonly path: string;
5
+ constructor(code: ProtocolExpressionErrorCode, path?: string);
6
+ }
7
+ export declare function expressionError(code: ProtocolExpressionErrorCode, path?: string): never;
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/expressions/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAE9D,qBAAa,uBAAwB,SAAQ,SAAS;IACpD,QAAQ,CAAC,IAAI,EAAE,2BAA2B,CAAC;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,2BAA2B,EAAE,IAAI,SAAM;CAM1D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,2BAA2B,EAAE,IAAI,SAAM,GAAG,KAAK,CAEpF"}
@@ -0,0 +1,13 @@
1
+ export class ProtocolExpressionError extends TypeError {
2
+ code;
3
+ path;
4
+ constructor(code, path = '$') {
5
+ super(`${code} at ${path}`);
6
+ this.name = 'ProtocolExpressionError';
7
+ this.code = code;
8
+ this.path = path;
9
+ }
10
+ }
11
+ export function expressionError(code, path = '$') {
12
+ throw new ProtocolExpressionError(code, path);
13
+ }
@@ -0,0 +1,5 @@
1
+ export { ProtocolExpressionError } from './errors.js';
2
+ export { DEFAULT_PROTOCOL_EXPRESSION_LIMITS } from './limits.js';
3
+ export { validateProtocolExpression, validateProtocolSemanticQuery } from './validate.js';
4
+ export type { ProtocolAggregation, ProtocolBinaryOperator, ProtocolComparisonOperator, ProtocolDatasetQuery, ProtocolExpression, ProtocolExpressionErrorCode, ProtocolExpressionLimits, ProtocolExpressionOptions, ProtocolFunctionName, ProtocolMetricQuery, ProtocolOrderBy, ProtocolSemanticQuery, ProtocolTimeGrain, } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/expressions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,kCAAkC,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,0BAA0B,EAAE,6BAA6B,EAAE,MAAM,eAAe,CAAC;AAC1F,YAAY,EACV,mBAAmB,EACnB,sBAAsB,EACtB,0BAA0B,EAC1B,oBAAoB,EACpB,kBAAkB,EAClB,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { ProtocolExpressionError } from './errors.js';
2
+ export { DEFAULT_PROTOCOL_EXPRESSION_LIMITS } from './limits.js';
3
+ export { validateProtocolExpression, validateProtocolSemanticQuery } from './validate.js';
@@ -0,0 +1,4 @@
1
+ import type { ProtocolExpressionLimits, ProtocolExpressionOptions } from './types.js';
2
+ export declare const DEFAULT_PROTOCOL_EXPRESSION_LIMITS: Readonly<ProtocolExpressionLimits>;
3
+ export declare function resolveExpressionLimits(options?: ProtocolExpressionOptions): Readonly<ProtocolExpressionLimits>;
4
+ //# sourceMappingURL=limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/expressions/limits.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAEtF,eAAO,MAAM,kCAAkC,EAAE,QAAQ,CAAC,wBAAwB,CAIhF,CAAC;AAEH,wBAAgB,uBAAuB,CACrC,OAAO,GAAE,yBAA8B,GACtC,QAAQ,CAAC,wBAAwB,CAAC,CAWpC"}
@@ -0,0 +1,17 @@
1
+ export const DEFAULT_PROTOCOL_EXPRESSION_LIMITS = Object.freeze({
2
+ maxDepth: 16,
3
+ maxNodes: 1_000,
4
+ maxCollectionItems: 100,
5
+ });
6
+ export function resolveExpressionLimits(options = {}) {
7
+ const limits = { ...DEFAULT_PROTOCOL_EXPRESSION_LIMITS };
8
+ for (const key of Object.keys(options.limits ?? {})) {
9
+ const value = options.limits?.[key];
10
+ if (!Number.isSafeInteger(value) || value < 1
11
+ || value > DEFAULT_PROTOCOL_EXPRESSION_LIMITS[key]) {
12
+ throw new RangeError(`${key} must be a positive integer no greater than the protocol v1 maximum`);
13
+ }
14
+ limits[key] = value;
15
+ }
16
+ return Object.freeze(limits);
17
+ }