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

mia-platform / crud-service / 8568193728

05 Apr 2024 10:10AM UTC coverage: 97.127% (+0.04%) from 97.091%
8568193728

push

github

danibix95
build(deps-dev): bump tap from 18.7.1 to 18.7.2

Bumps [tap](https://github.com/tapjs/tapjs) from 18.7.1 to 18.7.2.
- [Release notes](https://github.com/tapjs/tapjs/releases)
- [Commits](https://github.com/tapjs/tapjs/compare/tap@18.7.1...tap@18.7.2)

---
updated-dependencies:
- dependency-name: tap
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

1828 of 1977 branches covered (92.46%)

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

93.43
/lib/loadModels.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 Ajv = require('ajv')
94✔
20
const ajvFormats = require('ajv-formats')
94✔
21
const ajvKeywords = require('ajv-keywords')
94✔
22
const { omit: lomit } = require('lodash')
94✔
23
const plimit = require('p-limit')
94✔
24

94✔
25
const mergeViewsInCollections = require('./mergeViewsInCollections')
94✔
26
const { compatibilityModelJsonSchema, modelJsonSchema } = require('./model.jsonschema')
94✔
27
const createIndexes = require('./createIndexes')
94✔
28
const { OBJECTID, aggregationConversion } = require('./consts')
94✔
29
const JSONSchemaGenerator = require('./JSONSchemaGenerator')
94✔
30
const AdditionalCaster = require('./AdditionalCaster')
94✔
31
const QueryParser = require('./QueryParser')
94✔
32
const CrudService = require('./CrudService')
94✔
33
const generatePathFieldsForRawSchema = require('./generatePathFieldsForRawSchema')
94✔
34
const { getIdType } = require('./mongo/mongo-plugin')
94✔
35
const { getDatabaseNameByType } = require('./pkFactories')
94✔
36

94✔
37
const ajv = new Ajv({ useDefaults: true })
94✔
38
ajvFormats(ajv)
94✔
39
ajvKeywords(ajv)
94✔
40

94✔
41
const compatibilityValidate = ajv.compile(compatibilityModelJsonSchema)
94✔
42
const validate = ajv.compile(modelJsonSchema)
94✔
43

94✔
44
const PREFIX_OF_INDEX_NAMES_TO_PRESERVE = 'preserve_'
94✔
45
const VIEW_TYPE = 'view'
94✔
46

94✔
47
const limiter = plimit(5)
94✔
48

94✔
49
async function loadModels(fastify) {
225✔
50
  const { collections, views = [] } = fastify
225✔
51
  const mergedCollections = mergeViewsInCollections(collections, views)
225✔
52

225✔
53
  fastify.log.trace({ collectionNames: mergedCollections.map(coll => coll.name) }, 'Registering CRUDs and Views')
225✔
54

225✔
55
  // A side-effect from the collectionModelMapper updates the following data models.
225✔
56
  const models = {}
225✔
57
  const existingStringCollection = []
225✔
58
  const existingObjectIdCollection = []
225✔
59

225✔
60
  const promises = mergedCollections.map(
225✔
61
    (collectionDefinition) => limiter(() =>
225✔
62
      collectionModelMapper(
4,404✔
63
        fastify,
4,404✔
64
        mergedCollections,
4,404✔
65
        { models, existingStringCollection, existingObjectIdCollection }
4,404✔
66
      )(collectionDefinition)
4,404✔
67
    )
4,404✔
68
  )
225✔
69
  await Promise.all(promises)
225✔
70
  fastify.decorate('models', models)
225✔
71
}
225✔
72

94✔
73
function collectionModelMapper(
4,404✔
74
  fastify,
4,404✔
75
  mergedCollections,
4,404✔
76
  // The previous implementation of the mapper was with a closure function that applied a
4,404✔
77
  // side-effect on these values. During the refactor to apply p-limit I've decided not to
4,404✔
78
  // change the implementation, therefore I've wrapped the params to isolate them.
4,404✔
79
  { models, existingStringCollection, existingObjectIdCollection }
4,404✔
80
) {
4,404✔
81
  // eslint-disable-next-line max-statements
4,404✔
82
  return async(collectionDefinition) => {
4,404✔
83
    // avoid validating the collection definition twice, since it would only
4,404✔
84
    // match one of the two, depending on the existence of schema property
4,404✔
85
    if (!collectionDefinition.schema) {
4,404✔
86
      if (!compatibilityValidate(collectionDefinition)) {
4,380!
87
        fastify.log.error({ collection: collectionDefinition.name }, compatibilityValidate.errors)
×
88
        throw new Error(`invalid collection definition: ${JSON.stringify(compatibilityValidate.errors)}`)
×
89
      }
×
90
    } else if (!validate(collectionDefinition)) {
4,404!
91
      fastify.log.error(validate.errors)
×
92
      throw new Error(`invalid collection definition: ${JSON.stringify(validate.errors)}`)
×
93
    }
×
94

4,404✔
95
    const {
4,404✔
96
      source: viewSourceCollectionName,
4,404✔
97
      name: collectionName,
4,404✔
98
      endpointBasePath: collectionEndpoint,
4,404✔
99
      type,
4,404✔
100
      indexes = [],
4,404✔
101
      enableLookup,
4,404✔
102
      source,
4,404✔
103
      pipeline,
4,404✔
104
    } = collectionDefinition ?? {}
4,404!
105
    const isView = type === VIEW_TYPE
4,404✔
106
    const viewLookupsEnabled = isView && enableLookup
4,404✔
107

4,404✔
108
    fastify.log.trace({ collectionEndpoint, collectionName }, 'Registering CRUD')
4,404✔
109

4,404✔
110
    const collectionIdType = getIdType(collectionDefinition)
4,404✔
111
    const collection = await fastify
4,404✔
112
      .mongo[getDatabaseNameByType(collectionIdType)]
4,404✔
113
      .db
4,404✔
114
      .collection(collectionName)
4,404✔
115
    const modelDependencies = buildModelDependencies(fastify, collectionDefinition, collection)
4,404✔
116

4,404✔
117
    let viewDependencies = {}
4,404✔
118
    if (viewLookupsEnabled) {
4,404✔
119
      const sourceCollection = mergedCollections.find(mod => mod.name === viewSourceCollectionName)
12✔
120
      const sourceCollectionDependencies = buildModelDependencies(fastify, sourceCollection)
12✔
121

12✔
122
      const viewIdType = getIdType(sourceCollection)
12✔
123
      const sourceCollectionMongo = await fastify
12✔
124
        .mongo[getDatabaseNameByType(viewIdType)]
12✔
125
        .db
12✔
126
        .collection(viewSourceCollectionName)
12✔
127
      viewDependencies = buildModelDependencies(fastify, collectionDefinition, sourceCollectionMongo)
12✔
128
      viewDependencies.queryParser = sourceCollectionDependencies.queryParser
12✔
129
      viewDependencies.allFieldNames = sourceCollectionDependencies.allFieldNames
12✔
130
      viewDependencies.lookupsModels = createLookupModel(fastify, collectionDefinition, mergedCollections)
12✔
131
    }
12✔
132

4,404✔
133
    models[getCollectionNameFromEndpoint(collectionEndpoint)] = {
4,404✔
134
      definition: collectionDefinition,
4,404✔
135
      ...modelDependencies,
4,404✔
136
      viewDependencies,
4,404✔
137
      isView,
4,404✔
138
      viewLookupsEnabled,
4,404✔
139
    }
4,404✔
140

4,404✔
141
    if (isView) {
4,404✔
142
      const existingCollections = collectionIdType === OBJECTID ? existingObjectIdCollection : existingStringCollection
627✔
143
      if (existingCollections.length === 0) {
627✔
144
        existingCollections.push(
626✔
145
          ...(
626✔
146
            await fastify
626✔
147
              .mongo[getDatabaseNameByType(collectionIdType)]
626✔
148
              .db
626✔
149
              .listCollections({}, { nameOnly: true })
626✔
150
              .toArray()
626✔
151
          )
626✔
152
            .filter(({ type: collectionType }) => collectionType === VIEW_TYPE)
626✔
153
            .map(({ name }) => name)
626✔
154
        )
626✔
155
      }
626✔
156

627✔
157
      if (existingCollections.includes(collectionName)) {
627!
158
        await fastify
×
159
          .mongo[getDatabaseNameByType(collectionIdType)]
×
160
          .db
×
161
          .collection(collectionName)
×
162
          .drop()
×
163
      }
×
164

627✔
165
      return fastify
627✔
166
        .mongo[getDatabaseNameByType(collectionIdType)]
627✔
167
        .db
627✔
168
        .createCollection(
627✔
169
          collectionName,
627✔
170
          {
627✔
171
            viewOn: source,
627✔
172
            pipeline,
627✔
173
          }
627✔
174
        )
627✔
175
    }
627✔
176

3,777✔
177
    return createIndexes(collection, indexes, PREFIX_OF_INDEX_NAMES_TO_PRESERVE)
3,777✔
178
  }
4,404✔
179
}
4,404✔
180

94✔
181
function getCollectionNameFromEndpoint(endpointBasePath) {
4,404✔
182
  return endpointBasePath.replace('/', '').replace(/\//g, '-')
4,404✔
183
}
4,404✔
184

94✔
185
function createLookupModel(fastify, viewDefinition, mergedCollections) {
12✔
186
  const lookupModels = []
12✔
187
  const viewLookups = viewDefinition.pipeline
12✔
188
    .filter(pipeline => '$lookup' in pipeline)
12✔
189
    .map(lookup => Object.values(lookup).shift())
12✔
190

12✔
191
  for (const lookup of viewLookups) {
12✔
192
    const { from, pipeline } = lookup
12✔
193
    const lookupCollection = mergedCollections.find(({ name }) => name === from)
12✔
194
    const lookupIdType = getIdType(lookupCollection)
12✔
195
    const lookupCollectionMongo = fastify.mongo[getDatabaseNameByType(lookupIdType)].db.collection(from)
12✔
196

12✔
197
    const lookupProjection = pipeline?.find(({ $project }) => $project)?.$project ?? {}
12!
198
    const parsedLookupProjection = []
12✔
199
    const lookupCollectionDefinition = {
12✔
200
      ...lomit(viewDefinition, ['fields']),
12✔
201
      schema: {
12✔
202
        type: 'object',
12✔
203
        properties: {},
12✔
204
        required: [],
12✔
205
      },
12✔
206
    }
12✔
207

12✔
208
    Object.entries(lookupProjection)
12✔
209
      .forEach(([fieldName, schema]) => {
12✔
210
        parsedLookupProjection.push({ [fieldName]: schema })
36✔
211
        const conversion = Object.keys(schema).shift()
36✔
212
        if (schema !== 0) {
36✔
213
          if (!aggregationConversion[conversion]) {
24!
214
            throw new Error(`Invalid view lookup definition: no explicit type found in ${JSON.stringify({ [fieldName]: schema })}`)
×
215
          }
×
216
          lookupCollectionDefinition.schema.properties[fieldName] = {
24✔
217
            type: aggregationConversion[conversion],
24✔
218
          }
24✔
219
        }
24✔
220
      })
12✔
221

12✔
222
    const lookupModel = {
12✔
223
      ...buildModelDependencies(fastify, lookupCollectionDefinition, lookupCollectionMongo),
12✔
224
      definition: lookupCollectionDefinition,
12✔
225
      lookup,
12✔
226
      parsedLookupProjection,
12✔
227
    }
12✔
228
    lookupModels.push(lookupModel)
12✔
229
  }
12✔
230
  return lookupModels
12✔
231
}
12✔
232

94✔
233
function buildModelDependencies(fastify, collectionDefinition, collection) {
4,440✔
234
  const {
4,440✔
235
    defaultState,
4,440✔
236
    defaultSorting,
4,440✔
237
  } = collectionDefinition
4,440✔
238

4,440✔
239
  const allFieldNames = collectionDefinition.fields
4,440✔
240
    ? collectionDefinition.fields.map(field => field.name)
4,440✔
241
    : Object.keys(collectionDefinition.schema.properties)
4,440✔
242
  const pathsForRawSchema = generatePathFieldsForRawSchema(fastify.log, collectionDefinition)
4,440✔
243

4,440✔
244
  // TODO: make this configurable
4,440✔
245
  const crudService = new CrudService(
4,440✔
246
    collection,
4,440✔
247
    defaultState,
4,440✔
248
    defaultSorting,
4,440✔
249
    { allowDiskUse: fastify.config.ALLOW_DISK_USE_IN_QUERIES },
4,440✔
250
  )
4,440✔
251
  const queryParser = new QueryParser(collectionDefinition, pathsForRawSchema)
4,440✔
252
  const additionalCaster = new AdditionalCaster(collectionDefinition)
4,440✔
253
  const jsonSchemaGenerator = new JSONSchemaGenerator(
4,440✔
254
    collectionDefinition,
4,440✔
255
    {},
4,440✔
256
    fastify.config.CRUD_LIMIT_CONSTRAINT_ENABLED,
4,440✔
257
    fastify.config.CRUD_MAX_LIMIT,
4,440✔
258
    fastify.config.ENABLE_STRICT_OUTPUT_VALIDATION,
4,440✔
259
    fastify.config.OPEN_API_SPECIFICATION,
4,440✔
260
  )
4,440✔
261
  const jsonSchemaGeneratorWithNested = new JSONSchemaGenerator(
4,440✔
262
    collectionDefinition,
4,440✔
263
    pathsForRawSchema,
4,440✔
264
    fastify.config.CRUD_LIMIT_CONSTRAINT_ENABLED,
4,440✔
265
    fastify.config.CRUD_MAX_LIMIT,
4,440✔
266
    fastify.config.ENABLE_STRICT_OUTPUT_VALIDATION,
4,440✔
267
    fastify.config.OPEN_API_SPECIFICATION,
4,440✔
268
  )
4,440✔
269

4,440✔
270
  return {
4,440✔
271
    crudService,
4,440✔
272
    queryParser,
4,440✔
273
    castResultsAsStream: () => additionalCaster.castResultsAsStream(),
4,440✔
274
    castItem: (item) => additionalCaster.castItem(item),
4,440✔
275
    allFieldNames,
4,440✔
276
    jsonSchemaGenerator,
4,440✔
277
    jsonSchemaGeneratorWithNested,
4,440✔
278
  }
4,440✔
279
}
4,440✔
280

94✔
281
module.exports = loadModels
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