@owlmeans/server-api 0.1.18-rc.17 → 0.1.18-rc.18
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 +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-api/SKILL.md +28 -5
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/protocol.d.ts +26 -0
- package/build/protocol.d.ts.map +1 -0
- package/build/protocol.js +41 -0
- package/build/protocol.js.map +1 -0
- package/package.json +10 -10
- package/src/index.ts +1 -0
- package/src/protocol.ts +104 -0
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
84
84
|
your project's skill store (`.agents/skills/`):
|
|
85
85
|
|
|
86
86
|
```sh
|
|
87
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
87
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.13
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-api",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.18",
|
|
5
|
+
"generatedAt": "2026-09-10T18:54:42.484Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: server-api
|
|
3
|
-
description: How to use @owlmeans/server-api — the Fastify-based API server factory,
|
|
3
|
+
description: How to use @owlmeans/server-api — typed protocol handlers, the Fastify-based API server factory, request pipeline, validation, holdApiPort() boot window, and error-to-status mapping. Auto-invoked when importing server-api types, writing protocol handlers, extending the middleware stack, or working out why a request answered the status it did.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,12 +8,14 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/server-api
|
|
9
9
|
|
|
10
10
|
**Layer:** Server
|
|
11
|
-
**Install:** `"@owlmeans/server-api": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/server-api": "^0.1.18-rc.18"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
15
15
|
| Export | Description |
|
|
16
16
|
|--------|-------------|
|
|
17
|
+
| `handlers<Context>()` | Build `body`, `params`, and `request` handlers inferred from an immutable protocol |
|
|
18
|
+
| `uploadedFile(request)` | Read Fastify multipart through the typed handler boundary |
|
|
17
19
|
| `createApiServer(alias)` | Factory for the Fastify-based API server |
|
|
18
20
|
| `appendApiServer(ctx, alias?)` | Register it and expose `ctx.getApiServer()` |
|
|
19
21
|
| `handleRequest` / `handleBody` / `handleParams` / `handleIntermediate` | Wrap an async function as an entrypoint handler |
|
|
@@ -42,9 +44,30 @@ appendApiServer(context)
|
|
|
42
44
|
|
|
43
45
|
## Writing handlers
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
the
|
|
47
|
-
|
|
47
|
+
For a typed protocol, pass the declaration once and let `handlers<Context>()` infer every request
|
|
48
|
+
section and the response. `body` and `params` are available only when the protocol contract
|
|
49
|
+
declares that section; `request` receives the complete typed request plus alias/auth/entity/path
|
|
50
|
+
metadata.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const handle = handlers<Context>()
|
|
54
|
+
|
|
55
|
+
export const create = handle.body(item.create, async (payload, context, request) => {
|
|
56
|
+
const owner = request.entity?.id
|
|
57
|
+
return await context.resource<ItemResource>('item').create({ ...payload, owner })
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The result is a protocol-bound implementation consumed by `bind(protocol, implementation)` from
|
|
62
|
+
`@owlmeans/server-entrypoint` (both exports are available from `@owlmeans/server-app`). Throwing is
|
|
63
|
+
still the failure path; the wrapper resolves successful returns with `EntrypointOutcome.Ok`.
|
|
64
|
+
Use `uploadedFile(request)` when the protocol handler needs the Fastify multipart object.
|
|
65
|
+
|
|
66
|
+
For a legacy entrypoint, `handleRequest(fn)` gives the whole request, `handleBody<T>(fn)` the
|
|
67
|
+
validated body, `handleParams<T>(fn)` the validated URL params, and `handleIntermediate(fn)` a
|
|
68
|
+
function that returns a context (or `null` to leave the chain unchanged) instead of a value.
|
|
69
|
+
|
|
70
|
+
All handler forms resolve the handler's context the same
|
|
48
71
|
way: from the request when it arrived over HTTP, and from the entrypoint's own context otherwise —
|
|
49
72
|
the same handler runs unchanged when a queued job or a socket frame reaches it, and only the way it
|
|
50
73
|
was reached differs.
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
|
package/build/index.js
CHANGED
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context';
|
|
2
|
+
import type { BodyOf, EntrypointProtocolDeclaration, HandlerRequest, OpenValue, ParamsOf, RequestOf, ResponseOf } from '@owlmeans/entrypoint';
|
|
3
|
+
import type { BoundEntrypointHandler } from '@owlmeans/server-entrypoint';
|
|
4
|
+
type HandlerResponse<Response> = [Response] extends [undefined] ? void | undefined : Response;
|
|
5
|
+
type MaybePromise<Value> = Value | Promise<Value>;
|
|
6
|
+
type BodyProtocol<Protocol extends EntrypointProtocolDeclaration> = RequestOf<Protocol> extends {
|
|
7
|
+
body: OpenValue;
|
|
8
|
+
} ? Protocol : never;
|
|
9
|
+
type ParamsProtocol<Protocol extends EntrypointProtocolDeclaration> = RequestOf<Protocol> extends {
|
|
10
|
+
params: object;
|
|
11
|
+
} ? Protocol : never;
|
|
12
|
+
/**
|
|
13
|
+
* Make server handlers that infer request sections and response values from their protocol.
|
|
14
|
+
* The protocol is passed once; application callbacks never name request or reply generics.
|
|
15
|
+
*/
|
|
16
|
+
export declare const handlers: <Context extends BasicContext<BasicConfig>>() => {
|
|
17
|
+
body: <Protocol extends EntrypointProtocolDeclaration>(protocol: Protocol, handler: Protocol extends BodyProtocol<Protocol> ? (payload: BodyOf<Protocol>, context: Context, request: HandlerRequest<RequestOf<Protocol>>) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>> : never) => BoundEntrypointHandler<Protocol>;
|
|
18
|
+
params: <Protocol extends EntrypointProtocolDeclaration>(protocol: Protocol, handler: Protocol extends ParamsProtocol<Protocol> ? (payload: ParamsOf<Protocol>, context: Context, request: HandlerRequest<RequestOf<Protocol>>) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>> : never) => BoundEntrypointHandler<Protocol>;
|
|
19
|
+
request: <Protocol extends EntrypointProtocolDeclaration>(protocol: Protocol, handler: (request: HandlerRequest<RequestOf<Protocol>>, context: Context) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>) => BoundEntrypointHandler<Protocol>;
|
|
20
|
+
};
|
|
21
|
+
/** Access a Fastify-specific upload through an explicit boundary helper, never `request.original`. */
|
|
22
|
+
export declare const uploadedFile: (request: HandlerRequest<{
|
|
23
|
+
body: object;
|
|
24
|
+
}>) => Promise<import('@fastify/multipart').MultipartFile | undefined>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,KAAK,EACV,MAAM,EAAE,6BAA6B,EAAE,cAAc,EAAe,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAC/G,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AAEzE,KAAK,eAAe,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,QAAQ,CAAA;AAC7F,KAAK,YAAY,CAAC,KAAK,IAAI,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;AACjD,KAAK,YAAY,CAAC,QAAQ,SAAS,6BAA6B,IAAI,SAAS,CAAC,QAAQ,CAAC,SAAS;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GAC/G,QAAQ,GAAG,KAAK,CAAA;AACpB,KAAK,cAAc,CAAC,QAAQ,SAAS,6BAA6B,IAAI,SAAS,CAAC,QAAQ,CAAC,SAAS;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAChH,QAAQ,GAAG,KAAK,CAAA;AAyCpB;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,SAAS,YAAY,CAAC,WAAW,CAAC;IAChE,IAAI,GAAG,QAAQ,SAAS,6BAA6B,YACzC,QAAQ,WACT,QAAQ,SAAS,YAAY,CAAC,QAAQ,CAAC,GAAG,CAC/C,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,EACzB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,KACzC,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,GACtD,KAAK,KACR,sBAAsB,CAAC,QAAQ,CAAC;IAKnC,MAAM,GAAG,QAAQ,SAAS,6BAA6B,YAC3C,QAAQ,WACT,QAAQ,SAAS,cAAc,CAAC,QAAQ,CAAC,GAAG,CACjD,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAC3B,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,KACzC,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,GACtD,KAAK,KACR,sBAAsB,CAAC,QAAQ,CAAC;IAKnC,OAAO,GAAG,QAAQ,SAAS,6BAA6B,YAC5C,QAAQ,WACT,CACP,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,EAC5C,OAAO,EAAE,OAAO,KACb,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,KACvD,sBAAsB,CAAC,QAAQ,CAAC;CACnC,CAAA;AAEF,sGAAsG;AACtG,eAAO,MAAM,YAAY,YACd,cAAc,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,KACxC,OAAO,CAAC,OAAO,oBAAoB,EAAE,aAAa,GAAG,SAAS,CAKhE,CAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { assertContext } from '@owlmeans/context';
|
|
2
|
+
import { EntrypointOutcome } from '@owlmeans/entrypoint';
|
|
3
|
+
const contextFor = (request, fallback) => {
|
|
4
|
+
const original = request.original;
|
|
5
|
+
if (original != null && typeof original === 'object' && '_ctx' in original) {
|
|
6
|
+
return original._ctx ?? fallback;
|
|
7
|
+
}
|
|
8
|
+
return fallback;
|
|
9
|
+
};
|
|
10
|
+
const bind = (protocol, execute) => ({
|
|
11
|
+
protocol,
|
|
12
|
+
bind: ref => async (request, response) => {
|
|
13
|
+
const fallback = assertContext(ref.ref?.ctx, protocol.alias);
|
|
14
|
+
try {
|
|
15
|
+
const typedRequest = request;
|
|
16
|
+
const value = await execute(typedRequest, contextFor(request, fallback));
|
|
17
|
+
response.resolve(value, EntrypointOutcome.Ok);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
response.reject(error);
|
|
21
|
+
}
|
|
22
|
+
return response.value;
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Make server handlers that infer request sections and response values from their protocol.
|
|
27
|
+
* The protocol is passed once; application callbacks never name request or reply generics.
|
|
28
|
+
*/
|
|
29
|
+
export const handlers = () => ({
|
|
30
|
+
body: (protocol, handler) => bind(protocol, (request, context) => handler(request.body, context, request)),
|
|
31
|
+
params: (protocol, handler) => bind(protocol, (request, context) => handler(request.params, context, request)),
|
|
32
|
+
request: (protocol, handler) => bind(protocol, handler),
|
|
33
|
+
});
|
|
34
|
+
/** Access a Fastify-specific upload through an explicit boundary helper, never `request.original`. */
|
|
35
|
+
export const uploadedFile = async (request) => {
|
|
36
|
+
const original = request.original;
|
|
37
|
+
if (original == null || typeof original !== 'object' || !('file' in original))
|
|
38
|
+
return undefined;
|
|
39
|
+
return original.file();
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAkBxD,MAAM,UAAU,GAAG,CACjB,OAAwB,EACxB,QAAiB,EACR,EAAE;IACX,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAA;IACjC,IAAI,QAAQ,IAAI,IAAI,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC3E,OAAQ,QAAoC,CAAC,IAAI,IAAI,QAAQ,CAAA;IAC/D,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,IAAI,GAAG,CACX,QAAkB,EAClB,OAAgI,EAC9F,EAAE,CAAC,CAAC;IACtC,QAAQ;IACR,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;QACvC,MAAM,QAAQ,GAAG,aAAa,CAAuB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAA;QAElF,IAAI,CAAC;YACH,MAAM,YAAY,GAAI,OAA8E,CAAA;YACpG,MAAM,KAAK,GAAG,MAAM,OAAO,CACzB,YAAY,EACZ,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAC9B,CAAA;YACD,QAAQ,CAAC,OAAO,CAAC,KAA6B,EAAE,iBAAiB,CAAC,EAAE,CAAC,CAAA;QACvE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,QAAQ,CAAC,MAAM,CAAC,KAAc,CAAC,CAAA;QACjC,CAAC;QAED,OAAO,QAAQ,CAAC,KAAK,CAAA;IACvB,CAAC;CACF,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,GAA8C,EAAE,CAAC,CAAC;IACxE,IAAI,EAAE,CACJ,QAAkB,EAClB,OAKS,EACyB,EAAE,CAAC,IAAI,CAAoB,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAC3F,OAEwD,CAAC,OAAO,CAAC,IAAwB,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAEhH,MAAM,EAAE,CACN,QAAkB,EAClB,OAKS,EACyB,EAAE,CAAC,IAAI,CAAoB,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,CAC3F,OAEwD,CAAC,OAAO,CAAC,MAA4B,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAEpH,OAAO,EAAE,CACP,QAAkB,EAClB,OAGwD,EACtB,EAAE,CAAC,IAAI,CAAoB,QAAQ,EAAE,OAAO,CAAC;CAClF,CAAC,CAAA;AAEF,sGAAsG;AACtG,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC/B,OAAyC,EACwB,EAAE;IACnE,MAAM,QAAQ,GAAI,OAA2B,CAAC,QAAQ,CAAA;IACtD,IAAI,QAAQ,IAAI,IAAI,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAA;IAE/F,OAAQ,QAA6C,CAAC,IAAI,EAAE,CAAA;AAC9D,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-api",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"@fastify/helmet": "^13.1.0",
|
|
33
33
|
"@fastify/middie": "^9.0.2",
|
|
34
34
|
"@fastify/multipart": "^9.0.1",
|
|
35
|
-
"@owlmeans/api": "^0.1.18-rc.
|
|
36
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
37
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
38
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
39
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
40
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
41
|
-
"@owlmeans/route": "^0.1.18-rc.
|
|
42
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
43
|
-
"@owlmeans/server-entrypoint": "^0.1.18-rc.
|
|
35
|
+
"@owlmeans/api": "^0.1.18-rc.14",
|
|
36
|
+
"@owlmeans/auth": "^0.1.18-rc.10",
|
|
37
|
+
"@owlmeans/auth-common": "^0.1.18-rc.14",
|
|
38
|
+
"@owlmeans/context": "^0.1.18-rc.9",
|
|
39
|
+
"@owlmeans/error": "^0.1.18-rc.9",
|
|
40
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.12",
|
|
41
|
+
"@owlmeans/route": "^0.1.18-rc.10",
|
|
42
|
+
"@owlmeans/server-context": "^0.1.18-rc.13",
|
|
43
|
+
"@owlmeans/server-entrypoint": "^0.1.18-rc.12",
|
|
44
44
|
"ajv-errors": "^3.0.0",
|
|
45
45
|
"fastify": "^5.0.0",
|
|
46
46
|
"fastify-raw-body": "^5.0.0",
|
package/src/index.ts
CHANGED
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assertContext } from '@owlmeans/context'
|
|
2
|
+
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
|
+
import { EntrypointOutcome } from '@owlmeans/entrypoint'
|
|
4
|
+
import type {
|
|
5
|
+
BodyOf, EntrypointProtocolDeclaration, HandlerRequest, OpenRequest, OpenValue, ParamsOf, RequestOf, ResponseOf,
|
|
6
|
+
} from '@owlmeans/entrypoint'
|
|
7
|
+
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
8
|
+
import type { BoundEntrypointHandler } from '@owlmeans/server-entrypoint'
|
|
9
|
+
|
|
10
|
+
type HandlerResponse<Response> = [Response] extends [undefined] ? void | undefined : Response
|
|
11
|
+
type MaybePromise<Value> = Value | Promise<Value>
|
|
12
|
+
type BodyProtocol<Protocol extends EntrypointProtocolDeclaration> = RequestOf<Protocol> extends { body: OpenValue }
|
|
13
|
+
? Protocol : never
|
|
14
|
+
type ParamsProtocol<Protocol extends EntrypointProtocolDeclaration> = RequestOf<Protocol> extends { params: object }
|
|
15
|
+
? Protocol : never
|
|
16
|
+
|
|
17
|
+
interface ContextCarrier<Context> {
|
|
18
|
+
_ctx?: Context
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const contextFor = <Context extends BasicContext<BasicConfig>>(
|
|
22
|
+
request: AbstractRequest,
|
|
23
|
+
fallback: Context,
|
|
24
|
+
): Context => {
|
|
25
|
+
const original = request.original
|
|
26
|
+
if (original != null && typeof original === 'object' && '_ctx' in original) {
|
|
27
|
+
return (original as ContextCarrier<Context>)._ctx ?? fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return fallback
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const bind = <Protocol extends EntrypointProtocolDeclaration, Context extends BasicContext<BasicConfig>>(
|
|
34
|
+
protocol: Protocol,
|
|
35
|
+
execute: (request: HandlerRequest<RequestOf<Protocol>>, context: Context) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>,
|
|
36
|
+
): BoundEntrypointHandler<Protocol> => ({
|
|
37
|
+
protocol,
|
|
38
|
+
bind: ref => async (request, response) => {
|
|
39
|
+
const fallback = assertContext<BasicConfig, Context>(ref.ref?.ctx, protocol.alias)
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const typedRequest = (request as HandlerRequest<OpenRequest>) as HandlerRequest<RequestOf<Protocol>>
|
|
43
|
+
const value = await execute(
|
|
44
|
+
typedRequest,
|
|
45
|
+
contextFor(request, fallback),
|
|
46
|
+
)
|
|
47
|
+
response.resolve(value as ResponseOf<Protocol>, EntrypointOutcome.Ok)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
response.reject(error as Error)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return response.value
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Make server handlers that infer request sections and response values from their protocol.
|
|
58
|
+
* The protocol is passed once; application callbacks never name request or reply generics.
|
|
59
|
+
*/
|
|
60
|
+
export const handlers = <Context extends BasicContext<BasicConfig>>() => ({
|
|
61
|
+
body: <Protocol extends EntrypointProtocolDeclaration>(
|
|
62
|
+
protocol: Protocol,
|
|
63
|
+
handler: Protocol extends BodyProtocol<Protocol> ? (
|
|
64
|
+
payload: BodyOf<Protocol>,
|
|
65
|
+
context: Context,
|
|
66
|
+
request: HandlerRequest<RequestOf<Protocol>>,
|
|
67
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>
|
|
68
|
+
: never,
|
|
69
|
+
): BoundEntrypointHandler<Protocol> => bind<Protocol, Context>(protocol, (request, context) =>
|
|
70
|
+
(handler as (
|
|
71
|
+
payload: BodyOf<Protocol>, context: Context, request: HandlerRequest<RequestOf<Protocol>>,
|
|
72
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>)(request.body as BodyOf<Protocol>, context, request)),
|
|
73
|
+
|
|
74
|
+
params: <Protocol extends EntrypointProtocolDeclaration>(
|
|
75
|
+
protocol: Protocol,
|
|
76
|
+
handler: Protocol extends ParamsProtocol<Protocol> ? (
|
|
77
|
+
payload: ParamsOf<Protocol>,
|
|
78
|
+
context: Context,
|
|
79
|
+
request: HandlerRequest<RequestOf<Protocol>>,
|
|
80
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>
|
|
81
|
+
: never,
|
|
82
|
+
): BoundEntrypointHandler<Protocol> => bind<Protocol, Context>(protocol, (request, context) =>
|
|
83
|
+
(handler as (
|
|
84
|
+
payload: ParamsOf<Protocol>, context: Context, request: HandlerRequest<RequestOf<Protocol>>,
|
|
85
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>)(request.params as ParamsOf<Protocol>, context, request)),
|
|
86
|
+
|
|
87
|
+
request: <Protocol extends EntrypointProtocolDeclaration>(
|
|
88
|
+
protocol: Protocol,
|
|
89
|
+
handler: (
|
|
90
|
+
request: HandlerRequest<RequestOf<Protocol>>,
|
|
91
|
+
context: Context,
|
|
92
|
+
) => MaybePromise<HandlerResponse<ResponseOf<Protocol>>>,
|
|
93
|
+
): BoundEntrypointHandler<Protocol> => bind<Protocol, Context>(protocol, handler),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/** Access a Fastify-specific upload through an explicit boundary helper, never `request.original`. */
|
|
97
|
+
export const uploadedFile = async (
|
|
98
|
+
request: HandlerRequest<{ body: object }>,
|
|
99
|
+
): Promise<import('@fastify/multipart').MultipartFile | undefined> => {
|
|
100
|
+
const original = (request as AbstractRequest).original
|
|
101
|
+
if (original == null || typeof original !== 'object' || !('file' in original)) return undefined
|
|
102
|
+
|
|
103
|
+
return (original as import('fastify').FastifyRequest).file()
|
|
104
|
+
}
|