@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/LICENSE +204 -0
- package/README.md +193 -0
- package/docs/api.md +695 -0
- package/docs/build.sh +24 -0
- package/docs/jsdoc.config.json +22 -0
- package/package.json +90 -0
- package/src/api/api.js +790 -0
- package/src/api/db-api.js +108 -0
- package/src/api/exception.js +288 -0
- package/src/api/response.js +8 -0
- package/src/component-registrar.js +27 -0
- package/src/got-wrapper.js +34 -0
- package/src/index.js +12 -0
- package/src/make-app.js +179 -0
- package/src/make-logger.js +119 -0
- package/src/plugins/compress.js +28 -0
- package/src/plugins/content-parser.js +21 -0
- package/src/plugins/cookie.js +17 -0
- package/src/plugins/error-handler.js +140 -0
- package/src/plugins/health-check.js +19 -0
- package/src/plugins/latency-tracker.js +45 -0
- package/src/plugins/swagger.js +50 -0
- package/test/base-test.js +154 -0
- package/test/environment.js +3 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import db from 'firestore-orm'
|
|
2
|
+
|
|
3
|
+
import API from './api.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thrown to avoid committing a transaction when an error occurs.
|
|
7
|
+
* @access private
|
|
8
|
+
*/
|
|
9
|
+
class TransactionAborted extends Error {
|
|
10
|
+
constructor (respData) {
|
|
11
|
+
super()
|
|
12
|
+
this.respData = respData
|
|
13
|
+
this.retryable = false
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An API whose response is computed inside a transaction.
|
|
19
|
+
* @public
|
|
20
|
+
* @class
|
|
21
|
+
*/
|
|
22
|
+
class DatabaseAPI extends API {
|
|
23
|
+
/**
|
|
24
|
+
* Whether this API can only read from Firestore. If false, the API's
|
|
25
|
+
* computeResponse() method will run in a Firestore transaction.
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
static IS_READ_ONLY = true
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options to pass the Firestore db.Context which wraps computeResponse().
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
static CONTEXT_OPTIONS = {}
|
|
35
|
+
|
|
36
|
+
async _computeResponse () {
|
|
37
|
+
await this.preTxStart()
|
|
38
|
+
|
|
39
|
+
let ret
|
|
40
|
+
try {
|
|
41
|
+
const opts = {
|
|
42
|
+
...this.constructor.CONTEXT_OPTIONS,
|
|
43
|
+
readOnly: this.constructor.IS_READ_ONLY
|
|
44
|
+
}
|
|
45
|
+
ret = await db.Context.run(opts, async tx => {
|
|
46
|
+
this.tx = tx
|
|
47
|
+
this.req.tx = tx
|
|
48
|
+
let respData = await super._computeResponse()
|
|
49
|
+
if (this.__reply.statusCode < 400) {
|
|
50
|
+
// pre-commit hook may change the response data (and status code!)
|
|
51
|
+
respData = await this._callAndHandleRequestDone(
|
|
52
|
+
this.preCommit, respData)
|
|
53
|
+
}
|
|
54
|
+
// if the response code indicates an error, then don't commit
|
|
55
|
+
if (this.__reply.statusCode >= 400) {
|
|
56
|
+
throw new TransactionAborted(respData)
|
|
57
|
+
}
|
|
58
|
+
return respData
|
|
59
|
+
})
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e instanceof TransactionAborted) {
|
|
62
|
+
return e.respData
|
|
63
|
+
} else {
|
|
64
|
+
throw e
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
delete this.tx
|
|
68
|
+
delete this.req.tx
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// _computeResponse() is called within _callAndHandleRequestDone; so we
|
|
72
|
+
// don't need to wrap this call to postCommit() in it (redundant)
|
|
73
|
+
return this.postCommit(ret)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Called just before the transaction starts. Useful to do async computation
|
|
78
|
+
* outside the transaction (reducing the window for contention).
|
|
79
|
+
* @protected
|
|
80
|
+
*/
|
|
81
|
+
async preTxStart () {}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Called just before the transaction ATTEMPTS to commit. May alter the
|
|
85
|
+
* response by returning a new response value, or throwing RequestDone.
|
|
86
|
+
* Throwing an error will be propagated and handled by Transaction.run(),
|
|
87
|
+
* and will prevent the transaction from committing (unless the error is
|
|
88
|
+
* retryable).
|
|
89
|
+
*
|
|
90
|
+
* @param {*} respData the response data
|
|
91
|
+
* @returns {*} the (possibly updated) response data
|
|
92
|
+
* @protected
|
|
93
|
+
*/
|
|
94
|
+
async preCommit (respData) { return respData }
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Called after the transaction commits (and ONLY if it commits
|
|
98
|
+
* successfully). May alter the response by returning a new response value,
|
|
99
|
+
* or throwing RequestDone.
|
|
100
|
+
*
|
|
101
|
+
* @param {*} respData the response data
|
|
102
|
+
* @returns {*} the (possibly updated) response data
|
|
103
|
+
* @protected
|
|
104
|
+
*/
|
|
105
|
+
async postCommit (respData) { return respData }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default DatabaseAPI
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
|
|
3
|
+
import S from '@pbvision/schema'
|
|
4
|
+
|
|
5
|
+
import RESPONSES from './response.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @namespace Exceptions
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Thrown to shortcut request handling.
|
|
13
|
+
*
|
|
14
|
+
* {@link API} will catch this exception and send the HTTP response code
|
|
15
|
+
* and (optional) data. This is useful to immediately stop request processing,
|
|
16
|
+
* especially from deeply nested locations in the call stack where it would
|
|
17
|
+
* be cumbersome to pass return information (especially errors) all the way
|
|
18
|
+
* back up the call stack.
|
|
19
|
+
*
|
|
20
|
+
* Typically, users should throw {@link RequestError} for error responses or
|
|
21
|
+
* @see RequestOkay for non-error responses (not @this).
|
|
22
|
+
*
|
|
23
|
+
* @arg {Number} httpCode The HTTP status code to respond with.
|
|
24
|
+
* @arg {String|Object} [respData=''] The object (to JSON.stringify()) or
|
|
25
|
+
* string to send in the HTTP response body.
|
|
26
|
+
* @package
|
|
27
|
+
* @memberof Exceptions
|
|
28
|
+
*/
|
|
29
|
+
class __RequestDone extends Error {
|
|
30
|
+
static STATUS = undefined
|
|
31
|
+
static SCHEMA = RESPONSES.NO_OUTPUT
|
|
32
|
+
|
|
33
|
+
static get schemaValidator () {
|
|
34
|
+
const cacheKey = '_CACHED_SCHEMA_VALIDATOR'
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(this, cacheKey)) {
|
|
36
|
+
this[cacheKey] = this.schema.compile(`data input to ${this.name}`)
|
|
37
|
+
}
|
|
38
|
+
return this[cacheKey]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets a Todea Schema object.
|
|
43
|
+
*/
|
|
44
|
+
static get schema () {
|
|
45
|
+
let schema = this.SCHEMA
|
|
46
|
+
if (!schema.isTodeaSchema) {
|
|
47
|
+
schema = S.obj(schema)
|
|
48
|
+
}
|
|
49
|
+
return schema
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create an exception.
|
|
54
|
+
* @param {String} message An error message
|
|
55
|
+
* @param {Object} data A JSON object containing additional data
|
|
56
|
+
* @param {Integer} code An optional code to override exception's default
|
|
57
|
+
* STATUS
|
|
58
|
+
*/
|
|
59
|
+
constructor (message, data = {}, code = undefined) {
|
|
60
|
+
super(message)
|
|
61
|
+
this.httpCode = code ?? this.constructor.STATUS
|
|
62
|
+
assert(this.httpCode !== undefined, 'Status must be defined')
|
|
63
|
+
assert(this.constructor.SCHEMA !== undefined, 'Schema must be defined')
|
|
64
|
+
this.constructor.schemaValidator(data)
|
|
65
|
+
this.data = data
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Concrete class to indicate request is completed ok.
|
|
71
|
+
* @public
|
|
72
|
+
* @memberof Exceptions
|
|
73
|
+
*/
|
|
74
|
+
class RequestDone extends __RequestDone {
|
|
75
|
+
static STATUS = 200
|
|
76
|
+
static SCHEMA = S.obj().optional()
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return data to return to caller.
|
|
80
|
+
*/
|
|
81
|
+
get respData () {
|
|
82
|
+
return this.data
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a success response. Status must be smaller than 400.
|
|
87
|
+
* @param {Object} data Data to return to caller
|
|
88
|
+
* @param {Integer} code A status to override default status.
|
|
89
|
+
*/
|
|
90
|
+
constructor (data, code = undefined) {
|
|
91
|
+
super(undefined, data, code)
|
|
92
|
+
assert(this.httpCode < 300, 'Status code must be less than 300')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Thrown to shortcut request handling and return an HTTP success code (200).
|
|
98
|
+
*
|
|
99
|
+
* @arg {String|Object} [respData=''] The object (to JSON.stringify()) or
|
|
100
|
+
* string to send in the HTTP response body.
|
|
101
|
+
* @public
|
|
102
|
+
* @memberof Exceptions
|
|
103
|
+
* @see RequestDone
|
|
104
|
+
*/
|
|
105
|
+
class RequestOkay extends RequestDone {
|
|
106
|
+
static STATUS = 200
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Thrown to shortcut request handling and return an HTTP error.
|
|
111
|
+
*
|
|
112
|
+
* @arg {String} message The human-readable error message to send back in the
|
|
113
|
+
* JSON response data (in the "error" key).
|
|
114
|
+
* @arg {Object} [data={}] Optional additional JSON data to send back
|
|
115
|
+
* in the error response body.
|
|
116
|
+
* @arg {Number} [code=400] The HTTP status code to respond with; defaults
|
|
117
|
+
* to STATUS
|
|
118
|
+
* @public
|
|
119
|
+
* @memberof Exceptions
|
|
120
|
+
* @see RequestDone
|
|
121
|
+
*/
|
|
122
|
+
class RequestError extends __RequestDone {
|
|
123
|
+
static SCHEMA = S.obj()
|
|
124
|
+
|
|
125
|
+
constructor (message, data = {}, code = undefined) {
|
|
126
|
+
super(message, data, code)
|
|
127
|
+
assert(this.httpCode >= 300, 'Status code must be at least 300')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Schema to use on returned data
|
|
132
|
+
*/
|
|
133
|
+
static get respSchema () {
|
|
134
|
+
return S.obj({
|
|
135
|
+
code: S.str,
|
|
136
|
+
message: S.str.optional(),
|
|
137
|
+
data: S.obj() // Data is validated in constructor, don't validate again.
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Data to return to the caller
|
|
143
|
+
*/
|
|
144
|
+
get respData () {
|
|
145
|
+
return {
|
|
146
|
+
code: this.constructor.name,
|
|
147
|
+
message: this.message,
|
|
148
|
+
data: this.data
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Thrown when a redirect is required.
|
|
155
|
+
* @public
|
|
156
|
+
* @memberof Exceptions
|
|
157
|
+
*/
|
|
158
|
+
class RedirectException extends RequestError {
|
|
159
|
+
static STATUS = 302
|
|
160
|
+
static SCHEMA = S.str.max(0)
|
|
161
|
+
|
|
162
|
+
constructor (url, code) {
|
|
163
|
+
super('', '', code)
|
|
164
|
+
assert(this.httpCode < 400, 'Status code must be less than 400')
|
|
165
|
+
assert(typeof url === 'string' && url.length)
|
|
166
|
+
this.url = url
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Thrown when a error is induced by client.
|
|
172
|
+
* @public
|
|
173
|
+
* @memberof Exceptions
|
|
174
|
+
*/
|
|
175
|
+
class BadRequestException extends RequestError {
|
|
176
|
+
static STATUS = 400
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Wraps Request Validation failures into a RequestError object for processing.
|
|
181
|
+
* @public
|
|
182
|
+
* @memberof Exceptions
|
|
183
|
+
*/
|
|
184
|
+
class InvalidInputException extends BadRequestException {
|
|
185
|
+
static STATUS = 400
|
|
186
|
+
static __ERROR_PREFIX = {
|
|
187
|
+
headers: 'Header Validation Failure',
|
|
188
|
+
body: 'Body Validation Failure',
|
|
189
|
+
querystring: 'Query Validation Failure',
|
|
190
|
+
params: 'Path Validation Failure'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
constructor (schemaError) {
|
|
194
|
+
const prefixMap = InvalidInputException.__ERROR_PREFIX
|
|
195
|
+
const prefix = prefixMap[schemaError.validationContext] ??
|
|
196
|
+
'Unknown Validation Failure'
|
|
197
|
+
super(`${prefix}: ${schemaError.message}`)
|
|
198
|
+
this.name = this.constructor.name
|
|
199
|
+
// istanbul ignore else
|
|
200
|
+
if (process.env.NODE_ENV !== 'prod' && schemaError.validation) {
|
|
201
|
+
for (const err of schemaError.validation) {
|
|
202
|
+
console.log(JSON.stringify(err))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Thrown when a client is not authorized to access the requested resource. For
|
|
210
|
+
* example, user is trying to log in using invalid credentials.
|
|
211
|
+
* @public
|
|
212
|
+
* @memberof Exceptions
|
|
213
|
+
*/
|
|
214
|
+
class UnauthorizedException extends RequestError {
|
|
215
|
+
static STATUS = 401
|
|
216
|
+
|
|
217
|
+
constructor (message = 'access denied') {
|
|
218
|
+
super(message)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Thrown when an authenticated client does not have enough privilege to access
|
|
224
|
+
* the requested resource. For example, a user tries to change other user's
|
|
225
|
+
* data.
|
|
226
|
+
* @public
|
|
227
|
+
* @memberof Exceptions
|
|
228
|
+
*/
|
|
229
|
+
class ForbiddenException extends RequestError {
|
|
230
|
+
static STATUS = 403
|
|
231
|
+
|
|
232
|
+
constructor (message = 'access denied') {
|
|
233
|
+
super(message)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Thrown when the requested resource is not found, or should be hidden.
|
|
239
|
+
* @public
|
|
240
|
+
* @memberof Exceptions
|
|
241
|
+
*/
|
|
242
|
+
class NotFoundException extends RequestError {
|
|
243
|
+
static STATUS = 404
|
|
244
|
+
|
|
245
|
+
constructor () {
|
|
246
|
+
super('Not found')
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Thrown when an error is induced by a server bug.
|
|
252
|
+
* @public
|
|
253
|
+
* @memberof Exceptions
|
|
254
|
+
*/
|
|
255
|
+
class InternalFailureException extends RequestError {
|
|
256
|
+
static STATUS = 500
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Thrown when server is temporarily unable to serve the request, due to
|
|
261
|
+
* internal timeouts or service outage.
|
|
262
|
+
* @public
|
|
263
|
+
* @memberof Exceptions
|
|
264
|
+
*/
|
|
265
|
+
class ServiceUnavailableException extends RequestError {
|
|
266
|
+
static STATUS = 503
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export {
|
|
270
|
+
// Base exceptions
|
|
271
|
+
RequestError,
|
|
272
|
+
RequestDone,
|
|
273
|
+
|
|
274
|
+
// Success
|
|
275
|
+
RequestOkay,
|
|
276
|
+
|
|
277
|
+
// Redirect
|
|
278
|
+
RedirectException,
|
|
279
|
+
|
|
280
|
+
// Error
|
|
281
|
+
BadRequestException,
|
|
282
|
+
InvalidInputException,
|
|
283
|
+
ForbiddenException,
|
|
284
|
+
InternalFailureException,
|
|
285
|
+
NotFoundException,
|
|
286
|
+
ServiceUnavailableException,
|
|
287
|
+
UnauthorizedException
|
|
288
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default class ComponentRegistrar {
|
|
2
|
+
apis = []
|
|
3
|
+
|
|
4
|
+
constructor (app, service) {
|
|
5
|
+
this.app = app
|
|
6
|
+
this.service = service
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async registerComponents (components) {
|
|
10
|
+
const promises = []
|
|
11
|
+
for (const component of Object.values(components)) {
|
|
12
|
+
if (component.register) {
|
|
13
|
+
promises.push(component.register(this))
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
await Promise.all(promises)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async registerAPI (api) {
|
|
20
|
+
this.apis.push(api)
|
|
21
|
+
await api.registerAPI(this)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async registerModel (model) {
|
|
25
|
+
// nothing to do here
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import zlib from 'node:zlib'
|
|
2
|
+
|
|
3
|
+
import realGot from 'got'
|
|
4
|
+
|
|
5
|
+
function gotWrapper (options, mockedGot) {
|
|
6
|
+
options = {
|
|
7
|
+
decompress: true,
|
|
8
|
+
...options
|
|
9
|
+
}
|
|
10
|
+
if (options.compress) {
|
|
11
|
+
// make a copy of the headers obj before we make changes to it
|
|
12
|
+
const headers = {
|
|
13
|
+
...options.headers
|
|
14
|
+
}
|
|
15
|
+
options.headers = headers
|
|
16
|
+
if (options.body) {
|
|
17
|
+
options.body = zlib.brotliCompressSync(options.body)
|
|
18
|
+
}
|
|
19
|
+
if (options.json) {
|
|
20
|
+
headers['content-type'] = 'application/json'
|
|
21
|
+
options.body = zlib.brotliCompressSync(JSON.stringify(options.json))
|
|
22
|
+
delete options.json
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (options.body) {
|
|
26
|
+
headers['content-encoding'] = 'br'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// istanbul ignore next
|
|
30
|
+
const got = mockedGot ?? gotWrapper.__mocked_got ?? realGot
|
|
31
|
+
return got(options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default gotWrapper
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import API from './api/api.js'
|
|
2
|
+
import DatabaseAPI from './api/db-api.js'
|
|
3
|
+
import * as EXCEPTIONS from './api/exception.js'
|
|
4
|
+
import RESPONSES from './api/response.js'
|
|
5
|
+
import ComponentRegistrar from './component-registrar.js'
|
|
6
|
+
import makeService from './make-app.js'
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
API, DatabaseAPI, EXCEPTIONS, RESPONSES,
|
|
10
|
+
makeService,
|
|
11
|
+
ComponentRegistrar
|
|
12
|
+
}
|
package/src/make-app.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fastify from 'fastify'
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
3
|
+
|
|
4
|
+
import ComponentRegistrar from './component-registrar.js'
|
|
5
|
+
import makeLogger from './make-logger.js'
|
|
6
|
+
import compressPlugin from './plugins/compress.js'
|
|
7
|
+
import contentParserPlugin from './plugins/content-parser.js'
|
|
8
|
+
import cookiePlugin from './plugins/cookie.js'
|
|
9
|
+
import errorHandlerPlugin from './plugins/error-handler.js'
|
|
10
|
+
import healthCheckPlugin from './plugins/health-check.js'
|
|
11
|
+
import latencyTrackerPlugin from './plugins/latency-tracker.js'
|
|
12
|
+
import swaggerPlugin from './plugins/swagger.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} CookieConfig
|
|
16
|
+
* @property {boolean} [disabled=false] Adds fastify-cookie plugin
|
|
17
|
+
* @property {string} [secret] A secret to use to secure cookie
|
|
18
|
+
*/
|
|
19
|
+
const COOKIE_CONFIG = {
|
|
20
|
+
disabled: false,
|
|
21
|
+
secret: undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} LoggingConfig
|
|
26
|
+
* @property {boolean} [useUnitTestLogFormat=false] Whether output logs to console with
|
|
27
|
+
* pretty printing
|
|
28
|
+
* @property {boolean} [reportAllErrors=false] Whether include all API
|
|
29
|
+
* validation errors in error logging. Recommend to keep it off for production,
|
|
30
|
+
* on for testing.
|
|
31
|
+
* @property {boolean} [reportErrorDetail=false] Whether include all details
|
|
32
|
+
* of an error. Recommend to keep it off for remote testing, on for local
|
|
33
|
+
* testing.
|
|
34
|
+
*/
|
|
35
|
+
const LOGGING_CONFIG = {
|
|
36
|
+
useUnitTestLogFormat: false,
|
|
37
|
+
reportAllErrors: false,
|
|
38
|
+
reportErrorDetail: false,
|
|
39
|
+
sentryDSN: null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} HealthCheckConfig
|
|
44
|
+
* @property {boolean} [disabled=false] Whether to add a health check endpoint
|
|
45
|
+
* that simply returns 200.
|
|
46
|
+
* @property {string} [path='/'] The path to the health check endpoint.
|
|
47
|
+
*/
|
|
48
|
+
const HEALTH_CHECK_CONFIG = {
|
|
49
|
+
disabled: false,
|
|
50
|
+
path: '/'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {object} SwaggerConfig
|
|
55
|
+
* @property {boolean} [disabled=false] Whether to disable
|
|
56
|
+
* @property {Array<string>} [servers=[]] The host endpoint (scheme + domain) to
|
|
57
|
+
* send requests to
|
|
58
|
+
* @property {Array<string>} [authHeaders=[]] Authentication headers
|
|
59
|
+
* @property {string} [routePrefix='/docs'] Authentication headers
|
|
60
|
+
*/
|
|
61
|
+
const SWAGGER_CONFIG = {
|
|
62
|
+
disabled: false,
|
|
63
|
+
servers: [],
|
|
64
|
+
authHeaders: [],
|
|
65
|
+
routePrefix: '/docs'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {object} LatencyTrackerConfig
|
|
70
|
+
* @property {boolean} [disabled=false] Whether to add a health check endpoint
|
|
71
|
+
* that simply returns 200.
|
|
72
|
+
* @property {string} [path='/'] The path to the health check endpoint.
|
|
73
|
+
*/
|
|
74
|
+
const LATENCY_TRACKER_CONFIG = {
|
|
75
|
+
disabled: false,
|
|
76
|
+
header: 'x-latency-ms'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const PARAMS_CONFIG = {
|
|
80
|
+
service: undefined,
|
|
81
|
+
components: undefined,
|
|
82
|
+
RegistrarCls: ComponentRegistrar,
|
|
83
|
+
cookie: {},
|
|
84
|
+
healthCheck: {},
|
|
85
|
+
latencyTracker: {},
|
|
86
|
+
logging: {},
|
|
87
|
+
swagger: {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function loadConfigDefault (config, defaultConfig) {
|
|
91
|
+
for (const key of Object.keys(config)) {
|
|
92
|
+
if (!Object.prototype.hasOwnProperty.call(defaultConfig, key)) {
|
|
93
|
+
throw new Error(`Unknown config ${key}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!config.disabled) {
|
|
97
|
+
for (const [key, defaultValue] of Object.entries(defaultConfig)) {
|
|
98
|
+
if (defaultValue === undefined && !config[key]) {
|
|
99
|
+
throw new Error(`Missing required value for ${key}`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
Object.assign(config, { ...defaultConfig, ...config })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {Object} params
|
|
108
|
+
* @param {string} params.service Name of the service, for example, iam,
|
|
109
|
+
* user-id, leaderboard. This affects API's prefixes.
|
|
110
|
+
* @param {Array<API|Model|component>} params.components A list of
|
|
111
|
+
* components.
|
|
112
|
+
* @param {object} [params.RegistrarCls=ComponentRegistrar] A subclass of
|
|
113
|
+
* ComponentRegistrar
|
|
114
|
+
* @param {CookieConfig} [params.cookie] Configures fastify-cookie.
|
|
115
|
+
* @param {HealthCheckConfig} [params.healthCheck] Configures health check endpoint.
|
|
116
|
+
* @param {LatencyTrackerConfig} [params.latencyTracker]
|
|
117
|
+
* @param {LoggingConfig} [params.logging] Configures logging.
|
|
118
|
+
* @param {SwaggerConfig} [params.swagger] Configures swagger.
|
|
119
|
+
* @returns {Promise<server>} fastify app with configured plugins
|
|
120
|
+
*/
|
|
121
|
+
export default async function makeService (params = {}) {
|
|
122
|
+
const configs = [
|
|
123
|
+
[() => params, PARAMS_CONFIG],
|
|
124
|
+
[() => params.cookie, COOKIE_CONFIG],
|
|
125
|
+
[() => params.healthCheck, HEALTH_CHECK_CONFIG],
|
|
126
|
+
[() => params.latencyTracker, LATENCY_TRACKER_CONFIG],
|
|
127
|
+
[() => params.logging, LOGGING_CONFIG],
|
|
128
|
+
[() => params.swagger, SWAGGER_CONFIG]
|
|
129
|
+
]
|
|
130
|
+
for (const [getter, defaultConfig] of configs) {
|
|
131
|
+
loadConfigDefault(getter(), defaultConfig)
|
|
132
|
+
}
|
|
133
|
+
const {
|
|
134
|
+
service,
|
|
135
|
+
components,
|
|
136
|
+
RegistrarCls,
|
|
137
|
+
cookie,
|
|
138
|
+
healthCheck,
|
|
139
|
+
latencyTracker,
|
|
140
|
+
logging,
|
|
141
|
+
swagger
|
|
142
|
+
} = params
|
|
143
|
+
const fastifyServerId = uuidv4()
|
|
144
|
+
let requestCount = 0
|
|
145
|
+
const app = fastify({
|
|
146
|
+
ignoreTrailingSlash: true,
|
|
147
|
+
disableRequestLogging: true,
|
|
148
|
+
logger: makeLogger(logging.useUnitTestLogFormat),
|
|
149
|
+
genReqId: () => `${fastifyServerId}-${++requestCount}`,
|
|
150
|
+
ajv: {
|
|
151
|
+
customOptions: {
|
|
152
|
+
removeAdditional: false,
|
|
153
|
+
allErrors: logging.reportAllErrors,
|
|
154
|
+
useDefaults: true,
|
|
155
|
+
strictSchema: false,
|
|
156
|
+
strictRequired: true
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const registrar = new RegistrarCls(app, service)
|
|
162
|
+
|
|
163
|
+
app.register(cookiePlugin, { cookie })
|
|
164
|
+
.register(compressPlugin)
|
|
165
|
+
.register(contentParserPlugin)
|
|
166
|
+
.register(latencyTrackerPlugin, { latencyTracker })
|
|
167
|
+
.register(errorHandlerPlugin, {
|
|
168
|
+
errorHandler: {
|
|
169
|
+
returnErrorDetail: logging.reportErrorDetail,
|
|
170
|
+
sentryDSN: logging.sentryDSN,
|
|
171
|
+
serverName: fastifyServerId
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
.register(healthCheckPlugin, { healthCheck })
|
|
175
|
+
.register(swaggerPlugin, { swagger: { service, ...swagger } })
|
|
176
|
+
|
|
177
|
+
await registrar.registerComponents(components)
|
|
178
|
+
return app
|
|
179
|
+
}
|