@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.
- 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/builder.d.ts +3 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +41 -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 +59 -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/pipeline.d.ts +72 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +317 -0
- package/dist/query-logger.d.ts +65 -0
- package/dist/query-logger.d.ts.map +1 -0
- package/dist/query-logger.js +91 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +56 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +191 -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 +373 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +16 -0
- package/package.json +9 -9
- package/LICENSE +0 -201
|
@@ -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
|
+
}
|
package/dist/router.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AuthContext, ServeBuilder, ServeConfig, ServeContextFactory, ServeEndpointMap, ServeInitializer, ServeQueriesMap } from "./types.js";
|
|
2
|
+
export declare const defineServe: <TContext extends Record<string, unknown> = Record<string, unknown>, TAuth extends AuthContext = AuthContext, const TQueries extends ServeQueriesMap<TContext, TAuth> = ServeQueriesMap<TContext, TAuth>>(config: ServeConfig<TContext, TAuth, TQueries>) => ServeBuilder<ServeEndpointMap<TQueries, TContext, TAuth>, TContext, TAuth>;
|
|
3
|
+
type InferInitializerContext<TFactory, TAuth extends AuthContext> = TFactory extends ServeContextFactory<infer TContext, TAuth> ? TContext : never;
|
|
4
|
+
type ServeInitializerOptions<TFactory extends ServeContextFactory<any, TAuth>, TAuth extends AuthContext> = Omit<ServeConfig<InferInitializerContext<TFactory, TAuth>, TAuth, ServeQueriesMap<InferInitializerContext<TFactory, TAuth>, TAuth>>, "queries" | "context"> & {
|
|
5
|
+
context: TFactory;
|
|
6
|
+
};
|
|
7
|
+
export declare const initServe: <TFactory extends ServeContextFactory<any, TAuth>, TAuth extends AuthContext = AuthContext>(options: ServeInitializerOptions<TFactory, TAuth>) => ServeInitializer<InferInitializerContext<TFactory, TAuth>, TAuth>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,WAAW,EAIX,YAAY,EACZ,WAAW,EACX,mBAAmB,EAEnB,gBAAgB,EAEhB,gBAAgB,EAIhB,eAAe,EAMhB,MAAM,YAAY,CAAC;AAWpB,eAAO,MAAM,WAAW,GACtB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClE,KAAK,SAAS,WAAW,GAAG,WAAW,EACvC,KAAK,CAAC,QAAQ,SAAS,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,EAE1F,QAAQ,WAAW,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,KAC7C,YAAY,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,CAgL3E,CAAC;AA8BF,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,CAsBN,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2
|
+
import { startNodeServer } from "./adapters/node.js";
|
|
3
|
+
import { createEndpoint } from "./endpoint.js";
|
|
4
|
+
import { applyBasePath, normalizeRoutePath, ServeRouter } from "./router.js";
|
|
5
|
+
import { createProcedureBuilder } from "./builder.js";
|
|
6
|
+
import { ensureArray, mergeTags } from "./utils.js";
|
|
7
|
+
import { createDocsEndpoint, createOpenApiEndpoint, createServeHandler, executeEndpoint, } from "./pipeline.js";
|
|
8
|
+
export const defineServe = (config) => {
|
|
9
|
+
const basePath = config.basePath ?? "/api/analytics";
|
|
10
|
+
const router = new ServeRouter(basePath);
|
|
11
|
+
const globalMiddlewares = [
|
|
12
|
+
...(config.middlewares ?? []),
|
|
13
|
+
];
|
|
14
|
+
const authStrategies = ensureArray(config.auth);
|
|
15
|
+
const globalTenantConfig = config.tenant;
|
|
16
|
+
const contextFactory = config.context;
|
|
17
|
+
const hooks = (config.hooks ?? {});
|
|
18
|
+
const openapiConfig = {
|
|
19
|
+
enabled: config.openapi?.enabled ?? true,
|
|
20
|
+
path: config.openapi?.path ?? "/openapi.json",
|
|
21
|
+
};
|
|
22
|
+
const docsConfig = {
|
|
23
|
+
enabled: config.docs?.enabled ?? true,
|
|
24
|
+
path: config.docs?.path ?? "/docs",
|
|
25
|
+
};
|
|
26
|
+
const openapiPublicPath = applyBasePath(basePath, openapiConfig.path);
|
|
27
|
+
const configuredQueries = config.queries ?? {};
|
|
28
|
+
const queryEntries = {};
|
|
29
|
+
const registerQuery = (key, definition) => {
|
|
30
|
+
queryEntries[key] = createEndpoint(String(key), definition);
|
|
31
|
+
};
|
|
32
|
+
for (const key of Object.keys(configuredQueries)) {
|
|
33
|
+
registerQuery(key, configuredQueries[key]);
|
|
34
|
+
}
|
|
35
|
+
const handler = createServeHandler({
|
|
36
|
+
router,
|
|
37
|
+
globalMiddlewares,
|
|
38
|
+
authStrategies,
|
|
39
|
+
tenantConfig: globalTenantConfig,
|
|
40
|
+
contextFactory,
|
|
41
|
+
hooks,
|
|
42
|
+
});
|
|
43
|
+
// Track route configuration for client config extraction
|
|
44
|
+
const routeConfig = {};
|
|
45
|
+
const executeQuery = async (key, options) => {
|
|
46
|
+
const endpoint = queryEntries[key];
|
|
47
|
+
if (!endpoint) {
|
|
48
|
+
throw new Error(`No query registered for key ${String(key)}`);
|
|
49
|
+
}
|
|
50
|
+
const request = {
|
|
51
|
+
method: endpoint.method,
|
|
52
|
+
path: options?.request?.path ?? endpoint.metadata.path ?? `/__execute/${String(key)}`,
|
|
53
|
+
query: options?.request?.query ?? {},
|
|
54
|
+
headers: options?.request?.headers ?? {},
|
|
55
|
+
body: options?.input ?? options?.request?.body,
|
|
56
|
+
raw: options?.request?.raw,
|
|
57
|
+
};
|
|
58
|
+
const response = await executeEndpoint({
|
|
59
|
+
endpoint,
|
|
60
|
+
request,
|
|
61
|
+
authStrategies,
|
|
62
|
+
contextFactory,
|
|
63
|
+
globalMiddlewares,
|
|
64
|
+
tenantConfig: globalTenantConfig,
|
|
65
|
+
hooks,
|
|
66
|
+
additionalContext: options?.context,
|
|
67
|
+
});
|
|
68
|
+
if (response.status !== 200) {
|
|
69
|
+
const errorBody = response.body;
|
|
70
|
+
const error = new Error(errorBody.error.message);
|
|
71
|
+
error.type = errorBody.error.type;
|
|
72
|
+
if (errorBody.error.details) {
|
|
73
|
+
error.details = errorBody.error.details;
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
return response.body;
|
|
78
|
+
};
|
|
79
|
+
const builder = {
|
|
80
|
+
queries: queryEntries,
|
|
81
|
+
_routeConfig: routeConfig,
|
|
82
|
+
route: (path, endpoint, options) => {
|
|
83
|
+
if (!endpoint) {
|
|
84
|
+
throw new Error("Endpoint definition is required when registering a route");
|
|
85
|
+
}
|
|
86
|
+
const method = options?.method ?? endpoint.method;
|
|
87
|
+
// Find the query key for this endpoint
|
|
88
|
+
const queryKey = Object.entries(queryEntries).find(([_, e]) => e === endpoint)?.[0];
|
|
89
|
+
if (queryKey) {
|
|
90
|
+
routeConfig[queryKey] = { method };
|
|
91
|
+
}
|
|
92
|
+
const normalizedPath = normalizeRoutePath(path);
|
|
93
|
+
const fallbackRequiresAuth = endpoint.auth
|
|
94
|
+
? true
|
|
95
|
+
: authStrategies.length > 0
|
|
96
|
+
? true
|
|
97
|
+
: undefined;
|
|
98
|
+
const requiresAuth = options?.requiresAuth ?? endpoint.metadata.requiresAuth ?? fallbackRequiresAuth;
|
|
99
|
+
const visibility = options?.visibility ?? endpoint.metadata.visibility ?? "public";
|
|
100
|
+
const metadata = {
|
|
101
|
+
...endpoint.metadata,
|
|
102
|
+
path: normalizedPath,
|
|
103
|
+
method,
|
|
104
|
+
name: options?.name ?? endpoint.metadata.name ?? endpoint.key,
|
|
105
|
+
summary: options?.summary ?? endpoint.metadata.summary,
|
|
106
|
+
description: options?.description ?? endpoint.metadata.description,
|
|
107
|
+
tags: mergeTags(endpoint.metadata.tags, options?.tags),
|
|
108
|
+
requiresAuth,
|
|
109
|
+
visibility,
|
|
110
|
+
};
|
|
111
|
+
const middlewares = [...endpoint.middlewares, ...(options?.middlewares ?? [])];
|
|
112
|
+
const registeredEndpoint = {
|
|
113
|
+
...endpoint,
|
|
114
|
+
method,
|
|
115
|
+
metadata,
|
|
116
|
+
middlewares,
|
|
117
|
+
};
|
|
118
|
+
router.register(registeredEndpoint);
|
|
119
|
+
return builder;
|
|
120
|
+
},
|
|
121
|
+
use: (middleware) => {
|
|
122
|
+
globalMiddlewares.push(middleware);
|
|
123
|
+
return builder;
|
|
124
|
+
},
|
|
125
|
+
useAuth: (strategy) => {
|
|
126
|
+
authStrategies.push(strategy);
|
|
127
|
+
router.markRoutesRequireAuth();
|
|
128
|
+
return builder;
|
|
129
|
+
},
|
|
130
|
+
execute: executeQuery,
|
|
131
|
+
run: executeQuery,
|
|
132
|
+
describe: () => {
|
|
133
|
+
const description = {
|
|
134
|
+
basePath: basePath || undefined,
|
|
135
|
+
queries: router.list().map(mapEndpointToToolkit),
|
|
136
|
+
};
|
|
137
|
+
return description;
|
|
138
|
+
},
|
|
139
|
+
handler,
|
|
140
|
+
start: async (options) => startNodeServer(handler, options),
|
|
141
|
+
};
|
|
142
|
+
if (openapiConfig.enabled) {
|
|
143
|
+
const openapiEndpoint = createOpenApiEndpoint(openapiConfig.path, () => router.list(), config.openapi);
|
|
144
|
+
router.register(openapiEndpoint);
|
|
145
|
+
}
|
|
146
|
+
if (docsConfig.enabled) {
|
|
147
|
+
const docsEndpoint = createDocsEndpoint(docsConfig.path, openapiPublicPath, config.docs);
|
|
148
|
+
router.register(docsEndpoint);
|
|
149
|
+
}
|
|
150
|
+
return builder;
|
|
151
|
+
};
|
|
152
|
+
const mapEndpointToToolkit = (endpoint) => {
|
|
153
|
+
// Use type assertion to avoid deep type instantiation issues with zodToJsonSchema
|
|
154
|
+
const inputSchema = endpoint.inputSchema
|
|
155
|
+
? zodToJsonSchema(endpoint.inputSchema, { target: "openApi3" })
|
|
156
|
+
: undefined;
|
|
157
|
+
const outputSchema = endpoint.outputSchema
|
|
158
|
+
? zodToJsonSchema(endpoint.outputSchema, { target: "openApi3" })
|
|
159
|
+
: undefined;
|
|
160
|
+
return {
|
|
161
|
+
key: endpoint.key,
|
|
162
|
+
path: endpoint.metadata.path,
|
|
163
|
+
method: endpoint.method,
|
|
164
|
+
name: endpoint.metadata.name ?? endpoint.key,
|
|
165
|
+
summary: endpoint.metadata.summary,
|
|
166
|
+
description: endpoint.metadata.description,
|
|
167
|
+
tags: endpoint.metadata.tags,
|
|
168
|
+
visibility: endpoint.metadata.visibility,
|
|
169
|
+
requiresAuth: Boolean(endpoint.metadata.requiresAuth),
|
|
170
|
+
requiresTenant: endpoint.tenant ? (endpoint.tenant.required !== false) : undefined,
|
|
171
|
+
inputSchema,
|
|
172
|
+
outputSchema,
|
|
173
|
+
custom: endpoint.metadata.custom,
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
export const initServe = (options) => {
|
|
177
|
+
const { context, ...staticOptions } = options;
|
|
178
|
+
const procedure = createProcedureBuilder();
|
|
179
|
+
return {
|
|
180
|
+
procedure,
|
|
181
|
+
query: procedure,
|
|
182
|
+
queries: (definitions) => definitions,
|
|
183
|
+
define: (config) => {
|
|
184
|
+
return defineServe({
|
|
185
|
+
...staticOptions,
|
|
186
|
+
...config,
|
|
187
|
+
context: (context ?? {}),
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
};
|
package/dist/tenant.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for multi-tenant query isolation
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Creates a tenant-scoped query builder wrapper that automatically
|
|
6
|
+
* adds WHERE clauses to filter by tenant.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const api = defineServe({
|
|
11
|
+
* context: ({ auth }) => ({
|
|
12
|
+
* db: createTenantScope(myDb, {
|
|
13
|
+
* tenantId: auth?.tenantId,
|
|
14
|
+
* column: 'organization_id',
|
|
15
|
+
* }),
|
|
16
|
+
* }),
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function createTenantScope<TDb extends {
|
|
21
|
+
table: (name: string) => any;
|
|
22
|
+
}>(db: TDb, options: {
|
|
23
|
+
tenantId: string | null | undefined;
|
|
24
|
+
column: string;
|
|
25
|
+
}): TDb;
|
|
26
|
+
/**
|
|
27
|
+
* Runtime warning when tenant isolation might be misconfigured
|
|
28
|
+
*/
|
|
29
|
+
export declare function warnTenantMisconfiguration(options: {
|
|
30
|
+
queryKey: string;
|
|
31
|
+
hasTenantConfig: boolean;
|
|
32
|
+
hasTenantId: boolean;
|
|
33
|
+
mode?: string;
|
|
34
|
+
}): void;
|
|
35
|
+
//# sourceMappingURL=tenant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,SAAS;IAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG,CAAA;CAAE,EAC5E,EAAE,EAAE,GAAG,EACP,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;CAChB,GACA,GAAG,CAoBL;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,QAYA"}
|
package/dist/tenant.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for multi-tenant query isolation
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Creates a tenant-scoped query builder wrapper that automatically
|
|
6
|
+
* adds WHERE clauses to filter by tenant.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const api = defineServe({
|
|
11
|
+
* context: ({ auth }) => ({
|
|
12
|
+
* db: createTenantScope(myDb, {
|
|
13
|
+
* tenantId: auth?.tenantId,
|
|
14
|
+
* column: 'organization_id',
|
|
15
|
+
* }),
|
|
16
|
+
* }),
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function createTenantScope(db, options) {
|
|
21
|
+
if (!options.tenantId) {
|
|
22
|
+
return db;
|
|
23
|
+
}
|
|
24
|
+
const originalTable = db.table.bind(db);
|
|
25
|
+
return {
|
|
26
|
+
...db,
|
|
27
|
+
table: (name) => {
|
|
28
|
+
const query = originalTable(name);
|
|
29
|
+
// Auto-inject tenant filter
|
|
30
|
+
if (query && typeof query.where === 'function') {
|
|
31
|
+
return query.where(options.column, 'eq', options.tenantId);
|
|
32
|
+
}
|
|
33
|
+
return query;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Runtime warning when tenant isolation might be misconfigured
|
|
39
|
+
*/
|
|
40
|
+
export function warnTenantMisconfiguration(options) {
|
|
41
|
+
if (!options.hasTenantConfig) {
|
|
42
|
+
console.warn(`[hypequery/serve] Query "${options.queryKey}" accesses user data but has no tenant configuration. ` +
|
|
43
|
+
`This may lead to data leaks. Add tenant config to defineServe or the query definition.`);
|
|
44
|
+
}
|
|
45
|
+
else if (options.hasTenantId && options.mode === 'manual') {
|
|
46
|
+
console.warn(`[hypequery/serve] Query "${options.queryKey}" uses manual tenant mode. ` +
|
|
47
|
+
`Ensure you manually filter queries by tenantId to prevent data leaks.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const api: import("../types.js").ServeBuilder<import("../types.js").ServeEndpointMap<{
|
|
3
|
+
typedQuery: import("../types.js").ServeQueryConfig<z.ZodObject<{
|
|
4
|
+
plan: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
plan?: string | undefined;
|
|
7
|
+
}, {
|
|
8
|
+
plan?: string | undefined;
|
|
9
|
+
}>, z.ZodTypeAny, {}, import("../types.js").AuthContext, {
|
|
10
|
+
plan: string;
|
|
11
|
+
}[]>;
|
|
12
|
+
}, {}, import("../types.js").AuthContext>, {}, import("../types.js").AuthContext>;
|
|
13
|
+
//# sourceMappingURL=builder.test-d.d.ts.map
|
|
@@ -0,0 +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;AAUxB,eAAO,MAAM,GAAG;;;;;;;;;;iFAUd,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { initServe } from '../index.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const serve = initServe({
|
|
4
|
+
context: () => ({}),
|
|
5
|
+
});
|
|
6
|
+
const { query } = serve;
|
|
7
|
+
export const api = serve.define({
|
|
8
|
+
queries: serve.queries({
|
|
9
|
+
typedQuery: query
|
|
10
|
+
.describe('builder infers input + output')
|
|
11
|
+
.input(z.object({ plan: z.string().optional() }))
|
|
12
|
+
.query(async ({ input }) => {
|
|
13
|
+
const plan = input.plan ?? 'starter';
|
|
14
|
+
return [{ plan }];
|
|
15
|
+
}),
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
const _resultIsTyped = [{ plan: 'starter' }];
|
|
19
|
+
// @ts-expect-error plan must be string
|
|
20
|
+
const _resultRejectsNumber = [{ plan: 123 }];
|