@hypequery/datasets 0.13.6 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/agent-catalog.d.ts +97 -0
- package/dist/agent-catalog.d.ts.map +1 -0
- package/dist/agent-catalog.js +79 -0
- package/dist/api.type-test.js +19 -1
- package/dist/catalog.d.ts +17 -6
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +18 -1
- package/dist/contract.d.ts +11 -7
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +22 -1
- package/dist/dataset-query.js +1 -1
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +19 -2
- package/dist/executor.d.ts +6 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +55 -23
- package/dist/field.d.ts.map +1 -1
- package/dist/field.js +2 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +4 -0
- package/dist/portable-execution-errors.d.ts +49 -0
- package/dist/portable-execution-errors.d.ts.map +1 -0
- package/dist/portable-execution-errors.js +52 -0
- package/dist/portable-executor.d.ts +61 -0
- package/dist/portable-executor.d.ts.map +1 -0
- package/dist/portable-executor.js +90 -0
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +26 -97
- package/dist/protocol-rehydrate.d.ts +56 -0
- package/dist/protocol-rehydrate.d.ts.map +1 -0
- package/dist/protocol-rehydrate.js +264 -0
- package/dist/publishing/features/dataset-publication.d.ts +10 -0
- package/dist/publishing/features/dataset-publication.d.ts.map +1 -0
- package/dist/publishing/features/dataset-publication.js +34 -0
- package/dist/publishing/node.d.ts +16 -0
- package/dist/publishing/node.d.ts.map +1 -0
- package/dist/publishing/node.js +35 -0
- package/dist/publishing/publisher.d.ts +17 -0
- package/dist/publishing/publisher.d.ts.map +1 -0
- package/dist/publishing/publisher.js +32 -0
- package/dist/publishing/types.d.ts +31 -0
- package/dist/publishing/types.d.ts.map +1 -0
- package/dist/publishing/types.js +1 -0
- package/dist/query-builder-protocol.d.ts +6 -2
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/semantic-plan.d.ts +3 -1
- package/dist/semantic-plan.d.ts.map +1 -1
- package/dist/semantic-planner.d.ts.map +1 -1
- package/dist/semantic-planner.js +4 -0
- package/dist/semantic-query-schema.d.ts +60 -0
- package/dist/semantic-query-schema.d.ts.map +1 -0
- package/dist/semantic-query-schema.js +223 -0
- package/dist/sql-utils.d.ts +36 -0
- package/dist/sql-utils.d.ts.map +1 -1
- package/dist/sql-utils.js +76 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +25 -267
- package/dist/types.d.ts +98 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.d.ts +78 -0
- package/dist/utils/agent-catalog-projection.d.ts.map +1 -0
- package/dist/utils/agent-catalog-projection.js +179 -0
- package/dist/utils/dataset-cache-policy.d.ts +34 -0
- package/dist/utils/dataset-cache-policy.d.ts.map +1 -0
- package/dist/utils/dataset-cache-policy.js +50 -0
- package/dist/utils/dataset-contract.d.ts +2 -2
- package/dist/utils/dataset-contract.d.ts.map +1 -1
- package/dist/utils/dataset-contract.js +3 -1
- package/dist/utils/dataset-definition-validation.d.ts +35 -0
- package/dist/utils/dataset-definition-validation.d.ts.map +1 -0
- package/dist/utils/dataset-definition-validation.js +215 -0
- package/dist/utils/dataset-metric-ref.d.ts +2 -2
- package/dist/utils/dataset-metric-ref.d.ts.map +1 -1
- package/dist/utils/dataset-metric-ref.js +5 -3
- package/dist/utils/metric-alias.d.ts +4 -0
- package/dist/utils/metric-alias.d.ts.map +1 -0
- package/dist/utils/metric-alias.js +28 -0
- package/dist/utils/portable-execution-deadline.d.ts +4 -0
- package/dist/utils/portable-execution-deadline.d.ts.map +1 -0
- package/dist/utils/portable-execution-deadline.js +27 -0
- package/dist/utils/portable-result-budget.d.ts +12 -0
- package/dist/utils/portable-result-budget.d.ts.map +1 -0
- package/dist/utils/portable-result-budget.js +21 -0
- package/dist/utils/portable-semantic-query.d.ts +5 -0
- package/dist/utils/portable-semantic-query.d.ts.map +1 -0
- package/dist/utils/portable-semantic-query.js +36 -0
- package/dist/utils/protocol-metric-capabilities.d.ts +7 -0
- package/dist/utils/protocol-metric-capabilities.d.ts.map +1 -0
- package/dist/utils/protocol-metric-capabilities.js +57 -0
- package/dist/utils/protocol-metric-expressions.d.ts +17 -0
- package/dist/utils/protocol-metric-expressions.d.ts.map +1 -0
- package/dist/utils/protocol-metric-expressions.js +118 -0
- package/dist/utils/protocol-rehydrate-derivation.d.ts +27 -0
- package/dist/utils/protocol-rehydrate-derivation.d.ts.map +1 -0
- package/dist/utils/protocol-rehydrate-derivation.js +104 -0
- package/dist/utils/protocol-rehydrate-filters.d.ts +10 -0
- package/dist/utils/protocol-rehydrate-filters.d.ts.map +1 -0
- package/dist/utils/protocol-rehydrate-filters.js +36 -0
- package/dist/utils/protocol-semantic-metadata.d.ts +5 -0
- package/dist/utils/protocol-semantic-metadata.d.ts.map +1 -0
- package/dist/utils/protocol-semantic-metadata.js +5 -0
- package/dist/utils/published-name.d.ts +3 -0
- package/dist/utils/published-name.d.ts.map +1 -0
- package/dist/utils/published-name.js +7 -0
- package/dist/utils/published-relationships.d.ts +34 -0
- package/dist/utils/published-relationships.d.ts.map +1 -0
- package/dist/utils/published-relationships.js +35 -0
- package/dist/utils/relationship-fields.d.ts +7 -0
- package/dist/utils/relationship-fields.d.ts.map +1 -1
- package/dist/utils/relationship-fields.js +13 -1
- package/dist/utils/result-limits.d.ts +34 -0
- package/dist/utils/result-limits.d.ts.map +1 -0
- package/dist/utils/result-limits.js +40 -0
- package/dist/utils/semantic-metadata-validation.d.ts +4 -0
- package/dist/utils/semantic-metadata-validation.d.ts.map +1 -0
- package/dist/utils/semantic-metadata-validation.js +103 -0
- package/dist/utils/semantic-metadata.d.ts +4 -0
- package/dist/utils/semantic-metadata.d.ts.map +1 -0
- package/dist/utils/semantic-metadata.js +13 -0
- package/dist/utils/semantic-tool.d.ts +5 -0
- package/dist/utils/semantic-tool.d.ts.map +1 -0
- package/dist/utils/semantic-tool.js +29 -0
- package/package.json +5 -4
package/dist/protocol-adapter.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { filterExpression, metricExpression, metricDerivation } from './utils/protocol-metric-expressions.js';
|
|
2
|
+
import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, validateProtocolDatasetContract, } from '@hypequery/protocol';
|
|
2
3
|
import { SEMANTIC_FILTER_OPERATORS } from './constants.js';
|
|
4
|
+
import { toProtocolSemanticMetadata } from './utils/protocol-semantic-metadata.js';
|
|
3
5
|
function byName(left, right) {
|
|
4
6
|
return left.name.localeCompare(right.name);
|
|
5
7
|
}
|
|
@@ -16,107 +18,11 @@ function fieldSchema(type) {
|
|
|
16
18
|
return { kind: 'any' };
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
|
-
function canonicalValue(input) {
|
|
20
|
-
if (Array.isArray(input)) {
|
|
21
|
-
return validateCanonicalValue({
|
|
22
|
-
$hypequery: {
|
|
23
|
-
type: 'array',
|
|
24
|
-
version: 1,
|
|
25
|
-
values: input.map(canonicalValue),
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
if (typeof input === 'object' && input !== null) {
|
|
30
|
-
if ('$hypequery' in input)
|
|
31
|
-
return validateCanonicalValue(input);
|
|
32
|
-
const prototype = Object.getPrototypeOf(input);
|
|
33
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
34
|
-
throw new TypeError('Dataset protocol adapter only accepts plain filter values.');
|
|
35
|
-
}
|
|
36
|
-
return validateCanonicalValue({
|
|
37
|
-
$hypequery: {
|
|
38
|
-
type: 'map',
|
|
39
|
-
version: 1,
|
|
40
|
-
entries: Object.entries(input)
|
|
41
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
42
|
-
.map(([key, value]) => [key, canonicalValue(value)]),
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return validateCanonicalValue(input);
|
|
47
|
-
}
|
|
48
|
-
function filterExpression(filter) {
|
|
49
|
-
const left = {
|
|
50
|
-
kind: 'reference',
|
|
51
|
-
name: parseProtocolQualifiedIdentifier(filter.field),
|
|
52
|
-
};
|
|
53
|
-
const right = {
|
|
54
|
-
kind: 'literal',
|
|
55
|
-
value: canonicalValue(filter.value),
|
|
56
|
-
};
|
|
57
|
-
const result = {
|
|
58
|
-
kind: 'comparison',
|
|
59
|
-
operator: filter.operator,
|
|
60
|
-
left,
|
|
61
|
-
right,
|
|
62
|
-
};
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
function aggregationExpression(spec) {
|
|
66
|
-
const result = {
|
|
67
|
-
kind: 'aggregate',
|
|
68
|
-
aggregation: spec.aggregation,
|
|
69
|
-
field: parseProtocolQualifiedIdentifier(spec.field),
|
|
70
|
-
...(spec.argField !== undefined
|
|
71
|
-
? { argField: parseProtocolQualifiedIdentifier(spec.argField) }
|
|
72
|
-
: {}),
|
|
73
|
-
...(spec.level !== undefined ? { level: spec.level } : {}),
|
|
74
|
-
...(spec.filters?.length
|
|
75
|
-
? { filters: spec.filters.map(filterExpression) }
|
|
76
|
-
: {}),
|
|
77
|
-
};
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
function semanticExpression(expression, references = {}) {
|
|
81
|
-
switch (expression.kind) {
|
|
82
|
-
case 'ref':
|
|
83
|
-
return references[expression.name]
|
|
84
|
-
?? {
|
|
85
|
-
kind: 'reference',
|
|
86
|
-
name: parseProtocolQualifiedIdentifier(expression.name),
|
|
87
|
-
};
|
|
88
|
-
case 'literal':
|
|
89
|
-
return { kind: 'literal', value: canonicalValue(expression.value) };
|
|
90
|
-
case 'binary':
|
|
91
|
-
return {
|
|
92
|
-
kind: 'binary',
|
|
93
|
-
operator: expression.operator,
|
|
94
|
-
left: semanticExpression(expression.left, references),
|
|
95
|
-
right: semanticExpression(expression.right, references),
|
|
96
|
-
};
|
|
97
|
-
case 'function':
|
|
98
|
-
return {
|
|
99
|
-
kind: 'call',
|
|
100
|
-
function: expression.name,
|
|
101
|
-
args: expression.args.map(argument => semanticExpression(argument, references)),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
21
|
function unwrapMetric(metric) {
|
|
106
22
|
return metric.__type === 'grained_metric_ref'
|
|
107
23
|
? { ref: metric.metric, grain: metric.grain }
|
|
108
24
|
: { ref: metric };
|
|
109
25
|
}
|
|
110
|
-
function metricExpression(spec) {
|
|
111
|
-
if (spec.__type === 'aggregation_spec')
|
|
112
|
-
return aggregationExpression(spec);
|
|
113
|
-
const aliases = Object.fromEntries(Object.keys(spec.uses).map(alias => [alias, alias]));
|
|
114
|
-
const references = Object.fromEntries(Object.entries(spec.uses).map(([alias, metric]) => [
|
|
115
|
-
alias,
|
|
116
|
-
aggregationExpression(metric.spec),
|
|
117
|
-
]));
|
|
118
|
-
return semanticExpression(spec.formula(aliases).expression, references);
|
|
119
|
-
}
|
|
120
26
|
function metricContract(exposedName, metric, endpoint) {
|
|
121
27
|
const { ref, grain } = unwrapMetric(metric);
|
|
122
28
|
const contract = metric.contract();
|
|
@@ -128,12 +34,16 @@ function metricContract(exposedName, metric, endpoint) {
|
|
|
128
34
|
? 'derived-metric'
|
|
129
35
|
: 'metric',
|
|
130
36
|
expression: metricExpression(ref.spec),
|
|
37
|
+
...(ref.spec.__type === 'derived_metric_spec'
|
|
38
|
+
? { derivation: metricDerivation(ref.spec) }
|
|
39
|
+
: {}),
|
|
131
40
|
dimensions: [...contract.dimensions].sort().map(parseProtocolQualifiedIdentifier),
|
|
132
41
|
filters: [...contract.filters].sort().map(parseProtocolIdentifier),
|
|
133
42
|
grains: [...contract.grains].sort(),
|
|
134
43
|
...(grain !== undefined ? { grain } : {}),
|
|
135
44
|
...(ref.label !== undefined ? { label: ref.label } : {}),
|
|
136
45
|
...(ref.description !== undefined ? { description: ref.description } : {}),
|
|
46
|
+
...toProtocolSemanticMetadata(ref),
|
|
137
47
|
endpoint,
|
|
138
48
|
};
|
|
139
49
|
return result;
|
|
@@ -170,7 +80,23 @@ export function buildProtocolDatasetContract(dataset, options = {}) {
|
|
|
170
80
|
.sort(byName);
|
|
171
81
|
const contract = {
|
|
172
82
|
name: dataset.name,
|
|
83
|
+
...(dataset.description !== undefined ? { description: dataset.description } : {}),
|
|
84
|
+
...toProtocolSemanticMetadata(dataset),
|
|
173
85
|
source: dataset.source,
|
|
86
|
+
...(dataset.freshness !== undefined ? { freshness: { ...dataset.freshness } } : {}),
|
|
87
|
+
...(dataset.owner !== undefined ? { owner: dataset.owner } : {}),
|
|
88
|
+
...(dataset.defaults !== undefined
|
|
89
|
+
? {
|
|
90
|
+
defaults: {
|
|
91
|
+
...(dataset.defaults.dimensions !== undefined
|
|
92
|
+
? { dimensions: [...dataset.defaults.dimensions].map(parseProtocolIdentifier) }
|
|
93
|
+
: {}),
|
|
94
|
+
...(dataset.defaults.timeGrain !== undefined
|
|
95
|
+
? { timeGrain: dataset.defaults.timeGrain }
|
|
96
|
+
: {}),
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
: {}),
|
|
174
100
|
tenant: dataset.tenantKey
|
|
175
101
|
? { kind: 'required', field: dataset.tenantKey }
|
|
176
102
|
: { kind: 'not-required' },
|
|
@@ -185,6 +111,7 @@ export function buildProtocolDatasetContract(dataset, options = {}) {
|
|
|
185
111
|
groupable: dimension.groupable !== false,
|
|
186
112
|
...(dimension.label !== undefined ? { label: dimension.label } : {}),
|
|
187
113
|
...(dimension.description !== undefined ? { description: dimension.description } : {}),
|
|
114
|
+
...toProtocolSemanticMetadata(dimension),
|
|
188
115
|
})).sort(byName),
|
|
189
116
|
measures: Object.entries(dataset.measures).map(([name, measure]) => ({
|
|
190
117
|
name,
|
|
@@ -200,6 +127,7 @@ export function buildProtocolDatasetContract(dataset, options = {}) {
|
|
|
200
127
|
filters: (measure.filters ?? []).map(filterExpression),
|
|
201
128
|
...(measure.label !== undefined ? { label: measure.label } : {}),
|
|
202
129
|
...(measure.description !== undefined ? { description: measure.description } : {}),
|
|
130
|
+
...toProtocolSemanticMetadata(measure),
|
|
203
131
|
})).sort(byName),
|
|
204
132
|
filters: Object.entries(dataset.filters).map(([name, filter]) => ({
|
|
205
133
|
name,
|
|
@@ -207,6 +135,7 @@ export function buildProtocolDatasetContract(dataset, options = {}) {
|
|
|
207
135
|
operators: [...(filter.operators ?? SEMANTIC_FILTER_OPERATORS)],
|
|
208
136
|
...(filter.label !== undefined ? { label: filter.label } : {}),
|
|
209
137
|
...(filter.description !== undefined ? { description: filter.description } : {}),
|
|
138
|
+
...toProtocolSemanticMetadata(filter),
|
|
210
139
|
})).sort(byName),
|
|
211
140
|
metrics,
|
|
212
141
|
relationships: Object.entries(dataset.relationships).map(([name, relationship]) => ({
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuilds executable datasets from a portable deployment contract.
|
|
3
|
+
*
|
|
4
|
+
* The inverse of `protocol-adapter.ts`. Decision 0005 chose portable native
|
|
5
|
+
* execution, which means a runtime resolves a dataset from the validated active
|
|
6
|
+
* contract and plans the query with the existing semantic planner — no customer
|
|
7
|
+
* module is loaded. This is the function that makes the contract executable.
|
|
8
|
+
*
|
|
9
|
+
* Rehydration deliberately routes through the public `dataset()` factory rather
|
|
10
|
+
* than assembling an instance by hand, so a rebuilt dataset is constructed by
|
|
11
|
+
* exactly the same code path as an authored one. That is what makes the
|
|
12
|
+
* byte-identical SQL claim mitigated by construction rather than by review.
|
|
13
|
+
*/
|
|
14
|
+
import type { ProtocolDatasetContract } from '@hypequery/protocol';
|
|
15
|
+
import type { AnyDatasetInstance, DatasetCachePolicy, MetricHandle } from './types.js';
|
|
16
|
+
/** A rebuilt dataset in the registry shape Serve, MCP, and the planner accept. */
|
|
17
|
+
export type RehydratedDataset = AnyDatasetInstance & {
|
|
18
|
+
readonly metrics: Readonly<Record<string, MetricHandle>>;
|
|
19
|
+
};
|
|
20
|
+
export interface RehydrateProtocolDatasetsOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Cache policy for the rebuilt datasets. Not carried by the contract — it is
|
|
23
|
+
* an operational concern of whoever runs the query, not of the deployment.
|
|
24
|
+
*/
|
|
25
|
+
readonly cache?: DatasetCachePolicy;
|
|
26
|
+
/**
|
|
27
|
+
* What to do with a metric portable execution cannot rebuild.
|
|
28
|
+
*
|
|
29
|
+
* `throw` (the default) refuses the whole contract, which is right when a
|
|
30
|
+
* caller is checking whether a deployment is fully portable. `skip` omits
|
|
31
|
+
* just that metric, which is what a runtime wants: one derived metric must
|
|
32
|
+
* not make every other dataset in the deployment unexecutable. A caller that
|
|
33
|
+
* skips is responsible for refusing the skipped target at the point of use.
|
|
34
|
+
*/
|
|
35
|
+
readonly onUnsupportedMetric?: 'throw' | 'skip';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Thrown when a contract describes something portable execution cannot rebuild.
|
|
39
|
+
*
|
|
40
|
+
* Callers map this onto the `unsupported-capability` invocation failure rather
|
|
41
|
+
* than executing an approximation, as decision 0005 requires.
|
|
42
|
+
*/
|
|
43
|
+
export declare class UnsupportedContractFeatureError extends Error {
|
|
44
|
+
readonly dataset: string;
|
|
45
|
+
readonly feature: string;
|
|
46
|
+
constructor(datasetName: string, feature: string, detail: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Rebuild every dataset in a validated deployment contract.
|
|
50
|
+
*
|
|
51
|
+
* Datasets are returned keyed by contract name with their named metrics
|
|
52
|
+
* attached, which is the registry shape `getDatasetCatalog`,
|
|
53
|
+
* `projectAgentSafeCatalog`, and `DatasetClient` already accept.
|
|
54
|
+
*/
|
|
55
|
+
export declare function rehydrateProtocolDatasets(contracts: readonly ProtocolDatasetContract[], options?: RehydrateProtocolDatasetsOptions): Readonly<Record<string, RehydratedDataset>>;
|
|
56
|
+
//# sourceMappingURL=protocol-rehydrate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-rehydrate.d.ts","sourceRoot":"","sources":["../src/protocol-rehydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,uBAAuB,EAKxB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAKlB,YAAY,EAIb,MAAM,YAAY,CAAC;AAMpB,kFAAkF;AAClF,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG;IACnD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC1D,CAAC;AAEF,MAAM,WAAW,gCAAgC;IAC/C;;;OAGG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CACjD;AAED;;;;;GAKG;AACH,qBAAa,+BAAgC,SAAQ,KAAK;IACxD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAMjE;AAwOD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,SAAS,uBAAuB,EAAE,EAC7C,OAAO,GAAE,gCAAqC,GAC7C,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAqE7C"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuilds executable datasets from a portable deployment contract.
|
|
3
|
+
*
|
|
4
|
+
* The inverse of `protocol-adapter.ts`. Decision 0005 chose portable native
|
|
5
|
+
* execution, which means a runtime resolves a dataset from the validated active
|
|
6
|
+
* contract and plans the query with the existing semantic planner — no customer
|
|
7
|
+
* module is loaded. This is the function that makes the contract executable.
|
|
8
|
+
*
|
|
9
|
+
* Rehydration deliberately routes through the public `dataset()` factory rather
|
|
10
|
+
* than assembling an instance by hand, so a rebuilt dataset is constructed by
|
|
11
|
+
* exactly the same code path as an authored one. That is what makes the
|
|
12
|
+
* byte-identical SQL claim mitigated by construction rather than by review.
|
|
13
|
+
*/
|
|
14
|
+
import { dataset } from './dataset.js';
|
|
15
|
+
import { snapshotSemanticMetadata } from './utils/semantic-metadata.js';
|
|
16
|
+
import { withContractCapabilities } from './utils/protocol-metric-capabilities.js';
|
|
17
|
+
import { rehydrateDerivedFormula } from './utils/protocol-rehydrate-derivation.js';
|
|
18
|
+
import { rehydrateMeasureFilter } from './utils/protocol-rehydrate-filters.js';
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when a contract describes something portable execution cannot rebuild.
|
|
21
|
+
*
|
|
22
|
+
* Callers map this onto the `unsupported-capability` invocation failure rather
|
|
23
|
+
* than executing an approximation, as decision 0005 requires.
|
|
24
|
+
*/
|
|
25
|
+
export class UnsupportedContractFeatureError extends Error {
|
|
26
|
+
dataset;
|
|
27
|
+
feature;
|
|
28
|
+
constructor(datasetName, feature, detail) {
|
|
29
|
+
super(`Cannot rebuild ${feature} on dataset "${datasetName}": ${detail}`);
|
|
30
|
+
this.name = 'UnsupportedContractFeatureError';
|
|
31
|
+
this.dataset = datasetName;
|
|
32
|
+
this.feature = feature;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function rehydrateDimensions(contract) {
|
|
36
|
+
const dimensions = {};
|
|
37
|
+
for (const dimension of contract.dimensions) {
|
|
38
|
+
const name = String(dimension.name);
|
|
39
|
+
const source = dimension.source;
|
|
40
|
+
dimensions[name] = {
|
|
41
|
+
__type: 'field_definition',
|
|
42
|
+
...snapshotSemanticMetadata(dimension),
|
|
43
|
+
fieldType: dimension.type,
|
|
44
|
+
label: dimension.label,
|
|
45
|
+
description: dimension.description,
|
|
46
|
+
// A column source always carries its column, including when it matches
|
|
47
|
+
// the dimension name — the forward adapter defaults it, so dropping it
|
|
48
|
+
// here would still round-trip but would lose an authored mapping.
|
|
49
|
+
column: source.kind === 'column' ? source.column : undefined,
|
|
50
|
+
sql: source.kind === 'column' ? undefined : source.sql,
|
|
51
|
+
dependencies: source.kind === 'column' ? undefined : [...source.dependencies],
|
|
52
|
+
filterable: dimension.filterable,
|
|
53
|
+
groupable: dimension.groupable,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return dimensions;
|
|
57
|
+
}
|
|
58
|
+
function rehydrateMeasure(datasetName, measure) {
|
|
59
|
+
const filters = measure.filters.map((expression, index) => rehydrateMeasureFilter(expression, () => new UnsupportedContractFeatureError(datasetName, `measure "${String(measure.name)}"`, `fixed filter ${index} is not a field/operator/value comparison`)));
|
|
60
|
+
return {
|
|
61
|
+
__type: 'measure_definition',
|
|
62
|
+
...snapshotSemanticMetadata(measure),
|
|
63
|
+
aggregation: measure.aggregation,
|
|
64
|
+
field: String(measure.field),
|
|
65
|
+
...(measure.argField !== undefined ? { argField: String(measure.argField) } : {}),
|
|
66
|
+
...(measure.level !== undefined ? { level: measure.level } : {}),
|
|
67
|
+
...(measure.sql !== undefined
|
|
68
|
+
? { sql: measure.sql.sql, dependencies: [...measure.sql.dependencies] }
|
|
69
|
+
: {}),
|
|
70
|
+
...(measure.label !== undefined ? { label: measure.label } : {}),
|
|
71
|
+
...(measure.description !== undefined ? { description: measure.description } : {}),
|
|
72
|
+
...(filters.length > 0 ? { filters: filters } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function rehydrateFilters(contract) {
|
|
76
|
+
const filters = {};
|
|
77
|
+
for (const filter of contract.filters) {
|
|
78
|
+
filters[String(filter.name)] = {
|
|
79
|
+
__type: 'filter_definition',
|
|
80
|
+
...snapshotSemanticMetadata(filter),
|
|
81
|
+
field: String(filter.field),
|
|
82
|
+
operators: [...filter.operators],
|
|
83
|
+
...(filter.label !== undefined ? { label: filter.label } : {}),
|
|
84
|
+
...(filter.description !== undefined ? { description: filter.description } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return filters;
|
|
88
|
+
}
|
|
89
|
+
function rehydrateRelationships(contract, resolve) {
|
|
90
|
+
const relationships = {};
|
|
91
|
+
for (const relationship of contract.relationships) {
|
|
92
|
+
const target = String(relationship.target);
|
|
93
|
+
relationships[String(relationship.name)] = {
|
|
94
|
+
__type: 'relationship',
|
|
95
|
+
kind: relationship.kind,
|
|
96
|
+
// Resolved on call: datasets in one contract may reference each other, so
|
|
97
|
+
// the target may not be built yet when this relationship is created.
|
|
98
|
+
target: () => resolve(target),
|
|
99
|
+
from: String(relationship.from),
|
|
100
|
+
to: String(relationship.to),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return relationships;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Identity of an aggregation, used to bind a metric back to the measure it was
|
|
107
|
+
* built from.
|
|
108
|
+
*
|
|
109
|
+
* Aggregation and field alone are not enough: two measures may share both and
|
|
110
|
+
* differ only by their fixed filters — `sum(amount)` and `sum(amount) where
|
|
111
|
+
* status = 'paid'`. Matching on the looser key binds the metric to whichever
|
|
112
|
+
* measure happens to sort first and silently changes the SQL it emits.
|
|
113
|
+
*/
|
|
114
|
+
function aggregationKey(value) {
|
|
115
|
+
return JSON.stringify([
|
|
116
|
+
value.aggregation,
|
|
117
|
+
String(value.field),
|
|
118
|
+
value.argField === undefined ? null : String(value.argField),
|
|
119
|
+
value.level ?? null,
|
|
120
|
+
value.filters ?? [],
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The declared measure an aggregate expression was built from.
|
|
125
|
+
*
|
|
126
|
+
* A metric's expression names an aggregation and a field, not the measure that
|
|
127
|
+
* produced it, so the measure is recovered by matching aggregation identity.
|
|
128
|
+
*/
|
|
129
|
+
function measureForAggregate(instance, contract, subject, expression) {
|
|
130
|
+
if (expression.kind !== 'aggregate') {
|
|
131
|
+
throw new UnsupportedContractFeatureError(instance.name, subject, `expected an aggregate expression, received "${expression.kind}"`);
|
|
132
|
+
}
|
|
133
|
+
const field = String(expression.field);
|
|
134
|
+
const candidates = contract.measures.filter(measure => (aggregationKey(measure) === aggregationKey(expression)));
|
|
135
|
+
if (candidates.length === 0) {
|
|
136
|
+
throw new UnsupportedContractFeatureError(instance.name, subject, `no declared measure matches ${expression.aggregation}(${field})`);
|
|
137
|
+
}
|
|
138
|
+
// A metric expression carries no raw SQL, so two measures that share an
|
|
139
|
+
// aggregation identity but override SQL differently are indistinguishable in
|
|
140
|
+
// contract v1. Binding to either would be a guess at which SQL to emit.
|
|
141
|
+
const distinctSql = new Set(candidates.map(measure => measure.sql?.sql ?? null));
|
|
142
|
+
if (distinctSql.size > 1) {
|
|
143
|
+
throw new UnsupportedContractFeatureError(instance.name, subject, `${candidates.map(measure => `"${String(measure.name)}"`).join(' and ')} share `
|
|
144
|
+
+ `${expression.aggregation}(${field}) but emit different SQL, so the contract cannot `
|
|
145
|
+
+ 'say which one this metric was built from');
|
|
146
|
+
}
|
|
147
|
+
return String(candidates[0].name);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Rebuilds the `uses` map and formula of a derived metric.
|
|
151
|
+
*
|
|
152
|
+
* Each input becomes a base metric named by its alias, because that alias is
|
|
153
|
+
* what both plan builders emit as the column of the intermediate aggregate.
|
|
154
|
+
*/
|
|
155
|
+
function derivedMetricConfig(instance, contract, subject, derivation) {
|
|
156
|
+
const unsupported = (reason) => new UnsupportedContractFeatureError(instance.name, subject, reason);
|
|
157
|
+
const uses = Object.fromEntries(derivation.inputs.map(input => {
|
|
158
|
+
const alias = String(input.alias);
|
|
159
|
+
return [
|
|
160
|
+
alias,
|
|
161
|
+
instance.metric(alias, {
|
|
162
|
+
measure: measureForAggregate(instance, contract, `${subject} input "${alias}"`, input.expression),
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
}));
|
|
166
|
+
return { uses, formula: rehydrateDerivedFormula(derivation, unsupported) };
|
|
167
|
+
}
|
|
168
|
+
function rehydrateMetric(instance, contract, metric) {
|
|
169
|
+
const name = String(metric.name);
|
|
170
|
+
const subject = `metric "${name}"`;
|
|
171
|
+
const derived = metric.expression.kind !== 'aggregate';
|
|
172
|
+
if (derived && metric.derivation === undefined) {
|
|
173
|
+
// The inlined expression states what a derived metric means but drops the
|
|
174
|
+
// aliases its SQL is written in terms of. Without them a rebuild computes
|
|
175
|
+
// the same number through different SQL, which decision 0005 excludes.
|
|
176
|
+
throw new UnsupportedContractFeatureError(instance.name, subject, 'a derived metric requires its authored formula, which this contract does not carry');
|
|
177
|
+
}
|
|
178
|
+
const metadata = {
|
|
179
|
+
...snapshotSemanticMetadata(metric),
|
|
180
|
+
...(metric.label !== undefined ? { label: metric.label } : {}),
|
|
181
|
+
...(metric.description !== undefined ? { description: metric.description } : {}),
|
|
182
|
+
};
|
|
183
|
+
const base = (metric.derivation === undefined
|
|
184
|
+
? instance.metric(name, {
|
|
185
|
+
measure: measureForAggregate(instance, contract, subject, metric.expression),
|
|
186
|
+
...metadata,
|
|
187
|
+
})
|
|
188
|
+
: instance.metric(name, {
|
|
189
|
+
...derivedMetricConfig(instance, contract, subject, metric.derivation),
|
|
190
|
+
...metadata,
|
|
191
|
+
}));
|
|
192
|
+
const handle = metric.grain === undefined
|
|
193
|
+
? base
|
|
194
|
+
: base.by(metric.grain);
|
|
195
|
+
return withContractCapabilities(handle, metric);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Rebuild every dataset in a validated deployment contract.
|
|
199
|
+
*
|
|
200
|
+
* Datasets are returned keyed by contract name with their named metrics
|
|
201
|
+
* attached, which is the registry shape `getDatasetCatalog`,
|
|
202
|
+
* `projectAgentSafeCatalog`, and `DatasetClient` already accept.
|
|
203
|
+
*/
|
|
204
|
+
export function rehydrateProtocolDatasets(contracts, options = {}) {
|
|
205
|
+
const instances = new Map();
|
|
206
|
+
const registry = {};
|
|
207
|
+
// Relationship targets resolve to the published entry, not the bare instance
|
|
208
|
+
// built below, so a caller that follows a relationship lands on the same
|
|
209
|
+
// object the registry exposes. Safe because `target()` is only called after
|
|
210
|
+
// the registry is complete.
|
|
211
|
+
const resolve = (target) => {
|
|
212
|
+
const instance = registry[target] ?? instances.get(target);
|
|
213
|
+
if (!instance) {
|
|
214
|
+
// The deployment validator already rejects a dangling relationship, so
|
|
215
|
+
// reaching this means the caller passed a partial contract.
|
|
216
|
+
throw new UnsupportedContractFeatureError(target, 'relationship target', `dataset "${target}" is not part of the supplied contract`);
|
|
217
|
+
}
|
|
218
|
+
return instance;
|
|
219
|
+
};
|
|
220
|
+
for (const contract of contracts) {
|
|
221
|
+
const name = String(contract.name);
|
|
222
|
+
const instance = dataset(name, {
|
|
223
|
+
source: contract.source,
|
|
224
|
+
...snapshotSemanticMetadata(contract),
|
|
225
|
+
...(contract.description !== undefined ? { description: contract.description } : {}),
|
|
226
|
+
...(contract.owner !== undefined ? { owner: contract.owner } : {}),
|
|
227
|
+
...(contract.freshness !== undefined ? { freshness: { ...contract.freshness } } : {}),
|
|
228
|
+
...(contract.defaults !== undefined ? { defaults: {
|
|
229
|
+
...contract.defaults,
|
|
230
|
+
...(contract.defaults.dimensions !== undefined ? { dimensions: [...contract.defaults.dimensions] } : {}),
|
|
231
|
+
} } : {}),
|
|
232
|
+
...(contract.tenant.kind === 'required' ? { tenantKey: contract.tenant.field } : {}),
|
|
233
|
+
...(contract.timeField !== undefined ? { timeKey: String(contract.timeField) } : {}),
|
|
234
|
+
dimensions: rehydrateDimensions(contract),
|
|
235
|
+
measures: Object.fromEntries(contract.measures.map(measure => [
|
|
236
|
+
String(measure.name),
|
|
237
|
+
rehydrateMeasure(name, measure),
|
|
238
|
+
])),
|
|
239
|
+
filters: rehydrateFilters(contract),
|
|
240
|
+
relationships: rehydrateRelationships(contract, resolve),
|
|
241
|
+
...(contract.limits !== undefined ? { limits: { ...contract.limits } } : {}),
|
|
242
|
+
...(options.cache !== undefined ? { cache: options.cache } : {}),
|
|
243
|
+
});
|
|
244
|
+
instances.set(name, instance);
|
|
245
|
+
}
|
|
246
|
+
for (const contract of contracts) {
|
|
247
|
+
const name = String(contract.name);
|
|
248
|
+
const instance = instances.get(name);
|
|
249
|
+
const metrics = {};
|
|
250
|
+
for (const metric of contract.metrics) {
|
|
251
|
+
try {
|
|
252
|
+
metrics[String(metric.name)] = rehydrateMetric(instance, contract, metric);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (options.onUnsupportedMetric !== 'skip'
|
|
256
|
+
|| !(error instanceof UnsupportedContractFeatureError)) {
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
registry[name] = Object.assign(Object.create(Object.getPrototypeOf(instance)), instance, { metrics });
|
|
262
|
+
}
|
|
263
|
+
return registry;
|
|
264
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, MetricHandle } from '../../types.js';
|
|
2
|
+
import type { DatasetPublisherNode } from '../node.js';
|
|
3
|
+
export declare class DatasetPublicationFeature {
|
|
4
|
+
private readonly publisher;
|
|
5
|
+
constructor(publisher: {
|
|
6
|
+
getPublicationNode(): DatasetPublisherNode;
|
|
7
|
+
});
|
|
8
|
+
add(dataset: AnyDatasetInstance, alias: string, metrics: Readonly<Record<string, MetricHandle>>): DatasetPublisherNode;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=dataset-publication.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-publication.d.ts","sourceRoot":"","sources":["../../../src/publishing/features/dataset-publication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,qBAAa,yBAAyB;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE;QACtC,kBAAkB,IAAI,oBAAoB,CAAC;KAC5C;IAED,GAAG,CACD,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,GAC9C,oBAAoB;CA8BxB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { aliasMetricHandle } from '../../utils/metric-alias.js';
|
|
2
|
+
import { assertPublishedName } from '../../utils/published-name.js';
|
|
3
|
+
export class DatasetPublicationFeature {
|
|
4
|
+
publisher;
|
|
5
|
+
constructor(publisher) {
|
|
6
|
+
this.publisher = publisher;
|
|
7
|
+
}
|
|
8
|
+
add(dataset, alias, metrics) {
|
|
9
|
+
const node = this.publisher.getPublicationNode();
|
|
10
|
+
assertPublishedName(alias, 'dataset');
|
|
11
|
+
if (node.entries.some(entry => entry.alias === alias)) {
|
|
12
|
+
throw new Error(`Dataset alias "${alias}" is already published.`);
|
|
13
|
+
}
|
|
14
|
+
if (node.entries.some(entry => entry.dataset.name === dataset.name)) {
|
|
15
|
+
throw new Error(`Dataset "${dataset.name}" is already published.`);
|
|
16
|
+
}
|
|
17
|
+
const publishedMetrics = {};
|
|
18
|
+
for (const [metricAlias, metric] of Object.entries(metrics)) {
|
|
19
|
+
assertPublishedName(metricAlias, 'metric');
|
|
20
|
+
const ref = metric.__type === 'grained_metric_ref' ? metric.metric : metric;
|
|
21
|
+
if (ref.datasetName !== dataset.name) {
|
|
22
|
+
throw new Error(`Metric "${metricAlias}" belongs to dataset "${ref.datasetName}", expected "${dataset.name}".`);
|
|
23
|
+
}
|
|
24
|
+
publishedMetrics[metricAlias] = aliasMetricHandle(metric, metricAlias, alias);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
kind: 'dataset-publisher',
|
|
28
|
+
entries: [
|
|
29
|
+
...node.entries,
|
|
30
|
+
{ alias, dataset, metrics: publishedMetrics },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, MetricHandle } from '../types.js';
|
|
2
|
+
export interface DatasetPublicationEntry {
|
|
3
|
+
readonly alias: string;
|
|
4
|
+
readonly dataset: AnyDatasetInstance;
|
|
5
|
+
readonly metrics: Readonly<Record<string, MetricHandle>>;
|
|
6
|
+
}
|
|
7
|
+
export interface DatasetPublisherNode {
|
|
8
|
+
readonly kind: 'dataset-publisher';
|
|
9
|
+
readonly entries: readonly DatasetPublicationEntry[];
|
|
10
|
+
}
|
|
11
|
+
export declare function createDatasetPublisherNode(entries?: readonly DatasetPublicationEntry[]): DatasetPublisherNode;
|
|
12
|
+
export declare function cloneDatasetPublisherNode(node: DatasetPublisherNode): DatasetPublisherNode;
|
|
13
|
+
export declare function materializeDatasetPublisherNode(node: DatasetPublisherNode): Readonly<Record<string, AnyDatasetInstance & {
|
|
14
|
+
readonly metrics: Readonly<Record<string, MetricHandle>>;
|
|
15
|
+
}>>;
|
|
16
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/publishing/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMpE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC1D;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;CACtD;AAED,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,SAAS,uBAAuB,EAAO,GAC/C,oBAAoB,CAStB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,oBAAoB,GAAG,oBAAoB,CAE1F;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,oBAAoB,GACzB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,GAAG;IAC9C,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;CAC1D,CAAC,CAAC,CAyBF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { publishedAliasesByName, rewirePublishedRelationships, } from '../utils/published-relationships.js';
|
|
2
|
+
export function createDatasetPublisherNode(entries = []) {
|
|
3
|
+
return {
|
|
4
|
+
kind: 'dataset-publisher',
|
|
5
|
+
entries: entries.map(entry => ({
|
|
6
|
+
alias: entry.alias,
|
|
7
|
+
dataset: entry.dataset,
|
|
8
|
+
metrics: { ...entry.metrics },
|
|
9
|
+
})),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function cloneDatasetPublisherNode(node) {
|
|
13
|
+
return createDatasetPublisherNode(node.entries);
|
|
14
|
+
}
|
|
15
|
+
export function materializeDatasetPublisherNode(node) {
|
|
16
|
+
const aliasesByName = publishedAliasesByName(node.entries);
|
|
17
|
+
const registry = new Map();
|
|
18
|
+
// Relationships resolve their target through this map rather than capturing
|
|
19
|
+
// an instance, so datasets that reference each other are rewired without an
|
|
20
|
+
// ordering constraint.
|
|
21
|
+
const lookup = (alias) => registry.get(alias);
|
|
22
|
+
const entries = [...node.entries]
|
|
23
|
+
.sort((left, right) => left.alias < right.alias ? -1 : left.alias > right.alias ? 1 : 0)
|
|
24
|
+
.map(entry => {
|
|
25
|
+
const published = Object.freeze({
|
|
26
|
+
...entry.dataset,
|
|
27
|
+
name: entry.alias,
|
|
28
|
+
relationships: rewirePublishedRelationships(entry.dataset.relationships, aliasesByName, lookup),
|
|
29
|
+
metrics: Object.freeze({ ...entry.metrics }),
|
|
30
|
+
});
|
|
31
|
+
registry.set(entry.alias, published);
|
|
32
|
+
return [entry.alias, published];
|
|
33
|
+
});
|
|
34
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AnyDatasetInstance } from '../types.js';
|
|
2
|
+
import { type DatasetPublisherNode } from './node.js';
|
|
3
|
+
import type { AddPublishedDataset, AnyDatasetPublisherState, DatasetPublisherState, PublishedAlias, PublishedMetricMap, PublishDatasetOptions } from './types.js';
|
|
4
|
+
/** Immutable builder for publishing datasets and named metrics to consumers. */
|
|
5
|
+
export declare class DatasetPublisher<TState extends AnyDatasetPublisherState = DatasetPublisherState> {
|
|
6
|
+
private readonly state;
|
|
7
|
+
private readonly node;
|
|
8
|
+
private readonly publication;
|
|
9
|
+
constructor(state: TState, node?: DatasetPublisherNode);
|
|
10
|
+
getPublicationNode(): DatasetPublisherNode;
|
|
11
|
+
private transition;
|
|
12
|
+
publish<TDataset extends AnyDatasetInstance, const TAlias extends string | undefined = undefined, const TMetrics extends PublishedMetricMap<TDataset> = Record<string, never>>(dataset: TDataset, options?: PublishDatasetOptions<TDataset, TAlias, TMetrics>): DatasetPublisher<AddPublishedDataset<TState, TDataset, PublishedAlias<TDataset, TAlias>, TMetrics>>;
|
|
13
|
+
/** Materialize the plain registry shape accepted by existing Serve/MCP APIs. */
|
|
14
|
+
build(): TState['registry'];
|
|
15
|
+
}
|
|
16
|
+
export declare function publishDatasets(): DatasetPublisher<DatasetPublisherState>;
|
|
17
|
+
//# sourceMappingURL=publisher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publisher.d.ts","sourceRoot":"","sources":["../../src/publishing/publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB,gFAAgF;AAChF,qBAAa,gBAAgB,CAC3B,MAAM,SAAS,wBAAwB,GAAG,qBAAqB;IAK7D,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;gBAGrC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,oBAAmD;IAK5E,kBAAkB,IAAI,oBAAoB;IAI1C,OAAO,CAAC,UAAU;IAOlB,OAAO,CACL,QAAQ,SAAS,kBAAkB,EACnC,KAAK,CAAC,MAAM,SAAS,MAAM,GAAG,SAAS,GAAG,SAAS,EACnD,KAAK,CAAC,QAAQ,SAAS,kBAAkB,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAE3E,OAAO,EAAE,QAAQ,EACjB,OAAO,GAAE,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAM,GAC9D,gBAAgB,CAAC,mBAAmB,CACrC,MAAM,EACN,QAAQ,EACR,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,EAChC,QAAQ,CACT,CAAC;IAaF,gFAAgF;IAChF,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC;CAG5B;AAED,wBAAgB,eAAe,IAAI,gBAAgB,CAAC,qBAAqB,CAAC,CAEzE"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DatasetPublicationFeature } from './features/dataset-publication.js';
|
|
2
|
+
import { cloneDatasetPublisherNode, createDatasetPublisherNode, materializeDatasetPublisherNode, } from './node.js';
|
|
3
|
+
/** Immutable builder for publishing datasets and named metrics to consumers. */
|
|
4
|
+
export class DatasetPublisher {
|
|
5
|
+
state;
|
|
6
|
+
node;
|
|
7
|
+
publication;
|
|
8
|
+
constructor(state, node = createDatasetPublisherNode()) {
|
|
9
|
+
this.state = state;
|
|
10
|
+
this.node = node;
|
|
11
|
+
this.publication = new DatasetPublicationFeature(this);
|
|
12
|
+
}
|
|
13
|
+
getPublicationNode() {
|
|
14
|
+
return cloneDatasetPublisherNode(this.node);
|
|
15
|
+
}
|
|
16
|
+
transition(state, node) {
|
|
17
|
+
return new DatasetPublisher(state, node);
|
|
18
|
+
}
|
|
19
|
+
publish(dataset, options = {}) {
|
|
20
|
+
const alias = options.alias ?? dataset.name;
|
|
21
|
+
const node = this.publication.add(dataset, alias, options.metrics ?? {});
|
|
22
|
+
const state = { registry: materializeDatasetPublisherNode(node) };
|
|
23
|
+
return this.transition(state, node);
|
|
24
|
+
}
|
|
25
|
+
/** Materialize the plain registry shape accepted by existing Serve/MCP APIs. */
|
|
26
|
+
build() {
|
|
27
|
+
return this.state.registry;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function publishDatasets() {
|
|
31
|
+
return new DatasetPublisher({ registry: {} });
|
|
32
|
+
}
|