@beignet/core 0.0.47 → 0.0.49
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/CHANGELOG.md +33 -0
- package/README.md +81 -15
- package/dist/client/client.d.ts +0 -2
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +28 -25
- package/dist/client/client.js.map +1 -1
- package/dist/contracts/contract-builder.d.ts +7 -2
- package/dist/contracts/contract-builder.d.ts.map +1 -1
- package/dist/contracts/contract-builder.js +20 -2
- package/dist/contracts/contract-builder.js.map +1 -1
- package/dist/contracts/contract-group.d.ts.map +1 -1
- package/dist/contracts/contract-group.js +1 -0
- package/dist/contracts/contract-group.js.map +1 -1
- package/dist/contracts/contract-like.d.ts +2 -0
- package/dist/contracts/contract-like.d.ts.map +1 -1
- package/dist/contracts/contract-like.js +27 -1
- package/dist/contracts/contract-like.js.map +1 -1
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +4 -0
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/query-transport.d.ts +126 -0
- package/dist/contracts/query-transport.d.ts.map +1 -0
- package/dist/contracts/query-transport.js +406 -0
- package/dist/contracts/query-transport.js.map +1 -0
- package/dist/contracts/schema-shape.d.ts +11 -0
- package/dist/contracts/schema-shape.d.ts.map +1 -1
- package/dist/contracts/schema-shape.js +13 -0
- package/dist/contracts/schema-shape.js.map +1 -1
- package/dist/contracts/types.d.ts +5 -0
- package/dist/contracts/types.d.ts.map +1 -1
- package/dist/contracts/types.js.map +1 -1
- package/dist/openapi/index.d.ts +8 -0
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +79 -5
- package/dist/openapi/index.js.map +1 -1
- package/dist/outbox/index.d.ts +8 -5
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +17 -3
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/cache.d.ts +9 -1
- package/dist/ports/cache.d.ts.map +1 -1
- package/dist/ports/cache.js +20 -5
- package/dist/ports/cache.js.map +1 -1
- package/dist/ports/index.d.ts +1 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +1 -1
- package/dist/ports/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js +14 -2
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/providers/metadata.d.ts +2 -0
- package/dist/providers/metadata.d.ts.map +1 -1
- package/dist/providers/metadata.js +78 -16
- package/dist/providers/metadata.js.map +1 -1
- package/dist/query-codec.d.ts +24 -0
- package/dist/query-codec.d.ts.map +1 -1
- package/dist/query-codec.js +207 -72
- package/dist/query-codec.js.map +1 -1
- package/dist/server/hooks/rate-limit.d.ts +16 -8
- package/dist/server/hooks/rate-limit.d.ts.map +1 -1
- package/dist/server/hooks/rate-limit.js +31 -17
- package/dist/server/hooks/rate-limit.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +9 -9
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/request-preparation.d.ts.map +1 -1
- package/dist/server/request-preparation.js +11 -9
- package/dist/server/request-preparation.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +2 -0
- package/dist/server/server.js.map +1 -1
- package/dist/server/use-case-route.d.ts +8 -5
- package/dist/server/use-case-route.d.ts.map +1 -1
- package/dist/server/use-case-route.js +44 -17
- package/dist/server/use-case-route.js.map +1 -1
- package/package.json +3 -3
- package/skills/app-architecture/SKILL.md +27 -1
- package/src/client/client.ts +29 -28
- package/src/contracts/contract-builder.ts +32 -2
- package/src/contracts/contract-group.ts +1 -0
- package/src/contracts/contract-like.ts +40 -1
- package/src/contracts/index.ts +23 -0
- package/src/contracts/query-transport.ts +697 -0
- package/src/contracts/schema-shape.ts +24 -0
- package/src/contracts/types.ts +5 -0
- package/src/openapi/index.ts +126 -2
- package/src/outbox/index.ts +26 -5
- package/src/ports/cache.ts +29 -7
- package/src/ports/index.ts +1 -1
- package/src/providers/instrumentation.ts +15 -2
- package/src/providers/metadata.ts +132 -16
- package/src/server/hooks/rate-limit.ts +50 -24
- package/src/server/request-executor.ts +17 -9
- package/src/server/request-preparation.ts +22 -9
- package/src/server/server.ts +2 -0
- package/src/server/use-case-route.ts +62 -23
- package/src/query-codec.ts +0 -130
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import type { RateLimitScope } from "../../contracts/index.js";
|
|
6
6
|
import { AppError, httpErrors } from "../../errors/index.js";
|
|
7
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
type ActivityActor,
|
|
9
|
+
AuthUnauthorizedError,
|
|
10
|
+
type RateLimitPort,
|
|
11
|
+
} from "../../ports/index.js";
|
|
8
12
|
import {
|
|
9
13
|
createProviderInstrumentation,
|
|
10
14
|
type ProviderInstrumentationTarget,
|
|
@@ -38,8 +42,9 @@ type EarlyRateLimitScope = Exclude<RateLimitScope, "user">;
|
|
|
38
42
|
* Strategy for resolving the client IP used by `ip`-scoped limits.
|
|
39
43
|
*
|
|
40
44
|
* - `"none"`: do not trust request headers for IP resolution. Every request
|
|
41
|
-
*
|
|
42
|
-
* that declare `ip` scopes without a trusted
|
|
45
|
+
* to one contract shares that contract's unknown-client bucket; this is the
|
|
46
|
+
* explicit opt-out for apps that declare `ip` scopes without a trusted
|
|
47
|
+
* client-IP source.
|
|
43
48
|
* - `"x-forwarded-for-last"`: the last `x-forwarded-for` entry. Use this only
|
|
44
49
|
* when the app is always behind a trusted reverse proxy that appends the
|
|
45
50
|
* socket address.
|
|
@@ -59,7 +64,9 @@ export interface RateLimitOptions<Ctx> {
|
|
|
59
64
|
/**
|
|
60
65
|
* Build a rate-limit key after context exists.
|
|
61
66
|
*
|
|
62
|
-
* This is used for user-scoped limits and any late key strategy.
|
|
67
|
+
* This is used for user-scoped limits and any late key strategy. The
|
|
68
|
+
* returned value is the complete key and is not automatically namespaced by
|
|
69
|
+
* contract.
|
|
63
70
|
*/
|
|
64
71
|
key?: (args: {
|
|
65
72
|
ctx: Ctx;
|
|
@@ -69,7 +76,8 @@ export interface RateLimitOptions<Ctx> {
|
|
|
69
76
|
/**
|
|
70
77
|
* Build a rate-limit key before request parsing and context creation.
|
|
71
78
|
*
|
|
72
|
-
* This is used for global and IP-scoped limits.
|
|
79
|
+
* This is used for global and IP-scoped limits. The returned value is the
|
|
80
|
+
* complete key and is not automatically namespaced by contract.
|
|
73
81
|
*/
|
|
74
82
|
earlyKey?: (args: {
|
|
75
83
|
req: HttpRequestLike;
|
|
@@ -124,16 +132,23 @@ function ipSourceConfigurationError(contractNames: string): Error {
|
|
|
124
132
|
`createRateLimitHooks(...) has no client IP source configured, but contract(s) ${contractNames} declare an "ip"-scoped rate limit. ` +
|
|
125
133
|
`Set trustedProxy.clientIp or ipSource to a header source written by a trusted edge, ` +
|
|
126
134
|
`for example "x-forwarded-for-last", "x-forwarded-for-first", "x-real-ip", "cf-connecting-ip", or a custom function, ` +
|
|
127
|
-
`or "none" to explicitly accept one shared
|
|
135
|
+
`or "none" to explicitly accept one shared unknown-client bucket per contract.`,
|
|
128
136
|
);
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
function
|
|
132
|
-
|
|
139
|
+
function namespaceContractKey(contractName: string, key: string): string {
|
|
140
|
+
const encodedContractName = contractName
|
|
141
|
+
.replaceAll("%", "%25")
|
|
142
|
+
.replaceAll(":", "%3A");
|
|
143
|
+
return `contract:${encodedContractName}:${key}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function emitUserKey(contractName: string, userId: string): string {
|
|
147
|
+
return namespaceContractKey(contractName, `user:${userId}`);
|
|
133
148
|
}
|
|
134
149
|
|
|
135
|
-
function emitIpKey(ip: string): string {
|
|
136
|
-
return `ip:${ip}
|
|
150
|
+
function emitIpKey(contractName: string, ip: string): string {
|
|
151
|
+
return namespaceContractKey(contractName, `ip:${ip}`);
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
function defaultRateLimitKey<Ctx extends CtxWithRateLimit>(
|
|
@@ -141,36 +156,41 @@ function defaultRateLimitKey<Ctx extends CtxWithRateLimit>(
|
|
|
141
156
|
ctx: Ctx;
|
|
142
157
|
req: HttpRequestLike;
|
|
143
158
|
scope: RateLimitScope;
|
|
159
|
+
contractName: string;
|
|
144
160
|
},
|
|
145
161
|
getClientIp: (req: HttpRequestLike) => string | undefined,
|
|
146
162
|
): string {
|
|
147
|
-
const { ctx, req, scope } = args;
|
|
163
|
+
const { contractName, ctx, req, scope } = args;
|
|
148
164
|
|
|
149
|
-
if (scope === "user"
|
|
150
|
-
|
|
165
|
+
if (scope === "user") {
|
|
166
|
+
if (ctx.actor?.type !== "user" || !ctx.actor.id) {
|
|
167
|
+
throw new AuthUnauthorizedError();
|
|
168
|
+
}
|
|
169
|
+
return emitUserKey(contractName, ctx.actor.id);
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
if (scope === "ip") {
|
|
154
173
|
const ip = getClientIp(req) || "unknown";
|
|
155
|
-
return emitIpKey(ip);
|
|
174
|
+
return emitIpKey(contractName, ip);
|
|
156
175
|
}
|
|
157
176
|
|
|
158
|
-
return "global";
|
|
177
|
+
return namespaceContractKey(contractName, "global");
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
function defaultEarlyRateLimitKey(
|
|
162
181
|
args: {
|
|
163
182
|
req: HttpRequestLike;
|
|
164
183
|
scope: EarlyRateLimitScope;
|
|
184
|
+
contractName: string;
|
|
165
185
|
},
|
|
166
186
|
getClientIp: (req: HttpRequestLike) => string | undefined,
|
|
167
187
|
): string {
|
|
168
188
|
if (args.scope === "ip") {
|
|
169
189
|
const ip = getClientIp(args.req) || "unknown";
|
|
170
|
-
return emitIpKey(ip);
|
|
190
|
+
return emitIpKey(args.contractName, ip);
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
return "global";
|
|
193
|
+
return namespaceContractKey(args.contractName, "global");
|
|
174
194
|
}
|
|
175
195
|
|
|
176
196
|
async function enforceRateLimit(
|
|
@@ -233,7 +253,10 @@ async function enforceRateLimit(
|
|
|
233
253
|
* The hook reads `contract.metadata.rateLimit`. Global and IP-scoped limits run
|
|
234
254
|
* in `onRequest` before context creation; user-scoped limits run in
|
|
235
255
|
* `beforeHandle` after route hooks have resolved identity and `ctx.actor` is
|
|
236
|
-
* available.
|
|
256
|
+
* available. Default keys include the contract name so unrelated contracts do
|
|
257
|
+
* not share counters. A user-scoped limit without a resolved user actor fails
|
|
258
|
+
* with `AuthUnauthorizedError` instead of falling back to a global bucket.
|
|
259
|
+
* Exceeded limits throw the framework `TooManyRequests` app error
|
|
237
260
|
* with `scope`, `retryAfterSeconds`, and `resetAt` details, and the 429
|
|
238
261
|
* response carries a `Retry-After` header when the limiter reports a reset
|
|
239
262
|
* time. The bucket key is
|
|
@@ -243,11 +266,12 @@ async function enforceRateLimit(
|
|
|
243
266
|
* `ip`-scoped limits require an explicit `trustedProxy.clientIp`, `ipSource`,
|
|
244
267
|
* or custom `earlyKey`: the hook's `validate` phase fails `createServer(...)`
|
|
245
268
|
* startup when a registered contract declares an `ip` scope without one,
|
|
246
|
-
* instead of silently collapsing all clients into
|
|
269
|
+
* instead of silently collapsing all clients into one shared bucket.
|
|
247
270
|
* Contracts added later through `server.route(...)` are not visible to
|
|
248
271
|
* `validate`, so enforcing an `ip`-scoped limit without a client-IP source
|
|
249
272
|
* throws the same configuration error at request time as a backstop. Pass
|
|
250
|
-
* `ipSource: "none"` to explicitly opt in to
|
|
273
|
+
* `ipSource: "none"` to explicitly opt in to one unknown-client bucket per
|
|
274
|
+
* contract.
|
|
251
275
|
*
|
|
252
276
|
* @param options - Optional key builders and client-IP source.
|
|
253
277
|
* @returns A server hook backed by `ctx.ports.rateLimit`.
|
|
@@ -309,8 +333,9 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
309
333
|
|
|
310
334
|
const key =
|
|
311
335
|
options.earlyKey?.({ req, scope }) ??
|
|
312
|
-
defaultEarlyRateLimitKey(
|
|
313
|
-
|
|
336
|
+
defaultEarlyRateLimitKey(
|
|
337
|
+
{ req, scope, contractName: contract.name },
|
|
338
|
+
(r) => getClientIp(r, requestInfo, contract.name),
|
|
314
339
|
);
|
|
315
340
|
|
|
316
341
|
await enforceRateLimit(ports, {
|
|
@@ -335,8 +360,9 @@ export function createRateLimitHooks<Ctx extends CtxWithRateLimit>(
|
|
|
335
360
|
|
|
336
361
|
const key =
|
|
337
362
|
options.key?.({ ctx, req, scope }) ??
|
|
338
|
-
defaultRateLimitKey(
|
|
339
|
-
|
|
363
|
+
defaultRateLimitKey(
|
|
364
|
+
{ ctx, req, scope, contractName: contract.name },
|
|
365
|
+
(r) => getClientIp(r, requestInfo, contract.name),
|
|
340
366
|
);
|
|
341
367
|
|
|
342
368
|
await enforceRateLimit(ctx.ports, {
|
|
@@ -949,17 +949,25 @@ export function createRequestExecutor<
|
|
|
949
949
|
error,
|
|
950
950
|
owner: "framework",
|
|
951
951
|
};
|
|
952
|
-
finalResponse = normalizeHttpResponse(result.response);
|
|
953
|
-
finalError = result.error;
|
|
954
|
-
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
955
|
-
result = await applyTransformHooks(result, true);
|
|
956
|
-
finalResponse = normalizeHttpResponse(result.response);
|
|
957
|
-
finalError = result.error;
|
|
958
|
-
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
959
|
-
responseValidation = "not-applicable";
|
|
960
952
|
} else {
|
|
961
|
-
|
|
953
|
+
result = await resolveErrorResult(
|
|
954
|
+
error,
|
|
955
|
+
result.ctx,
|
|
956
|
+
pathValue,
|
|
957
|
+
queryValue,
|
|
958
|
+
headersValue,
|
|
959
|
+
bodyValue,
|
|
960
|
+
{ owner: "framework" },
|
|
961
|
+
);
|
|
962
962
|
}
|
|
963
|
+
finalResponse = normalizeHttpResponse(result.response);
|
|
964
|
+
finalError = result.error;
|
|
965
|
+
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
966
|
+
result = await applyTransformHooks(result, true);
|
|
967
|
+
finalResponse = normalizeHttpResponse(result.response);
|
|
968
|
+
finalError = result.error;
|
|
969
|
+
finalOwner = responseOwnerFor(finalResponse, result.owner);
|
|
970
|
+
responseValidation = "not-applicable";
|
|
963
971
|
}
|
|
964
972
|
}
|
|
965
973
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
decodeQueryTransport,
|
|
2
3
|
getContractHeaderSchemas,
|
|
3
4
|
type HttpContractConfig,
|
|
4
5
|
methodSupportsRequestBody,
|
|
6
|
+
QueryTransportError,
|
|
5
7
|
type StandardSchema,
|
|
6
8
|
} from "../contracts/index.js";
|
|
7
|
-
import { decodeQueryValue } from "../query-codec.js";
|
|
8
9
|
import type { HttpRequestLike, HttpResponseLike } from "./http.js";
|
|
9
10
|
import {
|
|
10
11
|
parseStandardSchema,
|
|
@@ -82,7 +83,10 @@ function requestValidationDetails(
|
|
|
82
83
|
...additionalDetails,
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
if (
|
|
86
|
+
if (
|
|
87
|
+
error instanceof SchemaValidationError ||
|
|
88
|
+
error instanceof QueryTransportError
|
|
89
|
+
) {
|
|
86
90
|
return {
|
|
87
91
|
...details,
|
|
88
92
|
issues: error.issues,
|
|
@@ -282,17 +286,26 @@ export async function prepareRequestInputs(args: {
|
|
|
282
286
|
maxRequestBodyBytes,
|
|
283
287
|
rawRoute,
|
|
284
288
|
} = args;
|
|
285
|
-
const rawQuery
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
const rawQuery = Object.fromEntries(
|
|
290
|
+
[...new Set(url.searchParams.keys())].map((key) => {
|
|
291
|
+
const values = url.searchParams.getAll(key);
|
|
292
|
+
return [key, values.length === 1 ? values[0] : values];
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
291
295
|
|
|
292
296
|
let query: unknown = rawQuery;
|
|
293
297
|
if (contract.query) {
|
|
298
|
+
if (!contract.queryTransport) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Contract "${contract.name}" declares a query schema without a query transport.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
294
303
|
try {
|
|
295
|
-
|
|
304
|
+
const decoded = decodeQueryTransport(
|
|
305
|
+
contract.queryTransport,
|
|
306
|
+
url.searchParams,
|
|
307
|
+
);
|
|
308
|
+
query = await parseStandardSchema(contract.query, decoded);
|
|
296
309
|
} catch (error) {
|
|
297
310
|
return {
|
|
298
311
|
ok: false,
|
package/src/server/server.ts
CHANGED
|
@@ -807,6 +807,7 @@ export async function createServer<
|
|
|
807
807
|
path,
|
|
808
808
|
pathParams: null,
|
|
809
809
|
query: null,
|
|
810
|
+
queryTransport: null,
|
|
810
811
|
body: null,
|
|
811
812
|
responses: {},
|
|
812
813
|
metadata: {},
|
|
@@ -929,6 +930,7 @@ function rawRouteContract(init: RawRouteInit): HttpContractConfig {
|
|
|
929
930
|
path: init.path,
|
|
930
931
|
pathParams: null,
|
|
931
932
|
query: null,
|
|
933
|
+
queryTransport: null,
|
|
932
934
|
body: null,
|
|
933
935
|
responses: {},
|
|
934
936
|
metadata: init.metadata ?? {},
|
|
@@ -192,8 +192,10 @@ type UseCaseRouteShape<
|
|
|
192
192
|
/**
|
|
193
193
|
* Map parsed request parts to the use case input.
|
|
194
194
|
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
195
|
+
* A sole declared path, query, or body schema is passed through unchanged
|
|
196
|
+
* when no additional path, query, or object body values are present.
|
|
197
|
+
* Otherwise `defaultBinderInput` merges query, body, and path objects (path
|
|
198
|
+
* wins collisions) and never merges headers.
|
|
197
199
|
*/
|
|
198
200
|
input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
199
201
|
handle?: never;
|
|
@@ -330,9 +332,10 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
330
332
|
* Merges parsed query, body, and path objects into one input object. Path
|
|
331
333
|
* keys win all collisions, then body keys, then query keys. Headers are never
|
|
332
334
|
* merged: parsed headers include every raw request header, so merging them
|
|
333
|
-
* would poison the use case input.
|
|
334
|
-
*
|
|
335
|
-
*
|
|
335
|
+
* would poison the use case input. The route binder passes a sole declared
|
|
336
|
+
* input schema through unchanged when no other object input contains values;
|
|
337
|
+
* this merge handles every other default mapping. Routes that combine a
|
|
338
|
+
* non-object body with another source declare an explicit `input` mapper.
|
|
336
339
|
*/
|
|
337
340
|
export function defaultBinderInput(parts: {
|
|
338
341
|
path: unknown;
|
|
@@ -356,16 +359,42 @@ export function isUseCaseRouteDef(route: {
|
|
|
356
359
|
return route.useCase !== undefined && route.useCase !== null;
|
|
357
360
|
}
|
|
358
361
|
|
|
359
|
-
function
|
|
362
|
+
function computeSingleInput(
|
|
360
363
|
contract: HttpContractConfig,
|
|
361
364
|
def: RuntimeUseCaseRouteDef,
|
|
362
|
-
):
|
|
363
|
-
if (def.input) return
|
|
365
|
+
): { source: "path" | "query" | "body"; schema: unknown } | undefined {
|
|
366
|
+
if (def.input) return undefined;
|
|
367
|
+
|
|
368
|
+
const sources = [
|
|
369
|
+
{ source: "path", schema: contract.pathParams },
|
|
370
|
+
{
|
|
371
|
+
source: "query",
|
|
372
|
+
schema: contract.query,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
source: "body",
|
|
376
|
+
schema: contract.body,
|
|
377
|
+
},
|
|
378
|
+
] as const;
|
|
379
|
+
const present = sources.filter(
|
|
380
|
+
(candidate) => candidate.schema !== null && candidate.schema !== undefined,
|
|
381
|
+
);
|
|
382
|
+
const single = present[0];
|
|
383
|
+
return present.length === 1 && single
|
|
384
|
+
? { source: single.source, schema: single.schema }
|
|
385
|
+
: undefined;
|
|
386
|
+
}
|
|
364
387
|
|
|
365
|
-
|
|
366
|
-
|
|
388
|
+
function canPassSingleInput(
|
|
389
|
+
parts: { path: unknown; query: unknown; body: unknown },
|
|
390
|
+
source: "path" | "query" | "body",
|
|
391
|
+
): boolean {
|
|
392
|
+
return (["path", "query", "body"] as const).every(
|
|
393
|
+
(candidate) =>
|
|
394
|
+
candidate === source ||
|
|
395
|
+
!isPlainObject(parts[candidate]) ||
|
|
396
|
+
Object.keys(parts[candidate]).length === 0,
|
|
367
397
|
);
|
|
368
|
-
return sources.length === 1 && sources[0] === def.useCase.inputSchema;
|
|
369
398
|
}
|
|
370
399
|
|
|
371
400
|
function computeResponseExemption(
|
|
@@ -405,11 +434,11 @@ export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
|
|
|
405
434
|
);
|
|
406
435
|
}
|
|
407
436
|
|
|
408
|
-
const
|
|
409
|
-
const trustedRun =
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
437
|
+
const singleInput = computeSingleInput(contract, def);
|
|
438
|
+
const trustedRun =
|
|
439
|
+
singleInput?.schema === def.useCase.inputSchema
|
|
440
|
+
? def.useCase[USE_CASE_TRUSTED_RUN_KEY]
|
|
441
|
+
: undefined;
|
|
413
442
|
|
|
414
443
|
const handler: Handler<Ctx, C> = async ({
|
|
415
444
|
ctx,
|
|
@@ -417,13 +446,23 @@ export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
|
|
|
417
446
|
query,
|
|
418
447
|
headers,
|
|
419
448
|
body,
|
|
420
|
-
}) =>
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
449
|
+
}) => {
|
|
450
|
+
const parts = { path, query, headers, body };
|
|
451
|
+
const passSingle =
|
|
452
|
+
singleInput !== undefined &&
|
|
453
|
+
canPassSingleInput(parts, singleInput.source);
|
|
454
|
+
const input = def.input
|
|
455
|
+
? def.input(parts)
|
|
456
|
+
: passSingle
|
|
457
|
+
? parts[singleInput.source]
|
|
458
|
+
: defaultBinderInput(parts);
|
|
459
|
+
const run = passSingle && trustedRun ? trustedRun : def.useCase.run;
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
status,
|
|
463
|
+
body: await run.call(def.useCase, { ctx, input }),
|
|
464
|
+
};
|
|
465
|
+
};
|
|
427
466
|
|
|
428
467
|
return {
|
|
429
468
|
handler,
|
package/src/query-codec.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
const QUERY_VALUE_PREFIX = "~beignet-query:";
|
|
2
|
-
|
|
3
|
-
type EncodedQueryValue =
|
|
4
|
-
| { type: "null" }
|
|
5
|
-
| { type: "string"; value: string }
|
|
6
|
-
| { type: "number"; value: number }
|
|
7
|
-
| { type: "boolean"; value: boolean }
|
|
8
|
-
| { type: "date"; value: string }
|
|
9
|
-
| { type: "array"; value: EncodedQueryValue[] }
|
|
10
|
-
| { type: "object"; value: Record<string, EncodedQueryValue> };
|
|
11
|
-
|
|
12
|
-
function encodeStructuredValue(
|
|
13
|
-
value: unknown,
|
|
14
|
-
ancestors: Set<object>,
|
|
15
|
-
): EncodedQueryValue {
|
|
16
|
-
if (value === null) return { type: "null" };
|
|
17
|
-
if (typeof value === "string") return { type: "string", value };
|
|
18
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
19
|
-
return { type: "number", value };
|
|
20
|
-
}
|
|
21
|
-
if (typeof value === "boolean") return { type: "boolean", value };
|
|
22
|
-
if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
23
|
-
return { type: "date", value: value.toISOString() };
|
|
24
|
-
}
|
|
25
|
-
if (typeof value !== "object" || value === null) {
|
|
26
|
-
throw new TypeError("Query value is not serializable.");
|
|
27
|
-
}
|
|
28
|
-
if (ancestors.has(value)) {
|
|
29
|
-
throw new TypeError("Query value contains a circular reference.");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
ancestors.add(value);
|
|
33
|
-
try {
|
|
34
|
-
if (Array.isArray(value)) {
|
|
35
|
-
return {
|
|
36
|
-
type: "array",
|
|
37
|
-
value: value.map((entry) => encodeStructuredValue(entry, ancestors)),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const prototype = Object.getPrototypeOf(value);
|
|
42
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
43
|
-
throw new TypeError("Query value contains a non-plain object.");
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
type: "object",
|
|
47
|
-
value: Object.fromEntries(
|
|
48
|
-
Object.entries(value)
|
|
49
|
-
.filter(([, entry]) => entry !== undefined)
|
|
50
|
-
.map(([key, entry]) => [
|
|
51
|
-
key,
|
|
52
|
-
encodeStructuredValue(entry, ancestors),
|
|
53
|
-
]),
|
|
54
|
-
),
|
|
55
|
-
};
|
|
56
|
-
} finally {
|
|
57
|
-
ancestors.delete(value);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function decodeStructuredValue(value: unknown): unknown {
|
|
62
|
-
if (typeof value !== "object" || value === null || !("type" in value)) {
|
|
63
|
-
throw new TypeError("Invalid encoded query value.");
|
|
64
|
-
}
|
|
65
|
-
const encoded = value as { type: unknown; value?: unknown };
|
|
66
|
-
|
|
67
|
-
switch (encoded.type) {
|
|
68
|
-
case "null":
|
|
69
|
-
return null;
|
|
70
|
-
case "string":
|
|
71
|
-
if (typeof encoded.value === "string") return encoded.value;
|
|
72
|
-
break;
|
|
73
|
-
case "number":
|
|
74
|
-
if (typeof encoded.value === "number" && Number.isFinite(encoded.value)) {
|
|
75
|
-
return encoded.value;
|
|
76
|
-
}
|
|
77
|
-
break;
|
|
78
|
-
case "boolean":
|
|
79
|
-
if (typeof encoded.value === "boolean") return encoded.value;
|
|
80
|
-
break;
|
|
81
|
-
case "date": {
|
|
82
|
-
if (typeof encoded.value !== "string") break;
|
|
83
|
-
const date = new Date(encoded.value);
|
|
84
|
-
if (Number.isFinite(date.getTime())) return date;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
case "array":
|
|
88
|
-
if (Array.isArray(encoded.value)) {
|
|
89
|
-
return encoded.value.map(decodeStructuredValue);
|
|
90
|
-
}
|
|
91
|
-
break;
|
|
92
|
-
case "object":
|
|
93
|
-
if (
|
|
94
|
-
typeof encoded.value === "object" &&
|
|
95
|
-
encoded.value !== null &&
|
|
96
|
-
!Array.isArray(encoded.value)
|
|
97
|
-
) {
|
|
98
|
-
return Object.fromEntries(
|
|
99
|
-
Object.entries(encoded.value).map(([key, entry]) => [
|
|
100
|
-
key,
|
|
101
|
-
decodeStructuredValue(entry),
|
|
102
|
-
]),
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
throw new TypeError("Invalid encoded query value.");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function encodeQueryValue(value: unknown): string {
|
|
112
|
-
if (typeof value === "string" && !value.startsWith(QUERY_VALUE_PREFIX)) {
|
|
113
|
-
return value;
|
|
114
|
-
}
|
|
115
|
-
return `${QUERY_VALUE_PREFIX}${JSON.stringify(
|
|
116
|
-
encodeStructuredValue(value, new Set()),
|
|
117
|
-
)}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function decodeQueryValue(value: string): unknown {
|
|
121
|
-
if (!value.startsWith(QUERY_VALUE_PREFIX)) return value;
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
return decodeStructuredValue(
|
|
125
|
-
JSON.parse(value.slice(QUERY_VALUE_PREFIX.length)) as unknown,
|
|
126
|
-
);
|
|
127
|
-
} catch {
|
|
128
|
-
return value;
|
|
129
|
-
}
|
|
130
|
-
}
|