@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.
- 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 +192 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +221 -0
- package/dist/builder.d.ts +3 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +56 -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 +30 -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 +65 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/openapi.d.ts +3 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +205 -0
- package/dist/pipeline.d.ts +77 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +424 -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/builder.d.ts +7 -0
- package/dist/server/builder.d.ts.map +1 -0
- package/dist/server/builder.js +73 -0
- package/dist/server/define-serve.d.ts +3 -0
- package/dist/server/define-serve.d.ts.map +1 -0
- package/dist/server/define-serve.js +88 -0
- package/dist/server/execute-query.d.ts +8 -0
- package/dist/server/execute-query.d.ts.map +1 -0
- package/dist/server/execute-query.js +39 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/init-serve.d.ts +8 -0
- package/dist/server/init-serve.d.ts.map +1 -0
- package/dist/server/init-serve.js +18 -0
- package/dist/server/mapper.d.ts +3 -0
- package/dist/server/mapper.d.ts.map +1 -0
- package/dist/server/mapper.js +30 -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 +514 -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 +1 -1
package/dist/auth.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Check if the authenticated user has at least one of the required roles (OR semantics).
|
|
40
|
+
*
|
|
41
|
+
* @param auth - The auth context from the request
|
|
42
|
+
* @param requiredRoles - Array of role names, any one of which grants access
|
|
43
|
+
* @returns { ok: true } if user has a role, or { ok: false, missing, reason } with details
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const result = checkRoleAuthorization(auth, ['admin', 'editor']);
|
|
48
|
+
* if (!result.ok) {
|
|
49
|
+
* console.log('Missing roles:', result.missing); // ['admin', 'editor'] (all required)
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export const checkRoleAuthorization = (auth, requiredRoles) => {
|
|
54
|
+
if (!requiredRoles || requiredRoles.length === 0) {
|
|
55
|
+
return { ok: true };
|
|
56
|
+
}
|
|
57
|
+
const userRoles = auth?.roles ?? [];
|
|
58
|
+
const hasRole = requiredRoles.some((role) => userRoles.includes(role));
|
|
59
|
+
// Note: We return ALL required roles in missing[], not just the ones the user lacks.
|
|
60
|
+
// This matches the original behavior for error reporting consistency.
|
|
61
|
+
return hasRole
|
|
62
|
+
? { ok: true }
|
|
63
|
+
: { ok: false, missing: requiredRoles, reason: 'MISSING_ROLE' };
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Check if the authenticated user has all of the required scopes (AND semantics).
|
|
67
|
+
*
|
|
68
|
+
* @param auth - The auth context from the request
|
|
69
|
+
* @param requiredScopes - Array of scope names, all of which are required
|
|
70
|
+
* @returns { ok: true } if user has all scopes, or { ok: false, missing, reason } with details
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const result = checkScopeAuthorization(auth, ['read:metrics', 'write:metrics']);
|
|
75
|
+
* if (!result.ok) {
|
|
76
|
+
* console.log('Missing scopes:', result.missing); // ['read:metrics', 'write:metrics'] (all required)
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const checkScopeAuthorization = (auth, requiredScopes) => {
|
|
81
|
+
if (!requiredScopes || requiredScopes.length === 0) {
|
|
82
|
+
return { ok: true };
|
|
83
|
+
}
|
|
84
|
+
const userScopes = auth?.scopes ?? [];
|
|
85
|
+
const hasAllScopes = requiredScopes.every((scope) => userScopes.includes(scope));
|
|
86
|
+
// Note: We return ALL required scopes in missing[], not just the ones the user lacks.
|
|
87
|
+
// This matches the original behavior for error reporting consistency.
|
|
88
|
+
return hasAllScopes
|
|
89
|
+
? { ok: true }
|
|
90
|
+
: { ok: false, missing: requiredScopes, reason: 'MISSING_SCOPE' };
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Middleware that requires the user to be authenticated.
|
|
94
|
+
* Returns 401 if no auth context is present.
|
|
95
|
+
*
|
|
96
|
+
* @deprecated Use `query.requireAuth()` instead for per-endpoint authentication.
|
|
97
|
+
* This middleware is kept for complex use cases where guards aren't suitable.
|
|
98
|
+
* See: https://hypequery.com/docs/serve/authentication#middleware-helpers
|
|
99
|
+
*
|
|
100
|
+
* Use this as a global middleware via `api.use(requireAuthMiddleware())`.
|
|
101
|
+
* For per-query guards, prefer `query.requireAuth()`.
|
|
102
|
+
*/
|
|
103
|
+
export const requireAuthMiddleware = () => async (ctx, next) => {
|
|
104
|
+
if (!ctx.auth) {
|
|
105
|
+
throw Object.assign(new Error("Authentication required"), {
|
|
106
|
+
status: 401,
|
|
107
|
+
type: "UNAUTHORIZED",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return next();
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Middleware that requires the user to have at least one of the specified roles.
|
|
114
|
+
* Returns 403 if the user lacks the required role.
|
|
115
|
+
*
|
|
116
|
+
* @deprecated Use `query.requireRole(...)` instead for per-endpoint authorization.
|
|
117
|
+
* This middleware is kept for complex use cases where guards aren't suitable.
|
|
118
|
+
* See: https://hypequery.com/docs/serve/authentication#middleware-helpers
|
|
119
|
+
*
|
|
120
|
+
* Use this as a global or per-query middleware via `api.use(requireRoleMiddleware('admin'))`.
|
|
121
|
+
* For per-query guards, prefer `query.requireRole('admin')`.
|
|
122
|
+
*/
|
|
123
|
+
export const requireRoleMiddleware = (...roles) => async (ctx, next) => {
|
|
124
|
+
const result = checkRoleAuthorization(ctx.auth, roles);
|
|
125
|
+
if (!result.ok) {
|
|
126
|
+
throw Object.assign(new Error(`Missing required role. Required one of: ${roles.join(", ")}`), { status: 403, type: "FORBIDDEN" });
|
|
127
|
+
}
|
|
128
|
+
return next();
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Middleware that requires the user to have all of the specified scopes.
|
|
132
|
+
* Returns 403 if the user lacks a required scope.
|
|
133
|
+
*
|
|
134
|
+
* @deprecated Use `query.requireScope(...)` instead for per-endpoint authorization.
|
|
135
|
+
* This middleware is kept for complex use cases where guards aren't suitable.
|
|
136
|
+
* See: https://hypequery.com/docs/serve/authentication#middleware-helpers
|
|
137
|
+
*
|
|
138
|
+
* Use this as a global or per-query middleware via `api.use(requireScopeMiddleware('read:metrics'))`.
|
|
139
|
+
* For per-query guards, prefer `query.requireScope('read:metrics')`.
|
|
140
|
+
*/
|
|
141
|
+
export const requireScopeMiddleware = (...scopes) => async (ctx, next) => {
|
|
142
|
+
const result = checkScopeAuthorization(ctx.auth, scopes);
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
throw Object.assign(new Error(`Missing required scopes: ${result.missing.join(", ")}`), { status: 403, type: "FORBIDDEN" });
|
|
145
|
+
}
|
|
146
|
+
return next();
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Creates a typed auth system with compile-time role and scope safety.
|
|
150
|
+
*
|
|
151
|
+
* This helper provides:
|
|
152
|
+
* - Type-safe auth context (combines AuthContextWithRoles and AuthContextWithScopes)
|
|
153
|
+
* - A `useAuth` wrapper for auth strategies
|
|
154
|
+
* - Helper to extract the typed auth type
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* import { createAuthSystem, defineServe, query } from '@hypequery/serve';
|
|
159
|
+
*
|
|
160
|
+
* // Define your roles and scopes up front
|
|
161
|
+
* const { useAuth, TypedAuth } = createAuthSystem({
|
|
162
|
+
* roles: ['admin', 'editor', 'viewer'] as const,
|
|
163
|
+
* scopes: ['read:metrics', 'write:metrics', 'delete:metrics'] as const,
|
|
164
|
+
* });
|
|
165
|
+
*
|
|
166
|
+
* // Extract the typed auth type for use with defineServe
|
|
167
|
+
* type AppAuth = TypedAuth;
|
|
168
|
+
*
|
|
169
|
+
* const api = defineServe<AppAuth>({
|
|
170
|
+
* auth: useAuth(jwtStrategy),
|
|
171
|
+
* queries: {
|
|
172
|
+
* adminOnly: query.requireRole('admin').query(async ({ ctx }) => {
|
|
173
|
+
* // ✅ TypeScript autocomplete for 'admin'
|
|
174
|
+
* // ❌ Compile error on typo like 'admn'
|
|
175
|
+
* return { secret: true };
|
|
176
|
+
* }),
|
|
177
|
+
* writeData: query.requireScope('write:metrics').query(async ({ ctx }) => {
|
|
178
|
+
* // ✅ TypeScript autocomplete for 'write:metrics'
|
|
179
|
+
* return { success: true };
|
|
180
|
+
* }),
|
|
181
|
+
* },
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export const createAuthSystem = (options = {}) => {
|
|
186
|
+
return {
|
|
187
|
+
/**
|
|
188
|
+
* Type-safe wrapper for auth strategies.
|
|
189
|
+
* Ensures the strategy returns auth context with the correct role/scope types.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```ts
|
|
193
|
+
* const jwtStrategy: AuthStrategy<AppAuth> = async ({ request }) => {
|
|
194
|
+
* const token = request.headers.authorization?.slice(7);
|
|
195
|
+
* const payload = await verifyJwt(token);
|
|
196
|
+
* return {
|
|
197
|
+
* userId: payload.sub,
|
|
198
|
+
* roles: payload.roles, // ✅ Type-checked against ['admin', 'editor', 'viewer']
|
|
199
|
+
* scopes: payload.scopes, // ✅ Type-checked against ['read:metrics', 'write:metrics']
|
|
200
|
+
* };
|
|
201
|
+
* };
|
|
202
|
+
*
|
|
203
|
+
* const api = defineServe<AppAuth>({
|
|
204
|
+
* auth: useAuth(jwtStrategy),
|
|
205
|
+
* // ...
|
|
206
|
+
* });
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
useAuth: (strategy) => strategy,
|
|
210
|
+
/**
|
|
211
|
+
* The combined typed auth context type.
|
|
212
|
+
* Use this to type your defineServe generic parameter.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* type AppAuth = typeof TypedAuth;
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
TypedAuth: null,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EAEX,qBAAqB,EAStB,MAAM,YAAY,CAAC;AA0BpB,eAAO,MAAM,sBAAsB,GACjC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,OACtB,qBAAqB,CAAC,QAAQ,EAAE,KAAK,CA4DzC,CAAC"}
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mergeTags } from './utils.js';
|
|
2
|
+
const defaultState = () => ({
|
|
3
|
+
tags: [],
|
|
4
|
+
middlewares: [],
|
|
5
|
+
});
|
|
6
|
+
export const createProcedureBuilder = () => {
|
|
7
|
+
const build = (state) => ({
|
|
8
|
+
input: (schema) => build({ ...state, inputSchema: schema }),
|
|
9
|
+
output: (schema) => build({ ...state, outputSchema: schema }),
|
|
10
|
+
describe: (description) => build({ ...state, description }),
|
|
11
|
+
name: (name) => build({ ...state, name }),
|
|
12
|
+
summary: (summary) => build({ ...state, summary }),
|
|
13
|
+
tag: (tag) => build({ ...state, tags: mergeTags(state.tags, [tag]) }),
|
|
14
|
+
tags: (tags) => build({ ...state, tags: mergeTags(state.tags, tags) }),
|
|
15
|
+
method: (method) => build({ ...state, method }),
|
|
16
|
+
cache: (ttlMs) => build({ ...state, cacheTtlMs: ttlMs }),
|
|
17
|
+
auth: (strategy) => build({ ...state, auth: strategy }),
|
|
18
|
+
requireAuth: () => build({ ...state, requiresAuth: true }),
|
|
19
|
+
requireRole: (...roles) => build({
|
|
20
|
+
...state,
|
|
21
|
+
requiresAuth: true,
|
|
22
|
+
requiredRoles: [...(state.requiredRoles ?? []), ...roles],
|
|
23
|
+
}),
|
|
24
|
+
requireScope: (...scopes) => build({
|
|
25
|
+
...state,
|
|
26
|
+
requiresAuth: true,
|
|
27
|
+
requiredScopes: [...(state.requiredScopes ?? []), ...scopes],
|
|
28
|
+
}),
|
|
29
|
+
public: () => build({ ...state, requiresAuth: false }),
|
|
30
|
+
tenant: (config) => build({ ...state, tenant: config }),
|
|
31
|
+
custom: (custom) => build({ ...state, custom: { ...(state.custom ?? {}), ...custom } }),
|
|
32
|
+
use: (...middlewares) => build({ ...state, middlewares: [...state.middlewares, ...middlewares] }),
|
|
33
|
+
query: (executable) => {
|
|
34
|
+
const config = {
|
|
35
|
+
description: state.description,
|
|
36
|
+
name: state.name,
|
|
37
|
+
summary: state.summary,
|
|
38
|
+
tags: state.tags,
|
|
39
|
+
method: state.method,
|
|
40
|
+
inputSchema: state.inputSchema,
|
|
41
|
+
outputSchema: state.outputSchema,
|
|
42
|
+
cacheTtlMs: state.cacheTtlMs,
|
|
43
|
+
auth: typeof state.auth === 'undefined' ? null : state.auth,
|
|
44
|
+
requiresAuth: state.requiresAuth,
|
|
45
|
+
tenant: state.tenant,
|
|
46
|
+
requiredRoles: state.requiredRoles,
|
|
47
|
+
requiredScopes: state.requiredScopes,
|
|
48
|
+
custom: state.custom,
|
|
49
|
+
middlewares: state.middlewares,
|
|
50
|
+
query: executable,
|
|
51
|
+
};
|
|
52
|
+
return config;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
return build(defaultState());
|
|
56
|
+
};
|
|
@@ -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;AAGnE,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;;;EA6B9B,CAAC"}
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { startNodeServer } from "./adapters/node.js";
|
|
2
|
+
import { formatQueryEvent } from "./query-logger.js";
|
|
3
|
+
const defaultLogger = (message) => {
|
|
4
|
+
console.log(message);
|
|
5
|
+
};
|
|
6
|
+
export const serveDev = async (api, options = {}) => {
|
|
7
|
+
const port = options.port ?? Number(process.env.PORT ?? 4000);
|
|
8
|
+
const hostname = options.hostname ?? "localhost";
|
|
9
|
+
const logger = options.logger ?? defaultLogger;
|
|
10
|
+
const unsubscribe = api.queryLogger.on((event) => {
|
|
11
|
+
const line = formatQueryEvent(event);
|
|
12
|
+
if (line)
|
|
13
|
+
logger(line);
|
|
14
|
+
});
|
|
15
|
+
const server = await startNodeServer(api.handler, {
|
|
16
|
+
...options,
|
|
17
|
+
hostname,
|
|
18
|
+
port,
|
|
19
|
+
quiet: true,
|
|
20
|
+
});
|
|
21
|
+
if (!options.quiet) {
|
|
22
|
+
const address = server.server.address();
|
|
23
|
+
const display = typeof address === "object" && address
|
|
24
|
+
? `${address.address}:${address.port}`
|
|
25
|
+
: `${hostname}:${port}`;
|
|
26
|
+
logger(`hypequery dev server running at http://${display}`);
|
|
27
|
+
logger(`Docs available at http://${display}/docs`);
|
|
28
|
+
}
|
|
29
|
+
return server;
|
|
30
|
+
};
|
|
@@ -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,CA4DrD,CAAC"}
|
package/dist/endpoint.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
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 hasRolesOrScopes = (definition.requiredRoles?.length ?? 0) > 0
|
|
22
|
+
|| (definition.requiredScopes?.length ?? 0) > 0;
|
|
23
|
+
const metadata = {
|
|
24
|
+
path: "",
|
|
25
|
+
method: method,
|
|
26
|
+
name: definition.name ?? definition.summary ?? key,
|
|
27
|
+
summary: definition.summary,
|
|
28
|
+
description: definition.description,
|
|
29
|
+
tags: definition.tags ?? [],
|
|
30
|
+
requiresAuth: definition.requiresAuth ?? (definition.auth ? true : hasRolesOrScopes ? true : undefined),
|
|
31
|
+
requiredRoles: definition.requiredRoles,
|
|
32
|
+
requiredScopes: definition.requiredScopes,
|
|
33
|
+
deprecated: undefined,
|
|
34
|
+
visibility: "public",
|
|
35
|
+
custom: definition.custom,
|
|
36
|
+
};
|
|
37
|
+
const runner = resolveQueryRunner(definition.query);
|
|
38
|
+
const handler = async (ctx) => {
|
|
39
|
+
if (!runner) {
|
|
40
|
+
throw new Error(`Endpoint "${key}" is missing an executable query`);
|
|
41
|
+
}
|
|
42
|
+
return runner({
|
|
43
|
+
input: ctx.input,
|
|
44
|
+
ctx: ctx,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const outputSchema = (definition.outputSchema ?? fallbackSchema);
|
|
48
|
+
const inputSchema = definition.inputSchema;
|
|
49
|
+
return {
|
|
50
|
+
key,
|
|
51
|
+
method,
|
|
52
|
+
inputSchema,
|
|
53
|
+
outputSchema,
|
|
54
|
+
handler,
|
|
55
|
+
query: definition.query,
|
|
56
|
+
middlewares: definition.middlewares ?? [],
|
|
57
|
+
auth: definition.auth ?? null,
|
|
58
|
+
tenant: definition.tenant,
|
|
59
|
+
metadata,
|
|
60
|
+
cacheTtlMs: definition.cacheTtlMs ?? null,
|
|
61
|
+
defaultHeaders: undefined,
|
|
62
|
+
requiredRoles: definition.requiredRoles,
|
|
63
|
+
requiredScopes: definition.requiredScopes,
|
|
64
|
+
};
|
|
65
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./query-logger.js";
|
|
3
|
+
export * from "./server/index.js";
|
|
4
|
+
export * from "./router.js";
|
|
5
|
+
export * from "./endpoint.js";
|
|
6
|
+
export * from "./openapi.js";
|
|
7
|
+
export * from "./docs-ui.js";
|
|
8
|
+
export * from "./auth.js";
|
|
9
|
+
export * from "./client-config.js";
|
|
10
|
+
export * from "./adapters/node.js";
|
|
11
|
+
export * from "./adapters/fetch.js";
|
|
12
|
+
export * from "./adapters/vercel.js";
|
|
13
|
+
export * from "./dev.js";
|
|
14
|
+
//# 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,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,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,13 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./query-logger.js";
|
|
3
|
+
export * from "./server/index.js";
|
|
4
|
+
export * from "./router.js";
|
|
5
|
+
export * from "./endpoint.js";
|
|
6
|
+
export * from "./openapi.js";
|
|
7
|
+
export * from "./docs-ui.js";
|
|
8
|
+
export * from "./auth.js";
|
|
9
|
+
export * from "./client-config.js";
|
|
10
|
+
export * from "./adapters/node.js";
|
|
11
|
+
export * from "./adapters/fetch.js";
|
|
12
|
+
export * from "./adapters/vercel.js";
|
|
13
|
+
export * from "./dev.js";
|
|
@@ -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;AAuMjF,eAAO,MAAM,oBAAoB,GAC/B,WAAW,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,EAC9C,UAAU,cAAc,KACvB,eAyCF,CAAC"}
|