@ts-kizuna/fastify 1.49.5
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/LICENSE.MD +9 -0
- package/README.md +37 -0
- package/dist/index.cjs +187 -0
- package/dist/index.d.cts +200 -0
- package/dist/index.d.mts +200 -0
- package/dist/index.mjs +162 -0
- package/package.json +76 -0
package/LICENSE.MD
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sondre Ørland
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @ts-kizuna/fastify
|
|
2
|
+
|
|
3
|
+
`@ts-kizuna/fastify` connects a ts-kizuna API to a Fastify application.
|
|
4
|
+
|
|
5
|
+
**Requires Fastify >= 5.**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @ts-kizuna/fastify fastify
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import Fastify from 'fastify';
|
|
17
|
+
import { KizunaServer } from '@ts-kizuna/fastify';
|
|
18
|
+
import { contract } from './contract';
|
|
19
|
+
import { router } from './router';
|
|
20
|
+
|
|
21
|
+
const server = new KizunaServer(contract);
|
|
22
|
+
|
|
23
|
+
const api = server.api({
|
|
24
|
+
router,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const app = Fastify();
|
|
28
|
+
await api.mount(app);
|
|
29
|
+
|
|
30
|
+
app.listen({
|
|
31
|
+
port: 3000,
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
[Fastify adapter](https://ts-kizuna.com/docs/adapters/fastify)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let fastify_plugin = require("fastify-plugin");
|
|
25
|
+
fastify_plugin = __toESM(fastify_plugin, 1);
|
|
26
|
+
let node_stream = require("node:stream");
|
|
27
|
+
let _ts_kizuna_core_adapter = require("@ts-kizuna/core/adapter");
|
|
28
|
+
//#region src/server.ts
|
|
29
|
+
/**
|
|
30
|
+
* Write a web `Response` to a Fastify reply. Plugins answer in web terms to stay
|
|
31
|
+
* adapter-agnostic, so the translation belongs here.
|
|
32
|
+
*/
|
|
33
|
+
const writeWebResponse = async (response, reply) => {
|
|
34
|
+
if (!(response instanceof globalThis.Response)) return;
|
|
35
|
+
reply.hijack();
|
|
36
|
+
reply.raw.statusCode = response.status;
|
|
37
|
+
response.headers.forEach((value, name) => reply.raw.setHeader(name, value));
|
|
38
|
+
if (!response.body) {
|
|
39
|
+
reply.raw.end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
node_stream.Readable.fromWeb(response.body).pipe(reply.raw);
|
|
43
|
+
};
|
|
44
|
+
const adapter = (0, _ts_kizuna_core_adapter.createAdapter)({
|
|
45
|
+
buildHandlerContext: (adapterRequest, { reply }) => ({
|
|
46
|
+
request: adapterRequest.request,
|
|
47
|
+
reply
|
|
48
|
+
}),
|
|
49
|
+
respond: (result, { reply, formatError }) => {
|
|
50
|
+
if (result.kind === "handler-error") throw result.error;
|
|
51
|
+
if (result.kind === "raw-response") {
|
|
52
|
+
writeWebResponse(result.response, reply);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const rendered = (0, _ts_kizuna_core_adapter.renderJsonResult)(result, formatError, reply.request);
|
|
56
|
+
for (const [key, value] of Object.entries(rendered.headers)) reply.header(key, value);
|
|
57
|
+
if (rendered.body === void 0) reply.status(rendered.status).send();
|
|
58
|
+
else if (rendered.raw) {
|
|
59
|
+
const body = rendered.body;
|
|
60
|
+
reply.status(rendered.status).send(typeof body === "string" || Buffer.isBuffer(body) ? body : Buffer.from(body));
|
|
61
|
+
} else reply.status(rendered.status).send(rendered.body);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* Fastify plugin that mounts a ts-kizuna API.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const app = Fastify();
|
|
69
|
+
* await api.mount(app);
|
|
70
|
+
*/
|
|
71
|
+
const fastifyKizuna = (0, fastify_plugin.default)(async (app, options) => {
|
|
72
|
+
const { api } = options;
|
|
73
|
+
const guards = api[_ts_kizuna_core_adapter.GUARDS_META];
|
|
74
|
+
const schemes = api[_ts_kizuna_core_adapter.SCHEMES_META];
|
|
75
|
+
const requestContext = api[_ts_kizuna_core_adapter.REQUEST_CONTEXT_META];
|
|
76
|
+
const pluginExports = (0, _ts_kizuna_core_adapter.pluginExportsOf)(api);
|
|
77
|
+
const jobsMeta = api[_ts_kizuna_core_adapter.JOBS_META];
|
|
78
|
+
const jobRunner = (0, _ts_kizuna_core_adapter.jobRunnerFrom)(jobsMeta);
|
|
79
|
+
const mountRoute = (routeKey, route, lane, resolvedRouter) => {
|
|
80
|
+
app.route({
|
|
81
|
+
method: route.method,
|
|
82
|
+
url: route.path,
|
|
83
|
+
preHandler: [async (request) => {
|
|
84
|
+
request.kizunaRoute = route;
|
|
85
|
+
}],
|
|
86
|
+
handler: async (request, reply) => {
|
|
87
|
+
const adapterRequest = {
|
|
88
|
+
request,
|
|
89
|
+
method: request.method,
|
|
90
|
+
resolution: {
|
|
91
|
+
kind: "pre-resolved",
|
|
92
|
+
routeKey,
|
|
93
|
+
route,
|
|
94
|
+
params: request.params ?? {}
|
|
95
|
+
},
|
|
96
|
+
query: request.query ?? {},
|
|
97
|
+
headers: request.headers,
|
|
98
|
+
readBody: () => request.body
|
|
99
|
+
};
|
|
100
|
+
await adapter.handle({
|
|
101
|
+
routes: lane,
|
|
102
|
+
router: resolvedRouter,
|
|
103
|
+
request: adapterRequest,
|
|
104
|
+
responseContext: {
|
|
105
|
+
reply,
|
|
106
|
+
formatError: options?.formatError
|
|
107
|
+
},
|
|
108
|
+
guards,
|
|
109
|
+
schemes,
|
|
110
|
+
requestContext,
|
|
111
|
+
pluginExports,
|
|
112
|
+
jobs: jobRunner,
|
|
113
|
+
responseValidation: options?.responseValidation
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const mountLane = (lane, resolvedRouter) => {
|
|
119
|
+
const declaredRoutes = [...adapter.eachRoute(lane, resolvedRouter)].sort((left, right) => Number(right.route.method === "HEAD") - Number(left.route.method === "HEAD"));
|
|
120
|
+
for (const { routeKey, route } of declaredRoutes) mountRoute(routeKey, route, lane, resolvedRouter);
|
|
121
|
+
};
|
|
122
|
+
mountLane(api.routes, api[_ts_kizuna_core_adapter.ROUTER_META]);
|
|
123
|
+
mountLane((0, _ts_kizuna_core_adapter.pluginRoutesOf)(api), (0, _ts_kizuna_core_adapter.pluginRouterOf)(api));
|
|
124
|
+
if (jobsMeta) {
|
|
125
|
+
const routes = (0, _ts_kizuna_core_adapter.jobRoutes)(jobsMeta);
|
|
126
|
+
const router = (0, _ts_kizuna_core_adapter.jobRouter)(jobsMeta);
|
|
127
|
+
for (const [routeKey, route] of Object.entries(routes)) mountRoute(routeKey, route, routes, router);
|
|
128
|
+
}
|
|
129
|
+
}, { name: "@ts-kizuna/fastify" });
|
|
130
|
+
const createServerSurface = (contract, options) => {
|
|
131
|
+
(0, _ts_kizuna_core_adapter.warnUnsupportedJobOptions)(contract.jobs, options?.jobTransport);
|
|
132
|
+
return {
|
|
133
|
+
guard: (_name, run) => run,
|
|
134
|
+
requestContext: (_name, run) => run,
|
|
135
|
+
router: (groupOrRouter, groupRouter) => groupRouter ?? groupOrRouter,
|
|
136
|
+
jobs: (handlers) => handlers,
|
|
137
|
+
api: ({ jobs, ...parts }) => {
|
|
138
|
+
const api = Object.assign((0, _ts_kizuna_core_adapter.assembleApi)(contract, parts), { [_ts_kizuna_core_adapter.JOBS_META]: contract.jobs ? {
|
|
139
|
+
jobs: contract.jobs,
|
|
140
|
+
handlers: jobs ?? {},
|
|
141
|
+
config: contract.jobsConfig,
|
|
142
|
+
transport: options?.jobTransport,
|
|
143
|
+
onError: options?.onJobError
|
|
144
|
+
} : void 0 });
|
|
145
|
+
const plugin = (0, fastify_plugin.default)(async (app, pluginOptions) => {
|
|
146
|
+
await fastifyKizuna(app, {
|
|
147
|
+
...pluginOptions,
|
|
148
|
+
api
|
|
149
|
+
});
|
|
150
|
+
}, { name: "@ts-kizuna/fastify" });
|
|
151
|
+
return Object.assign(api, {
|
|
152
|
+
plugin,
|
|
153
|
+
mount: async (app, mountOptions) => {
|
|
154
|
+
await app.register(plugin, mountOptions ?? {});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Turn a contract into a server handle: the serving counterpart to `Kizuna`.
|
|
162
|
+
* Keep the instance and use `server.guard` to define guards, `server.router`
|
|
163
|
+
* to write typed handlers, and `server.api` to assemble them.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* const server = new KizunaServer(contract);
|
|
167
|
+
*
|
|
168
|
+
* const requireUser = server.guard('user', ({ bearer, deny }) => {
|
|
169
|
+
* const session = bearer && sessions.get(bearer.token);
|
|
170
|
+
* return session ? { userId: session.userId } : deny(401, 'Unauthorized');
|
|
171
|
+
* });
|
|
172
|
+
*
|
|
173
|
+
* export const api = server.api({
|
|
174
|
+
* router,
|
|
175
|
+
* guards: {
|
|
176
|
+
* user: requireUser,
|
|
177
|
+
* },
|
|
178
|
+
* });
|
|
179
|
+
*/
|
|
180
|
+
var KizunaServer = class {
|
|
181
|
+
constructor(contract, options) {
|
|
182
|
+
Object.assign(this, createServerSurface(contract, options));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
//#endregion
|
|
186
|
+
exports.KizunaServer = KizunaServer;
|
|
187
|
+
exports.fastifyKizuna = fastifyKizuna;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { ApiWithRouter, ContractPlugins, ErrorFormatter, GUARDS_META, GuardDenial, GuardDeny, GuardParams, GuardRun, HandlersFromAuth, JOBS_META, Jobs, PluginArgs, PluginImplementations, REQUEST_CONTEXT_META, RequestContextRun, RequestContextValues, RouteDefinition, RouteHandler as RouteHandler$1, Router as Router$1, Routes, SCHEMES_META, ServerOptions } from "@ts-kizuna/core/adapter";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { Contract, CredentialOf, GuardSuccess, JobHandlers, JobsArg, RequestContextHeaderValues, RequestContextSchema, SecurityScheme, TagOptions } from "@ts-kizuna/core";
|
|
5
|
+
|
|
6
|
+
//#region src/server.d.ts
|
|
7
|
+
type FastifyApi<R extends Routes = Routes> = ApiWithRouter<R> & {
|
|
8
|
+
readonly [GUARDS_META]?: unknown;
|
|
9
|
+
readonly [SCHEMES_META]?: unknown;
|
|
10
|
+
readonly [REQUEST_CONTEXT_META]?: unknown;
|
|
11
|
+
readonly [JOBS_META]?: unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Register every contract route on a Fastify instance. Calls
|
|
14
|
+
* `app.register` internally, so encapsulation behaves as Fastify expects.
|
|
15
|
+
*/
|
|
16
|
+
mount: (app: FastifyInstance, options?: FastifyOptions) => Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* The same routes as a Fastify plugin, for composing inside your own plugin
|
|
19
|
+
* tree: `app.register(api.plugin, { prefix: '/v1' })`.
|
|
20
|
+
*/
|
|
21
|
+
plugin: FastifyPluginAsync<FastifyOptions>;
|
|
22
|
+
};
|
|
23
|
+
interface FastifyHandlerContext {
|
|
24
|
+
request: FastifyRequest;
|
|
25
|
+
reply: FastifyReply;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The handler type for a single route, typed against its contract definition.
|
|
29
|
+
*/
|
|
30
|
+
type RouteHandler<R extends RouteDefinition> = RouteHandler$1<R, FastifyHandlerContext>;
|
|
31
|
+
/**
|
|
32
|
+
* The handler tree for a contract or route group, typed against it. Routes
|
|
33
|
+
* secured by the contract's `auth` map additionally receive each required
|
|
34
|
+
* identity's context in their handler args, under `auth`, keyed by the identity's name.
|
|
35
|
+
*/
|
|
36
|
+
type Router<C> = C extends Contract<infer R, infer _Tags, infer _Codes, infer Schemes, infer Auth, infer RequestContext, infer Plugins, infer J> ? HandlersFromAuth<R, FastifyHandlerContext & RequestContextValues<RequestContext> & PluginArgs<Plugins> & JobsArg<J>, Schemes, Auth> : C extends Routes ? Router$1<C, FastifyHandlerContext> : never;
|
|
37
|
+
/**
|
|
38
|
+
* The handler for each of a contract's scheduled jobs, typed against it. Each
|
|
39
|
+
* receives only the job's `input`, so the same handler can be run in process.
|
|
40
|
+
*/
|
|
41
|
+
type JobsRouter<C> = C extends Contract<infer _R, infer _Tags, infer _Codes, infer _Schemes, infer _Auth, infer _RequestContext, infer _Plugins, infer J> ? JobHandlers<J> : never;
|
|
42
|
+
/**
|
|
43
|
+
* The handlers for a group named on the contract, or for a bare route group.
|
|
44
|
+
* Both forms resolve through one signature: a second candidate of the same
|
|
45
|
+
* arity costs zero-argument handlers their contextual type.
|
|
46
|
+
*/
|
|
47
|
+
type GroupRouter<Source, GroupOrRoutes> = GroupOrRoutes extends string ? Router<Source>[Extract<GroupOrRoutes, keyof Router<Source>>] : Router<GroupOrRoutes>;
|
|
48
|
+
declare module 'fastify' {
|
|
49
|
+
interface FastifyRequest {
|
|
50
|
+
kizunaRoute?: RouteDefinition;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
type FastifyPreHandler = (request: FastifyRequest, reply: FastifyReply) => void | Promise<void>;
|
|
54
|
+
interface FastifyOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Validate handler return values against the routes' response schemas.
|
|
57
|
+
* Mismatches surface as 500 errors. Intended for development; disable in
|
|
58
|
+
* production.
|
|
59
|
+
*
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
responseValidation?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Reshape error (status >= 400) response bytes before they are sent. See
|
|
65
|
+
* {@link ErrorFormatter}.
|
|
66
|
+
*/
|
|
67
|
+
formatError?: ErrorFormatter<FastifyRequest>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A guard per identity, keyed by name. Each receives the handler context, a
|
|
71
|
+
* `deny` helper, and the matched route's required scopes, and returns that
|
|
72
|
+
* identity's {@link GuardSuccess} (its context and access fields) or a `deny(...)`
|
|
73
|
+
* result. Keying by name lets each guard's return be typed against its own
|
|
74
|
+
* identity, so access values narrow without an annotation. An
|
|
75
|
+
* authentication-only identity (no context, no access) returns nothing on
|
|
76
|
+
* success, or `deny(...)`.
|
|
77
|
+
*/
|
|
78
|
+
type GuardFns<Schemes extends Record<string, SecurityScheme>, Params> = { [Name in keyof Schemes]: (args: FastifyHandlerContext & CredentialOf<Schemes[Name]> & {
|
|
79
|
+
params: Params;
|
|
80
|
+
deny: GuardDeny;
|
|
81
|
+
scopes: string[];
|
|
82
|
+
}) => [keyof GuardSuccess<Schemes[Name]>] extends [never] ? void | GuardDenial | Promise<void | GuardDenial> : GuardSuccess<Schemes[Name]> | GuardDenial | Promise<GuardSuccess<Schemes[Name]> | GuardDenial> };
|
|
83
|
+
/**
|
|
84
|
+
* One guard per identity declared on the contract.
|
|
85
|
+
*/
|
|
86
|
+
type GuardsForSchemes<Schemes extends Record<string, SecurityScheme>> = { [Name in keyof Schemes]: GuardRun<FastifyHandlerContext> };
|
|
87
|
+
/**
|
|
88
|
+
* The resolver functions for the request context schemas declared on `kizuna`,
|
|
89
|
+
* keyed by name. Each runs on every route and returns its schema's value.
|
|
90
|
+
*/
|
|
91
|
+
type RequestResolverFns<RequestContext extends Record<string, RequestContextSchema>> = { [Name in keyof RequestContext]: (args: FastifyHandlerContext & {
|
|
92
|
+
params: Record<string, string>;
|
|
93
|
+
headers: RequestContextHeaderValues<RequestContext[Name]>;
|
|
94
|
+
}) => z.output<RequestContext[Name]['context']> | Promise<z.output<RequestContext[Name]['context']>> };
|
|
95
|
+
interface KizunaPluginOptions extends FastifyOptions {
|
|
96
|
+
/**
|
|
97
|
+
* The API object built by `server.api`.
|
|
98
|
+
*/
|
|
99
|
+
api: FastifyApi;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Fastify plugin that mounts a ts-kizuna API.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* const app = Fastify();
|
|
106
|
+
* await api.mount(app);
|
|
107
|
+
*/
|
|
108
|
+
declare const fastifyKizuna: (app: FastifyInstance, options: KizunaPluginOptions) => Promise<void>;
|
|
109
|
+
type ServerContract<R extends Routes, Schemes extends Record<string, SecurityScheme>, Auth, RequestContext extends Record<string, RequestContextSchema>, Plugins extends ContractPlugins, J extends Jobs = Jobs> = Contract<R, Record<string, TagOptions>, string, Schemes, Auth, RequestContext, Plugins, J>;
|
|
110
|
+
interface Server<R extends Routes, Schemes extends Record<string, SecurityScheme>, Auth, RequestContext extends Record<string, RequestContextSchema>, Plugins extends ContractPlugins, J extends Jobs = Jobs> {
|
|
111
|
+
/**
|
|
112
|
+
* Define a guard for one of the contract's identities. It runs before the
|
|
113
|
+
* handlers of every route whose `auth` entry requires the identity, and
|
|
114
|
+
* receives the credential its method extracted (`bearer`, `apiKey`, or
|
|
115
|
+
* `basic`, `null` when absent). Return the identity's context and access
|
|
116
|
+
* fields to allow the request, or call `deny(status, detail)`.
|
|
117
|
+
*/
|
|
118
|
+
guard<const Name extends Extract<keyof Schemes, string>>(name: Name, run: GuardFns<Schemes, GuardParams<R, Auth, Name>>[Name]): GuardRun<FastifyHandlerContext>;
|
|
119
|
+
/**
|
|
120
|
+
* Define a request context resolver declared on the contract. It runs on
|
|
121
|
+
* every route, public ones included, and never denies.
|
|
122
|
+
*/
|
|
123
|
+
requestContext<const Name extends Extract<keyof RequestContext, string>>(name: Name, run: RequestResolverFns<RequestContext>[Name]): RequestContextRun<FastifyHandlerContext>;
|
|
124
|
+
/**
|
|
125
|
+
* Write typed handlers for the contract or one of its route groups.
|
|
126
|
+
*/
|
|
127
|
+
router: {
|
|
128
|
+
<const GroupOrRoutes extends Extract<keyof Router<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>, string> | Routes>(group: GroupOrRoutes, router: GroupRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>, GroupOrRoutes>): GroupRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>, GroupOrRoutes>;
|
|
129
|
+
(router: Router<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>): Router<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Write a handler for each of the contract's jobs.
|
|
133
|
+
*
|
|
134
|
+
* Pass a `transport` to say where a queued job goes. Without one, `queue`
|
|
135
|
+
* runs the job in this process and it is lost on a crash.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* export const jobs = server.jobs({
|
|
139
|
+
* sendDigests: async () => ({
|
|
140
|
+
* status: 200,
|
|
141
|
+
* body: {
|
|
142
|
+
* sent: await sendPendingDigests(),
|
|
143
|
+
* },
|
|
144
|
+
* }),
|
|
145
|
+
* });
|
|
146
|
+
*/
|
|
147
|
+
jobs(handlers: JobsRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>): JobsRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>;
|
|
148
|
+
/**
|
|
149
|
+
* Assemble the router, guards, and job handlers into the api object.
|
|
150
|
+
*/
|
|
151
|
+
api(options: {
|
|
152
|
+
router: Router<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>;
|
|
153
|
+
} & (string extends keyof Schemes ? {
|
|
154
|
+
guards?: undefined;
|
|
155
|
+
} : {
|
|
156
|
+
guards: NoInfer<GuardsForSchemes<Schemes>>;
|
|
157
|
+
}) & (string extends keyof J ? {
|
|
158
|
+
jobs?: undefined;
|
|
159
|
+
} : {
|
|
160
|
+
jobs: NoInfer<JobsRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>>;
|
|
161
|
+
}) & (string extends keyof RequestContext ? {
|
|
162
|
+
requestContext?: undefined;
|
|
163
|
+
} : {
|
|
164
|
+
requestContext: NoInfer<{ [Name in keyof RequestContext]: RequestContextRun<FastifyHandlerContext> }>;
|
|
165
|
+
}) & (string extends keyof Plugins ? {
|
|
166
|
+
plugins?: undefined;
|
|
167
|
+
} : {
|
|
168
|
+
plugins: PluginImplementations<Plugins, FastifyHandlerContext>;
|
|
169
|
+
})): FastifyApi<R>;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Turn a contract into a server handle: the serving counterpart to `Kizuna`.
|
|
173
|
+
* Keep the instance and use `server.guard` to define guards, `server.router`
|
|
174
|
+
* to write typed handlers, and `server.api` to assemble them.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* const server = new KizunaServer(contract);
|
|
178
|
+
*
|
|
179
|
+
* const requireUser = server.guard('user', ({ bearer, deny }) => {
|
|
180
|
+
* const session = bearer && sessions.get(bearer.token);
|
|
181
|
+
* return session ? { userId: session.userId } : deny(401, 'Unauthorized');
|
|
182
|
+
* });
|
|
183
|
+
*
|
|
184
|
+
* export const api = server.api({
|
|
185
|
+
* router,
|
|
186
|
+
* guards: {
|
|
187
|
+
* user: requireUser,
|
|
188
|
+
* },
|
|
189
|
+
* });
|
|
190
|
+
*/
|
|
191
|
+
declare class KizunaServer<const R extends Routes, Schemes extends Record<string, SecurityScheme>, Auth, RequestContext extends Record<string, RequestContextSchema>, Plugins extends ContractPlugins, J extends Jobs = Jobs> implements Server<R, Schemes, Auth, RequestContext, Plugins, J> {
|
|
192
|
+
readonly guard: Server<R, Schemes, Auth, RequestContext, Plugins, J>['guard'];
|
|
193
|
+
readonly requestContext: Server<R, Schemes, Auth, RequestContext, Plugins, J>['requestContext'];
|
|
194
|
+
readonly router: Server<R, Schemes, Auth, RequestContext, Plugins, J>['router'];
|
|
195
|
+
readonly jobs: Server<R, Schemes, Auth, RequestContext, Plugins, J>['jobs'];
|
|
196
|
+
readonly api: Server<R, Schemes, Auth, RequestContext, Plugins, J>['api'];
|
|
197
|
+
constructor(contract: ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>, options?: ServerOptions);
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
export { FastifyApi, FastifyHandlerContext, FastifyOptions, FastifyPreHandler, JobsRouter, KizunaPluginOptions, KizunaServer, RouteHandler, Router, Server, fastifyKizuna };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { ApiWithRouter, ContractPlugins, ErrorFormatter, GUARDS_META, GuardDenial, GuardDeny, GuardParams, GuardRun, HandlersFromAuth, JOBS_META, Jobs, PluginArgs, PluginImplementations, REQUEST_CONTEXT_META, RequestContextRun, RequestContextValues, RouteDefinition, RouteHandler as RouteHandler$1, Router as Router$1, Routes, SCHEMES_META, ServerOptions } from "@ts-kizuna/core/adapter";
|
|
2
|
+
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { Contract, CredentialOf, GuardSuccess, JobHandlers, JobsArg, RequestContextHeaderValues, RequestContextSchema, SecurityScheme, TagOptions } from "@ts-kizuna/core";
|
|
5
|
+
|
|
6
|
+
//#region src/server.d.ts
|
|
7
|
+
type FastifyApi<R extends Routes = Routes> = ApiWithRouter<R> & {
|
|
8
|
+
readonly [GUARDS_META]?: unknown;
|
|
9
|
+
readonly [SCHEMES_META]?: unknown;
|
|
10
|
+
readonly [REQUEST_CONTEXT_META]?: unknown;
|
|
11
|
+
readonly [JOBS_META]?: unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Register every contract route on a Fastify instance. Calls
|
|
14
|
+
* `app.register` internally, so encapsulation behaves as Fastify expects.
|
|
15
|
+
*/
|
|
16
|
+
mount: (app: FastifyInstance, options?: FastifyOptions) => Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* The same routes as a Fastify plugin, for composing inside your own plugin
|
|
19
|
+
* tree: `app.register(api.plugin, { prefix: '/v1' })`.
|
|
20
|
+
*/
|
|
21
|
+
plugin: FastifyPluginAsync<FastifyOptions>;
|
|
22
|
+
};
|
|
23
|
+
interface FastifyHandlerContext {
|
|
24
|
+
request: FastifyRequest;
|
|
25
|
+
reply: FastifyReply;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The handler type for a single route, typed against its contract definition.
|
|
29
|
+
*/
|
|
30
|
+
type RouteHandler<R extends RouteDefinition> = RouteHandler$1<R, FastifyHandlerContext>;
|
|
31
|
+
/**
|
|
32
|
+
* The handler tree for a contract or route group, typed against it. Routes
|
|
33
|
+
* secured by the contract's `auth` map additionally receive each required
|
|
34
|
+
* identity's context in their handler args, under `auth`, keyed by the identity's name.
|
|
35
|
+
*/
|
|
36
|
+
type Router<C> = C extends Contract<infer R, infer _Tags, infer _Codes, infer Schemes, infer Auth, infer RequestContext, infer Plugins, infer J> ? HandlersFromAuth<R, FastifyHandlerContext & RequestContextValues<RequestContext> & PluginArgs<Plugins> & JobsArg<J>, Schemes, Auth> : C extends Routes ? Router$1<C, FastifyHandlerContext> : never;
|
|
37
|
+
/**
|
|
38
|
+
* The handler for each of a contract's scheduled jobs, typed against it. Each
|
|
39
|
+
* receives only the job's `input`, so the same handler can be run in process.
|
|
40
|
+
*/
|
|
41
|
+
type JobsRouter<C> = C extends Contract<infer _R, infer _Tags, infer _Codes, infer _Schemes, infer _Auth, infer _RequestContext, infer _Plugins, infer J> ? JobHandlers<J> : never;
|
|
42
|
+
/**
|
|
43
|
+
* The handlers for a group named on the contract, or for a bare route group.
|
|
44
|
+
* Both forms resolve through one signature: a second candidate of the same
|
|
45
|
+
* arity costs zero-argument handlers their contextual type.
|
|
46
|
+
*/
|
|
47
|
+
type GroupRouter<Source, GroupOrRoutes> = GroupOrRoutes extends string ? Router<Source>[Extract<GroupOrRoutes, keyof Router<Source>>] : Router<GroupOrRoutes>;
|
|
48
|
+
declare module 'fastify' {
|
|
49
|
+
interface FastifyRequest {
|
|
50
|
+
kizunaRoute?: RouteDefinition;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
type FastifyPreHandler = (request: FastifyRequest, reply: FastifyReply) => void | Promise<void>;
|
|
54
|
+
interface FastifyOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Validate handler return values against the routes' response schemas.
|
|
57
|
+
* Mismatches surface as 500 errors. Intended for development; disable in
|
|
58
|
+
* production.
|
|
59
|
+
*
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
responseValidation?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Reshape error (status >= 400) response bytes before they are sent. See
|
|
65
|
+
* {@link ErrorFormatter}.
|
|
66
|
+
*/
|
|
67
|
+
formatError?: ErrorFormatter<FastifyRequest>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A guard per identity, keyed by name. Each receives the handler context, a
|
|
71
|
+
* `deny` helper, and the matched route's required scopes, and returns that
|
|
72
|
+
* identity's {@link GuardSuccess} (its context and access fields) or a `deny(...)`
|
|
73
|
+
* result. Keying by name lets each guard's return be typed against its own
|
|
74
|
+
* identity, so access values narrow without an annotation. An
|
|
75
|
+
* authentication-only identity (no context, no access) returns nothing on
|
|
76
|
+
* success, or `deny(...)`.
|
|
77
|
+
*/
|
|
78
|
+
type GuardFns<Schemes extends Record<string, SecurityScheme>, Params> = { [Name in keyof Schemes]: (args: FastifyHandlerContext & CredentialOf<Schemes[Name]> & {
|
|
79
|
+
params: Params;
|
|
80
|
+
deny: GuardDeny;
|
|
81
|
+
scopes: string[];
|
|
82
|
+
}) => [keyof GuardSuccess<Schemes[Name]>] extends [never] ? void | GuardDenial | Promise<void | GuardDenial> : GuardSuccess<Schemes[Name]> | GuardDenial | Promise<GuardSuccess<Schemes[Name]> | GuardDenial> };
|
|
83
|
+
/**
|
|
84
|
+
* One guard per identity declared on the contract.
|
|
85
|
+
*/
|
|
86
|
+
type GuardsForSchemes<Schemes extends Record<string, SecurityScheme>> = { [Name in keyof Schemes]: GuardRun<FastifyHandlerContext> };
|
|
87
|
+
/**
|
|
88
|
+
* The resolver functions for the request context schemas declared on `kizuna`,
|
|
89
|
+
* keyed by name. Each runs on every route and returns its schema's value.
|
|
90
|
+
*/
|
|
91
|
+
type RequestResolverFns<RequestContext extends Record<string, RequestContextSchema>> = { [Name in keyof RequestContext]: (args: FastifyHandlerContext & {
|
|
92
|
+
params: Record<string, string>;
|
|
93
|
+
headers: RequestContextHeaderValues<RequestContext[Name]>;
|
|
94
|
+
}) => z.output<RequestContext[Name]['context']> | Promise<z.output<RequestContext[Name]['context']>> };
|
|
95
|
+
interface KizunaPluginOptions extends FastifyOptions {
|
|
96
|
+
/**
|
|
97
|
+
* The API object built by `server.api`.
|
|
98
|
+
*/
|
|
99
|
+
api: FastifyApi;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Fastify plugin that mounts a ts-kizuna API.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* const app = Fastify();
|
|
106
|
+
* await api.mount(app);
|
|
107
|
+
*/
|
|
108
|
+
declare const fastifyKizuna: (app: FastifyInstance, options: KizunaPluginOptions) => Promise<void>;
|
|
109
|
+
type ServerContract<R extends Routes, Schemes extends Record<string, SecurityScheme>, Auth, RequestContext extends Record<string, RequestContextSchema>, Plugins extends ContractPlugins, J extends Jobs = Jobs> = Contract<R, Record<string, TagOptions>, string, Schemes, Auth, RequestContext, Plugins, J>;
|
|
110
|
+
interface Server<R extends Routes, Schemes extends Record<string, SecurityScheme>, Auth, RequestContext extends Record<string, RequestContextSchema>, Plugins extends ContractPlugins, J extends Jobs = Jobs> {
|
|
111
|
+
/**
|
|
112
|
+
* Define a guard for one of the contract's identities. It runs before the
|
|
113
|
+
* handlers of every route whose `auth` entry requires the identity, and
|
|
114
|
+
* receives the credential its method extracted (`bearer`, `apiKey`, or
|
|
115
|
+
* `basic`, `null` when absent). Return the identity's context and access
|
|
116
|
+
* fields to allow the request, or call `deny(status, detail)`.
|
|
117
|
+
*/
|
|
118
|
+
guard<const Name extends Extract<keyof Schemes, string>>(name: Name, run: GuardFns<Schemes, GuardParams<R, Auth, Name>>[Name]): GuardRun<FastifyHandlerContext>;
|
|
119
|
+
/**
|
|
120
|
+
* Define a request context resolver declared on the contract. It runs on
|
|
121
|
+
* every route, public ones included, and never denies.
|
|
122
|
+
*/
|
|
123
|
+
requestContext<const Name extends Extract<keyof RequestContext, string>>(name: Name, run: RequestResolverFns<RequestContext>[Name]): RequestContextRun<FastifyHandlerContext>;
|
|
124
|
+
/**
|
|
125
|
+
* Write typed handlers for the contract or one of its route groups.
|
|
126
|
+
*/
|
|
127
|
+
router: {
|
|
128
|
+
<const GroupOrRoutes extends Extract<keyof Router<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>, string> | Routes>(group: GroupOrRoutes, router: GroupRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>, GroupOrRoutes>): GroupRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>, GroupOrRoutes>;
|
|
129
|
+
(router: Router<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>): Router<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Write a handler for each of the contract's jobs.
|
|
133
|
+
*
|
|
134
|
+
* Pass a `transport` to say where a queued job goes. Without one, `queue`
|
|
135
|
+
* runs the job in this process and it is lost on a crash.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* export const jobs = server.jobs({
|
|
139
|
+
* sendDigests: async () => ({
|
|
140
|
+
* status: 200,
|
|
141
|
+
* body: {
|
|
142
|
+
* sent: await sendPendingDigests(),
|
|
143
|
+
* },
|
|
144
|
+
* }),
|
|
145
|
+
* });
|
|
146
|
+
*/
|
|
147
|
+
jobs(handlers: JobsRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>): JobsRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>;
|
|
148
|
+
/**
|
|
149
|
+
* Assemble the router, guards, and job handlers into the api object.
|
|
150
|
+
*/
|
|
151
|
+
api(options: {
|
|
152
|
+
router: Router<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>;
|
|
153
|
+
} & (string extends keyof Schemes ? {
|
|
154
|
+
guards?: undefined;
|
|
155
|
+
} : {
|
|
156
|
+
guards: NoInfer<GuardsForSchemes<Schemes>>;
|
|
157
|
+
}) & (string extends keyof J ? {
|
|
158
|
+
jobs?: undefined;
|
|
159
|
+
} : {
|
|
160
|
+
jobs: NoInfer<JobsRouter<ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>>>;
|
|
161
|
+
}) & (string extends keyof RequestContext ? {
|
|
162
|
+
requestContext?: undefined;
|
|
163
|
+
} : {
|
|
164
|
+
requestContext: NoInfer<{ [Name in keyof RequestContext]: RequestContextRun<FastifyHandlerContext> }>;
|
|
165
|
+
}) & (string extends keyof Plugins ? {
|
|
166
|
+
plugins?: undefined;
|
|
167
|
+
} : {
|
|
168
|
+
plugins: PluginImplementations<Plugins, FastifyHandlerContext>;
|
|
169
|
+
})): FastifyApi<R>;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Turn a contract into a server handle: the serving counterpart to `Kizuna`.
|
|
173
|
+
* Keep the instance and use `server.guard` to define guards, `server.router`
|
|
174
|
+
* to write typed handlers, and `server.api` to assemble them.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* const server = new KizunaServer(contract);
|
|
178
|
+
*
|
|
179
|
+
* const requireUser = server.guard('user', ({ bearer, deny }) => {
|
|
180
|
+
* const session = bearer && sessions.get(bearer.token);
|
|
181
|
+
* return session ? { userId: session.userId } : deny(401, 'Unauthorized');
|
|
182
|
+
* });
|
|
183
|
+
*
|
|
184
|
+
* export const api = server.api({
|
|
185
|
+
* router,
|
|
186
|
+
* guards: {
|
|
187
|
+
* user: requireUser,
|
|
188
|
+
* },
|
|
189
|
+
* });
|
|
190
|
+
*/
|
|
191
|
+
declare class KizunaServer<const R extends Routes, Schemes extends Record<string, SecurityScheme>, Auth, RequestContext extends Record<string, RequestContextSchema>, Plugins extends ContractPlugins, J extends Jobs = Jobs> implements Server<R, Schemes, Auth, RequestContext, Plugins, J> {
|
|
192
|
+
readonly guard: Server<R, Schemes, Auth, RequestContext, Plugins, J>['guard'];
|
|
193
|
+
readonly requestContext: Server<R, Schemes, Auth, RequestContext, Plugins, J>['requestContext'];
|
|
194
|
+
readonly router: Server<R, Schemes, Auth, RequestContext, Plugins, J>['router'];
|
|
195
|
+
readonly jobs: Server<R, Schemes, Auth, RequestContext, Plugins, J>['jobs'];
|
|
196
|
+
readonly api: Server<R, Schemes, Auth, RequestContext, Plugins, J>['api'];
|
|
197
|
+
constructor(contract: ServerContract<R, Schemes, Auth, RequestContext, Plugins, J>, options?: ServerOptions);
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
export { FastifyApi, FastifyHandlerContext, FastifyOptions, FastifyPreHandler, JobsRouter, KizunaPluginOptions, KizunaServer, RouteHandler, Router, Server, fastifyKizuna };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fastifyPlugin from "fastify-plugin";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { GUARDS_META, JOBS_META, REQUEST_CONTEXT_META, ROUTER_META, SCHEMES_META, assembleApi, createAdapter, jobRouter, jobRoutes, jobRunnerFrom, pluginExportsOf, pluginRouterOf, pluginRoutesOf, renderJsonResult, warnUnsupportedJobOptions } from "@ts-kizuna/core/adapter";
|
|
4
|
+
//#region src/server.ts
|
|
5
|
+
/**
|
|
6
|
+
* Write a web `Response` to a Fastify reply. Plugins answer in web terms to stay
|
|
7
|
+
* adapter-agnostic, so the translation belongs here.
|
|
8
|
+
*/
|
|
9
|
+
const writeWebResponse = async (response, reply) => {
|
|
10
|
+
if (!(response instanceof globalThis.Response)) return;
|
|
11
|
+
reply.hijack();
|
|
12
|
+
reply.raw.statusCode = response.status;
|
|
13
|
+
response.headers.forEach((value, name) => reply.raw.setHeader(name, value));
|
|
14
|
+
if (!response.body) {
|
|
15
|
+
reply.raw.end();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
Readable.fromWeb(response.body).pipe(reply.raw);
|
|
19
|
+
};
|
|
20
|
+
const adapter = createAdapter({
|
|
21
|
+
buildHandlerContext: (adapterRequest, { reply }) => ({
|
|
22
|
+
request: adapterRequest.request,
|
|
23
|
+
reply
|
|
24
|
+
}),
|
|
25
|
+
respond: (result, { reply, formatError }) => {
|
|
26
|
+
if (result.kind === "handler-error") throw result.error;
|
|
27
|
+
if (result.kind === "raw-response") {
|
|
28
|
+
writeWebResponse(result.response, reply);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const rendered = renderJsonResult(result, formatError, reply.request);
|
|
32
|
+
for (const [key, value] of Object.entries(rendered.headers)) reply.header(key, value);
|
|
33
|
+
if (rendered.body === void 0) reply.status(rendered.status).send();
|
|
34
|
+
else if (rendered.raw) {
|
|
35
|
+
const body = rendered.body;
|
|
36
|
+
reply.status(rendered.status).send(typeof body === "string" || Buffer.isBuffer(body) ? body : Buffer.from(body));
|
|
37
|
+
} else reply.status(rendered.status).send(rendered.body);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Fastify plugin that mounts a ts-kizuna API.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const app = Fastify();
|
|
45
|
+
* await api.mount(app);
|
|
46
|
+
*/
|
|
47
|
+
const fastifyKizuna = fastifyPlugin(async (app, options) => {
|
|
48
|
+
const { api } = options;
|
|
49
|
+
const guards = api[GUARDS_META];
|
|
50
|
+
const schemes = api[SCHEMES_META];
|
|
51
|
+
const requestContext = api[REQUEST_CONTEXT_META];
|
|
52
|
+
const pluginExports = pluginExportsOf(api);
|
|
53
|
+
const jobsMeta = api[JOBS_META];
|
|
54
|
+
const jobRunner = jobRunnerFrom(jobsMeta);
|
|
55
|
+
const mountRoute = (routeKey, route, lane, resolvedRouter) => {
|
|
56
|
+
app.route({
|
|
57
|
+
method: route.method,
|
|
58
|
+
url: route.path,
|
|
59
|
+
preHandler: [async (request) => {
|
|
60
|
+
request.kizunaRoute = route;
|
|
61
|
+
}],
|
|
62
|
+
handler: async (request, reply) => {
|
|
63
|
+
const adapterRequest = {
|
|
64
|
+
request,
|
|
65
|
+
method: request.method,
|
|
66
|
+
resolution: {
|
|
67
|
+
kind: "pre-resolved",
|
|
68
|
+
routeKey,
|
|
69
|
+
route,
|
|
70
|
+
params: request.params ?? {}
|
|
71
|
+
},
|
|
72
|
+
query: request.query ?? {},
|
|
73
|
+
headers: request.headers,
|
|
74
|
+
readBody: () => request.body
|
|
75
|
+
};
|
|
76
|
+
await adapter.handle({
|
|
77
|
+
routes: lane,
|
|
78
|
+
router: resolvedRouter,
|
|
79
|
+
request: adapterRequest,
|
|
80
|
+
responseContext: {
|
|
81
|
+
reply,
|
|
82
|
+
formatError: options?.formatError
|
|
83
|
+
},
|
|
84
|
+
guards,
|
|
85
|
+
schemes,
|
|
86
|
+
requestContext,
|
|
87
|
+
pluginExports,
|
|
88
|
+
jobs: jobRunner,
|
|
89
|
+
responseValidation: options?.responseValidation
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const mountLane = (lane, resolvedRouter) => {
|
|
95
|
+
const declaredRoutes = [...adapter.eachRoute(lane, resolvedRouter)].sort((left, right) => Number(right.route.method === "HEAD") - Number(left.route.method === "HEAD"));
|
|
96
|
+
for (const { routeKey, route } of declaredRoutes) mountRoute(routeKey, route, lane, resolvedRouter);
|
|
97
|
+
};
|
|
98
|
+
mountLane(api.routes, api[ROUTER_META]);
|
|
99
|
+
mountLane(pluginRoutesOf(api), pluginRouterOf(api));
|
|
100
|
+
if (jobsMeta) {
|
|
101
|
+
const routes = jobRoutes(jobsMeta);
|
|
102
|
+
const router = jobRouter(jobsMeta);
|
|
103
|
+
for (const [routeKey, route] of Object.entries(routes)) mountRoute(routeKey, route, routes, router);
|
|
104
|
+
}
|
|
105
|
+
}, { name: "@ts-kizuna/fastify" });
|
|
106
|
+
const createServerSurface = (contract, options) => {
|
|
107
|
+
warnUnsupportedJobOptions(contract.jobs, options?.jobTransport);
|
|
108
|
+
return {
|
|
109
|
+
guard: (_name, run) => run,
|
|
110
|
+
requestContext: (_name, run) => run,
|
|
111
|
+
router: (groupOrRouter, groupRouter) => groupRouter ?? groupOrRouter,
|
|
112
|
+
jobs: (handlers) => handlers,
|
|
113
|
+
api: ({ jobs, ...parts }) => {
|
|
114
|
+
const api = Object.assign(assembleApi(contract, parts), { [JOBS_META]: contract.jobs ? {
|
|
115
|
+
jobs: contract.jobs,
|
|
116
|
+
handlers: jobs ?? {},
|
|
117
|
+
config: contract.jobsConfig,
|
|
118
|
+
transport: options?.jobTransport,
|
|
119
|
+
onError: options?.onJobError
|
|
120
|
+
} : void 0 });
|
|
121
|
+
const plugin = fastifyPlugin(async (app, pluginOptions) => {
|
|
122
|
+
await fastifyKizuna(app, {
|
|
123
|
+
...pluginOptions,
|
|
124
|
+
api
|
|
125
|
+
});
|
|
126
|
+
}, { name: "@ts-kizuna/fastify" });
|
|
127
|
+
return Object.assign(api, {
|
|
128
|
+
plugin,
|
|
129
|
+
mount: async (app, mountOptions) => {
|
|
130
|
+
await app.register(plugin, mountOptions ?? {});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Turn a contract into a server handle: the serving counterpart to `Kizuna`.
|
|
138
|
+
* Keep the instance and use `server.guard` to define guards, `server.router`
|
|
139
|
+
* to write typed handlers, and `server.api` to assemble them.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* const server = new KizunaServer(contract);
|
|
143
|
+
*
|
|
144
|
+
* const requireUser = server.guard('user', ({ bearer, deny }) => {
|
|
145
|
+
* const session = bearer && sessions.get(bearer.token);
|
|
146
|
+
* return session ? { userId: session.userId } : deny(401, 'Unauthorized');
|
|
147
|
+
* });
|
|
148
|
+
*
|
|
149
|
+
* export const api = server.api({
|
|
150
|
+
* router,
|
|
151
|
+
* guards: {
|
|
152
|
+
* user: requireUser,
|
|
153
|
+
* },
|
|
154
|
+
* });
|
|
155
|
+
*/
|
|
156
|
+
var KizunaServer = class {
|
|
157
|
+
constructor(contract, options) {
|
|
158
|
+
Object.assign(this, createServerSurface(contract, options));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
//#endregion
|
|
162
|
+
export { KizunaServer, fastifyKizuna };
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ts-kizuna/fastify",
|
|
3
|
+
"version": "1.49.5",
|
|
4
|
+
"description": "Fastify adapter for ts-kizuna",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ts-kizuna",
|
|
7
|
+
"typescript",
|
|
8
|
+
"zod",
|
|
9
|
+
"openapi",
|
|
10
|
+
"contract-first",
|
|
11
|
+
"type-safe",
|
|
12
|
+
"rest",
|
|
13
|
+
"api",
|
|
14
|
+
"fastify",
|
|
15
|
+
"adapter",
|
|
16
|
+
"server"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://ts-kizuna.com/docs/adapters/fastify",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ts-kizuna/kizuna.git",
|
|
23
|
+
"directory": "packages/fastify"
|
|
24
|
+
},
|
|
25
|
+
"bugs": "https://github.com/ts-kizuna/kizuna/issues",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=24"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.cjs",
|
|
32
|
+
"module": "./dist/index.mjs",
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
38
|
+
"default": "./dist/index.mjs"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/index.d.cts",
|
|
42
|
+
"default": "./dist/index.cjs"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"kizuna": {
|
|
47
|
+
"entries": {
|
|
48
|
+
".": "server"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist"
|
|
53
|
+
],
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"fastify-plugin": "^5.1.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"fastify": "^5.0.0",
|
|
62
|
+
"zod": "^4.0.0",
|
|
63
|
+
"@ts-kizuna/core": "1.49.5"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"fastify": "^5.0.0",
|
|
67
|
+
"tsdown": "^0.21.0",
|
|
68
|
+
"typescript": "^5.6.3",
|
|
69
|
+
"zod": "^4.0.0",
|
|
70
|
+
"@ts-kizuna/core": "1.49.5"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsdown src/index.ts --format esm,cjs --dts --clean --external @ts-kizuna/core --external fastify --external fastify-plugin --external zod",
|
|
74
|
+
"typecheck": "tsc --noEmit"
|
|
75
|
+
}
|
|
76
|
+
}
|