@hypequery/datasets 0.13.6 → 0.14.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 +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +4 -0
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +21 -0
- package/dist/protocol-rehydrate.d.ts +46 -0
- package/dist/protocol-rehydrate.d.ts.map +1 -0
- package/dist/protocol-rehydrate.js +218 -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 +48 -0
- package/dist/semantic-query-schema.d.ts.map +1 -0
- package/dist/semantic-query-schema.js +218 -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/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-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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-source projections behind `projectAgentSafeCatalog()`.
|
|
3
|
+
*
|
|
4
|
+
* Each function maps one dataset representation onto the logical shape an agent
|
|
5
|
+
* may see. They all apply the same rules — drop dimensions that are neither
|
|
6
|
+
* filterable nor groupable, keep only filters whose field survived that drop,
|
|
7
|
+
* keep only metric references that resolve, snapshot semantic metadata, and
|
|
8
|
+
* sort every collection by name — so the projection is deterministic regardless
|
|
9
|
+
* of which source produced it.
|
|
10
|
+
*/
|
|
11
|
+
import type { ProtocolDatasetContract } from '@hypequery/protocol';
|
|
12
|
+
import type { AgentCatalogDataset } from '../agent-catalog.js';
|
|
13
|
+
import type { DatasetDefaults, DatasetFreshness, DatasetLimits, FieldType, SemanticMetadata } from '../types.js';
|
|
14
|
+
/**
|
|
15
|
+
* The structural shape shared by `DatasetCatalog` and `ContractDataset`.
|
|
16
|
+
*
|
|
17
|
+
* Both carry their collections as name-keyed records with the same entry
|
|
18
|
+
* fields, so one projection covers the local-catalog and semantic-contract
|
|
19
|
+
* sources. Only what the projection reads is declared here.
|
|
20
|
+
*/
|
|
21
|
+
export interface RecordShapedDataset extends SemanticMetadata {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly description?: string;
|
|
24
|
+
readonly freshness?: DatasetFreshness;
|
|
25
|
+
readonly owner?: string;
|
|
26
|
+
readonly defaults?: DatasetDefaults;
|
|
27
|
+
readonly timeKey?: string;
|
|
28
|
+
readonly dimensions: Readonly<Record<string, SemanticMetadata & {
|
|
29
|
+
readonly type: FieldType;
|
|
30
|
+
readonly label?: string;
|
|
31
|
+
readonly description?: string;
|
|
32
|
+
readonly filterable: boolean;
|
|
33
|
+
readonly groupable: boolean;
|
|
34
|
+
}>>;
|
|
35
|
+
readonly measures: Readonly<Record<string, SemanticMetadata & {
|
|
36
|
+
readonly label?: string;
|
|
37
|
+
readonly description?: string;
|
|
38
|
+
}>>;
|
|
39
|
+
readonly metrics: Readonly<Record<string, SemanticMetadata & {
|
|
40
|
+
readonly label?: string;
|
|
41
|
+
readonly description?: string;
|
|
42
|
+
readonly dimensions: readonly string[];
|
|
43
|
+
readonly filters: readonly string[];
|
|
44
|
+
readonly grains: readonly string[];
|
|
45
|
+
readonly grain?: string;
|
|
46
|
+
}>>;
|
|
47
|
+
readonly filters: Readonly<Record<string, SemanticMetadata & {
|
|
48
|
+
readonly field: string;
|
|
49
|
+
readonly label?: string;
|
|
50
|
+
readonly description?: string;
|
|
51
|
+
readonly operators?: readonly string[];
|
|
52
|
+
readonly valueType?: FieldType;
|
|
53
|
+
}>>;
|
|
54
|
+
readonly relationships: Readonly<Record<string, {
|
|
55
|
+
readonly target: string;
|
|
56
|
+
readonly queryable: boolean;
|
|
57
|
+
readonly fields: readonly string[];
|
|
58
|
+
}>>;
|
|
59
|
+
readonly limits?: DatasetLimits;
|
|
60
|
+
}
|
|
61
|
+
export declare function optionalText<T extends {
|
|
62
|
+
label?: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
}>(value: T): Pick<T, 'label' | 'description'>;
|
|
65
|
+
/** Reduces a limits object to the four keys the agent-safe catalog publishes. */
|
|
66
|
+
export declare function normalizedLimits(limits: DatasetLimits | undefined): DatasetLimits;
|
|
67
|
+
export declare function datasetDescription(name: string, description?: string): string;
|
|
68
|
+
/** Projects a `DatasetCatalog` or `ContractDataset` — they share one shape. */
|
|
69
|
+
export declare function recordDatasetToAgentDataset(dataset: RecordShapedDataset): AgentCatalogDataset;
|
|
70
|
+
/**
|
|
71
|
+
* Projects a portable deployment contract dataset.
|
|
72
|
+
*
|
|
73
|
+
* Kept separate from `recordDatasetToAgentDataset`: the contract carries its
|
|
74
|
+
* collections as arrays of named entries, and relationship fields are not
|
|
75
|
+
* stored — they are derived from the target dataset's publishable dimensions.
|
|
76
|
+
*/
|
|
77
|
+
export declare function protocolDatasetToAgentDataset(dataset: ProtocolDatasetContract, datasets: ReadonlyMap<string, ProtocolDatasetContract>): AgentCatalogDataset;
|
|
78
|
+
//# sourceMappingURL=agent-catalog-projection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-catalog-projection.d.ts","sourceRoot":"","sources":["../../src/utils/agent-catalog-projection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-source projections behind `projectAgentSafeCatalog()`.
|
|
3
|
+
*
|
|
4
|
+
* Each function maps one dataset representation onto the logical shape an agent
|
|
5
|
+
* may see. They all apply the same rules — drop dimensions that are neither
|
|
6
|
+
* filterable nor groupable, keep only filters whose field survived that drop,
|
|
7
|
+
* keep only metric references that resolve, snapshot semantic metadata, and
|
|
8
|
+
* sort every collection by name — so the projection is deterministic regardless
|
|
9
|
+
* of which source produced it.
|
|
10
|
+
*/
|
|
11
|
+
import { compareStrings, uniqueSorted } from './canonical-json.js';
|
|
12
|
+
import { snapshotSemanticMetadata } from './semantic-metadata.js';
|
|
13
|
+
export function optionalText(value) {
|
|
14
|
+
return {
|
|
15
|
+
...(value.label !== undefined ? { label: value.label } : {}),
|
|
16
|
+
...(value.description !== undefined ? { description: value.description } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Reduces a limits object to the four keys the agent-safe catalog publishes. */
|
|
20
|
+
export function normalizedLimits(limits) {
|
|
21
|
+
return {
|
|
22
|
+
...(limits?.maxDimensions !== undefined ? { maxDimensions: limits.maxDimensions } : {}),
|
|
23
|
+
...(limits?.maxMeasures !== undefined ? { maxMeasures: limits.maxMeasures } : {}),
|
|
24
|
+
...(limits?.maxFilters !== undefined ? { maxFilters: limits.maxFilters } : {}),
|
|
25
|
+
...(limits?.maxResultSize !== undefined ? { maxResultSize: limits.maxResultSize } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function datasetDescription(name, description) {
|
|
29
|
+
return description ?? `${name} analytics dataset.`;
|
|
30
|
+
}
|
|
31
|
+
function snapshotDefaults(defaults) {
|
|
32
|
+
return {
|
|
33
|
+
...(defaults.dimensions !== undefined
|
|
34
|
+
? { dimensions: uniqueSorted(defaults.dimensions) }
|
|
35
|
+
: {}),
|
|
36
|
+
...(defaults.timeGrain !== undefined ? { timeGrain: defaults.timeGrain } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** The dataset-level metadata every source projects the same way. */
|
|
40
|
+
function datasetMetadata(dataset) {
|
|
41
|
+
return {
|
|
42
|
+
...snapshotSemanticMetadata(dataset),
|
|
43
|
+
...(dataset.freshness !== undefined ? { freshness: { ...dataset.freshness } } : {}),
|
|
44
|
+
...(dataset.owner !== undefined ? { owner: dataset.owner } : {}),
|
|
45
|
+
...(dataset.defaults !== undefined ? { defaults: snapshotDefaults(dataset.defaults) } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function sortedByName(items) {
|
|
49
|
+
return items.sort((left, right) => compareStrings(left.name, right.name));
|
|
50
|
+
}
|
|
51
|
+
/** Projects a `DatasetCatalog` or `ContractDataset` — they share one shape. */
|
|
52
|
+
export function recordDatasetToAgentDataset(dataset) {
|
|
53
|
+
const dimensions = sortedByName(Object.entries(dataset.dimensions)
|
|
54
|
+
.filter(([, dimension]) => dimension.filterable || dimension.groupable)
|
|
55
|
+
.map(([name, dimension]) => ({
|
|
56
|
+
name,
|
|
57
|
+
type: dimension.type,
|
|
58
|
+
...optionalText(dimension),
|
|
59
|
+
...snapshotSemanticMetadata(dimension),
|
|
60
|
+
filterable: dimension.filterable,
|
|
61
|
+
groupable: dimension.groupable,
|
|
62
|
+
})));
|
|
63
|
+
const dimensionNames = new Set(dimensions.map(dimension => dimension.name));
|
|
64
|
+
// A filter is only publishable when its backing dimension survived above;
|
|
65
|
+
// otherwise it would name a field the agent cannot see or select.
|
|
66
|
+
const filterNames = new Set(Object.entries(dataset.filters)
|
|
67
|
+
.filter(([, filter]) => filter.valueType !== undefined && dimensionNames.has(filter.field))
|
|
68
|
+
.map(([name]) => name));
|
|
69
|
+
return {
|
|
70
|
+
name: dataset.name,
|
|
71
|
+
description: datasetDescription(dataset.name, dataset.description),
|
|
72
|
+
...datasetMetadata(dataset),
|
|
73
|
+
timeDimension: dataset.timeKey !== undefined && dimensionNames.has(dataset.timeKey)
|
|
74
|
+
? dataset.timeKey
|
|
75
|
+
: null,
|
|
76
|
+
dimensions,
|
|
77
|
+
measures: sortedByName(Object.entries(dataset.measures).map(([name, measure]) => ({
|
|
78
|
+
name,
|
|
79
|
+
...optionalText(measure),
|
|
80
|
+
...snapshotSemanticMetadata(measure),
|
|
81
|
+
}))),
|
|
82
|
+
metrics: sortedByName(Object.entries(dataset.metrics).map(([name, metric]) => ({
|
|
83
|
+
name,
|
|
84
|
+
...optionalText(metric),
|
|
85
|
+
...snapshotSemanticMetadata(metric),
|
|
86
|
+
dimensions: uniqueSorted(metric.dimensions.filter(item => dimensionNames.has(item))),
|
|
87
|
+
filters: uniqueSorted(metric.filters.filter(item => filterNames.has(item))),
|
|
88
|
+
grains: uniqueSorted(metric.grains),
|
|
89
|
+
...(metric.grain !== undefined ? { grain: metric.grain } : {}),
|
|
90
|
+
}))),
|
|
91
|
+
filters: sortedByName(Object.entries(dataset.filters)
|
|
92
|
+
.filter(([name]) => filterNames.has(name))
|
|
93
|
+
.map(([name, filter]) => ({
|
|
94
|
+
name,
|
|
95
|
+
type: filter.valueType,
|
|
96
|
+
...optionalText(filter),
|
|
97
|
+
...snapshotSemanticMetadata(filter),
|
|
98
|
+
operators: uniqueSorted(filter.operators ?? []),
|
|
99
|
+
}))),
|
|
100
|
+
relationships: sortedByName(Object.entries(dataset.relationships)
|
|
101
|
+
.filter(([, relationship]) => relationship.queryable)
|
|
102
|
+
.map(([name, relationship]) => ({
|
|
103
|
+
name,
|
|
104
|
+
target: relationship.target,
|
|
105
|
+
fields: uniqueSorted(relationship.fields),
|
|
106
|
+
}))),
|
|
107
|
+
limits: normalizedLimits(dataset.limits),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Projects a portable deployment contract dataset.
|
|
112
|
+
*
|
|
113
|
+
* Kept separate from `recordDatasetToAgentDataset`: the contract carries its
|
|
114
|
+
* collections as arrays of named entries, and relationship fields are not
|
|
115
|
+
* stored — they are derived from the target dataset's publishable dimensions.
|
|
116
|
+
*/
|
|
117
|
+
export function protocolDatasetToAgentDataset(dataset, datasets) {
|
|
118
|
+
const dimensions = sortedByName(dataset.dimensions
|
|
119
|
+
.filter(dimension => dimension.filterable || dimension.groupable)
|
|
120
|
+
.map(dimension => ({
|
|
121
|
+
name: dimension.name,
|
|
122
|
+
type: dimension.type,
|
|
123
|
+
...optionalText(dimension),
|
|
124
|
+
...snapshotSemanticMetadata(dimension),
|
|
125
|
+
filterable: dimension.filterable,
|
|
126
|
+
groupable: dimension.groupable,
|
|
127
|
+
})));
|
|
128
|
+
const dimensionNames = new Set(dimensions.map(dimension => dimension.name));
|
|
129
|
+
const dimensionTypes = new Map(dataset.dimensions.map(dimension => [dimension.name, dimension.type]));
|
|
130
|
+
const filterNames = new Set(dataset.filters
|
|
131
|
+
.filter(filter => dimensionNames.has(String(filter.field)))
|
|
132
|
+
.map(filter => String(filter.name)));
|
|
133
|
+
return {
|
|
134
|
+
name: dataset.name,
|
|
135
|
+
description: datasetDescription(dataset.name, dataset.description),
|
|
136
|
+
...datasetMetadata(dataset),
|
|
137
|
+
timeDimension: dataset.timeField !== undefined && dimensionNames.has(String(dataset.timeField))
|
|
138
|
+
? String(dataset.timeField)
|
|
139
|
+
: null,
|
|
140
|
+
dimensions,
|
|
141
|
+
measures: sortedByName(dataset.measures.map(measure => ({
|
|
142
|
+
name: measure.name,
|
|
143
|
+
...optionalText(measure),
|
|
144
|
+
...snapshotSemanticMetadata(measure),
|
|
145
|
+
}))),
|
|
146
|
+
metrics: sortedByName(dataset.metrics.map(metric => ({
|
|
147
|
+
name: metric.name,
|
|
148
|
+
...optionalText(metric),
|
|
149
|
+
...snapshotSemanticMetadata(metric),
|
|
150
|
+
dimensions: uniqueSorted(metric.dimensions.filter(name => dimensionNames.has(String(name)))),
|
|
151
|
+
filters: uniqueSorted(metric.filters.filter(name => filterNames.has(String(name)))),
|
|
152
|
+
grains: uniqueSorted(metric.grains),
|
|
153
|
+
...(metric.grain !== undefined ? { grain: metric.grain } : {}),
|
|
154
|
+
}))),
|
|
155
|
+
filters: sortedByName(dataset.filters
|
|
156
|
+
.filter(filter => filterNames.has(String(filter.name)))
|
|
157
|
+
.map((filter) => {
|
|
158
|
+
const type = dimensionTypes.get(String(filter.field));
|
|
159
|
+
return type === undefined ? undefined : {
|
|
160
|
+
name: filter.name,
|
|
161
|
+
type,
|
|
162
|
+
...optionalText(filter),
|
|
163
|
+
...snapshotSemanticMetadata(filter),
|
|
164
|
+
operators: uniqueSorted(filter.operators),
|
|
165
|
+
};
|
|
166
|
+
})
|
|
167
|
+
.filter((filter) => filter !== undefined)),
|
|
168
|
+
relationships: sortedByName(dataset.relationships
|
|
169
|
+
.filter(relationship => relationship.queryable)
|
|
170
|
+
.map(relationship => ({
|
|
171
|
+
name: relationship.name,
|
|
172
|
+
target: relationship.target,
|
|
173
|
+
fields: uniqueSorted((datasets.get(relationship.target)?.dimensions ?? [])
|
|
174
|
+
.filter(dimension => dimension.filterable || dimension.groupable)
|
|
175
|
+
.map(dimension => `${relationship.name}.${dimension.name}`)),
|
|
176
|
+
}))),
|
|
177
|
+
limits: normalizedLimits(dataset.limits),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DatasetCachePolicy, ExecutionContext } from '../types.js';
|
|
2
|
+
import type { SemanticCacheOptions } from '../cache/semantic-query-cache.js';
|
|
3
|
+
/**
|
|
4
|
+
* Client-level cache defaults, as the resolver needs to see them.
|
|
5
|
+
*
|
|
6
|
+
* They have to be resolved here rather than left to the cache: the cache fills a
|
|
7
|
+
* missing `ttlMs` from its own defaults *after* this runs, so a value left
|
|
8
|
+
* undefined here would be filled in unclamped and escape the dataset's ceiling
|
|
9
|
+
* entirely.
|
|
10
|
+
*/
|
|
11
|
+
export type ClientCacheDefaults = Pick<SemanticCacheOptions, 'ttlMs' | 'staleWhileRevalidateMs'>;
|
|
12
|
+
/**
|
|
13
|
+
* Folds a dataset's declared cache policy into one call's cache runtime.
|
|
14
|
+
*
|
|
15
|
+
* Precedence mirrors `resolveCompiledDeadline` in `@hypequery/clickhouse`: the
|
|
16
|
+
* call site may shorten the window but never extend it. A declared `ttlMs`
|
|
17
|
+
* supplies the default when neither the call nor the client does; a declared
|
|
18
|
+
* `maxTtlMs` clamps whatever any of the three asked for.
|
|
19
|
+
*
|
|
20
|
+
* **`maxTtlMs` is a ceiling, not a default.** When no layer supplies a TTL, the
|
|
21
|
+
* result stays uncached — a maximum lifetime is not a reason to start caching
|
|
22
|
+
* something nobody asked to cache.
|
|
23
|
+
*
|
|
24
|
+
* **The ceiling bounds total age, not each window separately.** A cached entry
|
|
25
|
+
* is servable for `ttlMs + staleWhileRevalidateMs`, so clamping the two
|
|
26
|
+
* independently would allow twice the declared maximum. The stale window gets
|
|
27
|
+
* whatever the TTL leaves of the budget.
|
|
28
|
+
*
|
|
29
|
+
* A caller that opted out (`cache: false`, `mode: 'bypass'`) stays opted out.
|
|
30
|
+
* Opting out is a shortening, and a dataset policy is not a way to force a
|
|
31
|
+
* result into a cache the caller declined.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveDatasetCacheRuntime(policy: DatasetCachePolicy | undefined, callerCache: ExecutionContext['cache'], clientDefaults?: ClientCacheDefaults): ExecutionContext['cache'];
|
|
34
|
+
//# sourceMappingURL=dataset-cache-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-cache-policy.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-cache-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,oBAAoB,EAAwB,MAAM,kCAAkC,CAAC;AAEnG;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,oBAAoB,EACpB,OAAO,GAAG,wBAAwB,CACnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,kBAAkB,GAAG,SAAS,EACtC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,EACtC,cAAc,CAAC,EAAE,mBAAmB,GACnC,gBAAgB,CAAC,OAAO,CAAC,CAsC3B"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folds a dataset's declared cache policy into one call's cache runtime.
|
|
3
|
+
*
|
|
4
|
+
* Precedence mirrors `resolveCompiledDeadline` in `@hypequery/clickhouse`: the
|
|
5
|
+
* call site may shorten the window but never extend it. A declared `ttlMs`
|
|
6
|
+
* supplies the default when neither the call nor the client does; a declared
|
|
7
|
+
* `maxTtlMs` clamps whatever any of the three asked for.
|
|
8
|
+
*
|
|
9
|
+
* **`maxTtlMs` is a ceiling, not a default.** When no layer supplies a TTL, the
|
|
10
|
+
* result stays uncached — a maximum lifetime is not a reason to start caching
|
|
11
|
+
* something nobody asked to cache.
|
|
12
|
+
*
|
|
13
|
+
* **The ceiling bounds total age, not each window separately.** A cached entry
|
|
14
|
+
* is servable for `ttlMs + staleWhileRevalidateMs`, so clamping the two
|
|
15
|
+
* independently would allow twice the declared maximum. The stale window gets
|
|
16
|
+
* whatever the TTL leaves of the budget.
|
|
17
|
+
*
|
|
18
|
+
* A caller that opted out (`cache: false`, `mode: 'bypass'`) stays opted out.
|
|
19
|
+
* Opting out is a shortening, and a dataset policy is not a way to force a
|
|
20
|
+
* result into a cache the caller declined.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveDatasetCacheRuntime(policy, callerCache, clientDefaults) {
|
|
23
|
+
if (policy === undefined) {
|
|
24
|
+
return callerCache;
|
|
25
|
+
}
|
|
26
|
+
if (callerCache === false || callerCache?.mode === 'bypass') {
|
|
27
|
+
return callerCache;
|
|
28
|
+
}
|
|
29
|
+
// The value each layer would have produced, most specific first. The client
|
|
30
|
+
// default is included so the ceiling below applies to it too.
|
|
31
|
+
const requestedTtl = callerCache?.ttlMs ?? policy.ttlMs ?? clientDefaults?.ttlMs;
|
|
32
|
+
const requestedStale = callerCache?.staleWhileRevalidateMs ?? clientDefaults?.staleWhileRevalidateMs;
|
|
33
|
+
const resolved = { ...callerCache };
|
|
34
|
+
if (requestedTtl === undefined) {
|
|
35
|
+
// No layer asked for caching. The ceiling does not create it, and there is
|
|
36
|
+
// no default left downstream for it to fall back to.
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
// Set unconditionally once any layer supplied a value, including an explicit
|
|
40
|
+
// `0`. Returning without it would leave the cache to re-derive a TTL from the
|
|
41
|
+
// client default, turning "do not cache this dataset" into the client's TTL.
|
|
42
|
+
resolved.ttlMs =
|
|
43
|
+
policy.maxTtlMs === undefined ? requestedTtl : Math.min(requestedTtl, policy.maxTtlMs);
|
|
44
|
+
if (policy.maxTtlMs !== undefined && resolved.ttlMs > 0) {
|
|
45
|
+
// Set explicitly for the same reason: an undefined stale window would be
|
|
46
|
+
// filled from the client default and layered on an already-clamped TTL.
|
|
47
|
+
resolved.staleWhileRevalidateMs = Math.min(requestedStale ?? 0, Math.max(0, policy.maxTtlMs - resolved.ttlMs));
|
|
48
|
+
}
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { AggregationSpec, AnyDatasetInstance, DerivedMetricSpec, MetricContract, TimeGrain } from '../types.js';
|
|
2
|
-
export declare function buildMetricContract(metricName: string, ds: AnyDatasetInstance, spec: AggregationSpec | DerivedMetricSpec, label?: string, description?: string, grain?: TimeGrain): MetricContract;
|
|
1
|
+
import type { AggregationSpec, AnyDatasetInstance, DerivedMetricSpec, MetricContract, SemanticMetadata, TimeGrain } from '../types.js';
|
|
2
|
+
export declare function buildMetricContract(metricName: string, ds: AnyDatasetInstance, spec: AggregationSpec | DerivedMetricSpec, label?: string, description?: string, metadata?: SemanticMetadata, grain?: TimeGrain): MetricContract;
|
|
3
3
|
//# sourceMappingURL=dataset-contract.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-contract.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,SAAS,EACV,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-contract.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,SAAS,EACV,MAAM,aAAa,CAAC;AAKrB,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,eAAe,GAAG,iBAAiB,EACzC,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,EACpB,QAAQ,GAAE,gBAAqB,EAC/B,KAAK,CAAC,EAAE,SAAS,GAChB,cAAc,CA8BhB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { snapshotSemanticMetadata } from './semantic-metadata.js';
|
|
1
2
|
const ALL_GRAINS = ['day', 'week', 'month', 'quarter', 'year'];
|
|
2
|
-
export function buildMetricContract(metricName, ds, spec, label, description, grain) {
|
|
3
|
+
export function buildMetricContract(metricName, ds, spec, label, description, metadata = {}, grain) {
|
|
3
4
|
const dimensionNames = Object.keys(ds.dimensions);
|
|
4
5
|
const measureNames = Object.keys(ds.measures);
|
|
5
6
|
const filterNames = Object.keys(ds.filters).length > 0
|
|
@@ -17,6 +18,7 @@ export function buildMetricContract(metricName, ds, spec, label, description, gr
|
|
|
17
18
|
valueType: 'number',
|
|
18
19
|
label,
|
|
19
20
|
description,
|
|
21
|
+
...snapshotSemanticMetadata(metadata),
|
|
20
22
|
dimensions: dimensionNames,
|
|
21
23
|
measures: measureNames,
|
|
22
24
|
filters: filterNames,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition-time structural validation for `dataset()`.
|
|
3
|
+
*
|
|
4
|
+
* Every check here runs when the dataset is *constructed*, not when it is
|
|
5
|
+
* queried. A dataset is normally defined at module scope, so a malformed model
|
|
6
|
+
* fails at import — in the build, in CI, and in the first test that touches the
|
|
7
|
+
* module — instead of on the first request that happens to exercise the broken
|
|
8
|
+
* part. The alternative is a typo in `tenantKey` that constructs cleanly, passes
|
|
9
|
+
* review, and is discovered by a query that quietly returns another tenant's
|
|
10
|
+
* rows.
|
|
11
|
+
*
|
|
12
|
+
* Two categories of check live here:
|
|
13
|
+
*
|
|
14
|
+
* - **Identifier safety.** `source`, `tenantKey`, `timeKey`, dimension columns
|
|
15
|
+
* and measure fields are all interpolated into SQL as identifiers. They are
|
|
16
|
+
* validated once here rather than trusted at every call site that builds a
|
|
17
|
+
* predicate from them.
|
|
18
|
+
* - **Raw SQL shape.** The `sql` escape hatch on dimensions and measures is an
|
|
19
|
+
* expression, never a statement. A value carrying a statement terminator or a
|
|
20
|
+
* comment is rejected structurally, so it cannot become a second statement or
|
|
21
|
+
* comment out the rest of a clause.
|
|
22
|
+
*/
|
|
23
|
+
import type { DatasetConfig, DimensionDefinition, MeasureDefinition, RelationshipDefinition } from '../types.js';
|
|
24
|
+
type AnyDimensions = Record<string, DimensionDefinition>;
|
|
25
|
+
type AnyMeasures = Record<string, MeasureDefinition>;
|
|
26
|
+
type AnyRelationships = Record<string, RelationshipDefinition>;
|
|
27
|
+
/**
|
|
28
|
+
* Validates a dataset definition. Throws on the first problem found.
|
|
29
|
+
*
|
|
30
|
+
* Called by `dataset()` before the instance is built, so an invalid model never
|
|
31
|
+
* becomes a queryable object.
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateDatasetDefinition(name: string, config: DatasetConfig<AnyDimensions, AnyMeasures, AnyRelationships>): void;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=dataset-definition-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-definition-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-definition-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAIrB,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AACrD,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAuM/D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAClE,IAAI,CAsDN"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition-time structural validation for `dataset()`.
|
|
3
|
+
*
|
|
4
|
+
* Every check here runs when the dataset is *constructed*, not when it is
|
|
5
|
+
* queried. A dataset is normally defined at module scope, so a malformed model
|
|
6
|
+
* fails at import — in the build, in CI, and in the first test that touches the
|
|
7
|
+
* module — instead of on the first request that happens to exercise the broken
|
|
8
|
+
* part. The alternative is a typo in `tenantKey` that constructs cleanly, passes
|
|
9
|
+
* review, and is discovered by a query that quietly returns another tenant's
|
|
10
|
+
* rows.
|
|
11
|
+
*
|
|
12
|
+
* Two categories of check live here:
|
|
13
|
+
*
|
|
14
|
+
* - **Identifier safety.** `source`, `tenantKey`, `timeKey`, dimension columns
|
|
15
|
+
* and measure fields are all interpolated into SQL as identifiers. They are
|
|
16
|
+
* validated once here rather than trusted at every call site that builds a
|
|
17
|
+
* predicate from them.
|
|
18
|
+
* - **Raw SQL shape.** The `sql` escape hatch on dimensions and measures is an
|
|
19
|
+
* expression, never a statement. A value carrying a statement terminator or a
|
|
20
|
+
* comment is rejected structurally, so it cannot become a second statement or
|
|
21
|
+
* comment out the rest of a clause.
|
|
22
|
+
*/
|
|
23
|
+
import { escapeRegExp, isSafeSQLIdentifier, stripSqlLiterals } from '../sql-utils.js';
|
|
24
|
+
import { validateDatasetAgentMetadata } from './semantic-metadata-validation.js';
|
|
25
|
+
/**
|
|
26
|
+
* Statement terminators and comment openers.
|
|
27
|
+
*
|
|
28
|
+
* A raw `sql` value is spliced into a larger expression, so any of these turns
|
|
29
|
+
* one expression into something else: `;` starts a second statement, `--` and
|
|
30
|
+
* `/*` comment out whatever the builder appends after it.
|
|
31
|
+
*/
|
|
32
|
+
const STATEMENT_BREAKERS = /;|--|\/\*/;
|
|
33
|
+
/** Dataset, dimension and measure names address fields as `<relationship>.<dimension>`. */
|
|
34
|
+
const QUALIFIED_SEPARATOR = '.';
|
|
35
|
+
function fail(datasetName, message) {
|
|
36
|
+
throw new Error(`Invalid dataset "${datasetName}": ${message}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validates a possibly-qualified physical name such as `orders` or
|
|
40
|
+
* `analytics.orders`. Each segment must stand alone as a safe identifier, so a
|
|
41
|
+
* qualified name cannot smuggle anything past the per-segment check.
|
|
42
|
+
*/
|
|
43
|
+
function isSafeQualifiedName(value, maxSegments) {
|
|
44
|
+
const segments = value.split(QUALIFIED_SEPARATOR);
|
|
45
|
+
if (segments.length > maxSegments) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return segments.every(isSafeSQLIdentifier);
|
|
49
|
+
}
|
|
50
|
+
function assertSafeColumn(datasetName, value, context) {
|
|
51
|
+
if (!isSafeSQLIdentifier(value)) {
|
|
52
|
+
fail(datasetName, `${context} "${value}" is not a safe column identifier. It is interpolated into SQL, so it ` +
|
|
53
|
+
'must contain only letters, numbers and underscores, and start with a letter or underscore.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validates a semantic name — a dimension or measure key.
|
|
58
|
+
*
|
|
59
|
+
* These are not physical columns, but they are identifiers: they appear in query
|
|
60
|
+
* inputs, generated tool schemas, and protocol artifacts, where the strict
|
|
61
|
+
* identifier grammar applies. Rejecting here beats failing later during artifact
|
|
62
|
+
* production, which is far from the definition that caused it.
|
|
63
|
+
*/
|
|
64
|
+
function assertSafeName(datasetName, kind, name) {
|
|
65
|
+
if (!isSafeSQLIdentifier(name)) {
|
|
66
|
+
fail(datasetName, `${kind} name "${name}" must contain only letters, numbers and underscores, and start ` +
|
|
67
|
+
'with a letter or underscore, so it stays a valid identifier in generated artifacts.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Checks a raw `sql` expression and the dependencies declared alongside it.
|
|
72
|
+
*
|
|
73
|
+
* The dependency check runs in the direction that fails silently: a declared
|
|
74
|
+
* dependency the expression never references means the definition believes it
|
|
75
|
+
* reads a column it does not read. The opposite direction — an identifier used
|
|
76
|
+
* but not declared — would need a SQL parser, and is caught by the protocol
|
|
77
|
+
* adapter when the artifact is produced.
|
|
78
|
+
*/
|
|
79
|
+
function validateRawSql(datasetName, kind, name, sql, dependencies) {
|
|
80
|
+
if (sql.trim().length === 0) {
|
|
81
|
+
fail(datasetName, `${kind} "${name}" declares an empty sql expression.`);
|
|
82
|
+
}
|
|
83
|
+
// Quoted spans are blanked first, so a terminator or comment opener *inside* a
|
|
84
|
+
// string literal is data rather than syntax. An unterminated quote is itself a
|
|
85
|
+
// rejection: left as data, an open quote would hide everything after it.
|
|
86
|
+
let code;
|
|
87
|
+
let referenceable;
|
|
88
|
+
try {
|
|
89
|
+
code = stripSqlLiterals(sql);
|
|
90
|
+
// A quoted identifier is a column reference, so the dependency check below
|
|
91
|
+
// needs its text even though the terminator check above must not see it.
|
|
92
|
+
referenceable = stripSqlLiterals(sql, { keepQuotedIdentifiers: true });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
fail(datasetName, `${kind} "${name}" sql has an unterminated quoted literal.`);
|
|
96
|
+
}
|
|
97
|
+
if (STATEMENT_BREAKERS.test(code)) {
|
|
98
|
+
fail(datasetName, `${kind} "${name}" sql must be a single expression without statement terminators or ` +
|
|
99
|
+
'comments (";", "--", "/*") outside a quoted literal.');
|
|
100
|
+
}
|
|
101
|
+
for (const dependency of dependencies ?? []) {
|
|
102
|
+
// Dependencies are qualified identifiers (`analytics.orders.amount`); the
|
|
103
|
+
// expression references the column itself, so the final segment is what has
|
|
104
|
+
// to appear in it. Escaped before it becomes a pattern: a raw value carrying
|
|
105
|
+
// regex syntax would either throw at construction or match something else.
|
|
106
|
+
const segments = dependency.split(QUALIFIED_SEPARATOR);
|
|
107
|
+
const column = segments[segments.length - 1] ?? '';
|
|
108
|
+
if (column.length === 0 || !new RegExp(`\\b${escapeRegExp(column)}\\b`).test(referenceable)) {
|
|
109
|
+
fail(datasetName, `${kind} "${name}" declares dependency "${dependency}", but its sql expression never ` +
|
|
110
|
+
'references it. A dependency the expression does not read makes the definition claim ' +
|
|
111
|
+
'a column it never touches.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function validateDimensions(datasetName, dimensions) {
|
|
116
|
+
for (const [name, definition] of Object.entries(dimensions)) {
|
|
117
|
+
if (name.includes(QUALIFIED_SEPARATOR)) {
|
|
118
|
+
fail(datasetName, `dimension "${name}" cannot contain "." because qualified fields are addressed as ` +
|
|
119
|
+
'"<relationship>.<dimension>".');
|
|
120
|
+
}
|
|
121
|
+
// The name is a semantic identifier in its own right — it appears in
|
|
122
|
+
// protocol artifacts and query inputs — so it is checked whether or not an
|
|
123
|
+
// explicit column stands in for it below. Without this, the check silently
|
|
124
|
+
// depended on the column being omitted.
|
|
125
|
+
assertSafeName(datasetName, 'dimension', name);
|
|
126
|
+
if (definition.sql !== undefined) {
|
|
127
|
+
validateRawSql(datasetName, 'dimension', name, definition.sql, definition.dependencies);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// A dimension with no explicit column is backed by a column of the same name.
|
|
131
|
+
assertSafeColumn(datasetName, definition.column ?? name, `dimension "${name}" column`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function validateMeasures(datasetName, measures, dimensions) {
|
|
135
|
+
for (const [name, definition] of Object.entries(measures)) {
|
|
136
|
+
assertSafeName(datasetName, 'measure', name);
|
|
137
|
+
if (definition.sql !== undefined) {
|
|
138
|
+
validateRawSql(datasetName, 'measure', name, definition.sql, definition.dependencies);
|
|
139
|
+
}
|
|
140
|
+
// `field` and `argField` name either a declared dimension or a physical
|
|
141
|
+
// column that the model deliberately does not expose (the `allowHiddenField`
|
|
142
|
+
// case in metric validation). Either way the value reaches SQL, so a name
|
|
143
|
+
// that is not a declared dimension must at least be a safe identifier.
|
|
144
|
+
for (const [label, value] of [
|
|
145
|
+
['field', definition.field],
|
|
146
|
+
['argField', definition.argField],
|
|
147
|
+
]) {
|
|
148
|
+
if (value === undefined || value in dimensions) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
assertSafeColumn(datasetName, value, `measure "${name}" ${label}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function validateLimits(datasetName, limits) {
|
|
156
|
+
if (!limits) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
160
|
+
if (value === undefined) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
164
|
+
fail(datasetName, `limits.${name} must be a positive integer, received ${String(value)}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Validates a dataset definition. Throws on the first problem found.
|
|
170
|
+
*
|
|
171
|
+
* Called by `dataset()` before the instance is built, so an invalid model never
|
|
172
|
+
* becomes a queryable object.
|
|
173
|
+
*/
|
|
174
|
+
export function validateDatasetDefinition(name, config) {
|
|
175
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
176
|
+
throw new Error('Invalid dataset: a non-empty dataset name is required.');
|
|
177
|
+
}
|
|
178
|
+
if (!isSafeSQLIdentifier(name)) {
|
|
179
|
+
fail(name, 'dataset names must contain only letters, numbers and underscores, and start with a letter ' +
|
|
180
|
+
'or underscore, so they remain valid identifiers in generated artifacts.');
|
|
181
|
+
}
|
|
182
|
+
if (typeof config.source !== 'string' || config.source.trim().length === 0) {
|
|
183
|
+
fail(name, 'a non-empty source table is required.');
|
|
184
|
+
}
|
|
185
|
+
// `database.table` is the deepest qualification ClickHouse addresses.
|
|
186
|
+
if (!isSafeQualifiedName(config.source, 2)) {
|
|
187
|
+
fail(name, `source "${config.source}" is not a safe table identifier. Expected "table" or ` +
|
|
188
|
+
'"database.table", each segment containing only letters, numbers and underscores.');
|
|
189
|
+
}
|
|
190
|
+
// The tenant key is the isolation boundary: it becomes a WHERE predicate on
|
|
191
|
+
// every query against a tenant-scoped dataset, so it is checked here rather
|
|
192
|
+
// than trusted wherever that predicate is assembled.
|
|
193
|
+
if (config.tenantKey !== undefined) {
|
|
194
|
+
if (config.tenantKey.trim().length === 0) {
|
|
195
|
+
fail(name, 'tenantKey cannot be empty. Omit it entirely for a dataset without tenancy.');
|
|
196
|
+
}
|
|
197
|
+
assertSafeColumn(name, config.tenantKey, 'tenantKey');
|
|
198
|
+
}
|
|
199
|
+
if (config.timeKey !== undefined) {
|
|
200
|
+
if (config.timeKey.trim().length === 0) {
|
|
201
|
+
fail(name, 'timeKey cannot be empty. Omit it entirely for a dataset without a time key.');
|
|
202
|
+
}
|
|
203
|
+
if (!(config.timeKey in (config.dimensions ?? {}))) {
|
|
204
|
+
assertSafeColumn(name, config.timeKey, 'timeKey');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// A dataset with no dimensions is not rejected here: the query layer already
|
|
208
|
+
// refuses an empty dataset query, which is the honest place for it — a
|
|
209
|
+
// measure-only model is a legitimate thing to define.
|
|
210
|
+
const dimensions = config.dimensions ?? {};
|
|
211
|
+
validateDimensions(name, dimensions);
|
|
212
|
+
validateMeasures(name, config.measures ?? {}, dimensions);
|
|
213
|
+
validateLimits(name, config.limits);
|
|
214
|
+
validateDatasetAgentMetadata(name, config);
|
|
215
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { AggregationSpec, BaseMetricConfig, BaseMetricRef, DatasetInstance, DerivedMetricConfig, DerivedMetricRef, DerivedMetricSpec, DimensionDefinition, MeasureDefinition, MetricRef, RelationshipDefinition } from '../types.js';
|
|
1
|
+
import type { AggregationSpec, BaseMetricConfig, BaseMetricRef, DatasetInstance, DerivedMetricConfig, DerivedMetricRef, DerivedMetricSpec, DimensionDefinition, MeasureDefinition, MetricRef, RelationshipDefinition, SemanticMetadata } from '../types.js';
|
|
2
2
|
type AnyDimensions = Record<string, DimensionDefinition>;
|
|
3
3
|
type AnyMeasures = Record<string, MeasureDefinition>;
|
|
4
4
|
type AnyRelationships = Record<string, RelationshipDefinition>;
|
|
5
5
|
export declare function isDerivedMetricConfig<TMeasures extends Record<string, MeasureDefinition>, TDatasetName extends string>(config: BaseMetricConfig<TMeasures> | DerivedMetricConfig<TDatasetName>): config is DerivedMetricConfig<TDatasetName>;
|
|
6
|
-
export declare function createMetricRef<TDatasetName extends string, TMetricName extends string, TSpec extends AggregationSpec | DerivedMetricSpec<TDatasetName>, TDataset extends DatasetInstance<AnyDimensions, AnyMeasures, AnyRelationships, TDatasetName>>(ds: TDataset, name: TMetricName, spec: TSpec, label?: string, description?: string): MetricRef<TDatasetName, TMetricName, TSpec, TDataset>;
|
|
6
|
+
export declare function createMetricRef<TDatasetName extends string, TMetricName extends string, TSpec extends AggregationSpec | DerivedMetricSpec<TDatasetName>, TDataset extends DatasetInstance<AnyDimensions, AnyMeasures, AnyRelationships, TDatasetName>>(ds: TDataset, name: TMetricName, spec: TSpec, label?: string, description?: string, metadata?: SemanticMetadata): MetricRef<TDatasetName, TMetricName, TSpec, TDataset>;
|
|
7
7
|
export declare function createDerivedMetricSpec<TDatasetName extends string>(config: DerivedMetricConfig<TDatasetName>): DerivedMetricSpec<TDatasetName>;
|
|
8
8
|
export type { BaseMetricRef, DerivedMetricRef };
|
|
9
9
|
//# sourceMappingURL=dataset-metric-ref.d.ts.map
|