@hypequery/serve 0.2.1 → 0.3.0
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/README.md +138 -1
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +3 -5
- package/dist/adapters/standalone.d.ts +41 -0
- package/dist/adapters/standalone.d.ts.map +1 -0
- package/dist/adapters/standalone.js +46 -0
- package/dist/auth.d.ts +59 -83
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +136 -102
- package/dist/client-config.d.ts +3 -2
- package/dist/client-config.d.ts.map +1 -1
- package/dist/client-config.js +4 -2
- package/dist/errors.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/openapi.js +1 -2
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +10 -22
- package/dist/query-logger.js +1 -3
- package/dist/rate-limit.js +4 -3
- package/dist/router.js +2 -1
- package/dist/semantic/datasets/dataset-endpoint.d.ts +85 -0
- package/dist/semantic/datasets/dataset-endpoint.d.ts.map +1 -0
- package/dist/semantic/datasets/dataset-endpoint.js +121 -0
- package/dist/semantic/datasets/index.d.ts +6 -0
- package/dist/semantic/datasets/index.d.ts.map +1 -0
- package/dist/semantic/datasets/index.js +5 -0
- package/dist/semantic/datasets/metric-endpoint.d.ts +82 -0
- package/dist/semantic/datasets/metric-endpoint.d.ts.map +1 -0
- package/dist/semantic/datasets/metric-endpoint.js +159 -0
- package/dist/semantic/datasets/utils/dataset-entry.d.ts +24 -0
- package/dist/semantic/datasets/utils/dataset-entry.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/dataset-entry.js +15 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.d.ts +3 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/dataset-query-metadata.js +12 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.d.ts +107 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.d.ts.map +1 -0
- package/dist/semantic/datasets/utils/semantic-input-schema.js +87 -0
- package/dist/semantic/index.d.ts +2 -0
- package/dist/semantic/index.d.ts.map +1 -0
- package/dist/semantic/index.js +1 -0
- package/dist/semantic/query-builder-context.d.ts +20 -0
- package/dist/semantic/query-builder-context.d.ts.map +1 -0
- package/dist/semantic/query-builder-context.js +66 -0
- package/dist/semantic/utils/tenant-runtime.d.ts +11 -0
- package/dist/semantic/utils/tenant-runtime.d.ts.map +1 -0
- package/dist/semantic/utils/tenant-runtime.js +48 -0
- package/dist/serve.d.ts +2 -2
- package/dist/serve.d.ts.map +1 -1
- package/dist/server/api-builder.d.ts +5 -0
- package/dist/server/api-builder.d.ts.map +1 -0
- package/dist/server/api-builder.js +76 -0
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.js +11 -1
- package/dist/server/create-api.d.ts +32 -0
- package/dist/server/create-api.d.ts.map +1 -0
- package/dist/server/create-api.js +211 -0
- package/dist/server/define-serve.d.ts +21 -2
- package/dist/server/define-serve.d.ts.map +1 -1
- package/dist/server/define-serve.js +53 -84
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/init-serve.d.ts +1 -1
- package/dist/server/init-serve.d.ts.map +1 -1
- package/dist/server/init-serve.js +7 -2
- package/dist/type-tests/builder.test-d.d.ts +4 -0
- package/dist/type-tests/builder.test-d.d.ts.map +1 -1
- package/dist/type-tests/builder.test-d.js +16 -1
- package/dist/type-tests/semantic.test-d.d.ts +2 -0
- package/dist/type-tests/semantic.test-d.d.ts.map +1 -0
- package/dist/type-tests/semantic.test-d.js +59 -0
- package/dist/types.d.ts +227 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
package/dist/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SignJWT, createRemoteJWKSet, jwtVerify, } from "jose";
|
|
1
2
|
const resolveHeaderValue = (value) => {
|
|
2
3
|
if (Array.isArray(value)) {
|
|
3
4
|
const first = value.find((item) => typeof item === "string");
|
|
@@ -28,6 +29,8 @@ export const getHeader = (request, name) => {
|
|
|
28
29
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
29
30
|
};
|
|
30
31
|
export class AuthError extends Error {
|
|
32
|
+
reason;
|
|
33
|
+
details;
|
|
31
34
|
constructor(reason, message, details) {
|
|
32
35
|
super(message);
|
|
33
36
|
this.name = "AuthError";
|
|
@@ -92,6 +95,131 @@ export const createBearerTokenStrategy = (options) => {
|
|
|
92
95
|
return options.validate(token, request);
|
|
93
96
|
};
|
|
94
97
|
};
|
|
98
|
+
export const fromContext = (extract) => async (context) => extract(context);
|
|
99
|
+
const defaultJwtClaimMapper = (payload) => {
|
|
100
|
+
const scopeClaim = payload.scope ?? payload.scopes;
|
|
101
|
+
const scopes = typeof scopeClaim === "string"
|
|
102
|
+
? scopeClaim.split(" ").filter(Boolean)
|
|
103
|
+
: Array.isArray(scopeClaim)
|
|
104
|
+
? scopeClaim.filter((scope) => typeof scope === "string")
|
|
105
|
+
: undefined;
|
|
106
|
+
const roles = Array.isArray(payload.roles)
|
|
107
|
+
? payload.roles.filter((role) => typeof role === "string")
|
|
108
|
+
: undefined;
|
|
109
|
+
return {
|
|
110
|
+
userId: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
111
|
+
tenantId: typeof payload.org_id === "string" ? payload.org_id : undefined,
|
|
112
|
+
roles,
|
|
113
|
+
scopes,
|
|
114
|
+
metadata: payload,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Verifies JWT bearer tokens with either a shared secret (HS256 by default) or
|
|
119
|
+
* a remote JWKS (RS256 by default). Use shared secrets when you mint the token
|
|
120
|
+
* yourself, and JWKS when a provider such as Auth0, Clerk, or Cognito mints it.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* const api = createAPI({
|
|
125
|
+
* auth: createJwtStrategy({
|
|
126
|
+
* jwksUri: 'https://example.auth0.com/.well-known/jwks.json',
|
|
127
|
+
* issuer: 'https://example.auth0.com/',
|
|
128
|
+
* audience: 'https://api.example.com',
|
|
129
|
+
* }),
|
|
130
|
+
* queries: { ... },
|
|
131
|
+
* });
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
const resolveSecretKey = (secret, helperName) => {
|
|
135
|
+
if (typeof secret === "string") {
|
|
136
|
+
if (secret.length === 0) {
|
|
137
|
+
throw new Error(`${helperName}: \`secret\` must not be empty.`);
|
|
138
|
+
}
|
|
139
|
+
return new TextEncoder().encode(secret);
|
|
140
|
+
}
|
|
141
|
+
if (secret.byteLength === 0) {
|
|
142
|
+
throw new Error(`${helperName}: \`secret\` must not be empty.`);
|
|
143
|
+
}
|
|
144
|
+
return secret;
|
|
145
|
+
};
|
|
146
|
+
const isSecretJwtOptions = (options) => "secret" in options && options.secret != null;
|
|
147
|
+
const isJwksJwtOptions = (options) => "jwksUri" in options && options.jwksUri != null;
|
|
148
|
+
export function createJwtStrategy(options) {
|
|
149
|
+
const hasSecret = isSecretJwtOptions(options);
|
|
150
|
+
const hasJwksUri = isJwksJwtOptions(options);
|
|
151
|
+
if (hasSecret === hasJwksUri) {
|
|
152
|
+
throw new Error("createJwtStrategy: provide exactly one of `secret` or `jwksUri`.");
|
|
153
|
+
}
|
|
154
|
+
const headerName = options.header ?? "authorization";
|
|
155
|
+
const prefix = options.prefix ?? "Bearer ";
|
|
156
|
+
const mapClaims = options.mapClaims
|
|
157
|
+
?? defaultJwtClaimMapper;
|
|
158
|
+
let verify;
|
|
159
|
+
if (hasSecret) {
|
|
160
|
+
const key = resolveSecretKey(options.secret, "createJwtStrategy");
|
|
161
|
+
verify = (token) => jwtVerify(token, key, {
|
|
162
|
+
issuer: options.issuer,
|
|
163
|
+
audience: options.audience,
|
|
164
|
+
algorithms: options.algorithms ?? ["HS256"],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
if (options.jwksUri.trim().length === 0) {
|
|
169
|
+
throw new Error("createJwtStrategy: `jwksUri` must not be empty.");
|
|
170
|
+
}
|
|
171
|
+
const jwks = createRemoteJWKSet(new URL(options.jwksUri));
|
|
172
|
+
verify = (token) => jwtVerify(token, jwks, {
|
|
173
|
+
issuer: options.issuer,
|
|
174
|
+
audience: options.audience,
|
|
175
|
+
algorithms: options.algorithms ?? ["RS256"],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return async ({ request }) => {
|
|
179
|
+
const raw = getHeader(request, headerName);
|
|
180
|
+
if (typeof raw !== "string" || !raw.startsWith(prefix)) {
|
|
181
|
+
if (options.optional)
|
|
182
|
+
return null;
|
|
183
|
+
throw new AuthError("MISSING", `Missing bearer token in "${headerName}" header`, { header: headerName });
|
|
184
|
+
}
|
|
185
|
+
const token = raw.slice(prefix.length).trim();
|
|
186
|
+
if (!token) {
|
|
187
|
+
if (options.optional)
|
|
188
|
+
return null;
|
|
189
|
+
throw new AuthError("MISSING", `Empty bearer token in "${headerName}" header`, { header: headerName });
|
|
190
|
+
}
|
|
191
|
+
let payload;
|
|
192
|
+
try {
|
|
193
|
+
const verified = await verify(token);
|
|
194
|
+
payload = verified.payload;
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
throw new AuthError("INVALID", "JWT verification failed", {
|
|
198
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return mapClaims(payload, request);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export const createAnalyticsTokenIssuer = (options) => {
|
|
205
|
+
const key = resolveSecretKey(options.secret, "createAnalyticsTokenIssuer");
|
|
206
|
+
const algorithm = options.algorithm ?? "HS256";
|
|
207
|
+
return async (claims) => {
|
|
208
|
+
let jwt = new SignJWT({
|
|
209
|
+
...(claims.tenantId ? { org_id: claims.tenantId } : {}),
|
|
210
|
+
...(claims.roles ? { roles: claims.roles } : {}),
|
|
211
|
+
})
|
|
212
|
+
.setProtectedHeader({ alg: algorithm })
|
|
213
|
+
.setSubject(claims.userId)
|
|
214
|
+
.setIssuedAt()
|
|
215
|
+
.setExpirationTime(options.expiresIn ?? "15m");
|
|
216
|
+
if (options.issuer)
|
|
217
|
+
jwt = jwt.setIssuer(options.issuer);
|
|
218
|
+
if (options.audience)
|
|
219
|
+
jwt = jwt.setAudience(options.audience);
|
|
220
|
+
return jwt.sign(key);
|
|
221
|
+
};
|
|
222
|
+
};
|
|
95
223
|
/**
|
|
96
224
|
* Check if the authenticated user has at least one of the required roles (OR semantics).
|
|
97
225
|
*
|
|
@@ -146,65 +274,15 @@ export const checkScopeAuthorization = (auth, requiredScopes) => {
|
|
|
146
274
|
? { ok: true }
|
|
147
275
|
: { ok: false, missing: requiredScopes, reason: 'MISSING_SCOPE' };
|
|
148
276
|
};
|
|
149
|
-
/**
|
|
150
|
-
* Middleware that requires the user to be authenticated.
|
|
151
|
-
* Returns 401 if no auth context is present.
|
|
152
|
-
*
|
|
153
|
-
* @deprecated Use `query.requireAuth()` instead for per-endpoint authentication.
|
|
154
|
-
* This middleware is kept for complex use cases where guards aren't suitable.
|
|
155
|
-
* See: https://hypequery.com/docs/authentication#middleware-helpers
|
|
156
|
-
*
|
|
157
|
-
* Use this as a global middleware via `api.use(requireAuthMiddleware())`.
|
|
158
|
-
* For per-query guards, prefer `query.requireAuth()`.
|
|
159
|
-
*/
|
|
160
|
-
export const requireAuthMiddleware = () => async (ctx, next) => {
|
|
161
|
-
if (!ctx.auth) {
|
|
162
|
-
throw Object.assign(new Error("Authentication required"), {
|
|
163
|
-
status: 401,
|
|
164
|
-
type: "UNAUTHORIZED",
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
return next();
|
|
168
|
-
};
|
|
169
|
-
/**
|
|
170
|
-
* Middleware that requires the user to have at least one of the specified roles.
|
|
171
|
-
* Returns 403 if the user lacks the required role.
|
|
172
|
-
*
|
|
173
|
-
* @deprecated Use `query.requireRole(...)` instead for per-endpoint authorization.
|
|
174
|
-
* This middleware is kept for complex use cases where guards aren't suitable.
|
|
175
|
-
* See: https://hypequery.com/docs/authentication#middleware-helpers
|
|
176
|
-
*
|
|
177
|
-
* Use this as a global or per-query middleware via `api.use(requireRoleMiddleware('admin'))`.
|
|
178
|
-
* For per-query guards, prefer `query.requireRole('admin')`.
|
|
179
|
-
*/
|
|
180
|
-
export const requireRoleMiddleware = (...roles) => async (ctx, next) => {
|
|
181
|
-
const result = checkRoleAuthorization(ctx.auth, roles);
|
|
182
|
-
if (!result.ok) {
|
|
183
|
-
throw Object.assign(new Error(`Missing required role. Required one of: ${roles.join(", ")}`), { status: 403, type: "FORBIDDEN" });
|
|
184
|
-
}
|
|
185
|
-
return next();
|
|
186
|
-
};
|
|
187
|
-
/**
|
|
188
|
-
* Middleware that requires the user to have all of the specified scopes.
|
|
189
|
-
* Returns 403 if the user lacks a required scope.
|
|
190
|
-
*
|
|
191
|
-
* @deprecated Use `query.requireScope(...)` instead for per-endpoint authorization.
|
|
192
|
-
* This middleware is kept for complex use cases where guards aren't suitable.
|
|
193
|
-
* See: https://hypequery.com/docs/authentication#middleware-helpers
|
|
194
|
-
*
|
|
195
|
-
* Use this as a global or per-query middleware via `api.use(requireScopeMiddleware('read:metrics'))`.
|
|
196
|
-
* For per-query guards, prefer `query.requireScope('read:metrics')`.
|
|
197
|
-
*/
|
|
198
|
-
export const requireScopeMiddleware = (...scopes) => async (ctx, next) => {
|
|
199
|
-
const result = checkScopeAuthorization(ctx.auth, scopes);
|
|
200
|
-
if (!result.ok) {
|
|
201
|
-
throw Object.assign(new Error(`Missing required scopes: ${result.missing.join(", ")}`), { status: 403, type: "FORBIDDEN" });
|
|
202
|
-
}
|
|
203
|
-
return next();
|
|
204
|
-
};
|
|
205
277
|
/**
|
|
206
278
|
* Creates a typed auth system with compile-time role and scope safety.
|
|
207
279
|
*
|
|
280
|
+
* @deprecated Prefer typing your auth context directly and passing it to
|
|
281
|
+
* `initServe<TContext, TAuth>(...)`. Define roles/scopes as a
|
|
282
|
+
* union on your auth type and use `query.requireRole(...)` /
|
|
283
|
+
* `query.requireScope(...)` guards. This helper is kept for
|
|
284
|
+
* backwards compatibility and will be removed in a future release.
|
|
285
|
+
*
|
|
208
286
|
* This helper provides:
|
|
209
287
|
* - Type-safe auth context (combines AuthContextWithRoles and AuthContextWithScopes)
|
|
210
288
|
* - A `useAuth` wrapper for auth strategies
|
|
@@ -221,32 +299,11 @@ export const requireScopeMiddleware = (...scopes) => async (ctx, next) => {
|
|
|
221
299
|
* });
|
|
222
300
|
*
|
|
223
301
|
* // Extract the typed auth type for use with initServe
|
|
224
|
-
* type AppAuth = TypedAuth;
|
|
302
|
+
* type AppAuth = typeof TypedAuth;
|
|
225
303
|
*
|
|
226
304
|
* const { query, serve } = initServe<Record<string, never>, AppAuth>({
|
|
227
305
|
* auth: useAuth(jwtStrategy),
|
|
228
306
|
* });
|
|
229
|
-
*
|
|
230
|
-
* const adminOnly = query({
|
|
231
|
-
* requiredRoles: ['admin'],
|
|
232
|
-
* query: async () => {
|
|
233
|
-
* // ✅ TypeScript autocomplete for 'admin'
|
|
234
|
-
* // ❌ Compile error on typo like 'admn'
|
|
235
|
-
* return { secret: true };
|
|
236
|
-
* },
|
|
237
|
-
* });
|
|
238
|
-
*
|
|
239
|
-
* const writeData = query({
|
|
240
|
-
* requiredScopes: ['write:metrics'],
|
|
241
|
-
* query: async () => {
|
|
242
|
-
* // ✅ TypeScript autocomplete for 'write:metrics'
|
|
243
|
-
* return { success: true };
|
|
244
|
-
* },
|
|
245
|
-
* });
|
|
246
|
-
*
|
|
247
|
-
* const api = serve({
|
|
248
|
-
* queries: { adminOnly, writeData },
|
|
249
|
-
* });
|
|
250
307
|
* ```
|
|
251
308
|
*/
|
|
252
309
|
export const createAuthSystem = () => {
|
|
@@ -254,34 +311,11 @@ export const createAuthSystem = () => {
|
|
|
254
311
|
/**
|
|
255
312
|
* Type-safe wrapper for auth strategies.
|
|
256
313
|
* Ensures the strategy returns auth context with the correct role/scope types.
|
|
257
|
-
*
|
|
258
|
-
* @example
|
|
259
|
-
* ```ts
|
|
260
|
-
* const jwtStrategy: AuthStrategy<AppAuth> = async ({ request }) => {
|
|
261
|
-
* const token = request.headers.authorization?.slice(7);
|
|
262
|
-
* const payload = await verifyJwt(token);
|
|
263
|
-
* return {
|
|
264
|
-
* userId: payload.sub,
|
|
265
|
-
* roles: payload.roles, // ✅ Type-checked against ['admin', 'editor', 'viewer']
|
|
266
|
-
* scopes: payload.scopes, // ✅ Type-checked against ['read:metrics', 'write:metrics']
|
|
267
|
-
* };
|
|
268
|
-
* };
|
|
269
|
-
*
|
|
270
|
-
* const api = defineServe<AppAuth>({
|
|
271
|
-
* auth: useAuth(jwtStrategy),
|
|
272
|
-
* // ...
|
|
273
|
-
* });
|
|
274
|
-
* ```
|
|
275
314
|
*/
|
|
276
315
|
useAuth: (strategy) => strategy,
|
|
277
316
|
/**
|
|
278
317
|
* The combined typed auth context type.
|
|
279
|
-
* Use this to type your
|
|
280
|
-
*
|
|
281
|
-
* @example
|
|
282
|
-
* ```ts
|
|
283
|
-
* type AppAuth = typeof TypedAuth;
|
|
284
|
-
* ```
|
|
318
|
+
* Use this to type your initServe generic parameter.
|
|
285
319
|
*/
|
|
286
320
|
TypedAuth: null,
|
|
287
321
|
};
|
package/dist/client-config.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ServeBuilder, HttpMethod, AuthContext } from "./types.js";
|
|
|
4
4
|
*/
|
|
5
5
|
export interface QueryClientConfig {
|
|
6
6
|
method: HttpMethod;
|
|
7
|
+
path?: string;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Map of query names to their client configurations
|
|
@@ -36,8 +37,8 @@ export declare function extractClientConfig<TQueries extends Record<string, any>
|
|
|
36
37
|
*
|
|
37
38
|
* @example
|
|
38
39
|
* const config = defineClientConfig({
|
|
39
|
-
* hello: { method: 'GET' },
|
|
40
|
-
* createUser: { method: 'POST' },
|
|
40
|
+
* hello: { method: 'GET', path: '/api/analytics/queries/hello' },
|
|
41
|
+
* createUser: { method: 'POST', path: '/api/analytics/queries/createUser' },
|
|
41
42
|
* });
|
|
42
43
|
*/
|
|
43
44
|
export declare function defineClientConfig<T extends ApiClientConfig>(config: T): T;
|
|
@@ -1 +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;
|
|
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;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;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,CAsB/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,eAAe,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAE1E"}
|
package/dist/client-config.js
CHANGED
|
@@ -25,6 +25,7 @@ export function extractClientConfig(api) {
|
|
|
25
25
|
for (const [key, routeConfig] of Object.entries(api._routeConfig)) {
|
|
26
26
|
config[key] = {
|
|
27
27
|
method: routeConfig.method,
|
|
28
|
+
path: api.queries[key]?.metadata?.path,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -33,6 +34,7 @@ export function extractClientConfig(api) {
|
|
|
33
34
|
for (const [key, endpoint] of Object.entries(api.queries)) {
|
|
34
35
|
config[key] = {
|
|
35
36
|
method: endpoint.method,
|
|
37
|
+
path: endpoint.metadata?.path,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -44,8 +46,8 @@ export function extractClientConfig(api) {
|
|
|
44
46
|
*
|
|
45
47
|
* @example
|
|
46
48
|
* const config = defineClientConfig({
|
|
47
|
-
* hello: { method: 'GET' },
|
|
48
|
-
* createUser: { method: 'POST' },
|
|
49
|
+
* hello: { method: 'GET', path: '/api/analytics/queries/hello' },
|
|
50
|
+
* createUser: { method: 'POST', path: '/api/analytics/queries/createUser' },
|
|
49
51
|
* });
|
|
50
52
|
*/
|
|
51
53
|
export function defineClientConfig(config) {
|
package/dist/errors.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export * from "./utils.js";
|
|
|
14
14
|
export * from "./adapters/node.js";
|
|
15
15
|
export * from "./adapters/fetch.js";
|
|
16
16
|
export * from "./adapters/vercel.js";
|
|
17
|
+
export { startServer, toNodeHandler, toFetchHandler } from "./adapters/standalone.js";
|
|
17
18
|
export * from "./dev.js";
|
|
18
19
|
export * from "./serve.js";
|
|
20
|
+
export * from "./semantic/index.js";
|
|
19
21
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC"}
|
|
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,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AACtF,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -14,5 +14,7 @@ export * from "./utils.js";
|
|
|
14
14
|
export * from "./adapters/node.js";
|
|
15
15
|
export * from "./adapters/fetch.js";
|
|
16
16
|
export * from "./adapters/vercel.js";
|
|
17
|
+
export { startServer, toNodeHandler, toFetchHandler } from "./adapters/standalone.js";
|
|
17
18
|
export * from "./dev.js";
|
|
18
19
|
export * from "./serve.js";
|
|
20
|
+
export * from "./semantic/index.js";
|
package/dist/openapi.js
CHANGED
|
@@ -168,7 +168,6 @@ const normalizeInfo = (options) => {
|
|
|
168
168
|
};
|
|
169
169
|
};
|
|
170
170
|
export const buildOpenApiDocument = (endpoints, options) => {
|
|
171
|
-
var _a;
|
|
172
171
|
const document = {
|
|
173
172
|
openapi: "3.1.0",
|
|
174
173
|
info: normalizeInfo(options),
|
|
@@ -182,7 +181,7 @@ export const buildOpenApiDocument = (endpoints, options) => {
|
|
|
182
181
|
}
|
|
183
182
|
const path = endpoint.metadata.path || "/";
|
|
184
183
|
const method = endpoint.method.toLowerCase();
|
|
185
|
-
const pathItem = (
|
|
184
|
+
const pathItem = (document.paths[path] ??= {});
|
|
186
185
|
const operation = toOperation(endpoint, "Response");
|
|
187
186
|
if (endpoint.metadata.requiresAuth) {
|
|
188
187
|
needsSecurityScheme = true;
|
package/dist/pipeline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAC;AAEzC,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EAKX,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,aAAa,EACb,YAAY,EAGb,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAC;AAEzC,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,WAAW,EAKX,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,aAAa,EACb,YAAY,EAGb,MAAM,YAAY,CAAC;AAMpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAOrD,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,WAAW,CAAC;AA4MvE,MAAM,WAAW,sBAAsB,CACrC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW;IAEzB,QAAQ,EAAE,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IACnD,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;IACtC,cAAc,CAAC,EAAE,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACtD,iBAAiB,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;IAChE,YAAY,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACnC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACtC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,eAAe,GAC1B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,EAEzB,SAAS,sBAAsB,CAAC,QAAQ,EAAE,KAAK,CAAC,KAC/C,OAAO,CAAC,aAAa,CAqTvB,CAAC;AAEF,UAAU,cAAc,CACtB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW;IAEzB,MAAM,EAAE,OAAO,aAAa,EAAE,WAAW,CAAC;IAC1C,iBAAiB,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;IAChE,cAAc,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;IACtC,YAAY,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC,cAAc,CAAC,EAAE,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACnC,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACxC;AAED,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,SAAS,WAAW,EACzB,iIAUC,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAG,YA0CpC,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,cAAc,MAAM,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,EACvD,UAAU,cAAc;;;;;;;;;;;;;;;;;;;;;CA8BzB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,MAAM,EACZ,aAAa,MAAM,EACnB,UAAU,WAAW;;;;;;;;;;;;;;;;;;;;;;;;CAyBmD,CAAC"}
|
package/dist/pipeline.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { warnTenantMisconfiguration } from './tenant.js';
|
|
3
|
+
import { applySemanticTenantRuntime } from './semantic/utils/tenant-runtime.js';
|
|
3
4
|
import { generateRequestId } from './utils.js';
|
|
4
5
|
import { buildOpenApiDocument } from './openapi.js';
|
|
5
6
|
import { buildDocsHtml } from './docs-ui.js';
|
|
@@ -259,7 +260,7 @@ export const executeEndpoint = async (options) => {
|
|
|
259
260
|
durationMs: Date.now() - startedAt,
|
|
260
261
|
error: new Error(errorMessage),
|
|
261
262
|
});
|
|
262
|
-
return createErrorResponse(403, '
|
|
263
|
+
return createErrorResponse(403, 'FORBIDDEN', errorMessage, {
|
|
263
264
|
reason: 'missing_tenant_context',
|
|
264
265
|
tenant_required: true,
|
|
265
266
|
}, { 'x-request-id': requestId });
|
|
@@ -268,26 +269,13 @@ export const executeEndpoint = async (options) => {
|
|
|
268
269
|
context.tenantId = tenantId;
|
|
269
270
|
const mode = activeTenantConfig.mode ?? 'manual';
|
|
270
271
|
const column = activeTenantConfig.column;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
column,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
else if (mode === 'manual') {
|
|
284
|
-
warnTenantMisconfiguration({
|
|
285
|
-
queryKey: endpoint.key,
|
|
286
|
-
hasTenantConfig: true,
|
|
287
|
-
hasTenantId: true,
|
|
288
|
-
mode: 'manual',
|
|
289
|
-
});
|
|
290
|
-
}
|
|
272
|
+
applySemanticTenantRuntime(context, {
|
|
273
|
+
queryKey: endpoint.key,
|
|
274
|
+
metadata: metadataWithAuth,
|
|
275
|
+
tenantId,
|
|
276
|
+
mode,
|
|
277
|
+
column,
|
|
278
|
+
});
|
|
291
279
|
}
|
|
292
280
|
else if (!tenantRequired) {
|
|
293
281
|
warnTenantMisconfiguration({
|
package/dist/query-logger.js
CHANGED
package/dist/rate-limit.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export class MemoryRateLimitStore {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
entries = new Map();
|
|
3
|
+
cleanupTimer;
|
|
4
|
+
constructor(cleanupIntervalMs = 60_000) {
|
|
4
5
|
// Periodic cleanup of expired entries to prevent memory leaks
|
|
5
6
|
this.cleanupTimer = setInterval(() => {
|
|
6
7
|
const now = Date.now();
|
|
@@ -72,7 +73,7 @@ const defaultKeyExtractor = (ctx) => {
|
|
|
72
73
|
* ```
|
|
73
74
|
*/
|
|
74
75
|
export const rateLimit = (config = {}) => {
|
|
75
|
-
const windowMs = config.windowMs ??
|
|
76
|
+
const windowMs = config.windowMs ?? 60_000;
|
|
76
77
|
const max = config.max ?? 100;
|
|
77
78
|
const keyBy = config.keyBy ?? defaultKeyExtractor;
|
|
78
79
|
const store = config.store ?? new MemoryRateLimitStore();
|
package/dist/router.js
CHANGED
|
@@ -11,9 +11,10 @@ export const applyBasePath = (basePath, path) => {
|
|
|
11
11
|
return combined.replace(/\/+/g, "/").replace(/\/$/, combined === "/" ? "/" : "");
|
|
12
12
|
};
|
|
13
13
|
export class ServeRouter {
|
|
14
|
+
basePath;
|
|
15
|
+
routes = [];
|
|
14
16
|
constructor(basePath = "") {
|
|
15
17
|
this.basePath = basePath;
|
|
16
|
-
this.routes = [];
|
|
17
18
|
}
|
|
18
19
|
list() {
|
|
19
20
|
return [...this.routes];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a DatasetInstance into a standard ServeEndpoint for semantic queries.
|
|
3
|
+
*
|
|
4
|
+
* The generated endpoint is a POST handler that:
|
|
5
|
+
* - Accepts dimensions, measures, filters, orderBy, limit, offset, by
|
|
6
|
+
* - Validates requested dimensions/measures/filters against the dataset contract
|
|
7
|
+
* - Executes via QueryBuilderFactoryLike
|
|
8
|
+
* - Applies Serve-managed tenant filtering when configured
|
|
9
|
+
* - Returns { data } or { data, meta } based on headers
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import type { AuthContext, ServeEndpoint } from '../../types.js';
|
|
13
|
+
import type { QueryBuilderFactoryLike } from '@hypequery/datasets';
|
|
14
|
+
import { type DatasetEntry } from './utils/dataset-entry.js';
|
|
15
|
+
export type { DatasetEntry } from './utils/dataset-entry.js';
|
|
16
|
+
declare const datasetResultSchema: z.ZodObject<{
|
|
17
|
+
data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
|
|
18
|
+
meta: z.ZodOptional<z.ZodObject<{
|
|
19
|
+
timingMs: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
sql: z.ZodOptional<z.ZodString>;
|
|
21
|
+
tenant: z.ZodOptional<z.ZodString>;
|
|
22
|
+
rowCount: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
pagination: z.ZodOptional<z.ZodObject<{
|
|
24
|
+
limit: z.ZodNumber;
|
|
25
|
+
offset: z.ZodNumber;
|
|
26
|
+
hasMore: z.ZodBoolean;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
limit: number;
|
|
29
|
+
offset: number;
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
}, {
|
|
32
|
+
limit: number;
|
|
33
|
+
offset: number;
|
|
34
|
+
hasMore: boolean;
|
|
35
|
+
}>>;
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
tenant?: string | undefined;
|
|
38
|
+
timingMs?: number | undefined;
|
|
39
|
+
sql?: string | undefined;
|
|
40
|
+
rowCount?: number | undefined;
|
|
41
|
+
pagination?: {
|
|
42
|
+
limit: number;
|
|
43
|
+
offset: number;
|
|
44
|
+
hasMore: boolean;
|
|
45
|
+
} | undefined;
|
|
46
|
+
}, {
|
|
47
|
+
tenant?: string | undefined;
|
|
48
|
+
timingMs?: number | undefined;
|
|
49
|
+
sql?: string | undefined;
|
|
50
|
+
rowCount?: number | undefined;
|
|
51
|
+
pagination?: {
|
|
52
|
+
limit: number;
|
|
53
|
+
offset: number;
|
|
54
|
+
hasMore: boolean;
|
|
55
|
+
} | undefined;
|
|
56
|
+
}>>;
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
data: Record<string, unknown>[];
|
|
59
|
+
meta?: {
|
|
60
|
+
tenant?: string | undefined;
|
|
61
|
+
timingMs?: number | undefined;
|
|
62
|
+
sql?: string | undefined;
|
|
63
|
+
rowCount?: number | undefined;
|
|
64
|
+
pagination?: {
|
|
65
|
+
limit: number;
|
|
66
|
+
offset: number;
|
|
67
|
+
hasMore: boolean;
|
|
68
|
+
} | undefined;
|
|
69
|
+
} | undefined;
|
|
70
|
+
}, {
|
|
71
|
+
data: Record<string, unknown>[];
|
|
72
|
+
meta?: {
|
|
73
|
+
tenant?: string | undefined;
|
|
74
|
+
timingMs?: number | undefined;
|
|
75
|
+
sql?: string | undefined;
|
|
76
|
+
rowCount?: number | undefined;
|
|
77
|
+
pagination?: {
|
|
78
|
+
limit: number;
|
|
79
|
+
offset: number;
|
|
80
|
+
hasMore: boolean;
|
|
81
|
+
} | undefined;
|
|
82
|
+
} | undefined;
|
|
83
|
+
}>;
|
|
84
|
+
export declare function createDatasetEndpoint<TAuth extends AuthContext>(name: string, entry: DatasetEntry<TAuth>, builderFactory: QueryBuilderFactoryLike): ServeEndpoint<z.ZodTypeAny, typeof datasetResultSchema, any, TAuth, any>;
|
|
85
|
+
//# sourceMappingURL=dataset-endpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-endpoint.d.ts","sourceRoot":"","sources":["../../../src/semantic/datasets/dataset-endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EACV,WAAW,EAGX,aAAa,EAEd,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EACV,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAY7B,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGlF,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAkB7D,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGvB,CAAC;AAMH,wBAAgB,qBAAqB,CAAC,KAAK,SAAS,WAAW,EAC7D,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,EAC1B,cAAc,EAAE,uBAAuB,GACtC,aAAa,CAAC,CAAC,CAAC,UAAU,EAAE,OAAO,mBAAmB,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAoG1E"}
|