@hypequery/datasets 0.0.0-canary-20260520183507

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.
Files changed (50) hide show
  1. package/LICENSE +201 -0
  2. package/dist/aggregations.d.ts +24 -0
  3. package/dist/aggregations.d.ts.map +1 -0
  4. package/dist/aggregations.js +41 -0
  5. package/dist/constants.d.ts +9 -0
  6. package/dist/constants.d.ts.map +1 -0
  7. package/dist/constants.js +13 -0
  8. package/dist/dataset.d.ts +33 -0
  9. package/dist/dataset.d.ts.map +1 -0
  10. package/dist/dataset.js +203 -0
  11. package/dist/executor.d.ts +39 -0
  12. package/dist/executor.d.ts.map +1 -0
  13. package/dist/executor.js +244 -0
  14. package/dist/field.d.ts +25 -0
  15. package/dist/field.d.ts.map +1 -0
  16. package/dist/field.js +35 -0
  17. package/dist/formulas.d.ts +25 -0
  18. package/dist/formulas.d.ts.map +1 -0
  19. package/dist/formulas.js +60 -0
  20. package/dist/index.d.ts +18 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +25 -0
  23. package/dist/measure.d.ts +10 -0
  24. package/dist/measure.d.ts.map +1 -0
  25. package/dist/measure.js +18 -0
  26. package/dist/query-builder-protocol.d.ts +36 -0
  27. package/dist/query-builder-protocol.d.ts.map +1 -0
  28. package/dist/query-builder-protocol.js +11 -0
  29. package/dist/query-helpers.d.ts +30 -0
  30. package/dist/query-helpers.d.ts.map +1 -0
  31. package/dist/query-helpers.js +55 -0
  32. package/dist/query-planner.d.ts +13 -0
  33. package/dist/query-planner.d.ts.map +1 -0
  34. package/dist/query-planner.js +87 -0
  35. package/dist/registry.d.ts +8 -0
  36. package/dist/registry.d.ts.map +1 -0
  37. package/dist/registry.js +25 -0
  38. package/dist/relationships.d.ts +44 -0
  39. package/dist/relationships.d.ts.map +1 -0
  40. package/dist/relationships.js +39 -0
  41. package/dist/sql-utils.d.ts +28 -0
  42. package/dist/sql-utils.d.ts.map +1 -0
  43. package/dist/sql-utils.js +38 -0
  44. package/dist/types.d.ts +214 -0
  45. package/dist/types.d.ts.map +1 -0
  46. package/dist/types.js +1 -0
  47. package/dist/validation.d.ts +28 -0
  48. package/dist/validation.d.ts.map +1 -0
  49. package/dist/validation.js +73 -0
  50. package/package.json +49 -0
@@ -0,0 +1,87 @@
1
+ import { GRAIN_FUNCTIONS } from "./constants.js";
2
+ export function resolveDimensionExpression(ds, dimensionName) {
3
+ const definition = ds.dimensions[dimensionName];
4
+ return definition?.sql ?? definition?.column ?? dimensionName;
5
+ }
6
+ export function resolveFilterField(ds, filterField) {
7
+ const resolvedField = ds.filters[filterField]?.field ?? filterField;
8
+ return resolveDimensionExpression(ds, resolvedField);
9
+ }
10
+ export function buildDimensionSelectionPlan(ds, dimensions, grain) {
11
+ const selectParts = [];
12
+ const groupByParts = [];
13
+ if (grain) {
14
+ const fn = GRAIN_FUNCTIONS[grain];
15
+ selectParts.push(`${fn}(${ds.timeKey}) AS period`);
16
+ groupByParts.push("period");
17
+ }
18
+ for (const dimensionName of dimensions) {
19
+ const expression = resolveDimensionExpression(ds, dimensionName);
20
+ if (expression === dimensionName) {
21
+ selectParts.push(dimensionName);
22
+ }
23
+ else {
24
+ selectParts.push(`${expression} AS ${dimensionName}`);
25
+ }
26
+ groupByParts.push(dimensionName);
27
+ }
28
+ return { selectParts, groupByParts };
29
+ }
30
+ export function applyAggregationSpec(qb, ds, spec, alias) {
31
+ const fieldOrExpr = resolveDimensionExpression(ds, spec.field);
32
+ switch (spec.aggregation) {
33
+ case "sum":
34
+ return qb.sum(fieldOrExpr, alias);
35
+ case "count":
36
+ return qb.count(fieldOrExpr, alias);
37
+ case "countDistinct":
38
+ return qb.countDistinct(fieldOrExpr, alias);
39
+ case "avg":
40
+ return qb.avg(fieldOrExpr, alias);
41
+ case "min":
42
+ return qb.min(fieldOrExpr, alias);
43
+ case "max":
44
+ return qb.max(fieldOrExpr, alias);
45
+ default:
46
+ throw new Error(`Unknown aggregation type: ${spec.aggregation}`);
47
+ }
48
+ }
49
+ export function applyMeasureDefinition(qb, ds, name, definition) {
50
+ const fieldOrExpr = definition.sql ?? resolveDimensionExpression(ds, definition.field);
51
+ switch (definition.aggregation) {
52
+ case "sum":
53
+ return qb.sum(fieldOrExpr, name);
54
+ case "count":
55
+ return qb.count(fieldOrExpr, name);
56
+ case "countDistinct":
57
+ return qb.countDistinct(fieldOrExpr, name);
58
+ case "avg":
59
+ return qb.avg(fieldOrExpr, name);
60
+ case "min":
61
+ return qb.min(fieldOrExpr, name);
62
+ case "max":
63
+ return qb.max(fieldOrExpr, name);
64
+ default:
65
+ throw new Error(`Unsupported measure aggregation: ${definition.aggregation}`);
66
+ }
67
+ }
68
+ export function appendOrderLimitOffset(qb, orderBy, grain, limit, offset) {
69
+ if (orderBy && orderBy.length > 0) {
70
+ for (const order of orderBy) {
71
+ qb = qb.orderBy(order.field, order.direction.toUpperCase());
72
+ }
73
+ }
74
+ else if (grain) {
75
+ qb = qb.orderBy("period", "ASC");
76
+ }
77
+ if (limit != null) {
78
+ qb = qb.limit(limit);
79
+ }
80
+ if (offset != null) {
81
+ qb = qb.offset(offset);
82
+ }
83
+ return qb;
84
+ }
85
+ export function resolveTenantFilterColumn(_ds, context) {
86
+ return context?.runtime?.tenant?.column;
87
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * DatasetRegistry — runtime registry of defined datasets.
3
+ *
4
+ * Used by MetricExecutor and serve() to discover datasets at startup.
5
+ */
6
+ import type { DatasetRegistryInstance } from './types.js';
7
+ export declare function createDatasetRegistry(): DatasetRegistryInstance;
8
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAmB,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE3E,wBAAgB,qBAAqB,IAAI,uBAAuB,CAyB/D"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * DatasetRegistry — runtime registry of defined datasets.
3
+ *
4
+ * Used by MetricExecutor and serve() to discover datasets at startup.
5
+ */
6
+ export function createDatasetRegistry() {
7
+ const datasets = new Map();
8
+ return {
9
+ register(ds) {
10
+ if (datasets.has(ds.name)) {
11
+ throw new Error(`Dataset "${ds.name}" is already registered. Dataset names must be unique.`);
12
+ }
13
+ datasets.set(ds.name, ds);
14
+ },
15
+ get(name) {
16
+ return datasets.get(name);
17
+ },
18
+ getAll() {
19
+ return Array.from(datasets.values());
20
+ },
21
+ has(name) {
22
+ return datasets.has(name);
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Relationship helpers for dataset definitions.
3
+ *
4
+ * These helpers currently define semantic model metadata only. The shipped
5
+ * executor does not yet resolve relationship paths into joined dataset queries
6
+ * or cross-dataset metrics.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const Orders = dataset("orders", {
11
+ * source: "orders",
12
+ * fields: { ... },
13
+ * relationships: {
14
+ * customer: belongsTo(() => Customers, { from: "customerId", to: "id" }),
15
+ * },
16
+ * });
17
+ * ```
18
+ */
19
+ import type { RelationshipDefinition } from './types.js';
20
+ /** Many-to-one relationship (FK on this table). */
21
+ export declare function belongsTo(target: () => {
22
+ __type: 'dataset';
23
+ name: string;
24
+ }, join: {
25
+ from: string;
26
+ to: string;
27
+ }): RelationshipDefinition;
28
+ /** One-to-many relationship (FK on target table). */
29
+ export declare function hasMany(target: () => {
30
+ __type: 'dataset';
31
+ name: string;
32
+ }, join: {
33
+ from: string;
34
+ to: string;
35
+ }): RelationshipDefinition;
36
+ /** One-to-one relationship (FK on target table). */
37
+ export declare function hasOne(target: () => {
38
+ __type: 'dataset';
39
+ name: string;
40
+ }, join: {
41
+ from: string;
42
+ to: string;
43
+ }): RelationshipDefinition;
44
+ //# sourceMappingURL=relationships.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../src/relationships.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAoB,MAAM,YAAY,CAAC;AAgB3E,mDAAmD;AACnD,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB;AAED,oDAAoD;AACpD,wBAAgB,MAAM,CACpB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Relationship helpers for dataset definitions.
3
+ *
4
+ * These helpers currently define semantic model metadata only. The shipped
5
+ * executor does not yet resolve relationship paths into joined dataset queries
6
+ * or cross-dataset metrics.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const Orders = dataset("orders", {
11
+ * source: "orders",
12
+ * fields: { ... },
13
+ * relationships: {
14
+ * customer: belongsTo(() => Customers, { from: "customerId", to: "id" }),
15
+ * },
16
+ * });
17
+ * ```
18
+ */
19
+ function createRelationship(kind, target, join) {
20
+ return {
21
+ __type: 'relationship',
22
+ kind,
23
+ target,
24
+ from: join.from,
25
+ to: join.to,
26
+ };
27
+ }
28
+ /** Many-to-one relationship (FK on this table). */
29
+ export function belongsTo(target, join) {
30
+ return createRelationship('belongsTo', target, join);
31
+ }
32
+ /** One-to-many relationship (FK on target table). */
33
+ export function hasMany(target, join) {
34
+ return createRelationship('hasMany', target, join);
35
+ }
36
+ /** One-to-one relationship (FK on target table). */
37
+ export function hasOne(target, join) {
38
+ return createRelationship('hasOne', target, join);
39
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * SQL utility functions for safe SQL generation.
3
+ */
4
+ /**
5
+ * Validates that a string is a safe SQL identifier (column/table name).
6
+ * Allows only alphanumeric characters and underscores, starting with letter or underscore.
7
+ *
8
+ * @param identifier - The identifier to validate
9
+ * @returns true if valid, false otherwise
10
+ */
11
+ export declare function isSafeSQLIdentifier(identifier: string): boolean;
12
+ /**
13
+ * Validates and throws if identifier is not safe for SQL.
14
+ *
15
+ * @param identifier - The identifier to validate
16
+ * @param context - Context for error message (e.g., "dimension name", "field name")
17
+ * @throws Error if identifier is not safe
18
+ */
19
+ export declare function validateSQLIdentifier(identifier: string, context: string): void;
20
+ /**
21
+ * Quotes a SQL identifier for safe use in queries.
22
+ * Uses double quotes which is standard SQL.
23
+ *
24
+ * @param identifier - The identifier to quote
25
+ * @returns Quoted identifier
26
+ */
27
+ export declare function quoteSQLIdentifier(identifier: string): string;
28
+ //# sourceMappingURL=sql-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-utils.d.ts","sourceRoot":"","sources":["../src/sql-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAO/E;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SQL utility functions for safe SQL generation.
3
+ */
4
+ /**
5
+ * Validates that a string is a safe SQL identifier (column/table name).
6
+ * Allows only alphanumeric characters and underscores, starting with letter or underscore.
7
+ *
8
+ * @param identifier - The identifier to validate
9
+ * @returns true if valid, false otherwise
10
+ */
11
+ export function isSafeSQLIdentifier(identifier) {
12
+ return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier);
13
+ }
14
+ /**
15
+ * Validates and throws if identifier is not safe for SQL.
16
+ *
17
+ * @param identifier - The identifier to validate
18
+ * @param context - Context for error message (e.g., "dimension name", "field name")
19
+ * @throws Error if identifier is not safe
20
+ */
21
+ export function validateSQLIdentifier(identifier, context) {
22
+ if (!isSafeSQLIdentifier(identifier)) {
23
+ throw new Error(`Invalid ${context}: "${identifier}". Must contain only letters, numbers, and underscores, ` +
24
+ `and start with a letter or underscore.`);
25
+ }
26
+ }
27
+ /**
28
+ * Quotes a SQL identifier for safe use in queries.
29
+ * Uses double quotes which is standard SQL.
30
+ *
31
+ * @param identifier - The identifier to quote
32
+ * @returns Quoted identifier
33
+ */
34
+ export function quoteSQLIdentifier(identifier) {
35
+ // Escape any existing double quotes by doubling them
36
+ const escaped = identifier.replace(/"/g, '""');
37
+ return `"${escaped}"`;
38
+ }
@@ -0,0 +1,214 @@
1
+ import type { QueryBuilderFactoryLike } from './query-builder-protocol.js';
2
+ export type FieldType = 'string' | 'number' | 'boolean' | 'timestamp';
3
+ export type DimensionType = FieldType;
4
+ export interface DimensionOptions {
5
+ label?: string;
6
+ description?: string;
7
+ column?: string;
8
+ sql?: string;
9
+ filterable?: boolean;
10
+ groupable?: boolean;
11
+ }
12
+ export interface DimensionDefinition<TType extends DimensionType = DimensionType> {
13
+ __type: 'field_definition';
14
+ fieldType: TType;
15
+ label?: string;
16
+ description?: string;
17
+ column?: string;
18
+ sql?: string;
19
+ filterable?: boolean;
20
+ groupable?: boolean;
21
+ }
22
+ export type InferDimensionType<T extends DimensionDefinition> = T['fieldType'] extends 'string' ? string : T['fieldType'] extends 'number' ? number : T['fieldType'] extends 'boolean' ? boolean : T['fieldType'] extends 'timestamp' ? string : never;
23
+ export type RelationshipKind = 'belongsTo' | 'hasMany' | 'hasOne';
24
+ export interface RelationshipDefinition {
25
+ __type: 'relationship';
26
+ kind: RelationshipKind;
27
+ target: () => {
28
+ __type: 'dataset';
29
+ name: string;
30
+ };
31
+ from: string;
32
+ to: string;
33
+ }
34
+ export type AggregationType = 'sum' | 'count' | 'countDistinct' | 'avg' | 'min' | 'max';
35
+ export type MeasureAggregation = AggregationType;
36
+ export interface AggregationSpec {
37
+ __type: 'aggregation_spec';
38
+ aggregation: AggregationType;
39
+ field: string;
40
+ }
41
+ export interface MeasureOptions {
42
+ sql?: string;
43
+ label?: string;
44
+ description?: string;
45
+ }
46
+ export interface MeasureDefinition {
47
+ __type: 'measure_definition';
48
+ aggregation: MeasureAggregation;
49
+ field: string;
50
+ sql?: string;
51
+ label?: string;
52
+ description?: string;
53
+ }
54
+ export type FormulaExpr = {
55
+ __type: 'formula_expr';
56
+ toSQL: () => string;
57
+ };
58
+ export interface MetricRef<TDatasetName extends string = string, TMetricName extends string = string> {
59
+ __type: 'metric_ref';
60
+ datasetName: TDatasetName;
61
+ name: TMetricName;
62
+ spec: AggregationSpec | DerivedMetricSpec;
63
+ label?: string;
64
+ description?: string;
65
+ dataset: DatasetInstance<any, any, any>;
66
+ by(grain: TimeGrain): GrainedMetricRef<TDatasetName, TMetricName>;
67
+ contract(): MetricContract;
68
+ }
69
+ export interface DerivedMetricSpec {
70
+ __type: 'derived_metric_spec';
71
+ uses: Record<string, MetricRef>;
72
+ formula: (inputs: Record<string, string>) => FormulaExpr;
73
+ }
74
+ export interface GrainedMetricRef<TDatasetName extends string = string, TMetricName extends string = string> {
75
+ __type: 'grained_metric_ref';
76
+ metric: MetricRef<TDatasetName, TMetricName>;
77
+ grain: TimeGrain;
78
+ contract(): MetricContract;
79
+ }
80
+ export type MetricHandle<TDatasetName extends string = string, TMetricName extends string = string> = MetricRef<TDatasetName, TMetricName> | GrainedMetricRef<TDatasetName, TMetricName>;
81
+ export type TimeGrain = 'day' | 'week' | 'month' | 'quarter' | 'year';
82
+ export interface MetricContract {
83
+ kind: 'metric' | 'derived_metric' | 'grained_metric';
84
+ name: string;
85
+ dataset: string;
86
+ valueType: 'number';
87
+ label?: string;
88
+ description?: string;
89
+ dimensions: string[];
90
+ measures?: string[];
91
+ filters: string[];
92
+ grains: TimeGrain[];
93
+ grain?: TimeGrain;
94
+ requires?: string[];
95
+ tenantScoped: boolean;
96
+ }
97
+ export interface MetricFilter {
98
+ field: string;
99
+ operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'between' | 'like';
100
+ value: unknown;
101
+ }
102
+ export interface MetricOrderBy {
103
+ field: string;
104
+ direction: 'asc' | 'desc';
105
+ }
106
+ export interface MetricQuery {
107
+ dimensions?: string[];
108
+ filters?: MetricFilter[];
109
+ orderBy?: MetricOrderBy[];
110
+ limit?: number;
111
+ offset?: number;
112
+ by?: TimeGrain;
113
+ }
114
+ export interface MetricResultMeta {
115
+ timingMs?: number;
116
+ sql?: string;
117
+ tenant?: string;
118
+ }
119
+ export interface MetricResult<T = Record<string, unknown>> {
120
+ data: T[];
121
+ meta?: MetricResultMeta;
122
+ }
123
+ export interface SemanticTenantRuntime {
124
+ id: string;
125
+ column: string;
126
+ handledByBuilder: boolean;
127
+ }
128
+ export interface SemanticExecutionRuntime {
129
+ builderFactory?: QueryBuilderFactoryLike;
130
+ tenant?: SemanticTenantRuntime;
131
+ }
132
+ export interface ExecutionContext {
133
+ runtime?: SemanticExecutionRuntime;
134
+ }
135
+ export interface SemanticFilterDefinition {
136
+ __type: 'filter_definition';
137
+ field: string;
138
+ label?: string;
139
+ description?: string;
140
+ operators?: MetricFilter['operator'][];
141
+ }
142
+ export type SemanticFiltersDefinition = Record<string, SemanticFilterDefinition>;
143
+ export interface DatasetLimits {
144
+ maxDimensions?: number;
145
+ maxMeasures?: number;
146
+ maxFilters?: number;
147
+ maxResultSize?: number;
148
+ }
149
+ export interface BaseMetricConfig<TDimensions extends Record<string, DimensionDefinition> = Record<string, DimensionDefinition>, TMeasures extends Record<string, MeasureDefinition> = Record<string, MeasureDefinition>> {
150
+ measure: keyof TMeasures & string;
151
+ label?: string;
152
+ description?: string;
153
+ }
154
+ export interface DerivedMetricConfig {
155
+ uses: Record<string, MetricRef>;
156
+ formula: (inputs: Record<string, string>) => FormulaExpr;
157
+ label?: string;
158
+ description?: string;
159
+ }
160
+ export interface DatasetQueryConfig<TDimensions extends Record<string, DimensionDefinition> = Record<string, DimensionDefinition>, TMeasures extends Record<string, MeasureDefinition> = Record<string, MeasureDefinition>> {
161
+ dimensions?: Array<keyof TDimensions & string>;
162
+ measures?: Array<keyof TMeasures & string>;
163
+ filters?: MetricFilter[];
164
+ orderBy?: MetricOrderBy[];
165
+ limit?: number;
166
+ offset?: number;
167
+ by?: TimeGrain;
168
+ }
169
+ export interface DatasetQueryContract {
170
+ dataset: string;
171
+ dimensions: string[];
172
+ measures: string[];
173
+ filters: string[];
174
+ grains: TimeGrain[];
175
+ tenantScoped: boolean;
176
+ }
177
+ export interface DatasetQueryRef<TDimensions extends Record<string, DimensionDefinition> = Record<string, DimensionDefinition>, TMeasures extends Record<string, MeasureDefinition> = Record<string, MeasureDefinition>> {
178
+ __type: 'dataset_query_ref';
179
+ dataset: DatasetInstance<TDimensions, TMeasures, any>;
180
+ config: DatasetQueryConfig<TDimensions, TMeasures>;
181
+ contract(): DatasetQueryContract;
182
+ }
183
+ export interface DatasetConfig<TDimensions extends Record<string, DimensionDefinition> = Record<string, DimensionDefinition>, TMeasures extends Record<string, MeasureDefinition> = Record<string, MeasureDefinition>, TRelationships extends Record<string, RelationshipDefinition> = Record<string, never>> {
184
+ source: string;
185
+ tenantKey?: string;
186
+ timeKey?: string;
187
+ dimensions: TDimensions;
188
+ measures?: TMeasures;
189
+ filters?: SemanticFiltersDefinition;
190
+ relationships?: TRelationships;
191
+ limits?: DatasetLimits;
192
+ }
193
+ export interface DatasetInstance<TDimensions extends Record<string, DimensionDefinition> = Record<string, DimensionDefinition>, TMeasures extends Record<string, MeasureDefinition> = Record<string, MeasureDefinition>, TRelationships extends Record<string, RelationshipDefinition> = Record<string, never>> {
194
+ __type: 'dataset';
195
+ name: string;
196
+ source: string;
197
+ tenantKey?: string;
198
+ timeKey?: string;
199
+ dimensions: TDimensions;
200
+ measures: TMeasures;
201
+ filters: SemanticFiltersDefinition;
202
+ relationships: TRelationships;
203
+ limits?: DatasetLimits;
204
+ metric<TName extends string>(metricName: TName, metricConfig: BaseMetricConfig<TDimensions, TMeasures> | DerivedMetricConfig): MetricRef<string, TName>;
205
+ query(config: DatasetQueryConfig<TDimensions, TMeasures>): DatasetQueryRef<TDimensions, TMeasures>;
206
+ }
207
+ export interface DatasetRegistryInstance {
208
+ register(ds: DatasetInstance): void;
209
+ get(name: string): DatasetInstance | undefined;
210
+ getAll(): DatasetInstance[];
211
+ has(name: string): boolean;
212
+ }
213
+ export type DatasetFieldNames<TDataset extends DatasetInstance> = keyof TDataset['dimensions'] & string;
214
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAE3E,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAC;AACtE,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC;AAEtC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB,CAAC,KAAK,SAAS,aAAa,GAAG,aAAa;IAC9E,MAAM,EAAE,kBAAkB,CAAC;IAC3B,SAAS,EAAE,KAAK,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,mBAAmB,IAC1D,CAAC,CAAC,WAAW,CAAC,SAAS,QAAQ,GAAG,MAAM,GACxC,CAAC,CAAC,WAAW,CAAC,SAAS,QAAQ,GAAG,MAAM,GACxC,CAAC,CAAC,WAAW,CAAC,SAAS,SAAS,GAAG,OAAO,GAC1C,CAAC,CAAC,WAAW,CAAC,SAAS,WAAW,GAAG,MAAM,GAC3C,KAAK,CAAC;AAER,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElE,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,OAAO,GAAG,eAAe,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AACxF,MAAM,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,WAAW,EAAE,eAAe,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,WAAW,EAAE,kBAAkB,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,WAAW,SAAS,CACxB,YAAY,SAAS,MAAM,GAAG,MAAM,EACpC,WAAW,SAAS,MAAM,GAAG,MAAM;IAEnC,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,EAAE,YAAY,CAAC;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IACxC,EAAE,CAAC,KAAK,EAAE,SAAS,GAAG,gBAAgB,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IAClE,QAAQ,IAAI,cAAc,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAChC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,WAAW,CAAC;CAC1D;AAED,MAAM,WAAW,gBAAgB,CAC/B,YAAY,SAAS,MAAM,GAAG,MAAM,EACpC,WAAW,SAAS,MAAM,GAAG,MAAM;IAEnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,MAAM,EAAE,SAAS,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IAC7C,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,IAAI,cAAc,CAAC;CAC5B;AAED,MAAM,MAAM,YAAY,CACtB,YAAY,SAAS,MAAM,GAAG,MAAM,EACpC,WAAW,SAAS,MAAM,GAAG,MAAM,IACjC,SAAS,CAAC,YAAY,EAAE,WAAW,CAAC,GAAG,gBAAgB,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;AAEvF,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEtE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,GAAG,gBAAgB,GAAG,gBAAgB,CAAC;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IAC3F,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,SAAS,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACvD,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,cAAc,CAAC,EAAE,uBAAuB,CAAC;IACzC,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,wBAAwB,CAAC;CACpC;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;CACxC;AAED,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAEjF,MAAM,WAAW,aAAa;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB,CAC/B,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7F,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAEvF,OAAO,EAAE,MAAM,SAAS,GAAG,MAAM,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAChC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,WAAW,CAAC;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB,CACjC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7F,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAEvF,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC;IAC/C,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC;IAC3C,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,SAAS,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,eAAe,CAC9B,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7F,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAEvF,MAAM,EAAE,mBAAmB,CAAC;IAC5B,OAAO,EAAE,eAAe,CAAC,WAAW,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;IACtD,MAAM,EAAE,kBAAkB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACnD,QAAQ,IAAI,oBAAoB,CAAC;CAClC;AAED,MAAM,WAAW,aAAa,CAC5B,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7F,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACvF,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAErF,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,WAAW,CAAC;IACxB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,yBAAyB,CAAC;IACpC,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,eAAe,CAC9B,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC7F,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,EACvF,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;IAErF,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,WAAW,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,EAAE,yBAAyB,CAAC;IACnC,aAAa,EAAE,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,MAAM,CAAC,KAAK,SAAS,MAAM,EACzB,UAAU,EAAE,KAAK,EACjB,YAAY,EAAE,gBAAgB,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,mBAAmB,GAC3E,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5B,KAAK,CAAC,MAAM,EAAE,kBAAkB,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,eAAe,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;CACpG;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAC/C,MAAM,IAAI,eAAe,EAAE,CAAC;IAC5B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,iBAAiB,CAAC,QAAQ,SAAS,eAAe,IAC5D,MAAM,QAAQ,CAAC,YAAY,CAAC,GAAG,MAAM,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared validation utilities for semantic layer queries.
3
+ */
4
+ import type { FieldType, MetricFilter } from './types.js';
5
+ /**
6
+ * Result of a validation operation.
7
+ */
8
+ export interface ValidationResult {
9
+ valid: boolean;
10
+ errors: string[];
11
+ }
12
+ /**
13
+ * Checks if a value matches the expected field type.
14
+ *
15
+ * @param fieldType - The expected field type
16
+ * @param value - The value to check
17
+ * @returns true if the value matches the field type
18
+ */
19
+ export declare function matchesFieldType(fieldType: FieldType, value: unknown): boolean;
20
+ /**
21
+ * Validates a filter value against the expected field type.
22
+ *
23
+ * @param filter - The filter to validate
24
+ * @param fieldType - The expected field type
25
+ * @returns null if valid, error message if invalid
26
+ */
27
+ export declare function validateFilterValue(filter: MetricFilter, fieldType: FieldType): string | null;
28
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAY9E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CA0C7F"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared validation utilities for semantic layer queries.
3
+ */
4
+ /**
5
+ * Checks if a value matches the expected field type.
6
+ *
7
+ * @param fieldType - The expected field type
8
+ * @param value - The value to check
9
+ * @returns true if the value matches the field type
10
+ */
11
+ export function matchesFieldType(fieldType, value) {
12
+ switch (fieldType) {
13
+ case 'string':
14
+ case 'timestamp':
15
+ return typeof value === 'string';
16
+ case 'number':
17
+ return typeof value === 'number' && Number.isFinite(value);
18
+ case 'boolean':
19
+ return typeof value === 'boolean';
20
+ default:
21
+ return false;
22
+ }
23
+ }
24
+ /**
25
+ * Validates a filter value against the expected field type.
26
+ *
27
+ * @param filter - The filter to validate
28
+ * @param fieldType - The expected field type
29
+ * @returns null if valid, error message if invalid
30
+ */
31
+ export function validateFilterValue(filter, fieldType) {
32
+ switch (filter.operator) {
33
+ case 'eq':
34
+ case 'neq':
35
+ return matchesFieldType(fieldType, filter.value)
36
+ ? null
37
+ : `"${filter.operator}" expects a ${fieldType} value for field "${filter.field}".`;
38
+ case 'gt':
39
+ case 'gte':
40
+ case 'lt':
41
+ case 'lte':
42
+ if (fieldType === 'boolean') {
43
+ return `"${filter.operator}" is not supported for boolean field "${filter.field}".`;
44
+ }
45
+ return matchesFieldType(fieldType, filter.value)
46
+ ? null
47
+ : `"${filter.operator}" expects a ${fieldType} value for field "${filter.field}".`;
48
+ case 'like':
49
+ if (fieldType !== 'string' && fieldType !== 'timestamp') {
50
+ return `"like" is only supported for string or timestamp field "${filter.field}".`;
51
+ }
52
+ return typeof filter.value === 'string'
53
+ ? null
54
+ : `"like" expects a string value for field "${filter.field}".`;
55
+ case 'in':
56
+ case 'notIn':
57
+ if (!Array.isArray(filter.value) || filter.value.length === 0) {
58
+ return `"${filter.operator}" expects a non-empty array for field "${filter.field}".`;
59
+ }
60
+ return filter.value.every(value => matchesFieldType(fieldType, value))
61
+ ? null
62
+ : `"${filter.operator}" expects ${fieldType} values for field "${filter.field}".`;
63
+ case 'between':
64
+ if (!Array.isArray(filter.value) || filter.value.length !== 2) {
65
+ return `"between" expects a two-item array for field "${filter.field}".`;
66
+ }
67
+ return filter.value.every(value => matchesFieldType(fieldType, value))
68
+ ? null
69
+ : `"between" expects ${fieldType} values for field "${filter.field}".`;
70
+ default:
71
+ return null;
72
+ }
73
+ }