@kiyasov/platform-hono 1.0.0 → 1.0.2

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.
Files changed (53) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/dist/cjs/index.d.ts +1 -0
  3. package/dist/cjs/index.js +1 -0
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/src/adapters/hono-adapter.d.ts +58 -0
  6. package/dist/cjs/src/adapters/hono-adapter.js +206 -0
  7. package/dist/cjs/src/adapters/hono-adapter.js.map +1 -0
  8. package/dist/cjs/src/adapters/index.d.ts +1 -0
  9. package/dist/cjs/src/adapters/index.js +18 -0
  10. package/dist/cjs/src/adapters/index.js.map +1 -0
  11. package/dist/cjs/src/drivers/graphql.driver.d.ts +16 -0
  12. package/dist/cjs/src/drivers/graphql.driver.js +99 -0
  13. package/dist/cjs/src/drivers/graphql.driver.js.map +1 -0
  14. package/dist/cjs/src/drivers/index.d.ts +1 -0
  15. package/dist/cjs/src/drivers/index.js +18 -0
  16. package/dist/cjs/src/drivers/index.js.map +1 -0
  17. package/dist/cjs/src/interfaces/index.d.ts +1 -0
  18. package/dist/cjs/src/interfaces/index.js +18 -0
  19. package/dist/cjs/src/interfaces/index.js.map +1 -0
  20. package/dist/cjs/src/interfaces/nest-hono-application.interface.d.ts +18 -0
  21. package/dist/cjs/src/interfaces/nest-hono-application.interface.js +3 -0
  22. package/dist/cjs/src/interfaces/nest-hono-application.interface.js.map +1 -0
  23. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  24. package/dist/esm/index.d.ts +1 -0
  25. package/dist/esm/index.js +1 -0
  26. package/dist/esm/index.js.map +1 -1
  27. package/dist/esm/src/adapters/hono-adapter.d.ts +58 -0
  28. package/dist/esm/src/adapters/hono-adapter.js +202 -0
  29. package/dist/esm/src/adapters/hono-adapter.js.map +1 -0
  30. package/dist/esm/src/adapters/index.d.ts +1 -0
  31. package/dist/esm/src/adapters/index.js +2 -0
  32. package/dist/esm/src/adapters/index.js.map +1 -0
  33. package/dist/esm/src/drivers/graphql.driver.d.ts +16 -0
  34. package/dist/esm/src/drivers/graphql.driver.js +95 -0
  35. package/dist/esm/src/drivers/graphql.driver.js.map +1 -0
  36. package/dist/esm/src/drivers/index.d.ts +1 -0
  37. package/dist/esm/src/drivers/index.js +2 -0
  38. package/dist/esm/src/drivers/index.js.map +1 -0
  39. package/dist/esm/src/interfaces/index.d.ts +1 -0
  40. package/dist/esm/src/interfaces/index.js +2 -0
  41. package/dist/esm/src/interfaces/index.js.map +1 -0
  42. package/dist/esm/src/interfaces/nest-hono-application.interface.d.ts +18 -0
  43. package/dist/esm/src/interfaces/nest-hono-application.interface.js +2 -0
  44. package/dist/esm/src/interfaces/nest-hono-application.interface.js.map +1 -0
  45. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  46. package/index.ts +1 -0
  47. package/package.json +13 -3
  48. package/src/adapters/hono-adapter.ts +293 -0
  49. package/src/adapters/index.ts +1 -0
  50. package/src/drivers/graphql.driver.ts +126 -0
  51. package/src/drivers/index.ts +1 -0
  52. package/src/interfaces/index.ts +1 -0
  53. package/src/interfaces/nest-hono-application.interface.ts +72 -0
package/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./src/adapters";
2
2
  export * from "./src/interfaces";
3
+ export * from "./src/drivers";
package/package.json CHANGED
@@ -1,22 +1,32 @@
1
1
  {
2
2
  "name": "@kiyasov/platform-hono",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Nest adapter for Hono",
5
5
  "author": "Islam Kiiasov",
6
6
  "license": "MIT",
7
- "main": "dist/cjs/index.js",
8
- "module": "dist/esm/index.js",
7
+ "main": "./dist/cjs/index.js",
8
+ "module": "./dist/esm/index.js",
9
+ "exports": {
10
+ ".": {
11
+ "require": "./dist/cjs/index.js",
12
+ "import": "./dist/esm/index.js"
13
+ }
14
+ },
9
15
  "publishConfig": {
10
16
  "access": "public"
11
17
  },
12
18
  "dependencies": {
19
+ "@hono/graphql-server": "^0.4.3",
13
20
  "@hono/node-server": "^1.11.2",
14
21
  "hono": "^4.4.3"
15
22
  },
16
23
  "devDependencies": {
24
+ "@apollo/server": "^4.10.4",
25
+ "@nestjs/apollo": "^12.1.0",
17
26
  "@nestjs/cli": "^10.3.2",
18
27
  "@nestjs/common": "^10.3.8",
19
28
  "@nestjs/core": "^10.3.8",
29
+ "@nestjs/graphql": "^12.1.1",
20
30
  "@swc/cli": "^0.3.12",
21
31
  "@swc/core": "^1.5.24",
22
32
  "reflect-metadata": "^0.2.2",
@@ -0,0 +1,293 @@
1
+ import { Server } from "node:net";
2
+ import { HttpBindings, createAdaptorServer } from "@hono/node-server";
3
+ import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
4
+ import { RequestMethod } from "@nestjs/common";
5
+ import { HttpStatus, Logger } from "@nestjs/common";
6
+ import { bodyLimit } from "hono/body-limit";
7
+ import {
8
+ ErrorHandler,
9
+ NestApplicationOptions,
10
+ RequestHandler,
11
+ } from "@nestjs/common/interfaces";
12
+ import {
13
+ ServeStaticOptions,
14
+ serveStatic,
15
+ } from "@hono/node-server/serve-static";
16
+ import { AbstractHttpAdapter } from "@nestjs/core/adapters/http-adapter";
17
+ import { Context, HonoRequest, Next } from "hono";
18
+ import { Hono } from "hono";
19
+ import { cors } from "hono/cors";
20
+ import { RedirectStatusCode, StatusCode } from "hono/utils/http-status";
21
+ import * as http from "http";
22
+ import { Http2SecureServer, Http2Server } from "http2";
23
+ import * as https from "https";
24
+ import { TypeBodyParser } from "../interfaces";
25
+
26
+ type HonoHandler = RequestHandler<HonoRequest, Context>;
27
+
28
+ type ServerType = Server | Http2Server | Http2SecureServer;
29
+
30
+ /**
31
+ * Adapter for using Hono with NestJS.
32
+ */
33
+ export class HonoAdapter extends AbstractHttpAdapter<
34
+ ServerType,
35
+ HonoRequest,
36
+ Context
37
+ > {
38
+ protected readonly instance: Hono<{ Bindings: HttpBindings }>;
39
+
40
+ constructor() {
41
+ super(new Hono());
42
+ }
43
+
44
+ private getRouteAndHandler(
45
+ pathOrHandler: string | HonoHandler,
46
+ handler?: HonoHandler
47
+ ): [string, HonoHandler] {
48
+ let path = typeof pathOrHandler === "function" ? "" : pathOrHandler;
49
+ handler = typeof pathOrHandler === "function" ? pathOrHandler : handler;
50
+ return [path, handler];
51
+ }
52
+
53
+ private createRouteHandler(routeHandler: HonoHandler) {
54
+ return async (ctx: Context, next: Next) => {
55
+ await routeHandler(ctx.req, ctx, next);
56
+ return this.send(ctx);
57
+ };
58
+ }
59
+
60
+ private send(ctx: Context) {
61
+ const body = ctx.get("body");
62
+ return typeof body === "string" ? ctx.text(body) : ctx.json(body);
63
+ }
64
+
65
+ public get(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
66
+ const [routePath, routeHandler] = this.getRouteAndHandler(
67
+ pathOrHandler,
68
+ handler
69
+ );
70
+ this.instance.get(routePath, this.createRouteHandler(routeHandler));
71
+ }
72
+
73
+ public post(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
74
+ const [routePath, routeHandler] = this.getRouteAndHandler(
75
+ pathOrHandler,
76
+ handler
77
+ );
78
+ this.instance.post(routePath, this.createRouteHandler(routeHandler));
79
+ }
80
+
81
+ public put(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
82
+ const [routePath, routeHandler] = this.getRouteAndHandler(
83
+ pathOrHandler,
84
+ handler
85
+ );
86
+ this.instance.put(routePath, this.createRouteHandler(routeHandler));
87
+ }
88
+
89
+ public delete(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
90
+ const [routePath, routeHandler] = this.getRouteAndHandler(
91
+ pathOrHandler,
92
+ handler
93
+ );
94
+ this.instance.delete(routePath, this.createRouteHandler(routeHandler));
95
+ }
96
+
97
+ public use(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
98
+ const [routePath, routeHandler] = this.getRouteAndHandler(
99
+ pathOrHandler,
100
+ handler
101
+ );
102
+ this.instance.use(routePath, this.createRouteHandler(routeHandler));
103
+ }
104
+
105
+ public patch(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
106
+ const [routePath, routeHandler] = this.getRouteAndHandler(
107
+ pathOrHandler,
108
+ handler
109
+ );
110
+ this.instance.patch(routePath, this.createRouteHandler(routeHandler));
111
+ }
112
+
113
+ public options(pathOrHandler: string | HonoHandler, handler?: HonoHandler) {
114
+ const [routePath, routeHandler] = this.getRouteAndHandler(
115
+ pathOrHandler,
116
+ handler
117
+ );
118
+ this.instance.options(routePath, this.createRouteHandler(routeHandler));
119
+ }
120
+
121
+ public async reply(ctx: Context, body: any, statusCode?: StatusCode) {
122
+ if (statusCode) ctx.status(statusCode);
123
+ const responseContentType = this.getHeader(ctx, "Content-Type");
124
+ if (
125
+ !responseContentType?.startsWith("application/json") &&
126
+ body?.statusCode >= HttpStatus.BAD_REQUEST
127
+ ) {
128
+ Logger.warn(
129
+ "Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses"
130
+ );
131
+ this.setHeader(ctx, "Content-Type", "application/json");
132
+ }
133
+ ctx.set("body", body);
134
+ }
135
+
136
+ public status(ctx: Context, statusCode: StatusCode) {
137
+ ctx.status(statusCode);
138
+ }
139
+
140
+ public async end() {
141
+ return RESPONSE_ALREADY_SENT;
142
+ }
143
+
144
+ public render(response: any, view: string, options: any) {
145
+ throw new Error("Method not implemented.");
146
+ }
147
+
148
+ public redirect(ctx: Context, statusCode: RedirectStatusCode, url: string) {
149
+ ctx.redirect(url, statusCode);
150
+ }
151
+
152
+ public setErrorHandler(handler: ErrorHandler) {
153
+ this.instance.onError(async (err: Error, ctx: Context) => {
154
+ await handler(err, ctx.req, ctx);
155
+ return this.send(ctx);
156
+ });
157
+ }
158
+
159
+ public setNotFoundHandler(handler: RequestHandler) {
160
+ this.instance.notFound(async (ctx: Context) => {
161
+ await handler(ctx.req, ctx);
162
+ return this.send(ctx);
163
+ });
164
+ }
165
+
166
+ public useStaticAssets(path: string, options: ServeStaticOptions) {
167
+ Logger.log("Registering static assets middleware");
168
+ this.instance.use(path, serveStatic(options));
169
+ }
170
+
171
+ public setViewEngine(options: any | string) {
172
+ throw new Error("Method not implemented.");
173
+ }
174
+
175
+ public isHeadersSent(ctx: Context): boolean {
176
+ return true;
177
+ }
178
+
179
+ public getHeader?(ctx: Context, name: string) {
180
+ return ctx.req.header(name);
181
+ }
182
+
183
+ public setHeader(ctx: Context, name: string, value: string) {
184
+ ctx.header(name, value);
185
+ }
186
+
187
+ public appendHeader?(ctx: Context, name: string, value: string) {
188
+ ctx.res.headers.append(name, value);
189
+ }
190
+
191
+ public getRequestHostname(ctx: Context): string {
192
+ return ctx.req.header().host;
193
+ }
194
+
195
+ public getRequestMethod(request: HonoRequest): string {
196
+ return request.method;
197
+ }
198
+
199
+ public getRequestUrl(request: HonoRequest): string {
200
+ return request.url;
201
+ }
202
+
203
+ public enableCors(options: any) {
204
+ this.instance.use(cors(options));
205
+ }
206
+
207
+ public useBodyParser(type: TypeBodyParser, bodyLimit: number = 1e6) {
208
+ Logger.log("Registering body parser middleware");
209
+ this.instance.use(this.bodyLimit(bodyLimit), async (ctx, next) => {
210
+ const contentType = ctx.req.header("content-type");
211
+ switch (type) {
212
+ case "application/json":
213
+ if (contentType === "application/json")
214
+ (ctx.req as any).body = await ctx.req.json();
215
+ break;
216
+ case "text/plain":
217
+ if (contentType === "text/plain") {
218
+ (ctx.req as any).rawBody = Buffer.from(await ctx.req.text());
219
+ (ctx.req as any).body = await ctx.req.json();
220
+ }
221
+ break;
222
+ default:
223
+ (ctx.req as any).body = await ctx.req.parseBody({ all: true });
224
+ break;
225
+ }
226
+ await next();
227
+ });
228
+ }
229
+
230
+ public close(): Promise<void> {
231
+ return new Promise((resolve) => this.httpServer.close(() => resolve()));
232
+ }
233
+
234
+ public initHttpServer(options: NestApplicationOptions) {
235
+ const isHttpsEnabled = options?.httpsOptions;
236
+ const createServer = isHttpsEnabled
237
+ ? https.createServer
238
+ : http.createServer;
239
+ this.httpServer = createAdaptorServer({
240
+ fetch: this.instance.fetch,
241
+ createServer,
242
+ overrideGlobalObjects: false,
243
+ });
244
+ }
245
+
246
+ public getType(): string {
247
+ return "hono";
248
+ }
249
+
250
+ public registerParserMiddleware(prefix?: string, rawBody?: boolean) {
251
+ Logger.log("Registering parser middleware");
252
+ this.useBodyParser("application/x-www-form-urlencoded");
253
+ this.useBodyParser("application/json");
254
+ this.useBodyParser("text/plain");
255
+ }
256
+
257
+ public async createMiddlewareFactory(requestMethod: RequestMethod) {
258
+ return (path: string, callback: Function) => {
259
+ const routeMethodsMap = {
260
+ [RequestMethod.ALL]: this.instance.all,
261
+ [RequestMethod.DELETE]: this.instance.delete,
262
+ [RequestMethod.GET]: this.instance.get,
263
+ [RequestMethod.OPTIONS]: this.instance.options,
264
+ [RequestMethod.PATCH]: this.instance.patch,
265
+ [RequestMethod.POST]: this.instance.post,
266
+ [RequestMethod.PUT]: this.instance.put,
267
+ };
268
+ const routeMethod = (
269
+ routeMethodsMap[requestMethod] || this.instance.get
270
+ ).bind(this.instance);
271
+ routeMethod(path, async (ctx: Context, next: Function) => {
272
+ await callback(ctx.req, ctx, next);
273
+ });
274
+ };
275
+ }
276
+
277
+ public applyVersionFilter(): () => () => any {
278
+ throw new Error("Versioning not yet supported in Hono");
279
+ }
280
+
281
+ public listen(port: string | number, ...args: any[]): ServerType {
282
+ return this.httpServer.listen(port, ...args);
283
+ }
284
+
285
+ public bodyLimit(maxSize: number) {
286
+ return bodyLimit({
287
+ maxSize,
288
+ onError: () => {
289
+ throw new Error("Body too large");
290
+ },
291
+ });
292
+ }
293
+ }
@@ -0,0 +1 @@
1
+ export * from "./hono-adapter";
@@ -0,0 +1,126 @@
1
+ import { ApolloServer, BaseContext, HeaderMap } from '@apollo/server';
2
+ import { AbstractGraphQLDriver } from '@nestjs/graphql';
3
+ import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
4
+ import { ApolloDriverConfig } from '@nestjs/apollo';
5
+ import { Context, HonoRequest } from 'hono';
6
+ import { StatusCode } from 'hono/utils/http-status';
7
+ import { Logger } from '@nestjs/common';
8
+
9
+ export class HonoGraphQLDriver<
10
+ T extends Record<string, any> = ApolloDriverConfig,
11
+ > extends AbstractGraphQLDriver {
12
+ protected apolloServer: ApolloServer<BaseContext>;
13
+
14
+ get instance(): ApolloServer<BaseContext> {
15
+ return this.apolloServer;
16
+ }
17
+
18
+ async start(options: T): Promise<void> {
19
+ const { httpAdapter } = this.httpAdapterHost;
20
+ const platformName = httpAdapter.getType();
21
+
22
+ if (platformName !== 'hono') {
23
+ throw new Error('This driver is only compatible with the Hono platform');
24
+ }
25
+
26
+ return this.registerHono(options);
27
+ }
28
+
29
+ protected async registerHono(
30
+ options: T,
31
+ { preStartHook }: { preStartHook?: () => void } = {},
32
+ ) {
33
+ const { path, typeDefs, resolvers, schema } = options;
34
+ const { httpAdapter } = this.httpAdapterHost;
35
+ const app = httpAdapter.getInstance();
36
+ const drainHttpServerPlugin = ApolloServerPluginDrainHttpServer({
37
+ httpServer: httpAdapter.getHttpServer(),
38
+ });
39
+
40
+ preStartHook?.();
41
+
42
+ const server = new ApolloServer({
43
+ typeDefs,
44
+ resolvers,
45
+ schema,
46
+ ...options,
47
+ plugins: (options.plugins || []).concat(drainHttpServerPlugin),
48
+ });
49
+
50
+ await server.start();
51
+
52
+ app.use(path, async (ctx: Context) => {
53
+ const bodyData = await this.parseBody(ctx.req);
54
+
55
+ const httpGraphQLResponse = await server.executeHTTPGraphQLRequest({
56
+ httpGraphQLRequest: {
57
+ body: bodyData,
58
+ method: ctx.req.method,
59
+ headers: this.httpHeadersToMap(ctx.req.raw.headers),
60
+ search: new URL(ctx.req.url).search,
61
+ },
62
+ context: options?.context ?? (() => Promise.resolve({} as BaseContext)),
63
+ });
64
+
65
+ const { headers, body, status } = httpGraphQLResponse;
66
+
67
+ for (const [headerKey, headerValue] of headers) {
68
+ ctx.header(headerKey, headerValue);
69
+ }
70
+
71
+ ctx.status(status === undefined ? 200 : (status as StatusCode));
72
+
73
+ if (body.kind === 'complete') {
74
+ return ctx.body(body.string);
75
+ }
76
+
77
+ const readableStream = new ReadableStream({
78
+ async start(controller) {
79
+ for await (const chunk of body.asyncIterator) {
80
+ controller.enqueue(new TextEncoder().encode(chunk));
81
+ }
82
+ controller.close();
83
+ },
84
+ });
85
+
86
+ return new Response(readableStream, {
87
+ headers: { 'Content-Type': 'application/octet-stream' },
88
+ });
89
+ });
90
+
91
+ this.apolloServer = server;
92
+ }
93
+
94
+ public stop() {
95
+ return this.apolloServer?.stop();
96
+ }
97
+
98
+ private httpHeadersToMap(headers: Headers) {
99
+ const map = new HeaderMap();
100
+ headers.forEach((value, key) => map.set(key, value));
101
+ return map;
102
+ }
103
+
104
+ private async parseBody(req: HonoRequest): Promise<Record<string, unknown>> {
105
+ const contentType = req.header('content-type');
106
+ if (contentType === 'application/graphql')
107
+ return { query: await req.text() };
108
+ if (contentType === 'application/json')
109
+ return req.json().catch(this.logError);
110
+ if (contentType === 'application/x-www-form-urlencoded')
111
+ return this.parseFormURL(req);
112
+ return {};
113
+ }
114
+
115
+ private logError(e: unknown): void {
116
+ if (e instanceof Error) {
117
+ Logger.error(e.stack || e.message);
118
+ }
119
+ throw new Error(`POST body sent invalid JSON: ${e}`);
120
+ }
121
+
122
+ private async parseFormURL(req: HonoRequest) {
123
+ const searchParams = new URLSearchParams(await req.text());
124
+ return Object.fromEntries(searchParams.entries());
125
+ }
126
+ }
@@ -0,0 +1 @@
1
+ export * from "./graphql.driver";
@@ -0,0 +1 @@
1
+ export * from './nest-hono-application.interface';
@@ -0,0 +1,72 @@
1
+ import { ServeStaticOptions } from "@hono/node-server/serve-static";
2
+ import { HttpServer, INestApplication } from "@nestjs/common";
3
+ import { Context, Hono, MiddlewareHandler } from "hono";
4
+
5
+ export type TypeBodyParser =
6
+ | "application/json"
7
+ | "text/plain"
8
+ | "application/x-www-form-urlencoded";
9
+
10
+ interface HonoViewOptions {
11
+ engine: string;
12
+ templates: string;
13
+ }
14
+
15
+ /**
16
+ * @publicApi
17
+ */
18
+ export interface NestHonoApplication<TServer extends Hono = Hono>
19
+ extends INestApplication<TServer> {
20
+ /**
21
+ * Returns the underlying HTTP adapter bounded to a Hono app.
22
+ *
23
+ * @returns {HttpServer}
24
+ */
25
+ getHttpAdapter(): HttpServer<Context, MiddlewareHandler, Hono>;
26
+
27
+ /**
28
+ * Register Hono body parsers on the fly.
29
+ *
30
+ * @example
31
+ * // enable the json parser with a parser limit of 50mb
32
+ * app.useBodyParser('application/json', 50 * 1024 * 1024);
33
+ *
34
+ * @returns {this}
35
+ */
36
+ useBodyParser(type: TypeBodyParser, bodyLimit?: number): this;
37
+
38
+ /**
39
+ * Sets a base directory for public assets.
40
+ * Example `app.useStaticAssets('public', { root: '/' })`
41
+ * @returns {this}
42
+ */
43
+ useStaticAssets(path: string, options: ServeStaticOptions): this;
44
+
45
+ /**
46
+ * Sets a view engine for templates (views), for example: `pug`, `handlebars`, or `ejs`.
47
+ *
48
+ * Don't pass in a string. The string type in the argument is for compatibility reason and will cause an exception.
49
+ * @returns {this}
50
+ */
51
+ setViewEngine(options: HonoViewOptions | string): this;
52
+
53
+ /**
54
+ * Starts the application.
55
+ * @returns A Promise that, when resolved, is a reference to the underlying HttpServer.
56
+ */
57
+ listen(
58
+ port: number | string,
59
+ callback?: (err: Error, address: string) => void
60
+ ): Promise<TServer>;
61
+ listen(
62
+ port: number | string,
63
+ address: string,
64
+ callback?: (err: Error, address: string) => void
65
+ ): Promise<TServer>;
66
+ listen(
67
+ port: number | string,
68
+ address: string,
69
+ backlog: number,
70
+ callback?: (err: Error, address: string) => void
71
+ ): Promise<TServer>;
72
+ }