@hypequery/serve 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -1
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +3 -5
- package/dist/adapters/standalone.d.ts +41 -0
- package/dist/adapters/standalone.d.ts.map +1 -0
- package/dist/adapters/standalone.js +46 -0
- package/dist/auth.d.ts +59 -83
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +136 -102
- package/dist/client-config.d.ts +3 -2
- package/dist/client-config.d.ts.map +1 -1
- package/dist/client-config.js +4 -2
- package/dist/errors.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/openapi.js +1 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +10 -22
- package/dist/query-logger.js +1 -3
- package/dist/rate-limit.js +4 -3
- package/dist/router.js +2 -1
- package/dist/semantic/datasets/dataset-endpoint.d.ts +85 -0
- package/dist/semantic/datasets/dataset-endpoint.d.ts.map +1 -0
- package/dist/semantic/datasets/dataset-endpoint.js +121 -0
- package/dist/semantic/datasets/index.d.ts +6 -0
- package/dist/semantic/datasets/index.d.ts.map +1 -0
- package/dist/semantic/datasets/index.js +5 -0
- package/dist/semantic/datasets/metric-endpoint.d.ts +82 -0
- package/dist/semantic/datasets/metric-endpoint.d.ts.map +1 -0
- package/dist/semantic/datasets/metric-endpoint.js +159 -0
- package/dist/semantic/datasets/utils/dataset-entry.d.ts +24 -0
- package/dist/semantic/datasets/utils/dataset-entry.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/dataset-entry.js +15 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.d.ts +3 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.js +12 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.d.ts +107 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.js +87 -0
- package/dist/semantic/index.d.ts +2 -0
- package/dist/semantic/index.d.ts.map +1 -0
- package/dist/semantic/index.js +1 -0
- package/dist/semantic/query-builder-context.d.ts +20 -0
- package/dist/semantic/query-builder-context.d.ts.map +1 -0
- package/dist/semantic/query-builder-context.js +66 -0
- package/dist/semantic/utils/tenant-runtime.d.ts +11 -0
- package/dist/semantic/utils/tenant-runtime.d.ts.map +1 -0
- package/dist/semantic/utils/tenant-runtime.js +48 -0
- package/dist/serve.d.ts +2 -2
- package/dist/serve.d.ts.map +1 -1
- package/dist/server/api-builder.d.ts +5 -0
- package/dist/server/api-builder.d.ts.map +1 -0
- package/dist/server/api-builder.js +76 -0
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.js +11 -1
- package/dist/server/create-api.d.ts +32 -0
- package/dist/server/create-api.d.ts.map +1 -0
- package/dist/server/create-api.js +211 -0
- package/dist/server/define-serve.d.ts +21 -2
- package/dist/server/define-serve.d.ts.map +1 -1
- package/dist/server/define-serve.js +53 -84
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/init-serve.d.ts +1 -1
- package/dist/server/init-serve.d.ts.map +1 -1
- package/dist/server/init-serve.js +7 -2
- package/dist/type-tests/builder.test-d.d.ts +4 -0
- package/dist/type-tests/builder.test-d.d.ts.map +1 -1
- package/dist/type-tests/builder.test-d.js +16 -1
- package/dist/type-tests/semantic.test-d.d.ts +2 -0
- package/dist/type-tests/semantic.test-d.d.ts.map +1 -0
- package/dist/type-tests/semantic.test-d.js +59 -0
- package/dist/types.d.ts +227 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a DatasetInstance into a standard ServeEndpoint for semantic queries.
|
|
3
|
+
*
|
|
4
|
+
* The generated endpoint is a POST handler that:
|
|
5
|
+
* - Accepts dimensions, measures, filters, orderBy, limit, offset, by
|
|
6
|
+
* - Validates requested dimensions/measures/filters against the dataset contract
|
|
7
|
+
* - Executes via QueryBuilderFactoryLike
|
|
8
|
+
* - Applies Serve-managed tenant filtering when configured
|
|
9
|
+
* - Returns { data } or { data, meta } based on headers
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { runDatasetQuery, validateDatasetQuery, } from '@hypequery/datasets/internal';
|
|
13
|
+
import { ServeHttpError } from '../../errors.js';
|
|
14
|
+
import { resolveSemanticExecutionRuntime, resolveSemanticQueryBuilder, } from '../query-builder-context.js';
|
|
15
|
+
import { buildDatasetQueryDescription } from './utils/dataset-query-metadata.js';
|
|
16
|
+
import { resolveDatasetEntry } from './utils/dataset-entry.js';
|
|
17
|
+
import { buildDatasetInputSchema } from './utils/semantic-input-schema.js';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Zod schemas for dataset query input / output
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const datasetResultMetaSchema = z.object({
|
|
22
|
+
timingMs: z.number().optional(),
|
|
23
|
+
sql: z.string().optional(),
|
|
24
|
+
tenant: z.string().optional(),
|
|
25
|
+
rowCount: z.number().optional(),
|
|
26
|
+
pagination: z.object({
|
|
27
|
+
limit: z.number(),
|
|
28
|
+
offset: z.number(),
|
|
29
|
+
hasMore: z.boolean(),
|
|
30
|
+
}).optional(),
|
|
31
|
+
}).optional();
|
|
32
|
+
const datasetResultSchema = z.object({
|
|
33
|
+
data: z.array(z.record(z.unknown())),
|
|
34
|
+
meta: datasetResultMetaSchema,
|
|
35
|
+
});
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Factory
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export function createDatasetEndpoint(name, entry, builderFactory) {
|
|
40
|
+
const resolved = resolveDatasetEntry(entry);
|
|
41
|
+
const ds = resolved.dataset;
|
|
42
|
+
const effectiveMaxLimit = resolved.maxLimit ?? ds.limits?.maxResultSize ?? 1000;
|
|
43
|
+
// Build a schema whose dimension/measure/filter fields are enumerated from
|
|
44
|
+
// this dataset's contract, so OpenAPI/docs and clients see the valid fields.
|
|
45
|
+
const datasetQueryInputSchema = buildDatasetInputSchema(ds);
|
|
46
|
+
const metadata = {
|
|
47
|
+
path: '', // filled by router.register
|
|
48
|
+
method: 'POST',
|
|
49
|
+
name: name,
|
|
50
|
+
summary: `Query the "${name}" semantic dataset`,
|
|
51
|
+
description: buildDatasetQueryDescription(ds, effectiveMaxLimit),
|
|
52
|
+
tags: ['datasets'],
|
|
53
|
+
requiresAuth: resolved.auth !== null ? undefined : false,
|
|
54
|
+
requiredRoles: resolved.requiredRoles,
|
|
55
|
+
requiredScopes: resolved.requiredScopes,
|
|
56
|
+
cacheTtlMs: resolved.cache,
|
|
57
|
+
visibility: 'public',
|
|
58
|
+
custom: {
|
|
59
|
+
usesServeTenantRuntime: true,
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const handler = async (ctx) => {
|
|
63
|
+
const semanticContext = ctx;
|
|
64
|
+
const input = ctx.input ?? {};
|
|
65
|
+
const start = Date.now();
|
|
66
|
+
const runtime = resolveSemanticExecutionRuntime(semanticContext);
|
|
67
|
+
// Validate
|
|
68
|
+
const query = {
|
|
69
|
+
dimensions: input.dimensions,
|
|
70
|
+
measures: input.measures,
|
|
71
|
+
filters: input.filters,
|
|
72
|
+
orderBy: input.orderBy,
|
|
73
|
+
limit: Math.min(input.limit ?? effectiveMaxLimit, effectiveMaxLimit),
|
|
74
|
+
offset: input.offset,
|
|
75
|
+
by: input.by,
|
|
76
|
+
};
|
|
77
|
+
const executionContext = runtime ? { runtime } : undefined;
|
|
78
|
+
const validation = validateDatasetQuery(ds, query, executionContext);
|
|
79
|
+
if (!validation.valid) {
|
|
80
|
+
throw new ServeHttpError(400, 'VALIDATION_ERROR', validation.errors.join('; '));
|
|
81
|
+
}
|
|
82
|
+
// Build query
|
|
83
|
+
const runtimeBuilderFactory = resolveSemanticQueryBuilder(semanticContext, builderFactory);
|
|
84
|
+
if (ctx.tenantId) {
|
|
85
|
+
if (!runtime?.tenant || !ds.tenantKey) {
|
|
86
|
+
throw new ServeHttpError(500, 'INTERNAL_SERVER_ERROR', `Dataset endpoint "${name}" requires dataset tenantKey and serve tenant runtime when tenant isolation is enabled.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const result = await runDatasetQuery(ds, query, {
|
|
90
|
+
builderFactory: runtimeBuilderFactory,
|
|
91
|
+
context: executionContext,
|
|
92
|
+
});
|
|
93
|
+
const timingMs = Date.now() - start;
|
|
94
|
+
// Meta — opt in via the `includeMeta` input field or the x-include-meta header.
|
|
95
|
+
const includeMeta = input.includeMeta === true
|
|
96
|
+
|| ctx.request?.headers?.['x-include-meta'] === 'true';
|
|
97
|
+
return {
|
|
98
|
+
data: result.data,
|
|
99
|
+
meta: includeMeta ? {
|
|
100
|
+
...(result.meta ?? {}),
|
|
101
|
+
timingMs,
|
|
102
|
+
} : undefined,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
key: name,
|
|
107
|
+
method: 'POST',
|
|
108
|
+
inputSchema: datasetQueryInputSchema,
|
|
109
|
+
outputSchema: datasetResultSchema,
|
|
110
|
+
handler,
|
|
111
|
+
query: undefined,
|
|
112
|
+
middlewares: (resolved.middlewares ?? []),
|
|
113
|
+
auth: resolved.auth ?? null,
|
|
114
|
+
tenant: resolved.tenant,
|
|
115
|
+
metadata,
|
|
116
|
+
cacheTtlMs: resolved.cache ?? null,
|
|
117
|
+
defaultHeaders: undefined,
|
|
118
|
+
requiredRoles: resolved.requiredRoles,
|
|
119
|
+
requiredScopes: resolved.requiredScopes,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { dataset, dimension, measure, belongsTo, hasMany, hasOne, sum, count, countDistinct, avg, min, max, divide, multiply, subtract, add, nullIfZero, coalesce, round, floor, ceil, eq, neq, gt, gte, lt, lte, inList, notInList, between, like, asc, desc, filter, order, createDatasetRegistry, } from '@hypequery/datasets';
|
|
2
|
+
export type { FieldType, DimensionType, DimensionOptions, DimensionDefinition, MeasureOptions, MeasureDefinition, InferDimensionType, RelationshipKind, RelationshipDefinition, AggregationType, MeasureAggregation, AggregationSpec, FormulaExpr, DerivedMetricSpec, TimeGrain, MetricRef, BaseMetricRef, DerivedMetricRef, GrainedMetricRef, MetricContract, MetricFilter, MetricOrderBy, MetricQuery, DatasetQuery, MetricResultMeta, MetricResult, DatasetQueryResult, MetricHandle, ExecutionContext, SemanticExecutionRuntime, SemanticTenantRuntime, SemanticFilterDefinition, SemanticFiltersDefinition, DatasetConfig, DatasetLimits, DatasetInstance, BaseMetricConfig, DerivedMetricConfig, DatasetRegistryInstance, DatasetFieldNames, } from '@hypequery/datasets';
|
|
3
|
+
export { createMetricEndpoint } from './metric-endpoint.js';
|
|
4
|
+
export { createDatasetEndpoint } from './dataset-endpoint.js';
|
|
5
|
+
export type { DatasetEntry } from './dataset-endpoint.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/semantic/datasets/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,OAAO,EACP,SAAS,EACT,OAAO,EACP,SAAS,EACT,OAAO,EACP,MAAM,EACN,GAAG,EACH,KAAK,EACL,aAAa,EACb,GAAG,EACH,GAAG,EACH,GAAG,EACH,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,GAAG,EACH,UAAU,EACV,QAAQ,EACR,KAAK,EACL,KAAK,EACL,IAAI,EACJ,EAAE,EACF,GAAG,EACH,EAAE,EACF,GAAG,EACH,EAAE,EACF,GAAG,EACH,MAAM,EACN,SAAS,EACT,OAAO,EACP,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,MAAM,EACN,KAAK,EACL,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,YAAY,EACV,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,YAAY,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Re-export public datasets APIs for convenience
|
|
2
|
+
export { dataset, dimension, measure, belongsTo, hasMany, hasOne, sum, count, countDistinct, avg, min, max, divide, multiply, subtract, add, nullIfZero, coalesce, round, floor, ceil, eq, neq, gt, gte, lt, lte, inList, notInList, between, like, asc, desc, filter, order, createDatasetRegistry, } from '@hypequery/datasets';
|
|
3
|
+
// Serve-specific endpoint integration
|
|
4
|
+
export { createMetricEndpoint } from './metric-endpoint.js';
|
|
5
|
+
export { createDatasetEndpoint } from './dataset-endpoint.js';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a MetricRef into a standard ServeEndpoint.
|
|
3
|
+
*
|
|
4
|
+
* The generated endpoint is a POST handler that:
|
|
5
|
+
* - Validates dimensions/filters against the metric's contract
|
|
6
|
+
* - Calls DatasetClient.execute() with the parsed query + tenant context
|
|
7
|
+
* - Returns { data } or { data, meta } based on headers
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import type { AuthContext, MetricEntry, ServeEndpoint } from '../../types.js';
|
|
11
|
+
import type { DatasetClient, QueryBuilderFactoryLike } from '@hypequery/datasets';
|
|
12
|
+
declare const metricResultSchema: z.ZodObject<{
|
|
13
|
+
data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
|
|
14
|
+
meta: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
timingMs: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
sql: z.ZodOptional<z.ZodString>;
|
|
17
|
+
tenant: z.ZodOptional<z.ZodString>;
|
|
18
|
+
rowCount: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
pagination: z.ZodOptional<z.ZodObject<{
|
|
20
|
+
limit: z.ZodNumber;
|
|
21
|
+
offset: z.ZodNumber;
|
|
22
|
+
hasMore: z.ZodBoolean;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
limit: number;
|
|
25
|
+
offset: number;
|
|
26
|
+
hasMore: boolean;
|
|
27
|
+
}, {
|
|
28
|
+
limit: number;
|
|
29
|
+
offset: number;
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
}>>;
|
|
32
|
+
}, "strip", z.ZodTypeAny, {
|
|
33
|
+
tenant?: string | undefined;
|
|
34
|
+
timingMs?: number | undefined;
|
|
35
|
+
sql?: string | undefined;
|
|
36
|
+
rowCount?: number | undefined;
|
|
37
|
+
pagination?: {
|
|
38
|
+
limit: number;
|
|
39
|
+
offset: number;
|
|
40
|
+
hasMore: boolean;
|
|
41
|
+
} | undefined;
|
|
42
|
+
}, {
|
|
43
|
+
tenant?: string | undefined;
|
|
44
|
+
timingMs?: number | undefined;
|
|
45
|
+
sql?: string | undefined;
|
|
46
|
+
rowCount?: number | undefined;
|
|
47
|
+
pagination?: {
|
|
48
|
+
limit: number;
|
|
49
|
+
offset: number;
|
|
50
|
+
hasMore: boolean;
|
|
51
|
+
} | undefined;
|
|
52
|
+
}>>;
|
|
53
|
+
}, "strip", z.ZodTypeAny, {
|
|
54
|
+
data: Record<string, unknown>[];
|
|
55
|
+
meta?: {
|
|
56
|
+
tenant?: string | undefined;
|
|
57
|
+
timingMs?: number | undefined;
|
|
58
|
+
sql?: string | undefined;
|
|
59
|
+
rowCount?: number | undefined;
|
|
60
|
+
pagination?: {
|
|
61
|
+
limit: number;
|
|
62
|
+
offset: number;
|
|
63
|
+
hasMore: boolean;
|
|
64
|
+
} | undefined;
|
|
65
|
+
} | undefined;
|
|
66
|
+
}, {
|
|
67
|
+
data: Record<string, unknown>[];
|
|
68
|
+
meta?: {
|
|
69
|
+
tenant?: string | undefined;
|
|
70
|
+
timingMs?: number | undefined;
|
|
71
|
+
sql?: string | undefined;
|
|
72
|
+
rowCount?: number | undefined;
|
|
73
|
+
pagination?: {
|
|
74
|
+
limit: number;
|
|
75
|
+
offset: number;
|
|
76
|
+
hasMore: boolean;
|
|
77
|
+
} | undefined;
|
|
78
|
+
} | undefined;
|
|
79
|
+
}>;
|
|
80
|
+
export declare function createMetricEndpoint<TAuth extends AuthContext>(name: string, entry: MetricEntry<TAuth>, analytics: DatasetClient, defaultBuilderFactory: QueryBuilderFactoryLike): ServeEndpoint<z.ZodTypeAny, typeof metricResultSchema, any, TAuth, any>;
|
|
81
|
+
export {};
|
|
82
|
+
//# sourceMappingURL=metric-endpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-endpoint.d.ts","sourceRoot":"","sources":["../../../src/semantic/datasets/metric-endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EACV,WAAW,EAIX,WAAW,EACX,aAAa,EAGd,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAsB,aAAa,EAAgC,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAwBpI,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGtB,CAAC;AAoDH,wBAAgB,oBAAoB,CAAC,KAAK,SAAS,WAAW,EAC5D,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,EACzB,SAAS,EAAE,aAAa,EACxB,qBAAqB,EAAE,uBAAuB,GAC7C,aAAa,CAAC,CAAC,CAAC,UAAU,EAAE,OAAO,kBAAkB,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CA4GzE"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a MetricRef into a standard ServeEndpoint.
|
|
3
|
+
*
|
|
4
|
+
* The generated endpoint is a POST handler that:
|
|
5
|
+
* - Validates dimensions/filters against the metric's contract
|
|
6
|
+
* - Calls DatasetClient.execute() with the parsed query + tenant context
|
|
7
|
+
* - Returns { data } or { data, meta } based on headers
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { ServeHttpError } from '../../errors.js';
|
|
11
|
+
import { resolveSemanticExecutionRuntime, resolveSemanticQueryBuilder, } from '../query-builder-context.js';
|
|
12
|
+
import { buildMetricInputSchema } from './utils/semantic-input-schema.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Zod schemas for metric query input / output
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const metricResultMetaSchema = z.object({
|
|
17
|
+
timingMs: z.number().optional(),
|
|
18
|
+
sql: z.string().optional(),
|
|
19
|
+
tenant: z.string().optional(),
|
|
20
|
+
rowCount: z.number().optional(),
|
|
21
|
+
pagination: z.object({
|
|
22
|
+
limit: z.number(),
|
|
23
|
+
offset: z.number(),
|
|
24
|
+
hasMore: z.boolean(),
|
|
25
|
+
}).optional(),
|
|
26
|
+
}).optional();
|
|
27
|
+
const metricResultSchema = z.object({
|
|
28
|
+
data: z.array(z.record(z.unknown())),
|
|
29
|
+
meta: metricResultMetaSchema,
|
|
30
|
+
});
|
|
31
|
+
function isMetricHandleEntry(entry) {
|
|
32
|
+
return (!!entry &&
|
|
33
|
+
typeof entry === 'object' &&
|
|
34
|
+
'__type' in entry &&
|
|
35
|
+
(entry.__type === 'metric_ref' || entry.__type === 'grained_metric_ref'));
|
|
36
|
+
}
|
|
37
|
+
function isMetricEntryOptions(entry) {
|
|
38
|
+
return !!entry && typeof entry === 'object' && 'metric' in entry;
|
|
39
|
+
}
|
|
40
|
+
function resolveMetricEntry(entry) {
|
|
41
|
+
if (isMetricHandleEntry(entry)) {
|
|
42
|
+
return { metric: entry };
|
|
43
|
+
}
|
|
44
|
+
if (isMetricEntryOptions(entry)) {
|
|
45
|
+
return entry;
|
|
46
|
+
}
|
|
47
|
+
throw new Error('Invalid metric entry.');
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Factory
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
export function createMetricEndpoint(name, entry, analytics, defaultBuilderFactory) {
|
|
53
|
+
const resolved = resolveMetricEntry(entry);
|
|
54
|
+
const metricRef = resolved.metric;
|
|
55
|
+
const contract = metricRef.contract();
|
|
56
|
+
// The metric's underlying dataset, used to enumerate valid query fields and
|
|
57
|
+
// page-size defaults.
|
|
58
|
+
const ds = (metricRef.__type === 'metric_ref' ? metricRef.dataset : metricRef.metric.dataset);
|
|
59
|
+
const metricQueryInputSchema = buildMetricInputSchema(ds, contract.name);
|
|
60
|
+
// Page-size cap, mirroring datasets: clamp (don't reject) and apply a default
|
|
61
|
+
// so a metric query is never unbounded.
|
|
62
|
+
const effectiveMaxLimit = resolved.maxLimit ?? ds.limits?.maxResultSize ?? 1000;
|
|
63
|
+
const metadata = {
|
|
64
|
+
path: '', // filled by router.register
|
|
65
|
+
method: 'POST',
|
|
66
|
+
name: contract.label ?? name,
|
|
67
|
+
summary: `Query the "${name}" metric`,
|
|
68
|
+
description: buildDescription(contract, effectiveMaxLimit),
|
|
69
|
+
tags: ['metrics'],
|
|
70
|
+
requiresAuth: resolved.auth !== null ? undefined : false,
|
|
71
|
+
requiredRoles: resolved.requiredRoles,
|
|
72
|
+
requiredScopes: resolved.requiredScopes,
|
|
73
|
+
cacheTtlMs: resolved.cache,
|
|
74
|
+
visibility: 'public',
|
|
75
|
+
custom: {
|
|
76
|
+
usesServeTenantRuntime: true,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const handler = async (ctx) => {
|
|
80
|
+
const semanticContext = ctx;
|
|
81
|
+
const input = ctx.input ?? {};
|
|
82
|
+
const runtime = resolveSemanticExecutionRuntime(semanticContext);
|
|
83
|
+
const runtimeBuilderFactory = resolveSemanticQueryBuilder(semanticContext, defaultBuilderFactory);
|
|
84
|
+
// Build the metric query
|
|
85
|
+
const query = {
|
|
86
|
+
dimensions: input.dimensions,
|
|
87
|
+
filters: input.filters,
|
|
88
|
+
orderBy: input.orderBy,
|
|
89
|
+
limit: Math.min(input.limit ?? effectiveMaxLimit, effectiveMaxLimit),
|
|
90
|
+
offset: input.offset,
|
|
91
|
+
by: input.by,
|
|
92
|
+
};
|
|
93
|
+
if (ctx.tenantId && !runtime?.tenant) {
|
|
94
|
+
throw new ServeHttpError(500, 'INTERNAL_SERVER_ERROR', `Metric endpoint "${name}" requires serve tenant runtime when tenant isolation is enabled.`);
|
|
95
|
+
}
|
|
96
|
+
const validationContext = ctx.tenantId && runtime?.tenant
|
|
97
|
+
? {
|
|
98
|
+
runtime: {
|
|
99
|
+
tenant: runtime.tenant,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
: undefined;
|
|
103
|
+
const validation = analytics.validate(metricRef, query, validationContext);
|
|
104
|
+
if (!validation.valid) {
|
|
105
|
+
throw new ServeHttpError(400, 'VALIDATION_ERROR', validation.errors.join('; '));
|
|
106
|
+
}
|
|
107
|
+
// Execute with tenant context
|
|
108
|
+
const result = await analytics.execute(metricRef, query, {
|
|
109
|
+
runtime: {
|
|
110
|
+
...runtime,
|
|
111
|
+
builderFactory: runtimeBuilderFactory,
|
|
112
|
+
tenant: runtime?.tenant,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
// Decide whether to include meta — `includeMeta` input field or x-include-meta header.
|
|
116
|
+
const includeMeta = input.includeMeta === true
|
|
117
|
+
|| ctx.request?.headers?.['x-include-meta'] === 'true';
|
|
118
|
+
return {
|
|
119
|
+
data: result.data,
|
|
120
|
+
meta: includeMeta ? result.meta : undefined,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
key: name,
|
|
125
|
+
method: 'POST',
|
|
126
|
+
inputSchema: metricQueryInputSchema,
|
|
127
|
+
outputSchema: metricResultSchema,
|
|
128
|
+
handler,
|
|
129
|
+
query: undefined,
|
|
130
|
+
middlewares: (resolved.middlewares ?? []),
|
|
131
|
+
auth: resolved.auth ?? null,
|
|
132
|
+
tenant: resolved.tenant,
|
|
133
|
+
metadata,
|
|
134
|
+
cacheTtlMs: resolved.cache ?? null,
|
|
135
|
+
defaultHeaders: undefined,
|
|
136
|
+
requiredRoles: resolved.requiredRoles,
|
|
137
|
+
requiredScopes: resolved.requiredScopes,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function buildDescription(contract, maxLimit) {
|
|
141
|
+
const lines = [
|
|
142
|
+
contract.description ?? `${contract.name} metric on the ${contract.dataset} dataset.`,
|
|
143
|
+
'',
|
|
144
|
+
`**Type:** ${contract.kind}`,
|
|
145
|
+
`**Dataset:** ${contract.dataset}`,
|
|
146
|
+
`**Dimensions:** ${contract.dimensions.join(', ') || 'none'}`,
|
|
147
|
+
`**Max limit:** ${maxLimit}`,
|
|
148
|
+
];
|
|
149
|
+
if (contract.grains.length > 0) {
|
|
150
|
+
lines.push(`**Time grains:** ${contract.grains.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
if (contract.requires && contract.requires.length > 0) {
|
|
153
|
+
lines.push(`**Requires:** ${contract.requires.join(', ')}`);
|
|
154
|
+
}
|
|
155
|
+
if (contract.tenantScoped) {
|
|
156
|
+
lines.push('**Tenant scoped:** yes');
|
|
157
|
+
}
|
|
158
|
+
return lines.join('\n');
|
|
159
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AuthContext, AuthStrategy, ServeMiddleware, TenantConfigOverride } from '../../../types.js';
|
|
2
|
+
import type { AnyDatasetInstance } from '@hypequery/datasets';
|
|
3
|
+
export type DatasetEntry<TAuth extends AuthContext = AuthContext> = AnyDatasetInstance | {
|
|
4
|
+
dataset: AnyDatasetInstance;
|
|
5
|
+
auth?: AuthStrategy<TAuth> | null;
|
|
6
|
+
tenant?: TenantConfigOverride<TAuth>;
|
|
7
|
+
cache?: number | null;
|
|
8
|
+
requiredRoles?: string[];
|
|
9
|
+
requiredScopes?: string[];
|
|
10
|
+
/** Middleware applied to this dataset endpoint. */
|
|
11
|
+
middlewares?: ServeMiddleware<any, any, any, TAuth>[];
|
|
12
|
+
maxLimit?: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function resolveDatasetEntry<TAuth extends AuthContext>(entry: DatasetEntry<TAuth>): {
|
|
15
|
+
dataset: AnyDatasetInstance;
|
|
16
|
+
auth?: AuthStrategy<TAuth> | null;
|
|
17
|
+
tenant?: TenantConfigOverride<TAuth>;
|
|
18
|
+
cache?: number | null;
|
|
19
|
+
requiredRoles?: string[];
|
|
20
|
+
requiredScopes?: string[];
|
|
21
|
+
middlewares?: ServeMiddleware<any, any, any, TAuth>[];
|
|
22
|
+
maxLimit?: number;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=dataset-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-entry.d.ts","sourceRoot":"","sources":["../../../../src/semantic/datasets/utils/dataset-entry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,eAAe,EACf,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,MAAM,MAAM,YAAY,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW,IAC5D,kBAAkB,GAClB;IACE,OAAO,EAAE,kBAAkB,CAAC;IAC5B,IAAI,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,oBAAoB,CAAC,KAAK,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,mDAAmD;IACnD,WAAW,CAAC,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAgBN,wBAAgB,mBAAmB,CAAC,KAAK,SAAS,WAAW,EAC3D,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,GACzB;IACD,OAAO,EAAE,kBAAkB,CAAC;IAC5B,IAAI,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,oBAAoB,CAAC,KAAK,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAUA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function isDatasetInstance(entry) {
|
|
2
|
+
return !!entry && typeof entry === 'object' && '__type' in entry && entry.__type === 'dataset';
|
|
3
|
+
}
|
|
4
|
+
function isDatasetEntryOptions(entry) {
|
|
5
|
+
return !!entry && typeof entry === 'object' && 'dataset' in entry;
|
|
6
|
+
}
|
|
7
|
+
export function resolveDatasetEntry(entry) {
|
|
8
|
+
if (isDatasetInstance(entry)) {
|
|
9
|
+
return { dataset: entry };
|
|
10
|
+
}
|
|
11
|
+
if (isDatasetEntryOptions(entry)) {
|
|
12
|
+
return entry;
|
|
13
|
+
}
|
|
14
|
+
throw new Error('Invalid dataset entry.');
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-query-metadata.d.ts","sourceRoot":"","sources":["../../../../src/semantic/datasets/utils/dataset-query-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,wBAAgB,4BAA4B,CAC1C,EAAE,EAAE,kBAAkB,EACtB,QAAQ,EAAE,MAAM,GACf,MAAM,CAYR"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function buildDatasetQueryDescription(ds, maxLimit) {
|
|
2
|
+
const dimensionNames = Object.keys(ds.dimensions);
|
|
3
|
+
const measureNames = Object.keys(ds.measures);
|
|
4
|
+
const lines = [
|
|
5
|
+
`Query the ${ds.name} semantic dataset (source: ${ds.source}).`,
|
|
6
|
+
'',
|
|
7
|
+
`**Dimensions:** ${dimensionNames.join(', ') || 'none'}`,
|
|
8
|
+
`**Measures:** ${measureNames.join(', ') || 'none'}`,
|
|
9
|
+
`**Max limit:** ${maxLimit}`,
|
|
10
|
+
];
|
|
11
|
+
return lines.join('\n');
|
|
12
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds per-dataset / per-metric Zod input schemas whose dimension, measure,
|
|
3
|
+
* filter, and orderBy fields are constrained to the names the semantic
|
|
4
|
+
* validators accept. This upgrades the generated OpenAPI/docs from "array of
|
|
5
|
+
* arbitrary strings" to enumerated fields and enables typed client codegen.
|
|
6
|
+
*
|
|
7
|
+
* The enums are deliberately a *superset-safe* mirror of the runtime validators
|
|
8
|
+
* (`validateDatasetQueryInput` / the metric `validateQuery`): they never reject
|
|
9
|
+
* a field the validator would accept. Where a list would be empty (e.g. a
|
|
10
|
+
* dataset with no declared filters) we fall back to `z.string()` and let the
|
|
11
|
+
* validator produce the precise error, rather than emitting an empty enum.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import type { AnyDatasetInstance } from '@hypequery/datasets';
|
|
15
|
+
/**
|
|
16
|
+
* Input schema for a dataset query endpoint, mirroring
|
|
17
|
+
* `validateDatasetQueryInput`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildDatasetInputSchema(ds: AnyDatasetInstance): z.ZodObject<{
|
|
20
|
+
dimensions: z.ZodOptional<z.ZodArray<z.ZodTypeAny, "many">>;
|
|
21
|
+
measures: z.ZodOptional<z.ZodArray<z.ZodTypeAny, "many">>;
|
|
22
|
+
filters: z.ZodOptional<z.ZodArray<z.ZodTypeAny, "many">>;
|
|
23
|
+
orderBy: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
24
|
+
field: z.ZodTypeAny;
|
|
25
|
+
direction: z.ZodEnum<["asc", "desc"]>;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
direction: "asc" | "desc";
|
|
28
|
+
field?: any;
|
|
29
|
+
}, {
|
|
30
|
+
direction: "asc" | "desc";
|
|
31
|
+
field?: any;
|
|
32
|
+
}>, "many">>;
|
|
33
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
34
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
by: z.ZodOptional<z.ZodEnum<["day", "week", "month", "quarter", "year"]>>;
|
|
36
|
+
includeMeta: z.ZodOptional<z.ZodBoolean>;
|
|
37
|
+
}, "strict", z.ZodTypeAny, {
|
|
38
|
+
dimensions?: any[] | undefined;
|
|
39
|
+
measures?: any[] | undefined;
|
|
40
|
+
filters?: any[] | undefined;
|
|
41
|
+
orderBy?: {
|
|
42
|
+
direction: "asc" | "desc";
|
|
43
|
+
field?: any;
|
|
44
|
+
}[] | undefined;
|
|
45
|
+
limit?: number | undefined;
|
|
46
|
+
offset?: number | undefined;
|
|
47
|
+
by?: "day" | "week" | "month" | "quarter" | "year" | undefined;
|
|
48
|
+
includeMeta?: boolean | undefined;
|
|
49
|
+
}, {
|
|
50
|
+
dimensions?: any[] | undefined;
|
|
51
|
+
measures?: any[] | undefined;
|
|
52
|
+
filters?: any[] | undefined;
|
|
53
|
+
orderBy?: {
|
|
54
|
+
direction: "asc" | "desc";
|
|
55
|
+
field?: any;
|
|
56
|
+
}[] | undefined;
|
|
57
|
+
limit?: number | undefined;
|
|
58
|
+
offset?: number | undefined;
|
|
59
|
+
by?: "day" | "week" | "month" | "quarter" | "year" | undefined;
|
|
60
|
+
includeMeta?: boolean | undefined;
|
|
61
|
+
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Input schema for a metric query endpoint, mirroring the metric `validateQuery`.
|
|
64
|
+
* Metrics select a single value, so there is no `measures` field; `orderBy` may
|
|
65
|
+
* reference the metric's own output column (`metricName`).
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildMetricInputSchema(ds: AnyDatasetInstance, metricName: string): z.ZodObject<{
|
|
68
|
+
dimensions: z.ZodOptional<z.ZodArray<z.ZodTypeAny, "many">>;
|
|
69
|
+
filters: z.ZodOptional<z.ZodArray<z.ZodTypeAny, "many">>;
|
|
70
|
+
orderBy: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
71
|
+
field: z.ZodTypeAny;
|
|
72
|
+
direction: z.ZodEnum<["asc", "desc"]>;
|
|
73
|
+
}, "strip", z.ZodTypeAny, {
|
|
74
|
+
direction: "asc" | "desc";
|
|
75
|
+
field?: any;
|
|
76
|
+
}, {
|
|
77
|
+
direction: "asc" | "desc";
|
|
78
|
+
field?: any;
|
|
79
|
+
}>, "many">>;
|
|
80
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
by: z.ZodOptional<z.ZodEnum<["day", "week", "month", "quarter", "year"]>>;
|
|
83
|
+
includeMeta: z.ZodOptional<z.ZodBoolean>;
|
|
84
|
+
}, "strip", z.ZodTypeAny, {
|
|
85
|
+
dimensions?: any[] | undefined;
|
|
86
|
+
filters?: any[] | undefined;
|
|
87
|
+
orderBy?: {
|
|
88
|
+
direction: "asc" | "desc";
|
|
89
|
+
field?: any;
|
|
90
|
+
}[] | undefined;
|
|
91
|
+
limit?: number | undefined;
|
|
92
|
+
offset?: number | undefined;
|
|
93
|
+
by?: "day" | "week" | "month" | "quarter" | "year" | undefined;
|
|
94
|
+
includeMeta?: boolean | undefined;
|
|
95
|
+
}, {
|
|
96
|
+
dimensions?: any[] | undefined;
|
|
97
|
+
filters?: any[] | undefined;
|
|
98
|
+
orderBy?: {
|
|
99
|
+
direction: "asc" | "desc";
|
|
100
|
+
field?: any;
|
|
101
|
+
}[] | undefined;
|
|
102
|
+
limit?: number | undefined;
|
|
103
|
+
offset?: number | undefined;
|
|
104
|
+
by?: "day" | "week" | "month" | "quarter" | "year" | undefined;
|
|
105
|
+
includeMeta?: boolean | undefined;
|
|
106
|
+
}>;
|
|
107
|
+
//# sourceMappingURL=semantic-input-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-input-schema.d.ts","sourceRoot":"","sources":["../../../../src/semantic/datasets/utils/semantic-input-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAqC9D;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmB7D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiBhF"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds per-dataset / per-metric Zod input schemas whose dimension, measure,
|
|
3
|
+
* filter, and orderBy fields are constrained to the names the semantic
|
|
4
|
+
* validators accept. This upgrades the generated OpenAPI/docs from "array of
|
|
5
|
+
* arbitrary strings" to enumerated fields and enables typed client codegen.
|
|
6
|
+
*
|
|
7
|
+
* The enums are deliberately a *superset-safe* mirror of the runtime validators
|
|
8
|
+
* (`validateDatasetQueryInput` / the metric `validateQuery`): they never reject
|
|
9
|
+
* a field the validator would accept. Where a list would be empty (e.g. a
|
|
10
|
+
* dataset with no declared filters) we fall back to `z.string()` and let the
|
|
11
|
+
* validator produce the precise error, rather than emitting an empty enum.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
const OPERATORS = [
|
|
15
|
+
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn', 'between', 'like',
|
|
16
|
+
];
|
|
17
|
+
const GRAINS = ['day', 'week', 'month', 'quarter', 'year'];
|
|
18
|
+
/** An enum over the given field names, or a plain string when none are known. */
|
|
19
|
+
function fieldEnum(values) {
|
|
20
|
+
const unique = Array.from(new Set(values));
|
|
21
|
+
return unique.length > 0
|
|
22
|
+
? z.enum(unique)
|
|
23
|
+
: z.string();
|
|
24
|
+
}
|
|
25
|
+
function filterSchema(fieldNames) {
|
|
26
|
+
return z.object({
|
|
27
|
+
field: fieldEnum(fieldNames),
|
|
28
|
+
operator: z.enum(OPERATORS),
|
|
29
|
+
value: z.unknown(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function orderBySchema(fieldNames) {
|
|
33
|
+
return z.object({
|
|
34
|
+
field: fieldEnum(fieldNames),
|
|
35
|
+
direction: z.enum(['asc', 'desc']),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Apply a `.max()` bound only when the dataset declares one. */
|
|
39
|
+
function boundedArray(item, max) {
|
|
40
|
+
const arr = z.array(item);
|
|
41
|
+
return (max != null ? arr.max(max) : arr).optional();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Input schema for a dataset query endpoint, mirroring
|
|
45
|
+
* `validateDatasetQueryInput`.
|
|
46
|
+
*/
|
|
47
|
+
export function buildDatasetInputSchema(ds) {
|
|
48
|
+
const dimensionNames = Object.keys(ds.dimensions);
|
|
49
|
+
const measureNames = Object.keys(ds.measures);
|
|
50
|
+
// Dataset filters are keyed by filter-definition name (no dimension fallback).
|
|
51
|
+
const filterNames = Object.keys(ds.filters);
|
|
52
|
+
// orderBy is query-dependent at runtime; the static superset is every
|
|
53
|
+
// dimension/measure plus the synthetic `period` column when grained.
|
|
54
|
+
const orderableNames = [...dimensionNames, ...measureNames, 'period'];
|
|
55
|
+
return z.object({
|
|
56
|
+
dimensions: boundedArray(fieldEnum(dimensionNames), ds.limits?.maxDimensions),
|
|
57
|
+
measures: boundedArray(fieldEnum(measureNames), ds.limits?.maxMeasures),
|
|
58
|
+
filters: boundedArray(filterSchema(filterNames), ds.limits?.maxFilters),
|
|
59
|
+
orderBy: z.array(orderBySchema(orderableNames)).optional(),
|
|
60
|
+
limit: z.number().int().positive().optional(),
|
|
61
|
+
offset: z.number().int().nonnegative().optional(),
|
|
62
|
+
by: z.enum(GRAINS).optional(),
|
|
63
|
+
includeMeta: z.boolean().optional(),
|
|
64
|
+
}).strict();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Input schema for a metric query endpoint, mirroring the metric `validateQuery`.
|
|
68
|
+
* Metrics select a single value, so there is no `measures` field; `orderBy` may
|
|
69
|
+
* reference the metric's own output column (`metricName`).
|
|
70
|
+
*/
|
|
71
|
+
export function buildMetricInputSchema(ds, metricName) {
|
|
72
|
+
const dimensionNames = Object.keys(ds.dimensions);
|
|
73
|
+
// Metric validator falls back to dimension names when no filters are declared.
|
|
74
|
+
const filterNames = Object.keys(ds.filters).length > 0
|
|
75
|
+
? Object.keys(ds.filters)
|
|
76
|
+
: dimensionNames;
|
|
77
|
+
const orderableNames = [...dimensionNames, metricName, 'period'];
|
|
78
|
+
return z.object({
|
|
79
|
+
dimensions: boundedArray(fieldEnum(dimensionNames), ds.limits?.maxDimensions),
|
|
80
|
+
filters: boundedArray(filterSchema(filterNames), ds.limits?.maxFilters),
|
|
81
|
+
orderBy: z.array(orderBySchema(orderableNames)).optional(),
|
|
82
|
+
limit: z.number().int().positive().optional(),
|
|
83
|
+
offset: z.number().int().nonnegative().optional(),
|
|
84
|
+
by: z.enum(GRAINS).optional(),
|
|
85
|
+
includeMeta: z.boolean().optional(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/semantic/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './datasets/index.js';
|