@hypequery/deployment 0.9.6 → 0.10.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/authorized-contract.d.ts +9 -1
- package/dist/authorized-contract.d.ts.map +1 -1
- package/dist/authorized-contract.js +46 -13
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/semantic-data-plane.d.ts +20 -1
- package/dist/semantic-data-plane.d.ts.map +1 -1
- package/dist/semantic-data-plane.js +76 -5
- package/dist/semantic-operation-validation.d.ts +11 -2
- package/dist/semantic-operation-validation.d.ts.map +1 -1
- package/dist/semantic-operation-validation.js +42 -2
- package/dist/utils/visibility-projection.d.ts +23 -0
- package/dist/utils/visibility-projection.d.ts.map +1 -0
- package/dist/utils/visibility-projection.js +46 -0
- package/dist/utils/visible-measure-selection.d.ts +10 -0
- package/dist/utils/visible-measure-selection.d.ts.map +1 -0
- package/dist/utils/visible-measure-selection.js +21 -0
- package/package.json +4 -4
|
@@ -42,6 +42,14 @@ export interface AuthorizedDeploymentProjection {
|
|
|
42
42
|
*/
|
|
43
43
|
readonly queryable: readonly string[];
|
|
44
44
|
}
|
|
45
|
+
export interface AuthorizedDeploymentProjectionOptions {
|
|
46
|
+
/**
|
|
47
|
+
* A `hypequery-field-visibility` record for this principal (RFC 0019),
|
|
48
|
+
* validated here exactly as the data plane validates it. A dataset it hides
|
|
49
|
+
* is not published; fields it hides are not offered.
|
|
50
|
+
*/
|
|
51
|
+
readonly visibility?: unknown;
|
|
52
|
+
}
|
|
45
53
|
/**
|
|
46
54
|
* Projects the contract down to what `principal` may see.
|
|
47
55
|
*
|
|
@@ -68,5 +76,5 @@ export interface AuthorizedDeploymentProjection {
|
|
|
68
76
|
* projection that could emit an invalid one would move that failure to whatever
|
|
69
77
|
* read it next.
|
|
70
78
|
*/
|
|
71
|
-
export declare function projectAuthorizedDeploymentContract(contract: ProtocolDeploymentContract, principal: DeploymentDataPlanePrincipal | null): AuthorizedDeploymentProjection;
|
|
79
|
+
export declare function projectAuthorizedDeploymentContract(contract: ProtocolDeploymentContract, principal: DeploymentDataPlanePrincipal | null, options?: AuthorizedDeploymentProjectionOptions): AuthorizedDeploymentProjection;
|
|
72
80
|
//# sourceMappingURL=authorized-contract.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authorized-contract.d.ts","sourceRoot":"","sources":["../src/authorized-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,oBAAoB,EACpB,0BAA0B,EAE1B,sBAAsB,
|
|
1
|
+
{"version":3,"file":"authorized-contract.d.ts","sourceRoot":"","sources":["../src/authorized-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,oBAAoB,EACpB,0BAA0B,EAE1B,sBAAsB,EAEvB,MAAM,qBAAqB,CAAC;AAM7B,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAInE;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,oBAAoB,EAC5B,SAAS,EAAE,4BAA4B,GAAG,IAAI,GAC7C,OAAO,CAIT;AAED,mFAAmF;AACnF,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,sBAAsB,GAAG,SAAS,EAC5C,SAAS,EAAE,4BAA4B,GAAG,IAAI,GAC7C,OAAO,CAKT;AAwCD,MAAM,WAAW,8BAA8B;IAC7C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;IAC9C;;;;;;;;;OASG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,qCAAqC;IACpD;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,0BAA0B,EACpC,SAAS,EAAE,4BAA4B,GAAG,IAAI,EAC9C,OAAO,GAAE,qCAA0C,GAClD,8BAA8B,CA0EhC"}
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* catalog projection or schema compilation therefore cannot advertise something
|
|
13
13
|
* execution would refuse.
|
|
14
14
|
*/
|
|
15
|
-
import { validateProtocolDeploymentContract } from '@hypequery/protocol';
|
|
15
|
+
import { resolveProtocolVisibleDatasetFields, validateProtocolDeploymentContract, validateProtocolFieldVisibility, } from '@hypequery/protocol';
|
|
16
16
|
import { missing } from './utils/required-access.js';
|
|
17
|
+
import { concealDimension, narrowDatasetToVisible } from './utils/visibility-projection.js';
|
|
17
18
|
/**
|
|
18
19
|
* Whether a principal satisfies an access policy.
|
|
19
20
|
*
|
|
@@ -42,14 +43,15 @@ export function isDeploymentEndpointAuthorized(endpoint, principal) {
|
|
|
42
43
|
* publish the name and type of every dimension, filter, and measure on it.
|
|
43
44
|
* What a join actually reaches is the target's dimensions — `resolveDataset`
|
|
44
45
|
* offers each groupable one as `<relationship>.<name>` and each filterable one
|
|
45
|
-
* under the full operator set — so those stay and everything else goes.
|
|
46
|
+
* under the full operator set — so those stay and everything else goes. Under
|
|
47
|
+
* a visibility policy, only the dimensions the target's own entry shows stay.
|
|
46
48
|
*
|
|
47
49
|
* A dataset no queryable relationship reaches keeps nothing but its time field,
|
|
48
50
|
* which the planner reads for every grained query. It is retained at all only
|
|
49
51
|
* because a relationship names it and the contract would not validate without
|
|
50
52
|
* it.
|
|
51
53
|
*/
|
|
52
|
-
function narrowToJoin(dataset, reachable) {
|
|
54
|
+
function narrowToJoin(dataset, reachable, joinable) {
|
|
53
55
|
// `defaults` describes how to query the dataset directly, which is exactly
|
|
54
56
|
// what this principal may not do.
|
|
55
57
|
const { defaults: _defaults, endpoint: _endpoint, ...rest } = dataset;
|
|
@@ -57,7 +59,9 @@ function narrowToJoin(dataset, reachable) {
|
|
|
57
59
|
return {
|
|
58
60
|
...rest,
|
|
59
61
|
dimensions: reachable
|
|
60
|
-
? dataset.dimensions
|
|
62
|
+
? dataset.dimensions.map(dimension => (joinable === undefined || joinable.has(String(dimension.name))
|
|
63
|
+
? dimension
|
|
64
|
+
: concealDimension(dimension)))
|
|
61
65
|
: dataset.dimensions.filter(dimension => String(dimension.name) === timeField),
|
|
62
66
|
measures: [],
|
|
63
67
|
filters: [],
|
|
@@ -89,11 +93,27 @@ function narrowToJoin(dataset, reachable) {
|
|
|
89
93
|
* projection that could emit an invalid one would move that failure to whatever
|
|
90
94
|
* read it next.
|
|
91
95
|
*/
|
|
92
|
-
export function projectAuthorizedDeploymentContract(contract, principal) {
|
|
96
|
+
export function projectAuthorizedDeploymentContract(contract, principal, options = {}) {
|
|
93
97
|
const byName = new Map(contract.datasets.map(dataset => [String(dataset.name), dataset]));
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
const visibility = options.visibility === undefined
|
|
99
|
+
? undefined
|
|
100
|
+
: validateProtocolFieldVisibility(options.visibility);
|
|
101
|
+
const visibleIn = (dataset) => (visibility === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: resolveProtocolVisibleDatasetFields(dataset, byName, visibility));
|
|
104
|
+
// Each published dataset as the principal sees it. Narrowing comes first,
|
|
105
|
+
// because a relationship it drops must not keep its target in the contract.
|
|
106
|
+
const projected = new Map();
|
|
107
|
+
for (const dataset of contract.datasets) {
|
|
108
|
+
if (!isDeploymentEndpointAuthorized(dataset.endpoint, principal))
|
|
109
|
+
continue;
|
|
110
|
+
const visible = visibleIn(dataset);
|
|
111
|
+
if (visible === null)
|
|
112
|
+
continue;
|
|
113
|
+
projected.set(String(dataset.name), visible ? narrowDatasetToVisible(dataset, visible) : dataset);
|
|
114
|
+
}
|
|
115
|
+
const published = new Set(projected.keys());
|
|
116
|
+
const relationshipsOf = (name) => (projected.get(name)?.relationships ?? byName.get(name).relationships);
|
|
97
117
|
const required = new Set();
|
|
98
118
|
const require = (name) => {
|
|
99
119
|
if (required.has(name) || !byName.has(name))
|
|
@@ -101,7 +121,7 @@ export function projectAuthorizedDeploymentContract(contract, principal) {
|
|
|
101
121
|
required.add(name);
|
|
102
122
|
// Transitive: a retained target has relationships of its own, and every
|
|
103
123
|
// target named in the contract has to still be there for it to validate.
|
|
104
|
-
for (const relationship of
|
|
124
|
+
for (const relationship of relationshipsOf(name)) {
|
|
105
125
|
require(String(relationship.target));
|
|
106
126
|
}
|
|
107
127
|
};
|
|
@@ -113,16 +133,29 @@ export function projectAuthorizedDeploymentContract(contract, principal) {
|
|
|
113
133
|
// validates passes none of it on.
|
|
114
134
|
const reachable = new Set();
|
|
115
135
|
for (const name of published) {
|
|
116
|
-
for (const relationship of
|
|
136
|
+
for (const relationship of relationshipsOf(name)) {
|
|
117
137
|
if (relationship.queryable)
|
|
118
138
|
reachable.add(String(relationship.target));
|
|
119
139
|
}
|
|
120
140
|
}
|
|
141
|
+
// What a join may reach on a target: the dimensions its own policy entry
|
|
142
|
+
// shows. A relationship to a target without one was already dropped above.
|
|
143
|
+
const joinable = (dataset) => {
|
|
144
|
+
if (visibility === undefined)
|
|
145
|
+
return undefined;
|
|
146
|
+
const entry = Object.hasOwn(visibility.datasets, String(dataset.name))
|
|
147
|
+
? visibility.datasets[String(dataset.name)]
|
|
148
|
+
: undefined;
|
|
149
|
+
if (entry === undefined)
|
|
150
|
+
return new Set();
|
|
151
|
+
return entry.dimensions === '*'
|
|
152
|
+
? new Set(dataset.dimensions.map(dimension => String(dimension.name)))
|
|
153
|
+
: new Set(entry.dimensions);
|
|
154
|
+
};
|
|
121
155
|
const datasets = contract.datasets
|
|
122
156
|
.filter(dataset => required.has(String(dataset.name)))
|
|
123
|
-
.map(dataset => (
|
|
124
|
-
|
|
125
|
-
: narrowToJoin(dataset, reachable.has(String(dataset.name)))));
|
|
157
|
+
.map(dataset => (projected.get(String(dataset.name))
|
|
158
|
+
?? narrowToJoin(dataset, reachable.has(String(dataset.name)), joinable(dataset))));
|
|
126
159
|
return Object.freeze({
|
|
127
160
|
contract: validateProtocolDeploymentContract({ ...contract, datasets }),
|
|
128
161
|
queryable: Object.freeze(datasets
|
package/dist/index.d.ts
CHANGED
|
@@ -18,8 +18,9 @@ export type { DeploymentIntakeLimits } from './limits.js';
|
|
|
18
18
|
export type { DeploymentAuthenticationInput, DeploymentAuthenticator, DeploymentAuthorizationInput, DeploymentAuthorizer, DeploymentIntake, DeploymentIntakeOptions, DeploymentIntakeRequest, DeploymentIntakeResponse, DeploymentSubmissionResponse, DeploymentSubmissionStore, VerifiedDeploymentSubmission, } from './types.js';
|
|
19
19
|
export type { DeploymentDataPlanePrincipal } from './principal.js';
|
|
20
20
|
export { createDeploymentSemanticDataPlane, DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure, } from './semantic-data-plane.js';
|
|
21
|
-
export type { DeploymentSemanticAuthenticationInput, DeploymentSemanticBudget, DeploymentSemanticDataPlane, DeploymentSemanticDataPlaneOptions, DeploymentSemanticExecutionInput, DeploymentSemanticInvocationRequest, DeploymentSemanticTenantInput, } from './semantic-data-plane.js';
|
|
22
|
-
export { validateSemanticOperation } from './semantic-operation-validation.js';
|
|
21
|
+
export type { DeploymentSemanticAuthenticationInput, DeploymentSemanticBudget, DeploymentSemanticDataPlane, DeploymentSemanticDataPlaneOptions, DeploymentSemanticExecutionInput, DeploymentSemanticInvocationRequest, DeploymentSemanticContextInput, DeploymentSemanticTenantInput, } from './semantic-data-plane.js';
|
|
22
|
+
export { validateSemanticConstraints, validateSemanticOperation, } from './semantic-operation-validation.js';
|
|
23
23
|
export type { SemanticOperationLimits, SemanticOperationViolation, } from './semantic-operation-validation.js';
|
|
24
24
|
export { isDeploymentEndpointAuthorized, projectAuthorizedDeploymentContract, satisfiesDeploymentAccess, } from './authorized-contract.js';
|
|
25
|
+
export type { AuthorizedDeploymentProjection, AuthorizedDeploymentProjectionOptions, } from './authorized-contract.js';
|
|
25
26
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4CAA4C,EAC5C,yBAAyB,EACzB,kCAAkC,GACnC,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,6BAA6B,EAC7B,+BAA+B,EAC/B,gCAAgC,EAChC,4BAA4B,EAC5B,2BAA2B,EAC3B,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,EAC1B,uBAAuB,EACvB,6CAA6C,GAC9C,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACL,yCAAyC,EACzC,8BAA8B,GAC/B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,kCAAkC,EAClC,mCAAmC,EACnC,0CAA0C,EAC1C,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,wCAAwC,EACxC,uCAAuC,GACxC,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,kCAAkC,EAClC,iCAAiC,GAClC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,4BAA4B,GAC7B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,sBAAsB,EACtB,4BAA4B,EAC5B,wCAAwC,EACxC,gCAAgC,EAChC,+BAA+B,EAC/B,6BAA6B,EAC7B,6BAA6B,EAC7B,8BAA8B,GAC/B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uCAAuC,EACvC,mCAAmC,GACpC,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAC9B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EACV,6BAA6B,EAC7B,uBAAuB,EACvB,4BAA4B,EAC5B,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,EACxB,4BAA4B,EAC5B,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAEnE,OAAO,EACL,iCAAiC,EACjC,iCAAiC,EACjC,mCAAmC,GACpC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,qCAAqC,EACrC,wBAAwB,EACxB,2BAA2B,EAC3B,kCAAkC,EAClC,gCAAgC,EAChC,mCAAmC,EACnC,6BAA6B,GAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4CAA4C,EAC5C,yBAAyB,EACzB,kCAAkC,GACnC,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,6BAA6B,EAC7B,+BAA+B,EAC/B,gCAAgC,EAChC,4BAA4B,EAC5B,2BAA2B,EAC3B,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,EAC1B,uBAAuB,EACvB,6CAA6C,GAC9C,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EACL,yCAAyC,EACzC,8BAA8B,GAC/B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,kCAAkC,EAClC,mCAAmC,EACnC,0CAA0C,EAC1C,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,wCAAwC,EACxC,uCAAuC,GACxC,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,kCAAkC,EAClC,iCAAiC,GAClC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,4BAA4B,GAC7B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,sBAAsB,EACtB,4BAA4B,EAC5B,wCAAwC,EACxC,gCAAgC,EAChC,+BAA+B,EAC/B,6BAA6B,EAC7B,6BAA6B,EAC7B,8BAA8B,GAC/B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uCAAuC,EACvC,mCAAmC,GACpC,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAC9E,OAAO,EACL,gCAAgC,EAChC,6BAA6B,GAC9B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC1D,YAAY,EACV,6BAA6B,EAC7B,uBAAuB,EACvB,4BAA4B,EAC5B,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,uBAAuB,EACvB,wBAAwB,EACxB,4BAA4B,EAC5B,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAEnE,OAAO,EACL,iCAAiC,EACjC,iCAAiC,EACjC,mCAAmC,GACpC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,qCAAqC,EACrC,wBAAwB,EACxB,2BAA2B,EAC3B,kCAAkC,EAClC,gCAAgC,EAChC,mCAAmC,EACnC,8BAA8B,EAC9B,6BAA6B,GAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EACV,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,oCAAoC,CAAC;AAI5C,OAAO,EACL,8BAA8B,EAC9B,mCAAmC,EACnC,yBAAyB,GAC1B,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,8BAA8B,EAC9B,qCAAqC,GACtC,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export { createDeploymentControlPlane, } from './control-plane.js';
|
|
|
8
8
|
export { DEFAULT_DEPLOYMENT_CONTROL_PLANE_LIMITS, resolveDeploymentControlPlaneLimits, } from './control-plane-limits.js';
|
|
9
9
|
export { DEFAULT_DEPLOYMENT_INTAKE_LIMITS, resolveDeploymentIntakeLimits, } from './limits.js';
|
|
10
10
|
export { createDeploymentSemanticDataPlane, DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure, } from './semantic-data-plane.js';
|
|
11
|
-
export { validateSemanticOperation } from './semantic-operation-validation.js';
|
|
11
|
+
export { validateSemanticConstraints, validateSemanticOperation, } from './semantic-operation-validation.js';
|
|
12
12
|
// What one principal may see of a contract, for a gateway listing targets
|
|
13
13
|
// before any call exists (decision 0002, CLOUD-03).
|
|
14
14
|
export { isDeploymentEndpointAuthorized, projectAuthorizedDeploymentContract, satisfiesDeploymentAccess, } from './authorized-contract.js';
|
|
@@ -25,11 +25,14 @@ export interface DeploymentSemanticAuthenticationInput {
|
|
|
25
25
|
readonly invocation: ProtocolSemanticInvocation;
|
|
26
26
|
readonly dataset: ProtocolDeploymentDataset;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/** What a provider callback knows about the call it resolves context for. */
|
|
29
|
+
export interface DeploymentSemanticContextInput {
|
|
29
30
|
readonly principal: DeploymentDataPlanePrincipal | null;
|
|
30
31
|
readonly invocation: ProtocolSemanticInvocation;
|
|
31
32
|
readonly dataset: ProtocolDeploymentDataset;
|
|
32
33
|
}
|
|
34
|
+
/** The input `resolveTenant` receives; the same as every context resolver's. */
|
|
35
|
+
export type DeploymentSemanticTenantInput = DeploymentSemanticContextInput;
|
|
33
36
|
export interface DeploymentSemanticExecutionInput {
|
|
34
37
|
readonly deployment: ProtocolDeploymentContract;
|
|
35
38
|
readonly dataset: ProtocolDeploymentDataset;
|
|
@@ -56,6 +59,22 @@ export interface DeploymentSemanticDataPlaneOptions {
|
|
|
56
59
|
readonly activationRevision: string;
|
|
57
60
|
readonly authenticate?: (input: DeploymentSemanticAuthenticationInput) => Promise<DeploymentDataPlanePrincipal | null>;
|
|
58
61
|
readonly resolveTenant?: (input: DeploymentSemanticTenantInput) => Promise<unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* The datasets and fields this principal may see (RFC 0019), as an
|
|
64
|
+
* unvalidated `hypequery-field-visibility` record. Resolve to `undefined`
|
|
65
|
+
* for no restriction. A hidden dataset or field is refused exactly like an
|
|
66
|
+
* unknown one. Like the tenant, never taken from the invocation.
|
|
67
|
+
*/
|
|
68
|
+
readonly resolveFieldVisibility?: (input: DeploymentSemanticContextInput) => Promise<unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Filters the provider enforces on this call, as unvalidated comparison
|
|
71
|
+
* expressions on the dataset's declared filters (RFC 0019), at most 32.
|
|
72
|
+
* They are ANDed onto the caller's filters and checked against the contract
|
|
73
|
+
* rather than the caller's visibility. They count toward the dataset's
|
|
74
|
+
* filter ceiling, since executors apply them as filters; the caller is told
|
|
75
|
+
* how many remain. A constraint the dataset can't enforce refuses the call.
|
|
76
|
+
*/
|
|
77
|
+
readonly resolveConstraints?: (input: DeploymentSemanticContextInput) => Promise<unknown>;
|
|
59
78
|
readonly execute: (input: DeploymentSemanticExecutionInput) => Promise<unknown>;
|
|
60
79
|
/** Server-side ceilings, applied on top of contract and caller limits. */
|
|
61
80
|
readonly limits?: Partial<SemanticOperationLimits> & {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-data-plane.d.ts","sourceRoot":"","sources":["../src/semantic-data-plane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,0BAA0B,EAC1B,yBAAyB,
|
|
1
|
+
{"version":3,"file":"semantic-data-plane.d.ts","sourceRoot":"","sources":["../src/semantic-data-plane.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
* Execution itself is injected. This module decides whether a call is allowed
|
|
11
11
|
* and what it is allowed to ask for.
|
|
12
12
|
*/
|
|
13
|
-
import { ProtocolSemanticInvocationError, validateProtocolDeploymentContract, validateProtocolSemanticInvocation, validateProtocolSemanticInvocationResult, } from '@hypequery/protocol';
|
|
14
|
-
import { validateSemanticOperation, } from './semantic-operation-validation.js';
|
|
13
|
+
import { ProtocolSemanticInvocationError, resolveProtocolVisibleDatasetFields, validateProtocolDeploymentContract, validateProtocolExpression, validateProtocolFieldVisibility, validateProtocolSemanticInvocation, validateProtocolSemanticInvocationResult, } from '@hypequery/protocol';
|
|
14
|
+
import { validateSemanticConstraints, validateSemanticOperation, } from './semantic-operation-validation.js';
|
|
15
15
|
import { DeploymentSemanticInvocationError, fail, throwIfAborted } from './semantic-invocation-errors.js';
|
|
16
16
|
import { missing } from './utils/required-access.js';
|
|
17
17
|
import { definedLimits, lowest, tighten } from './utils/semantic-budget-limits.js';
|
|
18
18
|
import { claimedFailure } from './utils/semantic-executor-failure.js';
|
|
19
|
+
import { withVisibleDefaultMeasures } from './utils/visible-measure-selection.js';
|
|
19
20
|
export { DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure } from './semantic-invocation-errors.js';
|
|
20
21
|
const DEFAULT_LIMITS = Object.freeze({
|
|
21
22
|
maxRows: 10_000,
|
|
@@ -25,6 +26,8 @@ const DEFAULT_LIMITS = Object.freeze({
|
|
|
25
26
|
maxFilters: 100,
|
|
26
27
|
});
|
|
27
28
|
const REVISION_PATTERN = /^[0-9a-f]{64}$/;
|
|
29
|
+
/** Provider constraints are policy, not queries; a handful per call is plenty. */
|
|
30
|
+
const MAX_CONSTRAINTS = 32;
|
|
28
31
|
export function createDeploymentSemanticDataPlane(options) {
|
|
29
32
|
let deployment;
|
|
30
33
|
try {
|
|
@@ -104,6 +107,37 @@ export function createDeploymentSemanticDataPlane(options) {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
throwIfAborted(request.signal);
|
|
110
|
+
// Before access and tenant checks: a dataset the principal can't see must
|
|
111
|
+
// fail exactly as one the contract doesn't have, not as a forbidden or
|
|
112
|
+
// tenant-required one, and no tenant is resolved for it.
|
|
113
|
+
let visible;
|
|
114
|
+
if (options.resolveFieldVisibility) {
|
|
115
|
+
let policy;
|
|
116
|
+
try {
|
|
117
|
+
policy = await options.resolveFieldVisibility({ principal, invocation, dataset });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
fail('forbidden', 'HQ_SEMANTIC_FORBIDDEN', 'Visibility resolution failed.', { cause: error });
|
|
121
|
+
}
|
|
122
|
+
if (policy !== undefined) {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = validateProtocolFieldVisibility(policy);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
fail('configuration-invalid', 'HQ_SEMANTIC_CONFIGURATION', 'The visibility policy is invalid.', { cause: error });
|
|
129
|
+
}
|
|
130
|
+
const resolved = resolveProtocolVisibleDatasetFields(dataset, datasets, parsed);
|
|
131
|
+
if (resolved === null) {
|
|
132
|
+
// Indistinguishable from a dataset the contract never had.
|
|
133
|
+
fail('not-found', 'HQ_SEMANTIC_DATASET_NOT_FOUND', 'The dataset was not found.', {
|
|
134
|
+
path: '$.operation.dataset',
|
|
135
|
+
relist: true,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
visible = resolved;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
107
141
|
if (endpoint.access.kind === 'authenticated') {
|
|
108
142
|
if (!principal) {
|
|
109
143
|
fail('unauthenticated', 'HQ_SEMANTIC_UNAUTHENTICATED', 'Authentication is required.');
|
|
@@ -135,6 +169,34 @@ export function createDeploymentSemanticDataPlane(options) {
|
|
|
135
169
|
}
|
|
136
170
|
}
|
|
137
171
|
throwIfAborted(request.signal);
|
|
172
|
+
let constraints = [];
|
|
173
|
+
if (options.resolveConstraints) {
|
|
174
|
+
let raw;
|
|
175
|
+
try {
|
|
176
|
+
raw = await options.resolveConstraints({ principal, invocation, dataset });
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
fail('forbidden', 'HQ_SEMANTIC_FORBIDDEN', 'Constraint resolution failed.', { cause: error });
|
|
180
|
+
}
|
|
181
|
+
if (raw !== undefined) {
|
|
182
|
+
try {
|
|
183
|
+
if (!Array.isArray(raw) || raw.length > MAX_CONSTRAINTS) {
|
|
184
|
+
throw new TypeError(`Constraints must be an array of at most ${MAX_CONSTRAINTS}.`);
|
|
185
|
+
}
|
|
186
|
+
constraints = Object.freeze(raw.map(expression => validateProtocolExpression(expression)));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
fail('configuration-invalid', 'HQ_SEMANTIC_CONFIGURATION', 'The enforced constraints are invalid.', { cause: error });
|
|
190
|
+
}
|
|
191
|
+
if (validateSemanticConstraints(constraints, dataset, datasets).length > 0
|
|
192
|
+
|| constraints.length > tighten(dataset.limits?.maxFilters, configured.maxFilters)) {
|
|
193
|
+
// Not caller-correctable, and the constraint is the provider's
|
|
194
|
+
// policy: say the target is forbidden without describing it.
|
|
195
|
+
fail('forbidden', 'HQ_SEMANTIC_CONSTRAINT_UNENFORCEABLE', 'The principal lacks required access.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
throwIfAborted(request.signal);
|
|
138
200
|
const budget = Object.freeze({
|
|
139
201
|
maxRows: lowest(invocation.budget?.maxRows, endpoint.maxLimit, dataset.limits?.maxResultSize, configured.maxRows) ?? configured.maxRows,
|
|
140
202
|
...(lowest(invocation.budget?.deadlineMs, options.limits?.deadlineMs) === undefined
|
|
@@ -156,19 +218,28 @@ export function createDeploymentSemanticDataPlane(options) {
|
|
|
156
218
|
maxOffset: configured.maxOffset,
|
|
157
219
|
maxDimensions: tighten(dataset.limits?.maxDimensions, configured.maxDimensions),
|
|
158
220
|
maxMeasures: tighten(dataset.limits?.maxMeasures, configured.maxMeasures),
|
|
159
|
-
maxFilters: tighten(dataset.limits?.maxFilters, configured.maxFilters),
|
|
160
|
-
});
|
|
221
|
+
maxFilters: tighten(dataset.limits?.maxFilters, configured.maxFilters) - constraints.length,
|
|
222
|
+
}, visible);
|
|
161
223
|
if (violations.length > 0) {
|
|
162
224
|
const [first] = violations;
|
|
163
225
|
fail('input-invalid', 'HQ_SEMANTIC_INPUT_INVALID', first.message, { path: first.path });
|
|
164
226
|
}
|
|
165
227
|
throwIfAborted(request.signal);
|
|
228
|
+
const selected = withVisibleDefaultMeasures(operation, dataset, visible);
|
|
229
|
+
const maxMeasures = tighten(dataset.limits?.maxMeasures, configured.maxMeasures);
|
|
230
|
+
if (selected !== operation && selected.kind === 'dataset' && (selected.measures?.length ?? 0) > maxMeasures) {
|
|
231
|
+
// The caller asked for the default selection; it narrowed to more
|
|
232
|
+
// measures than one query may select, so the caller has to choose.
|
|
233
|
+
fail('input-invalid', 'HQ_SEMANTIC_INPUT_INVALID', `Select at most ${maxMeasures} measures explicitly.`, { path: '$.operation.measures' });
|
|
234
|
+
}
|
|
166
235
|
let output;
|
|
167
236
|
try {
|
|
168
237
|
output = await options.execute({
|
|
169
238
|
deployment,
|
|
170
239
|
dataset,
|
|
171
|
-
operation
|
|
240
|
+
operation: constraints.length === 0
|
|
241
|
+
? selected
|
|
242
|
+
: Object.freeze({ ...selected, filters: [...(selected.filters ?? []), ...constraints] }),
|
|
172
243
|
principal,
|
|
173
244
|
tenant,
|
|
174
245
|
budget,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* loading the authoring package. A gateway must be able to reject a bad
|
|
7
7
|
* invocation without any of the deploying application's code.
|
|
8
8
|
*/
|
|
9
|
-
import type { ProtocolDeploymentDataset, ProtocolSemanticQuery } from '@hypequery/protocol';
|
|
9
|
+
import type { ProtocolDeploymentDataset, ProtocolExpression, ProtocolSemanticQuery, ProtocolVisibleDatasetFields } from '@hypequery/protocol';
|
|
10
10
|
export interface SemanticOperationViolation {
|
|
11
11
|
readonly message: string;
|
|
12
12
|
readonly path: string;
|
|
@@ -25,5 +25,14 @@ export interface SemanticOperationLimits {
|
|
|
25
25
|
* Returns every violation rather than the first, so a caller correcting an
|
|
26
26
|
* agent-authored query sees the whole problem in one round trip.
|
|
27
27
|
*/
|
|
28
|
-
export declare function validateSemanticOperation(operation: ProtocolSemanticQuery, dataset: ProtocolDeploymentDataset, datasets: ReadonlyMap<string, ProtocolDeploymentDataset>, limits: SemanticOperationLimits
|
|
28
|
+
export declare function validateSemanticOperation(operation: ProtocolSemanticQuery, dataset: ProtocolDeploymentDataset, datasets: ReadonlyMap<string, ProtocolDeploymentDataset>, limits: SemanticOperationLimits,
|
|
29
|
+
/** What the principal may see; omitted means everything the contract publishes. */
|
|
30
|
+
visible?: ProtocolVisibleDatasetFields): readonly SemanticOperationViolation[];
|
|
31
|
+
/**
|
|
32
|
+
* Validates filters the provider enforces on every call, such as an embedded
|
|
33
|
+
* end user's account (RFC 0019). They are checked against what the contract
|
|
34
|
+
* publishes, not against the caller's visibility: a provider may constrain on
|
|
35
|
+
* a field the caller can't see.
|
|
36
|
+
*/
|
|
37
|
+
export declare function validateSemanticConstraints(constraints: readonly ProtocolExpression[], dataset: ProtocolDeploymentDataset, datasets: ReadonlyMap<string, ProtocolDeploymentDataset>): readonly SemanticOperationViolation[];
|
|
29
38
|
//# sourceMappingURL=semantic-operation-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-operation-validation.d.ts","sourceRoot":"","sources":["../src/semantic-operation-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,yBAAyB,
|
|
1
|
+
{"version":3,"file":"semantic-operation-validation.d.ts","sourceRoot":"","sources":["../src/semantic-operation-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,yBAAyB,EACzB,kBAAkB,EAClB,qBAAqB,EAErB,4BAA4B,EAC7B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AA+FD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,qBAAqB,EAChC,OAAO,EAAE,yBAAyB,EAClC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,yBAAyB,CAAC,EACxD,MAAM,EAAE,uBAAuB;AAC/B,mFAAmF;AACnF,OAAO,CAAC,EAAE,4BAA4B,GACrC,SAAS,0BAA0B,EAAE,CA2FvC;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,SAAS,kBAAkB,EAAE,EAC1C,OAAO,EAAE,yBAAyB,EAClC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,yBAAyB,CAAC,GACvD,SAAS,0BAA0B,EAAE,CAgBvC"}
|
|
@@ -52,6 +52,20 @@ function resolveDataset(dataset, datasets) {
|
|
|
52
52
|
grains: new Set(dataset.timeField === undefined ? [] : ALL_GRAINS),
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Narrows what a dataset exposes to what a visibility policy lets this
|
|
57
|
+
* principal see (RFC 0019). Hidden fields drop out of the same sets unknown
|
|
58
|
+
* ones are missing from, so a refusal reads exactly like an unknown field and
|
|
59
|
+
* does not confirm that the hidden one exists.
|
|
60
|
+
*/
|
|
61
|
+
function narrowResolvedToVisible(allowed, visible) {
|
|
62
|
+
return {
|
|
63
|
+
groupable: new Set([...allowed.groupable].filter(name => visible.dimensions.has(name))),
|
|
64
|
+
filters: new Map([...allowed.filters].filter(([name]) => (visible.filters.has(name) || (name.includes('.') && visible.dimensions.has(name))))),
|
|
65
|
+
measures: new Set([...allowed.measures].filter(name => visible.measures.has(name))),
|
|
66
|
+
grains: visible.timeField ? allowed.grains : new Set(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
55
69
|
/**
|
|
56
70
|
* The field a filter expression addresses, or null when it is not a plain
|
|
57
71
|
* comparison.
|
|
@@ -76,7 +90,9 @@ function comparisonField(expression) {
|
|
|
76
90
|
* Returns every violation rather than the first, so a caller correcting an
|
|
77
91
|
* agent-authored query sees the whole problem in one round trip.
|
|
78
92
|
*/
|
|
79
|
-
export function validateSemanticOperation(operation, dataset, datasets, limits
|
|
93
|
+
export function validateSemanticOperation(operation, dataset, datasets, limits,
|
|
94
|
+
/** What the principal may see; omitted means everything the contract publishes. */
|
|
95
|
+
visible) {
|
|
80
96
|
const violations = [];
|
|
81
97
|
const fail = (message, path) => violations.push({ message, path });
|
|
82
98
|
// A deployment carries no metric, so a metric operation addresses nothing the
|
|
@@ -85,7 +101,8 @@ export function validateSemanticOperation(operation, dataset, datasets, limits)
|
|
|
85
101
|
fail(`Unknown metric "${String(operation.metric)}".`, '$.operation.metric');
|
|
86
102
|
return violations;
|
|
87
103
|
}
|
|
88
|
-
const
|
|
104
|
+
const published = resolveDataset(dataset, datasets);
|
|
105
|
+
const allowed = visible === undefined ? published : narrowResolvedToVisible(published, visible);
|
|
89
106
|
const dimensions = operation.dimensions ?? [];
|
|
90
107
|
if (dimensions.length > limits.maxDimensions) {
|
|
91
108
|
fail(`At most ${limits.maxDimensions} dimensions may be selected.`, '$.operation.dimensions');
|
|
@@ -155,3 +172,26 @@ export function validateSemanticOperation(operation, dataset, datasets, limits)
|
|
|
155
172
|
}
|
|
156
173
|
return violations;
|
|
157
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Validates filters the provider enforces on every call, such as an embedded
|
|
177
|
+
* end user's account (RFC 0019). They are checked against what the contract
|
|
178
|
+
* publishes, not against the caller's visibility: a provider may constrain on
|
|
179
|
+
* a field the caller can't see.
|
|
180
|
+
*/
|
|
181
|
+
export function validateSemanticConstraints(constraints, dataset, datasets) {
|
|
182
|
+
const violations = [];
|
|
183
|
+
const allowed = resolveDataset(dataset, datasets);
|
|
184
|
+
constraints.forEach((expression, index) => {
|
|
185
|
+
const path = `$.constraints[${index}]`;
|
|
186
|
+
const comparison = comparisonField(expression);
|
|
187
|
+
if (comparison === null) {
|
|
188
|
+
violations.push({ message: 'A constraint must be a field/operator/value comparison.', path });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const operators = allowed.filters.get(comparison.field);
|
|
192
|
+
if (operators === undefined || !operators.has(comparison.operator)) {
|
|
193
|
+
violations.push({ message: 'The constraint cannot be enforced on this dataset.', path });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return violations;
|
|
197
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ProtocolDeploymentDataset, ProtocolVisibleDatasetFields } from '@hypequery/protocol';
|
|
2
|
+
type Dimension = ProtocolDeploymentDataset['dimensions'][number];
|
|
3
|
+
/**
|
|
4
|
+
* A dimension the principal may neither group nor filter by, with its label,
|
|
5
|
+
* description and semantic metadata removed (RFC 0019).
|
|
6
|
+
*
|
|
7
|
+
* It stays in the contract because measures, relationships and the tenant key
|
|
8
|
+
* may reference it. The agent-safe catalog omits a dimension that is neither
|
|
9
|
+
* groupable nor filterable; a consumer serving the projected contract itself
|
|
10
|
+
* still sees its name, type and source, so that contract is not a
|
|
11
|
+
* disclosure-safe artifact on its own.
|
|
12
|
+
*/
|
|
13
|
+
export declare function concealDimension(dimension: Dimension): Dimension;
|
|
14
|
+
/**
|
|
15
|
+
* A published dataset narrowed to what a visibility policy shows, from the
|
|
16
|
+
* same resolution the data plane enforces. Hidden measures and filters go;
|
|
17
|
+
* hidden dimensions are concealed; a relationship whose keys or target are
|
|
18
|
+
* hidden goes with every field it would contribute; a hidden time field and
|
|
19
|
+
* the defaults that depend on it go. The result is still a valid dataset.
|
|
20
|
+
*/
|
|
21
|
+
export declare function narrowDatasetToVisible(dataset: ProtocolDeploymentDataset, visible: ProtocolVisibleDatasetFields): ProtocolDeploymentDataset;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=visibility-projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visibility-projection.d.ts","sourceRoot":"","sources":["../../src/utils/visibility-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,4BAA4B,EAC7B,MAAM,qBAAqB,CAAC;AAE7B,KAAK,SAAS,GAAG,yBAAyB,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,CAQhE;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,yBAAyB,EAClC,OAAO,EAAE,4BAA4B,GACpC,yBAAyB,CAuB3B"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dimension the principal may neither group nor filter by, with its label,
|
|
3
|
+
* description and semantic metadata removed (RFC 0019).
|
|
4
|
+
*
|
|
5
|
+
* It stays in the contract because measures, relationships and the tenant key
|
|
6
|
+
* may reference it. The agent-safe catalog omits a dimension that is neither
|
|
7
|
+
* groupable nor filterable; a consumer serving the projected contract itself
|
|
8
|
+
* still sees its name, type and source, so that contract is not a
|
|
9
|
+
* disclosure-safe artifact on its own.
|
|
10
|
+
*/
|
|
11
|
+
export function concealDimension(dimension) {
|
|
12
|
+
return {
|
|
13
|
+
name: dimension.name,
|
|
14
|
+
type: dimension.type,
|
|
15
|
+
source: dimension.source,
|
|
16
|
+
filterable: false,
|
|
17
|
+
groupable: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A published dataset narrowed to what a visibility policy shows, from the
|
|
22
|
+
* same resolution the data plane enforces. Hidden measures and filters go;
|
|
23
|
+
* hidden dimensions are concealed; a relationship whose keys or target are
|
|
24
|
+
* hidden goes with every field it would contribute; a hidden time field and
|
|
25
|
+
* the defaults that depend on it go. The result is still a valid dataset.
|
|
26
|
+
*/
|
|
27
|
+
export function narrowDatasetToVisible(dataset, visible) {
|
|
28
|
+
const { defaults, timeField, ...rest } = dataset;
|
|
29
|
+
const defaultDimensions = defaults?.dimensions?.filter(name => visible.dimensions.has(String(name)));
|
|
30
|
+
const narrowedDefaults = {
|
|
31
|
+
...(defaultDimensions === undefined || defaultDimensions.length === 0
|
|
32
|
+
? {}
|
|
33
|
+
: { dimensions: defaultDimensions }),
|
|
34
|
+
...(defaults?.timeGrain === undefined || !visible.timeField ? {} : { timeGrain: defaults.timeGrain }),
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
...rest,
|
|
38
|
+
...(visible.timeField && timeField !== undefined ? { timeField } : {}),
|
|
39
|
+
// An empty `defaults` object is not a valid contract value.
|
|
40
|
+
...(Object.keys(narrowedDefaults).length === 0 ? {} : { defaults: narrowedDefaults }),
|
|
41
|
+
dimensions: dataset.dimensions.map(dimension => (visible.dimensions.has(String(dimension.name)) ? dimension : concealDimension(dimension))),
|
|
42
|
+
measures: dataset.measures.filter(measure => visible.measures.has(String(measure.name))),
|
|
43
|
+
filters: dataset.filters.filter(filter => visible.filters.has(String(filter.name))),
|
|
44
|
+
relationships: dataset.relationships.filter(relationship => (visible.relationships.has(String(relationship.name)))),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProtocolDeploymentDataset, ProtocolSemanticQuery, ProtocolVisibleDatasetFields } from '@hypequery/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* A dataset operation that omits `measures` asks the executor for every base
|
|
4
|
+
* measure of the full contract. When the visibility policy hides any of them,
|
|
5
|
+
* spell out the visible base measures instead, so the selection the executor
|
|
6
|
+
* runs is one the visibility check allows. When none is hidden the operation
|
|
7
|
+
* is returned unchanged and keeps the executor's default selection.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withVisibleDefaultMeasures(operation: ProtocolSemanticQuery, dataset: ProtocolDeploymentDataset, visible: ProtocolVisibleDatasetFields | undefined): ProtocolSemanticQuery;
|
|
10
|
+
//# sourceMappingURL=visible-measure-selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visible-measure-selection.d.ts","sourceRoot":"","sources":["../../src/utils/visible-measure-selection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,qBAAqB,EACrB,4BAA4B,EAC7B,MAAM,qBAAqB,CAAC;AAQ7B;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,qBAAqB,EAChC,OAAO,EAAE,yBAAyB,EAClC,OAAO,EAAE,4BAA4B,GAAG,SAAS,GAChD,qBAAqB,CAMvB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { joinProtocolQualifiedIdentifier } from '@hypequery/protocol';
|
|
2
|
+
/** A measure computed from no other measure: neither derived (`uses`) nor a time measure (`measure`). */
|
|
3
|
+
function isBaseMeasure(measure) {
|
|
4
|
+
return !('uses' in measure) && !('measure' in measure);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A dataset operation that omits `measures` asks the executor for every base
|
|
8
|
+
* measure of the full contract. When the visibility policy hides any of them,
|
|
9
|
+
* spell out the visible base measures instead, so the selection the executor
|
|
10
|
+
* runs is one the visibility check allows. When none is hidden the operation
|
|
11
|
+
* is returned unchanged and keeps the executor's default selection.
|
|
12
|
+
*/
|
|
13
|
+
export function withVisibleDefaultMeasures(operation, dataset, visible) {
|
|
14
|
+
if (visible === undefined || operation.kind !== 'dataset' || operation.measures !== undefined)
|
|
15
|
+
return operation;
|
|
16
|
+
const base = dataset.measures.filter(isBaseMeasure);
|
|
17
|
+
const shown = base.filter(measure => visible.measures.has(String(measure.name)));
|
|
18
|
+
if (shown.length === base.length)
|
|
19
|
+
return operation;
|
|
20
|
+
return Object.freeze({ ...operation, measures: shown.map(measure => joinProtocolQualifiedIdentifier([measure.name])) });
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/deployment",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Verified deployment intake, activation, and semantic invocation for Hypequery analytics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hypequery",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@hypequery/protocol": "0.
|
|
29
|
+
"@hypequery/protocol": "0.21.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.5.0",
|
|
33
33
|
"typescript": "^6.0.3",
|
|
34
|
-
"vitest": "^5.0.
|
|
35
|
-
"@hypequery/datasets": "0.
|
|
34
|
+
"vitest": "^5.0.5",
|
|
35
|
+
"@hypequery/datasets": "0.24.0"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": ">=20"
|