@hypequery/datasets 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client-contract/build-from-deployment.d.ts +26 -0
- package/dist/client-contract/build-from-deployment.d.ts.map +1 -0
- package/dist/client-contract/build-from-deployment.js +76 -0
- package/dist/client-contract/deployment-annotations.d.ts +6 -0
- package/dist/client-contract/deployment-annotations.d.ts.map +1 -0
- package/dist/client-contract/deployment-annotations.js +32 -0
- package/dist/client-contract/deployment-fields.d.ts +19 -0
- package/dist/client-contract/deployment-fields.d.ts.map +1 -0
- package/dist/client-contract/deployment-fields.js +103 -0
- package/dist/client-contract/deployment-profile.d.ts +22 -0
- package/dist/client-contract/deployment-profile.d.ts.map +1 -0
- package/dist/client-contract/deployment-profile.js +29 -0
- package/dist/client-contract/query-schema.d.ts.map +1 -1
- package/dist/client-contract/query-schema.js +5 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/query-planner.js +1 -1
- package/dist/relationship-check.d.ts.map +1 -1
- package/dist/relationship-check.js +11 -6
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +19 -11
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -1
- package/dist/utils/clickhouse-dataset-sql-dialect.js +5 -0
- package/dist/utils/dataset-filter-types.d.ts +35 -10
- package/dist/utils/dataset-filter-types.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +5 -3
- package/dist/utils/effective-filters.d.ts +27 -0
- package/dist/utils/effective-filters.d.ts.map +1 -0
- package/dist/utils/effective-filters.js +36 -0
- package/dist/utils/enforced-constraints.d.ts.map +1 -1
- package/dist/utils/enforced-constraints.js +5 -3
- package/dist/utils/json-pointer-segments.d.ts +7 -0
- package/dist/utils/json-pointer-segments.d.ts.map +1 -0
- package/dist/utils/json-pointer-segments.js +9 -0
- package/dist/utils/metric-query-validation.d.ts.map +1 -1
- package/dist/utils/metric-query-validation.js +5 -3
- package/dist/utils/relationship-builder-plan.d.ts +7 -2
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
- package/dist/utils/relationship-builder-plan.js +14 -4
- package/dist/utils/relationship-validation.d.ts.map +1 -1
- package/dist/utils/relationship-validation.js +5 -4
- package/dist/utils/scoped-dataset-source.js +1 -1
- package/dist/utils/tenant-runtime.d.ts +7 -0
- package/dist/utils/tenant-runtime.d.ts.map +1 -1
- package/dist/utils/tenant-runtime.js +16 -0
- package/package.json +2 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type PreparedProtocolClientContract, type ProtocolDeploymentContract, type ProtocolDeploymentDataset } from '@hypequery/protocol';
|
|
2
|
+
import { type ClientContractProfile } from './deployment-profile.js';
|
|
3
|
+
export interface BuildClientContractOptions {
|
|
4
|
+
/** Ceilings the host enforces; defaults mirror Python's ClientQueryProfile. */
|
|
5
|
+
readonly profile?: ClientContractProfile;
|
|
6
|
+
/**
|
|
7
|
+
* Trusted mount prefix, such as `/v1`: absolute, no trailing slash. Never a
|
|
8
|
+
* proxy or request header; the host knows how it is mounted.
|
|
9
|
+
*/
|
|
10
|
+
readonly mountPath?: string;
|
|
11
|
+
/** Final route per published dataset, when the host serves it somewhere other than `endpoint.path`. */
|
|
12
|
+
readonly paths?: Readonly<Record<string, string>>;
|
|
13
|
+
/** `authorized` is a caller's view, narrowed by `include`. */
|
|
14
|
+
readonly view?: 'build' | 'authorized';
|
|
15
|
+
/** Which published datasets to advertise; every one by default. */
|
|
16
|
+
readonly include?: (dataset: ProtocolDeploymentDataset) => boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The client contract for a deployment's published datasets: typed fields,
|
|
20
|
+
* routes and schema hashes `hypequery generate:client` turns into React types
|
|
21
|
+
* and a route manifest. Pure: no network, no execution. Datasets without an
|
|
22
|
+
* endpoint policy (relationship targets) are not advertised, and neither are
|
|
23
|
+
* relationship fields yet.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildClientContract(contract: ProtocolDeploymentContract, options?: BuildClientContractOptions): PreparedProtocolClientContract;
|
|
26
|
+
//# sourceMappingURL=build-from-deployment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-from-deployment.d.ts","sourceRoot":"","sources":["../../src/client-contract/build-from-deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,8BAA8B,EACnC,KAAK,0BAA0B,EAC/B,KAAK,yBAAyB,EAC/B,MAAM,qBAAqB,CAAC;AAI7B,OAAO,EAAmD,KAAK,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAEtH,MAAM,WAAW,0BAA0B;IACzC,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC;IACzC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,uGAAuG;IACvG,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;IACvC,mEAAmE;IACnE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,yBAAyB,KAAK,OAAO,CAAC;CACpE;AA2DD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,0BAA0B,EACpC,OAAO,GAAE,0BAA+B,GACvC,8BAA8B,CAchC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { prepareProtocolClientContract, } from '@hypequery/protocol';
|
|
2
|
+
import { PORTABLE_TIME_GRAINS } from '../utils/portable-grains.js';
|
|
3
|
+
import { endpointAnnotations } from './deployment-annotations.js';
|
|
4
|
+
import { publicFields } from './deployment-fields.js';
|
|
5
|
+
import { clientQueryLimits, resolveClientContractProfile } from './deployment-profile.js';
|
|
6
|
+
function endpointPath(dataset, options) {
|
|
7
|
+
const path = options.paths?.[dataset.name] ?? dataset.endpoint?.path;
|
|
8
|
+
if (path === undefined || !path.startsWith('/')) {
|
|
9
|
+
throw new Error(`Client export: dataset "${dataset.name}" needs an absolute endpoint path.`);
|
|
10
|
+
}
|
|
11
|
+
return (options.mountPath ?? '') + path;
|
|
12
|
+
}
|
|
13
|
+
function endpointDescriptor(dataset, options, profile) {
|
|
14
|
+
const { dimensions, measures, filters } = publicFields(dataset);
|
|
15
|
+
// Rows carry the time bucket as `period`, so a field of that name would collide.
|
|
16
|
+
if (dataset.timeField !== undefined && (Object.hasOwn(dimensions, 'period') || Object.hasOwn(measures, 'period'))) {
|
|
17
|
+
throw new Error(`Client export: dataset "${dataset.name}" has a field named "period", which its time bucket uses. Rename it.`);
|
|
18
|
+
}
|
|
19
|
+
const limits = clientQueryLimits(dataset, profile);
|
|
20
|
+
// An endpoint's default selection is the base measures it can serve, capped
|
|
21
|
+
// at the dataset's own measure limit so a dataset with more base measures
|
|
22
|
+
// than it may select at once still exports. The selection is hashed, so a
|
|
23
|
+
// host budget never trims it: a budget-only change must not move the schema
|
|
24
|
+
// hash a generated client pins. A budget too small for it is refused.
|
|
25
|
+
const defaultMeasures = dataset.measures
|
|
26
|
+
.filter(measure => !('kind' in measure) && Object.hasOwn(measures, measure.name))
|
|
27
|
+
.map(measure => measure.name)
|
|
28
|
+
.slice(0, dataset.limits?.maxMeasures);
|
|
29
|
+
if (defaultMeasures.length > limits.maxMeasures) {
|
|
30
|
+
throw new Error(`Client export: dataset "${dataset.name}" selects ${defaultMeasures.length} measures by default, `
|
|
31
|
+
+ `more than the host's measure budget of ${limits.maxMeasures}. Raise the budget, or set the dataset's limits.maxMeasures.`);
|
|
32
|
+
}
|
|
33
|
+
const annotations = endpointAnnotations(dataset, Object.keys(dimensions), Object.keys(measures));
|
|
34
|
+
return {
|
|
35
|
+
id: dataset.name,
|
|
36
|
+
kind: 'dataset',
|
|
37
|
+
dataset: dataset.name,
|
|
38
|
+
path: endpointPath(dataset, options),
|
|
39
|
+
dimensions,
|
|
40
|
+
measures,
|
|
41
|
+
filters,
|
|
42
|
+
metadata: true,
|
|
43
|
+
limits,
|
|
44
|
+
// Deployment contract 2 carries day-and-longer grains only.
|
|
45
|
+
time: dataset.timeField === undefined ? null : {
|
|
46
|
+
grains: [...PORTABLE_TIME_GRAINS],
|
|
47
|
+
defaultTimezone: profile.defaultTimezone,
|
|
48
|
+
timezone: true,
|
|
49
|
+
periodFormat: 'calendar-date-or-local-datetime-v1',
|
|
50
|
+
},
|
|
51
|
+
defaultMeasures,
|
|
52
|
+
...(annotations ? { annotations } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The client contract for a deployment's published datasets: typed fields,
|
|
57
|
+
* routes and schema hashes `hypequery generate:client` turns into React types
|
|
58
|
+
* and a route manifest. Pure: no network, no execution. Datasets without an
|
|
59
|
+
* endpoint policy (relationship targets) are not advertised, and neither are
|
|
60
|
+
* relationship fields yet.
|
|
61
|
+
*/
|
|
62
|
+
export function buildClientContract(contract, options = {}) {
|
|
63
|
+
const { mountPath } = options;
|
|
64
|
+
if (mountPath && (!mountPath.startsWith('/') || mountPath.endsWith('/'))) {
|
|
65
|
+
throw new Error('Client export: mountPath must be empty or an absolute path without a trailing slash.');
|
|
66
|
+
}
|
|
67
|
+
const profile = resolveClientContractProfile(options.profile);
|
|
68
|
+
const published = contract.datasets
|
|
69
|
+
.filter(dataset => dataset.endpoint !== undefined && (options.include?.(dataset) ?? true));
|
|
70
|
+
return prepareProtocolClientContract({
|
|
71
|
+
kind: 'hypequery-client-contract',
|
|
72
|
+
version: 1,
|
|
73
|
+
view: options.view ?? 'build',
|
|
74
|
+
endpoints: published.map(dataset => endpointDescriptor(dataset, options, profile)),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ClientEndpointAnnotations, ProtocolDeploymentDataset } from '@hypequery/protocol';
|
|
2
|
+
/** Fold whitespace runs so multi-line definitions stay advertisable; omit blank text. */
|
|
3
|
+
export declare function annotationText(value: string | undefined): string | undefined;
|
|
4
|
+
/** Group text from the dataset; field text for each advertised dimension and measure. */
|
|
5
|
+
export declare function endpointAnnotations(dataset: ProtocolDeploymentDataset, dimensions: readonly string[], measures: readonly string[]): ClientEndpointAnnotations | undefined;
|
|
6
|
+
//# sourceMappingURL=deployment-annotations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-annotations.d.ts","sourceRoot":"","sources":["../../src/client-contract/deployment-annotations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EAEzB,yBAAyB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAI5E;AASD,yFAAyF;AACzF,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,yBAAyB,EAClC,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,yBAAyB,GAAG,SAAS,CAgBvC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Fold whitespace runs so multi-line definitions stay advertisable; omit blank text. */
|
|
2
|
+
export function annotationText(value) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
const folded = value.split(/\s+/).filter(Boolean).join(' ');
|
|
6
|
+
return folded.trim() ? folded : undefined;
|
|
7
|
+
}
|
|
8
|
+
function fieldAnnotation(definition) {
|
|
9
|
+
const label = annotationText(definition.label);
|
|
10
|
+
const description = annotationText(definition.description);
|
|
11
|
+
if (label === undefined && description === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
return { ...(label !== undefined ? { label } : {}), ...(description !== undefined ? { description } : {}) };
|
|
14
|
+
}
|
|
15
|
+
/** Group text from the dataset; field text for each advertised dimension and measure. */
|
|
16
|
+
export function endpointAnnotations(dataset, dimensions, measures) {
|
|
17
|
+
const advertised = [
|
|
18
|
+
...dataset.dimensions.filter(item => dimensions.includes(item.name)),
|
|
19
|
+
...dataset.measures.filter(item => measures.includes(item.name)),
|
|
20
|
+
];
|
|
21
|
+
// Object.fromEntries defines each name as an own key, `__proto__` included.
|
|
22
|
+
const fields = Object.fromEntries(advertised.flatMap(definition => {
|
|
23
|
+
const node = fieldAnnotation(definition);
|
|
24
|
+
return node ? [[definition.name, node]] : [];
|
|
25
|
+
}));
|
|
26
|
+
const description = annotationText(dataset.description);
|
|
27
|
+
const result = {
|
|
28
|
+
...(description !== undefined ? { description } : {}),
|
|
29
|
+
...(Object.keys(fields).length ? { fields } : {}),
|
|
30
|
+
};
|
|
31
|
+
return Object.keys(result).length ? result : undefined;
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClientDimension, ClientFilter, ClientMeasure, ProtocolDatasetDimension, ProtocolDatasetFilter, ProtocolDeploymentDataset, ProtocolDeploymentMeasure } from '@hypequery/protocol';
|
|
2
|
+
/** Local fields have unknown nullability; the client contract says so rather than guess. */
|
|
3
|
+
export declare function dimensionDescriptor(dimension: ProtocolDatasetDimension): ClientDimension;
|
|
4
|
+
/** `having` compares numbers, so ordered aggregations over non-numeric fields can't take it. */
|
|
5
|
+
export declare function measureDescriptor(dataset: ProtocolDeploymentDataset, measure: ProtocolDeploymentMeasure): ClientMeasure;
|
|
6
|
+
export declare function filterDescriptor(dimension: ProtocolDatasetDimension, filter: ProtocolDatasetFilter): ClientFilter | null;
|
|
7
|
+
/** Tenant identities come from the caller's credential, never from request predicates. */
|
|
8
|
+
export declare function requestFilter(dataset: ProtocolDeploymentDataset, dimension: ProtocolDatasetDimension): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The fields one dataset endpoint advertises. Relationship fields are not
|
|
11
|
+
* exported yet, matching Python's default (`related=()`), so a measure whose
|
|
12
|
+
* fixed filters cross a relationship is left out rather than advertised.
|
|
13
|
+
*/
|
|
14
|
+
export declare function publicFields(dataset: ProtocolDeploymentDataset): {
|
|
15
|
+
dimensions: Record<string, ClientDimension>;
|
|
16
|
+
measures: Record<string, ClientMeasure>;
|
|
17
|
+
filters: Record<string, ClientFilter>;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=deployment-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-fields.d.ts","sourceRoot":"","sources":["../../src/client-contract/deployment-fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,aAAa,EACb,wBAAwB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,yBAAyB,EAE1B,MAAM,qBAAqB,CAAC;AAQ7B,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,wBAAwB,GAAG,eAAe,CAQxF;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,yBAAyB,EAAE,OAAO,EAAE,yBAAyB,GAAG,aAAa,CAUvH;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,wBAAwB,EAAE,MAAM,EAAE,qBAAqB,GAAG,YAAY,GAAG,IAAI,CAKxH;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,OAAO,EAAE,yBAAyB,EAAE,SAAS,EAAE,wBAAwB,GAAG,OAAO,CAI9G;AAmCD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,yBAAyB,GAAG;IAChE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACvC,CAkBA"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// As Python's builder: booleans compare by equality and membership only.
|
|
2
|
+
const BOOLEAN_OPERATORS = new Set(['eq', 'neq', 'in', 'notIn']);
|
|
3
|
+
const ORDERED_AGGREGATIONS = new Set(['min', 'max', 'argMin', 'argMax']);
|
|
4
|
+
/** Stored states whose merged value is the stored value itself, which may not be a number. */
|
|
5
|
+
const ORDERED_STATES = new Set(['min', 'max', 'any', 'anyLast']);
|
|
6
|
+
/** Local fields have unknown nullability; the client contract says so rather than guess. */
|
|
7
|
+
export function dimensionDescriptor(dimension) {
|
|
8
|
+
return {
|
|
9
|
+
type: dimension.type,
|
|
10
|
+
wireType: dimension.type === 'number' ? 'number-or-string' : dimension.type === 'boolean' ? 'boolean' : 'string',
|
|
11
|
+
nullable: 'unknown',
|
|
12
|
+
groupable: dimension.groupable,
|
|
13
|
+
orderable: dimension.groupable,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** `having` compares numbers, so ordered aggregations over non-numeric fields can't take it. */
|
|
17
|
+
export function measureDescriptor(dataset, measure) {
|
|
18
|
+
if ('kind' in measure)
|
|
19
|
+
return { wireType: 'string', nullable: true, having: true };
|
|
20
|
+
if (measure.aggregation === 'merge') {
|
|
21
|
+
return { wireType: 'string', nullable: true, having: !ORDERED_STATES.has(String(measure.stateFunction)) };
|
|
22
|
+
}
|
|
23
|
+
if (!ORDERED_AGGREGATIONS.has(measure.aggregation)) {
|
|
24
|
+
return { wireType: 'string', nullable: true, having: true };
|
|
25
|
+
}
|
|
26
|
+
const dimension = dataset.dimensions.find(item => item.name === measure.field);
|
|
27
|
+
return { wireType: 'string', nullable: true, having: dimension?.type === 'number' };
|
|
28
|
+
}
|
|
29
|
+
export function filterDescriptor(dimension, filter) {
|
|
30
|
+
const operators = [...new Set(filter.operators.filter(operator => dimension.type === 'boolean'
|
|
31
|
+
? BOOLEAN_OPERATORS.has(operator)
|
|
32
|
+
: dimension.type !== 'number' || operator !== 'like'))].sort();
|
|
33
|
+
return operators.length ? { type: dimension.type, operators } : null;
|
|
34
|
+
}
|
|
35
|
+
/** Tenant identities come from the caller's credential, never from request predicates. */
|
|
36
|
+
export function requestFilter(dataset, dimension) {
|
|
37
|
+
if (dataset.tenant.kind !== 'required')
|
|
38
|
+
return true;
|
|
39
|
+
const column = dimension.source.kind === 'column' ? dimension.source.column : dimension.name;
|
|
40
|
+
return column !== dataset.tenant.field;
|
|
41
|
+
}
|
|
42
|
+
function expressionFields(expression) {
|
|
43
|
+
switch (expression.kind) {
|
|
44
|
+
case 'reference': return [expression.name];
|
|
45
|
+
case 'aggregate': return [expression.field];
|
|
46
|
+
case 'binary': return [...expressionFields(expression.left), ...expressionFields(expression.right)];
|
|
47
|
+
case 'call': return expression.args.flatMap(expressionFields);
|
|
48
|
+
case 'comparison': return 'right' in expression
|
|
49
|
+
? [...expressionFields(expression.left), ...expressionFields(expression.right)]
|
|
50
|
+
: expressionFields(expression.left);
|
|
51
|
+
case 'logical': return 'operands' in expression ? expression.operands.flatMap(expressionFields) : expressionFields(expression.operand);
|
|
52
|
+
default: return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The fields a measure's fixed filters read, through any derived dependencies. */
|
|
56
|
+
function measureFilterFields(dataset, name) {
|
|
57
|
+
const filters = new Map(dataset.filters.map(filter => [filter.name, filter.field]));
|
|
58
|
+
const measures = new Map(dataset.measures.map(measure => [measure.name, measure]));
|
|
59
|
+
const pending = [name];
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const fields = [];
|
|
62
|
+
while (pending.length) {
|
|
63
|
+
const current = pending.pop();
|
|
64
|
+
if (seen.has(current))
|
|
65
|
+
continue;
|
|
66
|
+
seen.add(current);
|
|
67
|
+
const measure = measures.get(current);
|
|
68
|
+
if (measure === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
if ('kind' in measure)
|
|
71
|
+
pending.push(...measure.uses.map(use => use.measure));
|
|
72
|
+
else
|
|
73
|
+
for (const field of measure.filters.flatMap(expressionFields))
|
|
74
|
+
fields.push(filters.get(field) ?? field);
|
|
75
|
+
}
|
|
76
|
+
return fields;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The fields one dataset endpoint advertises. Relationship fields are not
|
|
80
|
+
* exported yet, matching Python's default (`related=()`), so a measure whose
|
|
81
|
+
* fixed filters cross a relationship is left out rather than advertised.
|
|
82
|
+
*/
|
|
83
|
+
export function publicFields(dataset) {
|
|
84
|
+
const dimensions = Object.fromEntries(dataset.dimensions.map(dimension => [dimension.name, dimensionDescriptor(dimension)]));
|
|
85
|
+
const measures = Object.fromEntries(dataset.measures
|
|
86
|
+
.filter(measure => measureFilterFields(dataset, measure.name).every(field => !field.includes('.')))
|
|
87
|
+
.map(measure => [measure.name, measureDescriptor(dataset, measure)]));
|
|
88
|
+
// Built with Object.fromEntries, as above: assigning a key such as
|
|
89
|
+
// `__proto__` would set the prototype instead of adding an entry.
|
|
90
|
+
const filters = Object.fromEntries(dataset.filters.flatMap((filter) => {
|
|
91
|
+
if (filter.field.includes('.'))
|
|
92
|
+
return [];
|
|
93
|
+
const dimension = dataset.dimensions.find(item => item.name === filter.field);
|
|
94
|
+
if (dimension === undefined) {
|
|
95
|
+
throw new Error(`Client export: filter "${dataset.name}.${filter.name}" references unknown dimension "${filter.field}".`);
|
|
96
|
+
}
|
|
97
|
+
if (!requestFilter(dataset, dimension))
|
|
98
|
+
return [];
|
|
99
|
+
const descriptor = filterDescriptor(dimension, filter);
|
|
100
|
+
return descriptor ? [[filter.name, descriptor]] : [];
|
|
101
|
+
}));
|
|
102
|
+
return { dimensions, measures, filters };
|
|
103
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ClientQueryLimits, ProtocolDeploymentDataset } from '@hypequery/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* Trusted host ceilings for an exported client contract. The host must enforce
|
|
4
|
+
* at least these bounds; the export only ever narrows them by each dataset's
|
|
5
|
+
* own limits and endpoint `maxLimit`. Defaults mirror Python's ClientQueryProfile.
|
|
6
|
+
*/
|
|
7
|
+
export interface ClientContractProfile {
|
|
8
|
+
readonly defaultLimit?: number;
|
|
9
|
+
readonly maxLimit?: number;
|
|
10
|
+
readonly maxOffset?: number;
|
|
11
|
+
readonly maxDimensions?: number;
|
|
12
|
+
readonly maxMeasures?: number;
|
|
13
|
+
readonly maxPredicates?: number;
|
|
14
|
+
readonly maxOrderBy?: number;
|
|
15
|
+
readonly maxFilterValues?: number;
|
|
16
|
+
/** IANA zone used when a request names none. */
|
|
17
|
+
readonly defaultTimezone?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolveClientContractProfile(profile?: ClientContractProfile): Required<ClientContractProfile>;
|
|
20
|
+
/** Intersect the profile with the dataset's own ceilings; never widen them. */
|
|
21
|
+
export declare function clientQueryLimits(dataset: ProtocolDeploymentDataset, profile: Required<ClientContractProfile>): ClientQueryLimits;
|
|
22
|
+
//# sourceMappingURL=deployment-profile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployment-profile.d.ts","sourceRoot":"","sources":["../../src/client-contract/deployment-profile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAExF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,gDAAgD;IAChD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAcD,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,qBAA0B,GAAG,QAAQ,CAAC,qBAAqB,CAAC,CAEjH;AAID,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,yBAAyB,EAAE,OAAO,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,iBAAiB,CAYjI"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const DEFAULT_PROFILE = {
|
|
2
|
+
defaultLimit: 1000,
|
|
3
|
+
maxLimit: 1000,
|
|
4
|
+
maxOffset: 1_000_000,
|
|
5
|
+
maxDimensions: 100,
|
|
6
|
+
maxMeasures: 100,
|
|
7
|
+
maxPredicates: 100,
|
|
8
|
+
maxOrderBy: 100,
|
|
9
|
+
maxFilterValues: 10_000,
|
|
10
|
+
defaultTimezone: 'UTC',
|
|
11
|
+
};
|
|
12
|
+
export function resolveClientContractProfile(profile = {}) {
|
|
13
|
+
return { ...DEFAULT_PROFILE, ...Object.fromEntries(Object.entries(profile).filter(([, value]) => value !== undefined)) };
|
|
14
|
+
}
|
|
15
|
+
const narrower = (ceiling, value) => value === undefined ? ceiling : Math.min(ceiling, value);
|
|
16
|
+
/** Intersect the profile with the dataset's own ceilings; never widen them. */
|
|
17
|
+
export function clientQueryLimits(dataset, profile) {
|
|
18
|
+
const maxLimit = narrower(narrower(profile.maxLimit, dataset.limits?.maxResultSize), dataset.endpoint?.maxLimit);
|
|
19
|
+
return {
|
|
20
|
+
defaultLimit: Math.min(profile.defaultLimit, maxLimit),
|
|
21
|
+
maxLimit,
|
|
22
|
+
maxOffset: profile.maxOffset,
|
|
23
|
+
maxDimensions: narrower(profile.maxDimensions, dataset.limits?.maxDimensions),
|
|
24
|
+
maxMeasures: narrower(profile.maxMeasures, dataset.limits?.maxMeasures),
|
|
25
|
+
maxPredicates: narrower(profile.maxPredicates, dataset.limits?.maxFilters),
|
|
26
|
+
maxOrderBy: profile.maxOrderBy,
|
|
27
|
+
maxFilterValues: profile.maxFilterValues,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-schema.d.ts","sourceRoot":"","sources":["../../src/client-contract/query-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,sBAAsB,EAAE,KAAK,sBAAsB,EACzD,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAK,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AACzC,OAAO,EAAwB,KAAK,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"query-schema.d.ts","sourceRoot":"","sources":["../../src/client-contract/query-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,sBAAsB,EAAE,KAAK,sBAAsB,EACzD,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAK,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AACzC,OAAO,EAAwB,KAAK,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC;CACrC;AACD,MAAM,WAAW,0BAA2B,SAAQ,6BAA6B;IAC/E,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;CACpE;AAiED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,CAe1F"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { validateProtocolClientContract, validateProtocolClientQuery, } from '@hypequery/protocol';
|
|
1
|
+
import { ProtocolClientContractError, validateProtocolClientContract, validateProtocolClientQuery, } from '@hypequery/protocol';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { toSemanticJsonSchema } from '../semantic-query-schema.js';
|
|
4
|
+
import { pointerSegments } from '../utils/json-pointer-segments.js';
|
|
4
5
|
function union(schemas) {
|
|
5
6
|
return schemas.length === 0 ? z.never() : schemas.length === 1 ? schemas[0]
|
|
6
7
|
: z.union(schemas);
|
|
@@ -60,8 +61,9 @@ function endpointSchema(contract, endpoint, transport) {
|
|
|
60
61
|
try {
|
|
61
62
|
validateProtocolClientQuery(contract, endpoint.id, input, transport);
|
|
62
63
|
}
|
|
63
|
-
catch {
|
|
64
|
-
context.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid client query.'
|
|
64
|
+
catch (error) {
|
|
65
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid client query.',
|
|
66
|
+
path: error instanceof ProtocolClientContractError ? pointerSegments(error.path) : [] });
|
|
65
67
|
return z.NEVER;
|
|
66
68
|
}
|
|
67
69
|
return input;
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export type { BuildProtocolDeploymentContractOptions } from './protocol-deployme
|
|
|
28
28
|
export { rehydrateProtocolDeploymentContract } from './protocol-rehydrate.js';
|
|
29
29
|
export type { BuildProtocolDatasetContractOptions } from './protocol-adapter.js';
|
|
30
30
|
export { publishToCloud } from './cloud-publishing.js';
|
|
31
|
+
export { buildClientContract } from './client-contract/build-from-deployment.js';
|
|
32
|
+
export type { BuildClientContractOptions } from './client-contract/build-from-deployment.js';
|
|
33
|
+
export type { ClientContractProfile } from './client-contract/deployment-profile.js';
|
|
31
34
|
export type { CloudPublishingOptions } from './cloud-publishing.js';
|
|
32
35
|
export { createPortableSemanticExecutor, createPortableSemanticRuntime, PortableExecutionBudgetError, PortableExecutionTenantError, PortableExecutionUnsupportedError, } from './portable-executor.js';
|
|
33
36
|
export type { PortableSemanticBudget, PortableSemanticRuntime, PortableDatasetCompilation, PortableSemanticExecutionInput, PortableSemanticExecutorOptions, } from './portable-executor.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export { buildProtocolDeploymentContract } from './protocol-deployment-adapter.j
|
|
|
33
33
|
export { assertDeployable } from './utils/deployable-formulas.js';
|
|
34
34
|
export { rehydrateProtocolDeploymentContract } from './protocol-rehydrate.js';
|
|
35
35
|
export { publishToCloud } from './cloud-publishing.js';
|
|
36
|
+
export { buildClientContract } from './client-contract/build-from-deployment.js';
|
|
36
37
|
// Portable native execution of a semantic invocation (decision 0005).
|
|
37
38
|
export { createPortableSemanticExecutor, createPortableSemanticRuntime, PortableExecutionBudgetError, PortableExecutionTenantError, PortableExecutionUnsupportedError, } from './portable-executor.js';
|
|
38
39
|
// SQL portability compiler (R1A-07)
|
package/dist/query-planner.js
CHANGED
|
@@ -18,7 +18,7 @@ export function resolveDimensionExpression(ds, dimensionName, joinCtx) {
|
|
|
18
18
|
assertNoRawSqlUnderJoins('dimension', dimensionName, definition.sql, joinCtx);
|
|
19
19
|
return definition.sql;
|
|
20
20
|
}
|
|
21
|
-
return qualifyBaseColumn(joinCtx, definition?.column ?? dimensionName);
|
|
21
|
+
return qualifyBaseColumn(joinCtx, definition?.column ?? dimensionName, ds.source);
|
|
22
22
|
}
|
|
23
23
|
export function resolveFilterField(ds, filterField, joinCtx) {
|
|
24
24
|
if (joinCtx && isQualifiedField(filterField)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-check.d.ts","sourceRoot":"","sources":["../src/relationship-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAyB,KAAK,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAInG,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,mCAAmC,CAAC;AAE3C,YAAY,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAE9E,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,YAAY,EAAE,wBAAwB,CAAC;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,6DAA6D;IAC7D,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,EAAE,EAAE,OAAO,CAAC;IACZ,4CAA4C;IAC5C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,oBAAoB,EAAE,CAAC;CAChC;AAKD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,
|
|
1
|
+
{"version":3,"file":"relationship-check.d.ts","sourceRoot":"","sources":["../src/relationship-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EAAyB,KAAK,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAInG,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,mCAAmC,CAAC;AAE3C,YAAY,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAE9E,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,YAAY,EAAE,wBAAwB,CAAC;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,6DAA6D;IAC7D,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,EAAE,EAAE,OAAO,CAAC;IACZ,4CAA4C;IAC5C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,oBAAoB,EAAE,CAAC;CAChC;AAKD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAuCnC"}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { datasetSourceBuilder } from './utils/dataset-source.js';
|
|
20
20
|
import { toQueryBuilderFactory } from './query-builder-protocol.js';
|
|
21
|
-
import { validateTenantRuntime } from './utils/tenant-runtime.js';
|
|
21
|
+
import { joinsWithinTenant, validateTenantRuntime } from './utils/tenant-runtime.js';
|
|
22
22
|
import { applyRowScope, datasetRowScope } from './utils/row-scope.js';
|
|
23
23
|
import { resolveDatasetSqlDialect } from './utils/dataset-sql-dialect.js';
|
|
24
24
|
import { listToOneRelationships, readCount, relationshipKeyIssue, } from './utils/relationship-key-check.js';
|
|
@@ -40,13 +40,18 @@ export async function checkRelationships(ds, options) {
|
|
|
40
40
|
if (tenantError) {
|
|
41
41
|
throw new Error(`Cannot check relationship "${entry.relationship}": ${tenantError}`);
|
|
42
42
|
}
|
|
43
|
+
// Joins then match keys within each tenant, so a key reused by another
|
|
44
|
+
// tenant is not a duplicate; count (key, tenant) pairs instead.
|
|
45
|
+
const distinctColumns = joinsWithinTenant(ds, entry.target, options.context)
|
|
46
|
+
? [...(entry.columns ?? [entry.column]), entry.target.tenantKey]
|
|
47
|
+
: entry.columns;
|
|
43
48
|
let qb = datasetSourceBuilder(factory, entry.target)
|
|
44
49
|
.count(entry.column, ROWS_ALIAS)
|
|
45
|
-
.countDistinct(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
.countDistinct(distinctColumns ? dialect.packRow(distinctColumns) : entry.column, KEYS_ALIAS);
|
|
51
|
+
// count() skips a NULL key but a packed row counts it, so filter NULL keys
|
|
52
|
+
// out of both: under a multi-tenant scope one would cancel a duplicate.
|
|
53
|
+
for (const column of entry.columns ?? [entry.column])
|
|
54
|
+
qb = qb.where(column, 'isNotNull', undefined);
|
|
50
55
|
qb = applyRowScope(qb, datasetRowScope(entry.target, options.context));
|
|
51
56
|
const [row] = await qb.execute({
|
|
52
57
|
abortSignal: options.context?.abortSignal,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-query-schema.d.ts","sourceRoot":"","sources":["../src/semantic-query-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAK,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AACzC,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"semantic-query-schema.d.ts","sourceRoot":"","sources":["../src/semantic-query-schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,7 @@ import { zodToProtocolSchema } from './protocol-schema-adapter.js';
|
|
|
8
8
|
import { compareStrings, stableStringify, uniqueSorted } from './utils/canonical-json.js';
|
|
9
9
|
import { zodToDraft7JsonSchema } from './utils/zod-json-schema.js';
|
|
10
10
|
import { datasetTimeKeys } from './utils/dataset-time-keys.js';
|
|
11
|
+
import { effectiveFilters } from './utils/effective-filters.js';
|
|
11
12
|
export const DEFAULT_SEMANTIC_QUERY_SCHEMA_LIMITS = Object.freeze({
|
|
12
13
|
maxResultSize: 10_000,
|
|
13
14
|
maxOffset: 10_000,
|
|
@@ -24,6 +25,11 @@ function resolveCatalog(source) {
|
|
|
24
25
|
? source
|
|
25
26
|
: getDatasetCatalog(source);
|
|
26
27
|
}
|
|
28
|
+
function localFilterPolicy(source) {
|
|
29
|
+
return isDatasetCatalog(source)
|
|
30
|
+
? effectiveFilters(source)
|
|
31
|
+
: effectiveFilters(source);
|
|
32
|
+
}
|
|
27
33
|
function relationshipFilterPolicy(source) {
|
|
28
34
|
const fields = new Map();
|
|
29
35
|
// A serialized catalog has no target filter allowlist. Do not infer one
|
|
@@ -34,8 +40,9 @@ function relationshipFilterPolicy(source) {
|
|
|
34
40
|
if (relationship.kind === 'hasMany')
|
|
35
41
|
continue;
|
|
36
42
|
const target = relationship.target();
|
|
43
|
+
const filters = effectiveFilters(target);
|
|
37
44
|
for (const [field, dimension] of Object.entries(target.dimensions)) {
|
|
38
|
-
const definition =
|
|
45
|
+
const definition = filters[field];
|
|
39
46
|
if (!dimension.sql && definition?.field === field) {
|
|
40
47
|
fields.set(`${name}.${field}`, definition.operators
|
|
41
48
|
? [...definition.operators]
|
|
@@ -59,13 +66,13 @@ function boundedArray(item, maximum) {
|
|
|
59
66
|
const array = z.array(item);
|
|
60
67
|
return (maximum === undefined ? array : array.max(maximum)).optional();
|
|
61
68
|
}
|
|
62
|
-
function filterSchema(
|
|
69
|
+
function filterSchema(localFilters, fields, relationshipOperators) {
|
|
63
70
|
const filterValue = z.unknown().refine(value => value !== undefined, 'Required');
|
|
64
71
|
// A null check takes no value (RFC 0023), so it gets its own value-free shape.
|
|
65
72
|
const variants = uniqueSorted(fields).flatMap((field) => {
|
|
66
73
|
const operators = relationshipOperators.has(field)
|
|
67
74
|
? relationshipOperators.get(field) ?? []
|
|
68
|
-
:
|
|
75
|
+
: localFilters[field]?.operators ?? [...SEMANTIC_FILTER_OPERATORS];
|
|
69
76
|
const valueOperators = operators.filter(operator => !isNullCheckFilterOperator(operator));
|
|
70
77
|
const nullChecks = operators.filter(isNullCheckFilterOperator);
|
|
71
78
|
return [
|
|
@@ -105,7 +112,7 @@ function havingSchema(measures, maximum) {
|
|
|
105
112
|
return conditions.describe('Conditions on aggregated measure values, applied after grouping (SQL HAVING). '
|
|
106
113
|
+ 'Each condition must name a measure selected in this query.');
|
|
107
114
|
}
|
|
108
|
-
function queryShape(catalog, metricName, options, relationshipOperators, metric, localRelationshipFields = []) {
|
|
115
|
+
function queryShape(catalog, metricName, options, relationshipOperators, localFilters = effectiveFilters(catalog), metric, localRelationshipFields = []) {
|
|
109
116
|
const limits = { ...DEFAULT_SEMANTIC_QUERY_SCHEMA_LIMITS, ...options };
|
|
110
117
|
const relationshipFields = getQueryableRelationshipFields(catalog);
|
|
111
118
|
const filterableRelationshipFields = [...relationshipOperators.keys()];
|
|
@@ -129,16 +136,17 @@ function queryShape(catalog, metricName, options, relationshipOperators, metric,
|
|
|
129
136
|
...Object.keys(catalog.dimensions).filter(isGroupable),
|
|
130
137
|
...relationshipFields.filter(isGroupable),
|
|
131
138
|
]);
|
|
132
|
-
const
|
|
133
|
-
const
|
|
139
|
+
const localFilterNames = Object.keys(localFilters);
|
|
140
|
+
const localFilterSet = new Set(localFilterNames);
|
|
141
|
+
// A metric's own filter list stays authoritative: a hosted catalog may narrow it.
|
|
134
142
|
const filterFields = metric
|
|
135
143
|
? uniqueSorted([
|
|
136
144
|
...metric.filters.filter(field => field.includes('.')
|
|
137
145
|
? filterableRelationshipSet.has(field)
|
|
138
|
-
:
|
|
146
|
+
: localFilterSet.has(field)),
|
|
139
147
|
...localRelationshipFields.filter(field => filterableRelationshipSet.has(field)),
|
|
140
148
|
])
|
|
141
|
-
: uniqueSorted([...
|
|
149
|
+
: uniqueSorted([...localFilterNames, ...filterableRelationshipFields]);
|
|
142
150
|
const grains = metric
|
|
143
151
|
? metric.grain ? [metric.grain] : metric.grains
|
|
144
152
|
: catalog.supportedGrains;
|
|
@@ -170,7 +178,7 @@ function queryShape(catalog, metricName, options, relationshipOperators, metric,
|
|
|
170
178
|
...(metricName ? {} : {
|
|
171
179
|
measures: boundedArray(fieldEnum(selectableMeasures), lowerLimit(catalog.limits?.maxMeasures, limits.maxMeasures)),
|
|
172
180
|
}),
|
|
173
|
-
filters: boundedArray(filterSchema(
|
|
181
|
+
filters: boundedArray(filterSchema(localFilters, filterFields, relationshipOperators), lowerLimit(catalog.limits?.maxFilters, limits.maxFilters)),
|
|
174
182
|
// Opt-in, so existing schemas and manifest hashes are unchanged and a
|
|
175
183
|
// surface only advertises conditions its executor can run.
|
|
176
184
|
...(!metricName && options.having ? {
|
|
@@ -202,7 +210,7 @@ function queryShape(catalog, metricName, options, relationshipOperators, metric,
|
|
|
202
210
|
};
|
|
203
211
|
}
|
|
204
212
|
export function buildDatasetInputSchema(dataset, options = {}) {
|
|
205
|
-
const schema = z.object(queryShape(resolveCatalog(dataset), undefined, options, relationshipFilterPolicy(dataset))).strict();
|
|
213
|
+
const schema = z.object(queryShape(resolveCatalog(dataset), undefined, options, relationshipFilterPolicy(dataset), localFilterPolicy(dataset))).strict();
|
|
206
214
|
if (options.requireSelection === false)
|
|
207
215
|
return schema;
|
|
208
216
|
return requireSelection(schema);
|
|
@@ -213,7 +221,7 @@ export function buildMetricInputSchema(dataset, metricName, options = {}, metric
|
|
|
213
221
|
const localRelationshipFields = isDatasetCatalog(dataset)
|
|
214
222
|
? []
|
|
215
223
|
: getQueryableRelationshipFields(catalog);
|
|
216
|
-
return z.object(queryShape(catalog, metricName, options, relationshipFilterPolicy(dataset), metric, localRelationshipFields)).strict();
|
|
224
|
+
return z.object(queryShape(catalog, metricName, options, relationshipFilterPolicy(dataset), localFilterPolicy(dataset), metric, localRelationshipFields)).strict();
|
|
217
225
|
}
|
|
218
226
|
function unionSchemas(schemas, emptyShape) {
|
|
219
227
|
if (schemas.length === 0)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clickhouse-dataset-sql-dialect.d.ts","sourceRoot":"","sources":["../../src/utils/clickhouse-dataset-sql-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4B,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAwClG,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,
|
|
1
|
+
{"version":3,"file":"clickhouse-dataset-sql-dialect.d.ts","sourceRoot":"","sources":["../../src/utils/clickhouse-dataset-sql-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4B,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAwClG,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,sBAqHP,CAAC"}
|
|
@@ -129,6 +129,11 @@ export const clickhouseDatasetSqlDialect = Object.freeze({
|
|
|
129
129
|
const symbol = BINARY_OPERATORS[operator];
|
|
130
130
|
if (!symbol)
|
|
131
131
|
throw new Error('Formula contains an unsupported binary operator.');
|
|
132
|
+
// Decimal division keeps the dividend's scale and truncates, so the average
|
|
133
|
+
// of 2.00 over 3 rows would be 0.66 before any ROUND. Integer division already
|
|
134
|
+
// returns Float64; a Float64 dividend makes decimals match.
|
|
135
|
+
if (operator === 'divide')
|
|
136
|
+
return `(toFloat64(${left}) ${symbol} ${right})`;
|
|
132
137
|
return `(${left} ${symbol} ${right})`;
|
|
133
138
|
},
|
|
134
139
|
formulaFunction(name, args) {
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import type { DatasetInstance, DimensionDefinition, MetricFilter, MetricFilterOperator, MetricValueFilterOperator, RelationshipDefinition, SemanticFilterDefinition, SemanticFiltersDefinition } from '../types.js';
|
|
2
|
+
type GeneratedFilter<K extends string> = Omit<SemanticFilterDefinition, 'operators'> & {
|
|
3
|
+
field: K;
|
|
4
|
+
/** Generated filters list no operators, so they admit no null checks (RFC 0023). */
|
|
5
|
+
operators?: undefined;
|
|
6
|
+
};
|
|
2
7
|
/** The generated allowlist mirrors normalizeFilters; explicit maps replace it. */
|
|
3
8
|
export type NormalizedDatasetFilters<TDimensions extends Record<string, DimensionDefinition>, TFilters extends SemanticFiltersDefinition | undefined> = TFilters extends SemanticFiltersDefinition ? TFilters : {
|
|
4
9
|
[K in keyof TDimensions as TDimensions[K] extends {
|
|
5
10
|
filterable: false;
|
|
6
|
-
} ? never : K]:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
} ? never : K]: GeneratedFilter<K & string>;
|
|
12
|
+
};
|
|
13
|
+
/** The declared filters whose field is `K`. */
|
|
14
|
+
type DeclaredFiltersOn<TFilters extends SemanticFiltersDefinition, K> = {
|
|
15
|
+
[F in keyof TFilters]: TFilters[F] extends {
|
|
16
|
+
field: K;
|
|
17
|
+
} ? TFilters[F] : never;
|
|
18
|
+
}[keyof TFilters];
|
|
19
|
+
type DimensionFilter<K extends string, TDeclared> = [TDeclared] extends [never] ? GeneratedFilter<K> : [Exclude<TDeclared, {
|
|
20
|
+
operators: readonly unknown[];
|
|
21
|
+
}>] extends [never] ? Omit<SemanticFilterDefinition, 'field' | 'operators'> & {
|
|
22
|
+
field: K;
|
|
23
|
+
operators: TDeclared extends {
|
|
24
|
+
operators: infer TOperators;
|
|
25
|
+
} ? TOperators : never;
|
|
26
|
+
} : GeneratedFilter<K>;
|
|
27
|
+
/**
|
|
28
|
+
* The filters a caller may apply, mirroring effectiveFilters: the declared
|
|
29
|
+
* filters plus one per filterable dimension no declared filter is named after.
|
|
30
|
+
*/
|
|
31
|
+
export type EffectiveDatasetFilters<TDimensions extends Record<string, DimensionDefinition>, TFilters extends SemanticFiltersDefinition> = TFilters & {
|
|
32
|
+
[K in keyof TDimensions as K extends keyof TFilters ? never : TDimensions[K] extends {
|
|
33
|
+
filterable: false;
|
|
34
|
+
} ? never : K]: DimensionFilter<K & string, DeclaredFiltersOn<TFilters, K>>;
|
|
11
35
|
};
|
|
36
|
+
type FiltersOf<TDataset extends DatasetInstance<any, any, any, any>> = EffectiveDatasetFilters<TDataset['dimensions'], TDataset['filters']>;
|
|
12
37
|
type FilterOperators<TDefinition> = TDefinition extends {
|
|
13
38
|
operators: readonly (infer TOperator extends MetricFilterOperator)[];
|
|
14
39
|
} ? TOperator : TDefinition extends {
|
|
@@ -19,14 +44,14 @@ type LocalFilterFor<TFilters extends SemanticFiltersDefinition> = {
|
|
|
19
44
|
}[keyof TFilters & string];
|
|
20
45
|
type RelationshipFilterFor<TRelationships extends Record<string, RelationshipDefinition>> = {
|
|
21
46
|
[R in keyof TRelationships & string]: TRelationships[R] extends RelationshipDefinition<infer TTarget, infer TKind> ? TKind extends 'hasMany' ? never : TTarget extends DatasetInstance<any, any, any, any> ? {
|
|
22
|
-
[K in keyof TTarget
|
|
47
|
+
[K in keyof FiltersOf<TTarget> & keyof TTarget['dimensions'] & string]: TTarget['dimensions'][K] extends {
|
|
23
48
|
sql: string;
|
|
24
|
-
} ? never : K extends TTarget[
|
|
25
|
-
}[keyof TTarget
|
|
49
|
+
} ? never : K extends FiltersOf<TTarget>[K]['field'] ? MetricFilter<`${R}.${K}`, unknown, FilterOperators<FiltersOf<TTarget>[K]>> : never;
|
|
50
|
+
}[keyof FiltersOf<TTarget> & keyof TTarget['dimensions'] & string] : never : never;
|
|
26
51
|
}[keyof TRelationships & string];
|
|
27
52
|
/** Filter names and operator policies exposed by a dataset and its to-one targets. */
|
|
28
|
-
export type DatasetFilterFor<TDataset extends DatasetInstance<any, any, any, any>> = LocalFilterFor<TDataset
|
|
29
|
-
/** Names
|
|
53
|
+
export type DatasetFilterFor<TDataset extends DatasetInstance<any, any, any, any>> = LocalFilterFor<FiltersOf<TDataset>> | RelationshipFilterFor<TDataset['relationships']>;
|
|
54
|
+
/** Names a caller may filter on, including one-hop relationship filters. */
|
|
30
55
|
export type DatasetFilterNames<TDataset extends DatasetInstance<any, any, any, any>> = DatasetFilterFor<TDataset>['field'];
|
|
31
56
|
export {};
|
|
32
57
|
//# sourceMappingURL=dataset-filter-types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-filter-types.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-filter-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,yBAAyB,EACzB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,kFAAkF;AAClF,MAAM,MAAM,wBAAwB,CAClC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACvD,QAAQ,SAAS,yBAAyB,GAAG,SAAS,IACpD,QAAQ,SAAS,yBAAyB,GAAG,QAAQ,GAAG;KACzD,CAAC,IAAI,MAAM,WAAW,IAAI,WAAW,CAAC,CAAC,CAAC,SAAS;QAAE,UAAU,EAAE,KAAK,CAAA;KAAE,GAAG,KAAK,GAAG,CAAC,GACjF,IAAI,CAAC,wBAAwB,EAAE,WAAW,CAAC,GAAG;
|
|
1
|
+
{"version":3,"file":"dataset-filter-types.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-filter-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAuB9E,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAsLlB"}
|
|
@@ -15,6 +15,7 @@ import { enforcedConstraintErrors } from './enforced-constraints.js';
|
|
|
15
15
|
import { analyzeTimeMeasureAxis } from './time-measure-axis.js';
|
|
16
16
|
import { datasetHavingErrors } from './dataset-having.js';
|
|
17
17
|
import { datasetOnTimeKey, selectedTimeKeyError } from './dataset-time-keys.js';
|
|
18
|
+
import { effectiveFilters } from './effective-filters.js';
|
|
18
19
|
export function validateDatasetQueryInput(dataset, query, context) {
|
|
19
20
|
// Validate against the selected time axis, so grain and window checks see it.
|
|
20
21
|
const timeKeyError = selectedTimeKeyError(dataset, query.timeKey);
|
|
@@ -33,7 +34,8 @@ export function validateDatasetQueryInput(dataset, query, context) {
|
|
|
33
34
|
errors.push(`Output "${name}" cannot be selected as both a dimension and a measure. Select one or give the definitions distinct names.`);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
const
|
|
37
|
+
const filters = effectiveFilters(ds);
|
|
38
|
+
const filterNames = Object.keys(filters);
|
|
37
39
|
const orderableFields = new Set([
|
|
38
40
|
...selectedDimensions,
|
|
39
41
|
...selectedMeasures,
|
|
@@ -100,12 +102,12 @@ export function validateDatasetQueryInput(dataset, query, context) {
|
|
|
100
102
|
errors.push(`Unknown filter fields: ${filter.field}. Available: ${filterNames.join(', ')}`);
|
|
101
103
|
continue;
|
|
102
104
|
}
|
|
103
|
-
const filterDefinition =
|
|
105
|
+
const filterDefinition = filters[filter.field];
|
|
104
106
|
if (!filterAllowsOperator(filterDefinition, filter.operator)) {
|
|
105
107
|
errors.push(`Filter "${filter.field}" does not allow operator "${filter.operator}". Allowed: ${(filterDefinition?.operators ?? SEMANTIC_FILTER_OPERATORS).join(', ')}`);
|
|
106
108
|
continue;
|
|
107
109
|
}
|
|
108
|
-
const resolvedField =
|
|
110
|
+
const resolvedField = filterDefinition.field;
|
|
109
111
|
const resolvedDimension = ds.dimensions[resolvedField];
|
|
110
112
|
const resolvedColumn = resolvedDimension?.sql
|
|
111
113
|
? undefined
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SemanticFilterDefinition } from '../types.js';
|
|
2
|
+
/** The parts of a filter definition that decide what a caller may filter on. */
|
|
3
|
+
export interface FilterPolicy {
|
|
4
|
+
readonly field: string;
|
|
5
|
+
readonly operators?: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
interface FilterPolicySource<TFilter extends FilterPolicy> {
|
|
8
|
+
readonly filters: Readonly<Record<string, TFilter>>;
|
|
9
|
+
readonly dimensions: Readonly<Record<string, {
|
|
10
|
+
readonly filterable?: boolean;
|
|
11
|
+
}>>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The filters a caller may apply: the declared filters, plus one per
|
|
15
|
+
* filterable dimension that no declared filter is named after.
|
|
16
|
+
*
|
|
17
|
+
* Declared filters add to the dimensions marked filterable rather than
|
|
18
|
+
* replacing them. A declared filter keeps its own name, field and operators,
|
|
19
|
+
* and can expose a dimension marked `filterable: false`. A dimension's own
|
|
20
|
+
* filter takes the operators of the declared filters on its field, so a
|
|
21
|
+
* declaration that narrows a field's operators cannot be bypassed by naming
|
|
22
|
+
* the dimension directly. Without one it gets the default list, which has no
|
|
23
|
+
* null checks (RFC 0023).
|
|
24
|
+
*/
|
|
25
|
+
export declare function effectiveFilters<TFilter extends FilterPolicy>(source: FilterPolicySource<TFilter>): Record<string, TFilter | SemanticFilterDefinition>;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=effective-filters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effective-filters.d.ts","sourceRoot":"","sources":["../../src/utils/effective-filters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE5D,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,UAAU,kBAAkB,CAAC,OAAO,SAAS,YAAY;IACvD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,CAAC;CAClF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,YAAY,EAC3D,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAClC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,wBAAwB,CAAC,CAYpD"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filters a caller may apply: the declared filters, plus one per
|
|
3
|
+
* filterable dimension that no declared filter is named after.
|
|
4
|
+
*
|
|
5
|
+
* Declared filters add to the dimensions marked filterable rather than
|
|
6
|
+
* replacing them. A declared filter keeps its own name, field and operators,
|
|
7
|
+
* and can expose a dimension marked `filterable: false`. A dimension's own
|
|
8
|
+
* filter takes the operators of the declared filters on its field, so a
|
|
9
|
+
* declaration that narrows a field's operators cannot be bypassed by naming
|
|
10
|
+
* the dimension directly. Without one it gets the default list, which has no
|
|
11
|
+
* null checks (RFC 0023).
|
|
12
|
+
*/
|
|
13
|
+
export function effectiveFilters(source) {
|
|
14
|
+
const filters = { ...source.filters };
|
|
15
|
+
for (const [name, dimension] of Object.entries(source.dimensions)) {
|
|
16
|
+
if (dimension.filterable === false || Object.hasOwn(filters, name))
|
|
17
|
+
continue;
|
|
18
|
+
const operators = declaredOperators(Object.values(source.filters), name);
|
|
19
|
+
filters[name] = {
|
|
20
|
+
__type: 'filter_definition',
|
|
21
|
+
field: name,
|
|
22
|
+
...(operators ? { operators: operators } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return filters;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The operators the declared filters on `field` allow between them, or
|
|
29
|
+
* undefined when none is declared or one allows the default list.
|
|
30
|
+
*/
|
|
31
|
+
function declaredOperators(filters, field) {
|
|
32
|
+
const declared = filters.filter(filter => filter.field === field);
|
|
33
|
+
if (declared.length === 0 || declared.some(filter => !filter.operators))
|
|
34
|
+
return undefined;
|
|
35
|
+
return [...new Set(declared.flatMap(filter => filter.operators ?? []))];
|
|
36
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enforced-constraints.d.ts","sourceRoot":"","sources":["../../src/utils/enforced-constraints.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAgB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"enforced-constraints.d.ts","sourceRoot":"","sources":["../../src/utils/enforced-constraints.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAgB,MAAM,aAAa,CAAC;AAOtF,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAkB3C;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,gBAAgB,GAAG,SAAS,EACrC,aAAa,EAAE,MAAM,GACpB,MAAM,EAAE,CAaV;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAGlG"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isSemanticFilterOperator } from '../constants.js';
|
|
2
2
|
import { validateFilterValue } from '../validation.js';
|
|
3
3
|
import { filterAllowsOperator } from './filter-operator-policy.js';
|
|
4
|
+
import { effectiveFilters } from './effective-filters.js';
|
|
4
5
|
import { isQualifiedField, resolveQualifiedField } from './relationship-fields.js';
|
|
5
6
|
import { validateQualifiedFilter } from './relationship-validation.js';
|
|
6
7
|
/** Constraints are policy, not queries; a handful per call is plenty (RFC 0019). */
|
|
@@ -16,12 +17,13 @@ function constraintError(ds, constraint, context) {
|
|
|
16
17
|
return true;
|
|
17
18
|
return validateQualifiedFilter(ds, filter, context) !== null;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
const filters = effectiveFilters(ds);
|
|
21
|
+
if (!Object.hasOwn(filters, filter.field))
|
|
20
22
|
return true;
|
|
21
|
-
const definition =
|
|
23
|
+
const definition = filters[filter.field];
|
|
22
24
|
if (!filterAllowsOperator(definition, filter.operator))
|
|
23
25
|
return true;
|
|
24
|
-
const fieldType = ds.dimensions[definition.field
|
|
26
|
+
const fieldType = ds.dimensions[definition.field]?.fieldType;
|
|
25
27
|
return !!fieldType && validateFilterValue(filter, fieldType) !== null;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An RFC 6901 pointer as path segments, item indexes as numbers, for zod
|
|
3
|
+
* issues and wire `issues[].path`. Client-contract member names are
|
|
4
|
+
* identifiers, so an all-digit segment is always an index.
|
|
5
|
+
*/
|
|
6
|
+
export declare function pointerSegments(pointer: string): (string | number)[];
|
|
7
|
+
//# sourceMappingURL=json-pointer-segments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-pointer-segments.d.ts","sourceRoot":"","sources":["../../src/utils/json-pointer-segments.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAGpE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An RFC 6901 pointer as path segments, item indexes as numbers, for zod
|
|
3
|
+
* issues and wire `issues[].path`. Client-contract member names are
|
|
4
|
+
* identifiers, so an all-digit segment is always an index.
|
|
5
|
+
*/
|
|
6
|
+
export function pointerSegments(pointer) {
|
|
7
|
+
return pointer.split('/').slice(1)
|
|
8
|
+
.map(part => /^\d+$/.test(part) ? Number(part) : part.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
9
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metric-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-validation.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAI9E,OAAO,EAAsD,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"metric-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-validation.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAI9E,OAAO,EAAsD,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAW3G,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAwIlB"}
|
|
@@ -13,6 +13,7 @@ import { datasetOnTimeKey, selectedTimeKeyError } from './dataset-time-keys.js';
|
|
|
13
13
|
import { enforcedConstraintErrors } from './enforced-constraints.js';
|
|
14
14
|
import { filterAllowsOperator } from './filter-operator-policy.js';
|
|
15
15
|
import { SEMANTIC_FILTER_OPERATORS } from '../constants.js';
|
|
16
|
+
import { effectiveFilters } from './effective-filters.js';
|
|
16
17
|
export function validateMetricQueryInput(metric, query, context) {
|
|
17
18
|
const errors = [...semanticQuerySyntaxErrors(query), ...protocolMetricCapabilityErrors(metric, query), ...queryTimezoneErrors(query.timezone)];
|
|
18
19
|
const ref = getMetricRef(metric);
|
|
@@ -21,7 +22,8 @@ export function validateMetricQueryInput(metric, query, context) {
|
|
|
21
22
|
errors.push(timeKeyError);
|
|
22
23
|
const ds = timeKeyError ? ref.dataset : datasetOnTimeKey(ref.dataset, query.timeKey);
|
|
23
24
|
const dimensionNames = Object.keys(ds.dimensions);
|
|
24
|
-
const
|
|
25
|
+
const filters = effectiveFilters(ds);
|
|
26
|
+
const filterNames = Object.keys(filters);
|
|
25
27
|
const grain = getMetricGrain(metric, query);
|
|
26
28
|
const orderableFields = new Set([
|
|
27
29
|
...(query.dimensions ?? []),
|
|
@@ -72,12 +74,12 @@ export function validateMetricQueryInput(metric, query, context) {
|
|
|
72
74
|
errors.push(`Unknown filter field "${filter.field}". Available: ${filterNames.join(', ')}`);
|
|
73
75
|
continue;
|
|
74
76
|
}
|
|
75
|
-
const filterDefinition =
|
|
77
|
+
const filterDefinition = filters[filter.field];
|
|
76
78
|
if (!filterAllowsOperator(filterDefinition, filter.operator)) {
|
|
77
79
|
errors.push(`Filter "${filter.field}" does not allow operator "${filter.operator}". Allowed: ${(filterDefinition?.operators ?? SEMANTIC_FILTER_OPERATORS).join(', ')}`);
|
|
78
80
|
continue;
|
|
79
81
|
}
|
|
80
|
-
const resolvedField =
|
|
82
|
+
const resolvedField = filterDefinition.field;
|
|
81
83
|
if (isTenantScopedFilter(ds, filter, context)) {
|
|
82
84
|
errors.push(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
|
|
83
85
|
continue;
|
|
@@ -52,8 +52,13 @@ type QueryLike = Pick<DatasetQuery & MetricQuery, 'dimensions' | 'filters' | 'or
|
|
|
52
52
|
* here is skipped defensively.
|
|
53
53
|
*/
|
|
54
54
|
export declare function buildRelationshipBuilderContext(ds: AnyDatasetInstance, query: QueryLike, context?: ExecutionContext, dialect?: DatasetSqlDialectRules): RelationshipBuilderContext | undefined;
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Qualifies a base physical column with the base source. Always qualified:
|
|
57
|
+
* ClickHouse resolves an output alias before a column of the same name, so a
|
|
58
|
+
* bare `status` could read a dimension aliased `status`, and a bare `amount`
|
|
59
|
+
* inside another aggregate could read a measure aliased `amount`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function qualifyBaseColumn(ctx: RelationshipBuilderContext | undefined, column: string, source: string): string;
|
|
57
62
|
/**
|
|
58
63
|
* Resolves a relationship-qualified field to a null-guarded SQL expression:
|
|
59
64
|
* the target column when the join matched, NULL otherwise
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAIxE;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG7F,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,uFAAuF;IACvF,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACrD,oEAAoE;IACpE,OAAO,EAAE,sBAAsB,CAAC;CACjC;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,GAAG,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEzH;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,GAAE,sBAAoD,GAC5D,0BAA0B,GAAG,SAAS,CAmDxC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,0BAA0B,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,0BAA0B,GAC9B,MAAM,CAaR;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAoClB"}
|
|
@@ -2,6 +2,7 @@ import { relationshipKeys } from './relationship-keys.js';
|
|
|
2
2
|
import { relationshipJoinSource } from './relationship-measure-sql.js';
|
|
3
3
|
import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
|
|
4
4
|
import { resolveRelationshipReference } from './relationship-measures.js';
|
|
5
|
+
import { joinsWithinTenant } from './tenant-runtime.js';
|
|
5
6
|
import { datasetRowScope, runtimeConstraints } from './row-scope.js';
|
|
6
7
|
import { isQualifiedField, resolveQualifiedField } from './relationship-fields.js';
|
|
7
8
|
/**
|
|
@@ -34,7 +35,11 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
|
|
|
34
35
|
const measureNames = (query.measures ?? [])
|
|
35
36
|
.filter(name => name.startsWith(`${relationshipName}.`))
|
|
36
37
|
.map(name => name.slice(relationshipName.length + 1));
|
|
37
|
-
const
|
|
38
|
+
const authoredKeys = relationshipKeys(relationship);
|
|
39
|
+
// Under a scope of several tenants, a key reused by another tenant must not match.
|
|
40
|
+
const keys = joinsWithinTenant(ds, target, context)
|
|
41
|
+
? [...authoredKeys, { from: ds.tenantKey, to: target.tenantKey }]
|
|
42
|
+
: authoredKeys;
|
|
38
43
|
const projected = relationshipJoinSource(target, keys.map(key => key.to), measureNames, dialect);
|
|
39
44
|
joinByRelationship.set(relationshipName, {
|
|
40
45
|
relationship: relationshipName,
|
|
@@ -54,9 +59,14 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
|
|
|
54
59
|
dialect,
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Qualifies a base physical column with the base source. Always qualified:
|
|
64
|
+
* ClickHouse resolves an output alias before a column of the same name, so a
|
|
65
|
+
* bare `status` could read a dimension aliased `status`, and a bare `amount`
|
|
66
|
+
* inside another aggregate could read a measure aliased `amount`.
|
|
67
|
+
*/
|
|
68
|
+
export function qualifyBaseColumn(ctx, column, source) {
|
|
69
|
+
return `${ctx ? ctx.baseSource : source}.${column}`;
|
|
60
70
|
}
|
|
61
71
|
/**
|
|
62
72
|
* Resolves a relationship-qualified field to a null-guarded SQL expression:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AAYrB,KAAK,iBAAiB,GAAG,IAAI,CAC3B,YAAY,GAAG,WAAW,EAC1B,YAAY,GAAG,SAAS,GAAG,SAAS,CACrC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,iBAAiB,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAqBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CA4Bf"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveRelationshipReference } from './relationship-measures.js';
|
|
2
2
|
import { filterAllowsOperator } from './filter-operator-policy.js';
|
|
3
|
+
import { effectiveFilters } from './effective-filters.js';
|
|
3
4
|
import { validateFilterValue } from '../validation.js';
|
|
4
5
|
import { getRuntimeTenantPredicate, hasTenantRuntime, } from './tenant-runtime.js';
|
|
5
6
|
import { runtimeConstraints } from './row-scope.js';
|
|
@@ -48,10 +49,10 @@ export function validateQualifiedFilter(ds, filter, context) {
|
|
|
48
49
|
if (getRuntimeTenantPredicate(context) && target.tenantKey && targetColumn === target.tenantKey) {
|
|
49
50
|
return `Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`;
|
|
50
51
|
}
|
|
51
|
-
// A relationship
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
const definition = target
|
|
52
|
+
// A relationship exposes the target's filters named after a dimension:
|
|
53
|
+
// its filterable dimensions, and declared filters, which can override
|
|
54
|
+
// filterable: false. A filter alias is not reachable through the hop.
|
|
55
|
+
const definition = effectiveFilters(target)[targetDimensionName];
|
|
55
56
|
if (!definition
|
|
56
57
|
|| definition.field !== targetDimensionName) {
|
|
57
58
|
return `Filter "${filter.field}" is not exposed by target dataset "${target.name}".`;
|
|
@@ -21,7 +21,7 @@ export function scopedDatasetSource(options) {
|
|
|
21
21
|
// A resolved join target is read with FINAL even when this dataset isn't.
|
|
22
22
|
if (queryReadsFinal(ds, joinCtx) && qb.settings)
|
|
23
23
|
qb = qb.settings(FINAL_SAFETY_SETTINGS);
|
|
24
|
-
qb = applyRowScope(qb, datasetRowScope(ds, context), column => qualifyBaseColumn(joinCtx, column));
|
|
24
|
+
qb = applyRowScope(qb, datasetRowScope(ds, context), column => qualifyBaseColumn(joinCtx, column, ds.source));
|
|
25
25
|
// Constraints are filters in the dataset's vocabulary, applied exactly as a
|
|
26
26
|
// caller filter on the same field would be.
|
|
27
27
|
for (const constraint of runtimeConstraints(context)) {
|
|
@@ -9,6 +9,13 @@ export type TenantPredicate = {
|
|
|
9
9
|
export declare function getRuntimeTenantPredicate(context?: ExecutionContext): TenantPredicate | undefined;
|
|
10
10
|
export declare function getRuntimeTenantId(context?: ExecutionContext): string | undefined;
|
|
11
11
|
export declare function isCrossTenantRuntime(context?: ExecutionContext): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a join from `base` to `target` must also match their tenant keys.
|
|
14
|
+
* One tenant pins both sides to the same id already. A scope over several
|
|
15
|
+
* tenants, or all of them, admits every scoped tenant's target rows, so a key
|
|
16
|
+
* reused across tenants would otherwise match another tenant's row.
|
|
17
|
+
*/
|
|
18
|
+
export declare function joinsWithinTenant(base: Pick<AnyDatasetInstance, 'tenantKey'>, target: Pick<AnyDatasetInstance, 'tenantKey'>, context?: ExecutionContext): boolean;
|
|
12
19
|
export declare function hasTenantRuntime(context?: ExecutionContext): boolean;
|
|
13
20
|
export declare function validateTenantRuntime(ds: AnyDatasetInstance, context?: ExecutionContext): string | undefined;
|
|
14
21
|
//# sourceMappingURL=tenant-runtime.d.ts.map
|
|
@@ -1 +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"}
|
|
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;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,EAC3C,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,EAC7C,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAST;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"}
|
|
@@ -22,6 +22,22 @@ export function isCrossTenantRuntime(context) {
|
|
|
22
22
|
const tenant = context?.runtime?.tenant;
|
|
23
23
|
return !!tenant && typeof tenant === 'object' && 'scope' in tenant && tenant.scope === 'all';
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether a join from `base` to `target` must also match their tenant keys.
|
|
27
|
+
* One tenant pins both sides to the same id already. A scope over several
|
|
28
|
+
* tenants, or all of them, admits every scoped tenant's target rows, so a key
|
|
29
|
+
* reused across tenants would otherwise match another tenant's row.
|
|
30
|
+
*/
|
|
31
|
+
export function joinsWithinTenant(base, target, context) {
|
|
32
|
+
if (!base.tenantKey || !target.tenantKey) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const predicate = getRuntimeTenantPredicate(context);
|
|
36
|
+
if (predicate) {
|
|
37
|
+
return predicate.operator === 'in' && predicate.value.length > 1;
|
|
38
|
+
}
|
|
39
|
+
return isCrossTenantRuntime(context);
|
|
40
|
+
}
|
|
25
41
|
export function hasTenantRuntime(context) {
|
|
26
42
|
return !!getRuntimeTenantPredicate(context) || isCrossTenantRuntime(context);
|
|
27
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/datasets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Code-first TypeScript semantic layer for ClickHouse datasets, metrics, multi-tenancy, and AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"semantic-layer",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"!dist/**/type-tests/**"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@hypequery/protocol": "^0.24.
|
|
58
|
+
"@hypequery/protocol": "^0.24.2",
|
|
59
59
|
"@noble/hashes": "^2.4.0",
|
|
60
60
|
"zod": "^4.6.6",
|
|
61
61
|
"zod-to-json-schema": "^3.25.2"
|