@beignet/core 0.0.51 → 0.0.53
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 +27 -0
- package/README.md +112 -20
- package/dist/application/index.d.ts +1 -1
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +5 -2
- package/dist/application/index.js.map +1 -1
- package/dist/encryption/index.d.ts +40 -0
- package/dist/encryption/index.d.ts.map +1 -0
- package/dist/encryption/index.js +134 -0
- package/dist/encryption/index.js.map +1 -0
- package/dist/events/index.d.ts +26 -2
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +84 -10
- package/dist/events/index.js.map +1 -1
- package/dist/events/payload-state.d.ts +14 -2
- package/dist/events/payload-state.d.ts.map +1 -1
- package/dist/events/payload-state.js +116 -4
- package/dist/events/payload-state.js.map +1 -1
- package/dist/events/transport.d.ts +25 -0
- package/dist/events/transport.d.ts.map +1 -0
- package/dist/events/transport.js +192 -0
- package/dist/events/transport.js.map +1 -0
- package/dist/openapi/index.d.ts.map +1 -1
- package/dist/openapi/index.js +27 -4
- package/dist/openapi/index.js.map +1 -1
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +10 -13
- package/dist/outbox/index.js.map +1 -1
- package/dist/ports/events.d.ts +4 -1
- package/dist/ports/events.d.ts.map +1 -1
- package/dist/ports/storage.d.ts +7 -0
- package/dist/ports/storage.d.ts.map +1 -1
- package/dist/ports/storage.js +4 -0
- package/dist/ports/storage.js.map +1 -1
- package/dist/ports/testing.d.ts +3 -2
- package/dist/ports/testing.d.ts.map +1 -1
- package/dist/ports/testing.js +7 -4
- package/dist/ports/testing.js.map +1 -1
- package/dist/ports/unit-of-work.d.ts +4 -4
- package/dist/ports/unit-of-work.d.ts.map +1 -1
- package/dist/ports/unit-of-work.js +8 -9
- package/dist/ports/unit-of-work.js.map +1 -1
- package/dist/search/index.js +2 -2
- package/dist/search/index.js.map +1 -1
- package/dist/server/instrumentation.d.ts +5 -5
- package/dist/server/instrumentation.d.ts.map +1 -1
- package/dist/server/instrumentation.js +3 -4
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/request-executor.d.ts.map +1 -1
- package/dist/server/request-executor.js +13 -0
- package/dist/server/request-executor.js.map +1 -1
- package/dist/server/response-finalization.d.ts.map +1 -1
- package/dist/server/response-finalization.js +25 -13
- package/dist/server/response-finalization.js.map +1 -1
- package/dist/server/server.d.ts +8 -5
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +1 -3
- package/dist/server/server.js.map +1 -1
- package/dist/server/use-case-route.d.ts +82 -17
- package/dist/server/use-case-route.d.ts.map +1 -1
- package/dist/server/use-case-route.js +40 -4
- package/dist/server/use-case-route.js.map +1 -1
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +37 -16
- package/dist/uploads/index.js.map +1 -1
- package/package.json +6 -2
- package/skills/app-architecture/SKILL.md +29 -6
- package/src/application/index.ts +24 -3
- package/src/encryption/index.ts +198 -0
- package/src/events/index.ts +137 -10
- package/src/events/payload-state.ts +205 -6
- package/src/events/transport.ts +242 -0
- package/src/openapi/index.ts +41 -3
- package/src/outbox/index.ts +26 -18
- package/src/ports/events.ts +4 -1
- package/src/ports/storage.ts +10 -0
- package/src/ports/testing.ts +11 -4
- package/src/ports/unit-of-work.ts +20 -16
- package/src/search/index.ts +2 -2
- package/src/server/instrumentation.ts +10 -8
- package/src/server/request-executor.ts +19 -0
- package/src/server/response-finalization.ts +33 -15
- package/src/server/server.ts +9 -8
- package/src/server/use-case-route.ts +234 -27
- package/src/uploads/index.ts +34 -16
|
@@ -312,18 +312,18 @@ function getDeclaredCatalogErrorsForStatus(
|
|
|
312
312
|
);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
async function
|
|
315
|
+
async function parseCatalogErrorResponse<C extends HttpContractConfig>(
|
|
316
316
|
contract: C,
|
|
317
317
|
res: HttpResponseLike,
|
|
318
|
-
): Promise<
|
|
318
|
+
): Promise<HttpResponseLike> {
|
|
319
319
|
const body = res.body;
|
|
320
|
-
if (res.status < 400 || !isErrorResponseBody(body)) return;
|
|
320
|
+
if (res.status < 400 || !isErrorResponseBody(body)) return res;
|
|
321
321
|
|
|
322
322
|
const declaredErrors = getDeclaredCatalogErrorsForStatus(
|
|
323
323
|
contract,
|
|
324
324
|
res.status,
|
|
325
325
|
);
|
|
326
|
-
if (declaredErrors.length === 0) return;
|
|
326
|
+
if (declaredErrors.length === 0) return res;
|
|
327
327
|
|
|
328
328
|
const matchingError = declaredErrors.find(
|
|
329
329
|
(error) => error.code === body.code,
|
|
@@ -346,7 +346,18 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
346
346
|
|
|
347
347
|
if (matchingError.details && body.details !== undefined) {
|
|
348
348
|
try {
|
|
349
|
-
await parseStandardSchema(
|
|
349
|
+
const parsedDetails = await parseStandardSchema(
|
|
350
|
+
matchingError.details,
|
|
351
|
+
body.details,
|
|
352
|
+
);
|
|
353
|
+
const { details: _details, ...bodyWithoutDetails } = body;
|
|
354
|
+
return {
|
|
355
|
+
...res,
|
|
356
|
+
body:
|
|
357
|
+
parsedDetails === undefined
|
|
358
|
+
? bodyWithoutDetails
|
|
359
|
+
: { ...bodyWithoutDetails, details: parsedDetails },
|
|
360
|
+
};
|
|
350
361
|
} catch (error) {
|
|
351
362
|
if (error instanceof SchemaValidationError) {
|
|
352
363
|
throw new ResponseContractViolationError({
|
|
@@ -360,18 +371,20 @@ async function validateCatalogErrorResponse<C extends HttpContractConfig>(
|
|
|
360
371
|
throw error;
|
|
361
372
|
}
|
|
362
373
|
}
|
|
374
|
+
|
|
375
|
+
return res;
|
|
363
376
|
}
|
|
364
377
|
|
|
365
|
-
async function
|
|
378
|
+
async function parseResponseAgainstContract<C extends HttpContractConfig>(
|
|
366
379
|
contract: C,
|
|
367
380
|
res: HttpResponseLike,
|
|
368
381
|
responseValidationExemptStatus?: number,
|
|
369
|
-
): Promise<
|
|
382
|
+
): Promise<HttpResponseLike> {
|
|
370
383
|
const statusKey = String(res.status);
|
|
371
384
|
const hasDeclaredStatus = Object.hasOwn(contract.responses, statusKey);
|
|
372
385
|
|
|
373
386
|
if (!hasDeclaredStatus) {
|
|
374
|
-
if (Object.keys(contract.responses).length === 0) return;
|
|
387
|
+
if (Object.keys(contract.responses).length === 0) return res;
|
|
375
388
|
|
|
376
389
|
throw new ResponseContractViolationError({
|
|
377
390
|
code: "UNDECLARED_RESPONSE_STATUS",
|
|
@@ -400,19 +413,22 @@ async function validateResponseAgainstContract<C extends HttpContractConfig>(
|
|
|
400
413
|
}),
|
|
401
414
|
});
|
|
402
415
|
}
|
|
403
|
-
return;
|
|
416
|
+
return res;
|
|
404
417
|
}
|
|
405
418
|
|
|
406
|
-
if (!responseSchema) return;
|
|
419
|
+
if (!responseSchema) return res;
|
|
407
420
|
|
|
408
421
|
// Binder routes whose use case output schema is the same object as the
|
|
409
422
|
// declared success response schema skip the redundant success-status parse.
|
|
410
423
|
// Error statuses and undeclared statuses are validated unchanged.
|
|
411
|
-
if (res.status === responseValidationExemptStatus) return;
|
|
424
|
+
if (res.status === responseValidationExemptStatus) return res;
|
|
412
425
|
|
|
413
426
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
427
|
+
const parsed = {
|
|
428
|
+
...res,
|
|
429
|
+
body: await parseStandardSchema(responseSchema, res.body),
|
|
430
|
+
};
|
|
431
|
+
return await parseCatalogErrorResponse(contract, parsed);
|
|
416
432
|
} catch (error) {
|
|
417
433
|
if (error instanceof SchemaValidationError) {
|
|
418
434
|
throw new ResponseContractViolationError({
|
|
@@ -463,7 +479,7 @@ export async function finalizeResponse<C extends HttpContractConfig>(
|
|
|
463
479
|
const normalized = normalizeResponse(res);
|
|
464
480
|
validateHttpResponseSemantics(contract, normalized);
|
|
465
481
|
if (options.validateContract ?? true) {
|
|
466
|
-
|
|
482
|
+
return parseResponseAgainstContract(
|
|
467
483
|
contract,
|
|
468
484
|
normalized,
|
|
469
485
|
responseValidationExemptStatus,
|
|
@@ -490,6 +506,8 @@ export function defaultErrorResponse(
|
|
|
490
506
|
ctx?: unknown,
|
|
491
507
|
): HttpResponseLike {
|
|
492
508
|
const requestId = getRequestIdFromContext(ctx);
|
|
509
|
+
const exposeErrorDetails =
|
|
510
|
+
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
493
511
|
return {
|
|
494
512
|
status: 500,
|
|
495
513
|
body: createErrorResponseBody({
|
|
@@ -497,7 +515,7 @@ export function defaultErrorResponse(
|
|
|
497
515
|
message: "Internal server error",
|
|
498
516
|
requestId,
|
|
499
517
|
details:
|
|
500
|
-
|
|
518
|
+
exposeErrorDetails && err instanceof Error
|
|
501
519
|
? {
|
|
502
520
|
error: {
|
|
503
521
|
message: err.message,
|
package/src/server/server.ts
CHANGED
|
@@ -204,15 +204,18 @@ export type CreateServerOptions<
|
|
|
204
204
|
* `ports.devtools`) when one is installed.
|
|
205
205
|
*
|
|
206
206
|
* Pass `false` to disable headers and event recording. Context factories
|
|
207
|
-
* still receive `requestId` and `trace` arguments
|
|
207
|
+
* still receive `requestId` and `trace` arguments, and request-scoped
|
|
208
|
+
* correlation remains available to ambient audit wrappers.
|
|
208
209
|
*/
|
|
209
210
|
instrumentation?: ServerInstrumentationOptions<Ctx> | false;
|
|
210
211
|
/**
|
|
211
|
-
* Whether route-owned responses are
|
|
212
|
-
*
|
|
212
|
+
* Whether route-owned responses are parsed against the contract's declared
|
|
213
|
+
* statuses and response schemas before they are sent. The parsed schema
|
|
214
|
+
* output becomes the response body.
|
|
213
215
|
*
|
|
214
|
-
* Disable this to
|
|
215
|
-
* client-side
|
|
216
|
+
* Disable this to send route-owned handler bodies as-is without validation,
|
|
217
|
+
* unknown-key stripping, or transforms, mirroring the client-side
|
|
218
|
+
* `validateResponses` option.
|
|
216
219
|
*
|
|
217
220
|
* @default true
|
|
218
221
|
*/
|
|
@@ -407,9 +410,7 @@ export async function createServer<
|
|
|
407
410
|
options.instrumentation,
|
|
408
411
|
);
|
|
409
412
|
const hooks = [
|
|
410
|
-
|
|
411
|
-
? [instrumentation.hook as ServerHook<Ctx, FinalPorts>]
|
|
412
|
-
: []),
|
|
413
|
+
instrumentation.hook as ServerHook<Ctx, FinalPorts>,
|
|
413
414
|
...((options.hooks ?? []) as ServerHook<Ctx, FinalPorts>[]),
|
|
414
415
|
];
|
|
415
416
|
const contracts = options.routes ? contractsFromRoutes(options.routes) : [];
|
|
@@ -149,20 +149,143 @@ export type UseCaseRouteInputParts<C extends HttpContractConfig> = {
|
|
|
149
149
|
body: InferBody<C>;
|
|
150
150
|
};
|
|
151
151
|
|
|
152
|
+
type SegmentPathParam<Segment extends string> = Segment extends `:${infer Name}`
|
|
153
|
+
? Name
|
|
154
|
+
: Segment extends `[${infer Name}]`
|
|
155
|
+
? Name
|
|
156
|
+
: never;
|
|
157
|
+
|
|
158
|
+
type PathParamNames<Path extends string> = string extends Path
|
|
159
|
+
? never
|
|
160
|
+
: Path extends `${infer Segment}/${infer Rest}`
|
|
161
|
+
? SegmentPathParam<Segment> | PathParamNames<Rest>
|
|
162
|
+
: SegmentPathParam<Path>;
|
|
163
|
+
|
|
164
|
+
type EmptyBinderInput = Record<never, never>;
|
|
165
|
+
|
|
166
|
+
declare const UNMERGEABLE_BINDER_INPUT: unique symbol;
|
|
167
|
+
|
|
168
|
+
type UnmergeableBinderInput = {
|
|
169
|
+
readonly [UNMERGEABLE_BINDER_INPUT]: true;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
173
|
+
|
|
174
|
+
type BinderObject<T> =
|
|
175
|
+
IsAny<T> extends true
|
|
176
|
+
? UnmergeableBinderInput
|
|
177
|
+
: [T] extends [object]
|
|
178
|
+
? [Extract<T, readonly unknown[]>] extends [never]
|
|
179
|
+
? T
|
|
180
|
+
: UnmergeableBinderInput
|
|
181
|
+
: UnmergeableBinderInput;
|
|
182
|
+
|
|
183
|
+
type MergeBinderObjects<LowerPrecedence, HigherPrecedence> = [
|
|
184
|
+
BinderObject<LowerPrecedence>,
|
|
185
|
+
] extends [UnmergeableBinderInput]
|
|
186
|
+
? UnmergeableBinderInput
|
|
187
|
+
: [BinderObject<HigherPrecedence>] extends [UnmergeableBinderInput]
|
|
188
|
+
? UnmergeableBinderInput
|
|
189
|
+
: Omit<
|
|
190
|
+
BinderObject<LowerPrecedence>,
|
|
191
|
+
keyof BinderObject<HigherPrecedence>
|
|
192
|
+
> &
|
|
193
|
+
BinderObject<HigherPrecedence>;
|
|
194
|
+
|
|
195
|
+
type InferredPathInput<C extends HttpContractConfig> = string extends C["path"]
|
|
196
|
+
? UnmergeableBinderInput
|
|
197
|
+
: [PathParamNames<C["path"]>] extends [never]
|
|
198
|
+
? EmptyBinderInput
|
|
199
|
+
: { [K in PathParamNames<C["path"]>]: string };
|
|
200
|
+
|
|
201
|
+
type BinderPathInput<C extends HttpContractConfig> =
|
|
202
|
+
C["pathParams"] extends StandardSchemaV1
|
|
203
|
+
? InferOutput<C["pathParams"]>
|
|
204
|
+
: InferredPathInput<C>;
|
|
205
|
+
|
|
206
|
+
type BinderQueryInput<C extends HttpContractConfig> =
|
|
207
|
+
C["query"] extends StandardSchemaV1
|
|
208
|
+
? InferOutput<C["query"]>
|
|
209
|
+
: EmptyBinderInput;
|
|
210
|
+
|
|
211
|
+
type BinderBodyInput<C extends HttpContractConfig> =
|
|
212
|
+
C["body"] extends StandardSchemaV1
|
|
213
|
+
? InferOutput<C["body"]>
|
|
214
|
+
: EmptyBinderInput;
|
|
215
|
+
|
|
216
|
+
type HasPathSchema<C extends HttpContractConfig> =
|
|
217
|
+
C["pathParams"] extends StandardSchemaV1 ? true : false;
|
|
218
|
+
|
|
219
|
+
type HasQuerySchema<C extends HttpContractConfig> =
|
|
220
|
+
C["query"] extends StandardSchemaV1 ? true : false;
|
|
221
|
+
|
|
222
|
+
type HasBodySchema<C extends HttpContractConfig> =
|
|
223
|
+
C["body"] extends StandardSchemaV1 ? true : false;
|
|
224
|
+
|
|
225
|
+
type HasInferredPathInput<C extends HttpContractConfig> =
|
|
226
|
+
string extends C["path"]
|
|
227
|
+
? true
|
|
228
|
+
: [PathParamNames<C["path"]>] extends [never]
|
|
229
|
+
? false
|
|
230
|
+
: true;
|
|
231
|
+
|
|
232
|
+
type MergedBinderInput<C extends HttpContractConfig> = MergeBinderObjects<
|
|
233
|
+
MergeBinderObjects<BinderQueryInput<C>, BinderBodyInput<C>>,
|
|
234
|
+
BinderPathInput<C>
|
|
235
|
+
>;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Input produced when a binder route omits an explicit `input` mapper.
|
|
239
|
+
*
|
|
240
|
+
* A sole declared request schema passes through unchanged when the literal
|
|
241
|
+
* path has no additional inferred parameters. Every other supported default
|
|
242
|
+
* binding merges object inputs with path over body over query precedence.
|
|
243
|
+
*/
|
|
244
|
+
type DefaultBinderRouteInput<C extends HttpContractConfig> =
|
|
245
|
+
HasPathSchema<C> extends true
|
|
246
|
+
? HasQuerySchema<C> extends true
|
|
247
|
+
? MergedBinderInput<C>
|
|
248
|
+
: HasBodySchema<C> extends true
|
|
249
|
+
? MergedBinderInput<C>
|
|
250
|
+
: BinderPathInput<C>
|
|
251
|
+
: HasQuerySchema<C> extends true
|
|
252
|
+
? HasBodySchema<C> extends true
|
|
253
|
+
? MergedBinderInput<C>
|
|
254
|
+
: HasInferredPathInput<C> extends true
|
|
255
|
+
? MergedBinderInput<C>
|
|
256
|
+
: BinderQueryInput<C>
|
|
257
|
+
: HasBodySchema<C> extends true
|
|
258
|
+
? HasInferredPathInput<C> extends true
|
|
259
|
+
? MergedBinderInput<C>
|
|
260
|
+
: BinderBodyInput<C>
|
|
261
|
+
: BinderPathInput<C>;
|
|
262
|
+
|
|
263
|
+
type UseCaseRouteInputMode = "default" | "mapped";
|
|
264
|
+
|
|
152
265
|
/**
|
|
153
266
|
* Constraint that checks a use case against the route that binds it.
|
|
154
267
|
*
|
|
155
268
|
* Produces a readable branded mismatch object on the `useCase` property when
|
|
156
|
-
* the use case requires a context the server does not provide,
|
|
157
|
-
* output does not match the contract's declared success response schema
|
|
269
|
+
* the use case requires a context the server does not provide, when its
|
|
270
|
+
* output does not match the contract's declared success response schema, or
|
|
271
|
+
* when the default binder input does not satisfy the use case input.
|
|
158
272
|
*/
|
|
159
|
-
export type UseCaseFitsRoute<
|
|
273
|
+
export type UseCaseFitsRoute<
|
|
160
274
|
Ctx,
|
|
161
|
-
|
|
275
|
+
C extends HttpContractConfig,
|
|
276
|
+
UC,
|
|
277
|
+
InputMode extends UseCaseRouteInputMode = "default",
|
|
278
|
+
> = [Ctx] extends [UseCaseRouteCtx<UC>]
|
|
162
279
|
? [UseCaseRouteOutput<UC>] extends [
|
|
163
280
|
SuccessBodyFromKeys<C["responses"], Success2xxKeys<C["responses"]>>,
|
|
164
281
|
]
|
|
165
|
-
?
|
|
282
|
+
? InputMode extends "mapped"
|
|
283
|
+
? unknown
|
|
284
|
+
: [DefaultBinderRouteInput<C>] extends [UseCaseRouteInput<UC>]
|
|
285
|
+
? unknown
|
|
286
|
+
: {
|
|
287
|
+
"~beignetError": "default binder input does not match the use case input; add an input mapper";
|
|
288
|
+
}
|
|
166
289
|
: {
|
|
167
290
|
"~beignetError": "useCase output does not match the contract's success response schema";
|
|
168
291
|
}
|
|
@@ -185,21 +308,34 @@ type UseCaseRouteShape<
|
|
|
185
308
|
* Route-scoped hooks that run after group hooks and before the use case.
|
|
186
309
|
*/
|
|
187
310
|
hooks?: Hooks;
|
|
188
|
-
/**
|
|
189
|
-
* Use case bound directly to the contract.
|
|
190
|
-
*/
|
|
191
|
-
useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
|
|
192
|
-
/**
|
|
193
|
-
* Map parsed request parts to the use case input.
|
|
194
|
-
*
|
|
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.
|
|
199
|
-
*/
|
|
200
|
-
input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
201
311
|
handle?: never;
|
|
202
|
-
} &
|
|
312
|
+
} & (
|
|
313
|
+
| {
|
|
314
|
+
/**
|
|
315
|
+
* Use case bound directly to the contract. The default binder input
|
|
316
|
+
* must satisfy the use case input type.
|
|
317
|
+
*/
|
|
318
|
+
useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC>;
|
|
319
|
+
input?: never;
|
|
320
|
+
}
|
|
321
|
+
| {
|
|
322
|
+
/**
|
|
323
|
+
* Use case bound directly to the contract through an explicit input
|
|
324
|
+
* mapper.
|
|
325
|
+
*/
|
|
326
|
+
useCase: UC & UseCaseFitsRoute<HandlerCtx, C, UC, "mapped">;
|
|
327
|
+
/**
|
|
328
|
+
* Map parsed request parts to the use case input.
|
|
329
|
+
*
|
|
330
|
+
* A sole declared path, query, or body schema is passed through
|
|
331
|
+
* unchanged when no additional path, query, or object body values are
|
|
332
|
+
* present. Otherwise `defaultBinderInput` merges query, body, and path
|
|
333
|
+
* objects (path wins collisions) and never merges headers.
|
|
334
|
+
*/
|
|
335
|
+
input: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
336
|
+
}
|
|
337
|
+
) &
|
|
338
|
+
BinderStatusOption<C>;
|
|
203
339
|
|
|
204
340
|
/**
|
|
205
341
|
* Route registration that binds a contract directly to a use case.
|
|
@@ -271,11 +407,25 @@ export type ValidatedRouteInput<Ctx, E> = E extends {
|
|
|
271
407
|
? {
|
|
272
408
|
contract: CL;
|
|
273
409
|
hooks?: HooksOf<E>;
|
|
274
|
-
useCase: UC &
|
|
275
|
-
UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
|
|
276
|
-
input?: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
277
410
|
handle?: never;
|
|
278
|
-
} &
|
|
411
|
+
} & (
|
|
412
|
+
| {
|
|
413
|
+
useCase: UC &
|
|
414
|
+
UseCaseFitsRoute<Ctx & AddedCtxFromHooks<HooksOf<E>>, C, UC>;
|
|
415
|
+
input?: never;
|
|
416
|
+
}
|
|
417
|
+
| {
|
|
418
|
+
useCase: UC &
|
|
419
|
+
UseCaseFitsRoute<
|
|
420
|
+
Ctx & AddedCtxFromHooks<HooksOf<E>>,
|
|
421
|
+
C,
|
|
422
|
+
UC,
|
|
423
|
+
"mapped"
|
|
424
|
+
>;
|
|
425
|
+
input: (parts: UseCaseRouteInputParts<C>) => UseCaseRouteInput<UC>;
|
|
426
|
+
}
|
|
427
|
+
) &
|
|
428
|
+
BinderStatusOption<C>
|
|
279
429
|
: unknown
|
|
280
430
|
: unknown;
|
|
281
431
|
|
|
@@ -322,6 +472,49 @@ export type RuntimeUseCaseRouteDef = {
|
|
|
322
472
|
status?: number;
|
|
323
473
|
};
|
|
324
474
|
|
|
475
|
+
type UseCaseInputValidationFailure = Error & {
|
|
476
|
+
name: "UseCaseValidationError";
|
|
477
|
+
phase: "input";
|
|
478
|
+
useCaseName: string;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
function isUseCaseInputValidationFailure(
|
|
482
|
+
error: unknown,
|
|
483
|
+
useCaseName: string,
|
|
484
|
+
): error is UseCaseInputValidationFailure {
|
|
485
|
+
if (!(error instanceof Error)) return false;
|
|
486
|
+
const candidate = error as Partial<UseCaseInputValidationFailure>;
|
|
487
|
+
return (
|
|
488
|
+
candidate.name === "UseCaseValidationError" &&
|
|
489
|
+
candidate.phase === "input" &&
|
|
490
|
+
candidate.useCaseName === useCaseName
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Internal framework error raised when a type-erased binder route produces an
|
|
496
|
+
* input that the bound use case rejects.
|
|
497
|
+
*/
|
|
498
|
+
export class UseCaseRouteInputValidationError extends Error {
|
|
499
|
+
readonly code = "USE_CASE_INPUT_VALIDATION_ERROR";
|
|
500
|
+
readonly contractName: string;
|
|
501
|
+
readonly useCaseName: string;
|
|
502
|
+
|
|
503
|
+
constructor(args: {
|
|
504
|
+
contractName: string;
|
|
505
|
+
useCaseName: string;
|
|
506
|
+
cause: UseCaseInputValidationFailure;
|
|
507
|
+
}) {
|
|
508
|
+
super(
|
|
509
|
+
`Default binder input for contract "${args.contractName}" does not satisfy use case "${args.useCaseName}". Add an explicit input mapper.`,
|
|
510
|
+
{ cause: args.cause },
|
|
511
|
+
);
|
|
512
|
+
this.name = "UseCaseRouteInputValidationError";
|
|
513
|
+
this.contractName = args.contractName;
|
|
514
|
+
this.useCaseName = args.useCaseName;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
325
518
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
326
519
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
327
520
|
}
|
|
@@ -458,10 +651,24 @@ export function createUseCaseRouteHandler<Ctx, C extends HttpContractConfig>(
|
|
|
458
651
|
: defaultBinderInput(parts);
|
|
459
652
|
const run = passSingle && trustedRun ? trustedRun : def.useCase.run;
|
|
460
653
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
654
|
+
try {
|
|
655
|
+
return {
|
|
656
|
+
status,
|
|
657
|
+
body: await run.call(def.useCase, { ctx, input }),
|
|
658
|
+
};
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (
|
|
661
|
+
!def.input &&
|
|
662
|
+
isUseCaseInputValidationFailure(error, def.useCase.name)
|
|
663
|
+
) {
|
|
664
|
+
throw new UseCaseRouteInputValidationError({
|
|
665
|
+
contractName: contract.name,
|
|
666
|
+
useCaseName: def.useCase.name,
|
|
667
|
+
cause: error,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
465
672
|
};
|
|
466
673
|
|
|
467
674
|
return {
|
package/src/uploads/index.ts
CHANGED
|
@@ -968,29 +968,47 @@ export function createUploadRouter<Ctx>(
|
|
|
968
968
|
},
|
|
969
969
|
});
|
|
970
970
|
}
|
|
971
|
-
const
|
|
971
|
+
const verifyBody = needsUploadBodyVerification(upload, file);
|
|
972
|
+
const objectBody = verifyBody
|
|
972
973
|
? await options.storage.get(file.key)
|
|
974
|
+
: null;
|
|
975
|
+
const object = verifyBody
|
|
976
|
+
? objectBody
|
|
973
977
|
: await options.storage.stat(file.key);
|
|
974
978
|
if (!object) {
|
|
975
979
|
throw new UploadObjectNotFoundError({
|
|
976
980
|
message: `Uploaded object "${file.key}" was not found.`,
|
|
977
981
|
});
|
|
978
982
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
983
|
+
let operationFailed = false;
|
|
984
|
+
try {
|
|
985
|
+
assertStoredObject(upload, file, object);
|
|
986
|
+
const verified = await verifyStoredUploadFile(upload, file, object);
|
|
987
|
+
const completedObject = storageObjectMetadata(object);
|
|
988
|
+
const completedFile = {
|
|
989
|
+
...file,
|
|
990
|
+
...(verified.checksum ? { checksum: verified.checksum } : {}),
|
|
991
|
+
object: completedObject,
|
|
992
|
+
};
|
|
993
|
+
await assertVerifiedFile(upload, {
|
|
994
|
+
ctx,
|
|
995
|
+
metadata,
|
|
996
|
+
file: completedFile,
|
|
997
|
+
storage: options.storage,
|
|
998
|
+
});
|
|
999
|
+
files.push(completedFile);
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
operationFailed = true;
|
|
1002
|
+
throw error;
|
|
1003
|
+
} finally {
|
|
1004
|
+
if (objectBody && !objectBody.bodyUsed) {
|
|
1005
|
+
if (operationFailed) {
|
|
1006
|
+
await objectBody.cancel().catch(() => undefined);
|
|
1007
|
+
} else {
|
|
1008
|
+
await objectBody.cancel();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
994
1012
|
}
|
|
995
1013
|
|
|
996
1014
|
const result = await upload.onComplete?.({ ctx, metadata, files });
|