@hypequery/serve 0.0.4 → 0.0.5

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 (62) 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 +14 -0
  14. package/dist/auth.d.ts.map +1 -0
  15. package/dist/auth.js +37 -0
  16. package/dist/builder.d.ts +3 -0
  17. package/dist/builder.d.ts.map +1 -0
  18. package/dist/builder.js +41 -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 +24 -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 +59 -0
  31. package/dist/index.d.ts +13 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +12 -0
  34. package/dist/openapi.d.ts +3 -0
  35. package/dist/openapi.d.ts.map +1 -0
  36. package/dist/openapi.js +189 -0
  37. package/dist/pipeline.d.ts +72 -0
  38. package/dist/pipeline.d.ts.map +1 -0
  39. package/dist/pipeline.js +317 -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.d.ts +9 -0
  47. package/dist/server.d.ts.map +1 -0
  48. package/dist/server.js +191 -0
  49. package/dist/tenant.d.ts +35 -0
  50. package/dist/tenant.d.ts.map +1 -0
  51. package/dist/tenant.js +49 -0
  52. package/dist/type-tests/builder.test-d.d.ts +13 -0
  53. package/dist/type-tests/builder.test-d.d.ts.map +1 -0
  54. package/dist/type-tests/builder.test-d.js +20 -0
  55. package/dist/types.d.ts +373 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +1 -0
  58. package/dist/utils.d.ts +4 -0
  59. package/dist/utils.d.ts.map +1 -0
  60. package/dist/utils.js +16 -0
  61. package/package.json +9 -9
  62. package/LICENSE +0 -201
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ const fallbackSchema = z.any();
3
+ const resolveQueryRunner = (query) => {
4
+ if (!query) {
5
+ return null;
6
+ }
7
+ const fn = typeof query === "function"
8
+ ? query
9
+ : typeof query === "object" && typeof query.run === "function"
10
+ ? query.run.bind(query)
11
+ : null;
12
+ if (!fn) {
13
+ return null;
14
+ }
15
+ return async (args) => {
16
+ return fn(args);
17
+ };
18
+ };
19
+ export const createEndpoint = (key, definition) => {
20
+ const method = definition.method ?? "GET";
21
+ const metadata = {
22
+ path: "",
23
+ method: method,
24
+ name: definition.name ?? definition.summary ?? key,
25
+ summary: definition.summary,
26
+ description: definition.description,
27
+ tags: definition.tags ?? [],
28
+ requiresAuth: definition.auth ? true : undefined,
29
+ deprecated: undefined,
30
+ visibility: "public",
31
+ custom: definition.custom,
32
+ };
33
+ const runner = resolveQueryRunner(definition.query);
34
+ const handler = async (ctx) => {
35
+ if (!runner) {
36
+ throw new Error(`Endpoint "${key}" is missing an executable query`);
37
+ }
38
+ return runner({
39
+ input: ctx.input,
40
+ ctx: ctx,
41
+ });
42
+ };
43
+ const outputSchema = (definition.outputSchema ?? fallbackSchema);
44
+ const inputSchema = definition.inputSchema;
45
+ return {
46
+ key,
47
+ method,
48
+ inputSchema,
49
+ outputSchema,
50
+ handler,
51
+ query: definition.query,
52
+ middlewares: definition.middlewares ?? [],
53
+ auth: definition.auth ?? null,
54
+ tenant: definition.tenant,
55
+ metadata,
56
+ cacheTtlMs: definition.cacheTtlMs ?? null,
57
+ defaultHeaders: undefined,
58
+ };
59
+ };
@@ -0,0 +1,13 @@
1
+ export * from "./types.js";
2
+ export * from "./server.js";
3
+ export * from "./router.js";
4
+ export * from "./endpoint.js";
5
+ export * from "./openapi.js";
6
+ export * from "./docs-ui.js";
7
+ export * from "./auth.js";
8
+ export * from "./client-config.js";
9
+ export * from "./adapters/node.js";
10
+ export * from "./adapters/fetch.js";
11
+ export * from "./adapters/vercel.js";
12
+ export * from "./dev.js";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export * from "./types.js";
2
+ export * from "./server.js";
3
+ export * from "./router.js";
4
+ export * from "./endpoint.js";
5
+ export * from "./openapi.js";
6
+ export * from "./docs-ui.js";
7
+ export * from "./auth.js";
8
+ export * from "./client-config.js";
9
+ export * from "./adapters/node.js";
10
+ export * from "./adapters/fetch.js";
11
+ export * from "./adapters/vercel.js";
12
+ export * from "./dev.js";
@@ -0,0 +1,3 @@
1
+ import type { OpenApiDocument, OpenApiOptions, ServeEndpoint } from "./types.js";
2
+ export declare const buildOpenApiDocument: (endpoints: ServeEndpoint<any, any, any, any>[], options?: OpenApiOptions) => OpenApiDocument;
3
+ //# sourceMappingURL=openapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAqLjF,eAAO,MAAM,oBAAoB,GAC/B,WAAW,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,EAC9C,UAAU,cAAc,KACvB,eAyCF,CAAC"}
@@ -0,0 +1,189 @@
1
+ import { zodToJsonSchema } from "zod-to-json-schema";
2
+ const ERROR_SCHEMA = {
3
+ type: "object",
4
+ properties: {
5
+ error: {
6
+ type: "object",
7
+ properties: {
8
+ type: { type: "string" },
9
+ message: { type: "string" },
10
+ details: { type: "object" },
11
+ },
12
+ required: ["type", "message"],
13
+ },
14
+ },
15
+ required: ["error"],
16
+ };
17
+ const dereferenceSchema = (schema) => {
18
+ if (!schema || typeof schema !== "object") {
19
+ return schema;
20
+ }
21
+ if (schema.$ref && schema.definitions) {
22
+ const refKey = String(schema.$ref).split("/").pop();
23
+ if (refKey && schema.definitions[refKey]) {
24
+ return schema.definitions[refKey];
25
+ }
26
+ }
27
+ return schema;
28
+ };
29
+ const removeDefinitions = (schema) => {
30
+ if (!schema || typeof schema !== "object") {
31
+ return schema;
32
+ }
33
+ // Handle arrays
34
+ if (Array.isArray(schema)) {
35
+ return schema.map(item => removeDefinitions(item));
36
+ }
37
+ // If this schema has a $ref and definitions, inline the definition
38
+ if (schema.$ref && schema.definitions) {
39
+ const refKey = String(schema.$ref).split("/").pop();
40
+ if (refKey && schema.definitions[refKey]) {
41
+ const resolved = schema.definitions[refKey];
42
+ delete schema.$ref;
43
+ delete schema.definitions;
44
+ return removeDefinitions({ ...schema, ...resolved });
45
+ }
46
+ }
47
+ // Remove definitions property if it exists
48
+ const { definitions: _definitions, $ref: _ref, ...rest } = schema;
49
+ // Recursively clean nested objects and arrays
50
+ const result = {};
51
+ for (const [key, value] of Object.entries(rest)) {
52
+ if (Array.isArray(value)) {
53
+ result[key] = value.map(item => typeof item === "object" && item !== null ? removeDefinitions(item) : item);
54
+ }
55
+ else if (typeof value === "object" && value !== null) {
56
+ result[key] = removeDefinitions(value);
57
+ }
58
+ else {
59
+ result[key] = value;
60
+ }
61
+ }
62
+ return result;
63
+ };
64
+ const toJsonSchema = (schema, name) => {
65
+ if (!schema) {
66
+ return { type: "object" };
67
+ }
68
+ const jsonSchema = zodToJsonSchema(schema, {
69
+ target: "openApi3",
70
+ name,
71
+ $refStrategy: "none",
72
+ });
73
+ return removeDefinitions(jsonSchema);
74
+ };
75
+ const toQueryParameters = (schema, name) => {
76
+ if (!schema) {
77
+ return [];
78
+ }
79
+ const jsonSchema = dereferenceSchema(toJsonSchema(schema, name));
80
+ if (!jsonSchema || typeof jsonSchema !== "object") {
81
+ return [];
82
+ }
83
+ const schemaType = jsonSchema.type;
84
+ const isObjectType = schemaType === "object" || (Array.isArray(schemaType) && schemaType.includes("object"));
85
+ if (!isObjectType || typeof jsonSchema.properties !== "object") {
86
+ return [];
87
+ }
88
+ const properties = jsonSchema.properties;
89
+ const requiredSet = new Set(Array.isArray(jsonSchema.required) ? jsonSchema.required : []);
90
+ return Object.entries(properties).map(([key, value]) => ({
91
+ name: key,
92
+ in: "query",
93
+ required: requiredSet.has(key),
94
+ schema: value,
95
+ }));
96
+ };
97
+ const toOperation = (endpoint, nameSuffix) => {
98
+ const operation = {
99
+ operationId: endpoint.key,
100
+ summary: endpoint.metadata.summary,
101
+ description: endpoint.metadata.description,
102
+ tags: endpoint.metadata.tags.length ? endpoint.metadata.tags : undefined,
103
+ responses: {
104
+ 200: {
105
+ description: "Successful response",
106
+ content: {
107
+ "application/json": {
108
+ schema: toJsonSchema(endpoint.outputSchema, `${endpoint.key}${nameSuffix}`),
109
+ },
110
+ },
111
+ },
112
+ default: {
113
+ description: "Error response",
114
+ content: {
115
+ "application/json": {
116
+ schema: ERROR_SCHEMA,
117
+ },
118
+ },
119
+ },
120
+ },
121
+ };
122
+ const queryParameters = endpoint.method === "GET"
123
+ ? toQueryParameters(endpoint.inputSchema, `${endpoint.key}Query`)
124
+ : [];
125
+ if (queryParameters.length > 0) {
126
+ operation.parameters = queryParameters;
127
+ }
128
+ else if (endpoint.inputSchema) {
129
+ operation.requestBody = {
130
+ required: true,
131
+ content: {
132
+ "application/json": {
133
+ schema: toJsonSchema(endpoint.inputSchema, `${endpoint.key}Request`),
134
+ },
135
+ },
136
+ };
137
+ }
138
+ if (endpoint.metadata.requiresAuth) {
139
+ operation.security = [{ ApiKeyAuth: [] }];
140
+ }
141
+ return operation;
142
+ };
143
+ const normalizeInfo = (options) => {
144
+ const info = options?.info;
145
+ return {
146
+ title: info?.title ?? "hypequery API",
147
+ version: options?.version ?? "1.0.0",
148
+ description: info?.description,
149
+ termsOfService: info?.termsOfService,
150
+ contact: info?.contact,
151
+ license: info?.license,
152
+ };
153
+ };
154
+ export const buildOpenApiDocument = (endpoints, options) => {
155
+ var _a;
156
+ const document = {
157
+ openapi: "3.1.0",
158
+ info: normalizeInfo(options),
159
+ servers: options?.servers ?? [],
160
+ paths: {},
161
+ };
162
+ let needsSecurityScheme = false;
163
+ for (const endpoint of endpoints) {
164
+ if (endpoint.metadata.visibility && endpoint.metadata.visibility !== "public") {
165
+ continue;
166
+ }
167
+ const path = endpoint.metadata.path || "/";
168
+ const method = endpoint.method.toLowerCase();
169
+ const pathItem = ((_a = document.paths)[path] ?? (_a[path] = {}));
170
+ const operation = toOperation(endpoint, "Response");
171
+ if (endpoint.metadata.requiresAuth) {
172
+ needsSecurityScheme = true;
173
+ }
174
+ pathItem[method] = operation;
175
+ }
176
+ if (needsSecurityScheme) {
177
+ document.components = {
178
+ ...(document.components ?? {}),
179
+ securitySchemes: {
180
+ ApiKeyAuth: {
181
+ type: "apiKey",
182
+ name: "Authorization",
183
+ in: "header",
184
+ },
185
+ },
186
+ };
187
+ }
188
+ return document;
189
+ };
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import type { AuthContext, AuthStrategy, DocsOptions, OpenApiOptions, ServeContextFactory, ServeEndpoint, ServeHandler, ServeLifecycleHooks, ServeMiddleware, ServeRequest, ServeResponse, TenantConfig } from './types.js';
3
+ export interface ExecuteEndpointOptions<TContext extends Record<string, unknown>, TAuth extends AuthContext> {
4
+ endpoint: ServeEndpoint<any, any, TContext, TAuth>;
5
+ request: ServeRequest;
6
+ requestId?: string;
7
+ authStrategies: AuthStrategy<TAuth>[];
8
+ contextFactory?: ServeContextFactory<TContext, TAuth>;
9
+ globalMiddlewares: ServeMiddleware<any, any, TContext, TAuth>[];
10
+ tenantConfig?: TenantConfig<TAuth>;
11
+ hooks?: ServeLifecycleHooks<TAuth>;
12
+ additionalContext?: Partial<TContext>;
13
+ }
14
+ export declare const executeEndpoint: <TContext extends Record<string, unknown>, TAuth extends AuthContext>(options: ExecuteEndpointOptions<TContext, TAuth>) => Promise<ServeResponse>;
15
+ interface HandlerOptions<TContext extends Record<string, unknown>, TAuth extends AuthContext> {
16
+ router: import('./router.js').ServeRouter;
17
+ globalMiddlewares: ServeMiddleware<any, any, TContext, TAuth>[];
18
+ authStrategies: AuthStrategy<TAuth>[];
19
+ tenantConfig?: TenantConfig<TAuth>;
20
+ contextFactory?: ServeContextFactory<TContext, TAuth>;
21
+ hooks?: ServeLifecycleHooks<TAuth>;
22
+ }
23
+ export declare const createServeHandler: <TContext extends Record<string, unknown>, TAuth extends AuthContext>({ router, globalMiddlewares, authStrategies, tenantConfig, contextFactory, hooks, }: HandlerOptions<TContext, TAuth>) => ServeHandler;
24
+ export declare const createOpenApiEndpoint: (path: string, getEndpoints: () => ServeEndpoint<any, any, any, any>[], options?: OpenApiOptions) => {
25
+ key: string;
26
+ method: "GET";
27
+ inputSchema: undefined;
28
+ outputSchema: z.ZodAny;
29
+ handler: () => Promise<unknown>;
30
+ query: undefined;
31
+ middlewares: never[];
32
+ auth: null;
33
+ metadata: {
34
+ path: string;
35
+ method: "GET";
36
+ name: string;
37
+ summary: string;
38
+ description: string;
39
+ tags: string[];
40
+ requiresAuth: false;
41
+ deprecated: false;
42
+ visibility: "internal";
43
+ };
44
+ cacheTtlMs: null;
45
+ };
46
+ export declare const createDocsEndpoint: (path: string, openapiPath: string, options?: DocsOptions) => {
47
+ key: string;
48
+ method: "GET";
49
+ inputSchema: undefined;
50
+ outputSchema: z.ZodString;
51
+ handler: () => Promise<string>;
52
+ query: undefined;
53
+ middlewares: never[];
54
+ auth: null;
55
+ metadata: {
56
+ path: string;
57
+ method: "GET";
58
+ name: string;
59
+ summary: string;
60
+ description: string;
61
+ tags: string[];
62
+ requiresAuth: false;
63
+ deprecated: false;
64
+ visibility: "internal";
65
+ };
66
+ cacheTtlMs: null;
67
+ defaultHeaders: {
68
+ 'content-type': string;
69
+ };
70
+ };
71
+ export {};
72
+ //# sourceMappingURL=pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAC;AAEzC,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EAKX,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,aAAa,EACb,YAAY,EAEb,MAAM,YAAY,CAAC;AAiIpB,MAAM,WAAW,sBAAsB,CACrC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW;IAEzB,QAAQ,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IACnD,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;IACtC,cAAc,CAAC,EAAE,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACtD,iBAAiB,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;IAChE,YAAY,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACnC,iBAAiB,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,eAAe,GAC1B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,EAEzB,SAAS,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC,KAC/C,OAAO,CAAC,aAAa,CA0LvB,CAAC;AAEF,UAAU,cAAc,CACtB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW;IAEzB,MAAM,EAAE,OAAO,aAAa,EAAE,WAAW,CAAC;IAC1C,iBAAiB,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;IAChE,cAAc,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;IACtC,YAAY,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC,cAAc,CAAC,EAAE,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,mBAAmB,CAAC,KAAK,CAAC,CAAC;CACpC;AAED,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,EACzB,qFAOC,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAG,YAqBpC,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,cAAc,MAAM,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,EACvD,UAAU,cAAc;;;;;;;;;;;;;;;;;;;;;CA8BzB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,MAAM,EACZ,aAAa,MAAM,EACnB,UAAU,WAAW;;;;;;;;;;;;;;;;;;;;;;;;CAyBmD,CAAC"}
@@ -0,0 +1,317 @@
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
+ const safeInvokeHook = async (name, hook, payload) => {
7
+ if (!hook)
8
+ return;
9
+ try {
10
+ await hook(payload);
11
+ }
12
+ catch (error) {
13
+ console.error(`[hypequery/serve] ${name} hook failed`, error);
14
+ }
15
+ };
16
+ const createErrorResponse = (status, type, message, details) => ({
17
+ status,
18
+ body: { error: { type, message, ...(details ? { details } : {}) } },
19
+ });
20
+ const buildContextInput = (request) => {
21
+ if (request.body !== undefined && request.body !== null) {
22
+ return request.body;
23
+ }
24
+ if (request.query && Object.keys(request.query).length > 0) {
25
+ return request.query;
26
+ }
27
+ return {};
28
+ };
29
+ const runMiddlewares = async (middlewares, ctx, handler) => {
30
+ let current = handler;
31
+ for (let i = middlewares.length - 1; i >= 0; i -= 1) {
32
+ const middleware = middlewares[i];
33
+ const next = current;
34
+ current = () => middleware(ctx, next);
35
+ }
36
+ return current();
37
+ };
38
+ const authenticateRequest = async (strategies, request, metadata) => {
39
+ for (const strategy of strategies) {
40
+ const result = await strategy({ request, endpoint: metadata });
41
+ if (result) {
42
+ return result;
43
+ }
44
+ }
45
+ return null;
46
+ };
47
+ const gatherAuthStrategies = (endpointStrategy, globalStrategies) => {
48
+ const combined = [];
49
+ if (endpointStrategy)
50
+ combined.push(endpointStrategy);
51
+ combined.push(...globalStrategies);
52
+ return combined;
53
+ };
54
+ const computeRequiresAuth = (metadata, endpointStrategy, globalStrategies) => {
55
+ if (typeof metadata.requiresAuth === 'boolean') {
56
+ return metadata.requiresAuth;
57
+ }
58
+ if (endpointStrategy) {
59
+ return true;
60
+ }
61
+ return globalStrategies.length > 0;
62
+ };
63
+ const validateInput = (schema, payload) => {
64
+ if (!schema) {
65
+ return { success: true, data: payload };
66
+ }
67
+ const result = schema.safeParse(payload);
68
+ return result.success
69
+ ? { success: true, data: result.data }
70
+ : { success: false, error: result.error };
71
+ };
72
+ const cloneContext = (ctx) => (ctx ? { ...ctx } : {});
73
+ const resolveContext = async (factory, request, auth) => {
74
+ if (!factory) {
75
+ return {};
76
+ }
77
+ if (typeof factory === 'function') {
78
+ const value = await factory({ request, auth });
79
+ return cloneContext(value);
80
+ }
81
+ return cloneContext(factory);
82
+ };
83
+ const resolveRequestId = (request, provided) => provided ?? request.headers['x-request-id'] ?? request.headers['x-trace-id'] ?? generateRequestId();
84
+ export const executeEndpoint = async (options) => {
85
+ const { endpoint, request, requestId: explicitRequestId, authStrategies, contextFactory, globalMiddlewares, tenantConfig, hooks = {}, additionalContext, } = options;
86
+ const requestId = resolveRequestId(request, explicitRequestId);
87
+ const locals = {};
88
+ let cacheTtlMs = endpoint.cacheTtlMs ?? null;
89
+ const setCacheTtl = (ttl) => {
90
+ cacheTtlMs = ttl;
91
+ };
92
+ const context = {
93
+ request,
94
+ input: buildContextInput(request),
95
+ auth: null,
96
+ metadata: endpoint.metadata,
97
+ locals,
98
+ setCacheTtl,
99
+ };
100
+ const startedAt = Date.now();
101
+ await safeInvokeHook('onRequestStart', hooks.onRequestStart, {
102
+ requestId,
103
+ queryKey: endpoint.key,
104
+ metadata: endpoint.metadata,
105
+ request,
106
+ auth: context.auth,
107
+ });
108
+ try {
109
+ const endpointAuth = endpoint.auth ?? null;
110
+ const strategies = gatherAuthStrategies(endpointAuth, authStrategies ?? []);
111
+ const requiresAuth = computeRequiresAuth(endpoint.metadata, endpointAuth, authStrategies ?? []);
112
+ const metadataWithAuth = {
113
+ ...endpoint.metadata,
114
+ requiresAuth,
115
+ };
116
+ context.metadata = metadataWithAuth;
117
+ const authContext = await authenticateRequest(strategies, request, metadataWithAuth);
118
+ if (!authContext && requiresAuth) {
119
+ await safeInvokeHook('onAuthFailure', hooks.onAuthFailure, {
120
+ requestId,
121
+ queryKey: endpoint.key,
122
+ metadata: metadataWithAuth,
123
+ request,
124
+ auth: context.auth,
125
+ reason: 'MISSING',
126
+ });
127
+ return createErrorResponse(401, 'UNAUTHORIZED', 'Authentication required', {
128
+ reason: 'missing_credentials',
129
+ strategies_attempted: strategies.length,
130
+ endpoint: endpoint.metadata.path,
131
+ });
132
+ }
133
+ context.auth = authContext;
134
+ const resolvedContext = await resolveContext(contextFactory, request, authContext);
135
+ Object.assign(context, resolvedContext, additionalContext);
136
+ const activeTenantConfig = endpoint.tenant ?? tenantConfig;
137
+ if (activeTenantConfig) {
138
+ const tenantRequired = activeTenantConfig.required !== false;
139
+ const tenantId = authContext ? activeTenantConfig.extract(authContext) : null;
140
+ if (!tenantId && tenantRequired) {
141
+ const errorMessage = activeTenantConfig.errorMessage ??
142
+ 'Tenant context is required but could not be determined from authentication';
143
+ await safeInvokeHook('onError', hooks.onError, {
144
+ requestId,
145
+ queryKey: endpoint.key,
146
+ metadata: metadataWithAuth,
147
+ request,
148
+ auth: context.auth,
149
+ durationMs: Date.now() - startedAt,
150
+ error: new Error(errorMessage),
151
+ });
152
+ return createErrorResponse(403, 'UNAUTHORIZED', errorMessage, {
153
+ reason: 'missing_tenant_context',
154
+ tenant_required: true,
155
+ });
156
+ }
157
+ if (tenantId) {
158
+ context.tenantId = tenantId;
159
+ const mode = activeTenantConfig.mode ?? 'manual';
160
+ const column = activeTenantConfig.column;
161
+ if (mode === 'auto-inject' && column) {
162
+ const contextValues = context;
163
+ for (const key of Object.keys(contextValues)) {
164
+ const value = contextValues[key];
165
+ if (value && typeof value === 'object' && 'table' in value && typeof value.table === 'function') {
166
+ contextValues[key] = createTenantScope(value, {
167
+ tenantId,
168
+ column,
169
+ });
170
+ }
171
+ }
172
+ }
173
+ else if (mode === 'manual') {
174
+ warnTenantMisconfiguration({
175
+ queryKey: endpoint.key,
176
+ hasTenantConfig: true,
177
+ hasTenantId: true,
178
+ mode: 'manual',
179
+ });
180
+ }
181
+ }
182
+ else if (!tenantRequired) {
183
+ warnTenantMisconfiguration({
184
+ queryKey: endpoint.key,
185
+ hasTenantConfig: true,
186
+ hasTenantId: false,
187
+ mode: activeTenantConfig.mode,
188
+ });
189
+ }
190
+ }
191
+ const validationResult = validateInput(endpoint.inputSchema, context.input);
192
+ if (!validationResult.success) {
193
+ await safeInvokeHook('onError', hooks.onError, {
194
+ requestId,
195
+ queryKey: endpoint.key,
196
+ metadata: metadataWithAuth,
197
+ request,
198
+ auth: context.auth,
199
+ durationMs: Date.now() - startedAt,
200
+ error: validationResult.error,
201
+ });
202
+ return createErrorResponse(400, 'VALIDATION_ERROR', 'Request validation failed', {
203
+ issues: validationResult.error.issues,
204
+ });
205
+ }
206
+ context.input = validationResult.data;
207
+ const pipeline = [
208
+ ...(globalMiddlewares ?? []),
209
+ ...endpoint.middlewares,
210
+ ];
211
+ const result = await runMiddlewares(pipeline, context, () => endpoint.handler(context));
212
+ const headers = { ...(endpoint.defaultHeaders ?? {}) };
213
+ if (typeof cacheTtlMs === 'number') {
214
+ headers['cache-control'] = cacheTtlMs > 0 ? `public, max-age=${Math.floor(cacheTtlMs / 1000)}` : 'no-store';
215
+ }
216
+ const durationMs = Date.now() - startedAt;
217
+ await safeInvokeHook('onRequestEnd', hooks.onRequestEnd, {
218
+ requestId,
219
+ queryKey: endpoint.key,
220
+ metadata: metadataWithAuth,
221
+ request,
222
+ auth: context.auth,
223
+ durationMs,
224
+ result,
225
+ });
226
+ return {
227
+ status: 200,
228
+ headers,
229
+ body: result,
230
+ };
231
+ }
232
+ catch (error) {
233
+ await safeInvokeHook('onError', hooks.onError, {
234
+ requestId,
235
+ queryKey: endpoint.key,
236
+ metadata: context.metadata,
237
+ request,
238
+ auth: context.auth,
239
+ durationMs: Date.now() - startedAt,
240
+ error,
241
+ });
242
+ const message = error instanceof Error ? error.message : 'Unexpected error';
243
+ return createErrorResponse(500, 'INTERNAL_SERVER_ERROR', message);
244
+ }
245
+ };
246
+ export const createServeHandler = ({ router, globalMiddlewares, authStrategies, tenantConfig, contextFactory, hooks, }) => {
247
+ return async (request) => {
248
+ const endpoint = router.match(request.method, request.path);
249
+ if (!endpoint) {
250
+ return createErrorResponse(404, 'NOT_FOUND', `No endpoint registered for ${request.method} ${request.path}`);
251
+ }
252
+ return executeEndpoint({
253
+ endpoint,
254
+ request,
255
+ authStrategies,
256
+ contextFactory,
257
+ globalMiddlewares,
258
+ tenantConfig,
259
+ hooks,
260
+ });
261
+ };
262
+ };
263
+ export const createOpenApiEndpoint = (path, getEndpoints, options) => {
264
+ let cachedDocument = null;
265
+ return {
266
+ key: '__hypequery_openapi__',
267
+ method: 'GET',
268
+ inputSchema: undefined,
269
+ outputSchema: z.any(),
270
+ handler: async () => {
271
+ if (!cachedDocument) {
272
+ cachedDocument = buildOpenApiDocument(getEndpoints(), options);
273
+ }
274
+ return cachedDocument;
275
+ },
276
+ query: undefined,
277
+ middlewares: [],
278
+ auth: null,
279
+ metadata: {
280
+ path,
281
+ method: 'GET',
282
+ name: 'OpenAPI schema',
283
+ summary: 'OpenAPI schema',
284
+ description: 'Generated OpenAPI specification for the registered endpoints',
285
+ tags: ['docs'],
286
+ requiresAuth: false,
287
+ deprecated: false,
288
+ visibility: 'internal',
289
+ },
290
+ cacheTtlMs: null,
291
+ };
292
+ };
293
+ export const createDocsEndpoint = (path, openapiPath, options) => ({
294
+ key: '__hypequery_docs__',
295
+ method: 'GET',
296
+ inputSchema: undefined,
297
+ outputSchema: z.string(),
298
+ handler: async () => buildDocsHtml(openapiPath, options),
299
+ query: undefined,
300
+ middlewares: [],
301
+ auth: null,
302
+ metadata: {
303
+ path,
304
+ method: 'GET',
305
+ name: 'Docs',
306
+ summary: 'API documentation',
307
+ description: 'Auto-generated documentation for your hypequery endpoints',
308
+ tags: ['docs'],
309
+ requiresAuth: false,
310
+ deprecated: false,
311
+ visibility: 'internal',
312
+ },
313
+ cacheTtlMs: null,
314
+ defaultHeaders: {
315
+ 'content-type': 'text/html; charset=utf-8',
316
+ },
317
+ });