@hypequery/serve 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +1039 -0
- package/dist/adapters/fetch.d.ts +3 -0
- package/dist/adapters/fetch.d.ts.map +1 -0
- package/dist/adapters/fetch.js +26 -0
- package/dist/adapters/node.d.ts +8 -0
- package/dist/adapters/node.d.ts.map +1 -0
- package/dist/adapters/node.js +105 -0
- package/dist/adapters/utils.d.ts +39 -0
- package/dist/adapters/utils.d.ts.map +1 -0
- package/dist/adapters/utils.js +114 -0
- package/dist/adapters/vercel.d.ts +7 -0
- package/dist/adapters/vercel.d.ts.map +1 -0
- package/dist/adapters/vercel.js +13 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +37 -0
- package/dist/client-config.d.ts +44 -0
- package/dist/client-config.d.ts.map +1 -0
- package/dist/client-config.js +53 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +24 -0
- package/dist/docs-ui.d.ts +3 -0
- package/dist/docs-ui.d.ts.map +1 -0
- package/dist/docs-ui.js +34 -0
- package/dist/endpoint.d.ts +5 -0
- package/dist/endpoint.d.ts.map +1 -0
- package/dist/endpoint.js +58 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +189 -0
- package/dist/queries.d.ts +3 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +1 -0
- package/dist/query.d.ts +4 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +1 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +56 -0
- package/dist/sdk-generator.d.ts +7 -0
- package/dist/sdk-generator.d.ts.map +1 -0
- package/dist/sdk-generator.js +143 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +580 -0
- package/dist/tenant.d.ts +35 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/tenant.js +49 -0
- package/dist/type-tests/builder.test-d.d.ts +13 -0
- package/dist/type-tests/builder.test-d.d.ts.map +1 -0
- package/dist/type-tests/builder.test-d.js +20 -0
- package/dist/types.d.ts +363 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/adapters/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAc,YAAY,EAAgB,MAAM,aAAa,CAAC;AAQxF,eAAO,MAAM,kBAAkB,GAAI,SAAS,YAAY,KAAG,YA4B1D,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalizeHeaders, parseQueryParams, parseRequestBody, serializeResponseBody, } from "./utils.js";
|
|
2
|
+
export const createFetchHandler = (handler) => {
|
|
3
|
+
return async (request) => {
|
|
4
|
+
const url = new URL(request.url);
|
|
5
|
+
const headers = normalizeHeaders(request.headers);
|
|
6
|
+
const contentType = headers["content-type"];
|
|
7
|
+
const serveRequest = {
|
|
8
|
+
method: (request.method ?? "GET").toUpperCase(),
|
|
9
|
+
path: url.pathname,
|
|
10
|
+
query: parseQueryParams(url.searchParams),
|
|
11
|
+
headers,
|
|
12
|
+
body: await parseRequestBody(request, contentType),
|
|
13
|
+
raw: request,
|
|
14
|
+
};
|
|
15
|
+
const response = await handler(serveRequest);
|
|
16
|
+
const responseHeaders = {
|
|
17
|
+
"content-type": "application/json; charset=utf-8",
|
|
18
|
+
...response.headers,
|
|
19
|
+
};
|
|
20
|
+
const body = serializeResponseBody(response.body);
|
|
21
|
+
return new Response(body, {
|
|
22
|
+
status: response.status,
|
|
23
|
+
headers: responseHeaders,
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "http";
|
|
2
|
+
import type { ServeHandler, StartServerOptions } from "../types.js";
|
|
3
|
+
export declare const createNodeHandler: (handler: ServeHandler) => (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
4
|
+
export declare const startNodeServer: (handler: ServeHandler, options?: StartServerOptions) => Promise<{
|
|
5
|
+
server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
6
|
+
stop: () => Promise<void>;
|
|
7
|
+
}>;
|
|
8
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/adapters/node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,MAAM,CAAC;AAG/E,OAAO,KAAK,EAEV,YAAY,EAGZ,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAyErB,eAAO,MAAM,iBAAiB,GAAI,SAAS,YAAY,MACvC,KAAK,eAAe,EAAE,KAAK,cAAc,kBASxD,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,SAAS,YAAY,EACrB,UAAS,kBAAuB;;;EA8CjC,CAAC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { normalizeHeaders, parseQueryParams, parseRequestBody, serializeResponseBody, } from "./utils.js";
|
|
4
|
+
const readRequestBody = async (req) => {
|
|
5
|
+
const chunks = [];
|
|
6
|
+
for await (const chunk of req) {
|
|
7
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
8
|
+
}
|
|
9
|
+
return Buffer.concat(chunks);
|
|
10
|
+
};
|
|
11
|
+
const buildServeRequest = async (req) => {
|
|
12
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
13
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
14
|
+
const bodyBuffer = await readRequestBody(req);
|
|
15
|
+
const headers = normalizeHeaders(req.headers);
|
|
16
|
+
const contentType = headers["content-type"] ?? headers["Content-Type"];
|
|
17
|
+
const body = await parseRequestBody(bodyBuffer, contentType);
|
|
18
|
+
return {
|
|
19
|
+
method,
|
|
20
|
+
path: url.pathname,
|
|
21
|
+
query: parseQueryParams(url.searchParams),
|
|
22
|
+
headers,
|
|
23
|
+
body,
|
|
24
|
+
raw: req,
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const sendResponse = (res, response) => {
|
|
28
|
+
res.statusCode = response.status;
|
|
29
|
+
const headers = response.headers ?? {};
|
|
30
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
31
|
+
if (value !== undefined) {
|
|
32
|
+
res.setHeader(key, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!res.hasHeader("content-type")) {
|
|
36
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
37
|
+
}
|
|
38
|
+
const serialized = serializeResponseBody(response.body);
|
|
39
|
+
res.end(serialized);
|
|
40
|
+
};
|
|
41
|
+
const sendError = (res, error) => {
|
|
42
|
+
const payload = error && typeof error === "object" && "status" in error
|
|
43
|
+
? error
|
|
44
|
+
: {
|
|
45
|
+
status: 500,
|
|
46
|
+
body: {
|
|
47
|
+
error: {
|
|
48
|
+
type: "INTERNAL_SERVER_ERROR",
|
|
49
|
+
message: error instanceof Error ? error.message : "Unexpected error",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
sendResponse(res, payload);
|
|
54
|
+
};
|
|
55
|
+
export const createNodeHandler = (handler) => {
|
|
56
|
+
return async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const request = await buildServeRequest(req);
|
|
59
|
+
const response = await handler(request);
|
|
60
|
+
sendResponse(res, response);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
sendError(res, error);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export const startNodeServer = async (handler, options = {}) => {
|
|
68
|
+
const listener = createNodeHandler(handler);
|
|
69
|
+
const server = createServer(listener);
|
|
70
|
+
const port = options.port ?? 3000;
|
|
71
|
+
const hostname = options.hostname ?? "0.0.0.0";
|
|
72
|
+
const onAbort = () => {
|
|
73
|
+
server.close();
|
|
74
|
+
};
|
|
75
|
+
if (options.signal) {
|
|
76
|
+
if (options.signal.aborted) {
|
|
77
|
+
server.close();
|
|
78
|
+
throw new Error("Start signal already aborted");
|
|
79
|
+
}
|
|
80
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
81
|
+
}
|
|
82
|
+
server.listen(port, hostname);
|
|
83
|
+
await once(server, "listening");
|
|
84
|
+
if (!options.quiet) {
|
|
85
|
+
const address = server.address();
|
|
86
|
+
const display = typeof address === "object" && address
|
|
87
|
+
? `${address.address}:${address.port}`
|
|
88
|
+
: `${hostname}:${port}`;
|
|
89
|
+
console.log(`hypequery serve listening on ${display}`);
|
|
90
|
+
}
|
|
91
|
+
const stop = () => new Promise((resolve, reject) => {
|
|
92
|
+
server.close((err) => {
|
|
93
|
+
if (err) {
|
|
94
|
+
reject(err);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
resolve();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
server,
|
|
103
|
+
stop,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for HTTP adapters (Node.js, Fetch API, etc.)
|
|
3
|
+
* These functions eliminate code duplication across different adapter implementations.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes headers from various sources into a consistent format.
|
|
7
|
+
* Handles both Node.js IncomingMessage headers and Web API Headers.
|
|
8
|
+
*
|
|
9
|
+
* @param headers - Headers from Node.js (Record with string | string[]) or Web Headers
|
|
10
|
+
* @returns Normalized headers as Record<string, string | undefined>
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeHeaders(headers: Record<string, string | string[] | undefined> | Headers): Record<string, string | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Parses URL search parameters into a structured query object.
|
|
15
|
+
* Handles multiple values for the same parameter by creating arrays.
|
|
16
|
+
*
|
|
17
|
+
* @param searchParams - URLSearchParams instance
|
|
18
|
+
* @returns Query parameters with support for arrays
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseQueryParams(searchParams: URLSearchParams): Record<string, string | string[] | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* Parses request body based on content type.
|
|
23
|
+
* Supports Node.js Buffer and Web API Request.
|
|
24
|
+
*
|
|
25
|
+
* @param input - Buffer (Node.js) or Request (Web API)
|
|
26
|
+
* @param contentType - Content-Type header value
|
|
27
|
+
* @returns Parsed body (JSON object, string, ArrayBuffer, or undefined)
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseRequestBody(input: Buffer | Request, contentType?: string): Promise<unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Serializes response body to string based on content type.
|
|
32
|
+
* Handles JSON serialization and string passthrough.
|
|
33
|
+
*
|
|
34
|
+
* @param body - Response body (any type)
|
|
35
|
+
* @param contentType - Content-Type header value
|
|
36
|
+
* @returns Serialized body as string
|
|
37
|
+
*/
|
|
38
|
+
export declare function serializeResponseBody(body: unknown): string;
|
|
39
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/adapters/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,OAAO,GAC/D,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAoBpC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,eAAe,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAc/C;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,GAAG,OAAO,EACvB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,OAAO,CAAC,CAuClB;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,OAAO,GACZ,MAAM,CAQR"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for HTTP adapters (Node.js, Fetch API, etc.)
|
|
3
|
+
* These functions eliminate code duplication across different adapter implementations.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes headers from various sources into a consistent format.
|
|
7
|
+
* Handles both Node.js IncomingMessage headers and Web API Headers.
|
|
8
|
+
*
|
|
9
|
+
* @param headers - Headers from Node.js (Record with string | string[]) or Web Headers
|
|
10
|
+
* @returns Normalized headers as Record<string, string | undefined>
|
|
11
|
+
*/
|
|
12
|
+
export function normalizeHeaders(headers) {
|
|
13
|
+
const normalized = {};
|
|
14
|
+
if (headers instanceof Headers || (typeof Headers !== 'undefined' && headers instanceof Headers)) {
|
|
15
|
+
// Web API Headers
|
|
16
|
+
headers.forEach((value, key) => {
|
|
17
|
+
normalized[key] = value;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// Node.js headers (Record<string, string | string[] | undefined>)
|
|
22
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
normalized[key] = value.join(", ");
|
|
25
|
+
}
|
|
26
|
+
else if (typeof value === "string") {
|
|
27
|
+
normalized[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parses URL search parameters into a structured query object.
|
|
35
|
+
* Handles multiple values for the same parameter by creating arrays.
|
|
36
|
+
*
|
|
37
|
+
* @param searchParams - URLSearchParams instance
|
|
38
|
+
* @returns Query parameters with support for arrays
|
|
39
|
+
*/
|
|
40
|
+
export function parseQueryParams(searchParams) {
|
|
41
|
+
const params = {};
|
|
42
|
+
for (const [key, value] of searchParams.entries()) {
|
|
43
|
+
if (params[key] === undefined) {
|
|
44
|
+
params[key] = value;
|
|
45
|
+
}
|
|
46
|
+
else if (Array.isArray(params[key])) {
|
|
47
|
+
params[key].push(value);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
params[key] = [params[key], value];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return params;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parses request body based on content type.
|
|
57
|
+
* Supports Node.js Buffer and Web API Request.
|
|
58
|
+
*
|
|
59
|
+
* @param input - Buffer (Node.js) or Request (Web API)
|
|
60
|
+
* @param contentType - Content-Type header value
|
|
61
|
+
* @returns Parsed body (JSON object, string, ArrayBuffer, or undefined)
|
|
62
|
+
*/
|
|
63
|
+
export async function parseRequestBody(input, contentType) {
|
|
64
|
+
// Node.js Buffer handling
|
|
65
|
+
if (Buffer.isBuffer(input)) {
|
|
66
|
+
if (!input.length) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
if (contentType && contentType.includes("application/json")) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(input.toString("utf8"));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// If JSON parsing fails, return as string
|
|
75
|
+
return input.toString("utf8");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Non-JSON content
|
|
79
|
+
return input.length ? input.toString("utf8") : undefined;
|
|
80
|
+
}
|
|
81
|
+
// Web API Request handling
|
|
82
|
+
if (!contentType) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (contentType.includes("application/json")) {
|
|
86
|
+
try {
|
|
87
|
+
return await input.json();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (contentType.includes("text/")) {
|
|
94
|
+
return await input.text();
|
|
95
|
+
}
|
|
96
|
+
// Binary data (images, files, etc.)
|
|
97
|
+
return await input.arrayBuffer();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Serializes response body to string based on content type.
|
|
101
|
+
* Handles JSON serialization and string passthrough.
|
|
102
|
+
*
|
|
103
|
+
* @param body - Response body (any type)
|
|
104
|
+
* @param contentType - Content-Type header value
|
|
105
|
+
* @returns Serialized body as string
|
|
106
|
+
*/
|
|
107
|
+
export function serializeResponseBody(body) {
|
|
108
|
+
// If already a string, pass through as-is
|
|
109
|
+
if (typeof body === "string") {
|
|
110
|
+
return body;
|
|
111
|
+
}
|
|
112
|
+
// Otherwise, JSON stringify for JSON content-type or default
|
|
113
|
+
return JSON.stringify(body ?? null);
|
|
114
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ServeHandler } from "../types.js";
|
|
2
|
+
type VercelEdgeHandler = (request: Request) => Promise<Response>;
|
|
3
|
+
type VercelNodeHandler = (req: unknown, res: unknown) => Promise<void> | void;
|
|
4
|
+
export declare const createVercelEdgeHandler: (handler: ServeHandler) => VercelEdgeHandler;
|
|
5
|
+
export declare const createVercelNodeHandler: (handler: ServeHandler) => VercelNodeHandler;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=vercel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../src/adapters/vercel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,KAAK,iBAAiB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AACjE,KAAK,iBAAiB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE9E,eAAO,MAAM,uBAAuB,GAAI,SAAS,YAAY,KAAG,iBAG/D,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,SAAS,YAAY,KAAG,iBAM/D,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createFetchHandler } from "./fetch.js";
|
|
2
|
+
import { createNodeHandler } from "./node.js";
|
|
3
|
+
export const createVercelEdgeHandler = (handler) => {
|
|
4
|
+
const fetchHandler = createFetchHandler(handler);
|
|
5
|
+
return async (request) => fetchHandler(request);
|
|
6
|
+
};
|
|
7
|
+
export const createVercelNodeHandler = (handler) => {
|
|
8
|
+
const nodeHandler = createNodeHandler(handler);
|
|
9
|
+
return async (req, res) => {
|
|
10
|
+
// Vercel's Node runtime passes standard Node req/res objects.
|
|
11
|
+
await nodeHandler(req, res);
|
|
12
|
+
};
|
|
13
|
+
};
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthContext, AuthStrategy, ServeRequest } from "./types.js";
|
|
2
|
+
export interface ApiKeyStrategyOptions<TAuth extends AuthContext = AuthContext> {
|
|
3
|
+
header?: string;
|
|
4
|
+
queryParam?: string;
|
|
5
|
+
validate: (key: string, request: ServeRequest) => Promise<TAuth | null> | TAuth | null;
|
|
6
|
+
}
|
|
7
|
+
export declare const createApiKeyStrategy: <TAuth extends AuthContext = AuthContext>(options: ApiKeyStrategyOptions<TAuth>) => AuthStrategy<TAuth>;
|
|
8
|
+
export interface BearerTokenStrategyOptions<TAuth extends AuthContext = AuthContext> {
|
|
9
|
+
header?: string;
|
|
10
|
+
prefix?: string;
|
|
11
|
+
validate: (token: string, request: ServeRequest) => Promise<TAuth | null> | TAuth | null;
|
|
12
|
+
}
|
|
13
|
+
export declare const createBearerTokenStrategy: <TAuth extends AuthContext = AuthContext>(options: BearerTokenStrategyOptions<TAuth>) => AuthStrategy<TAuth>;
|
|
14
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC;CACxF;AAED,eAAO,MAAM,oBAAoB,GAAI,KAAK,SAAS,WAAW,GAAG,WAAW,EAC1E,SAAS,qBAAqB,CAAC,KAAK,CAAC,KACpC,YAAY,CAAC,KAAK,CA0BpB,CAAC;AAEF,MAAM,WAAW,0BAA0B,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC;CAC1F;AAED,eAAO,MAAM,yBAAyB,GAAI,KAAK,SAAS,WAAW,GAAG,WAAW,EAC/E,SAAS,0BAA0B,CAAC,KAAK,CAAC,KACzC,YAAY,CAAC,KAAK,CAepB,CAAC"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const createApiKeyStrategy = (options) => {
|
|
2
|
+
const headerName = options.header ?? "authorization";
|
|
3
|
+
const queryParam = options.queryParam;
|
|
4
|
+
return async ({ request }) => {
|
|
5
|
+
let key;
|
|
6
|
+
if (queryParam && typeof request.query[queryParam] === "string") {
|
|
7
|
+
key = request.query[queryParam];
|
|
8
|
+
}
|
|
9
|
+
if (!key) {
|
|
10
|
+
const headerValue = request.headers[headerName] ?? request.headers[headerName.toLowerCase()];
|
|
11
|
+
if (typeof headerValue === "string") {
|
|
12
|
+
key = headerValue.startsWith("Bearer ")
|
|
13
|
+
? headerValue.slice("Bearer ".length)
|
|
14
|
+
: headerValue;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!key) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return options.validate(key, request);
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export const createBearerTokenStrategy = (options) => {
|
|
24
|
+
const headerName = options.header ?? "authorization";
|
|
25
|
+
const prefix = options.prefix ?? "Bearer ";
|
|
26
|
+
return async ({ request }) => {
|
|
27
|
+
const raw = request.headers[headerName] ?? request.headers[headerName.toLowerCase()];
|
|
28
|
+
if (typeof raw !== "string" || !raw.startsWith(prefix)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const token = raw.slice(prefix.length).trim();
|
|
32
|
+
if (!token) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return options.validate(token, request);
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ServeBuilder, HttpMethod, AuthContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for a single query's client-side behavior
|
|
4
|
+
*/
|
|
5
|
+
export interface QueryClientConfig {
|
|
6
|
+
method: HttpMethod;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Map of query names to their client configurations
|
|
10
|
+
*/
|
|
11
|
+
export type ApiClientConfig = Record<string, QueryClientConfig>;
|
|
12
|
+
/**
|
|
13
|
+
* Extract serializable client configuration from a ServeBuilder.
|
|
14
|
+
* This generates a runtime object that can be used client-side to configure
|
|
15
|
+
* HTTP methods and paths for each query.
|
|
16
|
+
*
|
|
17
|
+
* Prioritizes route-level method configuration over endpoint defaults.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Server-side (e.g., in api/config/route.ts)
|
|
21
|
+
* import { api } from '@/analytics/queries';
|
|
22
|
+
* import { extractClientConfig } from '@hypequery/serve';
|
|
23
|
+
*
|
|
24
|
+
* export async function GET() {
|
|
25
|
+
* return Response.json(extractClientConfig(api));
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* // Client-side
|
|
29
|
+
* const config = await fetch('/api/config').then(r => r.json());
|
|
30
|
+
* createHooks<Api>({ baseUrl: '/api/hypequery', config });
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractClientConfig<TQueries extends Record<string, any>, TContext extends Record<string, unknown>, TAuth extends AuthContext>(api: ServeBuilder<TQueries, TContext, TAuth>): ApiClientConfig;
|
|
33
|
+
/**
|
|
34
|
+
* Type-safe helper to manually define client configuration.
|
|
35
|
+
* Use this when you can't access the api object client-side.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const config = defineClientConfig({
|
|
39
|
+
* hello: { method: 'GET' },
|
|
40
|
+
* createUser: { method: 'POST' },
|
|
41
|
+
* });
|
|
42
|
+
*/
|
|
43
|
+
export declare function defineClientConfig<T extends ApiClientConfig>(config: T): T;
|
|
44
|
+
//# sourceMappingURL=client-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-config.d.ts","sourceRoot":"","sources":["../src/client-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,UAAU,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACpC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,EACzB,GAAG,EAAE,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,GAAG,eAAe,CAoB/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,eAAe,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAE1E"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract serializable client configuration from a ServeBuilder.
|
|
3
|
+
* This generates a runtime object that can be used client-side to configure
|
|
4
|
+
* HTTP methods and paths for each query.
|
|
5
|
+
*
|
|
6
|
+
* Prioritizes route-level method configuration over endpoint defaults.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Server-side (e.g., in api/config/route.ts)
|
|
10
|
+
* import { api } from '@/analytics/queries';
|
|
11
|
+
* import { extractClientConfig } from '@hypequery/serve';
|
|
12
|
+
*
|
|
13
|
+
* export async function GET() {
|
|
14
|
+
* return Response.json(extractClientConfig(api));
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* // Client-side
|
|
18
|
+
* const config = await fetch('/api/config').then(r => r.json());
|
|
19
|
+
* createHooks<Api>({ baseUrl: '/api/hypequery', config });
|
|
20
|
+
*/
|
|
21
|
+
export function extractClientConfig(api) {
|
|
22
|
+
const config = {};
|
|
23
|
+
// Prefer route-level config if available
|
|
24
|
+
if (api._routeConfig) {
|
|
25
|
+
for (const [key, routeConfig] of Object.entries(api._routeConfig)) {
|
|
26
|
+
config[key] = {
|
|
27
|
+
method: routeConfig.method,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Fallback to endpoint method
|
|
33
|
+
for (const [key, endpoint] of Object.entries(api.queries)) {
|
|
34
|
+
config[key] = {
|
|
35
|
+
method: endpoint.method,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Type-safe helper to manually define client configuration.
|
|
43
|
+
* Use this when you can't access the api object client-side.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const config = defineClientConfig({
|
|
47
|
+
* hello: { method: 'GET' },
|
|
48
|
+
* createUser: { method: 'POST' },
|
|
49
|
+
* });
|
|
50
|
+
*/
|
|
51
|
+
export function defineClientConfig(config) {
|
|
52
|
+
return config;
|
|
53
|
+
}
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ServeBuilder, StartServerOptions } from "./types.js";
|
|
2
|
+
export interface ServeDevOptions extends StartServerOptions {
|
|
3
|
+
logger?: (message: string) => void;
|
|
4
|
+
}
|
|
5
|
+
export declare const serveDev: <TQueries extends Record<string, any>, TAuth extends Record<string, unknown>>(api: ServeBuilder<TQueries, TAuth>, options?: ServeDevOptions) => Promise<{
|
|
6
|
+
server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|
|
7
|
+
stop: () => Promise<void>;
|
|
8
|
+
}>;
|
|
9
|
+
//# sourceMappingURL=dev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../src/dev.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEnE,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAMD,eAAO,MAAM,QAAQ,GACnB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACpC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAErC,KAAK,YAAY,CAAC,QAAQ,EAAE,KAAK,CAAC,EAClC,UAAS,eAAoB;;;EAwB9B,CAAC"}
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { startNodeServer } from "./adapters/node.js";
|
|
2
|
+
const defaultLogger = (message) => {
|
|
3
|
+
console.log(message);
|
|
4
|
+
};
|
|
5
|
+
export const serveDev = async (api, options = {}) => {
|
|
6
|
+
const port = options.port ?? Number(process.env.PORT ?? 4000);
|
|
7
|
+
const hostname = options.hostname ?? "localhost";
|
|
8
|
+
const logger = options.logger ?? defaultLogger;
|
|
9
|
+
const server = await startNodeServer(api.handler, {
|
|
10
|
+
...options,
|
|
11
|
+
hostname,
|
|
12
|
+
port,
|
|
13
|
+
quiet: true,
|
|
14
|
+
});
|
|
15
|
+
if (!options.quiet) {
|
|
16
|
+
const address = server.server.address();
|
|
17
|
+
const display = typeof address === "object" && address
|
|
18
|
+
? `${address.address}:${address.port}`
|
|
19
|
+
: `${hostname}:${port}`;
|
|
20
|
+
logger(`hypequery dev server running at http://${display}`);
|
|
21
|
+
logger(`Docs available at http://${display}/docs`);
|
|
22
|
+
}
|
|
23
|
+
return server;
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs-ui.d.ts","sourceRoot":"","sources":["../src/docs-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAO9C,eAAO,MAAM,aAAa,GAAI,YAAY,MAAM,EAAE,UAAU,WAAW,WAgCtE,CAAC"}
|
package/dist/docs-ui.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const REDOC_CDN = "https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js";
|
|
2
|
+
const sanitize = (value, fallback = "") => (value ?? fallback).replace(/</g, "<").replace(/>/g, ">");
|
|
3
|
+
export const buildDocsHtml = (openapiUrl, options) => {
|
|
4
|
+
const title = sanitize(options?.title, "hypequery");
|
|
5
|
+
const subtitle = sanitize(options?.subtitle);
|
|
6
|
+
const darkClass = options?.darkMode ? "hq-docs--dark" : "";
|
|
7
|
+
return `<!DOCTYPE html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8" />
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
12
|
+
<title>${title}</title>
|
|
13
|
+
<style>
|
|
14
|
+
body, html { margin: 0; padding: 0; height: 100%; font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
|
|
15
|
+
.hq-docs { display: flex; flex-direction: column; height: 100%; }
|
|
16
|
+
.hq-docs__header { padding: 1.25rem 1.5rem; border-bottom: 1px solid rgba(0,0,0,0.08); }
|
|
17
|
+
.hq-docs--dark .hq-docs__header { border-color: rgba(255,255,255,0.12); }
|
|
18
|
+
.hq-docs__title { margin: 0; font-size: 1.25rem; }
|
|
19
|
+
.hq-docs__subtitle { margin: 0.25rem 0 0; color: #555; }
|
|
20
|
+
.hq-docs--dark { background: #0f1115; color: #f8f8f2; }
|
|
21
|
+
.hq-docs--dark .hq-docs__subtitle { color: #b4b6c2; }
|
|
22
|
+
redoc { flex: 1; }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body class="hq-docs ${darkClass}">
|
|
26
|
+
<header class="hq-docs__header">
|
|
27
|
+
<h1 class="hq-docs__title">${title}</h1>
|
|
28
|
+
${subtitle ? `<p class="hq-docs__subtitle">${subtitle}</p>` : ""}
|
|
29
|
+
</header>
|
|
30
|
+
<redoc spec-url="${openapiUrl}"></redoc>
|
|
31
|
+
<script src="${REDOC_CDN}"></script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>`;
|
|
34
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AuthContext, ServeEndpoint, ServeQueryConfig } from "./types.js";
|
|
2
|
+
type EndpointFromDefinition<TDefinition extends ServeQueryConfig<any, any, TContext, TAuth, any>, TContext extends Record<string, unknown>, TAuth extends AuthContext> = TDefinition extends ServeQueryConfig<infer TInputSchema, infer TOutputSchema, TContext, TAuth, infer TResult> ? ServeEndpoint<TInputSchema, TOutputSchema, TContext, TAuth, TResult> : ServeEndpoint<any, any, TContext, TAuth>;
|
|
3
|
+
export declare const createEndpoint: <TContext extends Record<string, unknown>, TAuth extends AuthContext, TDefinition extends ServeQueryConfig<any, any, TContext, TAuth, any>>(key: string, definition: TDefinition) => EndpointFromDefinition<TDefinition, TContext, TAuth>;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=endpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoint.d.ts","sourceRoot":"","sources":["../src/endpoint.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EAUX,aAAa,EACb,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAIpB,KAAK,sBAAsB,CACzB,WAAW,SAAS,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,EACpE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,IACvB,WAAW,SAAS,gBAAgB,CACtC,MAAM,YAAY,EAClB,MAAM,aAAa,EACnB,QAAQ,EACR,KAAK,EACL,MAAM,OAAO,CACd,GACG,aAAa,CAAC,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,CAAC,GACpE,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;AA8B7C,eAAO,MAAM,cAAc,GACzB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,EACzB,WAAW,SAAS,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,EAEpE,KAAK,MAAM,EACX,YAAY,WAAW,KACtB,sBAAsB,CAAC,WAAW,EAAE,QAAQ,EAAE,KAAK,CAqDrD,CAAC"}
|
package/dist/endpoint.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
summary: definition.summary,
|
|
25
|
+
description: definition.description,
|
|
26
|
+
tags: definition.tags ?? [],
|
|
27
|
+
requiresAuth: definition.auth ? true : undefined,
|
|
28
|
+
deprecated: undefined,
|
|
29
|
+
visibility: "public",
|
|
30
|
+
custom: definition.custom,
|
|
31
|
+
};
|
|
32
|
+
const runner = resolveQueryRunner(definition.query);
|
|
33
|
+
const handler = async (ctx) => {
|
|
34
|
+
if (!runner) {
|
|
35
|
+
throw new Error(`Endpoint "${key}" is missing an executable query`);
|
|
36
|
+
}
|
|
37
|
+
return runner({
|
|
38
|
+
input: ctx.input,
|
|
39
|
+
ctx: ctx,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
const outputSchema = (definition.outputSchema ?? fallbackSchema);
|
|
43
|
+
const inputSchema = definition.inputSchema;
|
|
44
|
+
return {
|
|
45
|
+
key,
|
|
46
|
+
method,
|
|
47
|
+
inputSchema,
|
|
48
|
+
outputSchema,
|
|
49
|
+
handler,
|
|
50
|
+
query: definition.query,
|
|
51
|
+
middlewares: definition.middlewares ?? [],
|
|
52
|
+
auth: definition.auth ?? null,
|
|
53
|
+
tenant: definition.tenant,
|
|
54
|
+
metadata,
|
|
55
|
+
cacheTtlMs: definition.cacheTtlMs ?? null,
|
|
56
|
+
defaultHeaders: undefined,
|
|
57
|
+
};
|
|
58
|
+
};
|