@pbvision/fastify-firestore-service 0.0.3

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/docs/api.md ADDED
@@ -0,0 +1,695 @@
1
+ # API Definition Library <!-- omit in toc -->
2
+ This library is used to define APIs.
3
+
4
+ ## Topics
5
+ - [Core Concepts](#core-concepts)
6
+ - [Minimal API](#minimal-api)
7
+ - [API Input Data](#api-input-data)
8
+ - [API Output Data](#api-output-data)
9
+ - [Short-circuit Response Processing](#short-circuit-response-processing)
10
+ - [Returning Errors](#returning-errors)
11
+ - [Custom Errors](#custom-errors)
12
+ - [Dynamic Errors](#dynamic-errors)
13
+ - [Return Success Response](#return-success-response)
14
+ - [Custom Success Status](#custom-success-status)
15
+ - [Database Transactions](#database-transactions)
16
+ - [Pre and Post Commit Processing](#pre-and-post-commit-processing)
17
+ - [Gotcha: Shared State](#gotcha-shared-state)
18
+ - [Gotcha: Expensive Computation](#gotcha-expensive-computation)
19
+ - [API Path](#api-path)
20
+ - [Long API Descriptions](#long-api-descriptions)
21
+ - [Swagger Interactive Documentation](#swagger-interactive-documentation)
22
+ - [One-Time Setup](#one-time-setup)
23
+ - [Asynchronous Processing](#asynchronous-processing)
24
+ - [Cross Origin (CORS)](#cross-origin-cors)
25
+ - [Calling other APIs](#calling-other-apis)
26
+ - [Niche Concepts](#niche-concepts)
27
+ - [Other API Input Data Options](#other-api-input-data-options)
28
+ - [Custom Middleware](#custom-middleware)
29
+ - [Gotcha: Sharing Schemas](#gotcha-sharing-schemas)
30
+ - [Localhost Cross-Origin Resource Sharing (CORS)](#localhost-cross-origin-resource-sharing-cors)
31
+ - [Appendix](#appendix)
32
+
33
+ # Core Concepts
34
+
35
+ ## Minimal API
36
+ Define a new API by subclassing `UnauthenticatedAPI` and implementing at
37
+ least these required members:
38
+
39
+ * `PATH` - the HTTP path used to call the API (more on this
40
+ [later](#api-path)). Should be camel-case (no underscores).
41
+ * `DESC` - a human-readable description of what the API does
42
+ * `RESPONSE` - describes the shape of the output JSON (more on this later)
43
+ * `computeResponse()` - processes the request and returns the response
44
+ ```javascript <!-- embed:../examples/docs.js:scope:WhatTimeIsItAPI -->
45
+ class WhatTimeIsItAPI extends API {
46
+ static PATH = '/whatTimeIsIt'
47
+ static DESC = 'Returns the current date string'
48
+ static RESPONSE = S.obj().prop('epoch', S.double)
49
+ async computeResponse () {
50
+ return { epoch: new Date().getTime() / 1000 }
51
+ }
52
+ }
53
+ ```
54
+
55
+
56
+ ## API Input Data
57
+ APIs can receive input data from a JSON-formatted HTTP request body. API
58
+ input MUST ALWAYS be validated. To streamline this, API inputs must be
59
+ described using [Schema](https://github.com/pbv-public/js-schema) (`S`)
60
+ like this:
61
+ ```javascript <!-- embed:../examples/docs.js:scope:AddNumbersAPI -->
62
+ class AddNumbersAPI extends API {
63
+ static PATH = '/add'
64
+ static DESC = 'returns the sum of a bunch of numbers'
65
+ // this API only takes numbers and arrays of numbers, but a schema
66
+ // can describe arbitrary JSON data including complex objects
67
+ static BODY = {
68
+ num1: S.double.desc('some number'),
69
+ num2: S.double.default(10)
70
+ .desc('another number; if omitted, a default value will be used'),
71
+ more: S.arr(S.double).optional()
72
+ .desc('optional array of more numbers to add')
73
+ }
74
+
75
+ static RESPONSE = RESPONSES.UNVALIDATED
76
+
77
+ // Input data is validated prior to computeResponse() being called. If any
78
+ // input is invalid, then an HTTP 400 response is returned. On test servers,
79
+ // the response body will also include a description of the error.
80
+ async computeResponse () {
81
+ const numbers = (this.req.body.more || []).concat([
82
+ this.req.body.num1, this.req.body.num2])
83
+ let sum = 0
84
+ for (let i = 0; i < numbers.length; i++) {
85
+ sum += numbers[i]
86
+ }
87
+ return sum
88
+ }
89
+ }
90
+ ```
91
+
92
+ Notice that inputs can be configured to use a default value if none is provided
93
+ (see `num2`). Inputs can also be configured to just have their value be
94
+ undefined if omitted with the `optional()` marker (see `more`).
95
+
96
+ The `BODY`, `PATH_PARAMS`, `HEADERS`, and `QS` properties all support a mapping of field names to Todea Schema objects. For advanced usages like specifying a default value or description, the mapping can be replaced with an Object schema `S.obj({})`. For example:
97
+ ```javascript
98
+ static BODY = S.obj({ a: S.str })
99
+ ```
100
+ is equivalent to
101
+ ```javascript
102
+ static BODY = { a: S.str }
103
+ ```
104
+
105
+ **Gotcha**: `desc()` should be called on the _type_ to describe a property.
106
+ ```javascript
107
+ static BODY = S.obj({
108
+ x: S.double.desc('describes x'),
109
+ y: S.double
110
+ }).desc('describes BODY')
111
+ ```
112
+
113
+ ## API Output Data
114
+ APIs can send output data in a JSON-formatted HTTP response body. To do
115
+ this, simply return an object from the `computeResponse()` method:
116
+ ```javascript <!-- embed:../examples/docs.js:scope:class ReplyWithValidatedObjectAPI:when an object is returned -->
117
+ // when an object is returned, it is automatically converted to a JSON string
118
+ // and the HTTP response's Content-Type header is set to application/json
119
+ async computeResponse () {
120
+ return {
121
+ canHaveArbitraryJSONContent: true,
122
+ hello: 'world',
123
+ address: {
124
+ houseNumber: 123,
125
+ street: 'Bush St'
126
+ },
127
+ walkScore: 3.14
128
+ }
129
+ }
130
+ ```
131
+
132
+ JSON response bodies must also be validated. The response schema documents the
133
+ expected output, as well as double-checks the correctness of output generated
134
+ by the API. Unexpected keys are considered as invalid. If you do not define an
135
+ output schema, then no output is permitted. Output validation can be performed
136
+ the same as input validation:
137
+ ```javascript <!-- embed:../examples/docs.js:section:response example start:response example end -->
138
+ // The RESPONSE getter defines which properties should be present in the
139
+ // returned data. Only the defined properties are validated and sent. Any
140
+ // extra properties would result in a 500 response.
141
+ static RESPONSE = {
142
+ canHaveArbitraryJSONContent: S.bool,
143
+ hello: S.str,
144
+ address: S.obj({
145
+ apartmentNumber: S.int.optional(),
146
+ houseNumber: S.int,
147
+ street: S.str
148
+ }),
149
+ walkScore: S.double
150
+ }
151
+ ```
152
+
153
+ The response schema values can optionally be documented like this:
154
+ ```javascript <!-- embed:../examples/docs.js:section:another resp example start:another resp example end -->
155
+ static RESPONSE = S.obj({
156
+ dragons: S.arr(S.str.desc('Dragon ID')).desc('Your dragons'),
157
+ guineaPigs: S.arr(S.str.desc('name').desc('Your dragons')),
158
+ optionalValue: S.int.desc('describe optional fields like this')
159
+ .optional()
160
+ }).desc('the mythic creatures you have collected')
161
+ ```
162
+
163
+ If the API has no response body, then `RESPONSE` should be omitted.
164
+
165
+ ## Short-circuit Response Processing
166
+ Complex APIs will have a `computeResponse()` which calls other functions. When
167
+ an error occurs deep in the call stack, it can be unwieldy and error-prone to
168
+ pass that error information back up the call stack simply to return that
169
+ information from `computeResponse()`. It can be useful to simply end request
170
+ processing at any point by throwing an exception which specifies the output.
171
+
172
+ ```javascript <!-- embed:../examples/docs.js:scope:ThrowToReplyAPI -->
173
+ class ThrowToReplyAPI extends API {
174
+ static PATH = '/throwToReturn'
175
+ static DESC = 'returns some data by throwing'
176
+ static QS = { shouldError: S.bool }
177
+ static RESPONSE = RESPONSES.UNVALIDATED
178
+
179
+ async computeResponse () {
180
+ this.help1()
181
+ }
182
+
183
+ help1 () { this.help2() }
184
+
185
+ help2 () {
186
+ // imagine we're deep in a complicated call stack but discover we know the
187
+ // response we need to send... instead of painstakingly passing it back up
188
+ // the call stack, we should use an exception to simply send the output
189
+ // from here
190
+ if (this.req.query.shouldError) {
191
+ throw new BadRequestException('run away!')
192
+ } else {
193
+ throw new RequestOkay({ hello: 'world' })
194
+ }
195
+ }
196
+ }
197
+ ```
198
+
199
+ These may also be thrown from an API's constructor.
200
+
201
+ ### Returning Errors
202
+ This library defines several exceptions classes, for example:
203
+ - InternalFailureException
204
+ - BadRequestException
205
+ - UnauthorizedException
206
+ - ...
207
+
208
+ These exceptions are exported in the `EXCEPTIONS` variable.
209
+ ```javascript
210
+ import { EXCEPTIONS } from '@pbvision/fastify-firestore-service'
211
+ const { UnauthorizedException } = EXCEPTIONS
212
+ ```
213
+
214
+ APIs must list the errors they may throw in `ERRORS` field.
215
+ ```javascript <!-- embed:../examples/docs.js:scope:DupErrorCodeAPI -->
216
+ class DupErrorCodeAPI extends API {
217
+ static PATH = '/dupErrorCode'
218
+ static DESC = 'API with multiple error with the same status code.'
219
+ static BODY = {
220
+ exception: S.str
221
+ }
222
+
223
+ static ERRORS = {
224
+ NotFoundException,
225
+ DupNotFoundException
226
+ }
227
+
228
+ async computeResponse (req) {
229
+ if (req.body.exception === 'notfound') {
230
+ throw new NotFoundException() // default error message "Not found"
231
+ } else {
232
+ throw new DupNotFoundException('custom message')
233
+ }
234
+ }
235
+ }
236
+ ```
237
+
238
+ ### Custom Errors
239
+ Custom errors should subclass `RequestError` base class, and provide `STATUS`
240
+ and `SCHEMA`.
241
+ ```javascript
242
+ class SessionExpiredException extends RequestError {
243
+ static STATUS = 403
244
+ static SCHEMA = S.obj().max(0)
245
+
246
+ constructor (message = 'session expired', data = {}) {
247
+ super(message, data)
248
+ }
249
+ }
250
+ ```
251
+
252
+ ### Dynamic Errors
253
+ APIs may need to re-throw an exception returned from an external API.
254
+ Dynamic error exceptions can be instantiated with
255
+ `new RequestError(message, data, code)`.
256
+
257
+ ### Return Success Response
258
+ Similar to short circuiting return errors, exceptions can be thrown to
259
+ return success responses. It is useful to avoid a long stack of return
260
+ statements. Success responses should throw `new RequestOkay(data)`. Data must
261
+ match the `RESPONSE` schema.
262
+
263
+ ### Custom Success Status
264
+ A non-standard 2xx status code can be used to indicate request success. The
265
+ `RESPONSE` field must be assigned with a subclass of `RequestDone` like this
266
+ ```javascript <!-- embed:../examples/docs.js:scope:NonStandardResponse -->
267
+ class NonStandardResponse extends RequestDone {
268
+ static STATUS = 201
269
+ }
270
+ ```
271
+
272
+ ## Database Transactions
273
+ `DatabaseAPI` wraps your request in a database transaction context from the
274
+ [Firestore ORM library](https://github.com/pbv-public/firestore-orm):
275
+ ```javascript
276
+ class SomeAPI extends DatabaseAPI {
277
+ static IS_READ_ONLY = false
278
+ async computeResponse () {
279
+ const someDoc = await this.tx.get(SomeModel, ...)
280
+ // changes are automatically saved when the request completes (assuming the
281
+ // transaction succeeds, i.e., it doesn't encounter excessive contention or
282
+ // some other problem)
283
+ someDoc.someField += 1
284
+ }
285
+ }
286
+ ```
287
+
288
+ The per-request transaction _only_ attempts to commit if the response code is
289
+ less than 400 (e.g., HTTP 200 "OK" or HTTP 302 "Moved Temporarily"). Response
290
+ codes 400 and higher are considered errors, and the transaction will be aborted
291
+ (no changes will be saved).
292
+
293
+ By default, `DatabaseAPI` uses a _read-only_ transaction. Set `IS_READ_ONLY` to
294
+ `false` (like in the above example) to allow database writes.
295
+
296
+ Other options for the database context can be set in the `CONTEXT_OPTIONS`
297
+ object:
298
+ ```javascript
299
+ static CONTEXT_OPTIONS = { retries: 3 }
300
+ ```
301
+
302
+ Note: If you leave the API read-only and add the `inconsistentReads: true` to
303
+ `CONTEXT_OPTIONS` then the database context will not be a transaction. See
304
+ Firestore ORM library's documentation for more detail.
305
+
306
+
307
+ ### Pre and Post Commit Processing
308
+ You can perform extra processing just before the per-request transaction
309
+ _attempts_ to commit by overriding the `preCommit(respData)` function (not
310
+ called if the transaction is being aborted because the HTTP response code
311
+ indicates an error as discussed in the previous section):
312
+ ```javascript
313
+ async preCommit (respData) {
314
+ // you can do more work within the original transaction if desired
315
+ assert(this.tx !== undefined) // tx is still available
316
+
317
+ const user = await tx.get(User, this.req.body.userID)
318
+ if (!user) {
319
+ // you can change the response code (and data)
320
+ throw new RequestError('unknown user')
321
+ }
322
+ else {
323
+ // you can also just modify the response data
324
+ respData.userName = user.name
325
+ }
326
+ return respData
327
+ }
328
+ ```
329
+
330
+ Similarly, you can perform extra processing just after the per-request
331
+ transaction _successfully_ commits by overriding the `postCommit(respData)`
332
+ function (not called if the transaction fails to commit):
333
+ ```javascript
334
+ async postCommit (respData) {
335
+ // you can do more work UNRELATED to the original transaction (this.tx is
336
+ // not even available here because it isn't valid here)
337
+ assert(this.tx === undefined) // tx has already ended!
338
+
339
+ // like preCommit(), you can modify response data (or throw RequestDone or
340
+ // any of its subclasses to change both the response code and data)
341
+ respData.postCommitMsg = 'commit succeeded'
342
+ return respData
343
+ }
344
+ ```
345
+ ```diff
346
+ --WARNING--
347
+ - In rare cases, the post-commit hook **MAY NOT RUN** after a transaction runs
348
+ - (e.g., the machine processing the request loses power after the database
349
+ - commits the transaction but before the post-commit hook runs). Take care to
350
+ - ONLY use the post-commit handler whose logic is okay to not run (on
351
+ - rare occasions).
352
+ ```
353
+
354
+
355
+ ### Gotcha: Shared State
356
+ Transactions sometimes retry due to contention, etc. It's important to not
357
+ store state on `this`, `req` or other heap variables while your transaction
358
+ runs, and then reference that data in a retry on accident.
359
+ ```javascript <!-- embed:../examples/db.js:scope:RememberingTooMuchAPI -->
360
+ class RememberingTooMuchAPI extends DatabaseAPI {
361
+ static NAME = 'unwise memory use'
362
+ static DESC = 'shares state across tx attempts and requests'
363
+ static PATH = '/overshare'
364
+ static IS_READ_ONLY = false
365
+ static CONTEXT_OPTIONS = { retries: 3 }
366
+ static BODY = {
367
+ numTries: S.int.min(0)
368
+ }
369
+
370
+ static RESPONSE = {
371
+ numTries: S.int.min(0),
372
+ numTriesOnThisMachine: S.int
373
+ }
374
+
375
+ static numTriesOnThisMachine = 0
376
+ constructor (fastify, req, reply) {
377
+ super(fastify, req, reply)
378
+ this.numTries = 0
379
+ }
380
+
381
+ async computeResponse (req) {
382
+ // The API instance (this) is created ONCE for each request. It isn't
383
+ // recreated if the transaction retries. So any changes you make to
384
+ // `this` will persist and be visible across retries!
385
+ this.numTries += 1
386
+
387
+ // Updating a static variable like this will affect ALL requests being
388
+ // processed by the same machine! Module variables are stored in RAM and
389
+ // are never cleared. They're only in their initial state when a machine
390
+ // first starts. (This is the case regardless of whether you're using
391
+ // transactions).
392
+ this.constructor.numTriesOnThisMachine += 1
393
+
394
+ if (this.numTries < req.body.numTries) {
395
+ // force the tx to retry to demonstrate a point
396
+ const err = new Error()
397
+ err.retryable = true
398
+ throw err
399
+ }
400
+ return {
401
+ numTries: this.numTries,
402
+ numTriesOnThisMachine: this.constructor.numTriesOnThisMachine
403
+ }
404
+ }
405
+ }
406
+ ```
407
+
408
+
409
+ ### Gotcha: Expensive Computation
410
+ It's best to do expensive computation _outside_ transactions in order to
411
+ minimize how long the transaction runs and thus minimize the opportunity for
412
+ contention. Of course, this is only possible if the computation is not
413
+ dependent on data from the database.
414
+
415
+ Your request _should_ do expensive computation unrelated to the database in its
416
+ constructor (`tx` is not defined at that point, and no transaction is running
417
+ and so transaction retries will not cause your constructor to re-run).
418
+ ```javascript
419
+ class SomeAPI extends DatabaseAPI {
420
+ constructor (fastify, req, reply) {
421
+ super(fastify, req, reply)
422
+ this.doSomeExpensiveComputation()
423
+ this.computeCalls = 0
424
+ this.postComputeCalls = 0
425
+
426
+ // the transaction hasn't started yet (or even been set yet) so the
427
+ // expensive computation will never be re-run, even if the tx retries
428
+ assert(this.tx === undefined) // tx hasn't started yet!
429
+ }
430
+ // ...
431
+ }
432
+ ```
433
+
434
+ You can also do expensive pre-computation in the `async preTxStart()` function.
435
+ This is more flexible than the constructor in that it is `async` and so you can
436
+ use `async/await` in it, unlike the constructor:
437
+ ```javascript
438
+ // this runs after the constructor but before the tx starts
439
+ async preTxStart () {
440
+ assert(this.tx === undefined) // tx hasn't started yet!
441
+ // ...
442
+ }
443
+ ```
444
+
445
+ ## API Path
446
+ An API's request path is of the form `/<Service Name><API PATH>`. For
447
+ example, a leaderboard service might have an API to get leaderboard entries
448
+ at the path `/leaderboard/entriesById`.
449
+
450
+
451
+ ## Long API Descriptions
452
+ An API description may sometimes be too long to fit on a single line. Use
453
+ Node's multiline string for this (also, keep in mind that markdown can be used
454
+ in descriptions):
455
+ ```javascript
456
+ static DESC = `
457
+ this will
458
+ get combined
459
+ into **one** string`
460
+ // The APIs description would be: "this will get combined into **one** string"
461
+ ```
462
+
463
+
464
+ ## Swagger Interactive Documentation
465
+ With the help of
466
+ [fastify-swagger](https://github.com/fastify/fastify-swagger), APIs are
467
+ automatically documented using Swagger UI. In addition to reviewing an APIs
468
+ specification, you can also _try_ the API from your browser (making API
469
+ requests and seeing their output right in your browser!). If your service
470
+ exposes many APIs, consider specifying a tag to group them in the generated
471
+ documentation:
472
+ ```javascript
473
+ static TAG = 'Some Group Name'
474
+ ```
475
+
476
+ If `TAG` isn't specified, it will be "default". If `TAG` is set to `null` then
477
+ the API will be omitted from the Swagger documentation.
478
+
479
+
480
+ ## One-Time Setup
481
+ Some APIs may need to do some one-time initialization work. This sort of thing
482
+ can be done by defining a static `setup()` method on your API:
483
+ ```javascript
484
+ static async setup (fastify) {
485
+ fastify.redis.defineCommand(/* add some custom local redis command... */)
486
+ }
487
+ ```
488
+
489
+
490
+ ## Asynchronous Processing
491
+ A service can accept _multiple_ requests for processing at once. However,
492
+ request processing is single-threaded (in a single process). JavaScript's
493
+ `async`/`await` syntax allows us to achieve lightweight concurrency;
494
+ understanding this concurrency model is important, but beyond the scope of this
495
+ documentation so check
496
+ [this async/await primer](https://medium.com/@garyo_83013/javascript-promises-and-async-await-for-c-programmers-aa349026f2e7)
497
+ to learn more.
498
+
499
+ This is a common setup because APIs tend to spend a lot of their time blocked
500
+ waiting on I/O, typically in the form of HTTP requests to other services (e.g.,
501
+ the database, cache or other API services). It is critical to never perform
502
+ synchronous I/O as this would stall the CPU (and every other request) until
503
+ the I/O completes. Use `async`/`await` to asynchronously block on I/O and
504
+ enable the server to continue productively using the CPU to process other requests.
505
+ Services with long-running CPU-bound requests may need to tweak (lower) request
506
+ processing concurrency to reduce queuing delays as the CPU works on one request
507
+ for an extended time, ignoring and starving other requests and possibly leading
508
+ to unacceptably high latency.
509
+ ```javascript
510
+ // the "async" keyword on this method is actually very important!
511
+ async computeResponse() {
512
+ // whenever possible, asynchronous work should be done in PARALLEL (rather
513
+ // than serially); in this example we make two API calls at the same time
514
+ // and then yield until both API calls have returned
515
+ const results = await Promise.all([
516
+ this.callAPI({/* params omitted from example */}),
517
+ this.callAPI({/* params omitted again */})
518
+ ])
519
+ // use the two results to compute our response...
520
+ }
521
+ ```
522
+
523
+ ## Cross Origin (CORS)
524
+ APIs which need be accessed from a web browser must indicate which hostnames
525
+ they may be accessed from. This is required for compatibility with browser
526
+ safety features. Without it, browsers will prevent the APIs from being
527
+ requested.
528
+
529
+ You can specify the CORS origin through the `CORS_ORIGIN` flag. For example,
530
+ ```js
531
+ static CORS_ORIGIN = 'some.example.com'
532
+ ```
533
+
534
+ You may also set the API to allow any origin like this:
535
+ ```js
536
+ static CORS_ORIGIN = '*'
537
+ ```
538
+
539
+ ## Calling other APIs
540
+ * If you want to have one API call another, see the `callAPI()` helper method.
541
+ * If you want to have your API redirect to a web application and optionally
542
+ pass some information in a cookie, see the `redirectToWebApp()` helper method.
543
+
544
+
545
+ # Niche Concepts
546
+ This section explains niche functionality.
547
+
548
+ ## Other API Input Data Options
549
+ APIs are typically requested via the HTTP POST method. API-specific
550
+ inputs are typically sent in the request body in JSON format (though some
551
+ universal inputs are sent in HTTP headers, such as which app and user sent the
552
+ request). API outputs are typically transmitted as JSON data in the HTTP
553
+ response body.
554
+
555
+ Occasionally, it may be necessary for an API to use a different HTTP method, or
556
+ different input or output types or formats. One possibility is when integrating
557
+ with a third-party who requires this. However, APIs should generally be
558
+ consistent and stick to HTTP POST data with HTTP request and response bodies
559
+ being JSON formatted.
560
+
561
+ Here is an example API which differs from our convention in every way:
562
+ * It is requested via the HTTP PUT method
563
+ * It receives input from many different sources:
564
+ * query string
565
+ * request body (in a custom format, not JSON)
566
+ * headers
567
+ * request path
568
+ * Its response body is XML formatted
569
+ ```javascript <!-- embed:../examples/docs.js:scope:NonStandardAddNumbersAPI -->
570
+ class NonStandardAddNumbersAPI extends API {
571
+ static METHOD = 'PUT'
572
+ static PATH = '/add/:num6/:num7'
573
+ static DESC = 'returns the sum of a bunch of numbers'
574
+
575
+ // we can send input as part of the query string (e.g., ?num=1&num2=...)
576
+ // sensitive data (such as a password hash) should not be sent in the query
577
+ // string as query strings are logged in request logs
578
+ static QS = {
579
+ num1: S.double.desc('some number'),
580
+ num2: S.double.desc('another number')
581
+ }
582
+
583
+ // input can also be sent in the body (for POST and PUT requests); this is
584
+ // preferred for large of complex data types like JSON objects
585
+ static BODY = {
586
+ num3: S.double.desc('yet another number'),
587
+ num4: S.double.desc('a 4th number to add'),
588
+ more: S.arr(S.double).optional()
589
+ .desc('optional array of more numbers to add')
590
+ }
591
+
592
+ // headers may also be used to communicate inputs, but are not typically used
593
+ // by APIs; they're primarily used for data sent on every request like user
594
+ // credentials
595
+ static HEADERS = {
596
+ num5: S.double.desc('a fifth number to add')
597
+ }
598
+
599
+ // input data may also be communicated in the path itself
600
+ static PATH_PARAMS = {
601
+ num6: S.double,
602
+ num7: S.double
603
+ }
604
+
605
+ static RESPONSE = RESPONSES.UNVALIDATED
606
+
607
+ // Input data is validated prior to computeResponse() being called. If any
608
+ // input is invalid, then an HTTP 400 response is returned. On test servers,
609
+ // the response body will also include a description of the error.
610
+ async computeResponse () {
611
+ const numbers = (this.req.body.more || []).concat([
612
+ this.req.query.num1, this.req.query.num2,
613
+ this.req.body.num3, this.req.body.num4,
614
+ this.req.headers.num5,
615
+ this.req.params.num6, this.req.params.num7
616
+ ])
617
+ let sum = 0
618
+ for (let i = 0; i < numbers.length; i++) {
619
+ sum += numbers[i]
620
+ }
621
+ // respond with an XML string instead of JSON
622
+ return `<sum>${sum}</sum>`
623
+ }
624
+
625
+ // add a custom content-type parser that will be available to just this API
626
+ // (to handle the custom data format sent to this API)
627
+ static async registerAPIWithFastify (fastify, fullPath) {
628
+ function parser (req, body, done) {
629
+ if (!body.startsWith('wacky!')) {
630
+ done(new InternalFailureException('invalid wacky format'))
631
+ } else {
632
+ try {
633
+ const jsonStr = body.substring(6)
634
+ done(null, JSON.parse(jsonStr))
635
+ } catch (err) {
636
+ err.statusCode = 400
637
+ done(err, undefined)
638
+ }
639
+ }
640
+ }
641
+ fastify.addContentTypeParser(
642
+ 'wackyCustom_thing', { parseAs: 'string' }, parser)
643
+ super.registerAPIWithFastify(fastify, fullPath)
644
+ }
645
+ }
646
+ ```
647
+
648
+
649
+ ## Custom Middleware
650
+ If desired, each API can be run with custom middleware and other fastify
651
+ options. To do this, you'll need to override the `register()` method and
652
+ call `app.register()` as desired.
653
+
654
+ ## Gotcha: Sharing Schemas
655
+ In order for multiple APIs to share these parameters via inheritance:
656
+ ```javascript
657
+ static get BODY () {
658
+ return S.obj({
659
+ leaderboard: S.str,
660
+ hasTiebreakers: S.bool.default(true).desc(
661
+ 'must be set to true if the leaderboard uses tiebreakers')
662
+ })
663
+ }
664
+ ```
665
+
666
+ Notice that `BODY` is defined as a getter function, not a static member
667
+ variable. The reason is that subclasses will call `super.BODY` to extend
668
+ the object. They each need their own copy of the schema (which the getter
669
+ creates and returns). If they both built from the same copy, it would cause
670
+ internal problems with the schema object.
671
+
672
+
673
+ ## Localhost Cross-Origin Resource Sharing (CORS)
674
+ When running a service locally for testing purposes, it will enable CORS
675
+ requests for all APIs from `http://localhost:3000`. This facilitates testing of
676
+ web applications also running locally that depend on the service. In the cloud,
677
+ CORS is not enabled (unless you choose to enable it).
678
+
679
+ # Appendix
680
+ The samples in this readme can be found in the APIs defined for unit testing
681
+ this library in `../examples/docs.js`.
682
+
683
+ When necessary it is possible to use static getter functions instead of static
684
+ member variables. For example, here's a contrived example that only accepts "n"
685
+ as an input on localhost:
686
+ ```javascript
687
+ // we use a getter function here so that we can more convenient define th
688
+ // body using more complex logic than usual
689
+ static get BODY () {
690
+ if (process.env.NODE_ENV === 'localhost') {
691
+ return S.obj({ n: S.int })
692
+ }
693
+ return S.obj()
694
+ }
695
+ ```