@molecule/api-resource 1.0.6 → 1.1.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 +146 -28
- package/dist/createRequestHandler.d.ts +41 -1
- package/dist/createRequestHandler.d.ts.map +1 -1
- package/dist/createRequestHandler.js +39 -15
- package/dist/createRequestHandler.js.map +1 -1
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-
|
|
6
|
+
Generated: 2026-11-06T22:08:33.809Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-resource
|
|
@@ -30,6 +30,33 @@ res.status(statusCode).send(body)
|
|
|
30
30
|
Monkey-patching `res.json` (or `res.send`/`res.end`) to re-shape a handler's answer intercepts
|
|
31
31
|
nothing the handler produces, so the request it was meant to answer is left unanswered and hangs.
|
|
32
32
|
|
|
33
|
+
**A different request or response envelope is configuration.** Pass
|
|
34
|
+
`mapRequest` and/or `mapResponse` as the second argument to `createRequestHandler`.
|
|
35
|
+
Both may be asynchronous. The existing handler still validates and authorizes
|
|
36
|
+
the adapted input. A response mapper sees the handler's errors and partial
|
|
37
|
+
responses too: preserve them and its headers instead of treating every result
|
|
38
|
+
as a success. `adaptHandler(handler, options)` offers the same mappings when
|
|
39
|
+
another HTTP adapter will send the structured response.
|
|
40
|
+
A request mapper can return `{ response: { statusCode, body } }` to answer
|
|
41
|
+
before calling the handler, for example when extra validation fails or a
|
|
42
|
+
filter lookup finds nothing. That response still passes through mapResponse.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { createRequestHandler } from '@molecule/api-resource'
|
|
46
|
+
import { handlers } from '@molecule/api-resource-user'
|
|
47
|
+
|
|
48
|
+
const register = createRequestHandler(handlers.create({ name: 'User', tableName: 'users' }), {
|
|
49
|
+
// Return overrides. The adapter retains framework methods and getters.
|
|
50
|
+
mapRequest: (req) => ({ body: req.body?.account }),
|
|
51
|
+
mapResponse: (result) =>
|
|
52
|
+
result.statusCode === 201
|
|
53
|
+
? { ...result, body: { account: result.body.props, accessToken: result.body.accessToken } }
|
|
54
|
+
: result,
|
|
55
|
+
})
|
|
56
|
+
// Keep the route's existing rate limiter and authorization around register.
|
|
57
|
+
// No mapping is inferred: a flat-body API keeps createRequestHandler(handler).
|
|
58
|
+
```
|
|
59
|
+
|
|
33
60
|
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
34
61
|
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
35
62
|
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
@@ -114,6 +141,38 @@ interface Response {
|
|
|
114
141
|
}
|
|
115
142
|
```
|
|
116
143
|
|
|
144
|
+
#### `RequestHandlerOptions`
|
|
145
|
+
|
|
146
|
+
Per-route wire-format adapters. Validation and authorization still belong to
|
|
147
|
+
the original handler. Omit these options to retain its native contract.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
interface RequestHandlerOptions {
|
|
151
|
+
/**
|
|
152
|
+
* Return the fields to replace before the handler validates the request.
|
|
153
|
+
* The adapter preserves the original request's methods and getters, and does
|
|
154
|
+
* not mutate it. Returning `{ body }` or `{ query }` is sufficient, including
|
|
155
|
+
* frameworks whose request properties have read-only accessors.
|
|
156
|
+
* Return `{ response: { statusCode, headers?, body } }` to answer before the
|
|
157
|
+
* handler runs. That result still goes through mapResponse, with the original request.
|
|
158
|
+
*/
|
|
159
|
+
mapRequest?: (
|
|
160
|
+
req: MoleculeRequest,
|
|
161
|
+
res: MoleculeResponse,
|
|
162
|
+
) => RequestMappingResult | Promise<RequestMappingResult>
|
|
163
|
+
/**
|
|
164
|
+
* Adapt the returned status, headers or body. This sees error and partial
|
|
165
|
+
* responses too: narrow the success shape before changing it, and preserve
|
|
166
|
+
* headers with `{ ...response, body }`. It never runs for fall-through.
|
|
167
|
+
*/
|
|
168
|
+
mapResponse?: (
|
|
169
|
+
response: Response,
|
|
170
|
+
req: MoleculeRequest,
|
|
171
|
+
res: MoleculeResponse,
|
|
172
|
+
) => Response | Promise<Response>
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
117
176
|
#### `MoleculeRequest`
|
|
118
177
|
|
|
119
178
|
Framework-agnostic HTTP request interface. Structurally compatible with Express's
|
|
@@ -258,6 +317,16 @@ type Handler = (
|
|
|
258
317
|
- `req` — The incoming HTTP request.
|
|
259
318
|
- `res` — The HTTP response (available but handlers should return a Response instead of calling `res` directly).
|
|
260
319
|
|
|
320
|
+
#### `RequestMappingResult`
|
|
321
|
+
|
|
322
|
+
Request overrides, or an explicit response that stops before the handler.
|
|
323
|
+
The response form supports extra validation and unresolved external filters
|
|
324
|
+
without writing data or inventing a placeholder identifier.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
type RequestMappingResult = Partial<MoleculeRequest> | { response: Response }
|
|
328
|
+
```
|
|
329
|
+
|
|
261
330
|
#### `MoleculeNextFunction`
|
|
262
331
|
|
|
263
332
|
Framework-agnostic next function. Call with no arguments to pass to the next
|
|
@@ -307,11 +376,14 @@ export const create = ({ name, tableName, schema }: types.Resource) => {
|
|
|
307
376
|
```
|
|
308
377
|
|
|
309
378
|
```typescript
|
|
310
|
-
function create
|
|
379
|
+
function create<
|
|
380
|
+
CreateProps extends Record<string, unknown> = Record<string, unknown>,
|
|
381
|
+
CreatedProps extends types.Props & CreateProps = types.Props & CreateProps,
|
|
382
|
+
>({
|
|
311
383
|
name,
|
|
312
384
|
tableName,
|
|
313
385
|
schema,
|
|
314
|
-
}: types.Resource
|
|
386
|
+
}: types.Resource): ({
|
|
315
387
|
props: createProps,
|
|
316
388
|
id,
|
|
317
389
|
}: {
|
|
@@ -330,7 +402,22 @@ function create({
|
|
|
330
402
|
|
|
331
403
|
**Returns:** A curried async function that accepts `{ props, id? }` and returns a `{ statusCode, body }` response.
|
|
332
404
|
|
|
333
|
-
#### `
|
|
405
|
+
#### `adaptHandler(handler, options)`
|
|
406
|
+
|
|
407
|
+
Compose wire-format adapters around a structured resource handler without
|
|
408
|
+
sending its response. The response mapper receives the adapted request.
|
|
409
|
+
Exceptions propagate to the caller's normal error boundary.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
function adaptHandler(handler: Handler, options?: RequestHandlerOptions): Handler
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
- `handler` — The existing resource handler, including its checks.
|
|
416
|
+
- `options` — Request and response mappings for this route only.
|
|
417
|
+
|
|
418
|
+
**Returns:** A structured handler that keeps null/undefined fall-through intact.
|
|
419
|
+
|
|
420
|
+
#### `createRequestHandler(handler, options)`
|
|
334
421
|
|
|
335
422
|
Wraps a resource handler into Express-compatible middleware. Calls the handler,
|
|
336
423
|
then applies `statusCode`, `headers`, and `body` to the response. If the handler
|
|
@@ -340,10 +427,12 @@ i18n-translated error messages.
|
|
|
340
427
|
```typescript
|
|
341
428
|
function createRequestHandler(
|
|
342
429
|
handler: Handler,
|
|
430
|
+
options?: RequestHandlerOptions,
|
|
343
431
|
): (req: MoleculeRequest, res: MoleculeResponse, next: MoleculeNextFunction) => Promise<void>
|
|
344
432
|
```
|
|
345
433
|
|
|
346
434
|
- `handler` — The resource handler function to wrap.
|
|
435
|
+
- `options` — Optional wire-format mappings; neither mutates the shared handler.
|
|
347
436
|
|
|
348
437
|
**Returns:** An Express-compatible middleware function `(req, res, next) => void`.
|
|
349
438
|
|
|
@@ -372,7 +461,13 @@ export const del = ({ name, tableName, schema }: types.Resource) => {
|
|
|
372
461
|
```
|
|
373
462
|
|
|
374
463
|
```typescript
|
|
375
|
-
function del
|
|
464
|
+
function del<
|
|
465
|
+
Props extends types.Props = types.Props,
|
|
466
|
+
DeletedResource extends types.Resource<Props> = types.Resource<Props>,
|
|
467
|
+
>({
|
|
468
|
+
name,
|
|
469
|
+
tableName,
|
|
470
|
+
}: DeletedResource): ({ id }: { id: Props['id'] }) => Promise<
|
|
376
471
|
| { statusCode: number; body: { error: string; errorKey: string; props?: undefined } }
|
|
377
472
|
| {
|
|
378
473
|
statusCode: number
|
|
@@ -394,7 +489,10 @@ Creates a request handler to be used to query resources.
|
|
|
394
489
|
Uses the bonded DataStore from `@molecule/api-database` for database-agnostic operations.
|
|
395
490
|
|
|
396
491
|
```typescript
|
|
397
|
-
function query
|
|
492
|
+
function query<
|
|
493
|
+
Props extends types.Props = types.Props,
|
|
494
|
+
QueriedResource extends types.Resource = types.Resource,
|
|
495
|
+
>({
|
|
398
496
|
tableName,
|
|
399
497
|
}: QueriedResource): (
|
|
400
498
|
req: MoleculeRequest,
|
|
@@ -432,7 +530,10 @@ export const read = ({ name, tableName, schema }: types.Resource) => {
|
|
|
432
530
|
```
|
|
433
531
|
|
|
434
532
|
```typescript
|
|
435
|
-
function read
|
|
533
|
+
function read<
|
|
534
|
+
ReadProps extends types.Props = types.Props,
|
|
535
|
+
ReadResource extends types.Resource<ReadProps> = types.Resource<ReadProps>,
|
|
536
|
+
>({
|
|
436
537
|
tableName,
|
|
437
538
|
}: ReadResource): ({
|
|
438
539
|
id,
|
|
@@ -504,11 +605,18 @@ export const update = ({ name, tableName, schema }: types.Resource) => {
|
|
|
504
605
|
```
|
|
505
606
|
|
|
506
607
|
```typescript
|
|
507
|
-
function update
|
|
608
|
+
function update<
|
|
609
|
+
UpdateProps extends Record<string, unknown> = Record<string, unknown>,
|
|
610
|
+
UpdatedProps extends Pick<types.Props, `updatedAt`> & UpdateProps = Pick<
|
|
611
|
+
types.Props,
|
|
612
|
+
`updatedAt`
|
|
613
|
+
> &
|
|
614
|
+
UpdateProps,
|
|
615
|
+
>({
|
|
508
616
|
name,
|
|
509
617
|
tableName,
|
|
510
618
|
schema,
|
|
511
|
-
}: types.Resource
|
|
619
|
+
}: types.Resource): ({
|
|
512
620
|
id,
|
|
513
621
|
props: updateProps,
|
|
514
622
|
}: {
|
|
@@ -597,9 +705,7 @@ Creates a Zod schema for the standard `{ statusCode, body }` response
|
|
|
597
705
|
returned by resource CRUD operations.
|
|
598
706
|
|
|
599
707
|
```typescript
|
|
600
|
-
function responseSchema(
|
|
601
|
-
propsSchema: T,
|
|
602
|
-
): z.ZodTypeAny<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>
|
|
708
|
+
function responseSchema<T extends z.ZodTypeAny>(propsSchema: T): z.ZodTypeAny
|
|
603
709
|
```
|
|
604
710
|
|
|
605
711
|
- `propsSchema` — The Zod schema for the resource's props (e.g. `userPropsSchema`).
|
|
@@ -612,9 +718,7 @@ Creates a Zod schema that validates a resource descriptor object
|
|
|
612
718
|
(`{ name, tableName, schema }`).
|
|
613
719
|
|
|
614
720
|
```typescript
|
|
615
|
-
function resourceSchema(
|
|
616
|
-
_propsSchema: T,
|
|
617
|
-
): z.ZodTypeAny<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>
|
|
721
|
+
function resourceSchema<T extends z.ZodTypeAny>(_propsSchema: T): z.ZodTypeAny
|
|
618
722
|
```
|
|
619
723
|
|
|
620
724
|
- `_propsSchema` — The Zod schema for the resource's props (used for type inference only).
|
|
@@ -627,10 +731,7 @@ Validates data against a Zod schema, throwing an `Error` with a
|
|
|
627
731
|
human-readable message listing all validation failures if invalid.
|
|
628
732
|
|
|
629
733
|
```typescript
|
|
630
|
-
function validate(
|
|
631
|
-
schema: z.ZodSchema<T, unknown, z.core.$ZodTypeInternals<T, unknown>>,
|
|
632
|
-
data: unknown,
|
|
633
|
-
): T
|
|
734
|
+
function validate<T>(schema: z.ZodSchema<T>, data: unknown): T
|
|
634
735
|
```
|
|
635
736
|
|
|
636
737
|
- `schema` — The Zod schema to validate against.
|
|
@@ -644,8 +745,8 @@ Validates data against a Zod schema without throwing. Returns a discriminated
|
|
|
644
745
|
union: `{ success: true, data }` on success or `{ success: false, error }` on failure.
|
|
645
746
|
|
|
646
747
|
```typescript
|
|
647
|
-
function safeParse(
|
|
648
|
-
schema: z.ZodSchema<T
|
|
748
|
+
function safeParse<T>(
|
|
749
|
+
schema: z.ZodSchema<T>,
|
|
649
750
|
data: unknown,
|
|
650
751
|
): { success: true; data: T } | { success: false; error: z.ZodError }
|
|
651
752
|
```
|
|
@@ -661,9 +762,9 @@ Creates a schema where all fields are optional, suitable for validating
|
|
|
661
762
|
partial update payloads where only changed fields are sent.
|
|
662
763
|
|
|
663
764
|
```typescript
|
|
664
|
-
function partial(
|
|
665
|
-
schema: z.ZodObject<T
|
|
666
|
-
): z.ZodObject<{ [K in keyof T]: z.ZodOptional<T[K]> }
|
|
765
|
+
function partial<T extends z.ZodRawShape>(
|
|
766
|
+
schema: z.ZodObject<T>,
|
|
767
|
+
): z.ZodObject<{ [K in keyof T]: z.ZodOptional<T[K]> }>
|
|
667
768
|
```
|
|
668
769
|
|
|
669
770
|
- `schema` — The Zod object schema to make partial.
|
|
@@ -676,10 +777,10 @@ Creates a schema with only the specified fields, useful for create payloads
|
|
|
676
777
|
where only a subset of the full props should be provided by the client.
|
|
677
778
|
|
|
678
779
|
```typescript
|
|
679
|
-
function pick(
|
|
680
|
-
schema: z.ZodObject<T
|
|
780
|
+
function pick<T extends z.ZodRawShape, K extends keyof T>(
|
|
781
|
+
schema: z.ZodObject<T>,
|
|
681
782
|
keys: readonly K[],
|
|
682
|
-
): z.ZodObject<Pick<T, K
|
|
783
|
+
): z.ZodObject<Pick<T, K>>
|
|
683
784
|
```
|
|
684
785
|
|
|
685
786
|
- `schema` — The Zod object schema to pick from.
|
|
@@ -839,8 +940,17 @@ const getValidProps: <Props>({
|
|
|
839
940
|
schema,
|
|
840
941
|
props,
|
|
841
942
|
}: {
|
|
943
|
+
/**
|
|
944
|
+
* The name of the resource (used in error messages).
|
|
945
|
+
*/
|
|
842
946
|
name: string
|
|
947
|
+
/**
|
|
948
|
+
* The Zod schema used when validating props.
|
|
949
|
+
*/
|
|
843
950
|
schema: ZodSchema<Props>
|
|
951
|
+
/**
|
|
952
|
+
* The `props` to be validated.
|
|
953
|
+
*/
|
|
844
954
|
props: unknown
|
|
845
955
|
}) => Props
|
|
846
956
|
```
|
|
@@ -860,7 +970,15 @@ Validates data against a Zod schema without throwing, returning a discriminated
|
|
|
860
970
|
const safeParse: <T>(
|
|
861
971
|
schema: ZodSchema<T>,
|
|
862
972
|
data: unknown,
|
|
863
|
-
) =>
|
|
973
|
+
) =>
|
|
974
|
+
| {
|
|
975
|
+
success: true
|
|
976
|
+
data: T
|
|
977
|
+
}
|
|
978
|
+
| {
|
|
979
|
+
success: false
|
|
980
|
+
error: ZodError
|
|
981
|
+
}
|
|
864
982
|
```
|
|
865
983
|
|
|
866
984
|
- `schema` — The Zod schema to validate against.
|
|
@@ -35,6 +35,45 @@ export interface Response {
|
|
|
35
35
|
*/
|
|
36
36
|
body: any;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Request overrides, or an explicit response that stops before the handler.
|
|
40
|
+
* The response form supports extra validation and unresolved external filters
|
|
41
|
+
* without writing data or inventing a placeholder identifier.
|
|
42
|
+
*/
|
|
43
|
+
export type RequestMappingResult = Partial<MoleculeRequest> | {
|
|
44
|
+
response: Response;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Per-route wire-format adapters. Validation and authorization still belong to
|
|
48
|
+
* the original handler. Omit these options to retain its native contract.
|
|
49
|
+
*/
|
|
50
|
+
export interface RequestHandlerOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Return the fields to replace before the handler validates the request.
|
|
53
|
+
* The adapter preserves the original request's methods and getters, and does
|
|
54
|
+
* not mutate it. Returning `{ body }` or `{ query }` is sufficient, including
|
|
55
|
+
* frameworks whose request properties have read-only accessors.
|
|
56
|
+
* Return `{ response: { statusCode, headers?, body } }` to answer before the
|
|
57
|
+
* handler runs. That result still goes through mapResponse, with the original request.
|
|
58
|
+
*/
|
|
59
|
+
mapRequest?: (req: MoleculeRequest, res: MoleculeResponse) => RequestMappingResult | Promise<RequestMappingResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Adapt the returned status, headers or body. This sees error and partial
|
|
62
|
+
* responses too: narrow the success shape before changing it, and preserve
|
|
63
|
+
* headers with `{ ...response, body }`. It never runs for fall-through.
|
|
64
|
+
*/
|
|
65
|
+
mapResponse?: (response: Response, req: MoleculeRequest, res: MoleculeResponse) => Response | Promise<Response>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compose wire-format adapters around a structured resource handler without
|
|
69
|
+
* sending its response. The response mapper receives the adapted request.
|
|
70
|
+
* Exceptions propagate to the caller's normal error boundary.
|
|
71
|
+
*
|
|
72
|
+
* @param handler - The existing resource handler, including its checks.
|
|
73
|
+
* @param options - Request and response mappings for this route only.
|
|
74
|
+
* @returns A structured handler that keeps null/undefined fall-through intact.
|
|
75
|
+
*/
|
|
76
|
+
export declare const adaptHandler: (handler: Handler, options?: RequestHandlerOptions) => Handler;
|
|
38
77
|
/**
|
|
39
78
|
* Wraps a resource handler into Express-compatible middleware. Calls the handler,
|
|
40
79
|
* then applies `statusCode`, `headers`, and `body` to the response. If the handler
|
|
@@ -42,7 +81,8 @@ export interface Response {
|
|
|
42
81
|
* i18n-translated error messages.
|
|
43
82
|
*
|
|
44
83
|
* @param handler - The resource handler function to wrap.
|
|
84
|
+
* @param options - Optional wire-format mappings; neither mutates the shared handler.
|
|
45
85
|
* @returns An Express-compatible middleware function `(req, res, next) => void`.
|
|
46
86
|
*/
|
|
47
|
-
export declare const createRequestHandler: (handler: Handler) => (req: MoleculeRequest, res: MoleculeResponse, next: MoleculeNextFunction) => Promise<void>;
|
|
87
|
+
export declare const createRequestHandler: (handler: Handler, options?: RequestHandlerOptions) => (req: MoleculeRequest, res: MoleculeResponse, next: MoleculeNextFunction) => Promise<void>;
|
|
48
88
|
//# sourceMappingURL=createRequestHandler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createRequestHandler.d.ts","sourceRoot":"","sources":["../src/createRequestHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAI9F;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,GAAG,CACpB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAA;AAEvE;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;OAEG;IACH,IAAI,EAAE,GAAG,CAAA;CACV;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"createRequestHandler.d.ts","sourceRoot":"","sources":["../src/createRequestHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAI9F;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,GAAG,CACpB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,CAAA;AAEvE;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;OAEG;IACH,IAAI,EAAE,GAAG,CAAA;CACV;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAA;AAEpF;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CACX,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,gBAAgB,KAClB,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;IACzD;;;;OAIG;IACH,WAAW,CAAC,EAAE,CACZ,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,gBAAgB,KAClB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAClC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GACtB,SAAS,OAAO,EAAE,UAAS,qBAA0B,KAAG,OAYxD,CAAA;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,OAAO,EAAE,UAAS,qBAA0B,MAE1E,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,kBAmBtF,CAAA"}
|
|
@@ -11,6 +11,26 @@
|
|
|
11
11
|
import { getLogger } from '@molecule/api-bond';
|
|
12
12
|
const logger = getLogger();
|
|
13
13
|
import { t } from '@molecule/api-i18n';
|
|
14
|
+
/**
|
|
15
|
+
* Compose wire-format adapters around a structured resource handler without
|
|
16
|
+
* sending its response. The response mapper receives the adapted request.
|
|
17
|
+
* Exceptions propagate to the caller's normal error boundary.
|
|
18
|
+
*
|
|
19
|
+
* @param handler - The existing resource handler, including its checks.
|
|
20
|
+
* @param options - Request and response mappings for this route only.
|
|
21
|
+
* @returns A structured handler that keeps null/undefined fall-through intact.
|
|
22
|
+
*/
|
|
23
|
+
export const adaptHandler = (handler, options = {}) => async (req, res) => {
|
|
24
|
+
const overrides = options.mapRequest ? await options.mapRequest(req, res) : undefined;
|
|
25
|
+
const earlyResponse = overrides && 'response' in overrides ? overrides.response : undefined;
|
|
26
|
+
const request = overrides && !earlyResponse
|
|
27
|
+
? Object.create(req, Object.getOwnPropertyDescriptors(overrides))
|
|
28
|
+
: req;
|
|
29
|
+
const response = earlyResponse ?? (await handler(request, res));
|
|
30
|
+
return response && options.mapResponse
|
|
31
|
+
? await options.mapResponse(response, request, res)
|
|
32
|
+
: response;
|
|
33
|
+
};
|
|
14
34
|
/**
|
|
15
35
|
* Wraps a resource handler into Express-compatible middleware. Calls the handler,
|
|
16
36
|
* then applies `statusCode`, `headers`, and `body` to the response. If the handler
|
|
@@ -18,25 +38,29 @@ import { t } from '@molecule/api-i18n';
|
|
|
18
38
|
* i18n-translated error messages.
|
|
19
39
|
*
|
|
20
40
|
* @param handler - The resource handler function to wrap.
|
|
41
|
+
* @param options - Optional wire-format mappings; neither mutates the shared handler.
|
|
21
42
|
* @returns An Express-compatible middleware function `(req, res, next) => void`.
|
|
22
43
|
*/
|
|
23
|
-
export const createRequestHandler = (handler
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
export const createRequestHandler = (handler, options = {}) => {
|
|
45
|
+
const adapted = adaptHandler(handler, options);
|
|
46
|
+
return async (req, res, next) => {
|
|
47
|
+
try {
|
|
48
|
+
const response = await adapted(req, res);
|
|
49
|
+
if (response) {
|
|
50
|
+
if (response.headers) {
|
|
51
|
+
res.set(response.headers);
|
|
52
|
+
}
|
|
53
|
+
res.status(response.statusCode);
|
|
54
|
+
res.send(response.body);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
next();
|
|
29
58
|
}
|
|
30
|
-
res.status(response.statusCode);
|
|
31
|
-
res.send(response.body);
|
|
32
59
|
}
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
catch (error) {
|
|
61
|
+
logger.error(error);
|
|
62
|
+
next(t('resource.error.unknownError', undefined, { defaultValue: 'Something went wrong.' }));
|
|
35
63
|
}
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
logger.error(error);
|
|
39
|
-
next(t('resource.error.unknownError', undefined, { defaultValue: 'Something went wrong.' }));
|
|
40
|
-
}
|
|
64
|
+
};
|
|
41
65
|
};
|
|
42
66
|
//# sourceMappingURL=createRequestHandler.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createRequestHandler.js","sourceRoot":"","sources":["../src/createRequestHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uDAAuD;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAG9C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"createRequestHandler.js","sourceRoot":"","sources":["../src/createRequestHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uDAAuD;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAG9C,MAAM,MAAM,GAAG,SAAS,EAAE,CAAA;AAC1B,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAA;AAsEtC;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GACvB,CAAC,OAAgB,EAAE,UAAiC,EAAE,EAAW,EAAE,CACnE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACjB,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACrF,MAAM,aAAa,GAAG,SAAS,IAAI,UAAU,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAA;IAC3F,MAAM,OAAO,GACX,SAAS,IAAI,CAAC,aAAa;QACzB,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,yBAAyB,CAAC,SAAS,CAAC,CAAC;QACjE,CAAC,CAAC,GAAG,CAAA;IACT,MAAM,QAAQ,GAAG,aAAa,IAAI,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAA;IAC/D,OAAO,QAAQ,IAAI,OAAO,CAAC,WAAW;QACpC,CAAC,CAAC,MAAM,OAAO,CAAC,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,CAAC;QACnD,CAAC,CAAC,QAAQ,CAAA;AACd,CAAC,CAAA;AAEH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,OAAgB,EAAE,UAAiC,EAAE,EAAE,EAAE;IAC5F,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAC9C,OAAO,KAAK,EAAE,GAAoB,EAAE,GAAqB,EAAE,IAA0B,EAAE,EAAE;QACvF,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;YAExC,IAAI,QAAQ,EAAE,CAAC;gBACb,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;oBACrB,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;gBAC3B,CAAC;gBAED,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;gBAC/B,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC;iBAAM,CAAC;gBACN,IAAI,EAAE,CAAA;YACR,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YACnB,IAAI,CAAC,CAAC,CAAC,6BAA6B,EAAE,SAAS,EAAE,EAAE,YAAY,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAA;QAC9F,CAAC;IACH,CAAC,CAAA;AACH,CAAC,CAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,32 @@
|
|
|
21
21
|
* Monkey-patching `res.json` (or `res.send`/`res.end`) to re-shape a handler's answer intercepts
|
|
22
22
|
* nothing the handler produces, so the request it was meant to answer is left unanswered and hangs.
|
|
23
23
|
*
|
|
24
|
+
* **A different request or response envelope is configuration.** Pass
|
|
25
|
+
* `mapRequest` and/or `mapResponse` as the second argument to `createRequestHandler`.
|
|
26
|
+
* Both may be asynchronous. The existing handler still validates and authorizes
|
|
27
|
+
* the adapted input. A response mapper sees the handler's errors and partial
|
|
28
|
+
* responses too: preserve them and its headers instead of treating every result
|
|
29
|
+
* as a success. `adaptHandler(handler, options)` offers the same mappings when
|
|
30
|
+
* another HTTP adapter will send the structured response.
|
|
31
|
+
* A request mapper can return `{ response: { statusCode, body } }` to answer
|
|
32
|
+
* before calling the handler, for example when extra validation fails or a
|
|
33
|
+
* filter lookup finds nothing. That response still passes through mapResponse.
|
|
34
|
+
*
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { createRequestHandler } from '@molecule/api-resource'
|
|
37
|
+
* import { handlers } from '@molecule/api-resource-user'
|
|
38
|
+
*
|
|
39
|
+
* const register = createRequestHandler(handlers.create({ name: 'User', tableName: 'users' }), {
|
|
40
|
+
* // Return overrides. The adapter retains framework methods and getters.
|
|
41
|
+
* mapRequest: (req) => ({ body: req.body?.account }),
|
|
42
|
+
* mapResponse: (result) => result.statusCode === 201
|
|
43
|
+
* ? { ...result, body: { account: result.body.props, accessToken: result.body.accessToken } }
|
|
44
|
+
* : result,
|
|
45
|
+
* })
|
|
46
|
+
* // Keep the route's existing rate limiter and authorization around register.
|
|
47
|
+
* // No mapping is inferred: a flat-body API keeps createRequestHandler(handler).
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
24
50
|
* @example
|
|
25
51
|
* ```typescript
|
|
26
52
|
* import { z } from 'zod'
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+HG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,2BAA2B,CAAA;AACzC,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,oBAAoB,CAAA;AAClC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,32 @@
|
|
|
21
21
|
* Monkey-patching `res.json` (or `res.send`/`res.end`) to re-shape a handler's answer intercepts
|
|
22
22
|
* nothing the handler produces, so the request it was meant to answer is left unanswered and hangs.
|
|
23
23
|
*
|
|
24
|
+
* **A different request or response envelope is configuration.** Pass
|
|
25
|
+
* `mapRequest` and/or `mapResponse` as the second argument to `createRequestHandler`.
|
|
26
|
+
* Both may be asynchronous. The existing handler still validates and authorizes
|
|
27
|
+
* the adapted input. A response mapper sees the handler's errors and partial
|
|
28
|
+
* responses too: preserve them and its headers instead of treating every result
|
|
29
|
+
* as a success. `adaptHandler(handler, options)` offers the same mappings when
|
|
30
|
+
* another HTTP adapter will send the structured response.
|
|
31
|
+
* A request mapper can return `{ response: { statusCode, body } }` to answer
|
|
32
|
+
* before calling the handler, for example when extra validation fails or a
|
|
33
|
+
* filter lookup finds nothing. That response still passes through mapResponse.
|
|
34
|
+
*
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { createRequestHandler } from '@molecule/api-resource'
|
|
37
|
+
* import { handlers } from '@molecule/api-resource-user'
|
|
38
|
+
*
|
|
39
|
+
* const register = createRequestHandler(handlers.create({ name: 'User', tableName: 'users' }), {
|
|
40
|
+
* // Return overrides. The adapter retains framework methods and getters.
|
|
41
|
+
* mapRequest: (req) => ({ body: req.body?.account }),
|
|
42
|
+
* mapResponse: (result) => result.statusCode === 201
|
|
43
|
+
* ? { ...result, body: { account: result.body.props, accessToken: result.body.accessToken } }
|
|
44
|
+
* : result,
|
|
45
|
+
* })
|
|
46
|
+
* // Keep the route's existing rate limiter and authorization around register.
|
|
47
|
+
* // No mapping is inferred: a flat-body API keeps createRequestHandler(handler).
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
24
50
|
* @example
|
|
25
51
|
* ```typescript
|
|
26
52
|
* import { z } from 'zod'
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+HG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,2BAA2B,CAAA;AACzC,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,oBAAoB,CAAA;AAClC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,OAAO,KAAK,SAAS,MAAM,sBAAsB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-resource",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Resource CRUD operations and utilities for molecule.dev.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"zod": "4.6.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@molecule/api-bond": "1.0.
|
|
43
|
-
"@molecule/api-database": "1.
|
|
42
|
+
"@molecule/api-bond": "1.0.4",
|
|
43
|
+
"@molecule/api-database": "1.2.2",
|
|
44
44
|
"@molecule/api-locales-resource": "1.0.2",
|
|
45
|
-
"@molecule/api-utilities-validation": "1.0.
|
|
45
|
+
"@molecule/api-utilities-validation": "1.0.4",
|
|
46
46
|
"@types/node": "26.6.5",
|
|
47
47
|
"typescript": "6.0.3",
|
|
48
48
|
"vitest": "4.1.12"
|