@hypequery/serve 0.0.7 → 0.0.9

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 (76) hide show
  1. package/dist/adapters/fetch.d.ts +3 -0
  2. package/dist/adapters/fetch.d.ts.map +1 -0
  3. package/dist/adapters/fetch.js +26 -0
  4. package/dist/adapters/node.d.ts +8 -0
  5. package/dist/adapters/node.d.ts.map +1 -0
  6. package/dist/adapters/node.js +105 -0
  7. package/dist/adapters/utils.d.ts +39 -0
  8. package/dist/adapters/utils.d.ts.map +1 -0
  9. package/dist/adapters/utils.js +114 -0
  10. package/dist/adapters/vercel.d.ts +7 -0
  11. package/dist/adapters/vercel.d.ts.map +1 -0
  12. package/dist/adapters/vercel.js +13 -0
  13. package/dist/auth.d.ts +192 -0
  14. package/dist/auth.d.ts.map +1 -0
  15. package/dist/auth.js +221 -0
  16. package/dist/builder.d.ts +3 -0
  17. package/dist/builder.d.ts.map +1 -0
  18. package/dist/builder.js +56 -0
  19. package/dist/client-config.d.ts +44 -0
  20. package/dist/client-config.d.ts.map +1 -0
  21. package/dist/client-config.js +53 -0
  22. package/dist/dev.d.ts +9 -0
  23. package/dist/dev.d.ts.map +1 -0
  24. package/dist/dev.js +30 -0
  25. package/dist/docs-ui.d.ts +3 -0
  26. package/dist/docs-ui.d.ts.map +1 -0
  27. package/dist/docs-ui.js +34 -0
  28. package/dist/endpoint.d.ts +5 -0
  29. package/dist/endpoint.d.ts.map +1 -0
  30. package/dist/endpoint.js +65 -0
  31. package/dist/index.d.ts +14 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +13 -0
  34. package/dist/openapi.d.ts +3 -0
  35. package/dist/openapi.d.ts.map +1 -0
  36. package/dist/openapi.js +205 -0
  37. package/dist/pipeline.d.ts +77 -0
  38. package/dist/pipeline.d.ts.map +1 -0
  39. package/dist/pipeline.js +424 -0
  40. package/dist/query-logger.d.ts +65 -0
  41. package/dist/query-logger.d.ts.map +1 -0
  42. package/dist/query-logger.js +91 -0
  43. package/dist/router.d.ts +13 -0
  44. package/dist/router.d.ts.map +1 -0
  45. package/dist/router.js +56 -0
  46. package/dist/server/builder.d.ts +7 -0
  47. package/dist/server/builder.d.ts.map +1 -0
  48. package/dist/server/builder.js +73 -0
  49. package/dist/server/define-serve.d.ts +3 -0
  50. package/dist/server/define-serve.d.ts.map +1 -0
  51. package/dist/server/define-serve.js +88 -0
  52. package/dist/server/execute-query.d.ts +8 -0
  53. package/dist/server/execute-query.d.ts.map +1 -0
  54. package/dist/server/execute-query.js +39 -0
  55. package/dist/server/index.d.ts +6 -0
  56. package/dist/server/index.d.ts.map +1 -0
  57. package/dist/server/index.js +5 -0
  58. package/dist/server/init-serve.d.ts +8 -0
  59. package/dist/server/init-serve.d.ts.map +1 -0
  60. package/dist/server/init-serve.js +18 -0
  61. package/dist/server/mapper.d.ts +3 -0
  62. package/dist/server/mapper.d.ts.map +1 -0
  63. package/dist/server/mapper.js +30 -0
  64. package/dist/tenant.d.ts +35 -0
  65. package/dist/tenant.d.ts.map +1 -0
  66. package/dist/tenant.js +49 -0
  67. package/dist/type-tests/builder.test-d.d.ts +13 -0
  68. package/dist/type-tests/builder.test-d.d.ts.map +1 -0
  69. package/dist/type-tests/builder.test-d.js +20 -0
  70. package/dist/types.d.ts +514 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +1 -0
  73. package/dist/utils.d.ts +4 -0
  74. package/dist/utils.d.ts.map +1 -0
  75. package/dist/utils.js +16 -0
  76. package/package.json +1 -1
@@ -0,0 +1,424 @@
1
+ import { z } from 'zod';
2
+ import { createTenantScope, warnTenantMisconfiguration } from './tenant.js';
3
+ import { generateRequestId } from './utils.js';
4
+ import { buildOpenApiDocument } from './openapi.js';
5
+ import { buildDocsHtml } from './docs-ui.js';
6
+ import { checkRoleAuthorization, checkScopeAuthorization, } from './auth.js';
7
+ const safeInvokeHook = async (name, hook, payload) => {
8
+ if (!hook)
9
+ return;
10
+ try {
11
+ await hook(payload);
12
+ }
13
+ catch (error) {
14
+ console.error(`[hypequery/serve] ${name} hook failed`, error);
15
+ }
16
+ };
17
+ const createErrorResponse = (status, type, message, details, headers) => ({
18
+ status,
19
+ headers,
20
+ body: { error: { type, message, ...(details ? { details } : {}) } },
21
+ });
22
+ const buildContextInput = (request) => {
23
+ if (request.body !== undefined && request.body !== null) {
24
+ return request.body;
25
+ }
26
+ if (request.query && Object.keys(request.query).length > 0) {
27
+ return request.query;
28
+ }
29
+ return undefined;
30
+ };
31
+ const runMiddlewares = async (middlewares, ctx, handler) => {
32
+ let current = handler;
33
+ for (let i = middlewares.length - 1; i >= 0; i -= 1) {
34
+ const middleware = middlewares[i];
35
+ const next = current;
36
+ current = () => middleware(ctx, next);
37
+ }
38
+ return current();
39
+ };
40
+ const authenticateRequest = async (strategies, request, metadata) => {
41
+ for (const strategy of strategies) {
42
+ const result = await strategy({ request, endpoint: metadata });
43
+ if (result) {
44
+ return result;
45
+ }
46
+ }
47
+ return null;
48
+ };
49
+ const gatherAuthStrategies = (endpointStrategy, globalStrategies) => {
50
+ const combined = [];
51
+ if (endpointStrategy)
52
+ combined.push(endpointStrategy);
53
+ combined.push(...globalStrategies);
54
+ return combined;
55
+ };
56
+ const computeRequiresAuth = (metadata, endpointStrategy, globalStrategies, endpoint) => {
57
+ // Explicit .public() overrides everything
58
+ if (metadata.requiresAuth === false) {
59
+ return false;
60
+ }
61
+ // Explicit .requireAuth() or roles/scopes imply auth
62
+ if (metadata.requiresAuth === true) {
63
+ return true;
64
+ }
65
+ if ((endpoint.requiredRoles?.length ?? 0) > 0 || (endpoint.requiredScopes?.length ?? 0) > 0) {
66
+ return true;
67
+ }
68
+ if (endpointStrategy) {
69
+ return true;
70
+ }
71
+ return globalStrategies.length > 0;
72
+ };
73
+ const checkAuthorization = (auth, requiredRoles, requiredScopes) => {
74
+ // Check roles first
75
+ if (requiredRoles && requiredRoles.length > 0) {
76
+ const roleResult = checkRoleAuthorization(auth, requiredRoles);
77
+ if (!roleResult.ok) {
78
+ const userRoles = auth?.roles ?? [];
79
+ return { ok: false, reason: roleResult.reason, required: roleResult.missing, actual: userRoles };
80
+ }
81
+ }
82
+ // Check scopes
83
+ if (requiredScopes && requiredScopes.length > 0) {
84
+ const scopeResult = checkScopeAuthorization(auth, requiredScopes);
85
+ if (!scopeResult.ok) {
86
+ const userScopes = auth?.scopes ?? [];
87
+ return { ok: false, reason: scopeResult.reason, required: scopeResult.missing, actual: userScopes };
88
+ }
89
+ }
90
+ return { ok: true };
91
+ };
92
+ const validateInput = (schema, payload) => {
93
+ if (!schema) {
94
+ return { success: true, data: payload };
95
+ }
96
+ const result = schema.safeParse(payload);
97
+ return result.success
98
+ ? { success: true, data: result.data }
99
+ : { success: false, error: result.error };
100
+ };
101
+ const cloneContext = (ctx) => (ctx ? { ...ctx } : {});
102
+ const resolveContext = async (factory, request, auth) => {
103
+ if (!factory) {
104
+ return {};
105
+ }
106
+ if (typeof factory === 'function') {
107
+ const value = await factory({ request, auth });
108
+ return cloneContext(value);
109
+ }
110
+ return cloneContext(factory);
111
+ };
112
+ const resolveRequestId = (request, provided) => provided ?? request.headers['x-request-id'] ?? request.headers['x-trace-id'] ?? generateRequestId();
113
+ export const executeEndpoint = async (options) => {
114
+ const { endpoint, request, requestId: explicitRequestId, authStrategies, contextFactory, globalMiddlewares, tenantConfig, hooks = {}, queryLogger, additionalContext, verboseAuthErrors = false, // Default to secure mode for production safety
115
+ } = options;
116
+ const requestId = resolveRequestId(request, explicitRequestId);
117
+ const locals = {};
118
+ let cacheTtlMs = endpoint.cacheTtlMs ?? null;
119
+ const setCacheTtl = (ttl) => {
120
+ cacheTtlMs = ttl;
121
+ };
122
+ const context = {
123
+ request,
124
+ input: buildContextInput(request),
125
+ auth: null,
126
+ metadata: endpoint.metadata,
127
+ locals,
128
+ setCacheTtl,
129
+ };
130
+ const startedAt = Date.now();
131
+ await safeInvokeHook('onRequestStart', hooks.onRequestStart, {
132
+ requestId,
133
+ queryKey: endpoint.key,
134
+ metadata: endpoint.metadata,
135
+ request,
136
+ auth: context.auth,
137
+ });
138
+ // Skip query logging if no listeners are subscribed
139
+ if (queryLogger?.listenerCount ?? 0 > 0) {
140
+ queryLogger?.emit({
141
+ requestId,
142
+ endpointKey: endpoint.key,
143
+ path: endpoint.metadata.path ?? `/${endpoint.key}`,
144
+ method: request.method,
145
+ status: 'started',
146
+ startTime: startedAt,
147
+ input: request.body ?? request.query,
148
+ });
149
+ }
150
+ try {
151
+ const endpointAuth = endpoint.auth ?? null;
152
+ const strategies = gatherAuthStrategies(endpointAuth, authStrategies ?? []);
153
+ const requiresAuth = computeRequiresAuth(endpoint.metadata, endpointAuth, authStrategies ?? [], endpoint);
154
+ const metadataWithAuth = {
155
+ ...endpoint.metadata,
156
+ requiresAuth,
157
+ };
158
+ context.metadata = metadataWithAuth;
159
+ const authContext = await authenticateRequest(strategies, request, metadataWithAuth);
160
+ if (!authContext && requiresAuth) {
161
+ await safeInvokeHook('onAuthFailure', hooks.onAuthFailure, {
162
+ requestId,
163
+ queryKey: endpoint.key,
164
+ metadata: metadataWithAuth,
165
+ request,
166
+ auth: context.auth,
167
+ reason: 'MISSING',
168
+ });
169
+ return createErrorResponse(401, 'UNAUTHORIZED', verboseAuthErrors ? 'Authentication required' : 'Access denied', {
170
+ reason: 'missing_credentials',
171
+ ...(verboseAuthErrors && { strategies_attempted: strategies.length }),
172
+ endpoint: endpoint.metadata.path,
173
+ }, { 'x-request-id': requestId });
174
+ }
175
+ context.auth = authContext;
176
+ // Check role/scope authorization after successful authentication
177
+ const authzResult = checkAuthorization(authContext, endpoint.requiredRoles, endpoint.requiredScopes);
178
+ if (!authzResult.ok) {
179
+ const label = authzResult.reason === 'MISSING_ROLE' ? 'role' : 'scope';
180
+ await safeInvokeHook('onAuthorizationFailure', hooks.onAuthorizationFailure, {
181
+ requestId,
182
+ queryKey: endpoint.key,
183
+ metadata: metadataWithAuth,
184
+ request,
185
+ auth: authContext,
186
+ reason: authzResult.reason,
187
+ required: authzResult.required,
188
+ actual: authzResult.actual,
189
+ });
190
+ return createErrorResponse(403, 'FORBIDDEN', verboseAuthErrors
191
+ ? `Missing required ${label}: ${authzResult.required.join(', ')}`
192
+ : 'Insufficient permissions', {
193
+ reason: authzResult.reason.toLowerCase(),
194
+ ...(verboseAuthErrors && {
195
+ required: authzResult.required,
196
+ actual: authzResult.actual,
197
+ }),
198
+ endpoint: endpoint.metadata.path,
199
+ });
200
+ }
201
+ const resolvedContext = await resolveContext(contextFactory, request, authContext);
202
+ Object.assign(context, resolvedContext, additionalContext);
203
+ const activeTenantConfig = endpoint.tenant ?? tenantConfig;
204
+ if (activeTenantConfig) {
205
+ const tenantRequired = activeTenantConfig.required !== false;
206
+ const tenantId = authContext ? activeTenantConfig.extract(authContext) : null;
207
+ if (!tenantId && tenantRequired) {
208
+ const errorMessage = activeTenantConfig.errorMessage ??
209
+ 'Tenant context is required but could not be determined from authentication';
210
+ await safeInvokeHook('onError', hooks.onError, {
211
+ requestId,
212
+ queryKey: endpoint.key,
213
+ metadata: metadataWithAuth,
214
+ request,
215
+ auth: context.auth,
216
+ durationMs: Date.now() - startedAt,
217
+ error: new Error(errorMessage),
218
+ });
219
+ return createErrorResponse(403, 'UNAUTHORIZED', errorMessage, {
220
+ reason: 'missing_tenant_context',
221
+ tenant_required: true,
222
+ }, { 'x-request-id': requestId });
223
+ }
224
+ if (tenantId) {
225
+ context.tenantId = tenantId;
226
+ const mode = activeTenantConfig.mode ?? 'manual';
227
+ const column = activeTenantConfig.column;
228
+ if (mode === 'auto-inject' && column) {
229
+ const contextValues = context;
230
+ for (const key of Object.keys(contextValues)) {
231
+ const value = contextValues[key];
232
+ if (value && typeof value === 'object' && 'table' in value && typeof value.table === 'function') {
233
+ contextValues[key] = createTenantScope(value, {
234
+ tenantId,
235
+ column,
236
+ });
237
+ }
238
+ }
239
+ }
240
+ else if (mode === 'manual') {
241
+ warnTenantMisconfiguration({
242
+ queryKey: endpoint.key,
243
+ hasTenantConfig: true,
244
+ hasTenantId: true,
245
+ mode: 'manual',
246
+ });
247
+ }
248
+ }
249
+ else if (!tenantRequired) {
250
+ warnTenantMisconfiguration({
251
+ queryKey: endpoint.key,
252
+ hasTenantConfig: true,
253
+ hasTenantId: false,
254
+ mode: activeTenantConfig.mode,
255
+ });
256
+ }
257
+ }
258
+ const validationResult = validateInput(endpoint.inputSchema, context.input);
259
+ if (!validationResult.success) {
260
+ await safeInvokeHook('onError', hooks.onError, {
261
+ requestId,
262
+ queryKey: endpoint.key,
263
+ metadata: metadataWithAuth,
264
+ request,
265
+ auth: context.auth,
266
+ durationMs: Date.now() - startedAt,
267
+ error: validationResult.error,
268
+ });
269
+ return createErrorResponse(400, 'VALIDATION_ERROR', 'Request validation failed', {
270
+ issues: validationResult.error.issues,
271
+ }, { 'x-request-id': requestId });
272
+ }
273
+ context.input = validationResult.data;
274
+ const pipeline = [
275
+ ...(globalMiddlewares ?? []),
276
+ ...endpoint.middlewares,
277
+ ];
278
+ const result = await runMiddlewares(pipeline, context, () => endpoint.handler(context));
279
+ const headers = {
280
+ ...(endpoint.defaultHeaders ?? {}),
281
+ 'x-request-id': requestId,
282
+ };
283
+ if (typeof cacheTtlMs === 'number') {
284
+ headers['cache-control'] = cacheTtlMs > 0 ? `public, max-age=${Math.floor(cacheTtlMs / 1000)}` : 'no-store';
285
+ }
286
+ const durationMs = Date.now() - startedAt;
287
+ await safeInvokeHook('onRequestEnd', hooks.onRequestEnd, {
288
+ requestId,
289
+ queryKey: endpoint.key,
290
+ metadata: metadataWithAuth,
291
+ request,
292
+ auth: context.auth,
293
+ durationMs,
294
+ result,
295
+ });
296
+ // Skip query logging if no listeners are subscribed
297
+ if (queryLogger?.listenerCount ?? 0 > 0) {
298
+ queryLogger?.emit({
299
+ requestId,
300
+ endpointKey: endpoint.key,
301
+ path: endpoint.metadata.path ?? `/${endpoint.key}`,
302
+ method: request.method,
303
+ status: 'completed',
304
+ startTime: startedAt,
305
+ endTime: startedAt + durationMs,
306
+ durationMs,
307
+ input: context.input,
308
+ responseStatus: 200,
309
+ result,
310
+ });
311
+ }
312
+ return {
313
+ status: 200,
314
+ headers,
315
+ body: result,
316
+ };
317
+ }
318
+ catch (error) {
319
+ const errorDurationMs = Date.now() - startedAt;
320
+ await safeInvokeHook('onError', hooks.onError, {
321
+ requestId,
322
+ queryKey: endpoint.key,
323
+ metadata: context.metadata,
324
+ request,
325
+ auth: context.auth,
326
+ durationMs: errorDurationMs,
327
+ error,
328
+ });
329
+ // Skip query logging if no listeners are subscribed
330
+ if (queryLogger?.listenerCount ?? 0 > 0) {
331
+ queryLogger?.emit({
332
+ requestId,
333
+ endpointKey: endpoint.key,
334
+ path: endpoint.metadata.path ?? `/${endpoint.key}`,
335
+ method: request.method,
336
+ status: 'error',
337
+ startTime: startedAt,
338
+ endTime: startedAt + errorDurationMs,
339
+ durationMs: errorDurationMs,
340
+ input: context.input,
341
+ responseStatus: 500,
342
+ error: error instanceof Error ? error : new Error(String(error)),
343
+ });
344
+ }
345
+ const message = error instanceof Error ? error.message : 'Unexpected error';
346
+ return createErrorResponse(500, 'INTERNAL_SERVER_ERROR', message, undefined, { 'x-request-id': requestId });
347
+ }
348
+ };
349
+ export const createServeHandler = ({ router, globalMiddlewares, authStrategies, tenantConfig, contextFactory, hooks, queryLogger, verboseAuthErrors = false, }) => {
350
+ return async (request) => {
351
+ const requestId = resolveRequestId(request);
352
+ const endpoint = router.match(request.method, request.path);
353
+ if (!endpoint) {
354
+ return createErrorResponse(404, 'NOT_FOUND', `No endpoint registered for ${request.method} ${request.path}`, undefined, { 'x-request-id': requestId });
355
+ }
356
+ return executeEndpoint({
357
+ endpoint,
358
+ request,
359
+ requestId,
360
+ authStrategies,
361
+ contextFactory,
362
+ globalMiddlewares,
363
+ tenantConfig,
364
+ hooks,
365
+ queryLogger,
366
+ verboseAuthErrors,
367
+ });
368
+ };
369
+ };
370
+ export const createOpenApiEndpoint = (path, getEndpoints, options) => {
371
+ let cachedDocument = null;
372
+ return {
373
+ key: '__hypequery_openapi__',
374
+ method: 'GET',
375
+ inputSchema: undefined,
376
+ outputSchema: z.any(),
377
+ handler: async () => {
378
+ if (!cachedDocument) {
379
+ cachedDocument = buildOpenApiDocument(getEndpoints(), options);
380
+ }
381
+ return cachedDocument;
382
+ },
383
+ query: undefined,
384
+ middlewares: [],
385
+ auth: null,
386
+ metadata: {
387
+ path,
388
+ method: 'GET',
389
+ name: 'OpenAPI schema',
390
+ summary: 'OpenAPI schema',
391
+ description: 'Generated OpenAPI specification for the registered endpoints',
392
+ tags: ['docs'],
393
+ requiresAuth: false,
394
+ deprecated: false,
395
+ visibility: 'internal',
396
+ },
397
+ cacheTtlMs: null,
398
+ };
399
+ };
400
+ export const createDocsEndpoint = (path, openapiPath, options) => ({
401
+ key: '__hypequery_docs__',
402
+ method: 'GET',
403
+ inputSchema: undefined,
404
+ outputSchema: z.string(),
405
+ handler: async () => buildDocsHtml(openapiPath, options),
406
+ query: undefined,
407
+ middlewares: [],
408
+ auth: null,
409
+ metadata: {
410
+ path,
411
+ method: 'GET',
412
+ name: 'Docs',
413
+ summary: 'API documentation',
414
+ description: 'Auto-generated documentation for your hypequery endpoints',
415
+ tags: ['docs'],
416
+ requiresAuth: false,
417
+ deprecated: false,
418
+ visibility: 'internal',
419
+ },
420
+ cacheTtlMs: null,
421
+ defaultHeaders: {
422
+ 'content-type': 'text/html; charset=utf-8',
423
+ },
424
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Backend-agnostic query event logger for the serve layer.
3
+ *
4
+ * Fires for every endpoint execution regardless of the underlying
5
+ * query backend (ClickHouse, BigQuery, mock data, etc.).
6
+ */
7
+ /**
8
+ * Event emitted by the serve-layer query logger.
9
+ */
10
+ export interface ServeQueryEvent {
11
+ requestId: string;
12
+ endpointKey: string;
13
+ path: string;
14
+ method: string;
15
+ status: 'started' | 'completed' | 'error';
16
+ startTime: number;
17
+ endTime?: number;
18
+ durationMs?: number;
19
+ input?: unknown;
20
+ responseStatus?: number;
21
+ error?: Error;
22
+ result?: unknown;
23
+ }
24
+ /**
25
+ * Callback for serve query events.
26
+ */
27
+ export type ServeQueryEventCallback = (event: ServeQueryEvent) => void;
28
+ /**
29
+ * Serve-layer query event emitter.
30
+ *
31
+ * Created per `defineServe()` call — not a singleton.
32
+ * Emits events at the request lifecycle level so that dev tools,
33
+ * logging, and analytics work with any query backend.
34
+ */
35
+ export declare class ServeQueryLogger {
36
+ private listeners;
37
+ /**
38
+ * Subscribe to query events.
39
+ * @returns Unsubscribe function.
40
+ */
41
+ on(callback: ServeQueryEventCallback): () => void;
42
+ /**
43
+ * Emit a query event to all listeners.
44
+ */
45
+ emit(event: ServeQueryEvent): void;
46
+ /**
47
+ * Number of active listeners.
48
+ */
49
+ get listenerCount(): number;
50
+ /**
51
+ * Remove all listeners.
52
+ */
53
+ removeAll(): void;
54
+ }
55
+ /**
56
+ * Format a query event as a human-readable log line.
57
+ * Returns null for 'started' events (only logs completions).
58
+ */
59
+ export declare function formatQueryEvent(event: ServeQueryEvent): string | null;
60
+ /**
61
+ * Format a query event as a structured JSON string for log aggregators.
62
+ * Returns null for 'started' events (only logs completions).
63
+ */
64
+ export declare function formatQueryEventJSON(event: ServeQueryEvent): string | null;
65
+ //# sourceMappingURL=query-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-logger.d.ts","sourceRoot":"","sources":["../src/query-logger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,OAAO,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;AAEvE;;;;;;GAMG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,SAAS,CAAsC;IAEvD;;;OAGG;IACH,EAAE,CAAC,QAAQ,EAAE,uBAAuB,GAAG,MAAM,IAAI;IAOjD;;OAEG;IACH,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAUlC;;OAEG;IACH,IAAI,aAAa,IAAI,MAAM,CAE1B;IAED;;OAEG;IACH,SAAS,IAAI,IAAI;CAGlB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAYtE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAiB1E"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Backend-agnostic query event logger for the serve layer.
3
+ *
4
+ * Fires for every endpoint execution regardless of the underlying
5
+ * query backend (ClickHouse, BigQuery, mock data, etc.).
6
+ */
7
+ /**
8
+ * Serve-layer query event emitter.
9
+ *
10
+ * Created per `defineServe()` call — not a singleton.
11
+ * Emits events at the request lifecycle level so that dev tools,
12
+ * logging, and analytics work with any query backend.
13
+ */
14
+ export class ServeQueryLogger {
15
+ constructor() {
16
+ this.listeners = new Set();
17
+ }
18
+ /**
19
+ * Subscribe to query events.
20
+ * @returns Unsubscribe function.
21
+ */
22
+ on(callback) {
23
+ this.listeners.add(callback);
24
+ return () => {
25
+ this.listeners.delete(callback);
26
+ };
27
+ }
28
+ /**
29
+ * Emit a query event to all listeners.
30
+ */
31
+ emit(event) {
32
+ for (const listener of this.listeners) {
33
+ try {
34
+ listener(event);
35
+ }
36
+ catch {
37
+ // Ignore listener errors
38
+ }
39
+ }
40
+ }
41
+ /**
42
+ * Number of active listeners.
43
+ */
44
+ get listenerCount() {
45
+ return this.listeners.size;
46
+ }
47
+ /**
48
+ * Remove all listeners.
49
+ */
50
+ removeAll() {
51
+ this.listeners.clear();
52
+ }
53
+ }
54
+ /**
55
+ * Format a query event as a human-readable log line.
56
+ * Returns null for 'started' events (only logs completions).
57
+ */
58
+ export function formatQueryEvent(event) {
59
+ if (event.status === 'started')
60
+ return null;
61
+ const status = event.status === 'completed' ? '✓' : '✗';
62
+ const duration = event.durationMs != null ? `${event.durationMs}ms` : '?';
63
+ const code = event.responseStatus ?? (event.status === 'error' ? 500 : 200);
64
+ let line = ` ${status} ${event.method} ${event.path} → ${code} (${duration})`;
65
+ if (event.status === 'error' && event.error) {
66
+ line += ` — ${event.error.message}`;
67
+ }
68
+ return line;
69
+ }
70
+ /**
71
+ * Format a query event as a structured JSON string for log aggregators.
72
+ * Returns null for 'started' events (only logs completions).
73
+ */
74
+ export function formatQueryEventJSON(event) {
75
+ if (event.status === 'started')
76
+ return null;
77
+ return JSON.stringify({
78
+ level: event.status === 'error' ? 'error' : 'info',
79
+ msg: `${event.method} ${event.path}`,
80
+ requestId: event.requestId,
81
+ endpoint: event.endpointKey,
82
+ path: event.path,
83
+ method: event.method,
84
+ status: event.responseStatus ?? (event.status === 'error' ? 500 : 200),
85
+ durationMs: event.durationMs,
86
+ ...(event.status === 'error' && event.error
87
+ ? { error: event.error.message }
88
+ : {}),
89
+ timestamp: new Date(event.endTime ?? event.startTime).toISOString(),
90
+ });
91
+ }
@@ -0,0 +1,13 @@
1
+ import type { EndpointRegistry, HttpMethod, ServeEndpoint } from "./types.js";
2
+ export declare const normalizeRoutePath: (path: string) => string;
3
+ export declare const applyBasePath: (basePath: string, path: string) => string;
4
+ export declare class ServeRouter implements EndpointRegistry {
5
+ private readonly basePath;
6
+ private routes;
7
+ constructor(basePath?: string);
8
+ list(): ServeEndpoint<any, any, any, any, any>[];
9
+ register(endpoint: ServeEndpoint<any, any, any, any>): void;
10
+ match(method: HttpMethod, path: string): ServeEndpoint<any, any, any, any, any> | null;
11
+ markRoutesRequireAuth(): void;
12
+ }
13
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAI9E,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,WAG9C,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,WAM3D,CAAC;AAEF,qBAAa,WAAY,YAAW,gBAAgB;IAGtC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAFrC,OAAO,CAAC,MAAM,CAA2C;gBAE5B,QAAQ,SAAK;IAE1C,IAAI;IAIJ,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC;IAuBpD,KAAK,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM;IAStC,qBAAqB;CAetB"}
package/dist/router.js ADDED
@@ -0,0 +1,56 @@
1
+ const trimSlashes = (value) => value.replace(/^\/+|\/+$/g, "");
2
+ export const normalizeRoutePath = (path) => {
3
+ const trimmed = trimSlashes(path || "/");
4
+ return `/${trimmed}`.replace(/\/+/g, "/").replace(/\/$/, trimmed ? "" : "/");
5
+ };
6
+ export const applyBasePath = (basePath, path) => {
7
+ const parts = [trimSlashes(basePath ?? ""), trimSlashes(path)]
8
+ .filter(Boolean)
9
+ .join("/");
10
+ const combined = parts ? `/${parts}` : "/";
11
+ return combined.replace(/\/+/g, "/").replace(/\/$/, combined === "/" ? "/" : "");
12
+ };
13
+ export class ServeRouter {
14
+ constructor(basePath = "") {
15
+ this.basePath = basePath;
16
+ this.routes = [];
17
+ }
18
+ list() {
19
+ return [...this.routes];
20
+ }
21
+ register(endpoint) {
22
+ const path = endpoint.metadata.path || "/";
23
+ const normalizedPath = applyBasePath(this.basePath, path);
24
+ const method = endpoint.method;
25
+ const existing = this.routes.find((route) => route.metadata.path === normalizedPath && route.method === method);
26
+ if (existing) {
27
+ throw new Error(`Route already registered for ${method} ${normalizedPath}`);
28
+ }
29
+ this.routes.push({
30
+ ...endpoint,
31
+ metadata: {
32
+ ...endpoint.metadata,
33
+ path: normalizedPath,
34
+ method,
35
+ },
36
+ });
37
+ }
38
+ match(method, path) {
39
+ const normalizedPath = normalizeRoutePath(path);
40
+ return (this.routes.find((route) => route.method === method && route.metadata.path === normalizedPath) ?? null);
41
+ }
42
+ markRoutesRequireAuth() {
43
+ this.routes = this.routes.map((route) => {
44
+ if (route.metadata.requiresAuth === false) {
45
+ return route;
46
+ }
47
+ return {
48
+ ...route,
49
+ metadata: {
50
+ ...route.metadata,
51
+ requiresAuth: true,
52
+ },
53
+ };
54
+ });
55
+ }
56
+ }
@@ -0,0 +1,7 @@
1
+ import type { AuthContext, AuthStrategy, HttpMethod, ServeBuilder, ServeEndpointMap, ServeMiddleware, ServeQueriesMap, ServeHandler, ExecuteQueryFunction } from "../types.js";
2
+ import type { ServeRouter } from "../router.js";
3
+ import { ServeQueryLogger } from "../query-logger.js";
4
+ export declare const createBuilderMethods: <TQueries extends ServeQueriesMap<TContext, TAuth>, TContext extends Record<string, unknown>, TAuth extends AuthContext>(queryEntries: ServeEndpointMap<TQueries, TContext, TAuth>, queryLogger: ServeQueryLogger, routeConfig: Record<string, {
5
+ method: HttpMethod;
6
+ }>, router: ServeRouter, authStrategies: AuthStrategy<TAuth>[], globalMiddlewares: ServeMiddleware<any, any, TContext, TAuth>[], executeQuery: ExecuteQueryFunction<ServeEndpointMap<TQueries, TContext, TAuth>, TContext, TAuth>, handler: ServeHandler, basePath: string) => ServeBuilder<ServeEndpointMap<TQueries, TContext, TAuth>, TContext, TAuth>;
7
+ //# sourceMappingURL=builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/server/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,UAAU,EACV,YAAY,EAEZ,gBAAgB,EAChB,eAAe,EAEf,eAAe,EACf,YAAY,EACZ,oBAAoB,EAGrB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAKtD,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,SAAS,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,EACjD,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,EAEzB,cAAc,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,EACzD,aAAa,gBAAgB,EAC7B,aAAa,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAAC,EACnD,QAAQ,WAAW,EACnB,gBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,EACrC,mBAAmB,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,EAC/D,cAAc,oBAAoB,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,EAChG,SAAS,YAAY,EACrB,UAAU,MAAM,KACf,YAAY,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,CAoF3E,CAAC"}