@pbvision/fastify-firestore-service 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,7 +81,6 @@ export default async (params) => makeService({
81
81
  },
82
82
  logging: {
83
83
  reportErrorDetail: true, // process.env.NODE_ENV === 'localhost',
84
- useUnitTestLogFormat: true, // process.env.NODE_ENV === 'localhost',
85
84
  reportAllErrors: true // process.env.NODE_ENV !== 'prod'
86
85
  },
87
86
  swagger: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pbvision/fastify-firestore-service",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Web Framework using Fastify and Firestore ORM",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,8 +41,8 @@
41
41
  "@fastify/static": "^6.12",
42
42
  "@fastify/swagger": "^8.1.0",
43
43
  "@fastify/swagger-ui": "^2.0.1",
44
- "@pbvision/firestore-orm": "^0.0.3",
45
- "@pbvision/schema": "^0.1.4",
44
+ "@pbvision/firestore-orm": "^0.0.6",
45
+ "@pbvision/schema": "^0.2.15",
46
46
  "@sentry/node": "^7.91.0",
47
47
  "fastify": "^4.10.0",
48
48
  "fastify-plugin": "^4.5.1",
@@ -55,10 +55,11 @@
55
55
  "@babel/eslint-parser": "^7.17.0",
56
56
  "@babel/plugin-transform-class-properties": "^7.12.1",
57
57
  "@babel/preset-env": "^7.17.12",
58
- "@pbvision/jest-unit-test": "^0.2.0",
58
+ "@pbvision/jest-unit-test": "^0.2.3",
59
59
  "babel-loader": "^9.1.3",
60
60
  "eslint": "^8.22.0",
61
61
  "eslint-config-standard": "17.1.0",
62
+ "eslint-import-resolver-webpack": "^0.13.8",
62
63
  "eslint-plugin-import": "^2.22.0",
63
64
  "eslint-plugin-node": "^11.1.0",
64
65
  "eslint-plugin-promise": "^6.0.0",
@@ -67,7 +68,6 @@
67
68
  "jsdoc": "3.6.11",
68
69
  "license-webpack-plugin": "^4.0.2",
69
70
  "nodemon": "^3.0.2",
70
- "pino-pretty": "^10.3.1",
71
71
  "standard": "^17.1.0",
72
72
  "superagent-defaults": "^0.1.14",
73
73
  "supertest": "^6.3.3",
package/src/make-app.js CHANGED
@@ -1,8 +1,9 @@
1
+ import Ajv from 'ajv/dist/2020.js'
1
2
  import fastify from 'fastify'
2
3
  import { v4 as uuidv4 } from 'uuid'
3
4
 
4
5
  import ComponentRegistrar from './component-registrar.js'
5
- import makeLogger from './make-logger.js'
6
+ import { makePinoLoggerOptions } from './make-logger.js'
6
7
  import compressPlugin from './plugins/compress.js'
7
8
  import contentParserPlugin from './plugins/content-parser.js'
8
9
  import cookiePlugin from './plugins/cookie.js'
@@ -23,17 +24,19 @@ const COOKIE_CONFIG = {
23
24
 
24
25
  /**
25
26
  * @typedef {object} LoggingConfig
26
- * @property {boolean} [useUnitTestLogFormat=false] Whether output logs to console with
27
- * pretty printing
28
27
  * @property {boolean} [reportAllErrors=false] Whether include all API
29
28
  * validation errors in error logging. Recommend to keep it off for production,
30
29
  * on for testing.
31
30
  * @property {boolean} [reportErrorDetail=false] Whether include all details
32
31
  * of an error. Recommend to keep it off for remote testing, on for local
33
32
  * testing.
33
+ * @property {String} [sentryDSN] The Sentry DSN to report errors to
34
+ * @property {function} [customizePinoOpts] An optional function customize the
35
+ * arguments to pass to Pino. Takes as input the options that will be sent
36
+ * to be Pino by default (can be modified). Return the new options.
34
37
  */
35
38
  const LOGGING_CONFIG = {
36
- useUnitTestLogFormat: false,
39
+ customizePinoOpts: null,
37
40
  reportAllErrors: false,
38
41
  reportErrorDetail: false,
39
42
  sentryDSN: null
@@ -67,9 +70,10 @@ const SWAGGER_CONFIG = {
67
70
 
68
71
  /**
69
72
  * @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
+ * @property {boolean} [disabled=false] Whether to record how long from when we
74
+ * start to process a request to when we start to send the response
75
+ * @property {string} [header='x-latency-ms'] response header to send the
76
+ * latency info back in
73
77
  */
74
78
  const LATENCY_TRACKER_CONFIG = {
75
79
  disabled: false,
@@ -142,21 +146,54 @@ export default async function makeService (params = {}) {
142
146
  } = params
143
147
  const fastifyServerId = uuidv4()
144
148
  let requestCount = 0
149
+ const ajvParams = {
150
+ removeAdditional: false,
151
+ allErrors: logging.reportAllErrors,
152
+ useDefaults: true,
153
+ strictSchema: false,
154
+ strictRequired: true
155
+ }
156
+ const ajvObjs = {
157
+ coerceTypes: new Ajv({ ...ajvParams, coerceTypes: true }),
158
+ noCoerceTypes: new Ajv({ ...ajvParams, coerceTypes: false })
159
+ }
160
+ const logger = makePinoLoggerOptions(logging.customizePinoOpts)
145
161
  const app = fastify({
146
- ignoreTrailingSlash: true,
147
162
  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
- }
163
+ logger,
164
+ genReqId: () => `${fastifyServerId}-${++requestCount}`
159
165
  })
166
+ .setValidatorCompiler(({ httpPart, schema }) => {
167
+ // header, route, and query string always come as strings; coerce them
168
+ // to the correct types if possible; body should come exactly in the
169
+ // right type so don't allow type coercion there
170
+ const useCoerce = httpPart !== 'body'
171
+ const ajv = useCoerce ? ajvObjs.coerceTypes : ajvObjs.noCoerceTypes
172
+ const validate = ajv.compile(schema)
173
+ return (value) => !validate(value)
174
+ ? ({ value, error: validate.errors })
175
+ : ({ value })
176
+ })
177
+ .addHook('onResponse', (req, reply, done) => {
178
+ let objToLog = {}
179
+ try {
180
+ objToLog = logger.serializers.res(reply.raw)
181
+ Object.assign(objToLog, logger.serializers.req(req))
182
+ // istanbul ignore else
183
+ if (latencyTracker.header) {
184
+ // latency in milliseconds
185
+ objToLog.latency = reply.getHeader(latencyTracker.header)
186
+ }
187
+ } catch (err) /* istanbul ignore next */ {
188
+ // crashes in onResponse() aren't reported or caught unless we do this
189
+ if (typeof objToLog !== 'object') {
190
+ objToLog = {}
191
+ }
192
+ objToLog.failedToLog = JSON.stringify(err)
193
+ }
194
+ req.log.info(objToLog)
195
+ done()
196
+ })
160
197
 
161
198
  const registrar = new RegistrarCls(app, service)
162
199
 
@@ -1,119 +1,28 @@
1
- // istanbul ignore file
2
- import querystring from 'node:querystring'
3
-
4
- import pino from 'pino'
5
- import wrap from 'word-wrap'
6
-
7
- // for unit testing, output to the console INSTEAD of to stdout
8
- // via pino (so that jest captures the output and groups it with the right test
9
- // suite)
10
- function getUnitTestLogFormatOverrides () {
11
- const prefixText = '/' + process.env.SERVICE + '/src/'
12
- function prettifier () {
13
- return (obj) => {
14
- if (obj.req) {
15
- const indent = ''
16
- if (!obj.status) {
17
- console.log(indent, obj.req.method, obj.req.path)
18
- } else {
19
- // output the status code and (if any) error message
20
- const msgs = wrap(obj.msg || '', { width: 80, indent }).split('\n')
21
- console.log(indent, ' \u2514 HTTP', obj.status, msgs[0])
22
- msgs.slice(1).forEach(msg => {
23
- console.log(indent, msg)
24
- })
25
- if (obj.error && obj.status >= 500) {
26
- console.log(`${indent}Error: ${JSON.stringify(obj.error, null, 2)}`)
27
- }
28
- if (obj.stack) {
29
- for (let i = 0; i < obj.stack.length; i++) {
30
- console.log(' ', obj.stack[i])
31
- }
32
- }
33
- }
34
- } else {
35
- // 1) Get the filename and line number the log message is from
36
- // the index of the first non-pino line may change in future versions
37
- // of fastify or pino; might be better to search each line until we
38
- // find /<service>/src (if present)
39
- const firstNonPinoLine = (new Error()).stack.split('\n')[5]
40
- const appFolderIdx = firstNonPinoLine.indexOf(prefixText)
41
- let prefix = ''
42
- if (appFolderIdx !== -1) {
43
- // prefix log message with filename and line number when logs
44
- // originated in our app's source code
45
- prefix = firstNonPinoLine.substring(
46
- appFolderIdx + 9,
47
- firstNonPinoLine.lastIndexOf(':')) + ' '
48
- }
49
-
50
- // 2) Create the logm msg: file, line, log message and log object
51
- const indent = ' \u2502 '
52
- const logObj = {}
53
- const ignoredKeys = ['level', 'msg', 'reqId', 'time', 'v']
54
- Object.getOwnPropertyNames(obj).forEach(key => {
55
- if (ignoredKeys.indexOf(key) === -1) {
56
- logObj[key] = obj[key]
57
- }
58
- })
59
- let msg = indent + prefix + (obj.msg || '')
60
- if (Object.keys(logObj).length) {
61
- msg += (obj.msg ? ' ' : '') + JSON.stringify(logObj)
62
- }
63
- if (obj.level < 30) {
64
- console.debug(msg)
65
- } else if (obj.level >= 50) {
66
- console.error(msg)
67
- } else if (obj.level >= 40) {
68
- console.warn(msg)
69
- } else {
70
- console.log(msg)
71
- }
72
- }
73
- return '' // skip pino logging
74
- }
75
- }
76
- return {
77
- prettifier,
78
- transport: {
79
- target: 'pino-pretty',
80
- options: {
81
- colorize: true
82
- }
83
- }
84
- }
85
- }
86
-
87
- export default function makeCustomLogger (useUnitTestLogFormat) {
1
+ export function makePinoLoggerOptions (customizeOpts) {
88
2
  function serializeReq (req) {
89
3
  const q = req.query
4
+ // istanbul ignore else
90
5
  if (req.raw) {
91
6
  req = req.raw
92
7
  }
93
8
  const path = req.path
94
- const qs = req.qs
95
9
  return {
96
10
  app: req.headers['x-app'] || '',
97
11
  uid: req.headers['x-uid'] || '',
98
12
  method: req.method,
99
13
  ua: req.headers['user-agent'] || '',
100
14
  path,
101
- q: q || querystring.decode(qs)
15
+ q
102
16
  }
103
17
  }
104
- const options = {
18
+ customizeOpts = customizeOpts ?? (x => x)
19
+ const options = customizeOpts({
105
20
  base: null, // omit pino default fields like pid and hostname
106
- level: (process.env.NODE_ENV === 'prod') ? 'info' : 'debug',
21
+ level: 'debug',
107
22
  serializers: {
108
23
  req: serializeReq,
109
- res: (res) => {
110
- return { status: res.statusCode, req: serializeReq(res.req) }
111
- }
24
+ res: res => ({ status: res.statusCode })
112
25
  }
113
- }
114
- // on localhost, we customize the logs to optimize for console-based debugging
115
- if (useUnitTestLogFormat) {
116
- Object.assign(options, getUnitTestLogFormatOverrides())
117
- }
118
- return pino(options)
26
+ })
27
+ return options
119
28
  }
@@ -27,17 +27,6 @@ export default fp(function (fastify, options, next) {
27
27
  return payload
28
28
  })
29
29
 
30
- fastify.addHook('onResponse', (req, reply, done) => {
31
- // if errored, then it was already logged
32
- if (!reply.raw.logged) {
33
- req.log.info({
34
- req,
35
- status: reply.raw.statusCode,
36
- latency: reply.getHeader(header)
37
- })
38
- }
39
- done()
40
- })
41
30
  next()
42
31
  }, {
43
32
  fastify: '>=3.x',
@@ -1,3 +1,3 @@
1
- import '@pbvision/firestore-orm/environment.js'
1
+ import '../node_modules/@pbvision/firestore-orm/environment.js'
2
2
 
3
3
  process.env.NODE_ENV = 'localhost'