allserver 1.1.0 → 1.3.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Multi-transport and multi-protocol simple RPC server and (optional) client. Boilerplate-less. Opinionated. Minimalistic. DX-first.
4
4
 
5
- Think HTTP, gRPC, GraphQL, WebSockets, Lambda, inter-process, unix sockets, etc Remote Procedure Calls using exactly the same client and server code.
5
+ Think of Remote Procedure Calls using exactly the same client and server code but via multiple protocols/mechanisms such as HTTP, gRPC, GraphQL, WebSockets, job queues, Lambda, inter-process, (Web)Workers, unix sockets, etc etc etc.
6
6
 
7
7
  Should be used in (micro)services where JavaScript is able to run - your computer, Docker, k8s, virtual machines, serverless functions (Lambdas, Google Cloud Functions, Azure Functions, etc), RaspberryPI, SharedWorker, thread, you name it.
8
8
 
@@ -12,6 +12,8 @@ Superpowers the `Allserver` gives you:
12
12
  - Run your HTTP server as gRPC with a single line change (almost).
13
13
  - Serve same logic via HTTP and gRPC (or more) simultaneously in the same node.js process.
14
14
  - Deploy and run your HTTP server on AWS Lambda with no code changes.
15
+ - Embed same exact code into your existing Express.js, no need to implement any handlers/middleware.
16
+ - Use Redis-backed job queue [BullMQ](https://docs.bullmq.io) to call remote procedures reliably.
15
17
  - And moar!
16
18
 
17
19
  Superpowers the `AllserverClient` gives you:
@@ -88,6 +90,7 @@ When calling a remote procedure I want something which:
88
90
  - Can be easily mapped to any language, any protocol. Especially to upstream GraphQL mutations.
89
91
  - Is simple to read in the source code, just like a method/function call. Without thinking of protocol-level details for every damn call.
90
92
  - Allows me to test gRPC server from my browser/Postman/curl (via HTTP!) by a simple one line config change.
93
+ - Replace flaky HTTP with Kafka
91
94
  - Does not bring tons of npm dependencies with it.
92
95
 
93
96
  Also, the main driving force was my vast experience splitting monolith servers onto (micro)services. Here is how I do it with much success.
@@ -141,15 +144,21 @@ Please note, that Allserver depends only on a single tiny npm module [`stampit`]
141
144
 
142
145
  The default `HttpTransport` is using the [`micro`](http://npmjs.com/package/micro) npm module as an optional dependency.
143
146
 
144
- ```shell script
147
+ ```shell
145
148
  npm i allserver micro
146
149
  ```
147
150
 
151
+ Alternatively, you can embed Allserver into your existing Express.js application:
152
+
153
+ ```shell
154
+ npm i allserver express
155
+ ```
156
+
148
157
  #### Client
149
158
 
150
159
  Optionally, you can use Allserver's built-in client:
151
160
 
152
- ```shell script
161
+ ```shell
153
162
  npm i allserver node-fetch
154
163
  ```
155
164
 
@@ -161,7 +170,7 @@ Or do HTTP requests using any module you like.
161
170
 
162
171
  No dependencies other than `allserver` itself.
163
172
 
164
- ```shell script
173
+ ```shell
165
174
  npm i allserver
166
175
  ```
167
176
 
@@ -175,7 +184,7 @@ Same as the HTTP protocol client above.
175
184
 
176
185
  The default `GrpcTransport` is using the standard the [`@grpc/grpc-js`](https://www.npmjs.com/package/@grpc/grpc-js) npm module as an optional dependency.
177
186
 
178
- ```shell script
187
+ ```shell
179
188
  npm i allserver @grpc/grpc-js@1 @grpc/proto-loader@0.5
180
189
  ```
181
190
 
@@ -185,12 +194,32 @@ Note, with gRPC server and client you'd need to have your own `.proto` file. See
185
194
 
186
195
  Optionally, you can use Allserver's built-in client:
187
196
 
188
- ```shell script
197
+ ```shell
189
198
  npm i allserver @grpc/grpc-js@1 @grpc/proto-loader@0.5
190
199
  ```
191
200
 
192
201
  Or do gRPC requests using any module you like.
193
202
 
203
+ ### [BullMQ](https://docs.bullmq.io) job queue
204
+
205
+ #### Server
206
+
207
+ The default `BullmqTransport` is using the [`bullmq`](https://www.npmjs.com/package/bullmq) module as a dependency, connects to Redis using `Worker` class.
208
+
209
+ ```shell
210
+ npm i allserver bullmq
211
+ ```
212
+
213
+ #### Client
214
+
215
+ Optionally, you can use Allserver's built-in client:
216
+
217
+ ```shell
218
+ npm i allserver bullmq
219
+ ```
220
+
221
+ Or use the `bullmq` module directly. You don't need to use Allserver to call remote procedures. See code example below.
222
+
194
223
  ## Code examples
195
224
 
196
225
  ### Procedures
@@ -297,19 +326,36 @@ const procedures = {
297
326
  async processEntity(_, ctx) {
298
327
  const micro = ctx.allserver.transport.micro; // same as require("micro")
299
328
  const req = ctx.http.req; // node.js Request
300
-
329
+
301
330
  // as a string
302
331
  const text = await micro.text(req);
303
332
  // as a node.js buffer
304
333
  const buffer = await micro.buffer(req);
305
-
306
- // ... process the request here ...
334
+
335
+ // ... process the request here ...
307
336
  },
308
337
  };
309
338
  ```
310
339
 
311
340
  More info can be found in the [`micro`](http://npmjs.com/package/micro) NPM module docs.
312
341
 
342
+ ### HTTP server in Express.js
343
+
344
+ Doesn't require a dedicated client transport. Use the HTTP client below.
345
+
346
+ NB: not yet tested in production.
347
+
348
+ ```js
349
+ const { Allserver, ExpressTransport } = require("allserver");
350
+
351
+ const middleware = Allserver({
352
+ procedures,
353
+ transport: ExpressTransport(),
354
+ }).start();
355
+
356
+ app.use("/route-with-allsever", middleware);
357
+ ```
358
+
313
359
  ### HTTP server in AWS Lambda
314
360
 
315
361
  Doesn't require a dedicated client transport. Use the HTTP client below.
@@ -343,7 +389,7 @@ exports = Allserver({
343
389
 
344
390
  You'd need to install `node-fetch` optional dependency.
345
391
 
346
- ```shell script
392
+ ```shell
347
393
  npm i allserver node-fetch
348
394
  ```
349
395
 
@@ -354,7 +400,7 @@ const { AllserverClient } = require("allserver");
354
400
  // or
355
401
  const AllserverClient = require("allserver/Client");
356
402
 
357
- const client = AllserverClient({ uri: "http://localhost:4000" });
403
+ const client = AllserverClient({ uri: "http://localhost:40000" });
358
404
 
359
405
  const { success, code, message, user } = await client.updateUser({
360
406
  id: "123412341234123412341234",
@@ -363,7 +409,7 @@ const { success, code, message, user } = await client.updateUser({
363
409
  });
364
410
  ```
365
411
 
366
- The `AllserverClient` will issue `HTTP POST` request to this URL: `http://localhost:4000/updateUser`.
412
+ The `AllserverClient` will issue `HTTP POST` request to this URL: `http://localhost:40000/updateUser`.
367
413
  The path of the URL is dynamically taken straight from the `client.updateUser` calling code using the ES6 [`Proxy`](https://stackoverflow.com/a/20147219/188475) class. In other words, `AllserverClient` intercepts non-existent property access.
368
414
 
369
415
  #### Using any HTTP client (axios in this example)
@@ -373,7 +419,7 @@ It's a regular HTTP `POST` call with JSON request and response. URI is `/updateU
373
419
  ```js
374
420
  import axios from "axios";
375
421
 
376
- const response = await axios.post("http://localhost:4000/updateUser", {
422
+ const response = await axios.post("http://localhost:40000/updateUser", {
377
423
  id: "123412341234123412341234",
378
424
  firstName: "Fred",
379
425
  lastName: "Flinstone",
@@ -479,15 +525,80 @@ const { success, code, message, user } = data;
479
525
  1. You can't have `import` statements in your `.proto` file. (Yet.)
480
526
  1. Your server-side `.proto` file must include Allserver's [mandatory declarations](./mandatory.proto). (Yet.)
481
527
 
528
+ ### BullMQ server side
529
+
530
+ Note that we are reusing the `procedures` from the example above.
531
+
532
+ Here is how your BullMQ server can look like:
533
+
534
+ ```js
535
+ const { Allserver, BullmqTransport } = require("allserver");
536
+
537
+ Allserver({
538
+ procedures,
539
+ transport: BullmqTransport({
540
+ connectionOptions: { host: "localhost", port: 6379 },
541
+ }),
542
+ }).start();
543
+ ```
544
+
545
+ ### BullMQ client side
546
+
547
+ #### Using built-in client
548
+
549
+ Note, that this code is **same** as the HTTP client code example above! The only difference is the URI.
550
+
551
+ ```js
552
+ const { AllserverClient, BullmqClientTransport } = require("allserver");
553
+ // or
554
+ const AllserverClient = require("allserver/Client");
555
+
556
+ const client = AllserverClient({ uri: "bullmq://localhost:6379" });
557
+ // or
558
+ const client = AllserverClient({
559
+ transport: BullmqClientTransport({ uri: "redis://localhost:6379" }),
560
+ });
561
+
562
+ const { success, code, message, user } = await client.updateUser({
563
+ id: "123412341234123412341234",
564
+ firstName: "Fred",
565
+ lastName: "Flinstone",
566
+ });
567
+ ```
568
+
569
+ The `bullmq://` schema uses same connection string as Redis: `bullmq://[[username:]password@]host[:port][/database]`
570
+
571
+ #### Using any BullMQ `Queue` class without Allserver
572
+
573
+ ```js
574
+ const { Queue, QueueEvents } = require("bullmq");
575
+
576
+ const queue = new Queue("Allserver", {
577
+ connection: { host: "localhost", port },
578
+ });
579
+ const queueEvents = new QueueEvents("Allserver", {
580
+ connection: { host: "localhost", port },
581
+ });
582
+
583
+ const job = await queue.add("updateUser", {
584
+ id: "123412341234123412341234",
585
+ firstName,
586
+ lastName,
587
+ });
588
+ const data = await job.waitUntilFinished(queueEvents, 30_000);
589
+
590
+ const { success, code, message, user } = data;
591
+ ```
592
+
482
593
  ## `AllserverClient` options
483
594
 
484
595
  **All the arguments are optional.** But either `uri` or `transport` must be provided. We are trying to keep the highest possible DX here.
485
596
 
486
597
  - `uri`<br>
487
- The remote server address string. Out of box supported schemas are: `http`, `https`, `grpc`. (More to come.)
598
+ The remote server address string. Out of box supported schemas are: `http`, `https`, `grpc`, `bullmq`. (More to come.)
488
599
 
489
600
  - `transport`<br>
490
- The transport implementation object. The `uri` is ignored if this option provided. If not given then it will be automatically created based on the `uri` schema. E.g. if it starts with `http://` or `https:/` then `HttpClientTransport` will be used. If starts with `grpc://` then `GrpcClientTransport` will be used.
601
+ The transport implementation object. The `uri` is ignored if this option provided. If not given then it will be automatically created based on the `uri` schema. E.g. if it starts with `http://` or `https://` then `HttpClientTransport` will be used. If starts with `grpc://` then `GrpcClientTransport` will be used. If starts with `bullmq://` then `BullmqClientTransport` is used.
491
602
 
492
603
  - `neverThrow=true`<br>
493
604
  Set it to `false` if you want to get exceptions when there are a network, or a server errors during a procedure call. Otherwise, the standard `{success,code,message}` object is returned from method calls. The Allserver error `code`s are always start with `"ALLSERVER_"`. E.g. `"ALLSERVER_CLIENT_MALFORMED_INTROSPECTION"`.
@@ -653,7 +764,7 @@ Yep.
653
764
  const { AllserverClient } = require("allserver");
654
765
 
655
766
  const client = AllserverClient({
656
- uri: "http://example.com:4000",
767
+ uri: "http://example.com:40000",
657
768
 
658
769
  async before(ctx) {
659
770
  console.log(ctx.procedureName, ctx.arg);
@@ -685,7 +796,7 @@ const { AllserverClient, HttpClientTransport } = require("allserver");
685
796
 
686
797
  const client = AllserverClient({
687
798
  transport: HttpClientTransport({
688
- uri: "http://my-server:4000",
799
+ uri: "http://my-server:40000",
689
800
  headers: { authorization: "Basic my-token" },
690
801
  }),
691
802
  });
@@ -712,7 +823,7 @@ If something more sophisticated is needed - you would need to mangle the `ctx` i
712
823
  const { AllserverClient } = require("allserver");
713
824
 
714
825
  const client = AllserverClient({
715
- uri: "http://my-server:4000",
826
+ uri: "http://my-server:40000",
716
827
  async before(ctx) {
717
828
  console.log(ctx.procedureName, ctx.arg);
718
829
  ctx.http.mode = "cors";
@@ -739,7 +850,7 @@ const MyAllserverClientWithAuth = AllserverClient.defaults({
739
850
  });
740
851
 
741
852
  const client = MyAllserverClientWithAuth({
742
- uri: "http://my-server:4000",
853
+ uri: "http://my-server:40000",
743
854
  });
744
855
  ```
745
856
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allserver",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Multi-protocol simple RPC server and [optional] client. Boilerplate-less. Opinionated. Minimalistic. DX-first.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -34,10 +34,12 @@
34
34
  "devDependencies": {
35
35
  "@grpc/grpc-js": "^1.1.7",
36
36
  "@grpc/proto-loader": "^0.5.5",
37
+ "bullmq": "^3.5.7",
37
38
  "eslint": "^7.9.0",
39
+ "express": "^4.18.2",
38
40
  "lambda-local": "^1.7.3",
39
41
  "micro": "^9.3.4",
40
- "mocha": "^8.1.3",
42
+ "mocha": "^10.2.0",
41
43
  "node-fetch": "^2.6.1",
42
44
  "nyc": "^15.1.0",
43
45
  "prettier": "^2.1.1"
@@ -164,6 +164,7 @@ module.exports = require("stampit")({
164
164
  http() { return require("./HttpClientTransport"); },
165
165
  https() { return require("./HttpClientTransport"); },
166
166
  grpc() { return require("./GrpcClientTransport"); },
167
+ bullmq() { return require("./BullmqClientTransport"); },
167
168
  },
168
169
  },
169
170
 
@@ -0,0 +1,71 @@
1
+ module.exports = require("./ClientTransport").compose({
2
+ name: "BullmqClientTransport",
3
+
4
+ props: {
5
+ Queue: require("bullmq").Queue,
6
+ QueueEvents: require("bullmq").QueueEvents,
7
+ _timeout: 60000,
8
+ _queue: null,
9
+ _queueEvents: null,
10
+ _jobsOptions: null,
11
+ },
12
+
13
+ init({ queueName = "Allserver", connectionOptions, timeout, jobsOptions }) {
14
+ if (!connectionOptions) {
15
+ const bullmqUrl = new URL(this.uri);
16
+ connectionOptions = {
17
+ host: bullmqUrl.hostname,
18
+ port: bullmqUrl.port,
19
+ username: bullmqUrl.username,
20
+ password: bullmqUrl.password,
21
+ db: Number(bullmqUrl.pathname.substr(1)) || 0,
22
+ retryStrategy: null, // only one attempt to connect
23
+ };
24
+ }
25
+ this._timeout = timeout || this._timeout;
26
+
27
+ this._queue = new this.Queue(queueName, { connection: connectionOptions });
28
+ this._queue.on("error", () => {}); // The only reason we subscribe is to avoid bullmq to print errors to console
29
+ this._queueEvents = new this.QueueEvents(queueName, { connection: connectionOptions });
30
+ this._queueEvents.on("error", () => {}); // The only reason we subscribe is to avoid bullmq to print errors to console
31
+
32
+ this._jobsOptions = jobsOptions || {};
33
+ if (this._jobsOptions.removeOnComplete === undefined) this._jobsOptions.removeOnComplete = 100;
34
+ if (this._jobsOptions.removeOnFail === undefined) this._jobsOptions.removeOnFail = 100;
35
+ if (this._jobsOptions.sizeLimit === undefined) this._jobsOptions.sizeLimit = 524288; // max data JSON size is 512KB
36
+ },
37
+
38
+ methods: {
39
+ async introspect(ctx) {
40
+ ctx.procedureName = "introspect";
41
+ const jobReturnResult = await this.call(ctx);
42
+ // The server-side Transport will not have job result if introspection is not available on the server side,
43
+ // but the server itself is up and running processing calls.
44
+ if (jobReturnResult == null) throw new Error("The bullmq introspection job returned nothing");
45
+ return jobReturnResult;
46
+ },
47
+
48
+ async call({ procedureName, bullmq }) {
49
+ try {
50
+ await this._queue.waitUntilReady();
51
+ const job = await bullmq.queue.add(procedureName, bullmq.data, bullmq.jobsOptions);
52
+ return await job.waitUntilFinished(bullmq.queueEvents, this._timeout);
53
+ } catch (err) {
54
+ if (err.code === "ECONNREFUSED") err.noNetToServer = true;
55
+ throw err;
56
+ }
57
+ },
58
+
59
+ createCallContext(defaultCtx) {
60
+ return {
61
+ ...defaultCtx,
62
+ bullmq: {
63
+ data: defaultCtx.arg,
64
+ queue: this._queue,
65
+ queueEvents: this._queueEvents,
66
+ jobsOptions: this._jobsOptions,
67
+ },
68
+ };
69
+ },
70
+ },
71
+ });
@@ -6,7 +6,9 @@ module.exports = require("./ClientTransport").compose({
6
6
  props: {
7
7
  // eslint-disable-next-line no-undef
8
8
  fetch: (typeof self !== "undefined" && self.fetch) || require("node-fetch"),
9
- headers: {},
9
+ headers: {
10
+ "Content-Type": "application/json; charset=utf-8",
11
+ },
10
12
  },
11
13
 
12
14
  init({ headers, fetch }) {
package/src/index.js CHANGED
@@ -11,12 +11,18 @@ module.exports = {
11
11
  get HttpTransport() {
12
12
  return require("./server/HttpTransport");
13
13
  },
14
+ get ExpressTransport() {
15
+ return require("./server/ExpressTransport");
16
+ },
14
17
  get LambdaTransport() {
15
18
  return require("./server/LambdaTransport");
16
19
  },
17
20
  get GrpcTransport() {
18
21
  return require("./server/GrpcTransport");
19
22
  },
23
+ get BullmqTransport() {
24
+ return require("./server/BullmqTransport");
25
+ },
20
26
 
21
27
  // client
22
28
 
@@ -32,4 +38,7 @@ module.exports = {
32
38
  get GrpcClientTransport() {
33
39
  return require("./client/GrpcClientTransport");
34
40
  },
41
+ get BullmqClientTransport() {
42
+ return require("./client/BullmqClientTransport");
43
+ },
35
44
  };
@@ -65,7 +65,7 @@ module.exports = require("stampit")({
65
65
  try {
66
66
  result = await ctx.procedure(ctx.arg, ctx);
67
67
  } catch (err) {
68
- const code = err.code || "ALLSERVER_PROCEDURE_ERROR"
68
+ const code = err.code || "ALLSERVER_PROCEDURE_ERROR";
69
69
  this.logger.error(code, err);
70
70
  ctx.error = err;
71
71
  ctx.result = {
@@ -103,7 +103,7 @@ module.exports = require("stampit")({
103
103
  break;
104
104
  }
105
105
  } catch (err) {
106
- const code = err.code || "ALLSERVER_MIDDLEWARE_ERROR";
106
+ const code = err.code || "ALLSERVER_MIDDLEWARE_ERROR";
107
107
  this.logger.error(code, err);
108
108
  ctx.error = err;
109
109
  ctx.result = {
@@ -136,7 +136,7 @@ module.exports = require("stampit")({
136
136
  // Warning! This call might overwrite an existing result.
137
137
  await this._callMiddlewares(ctx, "after");
138
138
 
139
- this.transport.reply(ctx);
139
+ return this.transport.reply(ctx);
140
140
  },
141
141
 
142
142
  start() {
@@ -0,0 +1,54 @@
1
+ module.exports = require("./Transport").compose({
2
+ name: "BullmqTransport",
3
+
4
+ props: {
5
+ Worker: require("bullmq").Worker,
6
+ _worker: null,
7
+ _queueName: null,
8
+ _connectionOptions: null,
9
+ _workerOptions: null,
10
+ },
11
+
12
+ init({ queueName = "Allserver", connectionOptions, workerOptions }) {
13
+ if (!connectionOptions) {
14
+ connectionOptions = { host: "localhost", port: 6379 };
15
+ }
16
+ this._queueName = queueName || this._queueName;
17
+ this._connectionOptions = connectionOptions || this._connectionOptions;
18
+ this._workerOptions = workerOptions || this._workerOptions || {};
19
+ if (this._workerOptions.autorun === undefined) this._workerOptions.autorun = true;
20
+ },
21
+
22
+ methods: {
23
+ async startServer(defaultCtx) {
24
+ this._worker = new this.Worker(
25
+ this._queueName,
26
+ async (job) => {
27
+ const ctx = { ...defaultCtx, bullmq: { job }, arg: job.data };
28
+ return await ctx.allserver.handleCall(ctx);
29
+ },
30
+ {
31
+ connection: this._connectionOptions,
32
+ ...this._workerOptions,
33
+ }
34
+ );
35
+ return await this._worker.waitUntilReady();
36
+ },
37
+
38
+ reply(ctx) {
39
+ return ctx.result;
40
+ },
41
+
42
+ async stopServer() {
43
+ return this._worker.close();
44
+ },
45
+
46
+ getProcedureName(ctx) {
47
+ return ctx.bullmq.job.name;
48
+ },
49
+
50
+ isIntrospection(ctx) {
51
+ return this.getProcedureName(ctx) === "introspect"; // could be conflicting with procedure name(s)
52
+ },
53
+ },
54
+ });
@@ -0,0 +1,74 @@
1
+ const { parse: parseUrl } = require("url");
2
+
3
+ module.exports = require("./Transport").compose({
4
+ name: "ExpressTransport",
5
+
6
+ props: {
7
+ _express: require("express"),
8
+ },
9
+
10
+ methods: {
11
+ async deserializeRequest(ctx) {
12
+ if (ctx.http.req.deserializationFailed) return false;
13
+ // If there is no body we will use request query (aka search params)
14
+ let arg = ctx.http.query;
15
+ if (ctx.http.req.body && Object.keys(ctx.http.req.body).length > 0) arg = ctx.http.req.body;
16
+ ctx.arg = arg;
17
+ return true;
18
+ },
19
+
20
+ async _handleRequest(ctx) {
21
+ if (await this.deserializeRequest(ctx)) {
22
+ await ctx.allserver.handleCall(ctx);
23
+ } else {
24
+ // HTTP protocol request was malformed (not expected structure).
25
+ // We are not going to process it.
26
+ ctx.result = { success: false, code: "ALLSERVER_BAD_REQUEST", message: "Can't parse JSON" };
27
+ ctx.http.statusCode = 400;
28
+ this.reply(ctx);
29
+ }
30
+ },
31
+
32
+ startServer(defaultCtx) {
33
+ return [
34
+ this._express.json(),
35
+ (err, req, res, next) => {
36
+ if (err.statusCode) req.deserializationFailed = err; // overriding the error, processing the request as normal
37
+ next();
38
+ },
39
+ async (req, res) => {
40
+ const ctx = {
41
+ ...defaultCtx,
42
+ http: { req, res, query: req.query || {} },
43
+ };
44
+
45
+ await this._handleRequest(ctx);
46
+ },
47
+ ];
48
+ },
49
+
50
+ getProcedureName(ctx) {
51
+ return parseUrl(ctx.http.req.url).pathname.substr(1);
52
+ },
53
+
54
+ isIntrospection(ctx) {
55
+ return this.getProcedureName(ctx) === "";
56
+ },
57
+
58
+ prepareNotFoundReply(ctx) {
59
+ ctx.http.statusCode = 404;
60
+ },
61
+ prepareProcedureErrorReply(ctx) {
62
+ // Generic exception.
63
+ ctx.http.statusCode = 500;
64
+
65
+ // nodejs assert() exception. In HTTP world this likely means 400 "Bad Request".
66
+ if (ctx.error.code === "ERR_ASSERTION") ctx.http.statusCode = 400;
67
+ },
68
+
69
+ reply(ctx) {
70
+ if (!ctx.http.statusCode) ctx.http.statusCode = 200;
71
+ ctx.http.res.status(ctx.http.statusCode).json(ctx.result);
72
+ },
73
+ },
74
+ });