• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mia-platform / crud-service / 8505752072

01 Apr 2024 08:19AM UTC coverage: 97.14% (+0.01%) from 97.127%
8505752072

Pull #284

github

web-flow
Merge c5cdf06e2 into 18bf8b65f
Pull Request #284: build(deps): bump the fastify group with 1 update

1811 of 1958 branches covered (92.49%)

Branch coverage included in aggregate %.

9058 of 9231 relevant lines covered (98.13%)

7219.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.42
/index.js
1
/*
94✔
2
 * Copyright 2023 Mia s.r.l.
94✔
3
 *
94✔
4
 * Licensed under the Apache License, Version 2.0 (the "License");
94✔
5
 * you may not use this file except in compliance with the License.
94✔
6
 * You may obtain a copy of the License at
94✔
7
 *
94✔
8
 *     http://www.apache.org/licenses/LICENSE-2.0
94✔
9
 *
94✔
10
 * Unless required by applicable law or agreed to in writing, software
94✔
11
 * distributed under the License is distributed on an "AS IS" BASIS,
94✔
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
94✔
13
 * See the License for the specific language governing permissions and
94✔
14
 * limitations under the License.
94✔
15
 */
94✔
16

94✔
17
'use strict'
94✔
18

94✔
19
const fp = require('fastify-plugin')
94✔
20
const fastifyEnv = require('@fastify/env')
94✔
21
const fastifyMultipart = require('@fastify/multipart')
94✔
22

94✔
23
const ajvFormats = require('ajv-formats')
94✔
24

94✔
25
const { unset: lunset } = require('lodash')
94✔
26
const { readdirSync } = require('fs')
94✔
27
const { join } = require('path')
94✔
28
const { ObjectId } = require('mongodb')
94✔
29
const { JSONPath } = require('jsonpath-plus')
94✔
30

94✔
31
const myPackage = require('./package')
94✔
32
const fastifyEnvSchema = require('./envSchema')
94✔
33

94✔
34
const httpInterface = require('./lib/httpInterface')
94✔
35
const loadModels = require('./lib/loadModels')
94✔
36
const joinPlugin = require('./lib/joinPlugin')
94✔
37

94✔
38
const { castCollectionId } = require('./lib/pkFactories')
94✔
39
const {
94✔
40
  SCHEMA_CUSTOM_KEYWORDS,
94✔
41
  SETCMD,
94✔
42
  PUSHCMD,
94✔
43
  PULLCMD,
94✔
44
  UNSETCMD,
94✔
45
  ADDTOSETCMD,
94✔
46
} = require('./lib/consts')
94✔
47
const { registerMongoInstances } = require('./lib/mongo/mongo-plugin')
94✔
48
const { getAjvResponseValidationFunction } = require('./lib/validatorGetters')
94✔
49
const { pointerSeparator } = require('./lib/JSONPath.utils')
94✔
50
const { registerHelperRoutes } = require('./lib/helpersRoutes')
94✔
51

94✔
52
async function registerCrud(fastify, { modelName, isView }) {
4,404✔
53
  if (!fastify.mongo) { throw new Error('`fastify.mongo` is undefined!') }
4,404!
54
  if (!modelName) { throw new Error('`modelName` is undefined!') }
4,404!
55

4,404✔
56
  fastify.log.trace({ modelName }, 'Registering CRUD')
4,404✔
57

4,404✔
58
  const model = fastify.models[modelName]
4,404✔
59
  const prefix = model.definition.endpointBasePath
4,404✔
60

4,404✔
61
  fastify.decorate('crudService', model.crudService)
4,404✔
62
  fastify.decorate('queryParser', model.queryParser)
4,404✔
63
  fastify.decorate('castResultsAsStream', model.castResultsAsStream)
4,404✔
64
  fastify.decorate('castItem', model.castItem)
4,404✔
65
  fastify.decorate('allFieldNames', model.allFieldNames)
4,404✔
66
  fastify.decorate('jsonSchemaGenerator', model.jsonSchemaGenerator)
4,404✔
67
  fastify.decorate('jsonSchemaGeneratorWithNested', model.jsonSchemaGeneratorWithNested)
4,404✔
68
  fastify.decorate('modelName', modelName)
4,404✔
69
  fastify.register(httpInterface, { prefix, registerGetters: true, registerSetters: !isView })
4,404✔
70
}
4,404✔
71

94✔
72
async function registerViewCrud(fastify, { modelName, lookups }) {
12✔
73
  if (!fastify.mongo) { throw new Error('`fastify.mongo` is undefined!') }
12!
74
  if (!modelName) { throw new Error('`modelName` is undefined!') }
12!
75

12✔
76
  fastify.log.trace({ modelName }, 'Registering View CRUD')
12✔
77

12✔
78
  const { definition, viewDependencies, crudService } = fastify.models[modelName]
12✔
79
  const prefix = definition.endpointBasePath
12✔
80

12✔
81
  fastify.decorate('crudService', viewDependencies.crudService)
12✔
82
  fastify.decorate('queryParser', viewDependencies.queryParser)
12✔
83
  fastify.decorate('castResultsAsStream', viewDependencies.castResultsAsStream)
12✔
84
  fastify.decorate('castItem', viewDependencies.castItem)
12✔
85
  fastify.decorate('allFieldNames', viewDependencies.allFieldNames)
12✔
86
  fastify.decorate('jsonSchemaGenerator', viewDependencies.jsonSchemaGenerator)
12✔
87
  fastify.decorate('jsonSchemaGeneratorWithNested', viewDependencies.jsonSchemaGenerator)
12✔
88
  fastify.decorate('modelName', modelName)
12✔
89

12✔
90
  // To allow writing views without having to rewrite all the logic of the HttpInterface,
12✔
91
  // it was decided to adapt the fields of the calls towards the view by converting them
12✔
92
  // to the fields of the underlying collection, thus hiding the complexity on the client
12✔
93
  // side while maintaining consistent interfaces.
12✔
94
  // This assumes that the key of the value is in the field "value" and should be made configurable.
12✔
95
  fastify.addHook('preHandler', (request, _reply, done) => {
12✔
96
    for (const { as, localField } of lookups) {
12✔
97
      if (request?.body?.[as]) {
12✔
98
        const lookupReference = request.body[as]
4✔
99
        delete request.body[as]
4✔
100

4✔
101
        request.body[localField] = mapLookupToObjectId(lookupReference)
4✔
102
      }
4✔
103

12✔
104
      for (const command of [SETCMD, UNSETCMD, PUSHCMD, PULLCMD, ADDTOSETCMD]) {
12✔
105
        if (request?.body?.[command]?.[as]) {
60✔
106
          const lookupReference = request.body[command][as]
4✔
107
          delete request.body[command][as]
4✔
108

4✔
109
          request.body[command][localField] = mapLookupToObjectId(lookupReference)
4✔
110
        }
4✔
111
      }
60✔
112
    }
12✔
113

12✔
114
    done()
12✔
115
  })
12✔
116

12✔
117
  // To obtain the updated object with a consistent interface after a patch,
12✔
118
  // it is necessary to retrieve the view object again before returning it to the client.
12✔
119
  fastify.addHook('preSerialization', async function preSerializer(request, _reply, payload) {
12✔
120
    const { _id } = payload
10✔
121
    if (request.method === 'PATCH' && _id) {
10✔
122
      const docId = this.castCollectionId(_id)
4✔
123
      // eslint-disable-next-line no-underscore-dangle
4✔
124
      const doc = await crudService._mongoCollection.findOne({ _id: docId })
4✔
125
      const response = this.castItem(doc)
4✔
126
      const validatePatch = getAjvResponseValidationFunction(request.routeSchema.response['200'])
4✔
127
      validatePatch(response)
4✔
128
      return response
4✔
129
    }
4✔
130
    return payload
6✔
131
  })
12✔
132

12✔
133
  fastify.register(httpInterface, { prefix, registerGetters: false, registerSetters: true })
12✔
134
}
12✔
135

94✔
136
function mapLookupToObjectId(reference) {
8✔
137
  // consider both one-to-one and one-to-many relationships
8✔
138
  if (Array.isArray(reference)) {
8✔
139
    return reference
2✔
140
      .map(ref => (ref?.value ? new ObjectId(ref.value) : undefined))
2!
141
      .filter(Boolean)
2✔
142
  }
2✔
143

6✔
144
  return reference?.value ? new ObjectId(reference.value) : undefined
8!
145
}
8✔
146

94✔
147
async function registerViewCrudLookup(fastify, { modelName, lookupModel }) {
12✔
148
  if (!fastify.mongo) { throw new Error('`fastify.mongo` is undefined!') }
12!
149

12✔
150
  fastify.log.trace({ modelName }, 'Registering ViewLookup CRUD')
12✔
151

12✔
152
  const {
12✔
153
    as: modelField,
12✔
154
  } = lookupModel.lookup
12✔
155

12✔
156
  const { definition } = fastify.models[modelName]
12✔
157
  const prefix = definition.endpointBasePath
12✔
158
  const lookupPrefix = join(prefix, 'lookup', modelField)
12✔
159

12✔
160

12✔
161
  fastify.decorate('crudService', lookupModel.crudService)
12✔
162
  fastify.decorate('queryParser', lookupModel.queryParser)
12✔
163
  fastify.decorate('castResultsAsStream', lookupModel.castResultsAsStream)
12✔
164
  fastify.decorate('castItem', lookupModel.castItem)
12✔
165
  fastify.decorate('allFieldNames', lookupModel.allFieldNames)
12✔
166
  fastify.decorate('jsonSchemaGenerator', lookupModel.jsonSchemaGenerator)
12✔
167
  fastify.decorate('jsonSchemaGeneratorWithNested', lookupModel.jsonSchemaGenerator)
12✔
168
  fastify.decorate('modelName', modelName)
12✔
169
  fastify.decorate('lookupProjection', lookupModel.parsedLookupProjection)
12✔
170
  fastify.register(httpInterface, {
12✔
171
    prefix: lookupPrefix,
12✔
172
    registerGetters: false,
12✔
173
    registerSetters: false,
12✔
174
    registerLookup: true,
12✔
175
  })
12✔
176
}
12✔
177

94✔
178
const registerDatabase = fp(registerMongoInstances, { decorators: { fastify: ['config'] } })
94✔
179

94✔
180
async function iterateOverCollectionDefinitionAndRegisterCruds(fastify) {
225✔
181
  fastify.decorate('castCollectionId', castCollectionId(fastify))
225✔
182
  fastify.decorate('userIdHeaderKey', fastify.config.USER_ID_HEADER_KEY.toLowerCase())
225✔
183
  fastify.decorate('validateOutput', fastify.config.ENABLE_STRICT_OUTPUT_VALIDATION)
225✔
184

225✔
185
  for (const [modelName, model] of Object.entries(fastify.models)) {
225✔
186
    const { isView, viewLookupsEnabled, viewDependencies } = model
4,404✔
187
    if (viewLookupsEnabled) {
4,404!
188
      const lookups = viewDependencies.lookupsModels.map(({ lookup }) => lookup)
12✔
189
      fastify.register(registerViewCrud, {
12✔
190
        modelName,
12✔
191
        lookups,
12✔
192
      })
12✔
193

12✔
194
      for (const lookupModel of viewDependencies.lookupsModels) {
12✔
195
        fastify.register(registerViewCrudLookup, {
12✔
196
          modelName,
12✔
197
          lookupModel,
12✔
198
        })
12✔
199
      }
12✔
200
    }
12✔
201

4,404✔
202
    fastify.register(registerCrud, {
4,404✔
203
      modelName,
4,404✔
204
      isView,
4,404✔
205
    })
4,404✔
206
  }
4,404✔
207
}
225✔
208

94✔
209
const validCrudFolder = path => !['.', '..'].includes(path) && /\.js(on)?$/.test(path)
94✔
210

94✔
211
async function setupCruds(fastify) {
249✔
212
  const {
249✔
213
    COLLECTION_DEFINITION_FOLDER,
249✔
214
    VIEWS_DEFINITION_FOLDER,
249✔
215
    HELPERS_PREFIX,
249✔
216
  } = fastify.config
249✔
217

249✔
218
  const collections = readdirSync(COLLECTION_DEFINITION_FOLDER)
249✔
219
    .filter(validCrudFolder)
249✔
220
    .map(path => join(COLLECTION_DEFINITION_FOLDER, path))
249✔
221
    .map(require)
249✔
222

249✔
223
  fastify.decorate('collections', collections)
249✔
224

249✔
225
  const viewsFolder = VIEWS_DEFINITION_FOLDER
249✔
226
  if (viewsFolder) {
249✔
227
    const views = readdirSync(viewsFolder)
219✔
228
      .filter(validCrudFolder)
219✔
229
      .map(path => join(viewsFolder, path))
219✔
230
      .map(require)
219✔
231

219✔
232
    fastify.decorate('views', views)
219✔
233
  }
219✔
234

249✔
235
  if (collections.length > 0) {
249✔
236
    fastify
228✔
237
      .register(registerDatabase)
228✔
238
      .register(fp(loadModels))
228✔
239
      .register(iterateOverCollectionDefinitionAndRegisterCruds)
228✔
240
      .register(joinPlugin, { prefix: '/join' })
228✔
241
      .register(registerHelperRoutes, { prefix: HELPERS_PREFIX })
228✔
242
  }
228✔
243
}
249✔
244

94✔
245
/* =============================================================================== */
94✔
246

94✔
247
module.exports = async function plugin(fastify, opts) {
94✔
248
  await fastify
276✔
249
    .register(fastifyEnv, { schema: fastifyEnvSchema, data: opts })
276✔
250
  fastify.register(fastifyMultipart, {
249✔
251
    limits: {
249✔
252
      fields: 5,
249✔
253
      // Conversion Byte to Mb
249✔
254
      fileSize: fastify.config.MAX_MULTIPART_FILE_BYTES * 1000000,
249✔
255
      files: 1,
249✔
256
    },
249✔
257
  })
249✔
258
    .register(fp(setupCruds, { decorators: { fastify: ['config'] } }))
249✔
259
}
276✔
260

94✔
261
module.exports.options = {
94✔
262
  trustProxy: process.env.TRUSTED_PROXIES,
94✔
263
  // configure Fastify to employ Ajv Formats plugin,
94✔
264
  // which components were stripped from main Ajv implementation
94✔
265
  ajv: {
94✔
266
    customOptions: {
94✔
267
      validateFormats: true,
94✔
268
    },
94✔
269
    plugins: [ajvFormats],
94✔
270
  },
94✔
271
}
94✔
272

94✔
273
module.exports.swaggerDefinition = {
94✔
274
  openApiSpecification: process.env.OPEN_API_SPECIFICATION ?? 'swagger',
94✔
275
  info: {
94✔
276
    title: 'Crud Service',
94✔
277
    description: myPackage.description,
94✔
278
    version: myPackage.version,
94✔
279
  },
94✔
280
}
94✔
281

94✔
282
module.exports.transformSchemaForSwagger = ({ schema, url } = {}) => {
94✔
283
  // route with undefined schema will not be shown
2,004✔
284
  if (!schema) {
2,004!
285
    return {
×
286
      url,
×
287
      schema: {
×
288
        hide: true,
×
289
      },
×
290
    }
×
291
  }
×
292

2,004✔
293
  const {
2,004✔
294
    params = undefined,
2,004✔
295
    body = undefined,
2,004✔
296
    querystring = undefined,
2,004✔
297
    response = undefined,
2,004✔
298
    ...others
2,004✔
299
  } = schema
2,004✔
300
  const transformed = { ...others }
2,004✔
301

2,004✔
302
  if (params) { transformed.params = getTransformedSchema(params) }
2,004✔
303
  if (body) { transformed.body = getTransformedSchema(body) }
2,004✔
304
  if (querystring) { transformed.querystring = getTransformedSchema(querystring) }
2,004✔
305
  if (response) {
2,004✔
306
    transformed.response = {
1,632✔
307
      ...response,
1,632✔
308
      ...response['200'] ? { 200: getTransformedSchema(response['200']) } : {},
1,632!
309
    }
1,632✔
310
  }
1,632✔
311

2,004✔
312
  return { schema: transformed, url }
2,004✔
313
}
2,004✔
314

94✔
315
function getTransformedSchema(httpPartSchema) {
4,338✔
316
  if (!httpPartSchema) { return }
4,338!
317
  const KEYS_TO_UNSET = [
4,338✔
318
    SCHEMA_CUSTOM_KEYWORDS.UNIQUE_OPERATION_ID,
4,338✔
319
    ...JSONPath({
4,338✔
320
      json: httpPartSchema,
4,338✔
321
      resultType: 'pointer',
4,338✔
322
      path: '$..[?(@ && @.type && Array.isArray(@.type))]',
4,338✔
323
    })
4,338✔
324
      .map(pointer => [
4,338✔
325
        `${pointer
948✔
326
          .slice(1)
948✔
327
          .replace(pointerSeparator, '.')}.type`,
948✔
328
        `${pointer
948✔
329
          .slice(1)
948✔
330
          .replace(pointerSeparator, '.')}.nullable`,
948✔
331
      ])
4,338✔
332
      .flat(),
4,338✔
333
  ]
4,338✔
334

4,338✔
335
  const response = httpPartSchema
4,338✔
336
  KEYS_TO_UNSET.forEach(keyToUnset => {
4,338✔
337
    lunset(response, `${keyToUnset}`)
6,234✔
338
  })
4,338✔
339

4,338✔
340
  return response
4,338✔
341
}
4,338✔
342

94✔
343
module.exports.getMetrics = function getMetrics(prometheusClient) {
94✔
344
  const collectionInvocation = new prometheusClient.Counter({
93✔
345
    name: 'mia_platform_crud_service_collection_invocation',
93✔
346
    help: 'Count collection invocation, not the document considered',
93✔
347
    labelNames: ['collection_name', 'type'],
93✔
348
  })
93✔
349

93✔
350
  return {
93✔
351
    collectionInvocation,
93✔
352
  }
93✔
353
}
93✔
354

94✔
355
// Note: when operating on a cluster with limited resources, due to fastify delay in registering,
94✔
356
// plugins we may miss the connectionCreated and connectionReady events thus we preferred using
94✔
357
// the isUp function that simply checks connection status is up and usable.
94✔
358
const isMongoUp = async(fastify) => fastify.collections.length === 0 || fastify.mongoDBCheckIsUp()
94✔
359

94✔
360
async function statusHandler(fastify) {
9✔
361
  const statusOK = await isMongoUp(fastify)
9✔
362
  const message = statusOK ? undefined : 'MongoDB status is unhealthy'
9✔
363
  return { statusOK, message }
9✔
364
}
9✔
365

94✔
366
async function readinessHandler(fastify) {
9✔
367
  const statusOK = await isMongoUp(fastify)
9✔
368

9✔
369
  return { statusOK }
9✔
370
}
9✔
371

94✔
372
module.exports.readinessHandler = readinessHandler
94✔
373
module.exports.checkUpHandler = statusHandler
94✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc