@hypequery/datasets 0.1.0 → 0.2.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 +498 -0
- package/dist/api.type-test.d.ts +2 -0
- package/dist/api.type-test.d.ts.map +1 -0
- package/dist/api.type-test.js +103 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -0
- package/dist/dataset-query.d.ts +16 -0
- package/dist/dataset-query.d.ts.map +1 -0
- package/dist/dataset-query.js +56 -0
- package/dist/dataset.d.ts +1 -1
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +22 -157
- package/dist/executor.d.ts +42 -14
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +188 -36
- package/dist/formulas.d.ts +1 -1
- package/dist/formulas.d.ts.map +1 -1
- package/dist/formulas.js +27 -12
- package/dist/in-memory-backend.d.ts +5 -0
- package/dist/in-memory-backend.d.ts.map +1 -0
- package/dist/in-memory-backend.js +221 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/internal.d.ts +23 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +19 -0
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +1 -0
- package/dist/query-builder-protocol.d.ts +2 -2
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/query-builder-protocol.js +1 -1
- package/dist/query-helpers.d.ts +12 -12
- package/dist/query-helpers.d.ts.map +1 -1
- package/dist/query-planner.d.ts +9 -7
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +26 -9
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +1 -1
- package/dist/relationships.d.ts +1 -1
- package/dist/relationships.js +1 -1
- package/dist/semantic-plan.d.ts +82 -0
- package/dist/semantic-plan.d.ts.map +1 -0
- package/dist/semantic-plan.js +1 -0
- package/dist/semantic-planner.d.ts +5 -0
- package/dist/semantic-planner.d.ts.map +1 -0
- package/dist/semantic-planner.js +155 -0
- package/dist/sql-utils.d.ts +1 -1
- package/dist/sql-utils.js +4 -4
- package/dist/types.d.ts +130 -52
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/dataset-contract.d.ts +3 -0
- package/dist/utils/dataset-contract.d.ts.map +1 -0
- package/dist/utils/dataset-contract.js +30 -0
- package/dist/utils/dataset-metric-ref.d.ts +9 -0
- package/dist/utils/dataset-metric-ref.d.ts.map +1 -0
- package/dist/utils/dataset-metric-ref.js +39 -0
- package/dist/utils/dataset-normalization.d.ts +10 -0
- package/dist/utils/dataset-normalization.d.ts.map +1 -0
- package/dist/utils/dataset-normalization.js +35 -0
- package/dist/utils/dataset-query-validation.d.ts +4 -0
- package/dist/utils/dataset-query-validation.d.ts.map +1 -0
- package/dist/utils/dataset-query-validation.js +96 -0
- package/dist/utils/dataset-validation.d.ts +6 -0
- package/dist/utils/dataset-validation.d.ts.map +1 -0
- package/dist/utils/dataset-validation.js +42 -0
- package/dist/utils/derived-cte-validation.d.ts +3 -0
- package/dist/utils/derived-cte-validation.d.ts.map +1 -0
- package/dist/utils/derived-cte-validation.js +32 -0
- package/dist/utils/filtered-aggregation-sql.d.ts +5 -0
- package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -0
- package/dist/utils/filtered-aggregation-sql.js +73 -0
- package/dist/utils/metric-handle.d.ts +11 -0
- package/dist/utils/metric-handle.d.ts.map +1 -0
- package/dist/utils/metric-handle.js +36 -0
- package/dist/utils/pagination.d.ts +17 -0
- package/dist/utils/pagination.d.ts.map +1 -0
- package/dist/utils/pagination.js +23 -0
- package/dist/utils/tenant-runtime.d.ts +14 -0
- package/dist/utils/tenant-runtime.d.ts.map +1 -0
- package/dist/utils/tenant-runtime.js +36 -0
- package/package.json +14 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AggregationSpec, BaseMetricConfig, BaseMetricRef, DatasetInstance, DerivedMetricConfig, DerivedMetricRef, DerivedMetricSpec, DimensionDefinition, MeasureDefinition, MetricRef, RelationshipDefinition } from '../types.js';
|
|
2
|
+
type AnyDimensions = Record<string, DimensionDefinition>;
|
|
3
|
+
type AnyMeasures = Record<string, MeasureDefinition>;
|
|
4
|
+
type AnyRelationships = Record<string, RelationshipDefinition>;
|
|
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>;
|
|
7
|
+
export declare function createDerivedMetricSpec<TDatasetName extends string>(config: DerivedMetricConfig<TDatasetName>): DerivedMetricSpec<TDatasetName>;
|
|
8
|
+
export type { BaseMetricRef, DerivedMetricRef };
|
|
9
|
+
//# sourceMappingURL=dataset-metric-ref.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-metric-ref.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-metric-ref.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EAEnB,iBAAiB,EACjB,SAAS,EACT,sBAAsB,EAEvB,MAAM,aAAa,CAAC;AAGrB,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;AAE/D,wBAAgB,qBAAqB,CACnC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACnD,YAAY,SAAS,MAAM,EAE3B,MAAM,EAAE,gBAAgB,CAAC,SAAS,CAAC,GAAG,mBAAmB,CAAC,YAAY,CAAC,GACtE,MAAM,IAAI,mBAAmB,CAAC,YAAY,CAAC,CAE7C;AAED,wBAAgB,eAAe,CAC7B,YAAY,SAAS,MAAM,EAC3B,WAAW,SAAS,MAAM,EAC1B,KAAK,SAAS,eAAe,GAAG,iBAAiB,CAAC,YAAY,CAAC,EAC/D,QAAQ,SAAS,eAAe,CAAC,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,CAAC,EAE5F,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,KAAK,EACX,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,GACnB,SAAS,CAAC,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,CAAC,CAiCvD;AAED,wBAAgB,uBAAuB,CAAC,YAAY,SAAS,MAAM,EACjE,MAAM,EAAE,mBAAmB,CAAC,YAAY,CAAC,GACxC,iBAAiB,CAAC,YAAY,CAAC,CAMjC;AAED,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { buildMetricContract } from './dataset-contract.js';
|
|
2
|
+
export function isDerivedMetricConfig(config) {
|
|
3
|
+
return 'uses' in config && 'formula' in config;
|
|
4
|
+
}
|
|
5
|
+
export function createMetricRef(ds, name, spec, label, description) {
|
|
6
|
+
const ref = {
|
|
7
|
+
__type: 'metric_ref',
|
|
8
|
+
datasetName: ds.name,
|
|
9
|
+
name,
|
|
10
|
+
spec,
|
|
11
|
+
label,
|
|
12
|
+
description,
|
|
13
|
+
dataset: ds,
|
|
14
|
+
by(grain) {
|
|
15
|
+
if (!ds.timeKey) {
|
|
16
|
+
throw new Error(`Cannot apply .by("${grain}") to metric "${name}" — dataset "${ds.name}" has no timeKey defined.`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
__type: 'grained_metric_ref',
|
|
20
|
+
metric: ref,
|
|
21
|
+
grain,
|
|
22
|
+
contract() {
|
|
23
|
+
return buildMetricContract(name, ds, spec, label, description, grain);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
contract() {
|
|
28
|
+
return buildMetricContract(name, ds, spec, label, description);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
return ref;
|
|
32
|
+
}
|
|
33
|
+
export function createDerivedMetricSpec(config) {
|
|
34
|
+
return {
|
|
35
|
+
__type: 'derived_metric_spec',
|
|
36
|
+
uses: config.uses,
|
|
37
|
+
formula: config.formula,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AggregationSpec, DatasetConfig, DimensionDefinition, MeasureDefinition, RelationshipDefinition, SemanticFiltersDefinition } from '../types.js';
|
|
2
|
+
type AnyMeasures = Record<string, MeasureDefinition>;
|
|
3
|
+
type AnyRelationships = Record<string, RelationshipDefinition>;
|
|
4
|
+
export declare function normalizeDimensions<TDimensions extends Record<string, DimensionDefinition>>(config: DatasetConfig<TDimensions, AnyMeasures, AnyRelationships>): TDimensions;
|
|
5
|
+
export declare function normalizeFilters(dimensions: Record<string, DimensionDefinition>, filters?: SemanticFiltersDefinition): SemanticFiltersDefinition;
|
|
6
|
+
export declare function normalizeMeasures<TMeasures extends Record<string, MeasureDefinition>>(measures: TMeasures | undefined): TMeasures;
|
|
7
|
+
export declare function normalizeRelationships<TRelationships extends Record<string, RelationshipDefinition>>(relationships: TRelationships | undefined): TRelationships;
|
|
8
|
+
export declare function measureToAggregationSpec(measureName: string, definition: MeasureDefinition): AggregationSpec;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=dataset-normalization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-normalization.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-normalization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AACrD,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAE/D,wBAAgB,mBAAmB,CAAC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACzF,MAAM,EAAE,aAAa,CAAC,WAAW,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAChE,WAAW,CAEb;AAED,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC/C,OAAO,CAAC,EAAE,yBAAyB,GAClC,yBAAyB,CAgB3B;AAED,wBAAgB,iBAAiB,CAAC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACnF,QAAQ,EAAE,SAAS,GAAG,SAAS,GAC9B,SAAS,CAEX;AAED,wBAAgB,sBAAsB,CAAC,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAClG,aAAa,EAAE,cAAc,GAAG,SAAS,GACxC,cAAc,CAEhB;AAED,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,GAC5B,eAAe,CAYjB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function normalizeDimensions(config) {
|
|
2
|
+
return config.dimensions;
|
|
3
|
+
}
|
|
4
|
+
export function normalizeFilters(dimensions, filters) {
|
|
5
|
+
if (filters) {
|
|
6
|
+
return filters;
|
|
7
|
+
}
|
|
8
|
+
return Object.fromEntries(Object.entries(dimensions)
|
|
9
|
+
.filter(([, definition]) => definition.filterable !== false)
|
|
10
|
+
.map(([name]) => [
|
|
11
|
+
name,
|
|
12
|
+
{
|
|
13
|
+
__type: 'filter_definition',
|
|
14
|
+
field: name,
|
|
15
|
+
},
|
|
16
|
+
]));
|
|
17
|
+
}
|
|
18
|
+
export function normalizeMeasures(measures) {
|
|
19
|
+
return (measures ?? {});
|
|
20
|
+
}
|
|
21
|
+
export function normalizeRelationships(relationships) {
|
|
22
|
+
return (relationships ?? {});
|
|
23
|
+
}
|
|
24
|
+
export function measureToAggregationSpec(measureName, definition) {
|
|
25
|
+
if (!definition.field) {
|
|
26
|
+
throw new Error(`Invalid measure "${measureName}": a backing field is required.`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
__type: 'aggregation_spec',
|
|
30
|
+
aggregation: definition.aggregation,
|
|
31
|
+
field: definition.field,
|
|
32
|
+
sql: definition.sql,
|
|
33
|
+
filters: definition.filters,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, DatasetQuery, ExecutionContext } from '../types.js';
|
|
2
|
+
import { type ValidationResult } from '../validation.js';
|
|
3
|
+
export declare function validateDatasetQueryInput(ds: AnyDatasetInstance, query: DatasetQuery, context?: ExecutionContext): ValidationResult;
|
|
4
|
+
//# sourceMappingURL=dataset-query-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAO9E,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAmHlB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { validateFilterValue } from '../validation.js';
|
|
2
|
+
import { SUPPORTED_TIME_GRAINS, isSupportedTimeGrain } from '../constants.js';
|
|
3
|
+
import { getRuntimeTenantPredicate, validateTenantRuntime, } from './tenant-runtime.js';
|
|
4
|
+
export function validateDatasetQueryInput(ds, query, context) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
const dimensionNames = Object.keys(ds.dimensions);
|
|
7
|
+
const measureNames = Object.keys(ds.measures);
|
|
8
|
+
const selectedDimensions = query.dimensions ?? [];
|
|
9
|
+
const selectedMeasures = query.measures ?? measureNames;
|
|
10
|
+
const filterNames = Object.keys(ds.filters);
|
|
11
|
+
const orderableFields = new Set([
|
|
12
|
+
...selectedDimensions,
|
|
13
|
+
...selectedMeasures,
|
|
14
|
+
...(query.by ? ['period'] : []),
|
|
15
|
+
]);
|
|
16
|
+
const tenantRuntimeError = validateTenantRuntime(ds, context);
|
|
17
|
+
if (tenantRuntimeError) {
|
|
18
|
+
errors.push(tenantRuntimeError);
|
|
19
|
+
}
|
|
20
|
+
if (selectedDimensions.length === 0 && selectedMeasures.length === 0) {
|
|
21
|
+
errors.push(`Dataset "${ds.name}" query must select at least one dimension or measure.`);
|
|
22
|
+
}
|
|
23
|
+
if (query.dimensions) {
|
|
24
|
+
const invalid = query.dimensions.filter(dimension => !dimensionNames.includes(dimension));
|
|
25
|
+
if (invalid.length > 0) {
|
|
26
|
+
errors.push(`Unknown dimensions: ${invalid.join(', ')}. Available: ${dimensionNames.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (query.measures) {
|
|
30
|
+
const invalid = query.measures.filter(measure => !measureNames.includes(measure));
|
|
31
|
+
if (invalid.length > 0) {
|
|
32
|
+
errors.push(`Unknown measures: ${invalid.join(', ')}. Available: ${measureNames.join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (query.filters) {
|
|
36
|
+
const invalid = query.filters.filter(filter => !filterNames.includes(filter.field));
|
|
37
|
+
if (invalid.length > 0) {
|
|
38
|
+
errors.push(`Unknown filter fields: ${invalid.map(filter => filter.field).join(', ')}. Available: ${filterNames.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
for (const filter of query.filters) {
|
|
41
|
+
const filterDefinition = ds.filters[filter.field];
|
|
42
|
+
if (filterDefinition?.operators && !filterDefinition.operators.includes(filter.operator)) {
|
|
43
|
+
errors.push(`Filter "${filter.field}" does not allow operator "${filter.operator}". Allowed: ${filterDefinition.operators.join(', ')}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const resolvedField = ds.filters[filter.field]?.field ?? filter.field;
|
|
47
|
+
const resolvedDimension = ds.dimensions[resolvedField];
|
|
48
|
+
const resolvedColumn = resolvedDimension?.sql
|
|
49
|
+
? undefined
|
|
50
|
+
: resolvedDimension?.column ?? resolvedField;
|
|
51
|
+
if (getRuntimeTenantPredicate(context) && ds.tenantKey && resolvedColumn === ds.tenantKey) {
|
|
52
|
+
errors.push(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const fieldType = resolvedDimension?.fieldType;
|
|
56
|
+
if (!fieldType) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const filterError = validateFilterValue(filter, fieldType);
|
|
60
|
+
if (filterError) {
|
|
61
|
+
errors.push(filterError);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (query.orderBy) {
|
|
66
|
+
const invalid = query.orderBy.filter(order => !orderableFields.has(order.field));
|
|
67
|
+
if (invalid.length > 0) {
|
|
68
|
+
errors.push(`Unknown orderBy fields: ${invalid.map(order => order.field).join(', ')}. Available: ${Array.from(orderableFields).join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (query.by && !ds.timeKey) {
|
|
72
|
+
errors.push(`Cannot use "by" grain — dataset "${ds.name}" has no timeKey.`);
|
|
73
|
+
}
|
|
74
|
+
if (query.by && !isSupportedTimeGrain(query.by)) {
|
|
75
|
+
errors.push(`Unsupported time grain "${query.by}". Supported: ${SUPPORTED_TIME_GRAINS.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
if (query.limit != null && (!Number.isInteger(query.limit) || query.limit < 0)) {
|
|
78
|
+
errors.push(`Invalid limit: expected a non-negative integer.`);
|
|
79
|
+
}
|
|
80
|
+
if (query.offset != null && (!Number.isInteger(query.offset) || query.offset < 0)) {
|
|
81
|
+
errors.push(`Invalid offset: expected a non-negative integer.`);
|
|
82
|
+
}
|
|
83
|
+
if (ds.limits?.maxDimensions && query.dimensions && query.dimensions.length > ds.limits.maxDimensions) {
|
|
84
|
+
errors.push(`Too many dimensions: ${query.dimensions.length} (max ${ds.limits.maxDimensions})`);
|
|
85
|
+
}
|
|
86
|
+
if (ds.limits?.maxMeasures && query.measures && query.measures.length > ds.limits.maxMeasures) {
|
|
87
|
+
errors.push(`Too many measures: ${query.measures.length} (max ${ds.limits.maxMeasures})`);
|
|
88
|
+
}
|
|
89
|
+
if (ds.limits?.maxFilters && query.filters && query.filters.length > ds.limits.maxFilters) {
|
|
90
|
+
errors.push(`Too many filters: ${query.filters.length} (max ${ds.limits.maxFilters})`);
|
|
91
|
+
}
|
|
92
|
+
if (ds.limits?.maxResultSize && query.limit != null && query.limit > ds.limits.maxResultSize) {
|
|
93
|
+
errors.push(`Too many results requested: ${query.limit} (max ${ds.limits.maxResultSize})`);
|
|
94
|
+
}
|
|
95
|
+
return { valid: errors.length === 0, errors };
|
|
96
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AggregationSpec, AnyDatasetInstance, DerivedMetricConfig } from '../types.js';
|
|
2
|
+
export declare function validateBaseMetric(ds: AnyDatasetInstance, metricName: string, spec: AggregationSpec, options?: {
|
|
3
|
+
allowHiddenField?: boolean;
|
|
4
|
+
}): void;
|
|
5
|
+
export declare function validateDerivedMetric(ds: AnyDatasetInstance, metricName: string, config: DerivedMetricConfig<string>): void;
|
|
6
|
+
//# sourceMappingURL=dataset-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAKrB,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;IAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAE,GACvC,IAAI,CAwCN;AAED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC,GAClC,IAAI,CAmBN"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { validateFilterValue } from '../validation.js';
|
|
2
|
+
const NUMERIC_FIELD_TYPES = new Set(['number']);
|
|
3
|
+
export function validateBaseMetric(ds, metricName, spec, options) {
|
|
4
|
+
const dimension = ds.dimensions[spec.field];
|
|
5
|
+
if (!dimension && !options?.allowHiddenField) {
|
|
6
|
+
throw new Error(`Invalid metric "${metricName}": dimension "${spec.field}" does not exist on dataset "${ds.name}".`);
|
|
7
|
+
}
|
|
8
|
+
if (dimension &&
|
|
9
|
+
(spec.aggregation === 'sum' || spec.aggregation === 'avg') &&
|
|
10
|
+
!NUMERIC_FIELD_TYPES.has(dimension.fieldType)) {
|
|
11
|
+
throw new Error(`Invalid metric "${metricName}": ${spec.aggregation}() requires a numeric dimension, but "${spec.field}" is ${dimension.fieldType}.`);
|
|
12
|
+
}
|
|
13
|
+
for (const filter of spec.filters ?? []) {
|
|
14
|
+
const filterDefinition = ds.filters[filter.field];
|
|
15
|
+
const resolvedField = filterDefinition?.field ?? filter.field;
|
|
16
|
+
const fieldType = ds.dimensions[resolvedField]?.fieldType;
|
|
17
|
+
if (!fieldType) {
|
|
18
|
+
throw new Error(`Invalid metric "${metricName}": measure filter field "${filter.field}" does not exist on dataset "${ds.name}".`);
|
|
19
|
+
}
|
|
20
|
+
if (filterDefinition?.operators && !filterDefinition.operators.includes(filter.operator)) {
|
|
21
|
+
throw new Error(`Invalid metric "${metricName}": measure filter "${filter.field}" does not allow operator "${filter.operator}".`);
|
|
22
|
+
}
|
|
23
|
+
const filterError = validateFilterValue(filter, fieldType);
|
|
24
|
+
if (filterError) {
|
|
25
|
+
throw new Error(`Invalid metric "${metricName}": ${filterError}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function validateDerivedMetric(ds, metricName, config) {
|
|
30
|
+
const usedMetrics = Object.entries(config.uses);
|
|
31
|
+
if (usedMetrics.length === 0) {
|
|
32
|
+
throw new Error(`Invalid metric "${metricName}": derived metrics must reference at least one base metric.`);
|
|
33
|
+
}
|
|
34
|
+
for (const [alias, metric] of usedMetrics) {
|
|
35
|
+
if (metric.datasetName !== ds.name) {
|
|
36
|
+
throw new Error(`Invalid metric "${metricName}": referenced metric "${alias}" belongs to dataset "${metric.datasetName}", expected "${ds.name}".`);
|
|
37
|
+
}
|
|
38
|
+
if (metric.spec.__type !== 'aggregation_spec') {
|
|
39
|
+
throw new Error(`Invalid metric "${metricName}": referenced metric "${alias}" must be a base aggregation on dataset "${ds.name}".`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived-cte-validation.d.ts","sourceRoot":"","sources":["../../src/utils/derived-cte-validation.ts"],"names":[],"mappings":"AAAA,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAU/D;AAED,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,EAC1B,eAAe,EAAE,MAAM,EAAE,GACxB,MAAM,EAAE,CAgCV"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function extractGroupByExpressions(sql) {
|
|
2
|
+
const match = sql.match(/\bGROUP BY\b\s+(.+?)(?:\bHAVING\b|\bORDER BY\b|\bLIMIT\b|\bOFFSET\b|$)/is);
|
|
3
|
+
if (!match) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
return match[1]
|
|
7
|
+
.split(',')
|
|
8
|
+
.map(part => part.trim())
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
export function validateDerivedCteGrouping(sql, aggregateAliases, intendedGroupBy) {
|
|
12
|
+
const errors = [];
|
|
13
|
+
const actualGroupBy = extractGroupByExpressions(sql);
|
|
14
|
+
if (intendedGroupBy.length === 0 && actualGroupBy.length > 0) {
|
|
15
|
+
errors.push('Derived metric planner emitted GROUP BY for an ungrouped query.');
|
|
16
|
+
}
|
|
17
|
+
const duplicates = actualGroupBy.filter((expression, index) => actualGroupBy.indexOf(expression) !== index);
|
|
18
|
+
if (duplicates.length > 0) {
|
|
19
|
+
errors.push(`Derived metric planner emitted duplicate GROUP BY expressions: ${Array.from(new Set(duplicates)).join(', ')}`);
|
|
20
|
+
}
|
|
21
|
+
const aggregateAliasSet = new Set(aggregateAliases);
|
|
22
|
+
const aggregateAliasesInGroupBy = actualGroupBy.filter(expression => aggregateAliasSet.has(expression));
|
|
23
|
+
if (aggregateAliasesInGroupBy.length > 0) {
|
|
24
|
+
errors.push(`Derived metric planner emitted aggregate aliases in GROUP BY: ${Array.from(new Set(aggregateAliasesInGroupBy)).join(', ')}`);
|
|
25
|
+
}
|
|
26
|
+
const intendedGroupBySet = new Set(intendedGroupBy);
|
|
27
|
+
const unexpectedGroupBy = actualGroupBy.filter(expression => !intendedGroupBySet.has(expression));
|
|
28
|
+
if (unexpectedGroupBy.length > 0) {
|
|
29
|
+
errors.push(`Derived metric planner emitted unexpected GROUP BY expressions: ${Array.from(new Set(unexpectedGroupBy)).join(', ')}`);
|
|
30
|
+
}
|
|
31
|
+
return errors;
|
|
32
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AggregationSpec, AnyDatasetInstance } from '../types.js';
|
|
2
|
+
type DatasetShape = AnyDatasetInstance;
|
|
3
|
+
export declare function applyFilteredAggregationExpression(ds: DatasetShape, spec: AggregationSpec, fieldOrExpr: string): string;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=filtered-aggregation-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filtered-aggregation-sql.d.ts","sourceRoot":"","sources":["../../src/utils/filtered-aggregation-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AAGrF,KAAK,YAAY,GAAG,kBAAkB,CAAC;AAiEvC,wBAAgB,kCAAkC,CAChD,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,eAAe,EACrB,WAAW,EAAE,MAAM,GAClB,MAAM,CAsBR"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { resolveFilterField } from '../query-planner.js';
|
|
2
|
+
function renderMeasureFilterLiteral(value) {
|
|
3
|
+
if (value === null) {
|
|
4
|
+
return 'NULL';
|
|
5
|
+
}
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
8
|
+
}
|
|
9
|
+
if (typeof value === 'number') {
|
|
10
|
+
if (!Number.isFinite(value)) {
|
|
11
|
+
throw new Error(`Invalid non-finite numeric literal in measure filter: ${value}`);
|
|
12
|
+
}
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === 'boolean') {
|
|
16
|
+
return value ? '1' : '0';
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Unsupported literal type in measure filter: ${typeof value}`);
|
|
19
|
+
}
|
|
20
|
+
function renderMeasureFilterCondition(ds, filter) {
|
|
21
|
+
const field = resolveFilterField(ds, filter.field);
|
|
22
|
+
switch (filter.operator) {
|
|
23
|
+
case 'eq':
|
|
24
|
+
return `${field} = ${renderMeasureFilterLiteral(filter.value)}`;
|
|
25
|
+
case 'neq':
|
|
26
|
+
return `${field} != ${renderMeasureFilterLiteral(filter.value)}`;
|
|
27
|
+
case 'gt':
|
|
28
|
+
return `${field} > ${renderMeasureFilterLiteral(filter.value)}`;
|
|
29
|
+
case 'gte':
|
|
30
|
+
return `${field} >= ${renderMeasureFilterLiteral(filter.value)}`;
|
|
31
|
+
case 'lt':
|
|
32
|
+
return `${field} < ${renderMeasureFilterLiteral(filter.value)}`;
|
|
33
|
+
case 'lte':
|
|
34
|
+
return `${field} <= ${renderMeasureFilterLiteral(filter.value)}`;
|
|
35
|
+
case 'like':
|
|
36
|
+
return `${field} LIKE ${renderMeasureFilterLiteral(filter.value)}`;
|
|
37
|
+
case 'in':
|
|
38
|
+
case 'notIn': {
|
|
39
|
+
if (!Array.isArray(filter.value) || filter.value.length === 0) {
|
|
40
|
+
throw new Error(`"${filter.operator}" measure filters require a non-empty array.`);
|
|
41
|
+
}
|
|
42
|
+
const values = filter.value.map(renderMeasureFilterLiteral).join(', ');
|
|
43
|
+
return `${field} ${filter.operator === 'in' ? 'IN' : 'NOT IN'} (${values})`;
|
|
44
|
+
}
|
|
45
|
+
case 'between': {
|
|
46
|
+
if (!Array.isArray(filter.value) || filter.value.length !== 2) {
|
|
47
|
+
throw new Error('"between" measure filters require a two-item array.');
|
|
48
|
+
}
|
|
49
|
+
return `${field} BETWEEN ${renderMeasureFilterLiteral(filter.value[0])} AND ${renderMeasureFilterLiteral(filter.value[1])}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function applyFilteredAggregationExpression(ds, spec, fieldOrExpr) {
|
|
54
|
+
if (!spec.filters?.length) {
|
|
55
|
+
return fieldOrExpr;
|
|
56
|
+
}
|
|
57
|
+
const combinedCondition = spec.filters
|
|
58
|
+
.map(filter => renderMeasureFilterCondition(ds, filter))
|
|
59
|
+
.map(condition => `(${condition})`)
|
|
60
|
+
.join(' AND ');
|
|
61
|
+
switch (spec.aggregation) {
|
|
62
|
+
case 'sum':
|
|
63
|
+
return `if(${combinedCondition}, ${fieldOrExpr}, 0)`;
|
|
64
|
+
case 'count':
|
|
65
|
+
case 'countDistinct':
|
|
66
|
+
case 'avg':
|
|
67
|
+
case 'min':
|
|
68
|
+
case 'max':
|
|
69
|
+
return `if(${combinedCondition}, ${fieldOrExpr}, NULL)`;
|
|
70
|
+
default:
|
|
71
|
+
return fieldOrExpr;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, ExecutionContext, GrainedMetricRef, MetricFilter, MetricQuery, MetricRef, TimeGrain } from '../types.js';
|
|
2
|
+
type DatasetShape = AnyDatasetInstance;
|
|
3
|
+
export type MetricHandle = MetricRef | GrainedMetricRef;
|
|
4
|
+
export declare function isMetricHandle(value: unknown): value is MetricHandle;
|
|
5
|
+
export declare function assertMetricHandle(value: unknown): asserts value is MetricHandle;
|
|
6
|
+
export declare function getMetricRef(metric: MetricHandle): MetricRef;
|
|
7
|
+
export declare function getMetricGrain(metric: MetricHandle, query: MetricQuery): TimeGrain | undefined;
|
|
8
|
+
export declare function getTenantRuntimeColumn(ds: DatasetShape, context?: ExecutionContext): string | undefined;
|
|
9
|
+
export declare function isTenantScopedFilter(ds: DatasetShape, filter: MetricFilter, context?: ExecutionContext): boolean;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=metric-handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-handle.d.ts","sourceRoot":"","sources":["../../src/utils/metric-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,SAAS,EACT,SAAS,EACV,MAAM,aAAa,CAAC;AAGrB,KAAK,YAAY,GAAG,kBAAkB,CAAC;AAMvC,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,gBAAgB,CAAC;AAExD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAKpE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAOhF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,SAAS,CAE5D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAE9F;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,YAAY,EAChB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAMpB;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAOT"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveDimensionExpression, resolveFilterField, resolveTenantFilterColumn } from '../query-planner.js';
|
|
2
|
+
import { getRuntimeTenantPredicate } from './tenant-runtime.js';
|
|
3
|
+
function isMetricHandleType(value) {
|
|
4
|
+
return value === 'metric_ref' || value === 'grained_metric_ref';
|
|
5
|
+
}
|
|
6
|
+
export function isMetricHandle(value) {
|
|
7
|
+
return typeof value === 'object'
|
|
8
|
+
&& value !== null
|
|
9
|
+
&& '__type' in value
|
|
10
|
+
&& isMetricHandleType(value.__type);
|
|
11
|
+
}
|
|
12
|
+
export function assertMetricHandle(value) {
|
|
13
|
+
if (!isMetricHandle(value)) {
|
|
14
|
+
throw new Error('Metric queries only support MetricRef and GrainedMetricRef. ' +
|
|
15
|
+
'dataset.query(...) is not part of the public execution API.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function getMetricRef(metric) {
|
|
19
|
+
return metric.__type === 'grained_metric_ref' ? metric.metric : metric;
|
|
20
|
+
}
|
|
21
|
+
export function getMetricGrain(metric, query) {
|
|
22
|
+
return metric.__type === 'grained_metric_ref' ? metric.grain : query.by ?? undefined;
|
|
23
|
+
}
|
|
24
|
+
export function getTenantRuntimeColumn(ds, context) {
|
|
25
|
+
if (!getRuntimeTenantPredicate(context)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return resolveTenantFilterColumn(ds, context);
|
|
29
|
+
}
|
|
30
|
+
export function isTenantScopedFilter(ds, filter, context) {
|
|
31
|
+
const tenantColumn = getTenantRuntimeColumn(ds, context);
|
|
32
|
+
if (!tenantColumn) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return resolveFilterField(ds, filter.field) === resolveDimensionExpression(ds, tenantColumn);
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MetricResultMeta } from '../types.js';
|
|
2
|
+
export type PaginationMeta = NonNullable<MetricResultMeta['pagination']>;
|
|
3
|
+
/**
|
|
4
|
+
* When a limit is set, request one extra row so the executor can report
|
|
5
|
+
* `hasMore` without issuing a separate COUNT query. Returns `undefined` when
|
|
6
|
+
* no limit is set (unbounded query — there is no "next page").
|
|
7
|
+
*/
|
|
8
|
+
export declare function overfetchLimit(limit?: number): number | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Trim an over-fetched result set back to `limit` rows and derive pagination
|
|
11
|
+
* metadata. The extra row (if present) signals that another page exists.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyPagination<T>(rows: T[], limit?: number, offset?: number): {
|
|
14
|
+
data: T[];
|
|
15
|
+
pagination?: PaginationMeta;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/utils/pagination.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,CAAC;AAEzE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,IAAI,EAAE,CAAC,EAAE,EACT,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,GACd;IAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAAC,UAAU,CAAC,EAAE,cAAc,CAAA;CAAE,CAU5C"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a limit is set, request one extra row so the executor can report
|
|
3
|
+
* `hasMore` without issuing a separate COUNT query. Returns `undefined` when
|
|
4
|
+
* no limit is set (unbounded query — there is no "next page").
|
|
5
|
+
*/
|
|
6
|
+
export function overfetchLimit(limit) {
|
|
7
|
+
return limit != null ? limit + 1 : undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Trim an over-fetched result set back to `limit` rows and derive pagination
|
|
11
|
+
* metadata. The extra row (if present) signals that another page exists.
|
|
12
|
+
*/
|
|
13
|
+
export function applyPagination(rows, limit, offset) {
|
|
14
|
+
if (limit == null) {
|
|
15
|
+
return { data: rows };
|
|
16
|
+
}
|
|
17
|
+
const hasMore = rows.length > limit;
|
|
18
|
+
const data = hasMore ? rows.slice(0, limit) : rows;
|
|
19
|
+
return {
|
|
20
|
+
data,
|
|
21
|
+
pagination: { limit, offset: offset ?? 0, hasMore },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, ExecutionContext } from '../types.js';
|
|
2
|
+
export type TenantPredicate = {
|
|
3
|
+
operator: 'eq';
|
|
4
|
+
value: string;
|
|
5
|
+
} | {
|
|
6
|
+
operator: 'in';
|
|
7
|
+
value: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare function getRuntimeTenantPredicate(context?: ExecutionContext): TenantPredicate | undefined;
|
|
10
|
+
export declare function getRuntimeTenantId(context?: ExecutionContext): string | undefined;
|
|
11
|
+
export declare function isCrossTenantRuntime(context?: ExecutionContext): boolean;
|
|
12
|
+
export declare function hasTenantRuntime(context?: ExecutionContext): boolean;
|
|
13
|
+
export declare function validateTenantRuntime(ds: AnyDatasetInstance, context?: ExecutionContext): string | undefined;
|
|
14
|
+
//# sourceMappingURL=tenant-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant-runtime.d.ts","sourceRoot":"","sources":["../../src/utils/tenant-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAExC,wBAAgB,yBAAyB,CACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe,GAAG,SAAS,CAe7B;AAED,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAGxE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAEpE;AAED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,kBAAkB,EACtB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAQpB"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function getRuntimeTenantPredicate(context) {
|
|
2
|
+
const tenant = context?.runtime?.tenant;
|
|
3
|
+
if (!tenant) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
if (typeof tenant === 'string') {
|
|
7
|
+
return { operator: 'eq', value: tenant };
|
|
8
|
+
}
|
|
9
|
+
if ('id' in tenant) {
|
|
10
|
+
return { operator: 'eq', value: tenant.id };
|
|
11
|
+
}
|
|
12
|
+
if ('in' in tenant && tenant.in.length > 0) {
|
|
13
|
+
return { operator: 'in', value: tenant.in };
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
export function getRuntimeTenantId(context) {
|
|
18
|
+
const predicate = getRuntimeTenantPredicate(context);
|
|
19
|
+
return predicate?.operator === 'eq' ? predicate.value : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function isCrossTenantRuntime(context) {
|
|
22
|
+
const tenant = context?.runtime?.tenant;
|
|
23
|
+
return !!tenant && typeof tenant === 'object' && 'scope' in tenant && tenant.scope === 'all';
|
|
24
|
+
}
|
|
25
|
+
export function hasTenantRuntime(context) {
|
|
26
|
+
return !!getRuntimeTenantPredicate(context) || isCrossTenantRuntime(context);
|
|
27
|
+
}
|
|
28
|
+
export function validateTenantRuntime(ds, context) {
|
|
29
|
+
if (!ds.tenantKey) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (!hasTenantRuntime(context)) {
|
|
33
|
+
return `Dataset "${ds.name}" requires runtime tenant scoping.`;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/datasets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Semantic layer for defining datasets, metrics, and semantic queries",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./internal": {
|
|
15
|
+
"types": "./dist/internal.d.ts",
|
|
16
|
+
"import": "./dist/internal.js",
|
|
17
|
+
"require": "./dist/internal.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"typesVersions": {
|
|
21
|
+
"*": {
|
|
22
|
+
"internal": [
|
|
23
|
+
"./dist/internal.d.ts"
|
|
24
|
+
]
|
|
13
25
|
}
|
|
14
26
|
},
|
|
15
27
|
"license": "Apache-2.0",
|
|
@@ -41,7 +53,7 @@
|
|
|
41
53
|
"dev": "tsc --watch",
|
|
42
54
|
"test": "npm run test:types && npm run test:unit",
|
|
43
55
|
"test:unit": "vitest run",
|
|
44
|
-
"test:integration": "
|
|
56
|
+
"test:integration": "node scripts/run-integration-tests.js",
|
|
45
57
|
"test:types": "tsc --project tsconfig.type-tests.json",
|
|
46
58
|
"test:watch": "vitest watch",
|
|
47
59
|
"test:coverage": "vitest run --coverage"
|