@hypequery/deployment 0.7.5 → 0.8.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 +101 -0
- package/dist/authorized-contract.d.ts.map +1 -0
- package/dist/authorized-contract.js +0 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/semantic-data-plane.d.ts +74 -0
- package/dist/semantic-data-plane.d.ts.map +1 -0
- package/dist/semantic-data-plane.js +224 -0
- package/dist/semantic-invocation-errors.d.ts +30 -0
- package/dist/semantic-invocation-errors.d.ts.map +1 -0
- package/dist/semantic-invocation-errors.js +45 -0
- package/dist/semantic-operation-validation.d.ts +29 -0
- package/dist/semantic-operation-validation.d.ts.map +1 -0
- package/dist/semantic-operation-validation.js +177 -0
- package/dist/utils/required-access.d.ts +2 -0
- package/dist/utils/required-access.d.ts.map +1 -0
- package/dist/utils/required-access.js +4 -0
- package/dist/utils/semantic-budget-limits.d.ts +7 -0
- package/dist/utils/semantic-budget-limits.d.ts.map +1 -0
- package/dist/utils/semantic-budget-limits.js +15 -0
- package/dist/utils/semantic-executor-failure.d.ts +21 -0
- package/dist/utils/semantic-executor-failure.d.ts.map +1 -0
- package/dist/utils/semantic-executor-failure.js +52 -0
- package/package.json +4 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one principal is allowed to see of a deployment contract.
|
|
3
|
+
*
|
|
4
|
+
* Discovery and execution must agree about who may reach what. The data plane
|
|
5
|
+
* already decides that per call, from the endpoint policy on the target; a
|
|
6
|
+
* gateway listing tools has to make the same decision over the whole contract,
|
|
7
|
+
* before any call exists. Restating the rule there would put an authorization
|
|
8
|
+
* predicate in two packages, and the copy that drifts is the one that leaks.
|
|
9
|
+
*
|
|
10
|
+
* The projection narrows, never widens: it can remove a target a principal may
|
|
11
|
+
* not reach, and can never add one or loosen a policy. Feeding the result to
|
|
12
|
+
* catalog projection or schema compilation therefore cannot advertise something
|
|
13
|
+
* execution would refuse.
|
|
14
|
+
*/
|
|
15
|
+
import type { ProtocolAccessPolicy, ProtocolDeploymentContract, ProtocolEndpointPolicy } from '@hypequery/protocol';
|
|
16
|
+
import type { DeploymentDataPlanePrincipal } from './data-plane.js';
|
|
17
|
+
/**
|
|
18
|
+
* Whether a principal satisfies an access policy.
|
|
19
|
+
*
|
|
20
|
+
* Shares `missing` with the semantic data plane rather than restating it, which
|
|
21
|
+
* is the whole point of this module existing in Core: one predicate, so
|
|
22
|
+
* discovery cannot come to answer differently than execution.
|
|
23
|
+
*/
|
|
24
|
+
export declare function satisfiesDeploymentAccess(access: ProtocolAccessPolicy, principal: DeploymentDataPlanePrincipal | null): boolean;
|
|
25
|
+
/** Whether a principal may reach a target published under this endpoint policy. */
|
|
26
|
+
export declare function isDeploymentEndpointAuthorized(endpoint: ProtocolEndpointPolicy | undefined, principal: DeploymentDataPlanePrincipal | null): boolean;
|
|
27
|
+
export interface AuthorizedDeploymentProjection {
|
|
28
|
+
/**
|
|
29
|
+
* A valid contract narrowed to what the principal may see, including any
|
|
30
|
+
* dataset retained only to support a join or a named query.
|
|
31
|
+
*/
|
|
32
|
+
readonly contract: ProtocolDeploymentContract;
|
|
33
|
+
/**
|
|
34
|
+
* Datasets to advertise at all, in contract order.
|
|
35
|
+
*
|
|
36
|
+
* Wider than `queryable`: a dataset the principal cannot address may still
|
|
37
|
+
* carry a metric it can, and hiding the dataset would hide that metric.
|
|
38
|
+
* Narrower than `contract.datasets`, which also holds datasets retained only
|
|
39
|
+
* to support a join.
|
|
40
|
+
*
|
|
41
|
+
* Always read this before advertising anything. Catalog projection and
|
|
42
|
+
* rehydration enumerate every dataset they are given and neither consults an
|
|
43
|
+
* endpoint, so handing them the whole contract would advertise a supporting
|
|
44
|
+
* dataset and disclose its dimensions and measures to a principal with no
|
|
45
|
+
* access to it.
|
|
46
|
+
*/
|
|
47
|
+
readonly advertised: readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* Datasets addressable as a `query_dataset` target. A subset of `advertised`.
|
|
50
|
+
*
|
|
51
|
+
* The two differ because a deployment authorizes a dataset and each of its
|
|
52
|
+
* metrics through separate endpoint policies. Collapsing them would either
|
|
53
|
+
* offer a target execution refuses or withhold a metric it would run.
|
|
54
|
+
*
|
|
55
|
+
* The intended composition rehydrates the whole contract, so relationship
|
|
56
|
+
* targets still resolve, and advertises the two sets separately:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* const { contract, advertised, queryable } =
|
|
60
|
+
* projectAuthorizedDeploymentContract(active, principal);
|
|
61
|
+
* const registry = rehydrateProtocolDatasets(contract.datasets, { onUnsupportedMetric: 'skip' });
|
|
62
|
+
* createMCPDiscoveryExecutor({
|
|
63
|
+
* datasets: Object.fromEntries(advertised.map(name => [name, registry[name]])),
|
|
64
|
+
* queryableDatasets: queryable,
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* A joined dimension such as `orders.employee.id` still resolves through the
|
|
69
|
+
* full registry, which is correct: that join is governed by the endpoint of
|
|
70
|
+
* the dataset being queried, not by the target's.
|
|
71
|
+
*/
|
|
72
|
+
readonly queryable: readonly string[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Projects the contract down to what `principal` may see.
|
|
76
|
+
*
|
|
77
|
+
* A dataset is *published* when the principal may address it directly, and
|
|
78
|
+
* *supporting* when something published still needs it to stay coherent — the
|
|
79
|
+
* target of a relationship, or the dataset a named query plans over. A
|
|
80
|
+
* supporting dataset keeps its shape and loses its endpoint and metrics, so a
|
|
81
|
+
* relationship can still be traversed exactly as execution would traverse it
|
|
82
|
+
* while the dataset itself is not offered as a target.
|
|
83
|
+
*
|
|
84
|
+
* Dropping a relationship target instead would be the tempting alternative and
|
|
85
|
+
* is wrong twice: it would invalidate any metric declaring a dimension across
|
|
86
|
+
* that relationship, and it would advertise less than the data plane permits,
|
|
87
|
+
* since a one-hop join is governed by the endpoint of the dataset being
|
|
88
|
+
* queried, not by the target's.
|
|
89
|
+
*
|
|
90
|
+
* The two are returned separately rather than as one contract because nothing
|
|
91
|
+
* downstream reads an endpoint. `projectAgentSafeCatalog` and
|
|
92
|
+
* `rehydrateProtocolDatasets` enumerate whatever they are handed, so an
|
|
93
|
+
* unpublished dataset left in the contract they see is an advertised one.
|
|
94
|
+
*
|
|
95
|
+
* The contract is revalidated. Callers hand it to catalog projection, schema
|
|
96
|
+
* compilation, and rehydration, all of which assume a valid contract; a
|
|
97
|
+
* projection that could emit an invalid one would move that failure to whatever
|
|
98
|
+
* read it next.
|
|
99
|
+
*/
|
|
100
|
+
export declare function projectAuthorizedDeploymentContract(contract: ProtocolDeploymentContract, principal: DeploymentDataPlanePrincipal | null): AuthorizedDeploymentProjection;
|
|
101
|
+
//# sourceMappingURL=authorized-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorized-contract.d.ts","sourceRoot":"","sources":["../src/authorized-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,oBAAoB,EAEpB,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAGpE;;;;;;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;AA4GD,MAAM,WAAW,8BAA8B;IAC7C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,0BAA0B,CAAC;IAC9C;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,0BAA0B,EACpC,SAAS,EAAE,4BAA4B,GAAG,IAAI,GAC7C,8BAA8B,CA8FhC"}
|
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -34,4 +34,9 @@ export { createDeploymentHost, DeploymentHostError, } from './host.js';
|
|
|
34
34
|
export { createFileSystemDeploymentHost } from './filesystem-host.js';
|
|
35
35
|
export type { FileSystemDeploymentHost, FileSystemDeploymentHostOptions, } from './filesystem-host.js';
|
|
36
36
|
export type { DeploymentHost, DeploymentHostDataPlaneConfiguration, DeploymentHostDataPlaneInput, DeploymentHostErrorCode, DeploymentHostOptions, } from './host.js';
|
|
37
|
+
export { createDeploymentSemanticDataPlane, DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure, } from './semantic-data-plane.js';
|
|
38
|
+
export type { DeploymentSemanticAuthenticationInput, DeploymentSemanticBudget, DeploymentSemanticDataPlane, DeploymentSemanticDataPlaneOptions, DeploymentSemanticExecutionInput, DeploymentSemanticInvocationRequest, DeploymentSemanticTenantInput, } from './semantic-data-plane.js';
|
|
39
|
+
export { validateSemanticOperation } from './semantic-operation-validation.js';
|
|
40
|
+
export type { SemanticOperationLimits, SemanticOperationViolation, } from './semantic-operation-validation.js';
|
|
41
|
+
export { isDeploymentEndpointAuthorized, projectAuthorizedDeploymentContract, satisfiesDeploymentAccess, } from './authorized-contract.js';
|
|
37
42
|
//# 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":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -16,3 +16,9 @@ export { createDeploymentRuntimeSupervisorExecutor } from './data-plane-runtime.
|
|
|
16
16
|
export { createDeploymentDataPlaneFetchHandler, createDeploymentDataPlaneNodeHandler, } from './data-plane-adapters.js';
|
|
17
17
|
export { createDeploymentHost, DeploymentHostError, } from './host.js';
|
|
18
18
|
export { createFileSystemDeploymentHost } from './filesystem-host.js';
|
|
19
|
+
// Semantic invocation beside named-query execution (decision 0002).
|
|
20
|
+
export { createDeploymentSemanticDataPlane, DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure, } from './semantic-data-plane.js';
|
|
21
|
+
export { validateSemanticOperation } from './semantic-operation-validation.js';
|
|
22
|
+
// What one principal may see of a contract, for a gateway listing targets
|
|
23
|
+
// before any call exists (decision 0002, CLOUD-03).
|
|
24
|
+
export { isDeploymentEndpointAuthorized, projectAuthorizedDeploymentContract, satisfiesDeploymentAccess, } from './authorized-contract.js';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export { DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure } from './semantic-invocation-errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Semantic invocation beside named-query execution.
|
|
4
|
+
*
|
|
5
|
+
* Decision 0002 requires a dataset or metric call to run the same enforcement
|
|
6
|
+
* sequence a named query does — select the active generation, resolve the
|
|
7
|
+
* target from its validated contract, authenticate, enforce roles and scopes,
|
|
8
|
+
* resolve tenant, apply the most restrictive budget, validate input, execute,
|
|
9
|
+
* then validate and bound the output.
|
|
10
|
+
*
|
|
11
|
+
* Execution itself is injected. This module decides whether a call is allowed
|
|
12
|
+
* and what it is allowed to ask for; `CORE-12` supplies the executor that
|
|
13
|
+
* answers it.
|
|
14
|
+
*/
|
|
15
|
+
import type { ProtocolDatasetContract, ProtocolDatasetMetric, ProtocolDeploymentContract, ProtocolSemanticInvocation, ProtocolSemanticInvocationResult, ProtocolSemanticQuery } from '@hypequery/protocol';
|
|
16
|
+
import type { DeploymentDataPlanePrincipal } from './data-plane.js';
|
|
17
|
+
import { type SemanticOperationLimits } from './semantic-operation-validation.js';
|
|
18
|
+
/** The ceilings that survived after every source was applied. */
|
|
19
|
+
export interface DeploymentSemanticBudget {
|
|
20
|
+
readonly maxRows: number;
|
|
21
|
+
readonly deadlineMs?: number;
|
|
22
|
+
readonly maxResponseBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface DeploymentSemanticAuthenticationInput {
|
|
25
|
+
readonly credentials: unknown;
|
|
26
|
+
readonly invocation: ProtocolSemanticInvocation;
|
|
27
|
+
readonly dataset: ProtocolDatasetContract;
|
|
28
|
+
readonly metric?: ProtocolDatasetMetric;
|
|
29
|
+
}
|
|
30
|
+
export interface DeploymentSemanticTenantInput {
|
|
31
|
+
readonly principal: DeploymentDataPlanePrincipal | null;
|
|
32
|
+
readonly invocation: ProtocolSemanticInvocation;
|
|
33
|
+
readonly dataset: ProtocolDatasetContract;
|
|
34
|
+
readonly metric?: ProtocolDatasetMetric;
|
|
35
|
+
}
|
|
36
|
+
export interface DeploymentSemanticExecutionInput {
|
|
37
|
+
readonly deployment: ProtocolDeploymentContract;
|
|
38
|
+
readonly dataset: ProtocolDatasetContract;
|
|
39
|
+
readonly metric?: ProtocolDatasetMetric;
|
|
40
|
+
readonly operation: ProtocolSemanticQuery;
|
|
41
|
+
readonly principal: DeploymentDataPlanePrincipal | null;
|
|
42
|
+
/**
|
|
43
|
+
* Resolved by the provider callback. Never a caller-supplied value — the
|
|
44
|
+
* invocation record has no field that could carry one.
|
|
45
|
+
*/
|
|
46
|
+
readonly tenant: unknown;
|
|
47
|
+
readonly budget: DeploymentSemanticBudget;
|
|
48
|
+
readonly activationRevision: string;
|
|
49
|
+
readonly signal?: AbortSignal;
|
|
50
|
+
}
|
|
51
|
+
export interface DeploymentSemanticInvocationRequest {
|
|
52
|
+
/** An unvalidated invocation record; validated before anything else runs. */
|
|
53
|
+
readonly invocation: unknown;
|
|
54
|
+
readonly credentials?: unknown;
|
|
55
|
+
readonly signal?: AbortSignal;
|
|
56
|
+
}
|
|
57
|
+
export interface DeploymentSemanticDataPlaneOptions {
|
|
58
|
+
readonly deployment: ProtocolDeploymentContract;
|
|
59
|
+
/** The immutable generation this data plane serves. */
|
|
60
|
+
readonly activationRevision: string;
|
|
61
|
+
readonly authenticate?: (input: DeploymentSemanticAuthenticationInput) => Promise<DeploymentDataPlanePrincipal | null>;
|
|
62
|
+
readonly resolveTenant?: (input: DeploymentSemanticTenantInput) => Promise<unknown>;
|
|
63
|
+
readonly execute: (input: DeploymentSemanticExecutionInput) => Promise<unknown>;
|
|
64
|
+
/** Server-side ceilings, applied on top of contract and caller limits. */
|
|
65
|
+
readonly limits?: Partial<SemanticOperationLimits> & {
|
|
66
|
+
readonly deadlineMs?: number;
|
|
67
|
+
readonly maxResponseBytes?: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export interface DeploymentSemanticDataPlane {
|
|
71
|
+
invoke(request: DeploymentSemanticInvocationRequest): Promise<ProtocolSemanticInvocationResult>;
|
|
72
|
+
}
|
|
73
|
+
export declare function createDeploymentSemanticDataPlane(options: DeploymentSemanticDataPlaneOptions): DeploymentSemanticDataPlane;
|
|
74
|
+
//# sourceMappingURL=semantic-data-plane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-data-plane.d.ts","sourceRoot":"","sources":["../src/semantic-data-plane.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { claimedFailure } from './utils/semantic-executor-failure.js';
|
|
2
|
+
import { DeploymentSemanticInvocationError, fail, throwIfAborted } from './semantic-invocation-errors.js';
|
|
3
|
+
export { DeploymentSemanticInvocationError, toProtocolSemanticInvocationFailure } from './semantic-invocation-errors.js';
|
|
4
|
+
import { definedLimits, lowest, tighten } from './utils/semantic-budget-limits.js';
|
|
5
|
+
import { missing } from './utils/required-access.js';
|
|
6
|
+
import { ProtocolSemanticInvocationError, validateProtocolDeploymentContract, validateProtocolSemanticInvocation, validateProtocolSemanticInvocationResult, } from '@hypequery/protocol';
|
|
7
|
+
import { validateSemanticOperation, } from './semantic-operation-validation.js';
|
|
8
|
+
const DEFAULT_LIMITS = Object.freeze({
|
|
9
|
+
maxRows: 10_000,
|
|
10
|
+
maxOffset: 10_000,
|
|
11
|
+
maxDimensions: 50,
|
|
12
|
+
maxMeasures: 50,
|
|
13
|
+
maxFilters: 100,
|
|
14
|
+
});
|
|
15
|
+
const REVISION_PATTERN = /^[0-9a-f]{64}$/;
|
|
16
|
+
export function createDeploymentSemanticDataPlane(options) {
|
|
17
|
+
let deployment;
|
|
18
|
+
try {
|
|
19
|
+
deployment = validateProtocolDeploymentContract(options.deployment);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new DeploymentSemanticInvocationError('configuration-invalid', 'HQ_SEMANTIC_CONFIGURATION', 'The semantic data plane requires a valid deployment contract.', { cause: error });
|
|
23
|
+
}
|
|
24
|
+
if (!REVISION_PATTERN.test(options.activationRevision)) {
|
|
25
|
+
throw new RangeError('activationRevision must be a lowercase SHA-256 identity');
|
|
26
|
+
}
|
|
27
|
+
const datasets = new Map(deployment.datasets.map(entry => [String(entry.name), entry]));
|
|
28
|
+
// Spreading `options.limits` directly would let an explicitly `undefined`
|
|
29
|
+
// property erase a default ceiling, and an undefined bound compares false
|
|
30
|
+
// against everything. A caller that omits a limit gets the default.
|
|
31
|
+
const configured = { ...DEFAULT_LIMITS, ...definedLimits(options.limits) };
|
|
32
|
+
function resolveTarget(operation) {
|
|
33
|
+
const dataset = datasets.get(String(operation.dataset));
|
|
34
|
+
if (dataset === undefined) {
|
|
35
|
+
fail('not-found', 'HQ_SEMANTIC_DATASET_NOT_FOUND', 'The dataset was not found.', {
|
|
36
|
+
path: '$.operation.dataset',
|
|
37
|
+
relist: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const metric = operation.kind === 'metric'
|
|
41
|
+
? dataset.metrics.find(entry => String(entry.name) === String(operation.metric))
|
|
42
|
+
: undefined;
|
|
43
|
+
if (operation.kind === 'metric' && metric === undefined) {
|
|
44
|
+
fail('not-found', 'HQ_SEMANTIC_METRIC_NOT_FOUND', 'The metric was not found.', {
|
|
45
|
+
path: '$.operation.metric',
|
|
46
|
+
relist: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// An endpoint policy is what publishes a target. Without one the contract
|
|
50
|
+
// describes the dataset but never exposed it, so it is not addressable.
|
|
51
|
+
const endpoint = metric?.endpoint ?? dataset.endpoint;
|
|
52
|
+
if (endpoint === undefined) {
|
|
53
|
+
fail('not-found', 'HQ_SEMANTIC_NOT_PUBLISHED', 'The target is not published.', {
|
|
54
|
+
path: '$.operation',
|
|
55
|
+
relist: true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return { dataset, ...(metric === undefined ? {} : { metric }), endpoint };
|
|
59
|
+
}
|
|
60
|
+
async function invoke(request) {
|
|
61
|
+
throwIfAborted(request.signal);
|
|
62
|
+
let invocation;
|
|
63
|
+
try {
|
|
64
|
+
invocation = validateProtocolSemanticInvocation(request.invocation);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
fail('input-invalid', 'HQ_SEMANTIC_INVOCATION_INVALID', 'The invocation record is invalid.', {
|
|
68
|
+
path: error instanceof ProtocolSemanticInvocationError ? error.path : undefined,
|
|
69
|
+
cause: error,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Pinning is checked before anything else observable, so a stale caller
|
|
73
|
+
// cannot learn whether a target exists in a generation it may not use.
|
|
74
|
+
if (invocation.activationRevision !== undefined
|
|
75
|
+
&& invocation.activationRevision !== options.activationRevision) {
|
|
76
|
+
fail('stale-activation', 'HQ_SEMANTIC_STALE_ACTIVATION', 'The pinned activation is no longer active.', { retryable: true, relist: true });
|
|
77
|
+
}
|
|
78
|
+
const operation = invocation.operation;
|
|
79
|
+
const { dataset, metric, endpoint } = resolveTarget(operation);
|
|
80
|
+
let principal = null;
|
|
81
|
+
if (endpoint.access.kind === 'authenticated' && !options.authenticate) {
|
|
82
|
+
fail('configuration-invalid', 'HQ_SEMANTIC_CONFIGURATION', 'An authenticator is required for this target.');
|
|
83
|
+
}
|
|
84
|
+
if (options.authenticate
|
|
85
|
+
&& (endpoint.access.kind === 'authenticated' || request.credentials !== undefined)) {
|
|
86
|
+
try {
|
|
87
|
+
principal = await options.authenticate({
|
|
88
|
+
credentials: request.credentials, invocation, dataset, ...(metric ? { metric } : {}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
fail('unauthenticated', 'HQ_SEMANTIC_UNAUTHENTICATED', 'Authentication failed.', { cause: error });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throwIfAborted(request.signal);
|
|
96
|
+
if (endpoint.access.kind === 'authenticated') {
|
|
97
|
+
if (!principal) {
|
|
98
|
+
fail('unauthenticated', 'HQ_SEMANTIC_UNAUTHENTICATED', 'Authentication is required.');
|
|
99
|
+
}
|
|
100
|
+
if (missing(endpoint.access.roles, principal.roles)
|
|
101
|
+
|| missing(endpoint.access.scopes, principal.scopes)) {
|
|
102
|
+
fail('forbidden', 'HQ_SEMANTIC_FORBIDDEN', 'The principal lacks required access.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let tenant;
|
|
106
|
+
if (endpoint.tenant.kind !== 'not-required') {
|
|
107
|
+
if (!options.resolveTenant) {
|
|
108
|
+
if (endpoint.tenant.kind === 'required') {
|
|
109
|
+
fail('configuration-invalid', 'HQ_SEMANTIC_CONFIGURATION', 'A tenant resolver is required for this target.');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
try {
|
|
114
|
+
tenant = await options.resolveTenant({
|
|
115
|
+
principal, invocation, dataset, ...(metric ? { metric } : {}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
fail('forbidden', 'HQ_SEMANTIC_FORBIDDEN', 'Tenant resolution failed.', { cause: error });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (endpoint.tenant.kind === 'required' && (tenant === undefined || tenant === null)) {
|
|
123
|
+
fail('tenant-required', 'HQ_SEMANTIC_TENANT_REQUIRED', 'Tenant context is required.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throwIfAborted(request.signal);
|
|
127
|
+
const budget = Object.freeze({
|
|
128
|
+
maxRows: lowest(invocation.budget?.maxRows, endpoint.maxLimit, dataset.limits?.maxResultSize, configured.maxRows) ?? configured.maxRows,
|
|
129
|
+
...(lowest(invocation.budget?.deadlineMs, options.limits?.deadlineMs) === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: { deadlineMs: lowest(invocation.budget?.deadlineMs, options.limits?.deadlineMs) }),
|
|
132
|
+
...(lowest(invocation.budget?.maxResponseBytes, options.limits?.maxResponseBytes) === undefined
|
|
133
|
+
? {}
|
|
134
|
+
: {
|
|
135
|
+
maxResponseBytes: lowest(invocation.budget?.maxResponseBytes, options.limits?.maxResponseBytes),
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
// Every ceiling the dataset declared binds here, not just the one that
|
|
139
|
+
// reaches the row budget. `maxDimensions`, `maxMeasures`, and `maxFilters`
|
|
140
|
+
// are published in the contract precisely so a gateway can apply them
|
|
141
|
+
// without loading the authoring package, and a set of ceilings where only
|
|
142
|
+
// one of the four binds is not a set of ceilings.
|
|
143
|
+
const violations = validateSemanticOperation(operation, dataset, datasets, {
|
|
144
|
+
maxRows: budget.maxRows,
|
|
145
|
+
maxOffset: configured.maxOffset,
|
|
146
|
+
maxDimensions: tighten(dataset.limits?.maxDimensions, configured.maxDimensions),
|
|
147
|
+
maxMeasures: tighten(dataset.limits?.maxMeasures, configured.maxMeasures),
|
|
148
|
+
maxFilters: tighten(dataset.limits?.maxFilters, configured.maxFilters),
|
|
149
|
+
});
|
|
150
|
+
if (violations.length > 0) {
|
|
151
|
+
const [first] = violations;
|
|
152
|
+
fail('input-invalid', 'HQ_SEMANTIC_INPUT_INVALID', first.message, { path: first.path });
|
|
153
|
+
}
|
|
154
|
+
throwIfAborted(request.signal);
|
|
155
|
+
let output;
|
|
156
|
+
try {
|
|
157
|
+
output = await options.execute({
|
|
158
|
+
deployment,
|
|
159
|
+
dataset,
|
|
160
|
+
...(metric === undefined ? {} : { metric }),
|
|
161
|
+
operation,
|
|
162
|
+
principal,
|
|
163
|
+
tenant,
|
|
164
|
+
budget,
|
|
165
|
+
activationRevision: options.activationRevision,
|
|
166
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof DeploymentSemanticInvocationError)
|
|
171
|
+
throw error;
|
|
172
|
+
if (request.signal?.aborted) {
|
|
173
|
+
fail('cancelled', 'HQ_SEMANTIC_CANCELLED', 'The invocation was cancelled.', { cause: error });
|
|
174
|
+
}
|
|
175
|
+
const claimed = claimedFailure(error);
|
|
176
|
+
if (claimed !== undefined) {
|
|
177
|
+
fail(claimed.category, claimed.code, claimed.message, {
|
|
178
|
+
cause: error,
|
|
179
|
+
retryable: claimed.retryable,
|
|
180
|
+
relist: claimed.relist,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
fail('executor-failed', 'HQ_SEMANTIC_EXECUTION_FAILED', 'Semantic execution failed.', {
|
|
184
|
+
cause: error,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// An executor may ignore the signal and resolve anyway. Every other await
|
|
188
|
+
// in this sequence is followed by this check; so is the last one.
|
|
189
|
+
throwIfAborted(request.signal);
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = validateProtocolSemanticInvocationResult(output);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// An executor that returned something unexpected is an internal fault,
|
|
196
|
+
// never a correctable caller error.
|
|
197
|
+
fail('output-invalid', 'HQ_SEMANTIC_OUTPUT_INVALID', 'The executor returned an invalid result.', {
|
|
198
|
+
cause: error,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// Protocol validation proves the record is well-formed, not that it honored
|
|
202
|
+
// this invocation. The bounds this layer computed are enforced here, so an
|
|
203
|
+
// executor cannot widen them by returning more than it was allowed to.
|
|
204
|
+
if (result.activationRevision !== options.activationRevision) {
|
|
205
|
+
fail('output-invalid', 'HQ_SEMANTIC_OUTPUT_INVALID', 'The executor served a different activation than the one selected.');
|
|
206
|
+
}
|
|
207
|
+
// The caller's own `limit` is part of what it was allowed to ask for, and
|
|
208
|
+
// validation already proved it is no larger than the budget. An executor
|
|
209
|
+
// that returns more rows than were requested widened the request just as
|
|
210
|
+
// surely as one that exceeded the budget.
|
|
211
|
+
const maxRows = Math.min(budget.maxRows, operation.limit ?? budget.maxRows);
|
|
212
|
+
if (result.data.length > maxRows) {
|
|
213
|
+
fail('budget-exceeded', 'HQ_SEMANTIC_BUDGET_EXCEEDED', `The result has ${result.data.length} rows; the effective limit is ${maxRows}.`);
|
|
214
|
+
}
|
|
215
|
+
if (budget.maxResponseBytes !== undefined) {
|
|
216
|
+
const bytes = new TextEncoder().encode(JSON.stringify(result)).byteLength;
|
|
217
|
+
if (bytes > budget.maxResponseBytes) {
|
|
218
|
+
fail('budget-exceeded', 'HQ_SEMANTIC_BUDGET_EXCEEDED', `The result is ${bytes} bytes; the effective limit is ${budget.maxResponseBytes}.`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
return Object.freeze({ invoke });
|
|
224
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ProtocolSemanticInvocationFailure, ProtocolSemanticInvocationFailureCategory } from '@hypequery/protocol';
|
|
2
|
+
/** A failure that already carries the public category a caller should see. */
|
|
3
|
+
export declare class DeploymentSemanticInvocationError extends Error {
|
|
4
|
+
readonly category: ProtocolSemanticInvocationFailureCategory;
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly path?: string;
|
|
7
|
+
readonly retryable: boolean;
|
|
8
|
+
readonly relist: boolean;
|
|
9
|
+
constructor(category: ProtocolSemanticInvocationFailureCategory, code: string, message: string, options?: {
|
|
10
|
+
readonly path?: string;
|
|
11
|
+
readonly cause?: unknown;
|
|
12
|
+
readonly retryable?: boolean;
|
|
13
|
+
readonly relist?: boolean;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export declare function fail(category: ProtocolSemanticInvocationFailureCategory, code: string, message: string, options?: {
|
|
17
|
+
readonly path?: string;
|
|
18
|
+
readonly cause?: unknown;
|
|
19
|
+
readonly retryable?: boolean;
|
|
20
|
+
readonly relist?: boolean;
|
|
21
|
+
}): never;
|
|
22
|
+
/**
|
|
23
|
+
* Projects a failure onto the portable record.
|
|
24
|
+
*
|
|
25
|
+
* The message is deliberately the one this module produced. A cause is never
|
|
26
|
+
* unwrapped into it, so a provider exception cannot reach a caller.
|
|
27
|
+
*/
|
|
28
|
+
export declare function toProtocolSemanticInvocationFailure(error: unknown, activationRevision?: string): ProtocolSemanticInvocationFailure;
|
|
29
|
+
export declare function throwIfAborted(signal: AbortSignal | undefined): void;
|
|
30
|
+
//# sourceMappingURL=semantic-invocation-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-invocation-errors.d.ts","sourceRoot":"","sources":["../src/semantic-invocation-errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iCAAiC,EAAE,yCAAyC,EAAE,MAAM,qBAAqB,CAAC;AAExH,8EAA8E;AAC9E,qBAAa,iCAAkC,SAAQ,KAAK;IAC1D,QAAQ,CAAC,QAAQ,EAAE,yCAAyC,CAAC;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;gBAGvB,QAAQ,EAAE,yCAAyC,EACnD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QACP,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;KACtB;CAUT;AAED,wBAAgB,IAAI,CAClB,QAAQ,EAAE,yCAAyC,EACnD,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IACP,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CACtB,GACL,KAAK,CAEP;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,OAAO,EACd,kBAAkB,CAAC,EAAE,MAAM,GAC1B,iCAAiC,CAanC;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAIpE"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** A failure that already carries the public category a caller should see. */
|
|
2
|
+
export class DeploymentSemanticInvocationError extends Error {
|
|
3
|
+
category;
|
|
4
|
+
code;
|
|
5
|
+
path;
|
|
6
|
+
retryable;
|
|
7
|
+
relist;
|
|
8
|
+
constructor(category, code, message, options = {}) {
|
|
9
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
10
|
+
this.name = 'DeploymentSemanticInvocationError';
|
|
11
|
+
this.category = category;
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.path = options.path;
|
|
14
|
+
this.retryable = options.retryable ?? false;
|
|
15
|
+
this.relist = options.relist ?? false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function fail(category, code, message, options = {}) {
|
|
19
|
+
throw new DeploymentSemanticInvocationError(category, code, message, options);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Projects a failure onto the portable record.
|
|
23
|
+
*
|
|
24
|
+
* The message is deliberately the one this module produced. A cause is never
|
|
25
|
+
* unwrapped into it, so a provider exception cannot reach a caller.
|
|
26
|
+
*/
|
|
27
|
+
export function toProtocolSemanticInvocationFailure(error, activationRevision) {
|
|
28
|
+
const known = error instanceof DeploymentSemanticInvocationError;
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
kind: 'hypequery-semantic-invocation-failure',
|
|
31
|
+
version: 1,
|
|
32
|
+
category: known ? error.category : 'executor-failed',
|
|
33
|
+
code: known ? error.code : 'HQ_SEMANTIC_EXECUTION_FAILED',
|
|
34
|
+
message: known ? error.message : 'Semantic invocation failed.',
|
|
35
|
+
...(known && error.path !== undefined ? { path: error.path } : {}),
|
|
36
|
+
retryable: known ? error.retryable : false,
|
|
37
|
+
relist: known ? error.relist : false,
|
|
38
|
+
...(activationRevision === undefined ? {} : { activationRevision }),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function throwIfAborted(signal) {
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
fail('cancelled', 'HQ_SEMANTIC_CANCELLED', 'The invocation was cancelled.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-driven validation of a semantic operation.
|
|
3
|
+
*
|
|
4
|
+
* The deployment package deliberately depends only on `@hypequery/protocol`, so
|
|
5
|
+
* every check here is made against the validated contract rather than by
|
|
6
|
+
* loading the authoring package. A gateway must be able to reject a bad
|
|
7
|
+
* invocation without any of the deploying application's code.
|
|
8
|
+
*/
|
|
9
|
+
import type { ProtocolDatasetContract, ProtocolSemanticQuery } from '@hypequery/protocol';
|
|
10
|
+
export interface SemanticOperationViolation {
|
|
11
|
+
readonly message: string;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SemanticOperationLimits {
|
|
15
|
+
/** Largest row count the caller may ask for after every ceiling is applied. */
|
|
16
|
+
readonly maxRows: number;
|
|
17
|
+
readonly maxOffset: number;
|
|
18
|
+
readonly maxDimensions: number;
|
|
19
|
+
readonly maxMeasures: number;
|
|
20
|
+
readonly maxFilters: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validates one semantic operation against the contract that will serve it.
|
|
24
|
+
*
|
|
25
|
+
* Returns every violation rather than the first, so a caller correcting an
|
|
26
|
+
* agent-authored query sees the whole problem in one round trip.
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateSemanticOperation(operation: ProtocolSemanticQuery, dataset: ProtocolDatasetContract, datasets: ReadonlyMap<string, ProtocolDatasetContract>, limits: SemanticOperationLimits): readonly SemanticOperationViolation[];
|
|
29
|
+
//# sourceMappingURL=semantic-operation-validation.d.ts.map
|
|
@@ -0,0 +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,uBAAuB,EAGvB,qBAAqB,EAEtB,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;AAiGD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,qBAAqB,EAChC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,uBAAuB,CAAC,EACtD,MAAM,EAAE,uBAAuB,GAC9B,SAAS,0BAA0B,EAAE,CAgGvC"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-driven validation of a semantic operation.
|
|
3
|
+
*
|
|
4
|
+
* The deployment package deliberately depends only on `@hypequery/protocol`, so
|
|
5
|
+
* every check here is made against the validated contract rather than by
|
|
6
|
+
* loading the authoring package. A gateway must be able to reject a bad
|
|
7
|
+
* invocation without any of the deploying application's code.
|
|
8
|
+
*/
|
|
9
|
+
const ALL_GRAINS = ['day', 'week', 'month', 'quarter', 'year'];
|
|
10
|
+
/**
|
|
11
|
+
* What a dataset exposes, before any metric narrows it.
|
|
12
|
+
*
|
|
13
|
+
* A relationship contributes `<name>.<dimension>` for its target's groupable
|
|
14
|
+
* dimensions, matching the one-hop rule the authoring layer enforces. Filters
|
|
15
|
+
* over a relationship are accepted on the same qualified names, because the
|
|
16
|
+
* contract has no separate filter declaration for a joined field.
|
|
17
|
+
*/
|
|
18
|
+
function resolveDataset(dataset, datasets) {
|
|
19
|
+
const groupable = new Set();
|
|
20
|
+
const filters = new Map();
|
|
21
|
+
for (const dimension of dataset.dimensions) {
|
|
22
|
+
if (dimension.groupable)
|
|
23
|
+
groupable.add(String(dimension.name));
|
|
24
|
+
}
|
|
25
|
+
for (const filter of dataset.filters) {
|
|
26
|
+
filters.set(String(filter.name), new Set(filter.operators));
|
|
27
|
+
}
|
|
28
|
+
for (const relationship of dataset.relationships) {
|
|
29
|
+
if (!relationship.queryable)
|
|
30
|
+
continue;
|
|
31
|
+
const target = datasets.get(String(relationship.target));
|
|
32
|
+
for (const dimension of target?.dimensions ?? []) {
|
|
33
|
+
const qualified = `${String(relationship.name)}.${String(dimension.name)}`;
|
|
34
|
+
// The two capabilities are declared separately and travel separately
|
|
35
|
+
// across a join. Coupling them would both hide a filterable field that
|
|
36
|
+
// cannot be grouped and expose one the target declared unfilterable.
|
|
37
|
+
if (dimension.groupable)
|
|
38
|
+
groupable.add(qualified);
|
|
39
|
+
if (dimension.filterable) {
|
|
40
|
+
// A joined field has no named filter in the contract, so it accepts the
|
|
41
|
+
// full operator set the protocol allows rather than a narrowed list.
|
|
42
|
+
filters.set(qualified, new Set([
|
|
43
|
+
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'between', 'like',
|
|
44
|
+
]));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
groupable,
|
|
50
|
+
filters,
|
|
51
|
+
measures: new Set(dataset.measures.map(measure => String(measure.name))),
|
|
52
|
+
grains: new Set(dataset.timeField === undefined ? [] : ALL_GRAINS),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** A metric may only narrow what its dataset exposes, never widen it. */
|
|
56
|
+
function narrowToMetric(base, metric) {
|
|
57
|
+
const declaredDimensions = new Set(metric.dimensions.map(String));
|
|
58
|
+
const declaredFilters = new Set(metric.filters.map(String));
|
|
59
|
+
return {
|
|
60
|
+
groupable: new Set([...base.groupable].filter(name => declaredDimensions.has(name))),
|
|
61
|
+
filters: new Map([...base.filters].filter(([name]) => declaredFilters.has(name))),
|
|
62
|
+
// A metric selects itself; a caller cannot add measures to it.
|
|
63
|
+
measures: new Set(),
|
|
64
|
+
grains: new Set(metric.grain === undefined
|
|
65
|
+
? metric.grains.filter(grain => base.grains.has(grain))
|
|
66
|
+
: [metric.grain]),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The field a filter expression addresses, or null when it is not a plain
|
|
71
|
+
* comparison.
|
|
72
|
+
*
|
|
73
|
+
* Both operands are checked. A right-hand side that is anything but a literal —
|
|
74
|
+
* another reference, an aggregate, a nested comparison — addresses something
|
|
75
|
+
* this validator never matched against the contract's allowlist, so accepting
|
|
76
|
+
* it would leave a published-surface check to whichever executor happens to be
|
|
77
|
+
* injected. It is also a caller mistake rather than a deployment capability
|
|
78
|
+
* gap, and only a violation raised here reports it as one.
|
|
79
|
+
*/
|
|
80
|
+
function comparisonField(expression) {
|
|
81
|
+
if (expression.kind !== 'comparison')
|
|
82
|
+
return null;
|
|
83
|
+
if (expression.left.kind !== 'reference' || expression.right.kind !== 'literal')
|
|
84
|
+
return null;
|
|
85
|
+
return { field: String(expression.left.name), operator: expression.operator };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validates one semantic operation against the contract that will serve it.
|
|
89
|
+
*
|
|
90
|
+
* Returns every violation rather than the first, so a caller correcting an
|
|
91
|
+
* agent-authored query sees the whole problem in one round trip.
|
|
92
|
+
*/
|
|
93
|
+
export function validateSemanticOperation(operation, dataset, datasets, limits) {
|
|
94
|
+
const violations = [];
|
|
95
|
+
const fail = (message, path) => violations.push({ message, path });
|
|
96
|
+
let metric;
|
|
97
|
+
if (operation.kind === 'metric') {
|
|
98
|
+
metric = dataset.metrics.find(entry => String(entry.name) === String(operation.metric));
|
|
99
|
+
if (metric === undefined) {
|
|
100
|
+
fail(`Unknown metric "${String(operation.metric)}".`, '$.operation.metric');
|
|
101
|
+
return violations;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const base = resolveDataset(dataset, datasets);
|
|
105
|
+
const allowed = metric === undefined ? base : narrowToMetric(base, metric);
|
|
106
|
+
const dimensions = operation.dimensions ?? [];
|
|
107
|
+
if (dimensions.length > limits.maxDimensions) {
|
|
108
|
+
fail(`At most ${limits.maxDimensions} dimensions may be selected.`, '$.operation.dimensions');
|
|
109
|
+
}
|
|
110
|
+
dimensions.forEach((name, index) => {
|
|
111
|
+
if (!allowed.groupable.has(String(name))) {
|
|
112
|
+
fail(`Unknown or non-groupable dimension "${String(name)}".`, `$.operation.dimensions[${index}]`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
if (new Set(dimensions.map(String)).size !== dimensions.length) {
|
|
116
|
+
fail('Dimensions must be unique.', '$.operation.dimensions');
|
|
117
|
+
}
|
|
118
|
+
if (operation.kind === 'dataset') {
|
|
119
|
+
const measures = operation.measures ?? [];
|
|
120
|
+
if (measures.length > limits.maxMeasures) {
|
|
121
|
+
fail(`At most ${limits.maxMeasures} measures may be selected.`, '$.operation.measures');
|
|
122
|
+
}
|
|
123
|
+
measures.forEach((name, index) => {
|
|
124
|
+
if (!allowed.measures.has(String(name))) {
|
|
125
|
+
fail(`Unknown measure "${String(name)}".`, `$.operation.measures[${index}]`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
if (dimensions.length === 0 && measures.length === 0) {
|
|
129
|
+
fail('At least one dimension or measure must be selected.', '$.operation');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const filters = operation.filters ?? [];
|
|
133
|
+
if (filters.length > limits.maxFilters) {
|
|
134
|
+
fail(`At most ${limits.maxFilters} filters may be applied.`, '$.operation.filters');
|
|
135
|
+
}
|
|
136
|
+
filters.forEach((expression, index) => {
|
|
137
|
+
const path = `$.operation.filters[${index}]`;
|
|
138
|
+
const comparison = comparisonField(expression);
|
|
139
|
+
if (comparison === null) {
|
|
140
|
+
// Only `field <operator> value` is expressible against a contract; a
|
|
141
|
+
// richer expression could reference something the contract never
|
|
142
|
+
// published.
|
|
143
|
+
fail('Only a field/operator/value comparison may be filtered on.', path);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const operators = allowed.filters.get(comparison.field);
|
|
147
|
+
if (operators === undefined) {
|
|
148
|
+
fail(`Unknown or unfilterable field "${comparison.field}".`, path);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!operators.has(comparison.operator)) {
|
|
152
|
+
fail(`Operator "${comparison.operator}" is not allowed on "${comparison.field}".`, `${path}.operator`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const orderBy = operation.orderBy ?? [];
|
|
156
|
+
const orderable = new Set([
|
|
157
|
+
...allowed.groupable,
|
|
158
|
+
...allowed.measures,
|
|
159
|
+
...(metric === undefined ? [] : [String(metric.name)]),
|
|
160
|
+
...(operation.by === undefined ? [] : ['period']),
|
|
161
|
+
]);
|
|
162
|
+
orderBy.forEach((entry, index) => {
|
|
163
|
+
if (!orderable.has(String(entry.field))) {
|
|
164
|
+
fail(`Cannot order by "${String(entry.field)}".`, `$.operation.orderBy[${index}].field`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
if (operation.by !== undefined && !allowed.grains.has(operation.by)) {
|
|
168
|
+
fail(`Time grain "${operation.by}" is not supported here.`, '$.operation.by');
|
|
169
|
+
}
|
|
170
|
+
if (operation.limit !== undefined && operation.limit > limits.maxRows) {
|
|
171
|
+
fail(`The row limit may not exceed ${limits.maxRows}.`, '$.operation.limit');
|
|
172
|
+
}
|
|
173
|
+
if (operation.offset !== undefined && operation.offset > limits.maxOffset) {
|
|
174
|
+
fail(`The offset may not exceed ${limits.maxOffset}.`, '$.operation.offset');
|
|
175
|
+
}
|
|
176
|
+
return violations;
|
|
177
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"required-access.d.ts","sourceRoot":"","sources":["../../src/utils/required-access.ts"],"names":[],"mappings":"AAAA,wBAAgB,OAAO,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAGjG"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Drops explicitly-undefined properties, so a spread cannot erase a default. */
|
|
2
|
+
export declare function definedLimits<T extends object>(limits: T | undefined): Partial<T>;
|
|
3
|
+
/** The lowest of every ceiling that applies. A caller can tighten, never widen. */
|
|
4
|
+
export declare function lowest(...values: readonly (number | undefined)[]): number | undefined;
|
|
5
|
+
/** A declared ceiling under a server one; declaring nothing leaves the server's. */
|
|
6
|
+
export declare function tighten(declared: number | undefined, ceiling: number): number;
|
|
7
|
+
//# sourceMappingURL=semantic-budget-limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-budget-limits.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-budget-limits.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAKjF;AAED,mFAAmF;AACnF,wBAAgB,MAAM,CAAC,GAAG,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,GAAG,SAAS,CAGrF;AAED,oFAAoF;AACpF,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Drops explicitly-undefined properties, so a spread cannot erase a default. */
|
|
2
|
+
export function definedLimits(limits) {
|
|
3
|
+
if (limits === undefined)
|
|
4
|
+
return {};
|
|
5
|
+
return Object.fromEntries(Object.entries(limits).filter(([, value]) => value !== undefined));
|
|
6
|
+
}
|
|
7
|
+
/** The lowest of every ceiling that applies. A caller can tighten, never widen. */
|
|
8
|
+
export function lowest(...values) {
|
|
9
|
+
const finite = values.filter((value) => value !== undefined);
|
|
10
|
+
return finite.length === 0 ? undefined : Math.min(...finite);
|
|
11
|
+
}
|
|
12
|
+
/** A declared ceiling under a server one; declaring nothing leaves the server's. */
|
|
13
|
+
export function tighten(declared, ceiling) {
|
|
14
|
+
return declared === undefined ? ceiling : Math.min(declared, ceiling);
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProtocolSemanticInvocationFailureCategory } from '@hypequery/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* The category an executor claimed, when it deliberately claimed one.
|
|
4
|
+
*
|
|
5
|
+
* The `hypequerySemanticFailure` marker, not the shape of the error, is what
|
|
6
|
+
* grants the claim. Allow-listing a `category` string alone would let a
|
|
7
|
+
* provider or library exception that happens to carry a generic one — a
|
|
8
|
+
* `not-found` from an HTTP client, say — put its own message in front of a
|
|
9
|
+
* caller and decide whether the call is retried. This module refuses to unwrap
|
|
10
|
+
* a cause for exactly that reason, and duck-typing would have reopened the door
|
|
11
|
+
* beside it. An error without the marker stays `executor-failed` with the
|
|
12
|
+
* generic message.
|
|
13
|
+
*/
|
|
14
|
+
export declare function claimedFailure(error: unknown): {
|
|
15
|
+
category: ProtocolSemanticInvocationFailureCategory;
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
retryable: boolean;
|
|
19
|
+
relist: boolean;
|
|
20
|
+
} | undefined;
|
|
21
|
+
//# sourceMappingURL=semantic-executor-failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-executor-failure.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-executor-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yCAAyC,EAAE,MAAM,qBAAqB,CAAC;AAiCrF;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG;IAC9C,QAAQ,EAAE,yCAAyC,CAAC;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,SAAS,CAgBZ"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categories an injected executor may claim for itself.
|
|
3
|
+
*
|
|
4
|
+
* Execution is injected, so the executor is the only component that knows the
|
|
5
|
+
* difference between "this query broke", "this deployment cannot express that",
|
|
6
|
+
* and "this deployment is incoherent". Every category here is one only the
|
|
7
|
+
* executor can determine; the ones it must never claim — `unauthenticated`,
|
|
8
|
+
* `forbidden`, `cancelled`, `stale-activation` — are decided by this module and
|
|
9
|
+
* are deliberately absent.
|
|
10
|
+
*/
|
|
11
|
+
const EXECUTOR_CATEGORIES = new Set([
|
|
12
|
+
'unsupported-capability',
|
|
13
|
+
'budget-exceeded',
|
|
14
|
+
'tenant-required',
|
|
15
|
+
'configuration-invalid',
|
|
16
|
+
'not-found',
|
|
17
|
+
'input-invalid',
|
|
18
|
+
'output-invalid',
|
|
19
|
+
'executor-unavailable',
|
|
20
|
+
'executor-failed',
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* The category an executor claimed, when it deliberately claimed one.
|
|
24
|
+
*
|
|
25
|
+
* The `hypequerySemanticFailure` marker, not the shape of the error, is what
|
|
26
|
+
* grants the claim. Allow-listing a `category` string alone would let a
|
|
27
|
+
* provider or library exception that happens to carry a generic one — a
|
|
28
|
+
* `not-found` from an HTTP client, say — put its own message in front of a
|
|
29
|
+
* caller and decide whether the call is retried. This module refuses to unwrap
|
|
30
|
+
* a cause for exactly that reason, and duck-typing would have reopened the door
|
|
31
|
+
* beside it. An error without the marker stays `executor-failed` with the
|
|
32
|
+
* generic message.
|
|
33
|
+
*/
|
|
34
|
+
export function claimedFailure(error) {
|
|
35
|
+
if (typeof error !== 'object' || error === null)
|
|
36
|
+
return undefined;
|
|
37
|
+
const shape = error;
|
|
38
|
+
if (shape.hypequerySemanticFailure !== true)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (typeof shape.category !== 'string' || !EXECUTOR_CATEGORIES.has(shape.category)) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
category: shape.category,
|
|
45
|
+
code: typeof shape.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(shape.code)
|
|
46
|
+
? shape.code
|
|
47
|
+
: 'HQ_SEMANTIC_EXECUTION_FAILED',
|
|
48
|
+
message: typeof shape.message === 'string' ? shape.message : 'Semantic execution failed.',
|
|
49
|
+
retryable: shape.retryable === true,
|
|
50
|
+
relist: shape.relist === true,
|
|
51
|
+
};
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/deployment",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Verified deployment intake, activation, and runtime hosting for Hypequery analytics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hypequery",
|
|
@@ -26,12 +26,13 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@hypequery/protocol": "0.
|
|
29
|
+
"@hypequery/protocol": "0.13.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.5.0",
|
|
33
33
|
"typescript": "^5.7.3",
|
|
34
|
-
"vitest": "^3.2.6"
|
|
34
|
+
"vitest": "^3.2.6",
|
|
35
|
+
"@hypequery/datasets": "0.15.0"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
|
37
38
|
"node": ">=20"
|