@hypequery/protocol 0.17.0 → 0.18.0
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/events/validate.d.ts.map +1 -1
- package/dist/events/validate.js +48 -16
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/events/validate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,wBAAwB,EAExB,kBAAkB,EAElB,yBAAyB,EAE1B,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/events/validate.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,wBAAwB,EAExB,kBAAkB,EAElB,yBAAyB,EAE1B,MAAM,YAAY,CAAC;AAuRpB,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CAiDpB;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,yBAA8B,GACtC,wBAAwB,CAiC1B"}
|
package/dist/events/validate.js
CHANGED
|
@@ -25,7 +25,7 @@ const DIAGNOSTICS_CONTEXT = {
|
|
|
25
25
|
fail: diagnosticsError,
|
|
26
26
|
};
|
|
27
27
|
const HEX64_PATTERN = /^[0-9a-f]{64}$/;
|
|
28
|
-
const OCCURRED_AT_PATTERN = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{
|
|
28
|
+
const OCCURRED_AT_PATTERN = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.[0-9]{3})?Z$/;
|
|
29
29
|
const OPERATIONS = ['query', 'command', 'insert'];
|
|
30
30
|
const OUTCOMES = ['success', 'failure'];
|
|
31
31
|
const ERROR_CATEGORIES = [
|
|
@@ -84,20 +84,37 @@ function requireString(context, value, path) {
|
|
|
84
84
|
context.fail(context.codes.type, path);
|
|
85
85
|
return value;
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
// The compiler's debug form spans lines, so debugQuery alone may carry tab,
|
|
88
|
+
// line feed, and carriage return.
|
|
89
|
+
const LINE_BREAK_CHARACTERS = new Set([0x09, 0x0a, 0x0d]);
|
|
90
|
+
// Rejects C0 controls, DEL, the C1 range, and unpaired surrogates, which have
|
|
91
|
+
// no UTF-8 encoding and so no canonical bytes.
|
|
92
|
+
function hasForbiddenCodeUnit(text, allowLineBreaks) {
|
|
88
93
|
for (let index = 0; index < text.length; index += 1) {
|
|
89
94
|
const code = text.charCodeAt(index);
|
|
95
|
+
if (code <= 0x1f && allowLineBreaks && LINE_BREAK_CHARACTERS.has(code))
|
|
96
|
+
continue;
|
|
90
97
|
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
|
|
91
98
|
return true;
|
|
99
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
100
|
+
// charCodeAt past the end is NaN, which fails the range check.
|
|
101
|
+
const low = text.charCodeAt(index + 1);
|
|
102
|
+
if (!(low >= 0xdc00 && low <= 0xdfff))
|
|
103
|
+
return true;
|
|
104
|
+
index += 1;
|
|
105
|
+
}
|
|
106
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
92
109
|
}
|
|
93
110
|
return false;
|
|
94
111
|
}
|
|
95
|
-
function boundedText(context, value, path, maximum) {
|
|
112
|
+
function boundedText(context, value, path, maximum, allowLineBreaks = false) {
|
|
96
113
|
const text = requireString(context, value, path);
|
|
97
114
|
if (text.length > maximum || textEncoder.encode(text).byteLength > maximum) {
|
|
98
115
|
context.fail(context.codes.tooLarge, path);
|
|
99
116
|
}
|
|
100
|
-
if (
|
|
117
|
+
if (hasForbiddenCodeUnit(text, allowLineBreaks)) {
|
|
101
118
|
context.fail(context.codes.invalidValue, path);
|
|
102
119
|
}
|
|
103
120
|
return text;
|
|
@@ -110,7 +127,21 @@ function hexIdentity(context, value, path) {
|
|
|
110
127
|
}
|
|
111
128
|
function occurredAt(context, value, path) {
|
|
112
129
|
const text = requireString(context, value, path);
|
|
113
|
-
|
|
130
|
+
const match = OCCURRED_AT_PATTERN.exec(text);
|
|
131
|
+
if (!match)
|
|
132
|
+
context.fail(context.codes.invalidValue, path);
|
|
133
|
+
const [year, month, day, hour, minute, second] = match.slice(1).map(Number);
|
|
134
|
+
// Round-trip through Date so impossible dates (2026-02-30) and 24:00 fail
|
|
135
|
+
// rather than rolling over. setUTCFullYear avoids Date.UTC's 0-99 remap.
|
|
136
|
+
const date = new Date(0);
|
|
137
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
138
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
139
|
+
if (date.getUTCFullYear() !== year
|
|
140
|
+
|| date.getUTCMonth() !== month - 1
|
|
141
|
+
|| date.getUTCDate() !== day
|
|
142
|
+
|| date.getUTCHours() !== hour
|
|
143
|
+
|| date.getUTCMinutes() !== minute
|
|
144
|
+
|| date.getUTCSeconds() !== second) {
|
|
114
145
|
context.fail(context.codes.invalidValue, path);
|
|
115
146
|
}
|
|
116
147
|
return text;
|
|
@@ -166,10 +197,18 @@ function kindAndVersion(context, value, kind) {
|
|
|
166
197
|
fail(codes.invalidVersion, '$.version');
|
|
167
198
|
}
|
|
168
199
|
}
|
|
200
|
+
function outcomeWithCategory(context, value, hasCategory) {
|
|
201
|
+
const outcome = stringChoice(context, value.outcome, '$.outcome', OUTCOMES);
|
|
202
|
+
if ((outcome === 'failure') !== hasCategory) {
|
|
203
|
+
context.fail(context.codes.invalidValue, '$.errorCategory');
|
|
204
|
+
}
|
|
205
|
+
return outcome;
|
|
206
|
+
}
|
|
169
207
|
export function validateProtocolQueryEvent(input, options = {}) {
|
|
170
208
|
const limits = resolveQueryEventLimits(options);
|
|
171
209
|
const context = EVENT_CONTEXT;
|
|
172
210
|
const value = requireRecord(context, input, '$');
|
|
211
|
+
kindAndVersion(context, value, 'hypequery-query-event');
|
|
173
212
|
exactFields(context, value, [
|
|
174
213
|
'kind',
|
|
175
214
|
'version',
|
|
@@ -181,15 +220,8 @@ export function validateProtocolQueryEvent(input, options = {}) {
|
|
|
181
220
|
'outcome',
|
|
182
221
|
'durationMs',
|
|
183
222
|
], ['errorCategory', 'rowCount', 'tenantFingerprint', 'correlationId'], '$');
|
|
184
|
-
kindAndVersion(context, value, 'hypequery-query-event');
|
|
185
|
-
const outcome = stringChoice(context, value.outcome, '$.outcome', OUTCOMES);
|
|
186
223
|
const hasCategory = Object.hasOwn(value, 'errorCategory');
|
|
187
|
-
|
|
188
|
-
eventError('HQ_EVENT_INVALID_VALUE', '$.errorCategory');
|
|
189
|
-
}
|
|
190
|
-
if (outcome === 'success' && hasCategory) {
|
|
191
|
-
eventError('HQ_EVENT_INVALID_VALUE', '$.errorCategory');
|
|
192
|
-
}
|
|
224
|
+
// Object literal entries evaluate in order, so fields are checked in RFC order.
|
|
193
225
|
return freezeRecord({
|
|
194
226
|
kind: 'hypequery-query-event',
|
|
195
227
|
version: 1,
|
|
@@ -198,7 +230,7 @@ export function validateProtocolQueryEvent(input, options = {}) {
|
|
|
198
230
|
target: queryTarget(context, value.target, '$.target'),
|
|
199
231
|
queryName: queryName(context, value.queryName, '$.queryName'),
|
|
200
232
|
operation: stringChoice(context, value.operation, '$.operation', OPERATIONS),
|
|
201
|
-
outcome,
|
|
233
|
+
outcome: outcomeWithCategory(context, value, hasCategory),
|
|
202
234
|
...(hasCategory ? {
|
|
203
235
|
errorCategory: stringChoice(context, value.errorCategory, '$.errorCategory', ERROR_CATEGORIES),
|
|
204
236
|
} : {}),
|
|
@@ -218,8 +250,8 @@ export function validateProtocolQueryDiagnostics(input, options = {}) {
|
|
|
218
250
|
const limits = resolveQueryEventLimits(options);
|
|
219
251
|
const context = DIAGNOSTICS_CONTEXT;
|
|
220
252
|
const value = requireRecord(context, input, '$');
|
|
221
|
-
exactFields(context, value, ['kind', 'version', 'eventId', 'queryId', 'terminalReason', 'attempts'], ['runtimeIdentity', 'debugQuery', 'safeMessage'], '$');
|
|
222
253
|
kindAndVersion(context, value, 'hypequery-query-diagnostics');
|
|
254
|
+
exactFields(context, value, ['kind', 'version', 'eventId', 'queryId', 'terminalReason', 'attempts'], ['runtimeIdentity', 'debugQuery', 'safeMessage'], '$');
|
|
223
255
|
return freezeRecord({
|
|
224
256
|
kind: 'hypequery-query-diagnostics',
|
|
225
257
|
version: 1,
|
|
@@ -231,7 +263,7 @@ export function validateProtocolQueryDiagnostics(input, options = {}) {
|
|
|
231
263
|
runtimeIdentity: hexIdentity(context, value.runtimeIdentity, '$.runtimeIdentity'),
|
|
232
264
|
} : {}),
|
|
233
265
|
...(Object.hasOwn(value, 'debugQuery') ? {
|
|
234
|
-
debugQuery: boundedText(context, value.debugQuery, '$.debugQuery', limits.maxDebugBytes),
|
|
266
|
+
debugQuery: boundedText(context, value.debugQuery, '$.debugQuery', limits.maxDebugBytes, true),
|
|
235
267
|
} : {}),
|
|
236
268
|
...(Object.hasOwn(value, 'safeMessage') ? {
|
|
237
269
|
safeMessage: boundedText(context, value.safeMessage, '$.safeMessage', limits.maxStringBytes),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Portable analytics contracts and TypeScript reference implementation for Hypequery",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hypequery",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"typescript": "^5.7.3",
|
|
34
|
-
"vitest": "^
|
|
34
|
+
"vitest": "^5.0.2"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsc --project tsconfig.json",
|
|
50
50
|
"dev": "tsc --project tsconfig.json --watch",
|
|
51
|
-
"lint": "eslint
|
|
51
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
52
52
|
"test": "vitest run",
|
|
53
53
|
"types": "tsc --project tsconfig.json --noEmit && tsc --project tsconfig.test.json"
|
|
54
54
|
}
|