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

mia-platform / crud-service / 6650818505

26 Oct 2023 07:24AM UTC coverage: 96.95% (+2.6%) from 94.301%
6650818505

push

github

web-flow
fix: review of ci to update to NodeJS v20 (#211)

* ci: update setup-node action to v4

* ci: separate tests of previous MongoDB versions from v7

* ci: update setup-node action to v4

* ci: set a fixed version for coverall github action

* ci: fix coverallapps version

1645 of 1787 branches covered (0.0%)

Branch coverage included in aggregate %.

8494 of 8671 relevant lines covered (97.96%)

6911.51 hits per line

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

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

88✔
17
'use strict'
88✔
18

88✔
19
const JSONStream = require('JSONStream')
88✔
20
const through2 = require('through2')
88✔
21

88✔
22
const Ajv = require('ajv')
88✔
23
const ajvFormats = require('ajv-formats')
88✔
24
const ajvKeywords = require('ajv-keywords')
88✔
25

88✔
26
const JoinService = require('./JoinService')
88✔
27
const { BAD_REQUEST_ERROR_STATUS_CODE } = require('./consts')
88✔
28
const BadRequestError = require('./BadRequestError')
88✔
29

88✔
30
const JOIN_TAG = 'CRUD Join'
88✔
31

88✔
32
const oneToOneSchema = {
88✔
33
  tags: [JOIN_TAG],
88✔
34
  body: {
88✔
35
    type: 'object',
88✔
36
    required: ['asField', 'localField', 'foreignField'],
88✔
37
    properties: {
88✔
38
      fromQueryFilter: { type: 'object', default: {} },
88✔
39
      toQueryFilter: { type: 'object', default: {} },
88✔
40
      asField: { type: 'string' },
88✔
41
      localField: { type: 'string' },
88✔
42
      foreignField: { type: 'string' },
88✔
43
      fromProjectBefore: { type: 'array', items: { type: 'string' } },
88✔
44
      fromProjectAfter: { type: 'array', items: { type: 'string' } },
88✔
45
      toProjectBefore: { type: 'array', items: { type: 'string' } },
88✔
46
      toProjectAfter: { type: 'array', items: { type: 'string' } },
88✔
47
      toMerge: { type: 'boolean', default: false },
88✔
48
    },
88✔
49
    additionalProperties: false,
88✔
50
  },
88✔
51
  params: {
88✔
52
    type: 'object',
88✔
53
    required: ['from', 'to'],
88✔
54
    properties: {
88✔
55
      from: { type: 'string' },
88✔
56
      to: { type: 'string' },
88✔
57
    },
88✔
58
  },
88✔
59
}
88✔
60

88✔
61
const manyToManySchema = {
88✔
62
  tags: [JOIN_TAG],
88✔
63
  body: {
88✔
64
    type: 'object',
88✔
65
    required: ['asField', 'localField', 'foreignField'],
88✔
66
    properties: {
88✔
67
      fromQueryFilter: { type: 'object', default: {} },
88✔
68
      toQueryFilter: { type: 'object', default: {} },
88✔
69
      asField: { type: 'string' },
88✔
70
      localField: { type: 'string' },
88✔
71
      foreignField: { type: 'string' },
88✔
72
      fromProjectBefore: { type: 'array', items: { type: 'string' } },
88✔
73
      fromProjectAfter: { type: 'array', items: { type: 'string' } },
88✔
74
      toProjectBefore: { type: 'array', items: { type: 'string' } },
88✔
75
      toProjectAfter: { type: 'array', items: { type: 'string' } },
88✔
76
    },
88✔
77
    additionalProperties: false,
88✔
78
  },
88✔
79
  params: {
88✔
80
    type: 'object',
88✔
81
    required: ['from', 'to'],
88✔
82
    properties: {
88✔
83
      from: { type: 'string' },
88✔
84
      to: { type: 'string' },
88✔
85
    },
88✔
86
  },
88✔
87
}
88✔
88

88✔
89
const oneToManySchema = {
88✔
90
  tags: [JOIN_TAG],
88✔
91
  body: {
88✔
92
    type: 'object',
88✔
93
    required: ['asField', 'localField', 'foreignField'],
88✔
94
    properties: {
88✔
95
      fromQueryFilter: { type: 'object', default: {} },
88✔
96
      toQueryFilter: { type: 'object', default: {} },
88✔
97
      asField: { type: 'string' },
88✔
98
      localField: { type: 'string' },
88✔
99
      foreignField: { type: 'string' },
88✔
100
      fromProjectBefore: { type: 'array', items: { type: 'string' } },
88✔
101
      fromProjectAfter: { type: 'array', items: { type: 'string' } },
88✔
102
      toProjectBefore: { type: 'array', items: { type: 'string' } },
88✔
103
      toProjectAfter: { type: 'array', items: { type: 'string' } },
88✔
104
    },
88✔
105
    additionalProperties: false,
88✔
106
  },
88✔
107
  params: {
88✔
108
    type: 'object',
88✔
109
    required: ['from', 'to'],
88✔
110
    properties: {
88✔
111
      from: { type: 'string' },
88✔
112
      to: { type: 'string' },
88✔
113
    },
88✔
114
  },
88✔
115
}
88✔
116

88✔
117
const ajv = new Ajv({ coerceTypes: true, useDefaults: true })
88✔
118
ajvFormats(ajv)
88✔
119
ajvKeywords(ajv)
88✔
120

88✔
121
module.exports = async function joinPlugin(fastify) {
88✔
122
  fastify.decorate('joinService', new JoinService(fastify.mongo.db, fastify.models))
208✔
123
  fastify.setValidatorCompiler(({ schema }) => ajv.compile(schema))
208✔
124

208✔
125
  const ndjsonSerializer = fastNdjsonSerializer(JSON.stringify)
208✔
126

208✔
127
  fastify.post(
208✔
128
    '/one-to-one/:from/:to/export',
208✔
129
    { schema: oneToOneSchema },
208✔
130
    async function handler(request, reply) {
208✔
131
      const { body, crudContext, params } = request
21✔
132
      const { from, to } = params
21✔
133
      const {
21✔
134
        fromQueryFilter,
21✔
135
        toQueryFilter,
21✔
136
        asField,
21✔
137
        localField,
21✔
138
        foreignField,
21✔
139
        fromProjectBefore,
21✔
140
        fromProjectAfter,
21✔
141
        toProjectBefore,
21✔
142
        toProjectAfter,
21✔
143
        toMerge,
21✔
144
      } = body
21✔
145

21✔
146
      const { collectionModelFrom, collectionModelTo, error } = getCollectionModels(this.models, [from, to])
21✔
147
      if (error) {
21✔
148
        return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error)
3✔
149
      }
3✔
150

18✔
151
      const stream = this.joinService.joinOneToOne(crudContext, {
18✔
152
        from,
18✔
153
        to,
18✔
154
        fromQueryFilter: resolveMongoQuery(collectionModelFrom.queryParser, fromQueryFilter),
18✔
155
        toQueryFilter: resolveMongoQuery(collectionModelTo.queryParser, toQueryFilter),
18✔
156
        asField,
18✔
157
        localField,
18✔
158
        foreignField,
18✔
159
        fromProjectBefore,
18✔
160
        fromProjectAfter,
18✔
161
        toProjectBefore,
18✔
162
        toProjectAfter,
18✔
163
      // TODO: How to map this?
18✔
164
      // fromACLMatching,
18✔
165
      // toACLMatching
18✔
166
      }, toMerge).stream()
18✔
167

18✔
168
      reply.type('application/x-ndjson')
18✔
169
      return ndjsonSerializer(stream)
18✔
170
    })
208✔
171

208✔
172
  fastify.post(
208✔
173
    '/one-to-one/:from/:to/',
208✔
174
    { schema: oneToOneSchema },
208✔
175
    async function handler(request, reply) {
208✔
176
      const { body, crudContext, params } = request
6✔
177
      const { from, to } = params
6✔
178
      const {
6✔
179
        fromQueryFilter,
6✔
180
        toQueryFilter,
6✔
181
        asField,
6✔
182
        localField,
6✔
183
        foreignField,
6✔
184
        fromProjectBefore,
6✔
185
        fromProjectAfter,
6✔
186
        toProjectBefore,
6✔
187
        toProjectAfter,
6✔
188
        toMerge,
6✔
189
      } = body
6✔
190

6✔
191
      const { collectionModelFrom, collectionModelTo, error } = getCollectionModels(this.models, [from, to])
6✔
192
      if (error) {
6✔
193
        return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error)
3✔
194
      }
3✔
195

3✔
196
      const stream = this.joinService.joinOneToOne(crudContext, {
3✔
197
        from,
3✔
198
        to,
3✔
199
        fromQueryFilter: resolveMongoQuery(collectionModelFrom.queryParser, fromQueryFilter),
3✔
200
        toQueryFilter: resolveMongoQuery(collectionModelTo.queryParser, toQueryFilter),
3✔
201
        asField,
3✔
202
        localField,
3✔
203
        foreignField,
3✔
204
        fromProjectBefore,
3✔
205
        fromProjectAfter,
3✔
206
        toProjectBefore,
3✔
207
        toProjectAfter,
3✔
208
      // TODO: How to map this?
3✔
209
      // fromACLMatching,
3✔
210
      // toACLMatching
3✔
211
      }, toMerge).stream()
3✔
212

3✔
213
      reply.type('application/json')
3✔
214
      return streamSerializer(stream)
3✔
215
    })
208✔
216

208✔
217
  fastify.post(
208✔
218
    '/one-to-many/:from/:to/export',
208✔
219
    { schema: oneToManySchema },
208✔
220
    async function handler(request, reply) {
208✔
221
      const { body, crudContext, params } = request
12✔
222
      const { from, to } = params
12✔
223
      const {
12✔
224
        fromQueryFilter,
12✔
225
        toQueryFilter,
12✔
226
        asField,
12✔
227
        localField,
12✔
228
        foreignField,
12✔
229
        fromProjectBefore,
12✔
230
        fromProjectAfter,
12✔
231
        toProjectBefore,
12✔
232
        toProjectAfter,
12✔
233
      } = body
12✔
234

12✔
235
      const { collectionModelFrom, collectionModelTo, error } = getCollectionModels(this.models, [from, to])
12✔
236
      if (error) {
12✔
237
        return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error)
3✔
238
      }
3✔
239

9✔
240
      const stream = this.joinService.joinOneToMany(crudContext, {
9✔
241
        from,
9✔
242
        to,
9✔
243
        fromQueryFilter: resolveMongoQuery(collectionModelFrom.queryParser, fromQueryFilter),
9✔
244
        toQueryFilter: resolveMongoQuery(collectionModelTo.queryParser, toQueryFilter),
9✔
245
        asField,
9✔
246
        localField,
9✔
247
        foreignField,
9✔
248
        fromProjectBefore,
9✔
249
        fromProjectAfter,
9✔
250
        toProjectBefore,
9✔
251
        toProjectAfter,
9✔
252
      // TODO: How to map this?
9✔
253
      // fromACLMatching,
9✔
254
      // toACLMatching
9✔
255
      }).stream()
9✔
256

9✔
257
      reply.type('application/x-ndjson')
9✔
258
      return ndjsonSerializer(stream)
9✔
259
    })
208✔
260

208✔
261
  fastify.post(
208✔
262
    '/many-to-many/:from/:to/export',
208✔
263
    { schema: manyToManySchema },
208✔
264
    async function handler(request, reply) {
208✔
265
      const { body, crudContext, params } = request
6✔
266
      const { from, to } = params
6✔
267
      const {
6✔
268
        fromQueryFilter,
6✔
269
        toQueryFilter,
6✔
270
        asField,
6✔
271
        localField,
6✔
272
        foreignField,
6✔
273
        fromProjectBefore,
6✔
274
        fromProjectAfter,
6✔
275
        toProjectBefore,
6✔
276
        toProjectAfter,
6✔
277
      } = body
6✔
278

6✔
279
      const { collectionModelFrom, collectionModelTo, error } = getCollectionModels(this.models, [from, to])
6✔
280
      if (error) {
6✔
281
        return reply.getHttpError(BAD_REQUEST_ERROR_STATUS_CODE, error)
3✔
282
      }
3✔
283

3✔
284
      const stream = this.joinService.joinManyToMany(crudContext, {
3✔
285
        from,
3✔
286
        to,
3✔
287
        fromQueryFilter: resolveMongoQuery(collectionModelFrom.queryParser, fromQueryFilter),
3✔
288
        toQueryFilter: resolveMongoQuery(collectionModelTo.queryParser, toQueryFilter),
3✔
289
        asField,
3✔
290
        localField,
3✔
291
        foreignField,
3✔
292
        fromProjectBefore,
3✔
293
        fromProjectAfter,
3✔
294
        toProjectBefore,
3✔
295
        toProjectAfter,
3✔
296
      // TODO: How to map this?
3✔
297
      // fromACLMatching,
3✔
298
      // toACLMatching
3✔
299
      }).stream()
3✔
300

3✔
301
      reply.type('application/x-ndjson')
3✔
302
      return ndjsonSerializer(stream)
3✔
303
    })
208✔
304
}
208✔
305

88✔
306
function fastNdjsonSerializer(stringify) {
208✔
307
  function ndjsonTransform(obj, encoding, callback) {
208✔
308
    this.push(`${stringify(obj)}\n`)
78✔
309
    callback()
78✔
310
  }
78✔
311
  return function ndjsonSerializer(stream) {
208✔
312
    return stream.pipe(through2.obj(ndjsonTransform))
30✔
313
  }
30✔
314
}
208✔
315

88✔
316
function getCollectionModels(models, endpoints) {
45✔
317
  const [collectionModelFrom, collectionModelTo] = endpoints.map(endpoint => getCollectionModel(models, endpoint))
45✔
318
  if (!collectionModelFrom) {
45✔
319
    return { error: `CRUD endpoint "${endpoints[0]}" does not exist` }
9✔
320
  }
9✔
321
  if (!collectionModelTo) {
45✔
322
    return { error: `CRUD endpoint "${endpoints[1]}" does not exist` }
3✔
323
  }
3✔
324
  return { collectionModelFrom, collectionModelTo }
33✔
325
}
45✔
326

88✔
327
function getCollectionModel(models, endpoint) {
90✔
328
  const collectionName = getCollectionNameFromEndpoint(endpoint)
90✔
329
  return models[collectionName]
90✔
330
}
90✔
331

88✔
332
function getCollectionNameFromEndpoint(endpointBasePath) {
90✔
333
  return endpointBasePath.replace('/', '')
90✔
334
    .replace(/\//g, '-')
90✔
335
}
90✔
336

88✔
337
function resolveMongoQuery(queryParser, clientQuery) {
66✔
338
  const mongoQuery = {
66✔
339
    $and: [],
66✔
340
  }
66✔
341
  if (clientQuery) {
66✔
342
    mongoQuery.$and.push(clientQuery)
66✔
343
  }
66✔
344

66✔
345
  try {
66✔
346
    queryParser.parseAndCast(mongoQuery)
66✔
347
  } catch (error) {
66!
348
    throw new BadRequestError(error.message)
×
349
  }
×
350

66✔
351
  if (mongoQuery.$and && !mongoQuery.$and.length) {
66!
352
    return {}
×
353
  }
×
354

66✔
355
  return mongoQuery
66✔
356
}
66✔
357

88✔
358
function streamSerializer(payload) {
3✔
359
  return payload.pipe(JSONStream.stringify())
3✔
360
}
3✔
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

© 2026 Coveralls, Inc