@hypequery/protocol 0.19.0 → 0.21.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/README.md +26 -0
- package/dist/bundles/codec.js +2 -2
- package/dist/cache-keys/cache-keys.js +2 -2
- package/dist/cache-preimages/cache-preimages.js +3 -3
- package/dist/client-contract/bounds.d.ts +3 -0
- package/dist/client-contract/bounds.d.ts.map +1 -0
- package/dist/client-contract/bounds.js +24 -0
- package/dist/client-contract/codec.d.ts +4 -0
- package/dist/client-contract/codec.d.ts.map +1 -0
- package/dist/client-contract/codec.js +8 -0
- package/dist/client-contract/decode.d.ts +4 -0
- package/dist/client-contract/decode.d.ts.map +1 -0
- package/dist/client-contract/decode.js +100 -0
- package/dist/client-contract/errors.d.ts +21 -0
- package/dist/client-contract/errors.d.ts.map +1 -0
- package/dist/client-contract/errors.js +26 -0
- package/dist/client-contract/index.d.ts +8 -0
- package/dist/client-contract/index.d.ts.map +1 -0
- package/dist/client-contract/index.js +4 -0
- package/dist/client-contract/nodes.d.ts +8 -0
- package/dist/client-contract/nodes.d.ts.map +1 -0
- package/dist/client-contract/nodes.js +68 -0
- package/dist/client-contract/primitives.d.ts +15 -0
- package/dist/client-contract/primitives.d.ts.map +1 -0
- package/dist/client-contract/primitives.js +74 -0
- package/dist/client-contract/query.d.ts +10 -0
- package/dist/client-contract/query.d.ts.map +1 -0
- package/dist/client-contract/query.js +121 -0
- package/dist/client-contract/safe-data.d.ts +6 -0
- package/dist/client-contract/safe-data.d.ts.map +1 -0
- package/dist/client-contract/safe-data.js +124 -0
- package/dist/client-contract/timezone-names.d.ts +2 -0
- package/dist/client-contract/timezone-names.d.ts.map +1 -0
- package/dist/client-contract/timezone-names.js +600 -0
- package/dist/client-contract/types.d.ts +68 -0
- package/dist/client-contract/types.d.ts.map +1 -0
- package/dist/client-contract/types.js +1 -0
- package/dist/client-contract/validate.d.ts +6 -0
- package/dist/client-contract/validate.d.ts.map +1 -0
- package/dist/client-contract/validate.js +95 -0
- package/dist/deployments/codec.js +2 -2
- package/dist/deployments/collect.d.ts +22 -0
- package/dist/deployments/collect.d.ts.map +1 -0
- package/dist/deployments/collect.js +69 -0
- package/dist/deployments/index.d.ts +2 -2
- package/dist/deployments/index.d.ts.map +1 -1
- package/dist/deployments/index.js +1 -1
- package/dist/deployments/types.d.ts +20 -1
- package/dist/deployments/types.d.ts.map +1 -1
- package/dist/deployments/validate.d.ts +22 -1
- package/dist/deployments/validate.d.ts.map +1 -1
- package/dist/deployments/validate.js +171 -79
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/releases/codec.js +2 -2
- package/dist/values/codec.js +2 -2
- package/dist/visibility/errors.d.ts +8 -0
- package/dist/visibility/errors.d.ts.map +1 -0
- package/dist/visibility/errors.js +13 -0
- package/dist/visibility/index.d.ts +5 -0
- package/dist/visibility/index.d.ts.map +1 -0
- package/dist/visibility/index.js +3 -0
- package/dist/visibility/resolve.d.ts +30 -0
- package/dist/visibility/resolve.d.ts.map +1 -0
- package/dist/visibility/resolve.js +108 -0
- package/dist/visibility/types.d.ts +32 -0
- package/dist/visibility/types.d.ts.map +1 -0
- package/dist/visibility/types.js +1 -0
- package/dist/visibility/validate.d.ts +11 -0
- package/dist/visibility/validate.d.ts.map +1 -0
- package/dist/visibility/validate.js +78 -0
- package/package.json +3 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function pick(all, allowed) {
|
|
2
|
+
if (allowed === '*')
|
|
3
|
+
return new Set(all);
|
|
4
|
+
const permitted = new Set(allowed);
|
|
5
|
+
return new Set(all.filter(name => permitted.has(name)));
|
|
6
|
+
}
|
|
7
|
+
/** The measures a measure is computed from: a derived measure's inputs, or the one a time measure wraps. */
|
|
8
|
+
function measureInputs(measure) {
|
|
9
|
+
if ('uses' in measure && Array.isArray(measure.uses)) {
|
|
10
|
+
return measure.uses.map((use) => String(use.measure));
|
|
11
|
+
}
|
|
12
|
+
if ('measure' in measure && typeof measure.measure === 'string')
|
|
13
|
+
return [measure.measure];
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether a relationship's local join keys are visible. A joined `to` field
|
|
18
|
+
* holds the same values as its `from` key, so exposing the join while hiding
|
|
19
|
+
* the key would let a caller read the key back through the relationship.
|
|
20
|
+
* Execution joins on keys as physical columns, so a key counts as visible only
|
|
21
|
+
* when a visible dimension reads that column. A dimension merely named like
|
|
22
|
+
* the key doesn't count: it may read a different column.
|
|
23
|
+
*/
|
|
24
|
+
function joinKeysVisible(relationship, dataset, visible, allDimensions) {
|
|
25
|
+
if (allDimensions)
|
|
26
|
+
return true;
|
|
27
|
+
const keys = [String(relationship.from), ...(relationship.keys ?? []).map(key => String(key.from))];
|
|
28
|
+
return keys.every(key => dataset.dimensions.some(dimension => (visible.has(String(dimension.name))
|
|
29
|
+
&& dimension.source.kind === 'column'
|
|
30
|
+
&& dimension.source.column === key)));
|
|
31
|
+
}
|
|
32
|
+
/** A measure over the dataset's time axis (window or shift), which needs the time field. */
|
|
33
|
+
function isTimeMeasure(measure) {
|
|
34
|
+
return 'measure' in measure && typeof measure.measure === 'string';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* What one dataset exposes under a visibility policy, or null when the policy
|
|
38
|
+
* hides the dataset (RFC 0019).
|
|
39
|
+
*
|
|
40
|
+
* Names the policy lists but the contract doesn't have are ignored, so a
|
|
41
|
+
* policy written against one release keeps working after a later release
|
|
42
|
+
* removes a field. Visibility only narrows: a dimension the contract marks
|
|
43
|
+
* non-groupable or non-filterable stays so.
|
|
44
|
+
*
|
|
45
|
+
* - A one-hop `relationship.dimension` is visible only when the target dataset
|
|
46
|
+
* and that dimension are visible in the target's own entry, and the
|
|
47
|
+
* relationship's local join keys are visible.
|
|
48
|
+
* - A declared filter is visible only when the dimension it filters is.
|
|
49
|
+
* - The time field is visible only as a visible dimension; grouping by period
|
|
50
|
+
* and time measures need it.
|
|
51
|
+
* - A measure is visible only when it is listed and every measure it is
|
|
52
|
+
* computed from is visible too. A derived ratio over a hidden measure would
|
|
53
|
+
* let a caller recover the hidden value.
|
|
54
|
+
* - Measures are authorized by name, independently of the dimensions they
|
|
55
|
+
* aggregate: `revenue = sum(amount)` stays visible with `amount` hidden. A
|
|
56
|
+
* `min`, `max`, `argMax` or `countDistinct` over a hidden dimension reveals
|
|
57
|
+
* its values, so list measures explicitly rather than use `*` when a dataset
|
|
58
|
+
* has any that must stay hidden; `*` also exposes measures added later.
|
|
59
|
+
* - Segments are not narrowed. Deployment contract 2, which this resolves, has
|
|
60
|
+
* none.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveProtocolVisibleDatasetFields(dataset, datasets, visibility) {
|
|
63
|
+
const entry = Object.hasOwn(visibility.datasets, String(dataset.name))
|
|
64
|
+
? visibility.datasets[String(dataset.name)]
|
|
65
|
+
: undefined;
|
|
66
|
+
if (entry === undefined)
|
|
67
|
+
return null;
|
|
68
|
+
const local = pick(dataset.dimensions.map(dimension => String(dimension.name)), entry.dimensions);
|
|
69
|
+
const dimensions = new Set(local);
|
|
70
|
+
const timeField = dataset.timeField !== undefined && local.has(String(dataset.timeField));
|
|
71
|
+
const relationships = new Set();
|
|
72
|
+
for (const relationship of dataset.relationships) {
|
|
73
|
+
const target = datasets.get(String(relationship.target));
|
|
74
|
+
const targetEntry = target && Object.hasOwn(visibility.datasets, String(target.name))
|
|
75
|
+
? visibility.datasets[String(target.name)]
|
|
76
|
+
: undefined;
|
|
77
|
+
if (!target || targetEntry === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
if (!joinKeysVisible(relationship, dataset, local, entry.dimensions === '*'))
|
|
80
|
+
continue;
|
|
81
|
+
relationships.add(String(relationship.name));
|
|
82
|
+
const visible = pick(target.dimensions.map(dimension => String(dimension.name)), targetEntry.dimensions);
|
|
83
|
+
for (const name of visible)
|
|
84
|
+
dimensions.add(`${String(relationship.name)}.${name}`);
|
|
85
|
+
}
|
|
86
|
+
const filters = new Set(dataset.filters
|
|
87
|
+
.filter(filter => dimensions.has(String(filter.field)))
|
|
88
|
+
.map(filter => String(filter.name)));
|
|
89
|
+
const byName = new Map(dataset.measures.map(measure => [String(measure.name), measure]));
|
|
90
|
+
const listed = pick([...byName.keys()], entry.measures);
|
|
91
|
+
const decided = new Map();
|
|
92
|
+
const isVisible = (name, path) => {
|
|
93
|
+
const known = decided.get(name);
|
|
94
|
+
if (known !== undefined)
|
|
95
|
+
return known;
|
|
96
|
+
const measure = byName.get(name);
|
|
97
|
+
// A cycle can't occur in a valid contract; refuse rather than recurse.
|
|
98
|
+
const visible = measure !== undefined
|
|
99
|
+
&& listed.has(name)
|
|
100
|
+
&& !path.has(name)
|
|
101
|
+
&& (!isTimeMeasure(measure) || timeField)
|
|
102
|
+
&& measureInputs(measure).every(input => isVisible(input, new Set([...path, name])));
|
|
103
|
+
decided.set(name, visible);
|
|
104
|
+
return visible;
|
|
105
|
+
};
|
|
106
|
+
const measures = new Set([...byName.keys()].filter(name => isVisible(name, new Set())));
|
|
107
|
+
return Object.freeze({ dimensions, measures, filters, relationships, timeField });
|
|
108
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Every field of its kind, including ones a later release adds. */
|
|
2
|
+
export type ProtocolVisibleFields = '*' | readonly string[];
|
|
3
|
+
export interface ProtocolDatasetVisibility {
|
|
4
|
+
readonly dimensions: ProtocolVisibleFields;
|
|
5
|
+
readonly measures: ProtocolVisibleFields;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Which datasets and fields a principal may see and query (RFC 0019).
|
|
9
|
+
*
|
|
10
|
+
* An allowlist: a dataset absent from `datasets` is hidden entirely, and a
|
|
11
|
+
* field absent from its dataset's list is hidden. It narrows what the
|
|
12
|
+
* contract publishes and never widens it.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProtocolFieldVisibility {
|
|
15
|
+
readonly kind: 'hypequery-field-visibility';
|
|
16
|
+
readonly version: 1;
|
|
17
|
+
readonly datasets: Readonly<Record<string, ProtocolDatasetVisibility>>;
|
|
18
|
+
}
|
|
19
|
+
/** What one dataset exposes under a visibility policy. */
|
|
20
|
+
export interface ProtocolVisibleDatasetFields {
|
|
21
|
+
/** Local dimension names and one-hop `relationship.dimension` names. */
|
|
22
|
+
readonly dimensions: ReadonlySet<string>;
|
|
23
|
+
readonly measures: ReadonlySet<string>;
|
|
24
|
+
/** Declared filter names whose field is a visible dimension. */
|
|
25
|
+
readonly filters: ReadonlySet<string>;
|
|
26
|
+
/** Relationships whose join keys and target are visible; only these contribute joined fields. */
|
|
27
|
+
readonly relationships: ReadonlySet<string>;
|
|
28
|
+
/** Whether the dataset's time field may be grouped by (`by`). */
|
|
29
|
+
readonly timeField: boolean;
|
|
30
|
+
}
|
|
31
|
+
export type ProtocolFieldVisibilityErrorCode = 'HQ_VISIBILITY_INVALID' | 'HQ_VISIBILITY_TOO_LARGE';
|
|
32
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/visibility/types.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,MAAM,MAAM,qBAAqB,GAAG,GAAG,GAAG,SAAS,MAAM,EAAE,CAAC;AAE5D,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAAC;CACxE;AAED,0DAA0D;AAC1D,MAAM,WAAW,4BAA4B;IAC3C,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,iGAAiG;IACjG,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC5C,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,MAAM,gCAAgC,GAAG,uBAAuB,GAAG,yBAAyB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProtocolFieldVisibility } from './types.js';
|
|
2
|
+
export declare const PROTOCOL_FIELD_VISIBILITY_LIMITS: Readonly<{
|
|
3
|
+
maxDatasets: 256;
|
|
4
|
+
maxFieldsPerList: 1024;
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* Parses an untrusted visibility record. Unknown keys, malformed names and
|
|
8
|
+
* oversized lists fail closed; the result is frozen with sorted field lists.
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateProtocolFieldVisibility(input: unknown): ProtocolFieldVisibility;
|
|
11
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/visibility/validate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,uBAAuB,EAExB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,gCAAgC;;;EAG3C,CAAC;AAoCH;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CA2BvF"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { isProtocolIdentifier } from '../identifiers/index.js';
|
|
2
|
+
import { visibilityError } from './errors.js';
|
|
3
|
+
export const PROTOCOL_FIELD_VISIBILITY_LIMITS = Object.freeze({
|
|
4
|
+
maxDatasets: 256,
|
|
5
|
+
maxFieldsPerList: 1_024,
|
|
6
|
+
});
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
9
|
+
return false;
|
|
10
|
+
const prototype = Object.getPrototypeOf(value);
|
|
11
|
+
return prototype === Object.prototype || prototype === null;
|
|
12
|
+
}
|
|
13
|
+
function exactKeys(value, keys, path) {
|
|
14
|
+
for (const key of Object.keys(value)) {
|
|
15
|
+
if (!keys.includes(key))
|
|
16
|
+
visibilityError('HQ_VISIBILITY_INVALID', `${path}.${key}`);
|
|
17
|
+
}
|
|
18
|
+
for (const key of keys) {
|
|
19
|
+
if (!Object.hasOwn(value, key))
|
|
20
|
+
visibilityError('HQ_VISIBILITY_INVALID', `${path}.${key}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function fields(value, path) {
|
|
24
|
+
if (value === '*')
|
|
25
|
+
return '*';
|
|
26
|
+
if (!Array.isArray(value))
|
|
27
|
+
visibilityError('HQ_VISIBILITY_INVALID', path);
|
|
28
|
+
if (value.length > PROTOCOL_FIELD_VISIBILITY_LIMITS.maxFieldsPerList) {
|
|
29
|
+
visibilityError('HQ_VISIBILITY_TOO_LARGE', path);
|
|
30
|
+
}
|
|
31
|
+
// Array.from visits holes as undefined, so a sparse list fails closed.
|
|
32
|
+
const names = Array.from(value, (name, index) => {
|
|
33
|
+
if (typeof name !== 'string' || !isProtocolIdentifier(name)) {
|
|
34
|
+
visibilityError('HQ_VISIBILITY_INVALID', `${path}[${index}]`);
|
|
35
|
+
}
|
|
36
|
+
return name;
|
|
37
|
+
});
|
|
38
|
+
if (new Set(names).size !== names.length)
|
|
39
|
+
visibilityError('HQ_VISIBILITY_INVALID', path);
|
|
40
|
+
return Object.freeze([...names].sort());
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parses an untrusted visibility record. Unknown keys, malformed names and
|
|
44
|
+
* oversized lists fail closed; the result is frozen with sorted field lists.
|
|
45
|
+
*/
|
|
46
|
+
export function validateProtocolFieldVisibility(input) {
|
|
47
|
+
if (!isRecord(input))
|
|
48
|
+
visibilityError('HQ_VISIBILITY_INVALID');
|
|
49
|
+
exactKeys(input, ['kind', 'version', 'datasets'], '$');
|
|
50
|
+
if (input.kind !== 'hypequery-field-visibility')
|
|
51
|
+
visibilityError('HQ_VISIBILITY_INVALID', '$.kind');
|
|
52
|
+
if (input.version !== 1)
|
|
53
|
+
visibilityError('HQ_VISIBILITY_INVALID', '$.version');
|
|
54
|
+
if (!isRecord(input.datasets))
|
|
55
|
+
visibilityError('HQ_VISIBILITY_INVALID', '$.datasets');
|
|
56
|
+
const entries = Object.entries(input.datasets);
|
|
57
|
+
if (entries.length > PROTOCOL_FIELD_VISIBILITY_LIMITS.maxDatasets) {
|
|
58
|
+
visibilityError('HQ_VISIBILITY_TOO_LARGE', '$.datasets');
|
|
59
|
+
}
|
|
60
|
+
const datasets = Object.create(null);
|
|
61
|
+
for (const [name, entry] of entries.sort(([left], [right]) => (left < right ? -1 : 1))) {
|
|
62
|
+
const path = `$.datasets.${name}`;
|
|
63
|
+
if (!isProtocolIdentifier(name))
|
|
64
|
+
visibilityError('HQ_VISIBILITY_INVALID', path);
|
|
65
|
+
if (!isRecord(entry))
|
|
66
|
+
visibilityError('HQ_VISIBILITY_INVALID', path);
|
|
67
|
+
exactKeys(entry, ['dimensions', 'measures'], path);
|
|
68
|
+
datasets[name] = Object.freeze({
|
|
69
|
+
dimensions: fields(entry.dimensions, `${path}.dimensions`),
|
|
70
|
+
measures: fields(entry.measures, `${path}.measures`),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
kind: 'hypequery-field-visibility',
|
|
75
|
+
version: 1,
|
|
76
|
+
datasets: Object.freeze(datasets),
|
|
77
|
+
});
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Portable analytics contracts and TypeScript reference implementation for Hypequery",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hypequery",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@noble/hashes": "^
|
|
30
|
+
"@noble/hashes": "^2.4.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.5.0",
|
|
34
34
|
"typescript": "^6.0.3",
|
|
35
|
-
"vitest": "^5.0.
|
|
35
|
+
"vitest": "^5.0.5"
|
|
36
36
|
},
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|