@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.
@@ -0,0 +1,223 @@
1
+ import Ajv from 'ajv/dist/2020.js'
2
+ import fastify from 'fastify'
3
+ import { v4 as uuidv4 } from 'uuid'
4
+
5
+ import ComponentRegistrar from './component-registrar.js'
6
+ import { makePinoLoggerOptions } from './make-logger.js'
7
+ import compressPlugin from './plugins/compress.js'
8
+ import contentParserPlugin from './plugins/content-parser.js'
9
+ import cookiePlugin from './plugins/cookie.js'
10
+ import errorHandlerPlugin from './plugins/error-handler.js'
11
+ import healthCheckPlugin from './plugins/health-check.js'
12
+ import latencyTrackerPlugin from './plugins/latency-tracker.js'
13
+ import swaggerPlugin from './plugins/swagger.js'
14
+
15
+ /**
16
+ * @typedef {object} CookieConfig
17
+ * @property {boolean} [disabled=false] Adds fastify-cookie plugin
18
+ * @property {string} [secret] A secret to use to secure cookie
19
+ */
20
+ const COOKIE_CONFIG = {
21
+ disabled: false,
22
+ secret: null
23
+ }
24
+
25
+ /**
26
+ * @typedef {object} LoggingConfig
27
+ * @property {boolean} [reportAllErrors=false] Whether include all API
28
+ * validation errors in error logging. Recommend to keep it off for production,
29
+ * on for testing.
30
+ * @property {boolean} [reportErrorDetail=false] Whether include all details
31
+ * of an error. Recommend to keep it off for remote testing, on for local
32
+ * testing.
33
+ * @property {String} [sentryDSN] The Sentry DSN to report errors to
34
+ * @property {Object} [sentryRateLimiter] Optional rate limiter instance (from
35
+ * {@link createSentryRateLimiter}) to control Sentry reporting for errors
36
+ * flagged via `RequestError.rateLimitSentry()`. Defaults to a fresh limiter
37
+ * using the real clock; tests inject one with a controllable clock.
38
+ * @property {function} [customizePinoOpts] An optional function customize the
39
+ * arguments to pass to Pino. Takes as input the options that will be sent
40
+ * to be Pino by default (can be modified). Return the new options.
41
+ */
42
+ const LOGGING_CONFIG = {
43
+ customizePinoOpts: null,
44
+ reportAllErrors: false,
45
+ reportErrorDetail: false,
46
+ sentryDSN: null,
47
+ sentryRateLimiter: null
48
+ }
49
+
50
+ /**
51
+ * @typedef {object} HealthCheckConfig
52
+ * @property {boolean} [disabled=false] Whether to add a health check endpoint
53
+ * that simply returns 200.
54
+ * @property {string} [path='/'] The path to the health check endpoint.
55
+ */
56
+ const HEALTH_CHECK_CONFIG = {
57
+ disabled: false,
58
+ path: '/'
59
+ }
60
+
61
+ /**
62
+ * @typedef {object} SwaggerConfig
63
+ * @property {boolean} [disabled=false] Whether to disable
64
+ * @property {Array<string>} [servers=[]] The host endpoint (scheme + domain) to
65
+ * send requests to
66
+ * @property {Array<string>} [authHeaders=[]] Authentication headers
67
+ * @property {string} [routePrefix='/docs'] Authentication headers
68
+ */
69
+ const SWAGGER_CONFIG = {
70
+ disabled: false,
71
+ servers: [],
72
+ authHeaders: [],
73
+ routePrefix: '/docs'
74
+ }
75
+
76
+ /**
77
+ * @typedef {object} LatencyTrackerConfig
78
+ * @property {boolean} [disabled=false] Whether to record how long from when we
79
+ * start to process a request to when we start to send the response
80
+ * @property {string} [header='x-latency-ms'] response header to send the
81
+ * latency info back in
82
+ */
83
+ const LATENCY_TRACKER_CONFIG = {
84
+ disabled: false,
85
+ header: 'x-latency-ms'
86
+ }
87
+
88
+ const PARAMS_CONFIG = {
89
+ service: undefined,
90
+ components: undefined,
91
+ RegistrarCls: ComponentRegistrar,
92
+ cookie: {},
93
+ healthCheck: {},
94
+ latencyTracker: {},
95
+ logging: {},
96
+ swagger: {}
97
+ }
98
+
99
+ function loadConfigDefault (config, defaultConfig) {
100
+ for (const key of Object.keys(config)) {
101
+ if (!Object.prototype.hasOwnProperty.call(defaultConfig, key)) {
102
+ throw new Error(`Unknown config ${key}`)
103
+ }
104
+ }
105
+ if (!config.disabled) {
106
+ for (const [key, defaultValue] of Object.entries(defaultConfig)) {
107
+ if (defaultValue === undefined && !config[key]) {
108
+ throw new Error(`Missing required value for ${key}`)
109
+ }
110
+ }
111
+ }
112
+ Object.assign(config, { ...defaultConfig, ...config })
113
+ }
114
+
115
+ /**
116
+ * @param {Object} params
117
+ * @param {string} params.service Name of the service, for example, iam,
118
+ * user-id, leaderboard. This affects API's prefixes.
119
+ * @param {Array<API|Model|component>} params.components A list of
120
+ * components.
121
+ * @param {object} [params.RegistrarCls=ComponentRegistrar] A subclass of
122
+ * ComponentRegistrar
123
+ * @param {CookieConfig} [params.cookie] Configures fastify-cookie.
124
+ * @param {HealthCheckConfig} [params.healthCheck] Configures health check endpoint.
125
+ * @param {LatencyTrackerConfig} [params.latencyTracker]
126
+ * @param {LoggingConfig} [params.logging] Configures logging.
127
+ * @param {SwaggerConfig} [params.swagger] Configures swagger.
128
+ * @returns {Promise<server>} fastify app with configured plugins
129
+ */
130
+ export default async function makeService (params = {}) {
131
+ const configs = [
132
+ [() => params, PARAMS_CONFIG],
133
+ [() => params.cookie, COOKIE_CONFIG],
134
+ [() => params.healthCheck, HEALTH_CHECK_CONFIG],
135
+ [() => params.latencyTracker, LATENCY_TRACKER_CONFIG],
136
+ [() => params.logging, LOGGING_CONFIG],
137
+ [() => params.swagger, SWAGGER_CONFIG]
138
+ ]
139
+ for (const [getter, defaultConfig] of configs) {
140
+ loadConfigDefault(getter(), defaultConfig)
141
+ }
142
+ const {
143
+ service,
144
+ components,
145
+ RegistrarCls,
146
+ cookie,
147
+ healthCheck,
148
+ latencyTracker,
149
+ logging,
150
+ swagger
151
+ } = params
152
+ const fastifyServerId = uuidv4()
153
+ let requestCount = 0
154
+ const ajvParams = {
155
+ removeAdditional: false,
156
+ allErrors: logging.reportAllErrors,
157
+ useDefaults: true,
158
+ strictSchema: false,
159
+ strictRequired: true
160
+ }
161
+ const ajvObjs = {
162
+ coerceTypes: new Ajv({ ...ajvParams, coerceTypes: true }),
163
+ noCoerceTypes: new Ajv({ ...ajvParams, coerceTypes: false })
164
+ }
165
+ const logger = makePinoLoggerOptions(logging.customizePinoOpts)
166
+ const app = fastify({
167
+ disableRequestLogging: true,
168
+ logger,
169
+ genReqId: () => `${fastifyServerId}-${++requestCount}`
170
+ })
171
+ .setValidatorCompiler(({ httpPart, schema }) => {
172
+ // header, route, and query string always come as strings; coerce them
173
+ // to the correct types if possible; body should come exactly in the
174
+ // right type so don't allow type coercion there
175
+ const useCoerce = httpPart !== 'body'
176
+ const ajv = useCoerce ? ajvObjs.coerceTypes : ajvObjs.noCoerceTypes
177
+ const validate = ajv.compile(schema)
178
+ return (value) => !validate(value)
179
+ ? ({ value, error: validate.errors })
180
+ : ({ value })
181
+ })
182
+ .addHook('onResponse', (req, reply, done) => {
183
+ let objToLog = {}
184
+ try {
185
+ objToLog = logger.serializers.res(reply.raw)
186
+ Object.assign(objToLog, logger.serializers.req(req))
187
+ // istanbul ignore else
188
+ if (latencyTracker.header) {
189
+ // latency in milliseconds
190
+ objToLog.latency = reply.getHeader(latencyTracker.header)
191
+ }
192
+ } catch (err) /* istanbul ignore next */ {
193
+ // crashes in onResponse() aren't reported or caught unless we do this
194
+ if (typeof objToLog !== 'object') {
195
+ objToLog = {}
196
+ }
197
+ objToLog.failedToLog = JSON.stringify(err)
198
+ }
199
+ req.log.info(objToLog)
200
+ done()
201
+ })
202
+
203
+ const registrar = new RegistrarCls(app, service)
204
+
205
+ app.register(cookiePlugin, { cookie })
206
+ .register(compressPlugin)
207
+ .register(contentParserPlugin)
208
+ .register(latencyTrackerPlugin, { latencyTracker })
209
+ .register(errorHandlerPlugin, {
210
+ errorHandler: {
211
+ returnErrorDetail: logging.reportErrorDetail,
212
+ sentryDSN: logging.sentryDSN,
213
+ sentryRateLimiter: logging.sentryRateLimiter,
214
+ serverName: fastifyServerId,
215
+ service
216
+ }
217
+ })
218
+ .register(healthCheckPlugin, { healthCheck })
219
+ .register(swaggerPlugin, { swagger: { service, ...swagger } })
220
+
221
+ await registrar.registerComponents(components)
222
+ return app
223
+ }
@@ -0,0 +1,27 @@
1
+ export function makePinoLoggerOptions (customizeOpts) {
2
+ function serializeReq (req) {
3
+ const q = req.query
4
+ const path = req.routeOptions.config.url
5
+ // istanbul ignore else
6
+ if (req.raw) {
7
+ req = req.raw
8
+ }
9
+ return {
10
+ uid: req.headers['x-uid'] || '',
11
+ method: req.method,
12
+ ua: req.headers['user-agent'] || '',
13
+ path,
14
+ q
15
+ }
16
+ }
17
+ customizeOpts = customizeOpts ?? (x => x)
18
+ const options = customizeOpts({
19
+ base: null, // omit pino default fields like pid and hostname
20
+ level: 'debug',
21
+ serializers: {
22
+ req: serializeReq,
23
+ res: res => ({ status: res.statusCode })
24
+ }
25
+ })
26
+ return options
27
+ }
@@ -0,0 +1,28 @@
1
+ // eslint babel made changes to chained expressions which is breaking ast-types
2
+ // https://github.com/babel/babel/issues/11908
3
+
4
+ // ast-types has a fix, but it is not yet in a versioned release
5
+ // https://github.com/benjamn/ast-types/pull/399
6
+
7
+ // to circumvent problems caused by importing zlib, we do it in a separate file
8
+ // so we can ignore it in linting
9
+ import zlib from 'node:zlib'
10
+
11
+ import { fastifyCompress } from '@fastify/compress'
12
+ import fp from 'fastify-plugin'
13
+
14
+ // TODO: fine tune configurations once we have a reliable usage pattern
15
+ export default fp(function (fastify, options, next) {
16
+ fastify.register(fastifyCompress, {
17
+ threshold: 20,
18
+ brotliOptions: {
19
+ params: {
20
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4
21
+ }
22
+ }
23
+ })
24
+ next()
25
+ }, {
26
+ fastify: '>=3.x',
27
+ name: 'compress'
28
+ })
@@ -0,0 +1,21 @@
1
+ import fp from 'fastify-plugin'
2
+
3
+ function addContentParser (fastify, options, next) {
4
+ fastify.addContentTypeParser('application/json',
5
+ { parseAs: 'string' },
6
+ function (req, body, done) {
7
+ try {
8
+ const json = JSON.parse(body || '{}')
9
+ done(null, json)
10
+ } catch (err) {
11
+ err.statusCode = 400
12
+ done(err, undefined)
13
+ }
14
+ })
15
+ next()
16
+ }
17
+
18
+ export default fp(addContentParser, {
19
+ fastify: '>=3.x',
20
+ name: 'content-parser'
21
+ })
@@ -0,0 +1,17 @@
1
+ import cookiePlugin from '@fastify/cookie'
2
+ import fp from 'fastify-plugin'
3
+
4
+ function addCookiePlugin (fastify, options, next) {
5
+ // istanbul ignore if
6
+ if (options.cookie.disabled || !options.cookie.secret) {
7
+ next()
8
+ return
9
+ }
10
+ fastify.register(cookiePlugin, { secret: options.cookie.secret })
11
+ next()
12
+ }
13
+
14
+ export default fp(addCookiePlugin, {
15
+ fastify: '>=3.x',
16
+ name: 'content-parser'
17
+ })
@@ -0,0 +1,180 @@
1
+ import S from '@pbvision/schema'
2
+ import * as Sentry from '@sentry/node'
3
+ import fp from 'fastify-plugin'
4
+
5
+ import { InvalidInputException, __RequestDone } from '../api/exception.js'
6
+ import { createSentryRateLimiter } from './sentry-rate-limit.js'
7
+
8
+ export default fp(function (fastify, options, next) {
9
+ const isLocalhost = process.env.NODE_ENV === 'localhost'
10
+ const sentryDSN = options.errorHandler.sentryDSN
11
+ // istanbul ignore next
12
+ const isSentryEnabled = sentryDSN && !isLocalhost
13
+ Sentry.init({
14
+ dsn: sentryDSN,
15
+ enabled: isSentryEnabled,
16
+ environment: process.env.NODE_ENV,
17
+ release: `${options.errorHandler.service}@${process.env.GIT_HASH ?? 'unknown'}`,
18
+ serverName: options.errorHandler.serverName
19
+ })
20
+
21
+ // Opt-in rate limiter: only errors flagged via RequestError.rateLimitSentry()
22
+ // consult this. See sentry-rate-limit.js for details.
23
+ const sentryRateLimiter = options.errorHandler.sentryRateLimiter ??
24
+ createSentryRateLimiter()
25
+
26
+ const returnErrorDetail = options.errorHandler.returnErrorDetail
27
+ // log any exception which occurs
28
+ fastify.setErrorHandler(async (error, req, reply) => {
29
+ // extract the relevant bit of the traceback: remove fastify lines
30
+ // istanbul ignore next
31
+ const traceback = error.stack?.split('\n') ?? []
32
+ // istanbul ignore next
33
+ const errorMessage = error.message?.split('\n') ?? ''
34
+ traceback.splice(0, errorMessage.length)
35
+ let removeFromIdx
36
+ if (error instanceof InvalidInputException) {
37
+ removeFromIdx = 1
38
+ } else {
39
+ for (let i = traceback.length - 1; i > 0; i--) {
40
+ const tbLine = traceback[i]
41
+ if (tbLine.indexOf('/fastify/lib') !== -1) {
42
+ removeFromIdx = i + 1
43
+ break
44
+ }
45
+ }
46
+ }
47
+ removeFromIdx = removeFromIdx ?? traceback.length
48
+
49
+ traceback.splice(removeFromIdx, traceback.length - removeFromIdx)
50
+
51
+ const response = reply.raw
52
+ /* istanbul ignore next */
53
+ const message = error.message || 'empty error message'
54
+ const statusCode = error.httpCode ?? error.statusCode ?? 500
55
+ reply.code(statusCode)
56
+ const errInfo = {
57
+ msg: message,
58
+ req,
59
+ status: statusCode,
60
+ stack: traceback
61
+ }
62
+
63
+ // improve the error emitted from bad requests (invalid input)
64
+ const isCrash = errInfo.status >= 500
65
+ let customFingerprint = false
66
+ if (!isCrash) {
67
+ const firstTB = traceback[0]
68
+ /* istanbul ignore else */
69
+ if (firstTB) {
70
+ /* istanbul ignore else */
71
+ if (firstTB.indexOf('fastify/lib/contentTypeParser.js') !== -1) {
72
+ customFingerprint = 'Content-Type Not Permitted'
73
+ } else if (error instanceof S.ValidationError) {
74
+ customFingerprint = message
75
+ }
76
+ /* istanbul ignore next */
77
+ if (customFingerprint) {
78
+ if (customFingerprint.indexOf(errInfo.msg) === -1) {
79
+ // prefix the error message with the custom fingerprint text if the
80
+ // fingerprint didn't already contain all of the error message text
81
+ errInfo.msg = customFingerprint + ': ' + errInfo.msg
82
+ }
83
+ // changing the error name results in a cleaner description on the
84
+ // Sentry dashboard
85
+ error.name = customFingerprint
86
+ }
87
+ }
88
+ }
89
+
90
+ Object.getOwnPropertyNames(error).forEach(key => {
91
+ if (key !== 'stack' && key !== 'message') {
92
+ if (!errInfo.error) {
93
+ errInfo.error = {}
94
+ }
95
+ errInfo.error[key] = error[key]
96
+ }
97
+ })
98
+ response.logged = true // don't double-log
99
+ if (statusCode >= 500) {
100
+ reply.log.error(errInfo)
101
+ } else {
102
+ reply.log.info(errInfo)
103
+ }
104
+
105
+ // Check if this error is opted in to Sentry rate limiting; if so and we're
106
+ // inside the window, skip Sentry.captureException entirely. The HTTP
107
+ // response and logs are unaffected regardless.
108
+ let shouldCaptureToSentry = true
109
+ let suppressedCount = 0
110
+ const rateLimitMs = error._sentryRateLimitMs
111
+ if (rateLimitMs) {
112
+ const rlKey = customFingerprint || message
113
+ const rlResult = sentryRateLimiter.shouldReport(rlKey, rateLimitMs)
114
+ shouldCaptureToSentry = rlResult.report
115
+ suppressedCount = rlResult.suppressedCount
116
+ }
117
+
118
+ if (shouldCaptureToSentry) {
119
+ Sentry.withScope(function (scope) {
120
+ if (customFingerprint) {
121
+ scope.setFingerprint(customFingerprint)
122
+ }
123
+ const user = {}
124
+ // istanbul ignore if
125
+ if (req.headers['x-uid']) {
126
+ user.id = req.headers['x-uid']
127
+ } else {
128
+ user.ip = req.ip
129
+ }
130
+ scope.setLevel(isCrash ? 'error' : 'warning')
131
+ scope.setUser({
132
+ ...user,
133
+ ...(req.__sentry?.userInfo ?? {})
134
+ })
135
+ scope.setTags({
136
+ ...(req.__sentry?.tags ?? {}),
137
+ method: req.method,
138
+ url: req.url,
139
+ status: errInfo.status,
140
+ // Tag -- not extra -- so operators can filter Sentry for "issues
141
+ // where rate-limiting kicked in" during an outage.
142
+ ...(suppressedCount > 0
143
+ ? { suppressedSimilarEvents: String(suppressedCount) }
144
+ : {})
145
+ })
146
+ const customContexts = req.__sentry?.context ?? {}
147
+ for (const [k, v] of Object.entries(customContexts)) {
148
+ scope.setContext(k, v)
149
+ }
150
+ const extras = {
151
+ msg: errInfo.message,
152
+ reqId: req.id,
153
+ userAgent: req.headers['user-agent'] ?? 'not set'
154
+ }
155
+ if (error instanceof __RequestDone) {
156
+ extras.data = error.data
157
+ }
158
+ scope.setExtras(extras)
159
+ Sentry.captureException(error)
160
+ })
161
+ }
162
+
163
+ const errorData = error.respData ?? {
164
+ code: error.constructor.name,
165
+ message: customFingerprint || error.message
166
+ }
167
+
168
+ // istanbul ignore else
169
+ if (returnErrorDetail && !error.respData) {
170
+ errorData.detail = errInfo.msg
171
+ errorData.stack = errInfo.stack
172
+ }
173
+
174
+ await reply.header('Content-Type', 'application/json; charset=utf-8')
175
+ .serializer(o => JSON.stringify(o, null, 2))
176
+ .send(errorData)
177
+ })
178
+
179
+ next()
180
+ })
@@ -0,0 +1,19 @@
1
+ import fp from 'fastify-plugin'
2
+
3
+ export default fp(function (fastify, options, next) {
4
+ // istanbul ignore if
5
+ if (options.healthCheck.disabled) {
6
+ next()
7
+ return
8
+ }
9
+ // istanbul ignore next
10
+ const path = options.healthCheck.path ?? '/'
11
+ fastify.get(path, { schema: { hide: true } },
12
+ async (req, reply) => {
13
+ await reply.send()
14
+ })
15
+ next()
16
+ }, {
17
+ fastify: '>=3.x',
18
+ name: 'healthCheck'
19
+ })
@@ -0,0 +1,34 @@
1
+ import fp from 'fastify-plugin'
2
+ const symbolRequestTime = Symbol('RequestTimer')
3
+
4
+ export default fp(function (fastify, options, next) {
5
+ // istanbul ignore if
6
+ if (options.latencyTracker.disabled) {
7
+ next()
8
+ return
9
+ }
10
+
11
+ const header = options.latencyTracker.header
12
+ fastify.addHook('onRequest', function onRequestHandler (req, reply, done) {
13
+ // Start the recording of process time
14
+ req.raw[symbolRequestTime] = process.hrtime.bigint()
15
+
16
+ done()
17
+ })
18
+
19
+ fastify.addHook('onSend', async (req, reply, payload) => {
20
+ const fixedDigits = 3
21
+ // Calculate the duration, in nanoseconds
22
+ const hrDuration = process.hrtime.bigint() - req.raw[symbolRequestTime]
23
+ // convert it to milliseconds
24
+ const duration = (Number(hrDuration) / 1e6).toFixed(fixedDigits)
25
+ // add the header to the response
26
+ reply.header(header, duration)
27
+ return payload
28
+ })
29
+
30
+ next()
31
+ }, {
32
+ fastify: '>=3.x',
33
+ name: 'latency-tracker'
34
+ })
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Creates a rate limiter for Sentry event reporting, keyed by an arbitrary
3
+ * string (typically the Sentry fingerprint or error message). State lives in
4
+ * a Map per rate limiter instance: one instance per process is the expected
5
+ * usage.
6
+ *
7
+ * @param {Function} [nowFn=Date.now] clock function (injectable for tests)
8
+ * @returns {Object} rate limiter with one method, {@link shouldReport}.
9
+ */
10
+ export function createSentryRateLimiter (nowFn = Date.now) {
11
+ const tracking = new Map()
12
+ return {
13
+ /**
14
+ * Record an event and decide whether it should be reported to Sentry.
15
+ *
16
+ * @param {String} key rate-limit key; typically the error fingerprint or
17
+ * message so that Sentry grouping lines up with our suppression.
18
+ * @param {Number} windowMs rate-limit window in ms; within this window
19
+ * after a report, subsequent events for the same key are suppressed.
20
+ * @returns {{report: Boolean, suppressedCount: Number}} when `report` is
21
+ * true, `suppressedCount` is the number of events suppressed for this
22
+ * key since the last report (0 if none were suppressed). When `report`
23
+ * is false, `suppressedCount` is 0 (caller should not surface it).
24
+ */
25
+ shouldReport (key, windowMs) {
26
+ const now = nowFn()
27
+ const state = tracking.get(key)
28
+ if (state && now - state.lastReported < windowMs) {
29
+ state.suppressed++
30
+ return { report: false, suppressedCount: 0 }
31
+ }
32
+ const suppressedCount = state?.suppressed ?? 0
33
+ tracking.set(key, { lastReported: now, suppressed: 0 })
34
+ return { report: true, suppressedCount }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,50 @@
1
+ // istanbul ignore file
2
+ import swagger from '@fastify/swagger'
3
+ import swaggerUI from '@fastify/swagger-ui'
4
+ import fp from 'fastify-plugin'
5
+
6
+ export default fp(function (fastify, options, next) {
7
+ if (options.swagger.disabled) {
8
+ next()
9
+ return
10
+ }
11
+
12
+ const authHeaders = options.swagger.authHeaders
13
+ fastify.register(swagger, {
14
+ openapi: {
15
+ servers: options.swagger.servers.map(x => { return { url: x } }),
16
+ info: {
17
+ title: `${options.swagger.service.toUpperCase()} Service`
18
+ },
19
+ consumes: ['application/json'],
20
+ produces: ['application/json'],
21
+ components: {
22
+ securitySchemes: authHeaders.reduce((all, c) => {
23
+ all[c.replace('x-', '')] = {
24
+ type: 'apiKey',
25
+ name: c,
26
+ in: 'header'
27
+ }
28
+ return all
29
+ }, {})
30
+ }
31
+ }
32
+ })
33
+ fastify.register(swaggerUI, {
34
+ routePrefix: options.swagger.routePrefix,
35
+ exposeRoute: true,
36
+ uiConfig: {
37
+ docExpansion: 'list',
38
+ deepLinking: false,
39
+ filter: true
40
+ }
41
+ })
42
+ // fastify.addHook('onReady', function (done) {
43
+ // fastify.swagger()
44
+ // done()
45
+ // })
46
+ next()
47
+ }, {
48
+ fastify: '>=3.x',
49
+ name: 'swagger'
50
+ })