@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.
Files changed (77) hide show
  1. package/README.md +138 -1
  2. package/dist/adapters/node.d.ts.map +1 -1
  3. package/dist/adapters/node.js +3 -5
  4. package/dist/adapters/standalone.d.ts +41 -0
  5. package/dist/adapters/standalone.d.ts.map +1 -0
  6. package/dist/adapters/standalone.js +46 -0
  7. package/dist/auth.d.ts +59 -83
  8. package/dist/auth.d.ts.map +1 -1
  9. package/dist/auth.js +136 -102
  10. package/dist/client-config.d.ts +3 -2
  11. package/dist/client-config.d.ts.map +1 -1
  12. package/dist/client-config.js +4 -2
  13. package/dist/errors.js +3 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -0
  17. package/dist/openapi.js +1 -2
  18. package/dist/pipeline.d.ts.map +1 -1
  19. package/dist/pipeline.js +10 -22
  20. package/dist/query-logger.js +1 -3
  21. package/dist/rate-limit.js +4 -3
  22. package/dist/router.js +2 -1
  23. package/dist/semantic/datasets/dataset-endpoint.d.ts +85 -0
  24. package/dist/semantic/datasets/dataset-endpoint.d.ts.map +1 -0
  25. package/dist/semantic/datasets/dataset-endpoint.js +121 -0
  26. package/dist/semantic/datasets/index.d.ts +6 -0
  27. package/dist/semantic/datasets/index.d.ts.map +1 -0
  28. package/dist/semantic/datasets/index.js +5 -0
  29. package/dist/semantic/datasets/metric-endpoint.d.ts +82 -0
  30. package/dist/semantic/datasets/metric-endpoint.d.ts.map +1 -0
  31. package/dist/semantic/datasets/metric-endpoint.js +159 -0
  32. package/dist/semantic/datasets/utils/dataset-entry.d.ts +24 -0
  33. package/dist/semantic/datasets/utils/dataset-entry.d.ts.map +1 -0
  34. package/dist/semantic/datasets/utils/dataset-entry.js +15 -0
  35. package/dist/semantic/datasets/utils/dataset-query-metadata.d.ts +3 -0
  36. package/dist/semantic/datasets/utils/dataset-query-metadata.d.ts.map +1 -0
  37. package/dist/semantic/datasets/utils/dataset-query-metadata.js +12 -0
  38. package/dist/semantic/datasets/utils/semantic-input-schema.d.ts +107 -0
  39. package/dist/semantic/datasets/utils/semantic-input-schema.d.ts.map +1 -0
  40. package/dist/semantic/datasets/utils/semantic-input-schema.js +87 -0
  41. package/dist/semantic/index.d.ts +2 -0
  42. package/dist/semantic/index.d.ts.map +1 -0
  43. package/dist/semantic/index.js +1 -0
  44. package/dist/semantic/query-builder-context.d.ts +20 -0
  45. package/dist/semantic/query-builder-context.d.ts.map +1 -0
  46. package/dist/semantic/query-builder-context.js +66 -0
  47. package/dist/semantic/utils/tenant-runtime.d.ts +11 -0
  48. package/dist/semantic/utils/tenant-runtime.d.ts.map +1 -0
  49. package/dist/semantic/utils/tenant-runtime.js +48 -0
  50. package/dist/serve.d.ts +2 -2
  51. package/dist/serve.d.ts.map +1 -1
  52. package/dist/server/api-builder.d.ts +5 -0
  53. package/dist/server/api-builder.d.ts.map +1 -0
  54. package/dist/server/api-builder.js +76 -0
  55. package/dist/server/builder.d.ts.map +1 -1
  56. package/dist/server/builder.js +11 -1
  57. package/dist/server/create-api.d.ts +32 -0
  58. package/dist/server/create-api.d.ts.map +1 -0
  59. package/dist/server/create-api.js +211 -0
  60. package/dist/server/define-serve.d.ts +21 -2
  61. package/dist/server/define-serve.d.ts.map +1 -1
  62. package/dist/server/define-serve.js +53 -84
  63. package/dist/server/index.d.ts +2 -0
  64. package/dist/server/index.d.ts.map +1 -1
  65. package/dist/server/index.js +2 -0
  66. package/dist/server/init-serve.d.ts +1 -1
  67. package/dist/server/init-serve.d.ts.map +1 -1
  68. package/dist/server/init-serve.js +7 -2
  69. package/dist/type-tests/builder.test-d.d.ts +4 -0
  70. package/dist/type-tests/builder.test-d.d.ts.map +1 -1
  71. package/dist/type-tests/builder.test-d.js +16 -1
  72. package/dist/type-tests/semantic.test-d.d.ts +2 -0
  73. package/dist/type-tests/semantic.test-d.d.ts.map +1 -0
  74. package/dist/type-tests/semantic.test-d.js +59 -0
  75. package/dist/types.d.ts +227 -6
  76. package/dist/types.d.ts.map +1 -1
  77. package/package.json +5 -2
@@ -1,91 +1,60 @@
1
- import { createEndpoint } from "../endpoint.js";
2
- import { ServeRouter, applyBasePath } from "../router.js";
3
- import { ensureArray } from "../utils.js";
4
- import { ServeQueryLogger, formatQueryEvent, formatQueryEventJSON } from "../query-logger.js";
5
- import { createServeHandler } from "../pipeline.js";
6
- import { createDocsEndpoint, createOpenApiEndpoint } from "../pipeline.js";
7
- import { resolveCorsConfig } from "../cors.js";
8
- import { createExecuteQuery } from "./execute-query.js";
9
- import { createBuilderMethods } from "./builder.js";
1
+ import { createAPI } from "./create-api.js";
2
+ /**
3
+ * Define and configure a serve API with embedded transport.
4
+ *
5
+ * @deprecated Prefer `createAPI()` with standalone transport functions for
6
+ * better separation of concerns. `defineServe()` couples API definition
7
+ * with the Node.js HTTP server via `.start()`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // Before (defineServe)
12
+ * const api = defineServe({ queries: { ... } });
13
+ * api.start({ port: 3000 });
14
+ *
15
+ * // After (createAPI + serve)
16
+ * import { createAPI, startServer } from '@hypequery/serve';
17
+ * const api = createAPI({ queries: { ... } });
18
+ * startServer(api, { port: 3000 });
19
+ * ```
20
+ */
10
21
  export const defineServe = (config) => {
11
- const basePath = config.basePath ?? "/api/analytics";
12
- const router = new ServeRouter(basePath);
13
- const globalMiddlewares = [
14
- ...(config.middlewares ?? []),
15
- ];
16
- const authStrategies = ensureArray(config.auth);
17
- const globalTenantConfig = config.tenant;
18
- const contextFactory = config.context;
19
- const hooks = (config.hooks ?? {});
20
- const queryLogger = new ServeQueryLogger();
21
- // Wire up production query logging if configured
22
- if (config.queryLogging) {
23
- if (typeof config.queryLogging === 'function') {
24
- queryLogger.on(config.queryLogging);
22
+ const api = createAPI(config);
23
+ const loadNodeAdapter = async () => {
24
+ if (typeof require !== "undefined") {
25
+ return require("../adapters/node.js");
25
26
  }
26
- else if (config.queryLogging === 'json') {
27
- queryLogger.on((event) => {
28
- const line = formatQueryEventJSON(event);
29
- if (line)
30
- console.log(line);
31
- });
32
- }
33
- else {
34
- queryLogger.on((event) => {
35
- const line = formatQueryEvent(event);
36
- if (line)
37
- console.log(line);
38
- });
39
- }
40
- }
41
- // Slow query warning
42
- if (config.slowQueryThreshold != null) {
43
- queryLogger.on((event) => {
44
- if (event.status === 'completed' && event.durationMs && event.durationMs > config.slowQueryThreshold) {
45
- console.warn(`[hypequery/slow-query] ${event.method} ${event.path} took ${event.durationMs}ms (threshold: ${config.slowQueryThreshold}ms)`);
46
- }
47
- });
48
- }
49
- const openapiConfig = {
50
- enabled: config.openapi?.enabled ?? true,
51
- path: config.openapi?.path ?? "/openapi.json",
27
+ return import("../adapters/node.js");
52
28
  };
53
- const docsConfig = {
54
- enabled: config.docs?.enabled ?? true,
55
- path: config.docs?.path ?? "/docs",
56
- };
57
- const openapiPublicPath = applyBasePath(basePath, openapiConfig.path);
58
- const configuredQueries = config.queries ?? {};
59
- const queryEntries = {};
60
- const registerQuery = (key, definition) => {
61
- queryEntries[key] = createEndpoint(String(key), definition);
62
- };
63
- for (const key of Object.keys(configuredQueries)) {
64
- registerQuery(key, configuredQueries[key]);
65
- }
66
- const corsConfig = resolveCorsConfig(config.cors);
67
- const handler = createServeHandler({
68
- router,
69
- globalMiddlewares,
70
- authStrategies,
71
- tenantConfig: globalTenantConfig,
72
- contextFactory,
73
- hooks,
74
- queryLogger,
75
- verboseAuthErrors: config.security?.verboseAuthErrors ?? false,
76
- corsConfig,
77
- });
78
- // Track route configuration for client config extraction
29
+ // Extend the API with backwards-compatible ServeBuilder methods
30
+ const builder = api;
79
31
  const routeConfig = {};
80
- const executeQuery = createExecuteQuery(queryEntries, authStrategies, contextFactory, globalMiddlewares, globalTenantConfig, hooks, queryLogger, config.security?.verboseAuthErrors ?? false);
81
- const builder = createBuilderMethods(queryEntries, queryLogger, routeConfig, router, authStrategies, globalMiddlewares, executeQuery, handler, basePath);
82
- if (openapiConfig.enabled) {
83
- const openapiEndpoint = createOpenApiEndpoint(openapiConfig.path, () => router.list(), config.openapi);
84
- router.register(openapiEndpoint);
85
- }
86
- if (docsConfig.enabled) {
87
- const docsEndpoint = createDocsEndpoint(docsConfig.path, openapiPublicPath, config.docs);
88
- router.register(docsEndpoint);
32
+ for (const [key, endpoint] of Object.entries(api.queries)) {
33
+ routeConfig[key] = { method: endpoint.method };
89
34
  }
35
+ Object.defineProperty(builder, "basePath", {
36
+ value: config.basePath ?? "/api/analytics",
37
+ enumerable: true,
38
+ configurable: true,
39
+ });
40
+ Object.defineProperty(builder, "_routeConfig", {
41
+ value: routeConfig,
42
+ enumerable: true,
43
+ configurable: true,
44
+ });
45
+ const originalRoute = builder.route.bind(builder);
46
+ builder.route = (path, endpoint, options = {}) => {
47
+ const result = originalRoute(path, endpoint, options);
48
+ const queryKey = Object.entries(api.queries).find(([_, entry]) => entry === endpoint)?.[0];
49
+ if (queryKey) {
50
+ routeConfig[queryKey] = { method: options?.method ?? endpoint.method };
51
+ }
52
+ return result;
53
+ };
54
+ // Add transport method that ServeBuilder expects
55
+ builder.start = async (options = {}) => {
56
+ const { startNodeServer } = await loadNodeAdapter();
57
+ return startNodeServer(api.handler, options);
58
+ };
90
59
  return builder;
91
60
  };
@@ -1,6 +1,8 @@
1
+ export { createAPI } from "./create-api.js";
1
2
  export { defineServe } from "./define-serve.js";
2
3
  export { initServe } from "./init-serve.js";
3
4
  export { createExecuteQuery } from "./execute-query.js";
4
5
  export { createBuilderMethods } from "./builder.js";
6
+ export { createAPImethods } from "./api-builder.js";
5
7
  export { mapEndpointToToolkit } from "./mapper.js";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
@@ -1,5 +1,7 @@
1
+ export { createAPI } from "./create-api.js";
1
2
  export { defineServe } from "./define-serve.js";
2
3
  export { initServe } from "./init-serve.js";
3
4
  export { createExecuteQuery } from "./execute-query.js";
4
5
  export { createBuilderMethods } from "./builder.js";
6
+ export { createAPImethods } from "./api-builder.js";
5
7
  export { mapEndpointToToolkit } from "./mapper.js";
@@ -1,6 +1,6 @@
1
1
  import type { AuthContext, ServeContextFactory, ServeInitializer, ServeQueriesMap, ServeConfig } from "../types.js";
2
2
  type InferInitializerContext<TFactory, TAuth extends AuthContext> = TFactory extends ServeContextFactory<infer TContext, TAuth> ? TContext : never;
3
- type ServeInitializerOptions<TFactory extends ServeContextFactory<any, TAuth>, TAuth extends AuthContext> = Omit<ServeConfig<InferInitializerContext<TFactory, TAuth>, TAuth, ServeQueriesMap<InferInitializerContext<TFactory, TAuth>, TAuth>>, "queries" | "context"> & {
3
+ type ServeInitializerOptions<TFactory extends ServeContextFactory<any, TAuth>, TAuth extends AuthContext> = Omit<ServeConfig<InferInitializerContext<TFactory, TAuth>, TAuth, ServeQueriesMap<InferInitializerContext<TFactory, TAuth>, TAuth>, Record<never, never>, Record<never, never>>, "queries" | "context"> & {
4
4
  context: TFactory;
5
5
  };
6
6
  export declare const initServe: <TFactory extends ServeContextFactory<any, TAuth>, TAuth extends AuthContext = AuthContext>(options: ServeInitializerOptions<TFactory, TAuth>) => ServeInitializer<InferInitializerContext<TFactory, TAuth>, TAuth>;
@@ -1 +1 @@
1
- {"version":3,"file":"init-serve.d.ts","sourceRoot":"","sources":["../../src/server/init-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAEX,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,WAAW,EACZ,MAAM,aAAa,CAAC;AAKrB,KAAK,uBAAuB,CAC1B,QAAQ,EACR,KAAK,SAAS,WAAW,IACvB,QAAQ,SAAS,mBAAmB,CAAC,MAAM,QAAQ,EAAE,KAAK,CAAC,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEnF,KAAK,uBAAuB,CAC1B,QAAQ,SAAS,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,EAChD,KAAK,SAAS,WAAW,IACvB,IAAI,CACN,WAAW,CACT,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,EACxC,KAAK,EACL,eAAe,CAAC,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,CACjE,EACD,SAAS,GAAG,SAAS,CACtB,GAAG;IAAE,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;AAQ1B,eAAO,MAAM,SAAS,GACpB,QAAQ,SAAS,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,EAChD,KAAK,SAAS,WAAW,GAAG,WAAW,EACvC,SAAS,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAG,gBAAgB,CACpE,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,EACxC,KAAK,CAqCN,CAAC"}
1
+ {"version":3,"file":"init-serve.d.ts","sourceRoot":"","sources":["../../src/server/init-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAEX,mBAAmB,EACnB,gBAAgB,EAChB,eAAe,EACf,WAAW,EAGZ,MAAM,aAAa,CAAC;AAKrB,KAAK,uBAAuB,CAC1B,QAAQ,EACR,KAAK,SAAS,WAAW,IACvB,QAAQ,SAAS,mBAAmB,CAAC,MAAM,QAAQ,EAAE,KAAK,CAAC,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEnF,KAAK,uBAAuB,CAC1B,QAAQ,SAAS,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,EAChD,KAAK,SAAS,WAAW,IACvB,IAAI,CACN,WAAW,CACT,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,EACxC,KAAK,EACL,eAAe,CAAC,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,EAChE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EACpB,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CACrB,EACD,SAAS,GAAG,SAAS,CACtB,GAAG;IAAE,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;AAU1B,eAAO,MAAM,SAAS,GACpB,QAAQ,SAAS,mBAAmB,CAAC,GAAG,EAAE,KAAK,CAAC,EAChD,KAAK,SAAS,WAAW,GAAG,WAAW,EACvC,SAAS,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAG,gBAAgB,CACpE,uBAAuB,CAAC,QAAQ,EAAE,KAAK,CAAC,EACxC,KAAK,CAkDN,CAAC"}
@@ -5,11 +5,16 @@ export const initServe = (options) => {
5
5
  const { context, ...staticOptions } = options;
6
6
  const procedure = createProcedureBuilder();
7
7
  const define = (config) => {
8
- return defineServe({
8
+ // Auto-extract queryBuilder from context.db if metrics/datasets are used
9
+ // This is handled in createAPI since it needs to support both sync and async contexts
10
+ const finalConfig = {
9
11
  ...staticOptions,
10
12
  ...config,
11
13
  context: (context ?? {}),
12
- });
14
+ };
15
+ // Note: defineServe is deprecated for public use, but initServe uses it internally
16
+ // to provide the .start() method on the returned builder. This is intentional.
17
+ return defineServe(finalConfig);
13
18
  };
14
19
  const queryFactory = createQueryFactory((context ?? {}));
15
20
  const query = new Proxy(queryFactory, {
@@ -13,6 +13,10 @@ export declare const api: import("../types.js").ServeBuilder<import("../types.js
13
13
  }[]>;
14
14
  }, {
15
15
  db: {};
16
+ }, import("../types.js").AuthContext> & import("../types.js").SemanticMetricEndpointMap<Record<never, never>, {
17
+ db: {};
18
+ }, import("../types.js").AuthContext> & import("../types.js").SemanticDatasetEndpointMap<Record<never, never>, {
19
+ db: {};
16
20
  }, import("../types.js").AuthContext>, {
17
21
  db: {};
18
22
  }, import("../types.js").AuthContext>;
@@ -1 +1 @@
1
- {"version":3,"file":"builder.test-d.d.ts","sourceRoot":"","sources":["../../src/type-tests/builder.test-d.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;qCAUd,CAAC"}
1
+ {"version":3,"file":"builder.test-d.d.ts","sourceRoot":"","sources":["../../src/type-tests/builder.test-d.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,eAAO,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;qCAUd,CAAC"}
@@ -1,4 +1,4 @@
1
- import { initServe } from '../index.js';
1
+ import { createAnalyticsTokenIssuer, createJwtStrategy, initServe } from '../index.js';
2
2
  import { z } from 'zod';
3
3
  const serve = initServe({
4
4
  context: () => ({
@@ -34,3 +34,18 @@ const executableResultPromise = executableQuery.execute({
34
34
  const _executableResultIsTyped = { total: 10 };
35
35
  // @ts-expect-error total must be number
36
36
  const _executableResultRejectsString = { total: '10' };
37
+ createJwtStrategy({ secret: 'secret' });
38
+ createJwtStrategy({ jwksUri: 'https://issuer.example.com/.well-known/jwks.json' });
39
+ // @ts-expect-error exactly one key source is allowed
40
+ createJwtStrategy({ secret: 'secret', jwksUri: 'https://issuer.example.com/.well-known/jwks.json' });
41
+ // @ts-expect-error custom auth types require a mapper that proves required fields exist
42
+ createJwtStrategy({ secret: 'secret' });
43
+ createJwtStrategy({
44
+ secret: 'secret',
45
+ mapClaims: (payload) => ({
46
+ userId: String(payload.sub),
47
+ tenantId: String(payload.org_id),
48
+ }),
49
+ });
50
+ // @ts-expect-error token issuer supports symmetric HMAC JWT algorithms only
51
+ createAnalyticsTokenIssuer({ secret: 'secret', algorithm: 'RS256' });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=semantic.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic.test-d.d.ts","sourceRoot":"","sources":["../../src/type-tests/semantic.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,59 @@
1
+ import { dataset, dimension, measure, divide, nullIfZero, belongsTo } from '../semantic/index.js';
2
+ import { createAPI } from '../index.js';
3
+ const Customers = dataset('customers', {
4
+ source: 'customers',
5
+ dimensions: {
6
+ id: dimension.string(),
7
+ name: dimension.string(),
8
+ country: dimension.string(),
9
+ },
10
+ measures: {
11
+ customerCount: measure.count('id'),
12
+ },
13
+ });
14
+ const Orders = dataset('orders', {
15
+ source: 'orders',
16
+ tenantKey: 'tenant_id',
17
+ timeKey: 'created_at',
18
+ dimensions: {
19
+ id: dimension.string(),
20
+ customerId: dimension.string({ column: 'customer_id' }),
21
+ status: dimension.string(),
22
+ createdAt: dimension.timestamp({ column: 'created_at' }),
23
+ },
24
+ measures: {
25
+ revenue: measure.sum('amount'),
26
+ orderCount: measure.count('id'),
27
+ },
28
+ relationships: {
29
+ customer: belongsTo(() => Customers, { from: 'customerId', to: 'id' }),
30
+ },
31
+ });
32
+ const totalRevenue = Orders.metric('totalRevenue', { measure: 'revenue' });
33
+ const orderCountMetric = Orders.metric('orderCountMetric', { measure: 'orderCount' });
34
+ const avgOrderValue = Orders.metric('avgOrderValue', {
35
+ uses: { revenue: totalRevenue, orders: orderCountMetric },
36
+ formula: ({ revenue, orders }) => divide(revenue, nullIfZero(orders)),
37
+ });
38
+ const monthlyRevenue = totalRevenue.by('month');
39
+ monthlyRevenue.contract();
40
+ avgOrderValue.contract();
41
+ const queryBuilder = {
42
+ table: (() => {
43
+ throw new Error('type-only query builder');
44
+ }),
45
+ rawQuery: async () => [],
46
+ };
47
+ const semanticApi = createAPI({
48
+ metrics: { totalRevenue },
49
+ datasets: { orders: Orders },
50
+ queryBuilder,
51
+ });
52
+ void semanticApi.execute('totalRevenue', {
53
+ input: { dimensions: ['status'] },
54
+ });
55
+ void semanticApi.execute('dataset:orders', {
56
+ input: { dimensions: ['status'], measures: ['revenue'] },
57
+ });
58
+ // @ts-expect-error only configured semantic keys are executable
59
+ void semanticApi.execute('dataset:customers', { input: {} });
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { ZodTypeAny } from "zod";
1
+ import type { ZodType, ZodTypeAny } from "zod";
2
2
  import type { ServeQueryLogger, ServeQueryEventCallback } from "./query-logger.js";
3
+ import type { QueryBuilderFactoryLike } from "@hypequery/datasets";
3
4
  /** Supported HTTP verbs for serve-managed endpoints. */
4
5
  export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS";
5
6
  export type HeaderMap = Record<string, string | undefined>;
@@ -328,8 +329,118 @@ export interface OpenApiDocument {
328
329
  securitySchemes?: Record<string, unknown>;
329
330
  };
330
331
  }
331
- export interface ServeConfig<TContext extends Record<string, unknown> = Record<string, unknown>, TAuth extends AuthContext = AuthContext, TQueries extends ServeQueriesMap<TContext, TAuth> = ServeQueriesMap<TContext, TAuth>> {
332
- queries: TQueries;
332
+ /** Per-metric entry: shorthand (just the ref) or with overrides. */
333
+ export type MetricEntry<TAuth extends AuthContext = AuthContext> = MetricHandle<any, any> | {
334
+ metric: MetricHandle<any, any>;
335
+ auth?: AuthStrategy<TAuth> | null;
336
+ tenant?: TenantConfigOverride<TAuth>;
337
+ cache?: number | null;
338
+ requiredRoles?: string[];
339
+ requiredScopes?: string[];
340
+ /** Middleware applied to this metric endpoint. */
341
+ middlewares?: ServeMiddleware<any, any, any, TAuth>[];
342
+ /**
343
+ * Caps the page size for this metric. Requests above it are clamped (not
344
+ * rejected). Defaults to the dataset's `limits.maxResultSize`, else 1000.
345
+ */
346
+ maxLimit?: number;
347
+ };
348
+ /** Map of metric names to entries. */
349
+ export type MetricsConfig<TAuth extends AuthContext = AuthContext> = Record<string, MetricEntry<TAuth>>;
350
+ import type { DatasetInstance, DatasetQueryFor, DatasetQueryResultFor, MetricHandle, MetricQueryFor, MetricResultFor } from "@hypequery/datasets";
351
+ import type { DatasetEntry } from "./semantic/datasets/dataset-endpoint.js";
352
+ export type { DatasetEntry } from "./semantic/datasets/dataset-endpoint.js";
353
+ /** Map of dataset names to entries. */
354
+ export type DatasetsConfig<TAuth extends AuthContext = AuthContext> = Record<string, DatasetEntry<TAuth>>;
355
+ /** The dataset instance behind a `DatasetEntry` (bare instance or `{ dataset }`). */
356
+ type DatasetInstanceOfEntry<TEntry> = TEntry extends {
357
+ __type: 'dataset';
358
+ } ? TEntry : TEntry extends {
359
+ dataset: infer TDataset;
360
+ } ? (TDataset extends DatasetInstance<any, any, any, any> ? TDataset : never) : never;
361
+ /** The `MetricHandle` behind a `MetricEntry` (bare handle or `{ metric }`). */
362
+ type MetricHandleOfEntry<TEntry> = TEntry extends {
363
+ __type: 'metric_ref' | 'grained_metric_ref';
364
+ } ? TEntry : TEntry extends {
365
+ metric: infer THandle;
366
+ } ? THandle : never;
367
+ /** Unwrap a grained metric ref to its underlying base ref. */
368
+ type BaseMetricRefOf<THandle> = THandle extends {
369
+ __type: 'grained_metric_ref';
370
+ metric: infer TRef;
371
+ } ? TRef : THandle;
372
+ /** The concrete dataset instance a metric is defined on. */
373
+ type MetricDatasetOfEntry<TEntry> = BaseMetricRefOf<MetricHandleOfEntry<TEntry>> extends {
374
+ dataset: infer TDataset;
375
+ } ? (TDataset extends DatasetInstance<any, any, any, any> ? TDataset : never) : never;
376
+ /** The metric's output column name (used for typed rows and orderBy). */
377
+ type MetricNameOfEntry<TEntry> = BaseMetricRefOf<MetricHandleOfEntry<TEntry>> extends {
378
+ name: infer TName;
379
+ } ? (TName extends string ? TName : string) : string;
380
+ export type SemanticMetricEndpointMap<TMetrics extends MetricsConfig<TAuth>, TContext extends Record<string, unknown>, TAuth extends AuthContext> = {
381
+ [TKey in keyof TMetrics & string]: ServeEndpoint<ZodType<MetricQueryFor<MetricDatasetOfEntry<TMetrics[TKey]>, MetricNameOfEntry<TMetrics[TKey]>>>, ZodType<MetricResultFor<MetricDatasetOfEntry<TMetrics[TKey]>, MetricNameOfEntry<TMetrics[TKey]>>>, TContext, TAuth, MetricResultFor<MetricDatasetOfEntry<TMetrics[TKey]>, MetricNameOfEntry<TMetrics[TKey]>>>;
382
+ };
383
+ export type SemanticDatasetEndpointMap<TDatasets extends DatasetsConfig<TAuth>, TContext extends Record<string, unknown>, TAuth extends AuthContext> = {
384
+ [TKey in keyof TDatasets & string as `dataset:${TKey}`]: ServeEndpoint<ZodType<DatasetQueryFor<DatasetInstanceOfEntry<TDatasets[TKey]>>>, ZodType<DatasetQueryResultFor<DatasetInstanceOfEntry<TDatasets[TKey]>>>, TContext, TAuth, DatasetQueryResultFor<DatasetInstanceOfEntry<TDatasets[TKey]>>>;
385
+ };
386
+ export type ServeSemanticEndpointMap<TMetrics extends MetricsConfig<TAuth>, TDatasets extends DatasetsConfig<TAuth>, TContext extends Record<string, unknown>, TAuth extends AuthContext> = SemanticMetricEndpointMap<TMetrics, TContext, TAuth> & SemanticDatasetEndpointMap<TDatasets, TContext, TAuth>;
387
+ export interface ServeConfig<TContext extends Record<string, unknown> = Record<string, unknown>, TAuth extends AuthContext = AuthContext, TQueries extends ServeQueriesMap<TContext, TAuth> = Record<never, never>, TMetrics extends MetricsConfig<TAuth> = Record<never, never>, TDatasets extends DatasetsConfig<TAuth> = Record<never, never>> {
388
+ queries?: TQueries;
389
+ /**
390
+ * Metrics: auto-generated POST endpoints for each metric.
391
+ * Each metric gets a `POST /api/analytics/metrics/:name` endpoint
392
+ * that validates dimensions/filters against the metric's contract.
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * const api = createAPI({
397
+ * metrics: {
398
+ * totalRevenue, // shorthand
399
+ * profitMargin: { // with overrides
400
+ * metric: profitMargin,
401
+ * auth: requireRole('finance'),
402
+ * cache: 300_000,
403
+ * },
404
+ * },
405
+ * });
406
+ * ```
407
+ */
408
+ metrics?: TMetrics;
409
+ /**
410
+ * Semantic dataset endpoints — auto-generated POST endpoints for each dataset.
411
+ * Each dataset gets a `POST /api/analytics/datasets/:name/query` endpoint
412
+ * that validates dimensions/measures/filters against the dataset definition.
413
+ *
414
+ * When Serve tenant isolation is enabled for semantic endpoints, set
415
+ * `tenant.column` in the Serve config or per-entry override so the runtime
416
+ * knows which column to enforce.
417
+ *
418
+ * Accepts an inline map of dataset instances.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * const api = createAPI({
423
+ * datasets: { orders, customers },
424
+ * queryBuilder: qb,
425
+ * });
426
+ * ```
427
+ */
428
+ datasets?: TDatasets;
429
+ /**
430
+ * Query builder instance for metric/dataset execution.
431
+ * Required when `metrics` or `datasets` are provided.
432
+ * Pass the return value of `createQueryBuilder(config)` directly.
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * const qb = createQueryBuilder<Schema>(config);
437
+ * const api = createAPI({
438
+ * metrics: { totalRevenue },
439
+ * queryBuilder: qb,
440
+ * });
441
+ * ```
442
+ */
443
+ queryBuilder?: QueryBuilderFactoryLike;
333
444
  basePath?: string;
334
445
  middlewares?: ServeMiddleware<any, any, TContext, TAuth>[];
335
446
  auth?: AuthStrategy<TAuth> | AuthStrategy<TAuth>[];
@@ -383,6 +494,31 @@ export interface ServeConfig<TContext extends Record<string, unknown> = Record<s
383
494
  security?: {
384
495
  verboseAuthErrors?: boolean;
385
496
  };
497
+ /**
498
+ * Customize path prefixes for auto-generated semantic layer endpoints.
499
+ * Useful for avoiding route collisions or organizing API structure.
500
+ *
501
+ * @default { metrics: '/metrics', datasets: '/datasets' }
502
+ *
503
+ * @example
504
+ * ```ts
505
+ * const api = createAPI({
506
+ * metrics: { totalRevenue },
507
+ * datasets: { orders: Orders },
508
+ * semanticPaths: {
509
+ * metrics: '/api/metrics',
510
+ * datasets: '/api/data',
511
+ * },
512
+ * });
513
+ * // Generates:
514
+ * // POST /api/metrics/totalRevenue
515
+ * // POST /api/data/orders/query
516
+ * ```
517
+ */
518
+ semanticPaths?: {
519
+ metrics?: string;
520
+ datasets?: string;
521
+ };
386
522
  }
387
523
  export interface RouteRegistrationOptions<TContext extends Record<string, unknown> = Record<string, unknown>, TAuth extends AuthContext = AuthContext> {
388
524
  method?: HttpMethod;
@@ -471,6 +607,87 @@ export type ExecuteQueryFunction<TQueries extends Record<string, ServeEndpoint<a
471
607
  context?: Partial<TContext>;
472
608
  request?: Partial<ServeRequest>;
473
609
  }) => Promise<ServeEndpointResult<TQueries[TKey]>>;
610
+ /**
611
+ * A transport-agnostic API definition. Contains queries, auth, tenancy,
612
+ * and a handler — but no HTTP server. Pass to standalone transport functions:
613
+ *
614
+ * @example
615
+ * ```ts
616
+ * const api = createAPI({ queries: { ... }, auth: jwtStrategy });
617
+ *
618
+ * // Use with any transport:
619
+ * startServer(api, { port: 3000 });
620
+ * app.use('/analytics', toNodeHandler(api));
621
+ * export default toFetchHandler(api);
622
+ * ```
623
+ */
624
+ /** A single route in a {@link RouteManifest}. */
625
+ export interface RouteManifestEntry {
626
+ method: HttpMethod;
627
+ /** Full request path including the API base path. */
628
+ path: string;
629
+ }
630
+ /**
631
+ * Serializable map of query/metric/dataset keys to their HTTP method and full
632
+ * path. Keys match {@link HypeQueryAPI.queries} (including `dataset:<name>`
633
+ * keys for datasets), so it can be paired with `InferAPIType` and handed to
634
+ * `@hypequery/react`'s `createHooks({ manifest })` to resolve client routes
635
+ * without importing server code into the browser bundle.
636
+ */
637
+ export type RouteManifest = Record<string, RouteManifestEntry>;
638
+ export interface HypeQueryAPI<TQueries extends Record<string, ServeEndpoint<any, any, any, any>> = Record<string, ServeEndpoint<any, any, any, any>>, TContext extends Record<string, unknown> = Record<string, unknown>, TAuth extends AuthContext = AuthContext> {
639
+ readonly queries: TQueries;
640
+ /** Serve-layer query logger for subscribing to endpoint execution events */
641
+ readonly queryLogger: ServeQueryLogger;
642
+ /** The underlying request handler. Can be passed directly to transport adapters. */
643
+ readonly handler: ServeHandler;
644
+ /**
645
+ * Build a serializable {@link RouteManifest} of every query/metric/dataset
646
+ * route (method + full path). Safe to JSON-serialize and ship to the client.
647
+ */
648
+ manifest(): RouteManifest;
649
+ route<Path extends string, TKey extends keyof TQueries>(path: Path, endpoint: TQueries[TKey], options?: Partial<RouteRegistrationOptions<TContext, TAuth>>): this;
650
+ use(middleware: ServeMiddleware<any, any, TContext, TAuth>): this;
651
+ useAuth(strategy: AuthStrategy<TAuth>): this;
652
+ execute<TKey extends keyof TQueries>(key: TKey, options?: {
653
+ input?: SchemaInput<TQueries[TKey]["inputSchema"]>;
654
+ context?: Partial<TContext>;
655
+ request?: Partial<ServeRequest>;
656
+ }): Promise<ServeEndpointResult<TQueries[TKey]>>;
657
+ /** Alias of execute() for in-process execution. */
658
+ client<TKey extends keyof TQueries>(key: TKey, options?: {
659
+ input?: SchemaInput<TQueries[TKey]["inputSchema"]>;
660
+ context?: Partial<TContext>;
661
+ request?: Partial<ServeRequest>;
662
+ }): Promise<ServeEndpointResult<TQueries[TKey]>>;
663
+ /** Alias of execute() for in-process execution. */
664
+ run<TKey extends keyof TQueries>(key: TKey, options?: {
665
+ input?: SchemaInput<TQueries[TKey]["inputSchema"]>;
666
+ context?: Partial<TContext>;
667
+ request?: Partial<ServeRequest>;
668
+ }): Promise<ServeEndpointResult<TQueries[TKey]>>;
669
+ describe(): ToolkitDescription;
670
+ }
671
+ /**
672
+ * Infer the API type from a HypeQueryAPI for use with @hypequery/react.
673
+ *
674
+ * @example
675
+ * ```ts
676
+ * const api = createAPI({ queries: { hello: ... } });
677
+ * type Api = InferAPIType<typeof api>;
678
+ * createHooks<Api>({ baseUrl: '/api' });
679
+ * ```
680
+ */
681
+ export type InferAPIType<TTarget> = TTarget extends HypeQueryAPI<infer TQueries, any, any> ? {
682
+ [K in keyof TQueries]: TQueries[K] extends ServeEndpoint<infer TInputSchema, infer TOutputSchema, any, any, any> ? {
683
+ input: SchemaInput<TInputSchema>;
684
+ output: SchemaOutput<TOutputSchema>;
685
+ } : never;
686
+ } : never;
687
+ /**
688
+ * @deprecated Use `HypeQueryAPI` and `createAPI()` instead. `ServeBuilder` adds
689
+ * transport concerns (`start()`) that should be handled separately.
690
+ */
474
691
  export interface ServeBuilder<TQueries extends Record<string, ServeEndpoint<any, any, any, any>> = Record<string, ServeEndpoint<any, any, any, any>>, TContext extends Record<string, unknown> = Record<string, unknown>, TAuth extends AuthContext = AuthContext> {
475
692
  readonly queries: TQueries;
476
693
  /** Base path applied to all registered routes, docs, and OpenAPI endpoints */
@@ -481,6 +698,11 @@ export interface ServeBuilder<TQueries extends Record<string, ServeEndpoint<any,
481
698
  readonly _routeConfig?: Record<string, {
482
699
  method: HttpMethod;
483
700
  }>;
701
+ /**
702
+ * Build a serializable {@link RouteManifest} of every query/metric/dataset
703
+ * route (method + full path). Safe to JSON-serialize and ship to the client.
704
+ */
705
+ manifest(): RouteManifest;
484
706
  route<Path extends string, TKey extends keyof TQueries>(path: Path, endpoint: TQueries[TKey], options?: Partial<RouteRegistrationOptions<TContext, TAuth>>): this;
485
707
  use(middleware: ServeMiddleware<any, any, TContext, TAuth>): this;
486
708
  useAuth(strategy: AuthStrategy<TAuth>): this;
@@ -508,8 +730,8 @@ export interface ServeInitializer<TContext extends Record<string, unknown> = Rec
508
730
  readonly procedure: QueryProcedureBuilder<TContext, TAuth>;
509
731
  readonly query: QueryFactory<TContext, TAuth>;
510
732
  queries<TQueries extends ServeQueriesMap<TContext, TAuth>>(queries: TQueries): TQueries;
511
- serve<TQueries extends ServeQueriesMap<TContext, TAuth>>(config: Omit<ServeConfig<TContext, TAuth, TQueries>, "context">): ServeBuilder<ServeEndpointMap<TQueries, TContext, TAuth>, TContext, TAuth>;
512
- define<TQueries extends ServeQueriesMap<TContext, TAuth>>(config: Omit<ServeConfig<TContext, TAuth, TQueries>, "context">): ServeBuilder<ServeEndpointMap<TQueries, TContext, TAuth>, TContext, TAuth>;
733
+ serve<TQueries extends ServeQueriesMap<TContext, TAuth>, TMetrics extends MetricsConfig<TAuth> = Record<never, never>, TDatasets extends DatasetsConfig<TAuth> = Record<never, never>>(config: Omit<ServeConfig<TContext, TAuth, TQueries, TMetrics, TDatasets>, "context">): ServeBuilder<ServeEndpointMap<TQueries, TContext, TAuth> & ServeSemanticEndpointMap<TMetrics, TDatasets, TContext, TAuth>, TContext, TAuth>;
734
+ define<TQueries extends ServeQueriesMap<TContext, TAuth>, TMetrics extends MetricsConfig<TAuth> = Record<never, never>, TDatasets extends DatasetsConfig<TAuth> = Record<never, never>>(config: Omit<ServeConfig<TContext, TAuth, TQueries, TMetrics, TDatasets>, "context">): ServeBuilder<ServeEndpointMap<TQueries, TContext, TAuth> & ServeSemanticEndpointMap<TMetrics, TDatasets, TContext, TAuth>, TContext, TAuth>;
513
735
  }
514
736
  export type QueryFactory<TContext extends Record<string, unknown>, TAuth extends AuthContext> = QueryProcedureBuilder<TContext, TAuth> & {
515
737
  <TInputSchema extends ZodTypeAny | undefined = undefined, TOutputSchema extends ZodTypeAny | undefined = undefined, TResult = TOutputSchema extends ZodTypeAny ? SchemaOutput<TOutputSchema> : unknown>(config: QueryObjectConfig<TInputSchema, TOutputSchema, TContext, TAuth, TResult>): StandaloneQueryDefinition<TInputSchema, TOutputSchema extends ZodTypeAny ? TOutputSchema : ZodTypeAny, TContext, TAuth, TResult>;
@@ -628,5 +850,4 @@ export type ServeContextFactory<TContext extends Record<string, unknown>, TAuth
628
850
  request: ServeRequest;
629
851
  auth: TAuth | null;
630
852
  }) => MaybePromise<TContext>);
631
- export {};
632
853
  //# sourceMappingURL=types.d.ts.map