@pbvision/fastify-firestore-service 0.0.50

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