@hypequery/protocol 0.20.0 → 0.22.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.
Files changed (72) hide show
  1. package/README.md +17 -0
  2. package/dist/bundles/codec.js +2 -2
  3. package/dist/cache-keys/cache-keys.js +2 -2
  4. package/dist/cache-preimages/cache-preimages.js +3 -3
  5. package/dist/client-contract/bounds.d.ts +3 -0
  6. package/dist/client-contract/bounds.d.ts.map +1 -0
  7. package/dist/client-contract/bounds.js +24 -0
  8. package/dist/client-contract/codec.d.ts +4 -0
  9. package/dist/client-contract/codec.d.ts.map +1 -0
  10. package/dist/client-contract/codec.js +8 -0
  11. package/dist/client-contract/decode.d.ts +4 -0
  12. package/dist/client-contract/decode.d.ts.map +1 -0
  13. package/dist/client-contract/decode.js +100 -0
  14. package/dist/client-contract/errors.d.ts +21 -0
  15. package/dist/client-contract/errors.d.ts.map +1 -0
  16. package/dist/client-contract/errors.js +26 -0
  17. package/dist/client-contract/index.d.ts +8 -0
  18. package/dist/client-contract/index.d.ts.map +1 -0
  19. package/dist/client-contract/index.js +4 -0
  20. package/dist/client-contract/nodes.d.ts +8 -0
  21. package/dist/client-contract/nodes.d.ts.map +1 -0
  22. package/dist/client-contract/nodes.js +68 -0
  23. package/dist/client-contract/primitives.d.ts +15 -0
  24. package/dist/client-contract/primitives.d.ts.map +1 -0
  25. package/dist/client-contract/primitives.js +74 -0
  26. package/dist/client-contract/query.d.ts +10 -0
  27. package/dist/client-contract/query.d.ts.map +1 -0
  28. package/dist/client-contract/query.js +121 -0
  29. package/dist/client-contract/safe-data.d.ts +6 -0
  30. package/dist/client-contract/safe-data.d.ts.map +1 -0
  31. package/dist/client-contract/safe-data.js +124 -0
  32. package/dist/client-contract/timezone-names.d.ts +2 -0
  33. package/dist/client-contract/timezone-names.d.ts.map +1 -0
  34. package/dist/client-contract/timezone-names.js +600 -0
  35. package/dist/client-contract/types.d.ts +68 -0
  36. package/dist/client-contract/types.d.ts.map +1 -0
  37. package/dist/client-contract/types.js +1 -0
  38. package/dist/client-contract/validate.d.ts +6 -0
  39. package/dist/client-contract/validate.d.ts.map +1 -0
  40. package/dist/client-contract/validate.js +95 -0
  41. package/dist/deployments/codec.js +2 -2
  42. package/dist/deployments/index.d.ts +2 -1
  43. package/dist/deployments/index.d.ts.map +1 -1
  44. package/dist/deployments/index.js +1 -0
  45. package/dist/deployments/row-resolution.d.ts +4 -0
  46. package/dist/deployments/row-resolution.d.ts.map +1 -0
  47. package/dist/deployments/row-resolution.js +48 -0
  48. package/dist/deployments/types.d.ts +18 -0
  49. package/dist/deployments/types.d.ts.map +1 -1
  50. package/dist/deployments/validate.d.ts.map +1 -1
  51. package/dist/deployments/validate.js +6 -2
  52. package/dist/index.d.ts +5 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +3 -0
  55. package/dist/releases/codec.js +2 -2
  56. package/dist/values/codec.js +2 -2
  57. package/dist/visibility/errors.d.ts +8 -0
  58. package/dist/visibility/errors.d.ts.map +1 -0
  59. package/dist/visibility/errors.js +13 -0
  60. package/dist/visibility/index.d.ts +5 -0
  61. package/dist/visibility/index.d.ts.map +1 -0
  62. package/dist/visibility/index.js +3 -0
  63. package/dist/visibility/resolve.d.ts +30 -0
  64. package/dist/visibility/resolve.d.ts.map +1 -0
  65. package/dist/visibility/resolve.js +108 -0
  66. package/dist/visibility/types.d.ts +32 -0
  67. package/dist/visibility/types.d.ts.map +1 -0
  68. package/dist/visibility/types.js +1 -0
  69. package/dist/visibility/validate.d.ts +11 -0
  70. package/dist/visibility/validate.d.ts.map +1 -0
  71. package/dist/visibility/validate.js +78 -0
  72. 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.20.0",
3
+ "version": "0.22.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": "^1.8.0"
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.2"
35
+ "vitest": "^5.0.5"
36
36
  },
37
37
  "repository": {
38
38
  "type": "git",