• 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

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

91✔
17
'use strict'
91✔
18

91✔
19
const { randomBytes } = require('crypto')
91✔
20
const { DATE_FORMATS, OBJECTID } = require('./consts')
91✔
21

91✔
22
const variableName = `a${randomBytes(5).toString('hex')}`
91✔
23

91✔
24
class JoinService {
91✔
25
  constructor(database, collections) {
91✔
26
    this.database = database
211✔
27
    this.collections = getCollections(collections)
211✔
28
  }
211✔
29

91✔
30
  joinOneToOne(context, joinOptions, toMerge) {
91✔
31
    const {
57✔
32
      from,
57✔
33
      to,
57✔
34
      fromQueryFilter,
57✔
35
      toQueryFilter,
57✔
36
      asField,
57✔
37
      localField,
57✔
38
      foreignField,
57✔
39
      fromProjectBefore,
57✔
40
      fromProjectAfter,
57✔
41
      toProjectBefore,
57✔
42
      toProjectAfter,
57✔
43
      fromACLMatching,
57✔
44
      toACLMatching,
57✔
45
    } = joinOptions
57✔
46
    const collection = this.database.collection(this.collections[from].name)
57✔
47
    const expectedLocalFieldType = this.collections[from].fields[localField]
57✔
48
    const innerPipeline = [
57✔
49
      {
57✔
50
        $match: {
57✔
51
          ...toQueryFilter,
57✔
52
          $expr: {
57✔
53
            $cond: {
57✔
54
              if: {
57✔
55
                $in: [
57✔
56
                  { $type: [`$$${variableName}`] },
57✔
57
                  expectedLocalFieldType,
57✔
58
                ],
57✔
59
              },
57✔
60
              then: { $eq: [`$${foreignField}`, `$$${variableName}`] },
57✔
61
              else: { $eq: [true, false] },
57✔
62
            },
57✔
63
          },
57✔
64
        },
57✔
65
      },
57✔
66
      { $limit: 1 },
57✔
67
    ]
57✔
68
    const aggregateSteps = [
57✔
69
      {
57✔
70
        $lookup: {
57✔
71
          from: this.collections[to].name,
57✔
72
          as: asField,
57✔
73
          let: { [variableName]: `$${localField}` },
57✔
74
          pipeline: innerPipeline,
57✔
75
        },
57✔
76
      },
57✔
77
    ]
57✔
78
    if (Object.keys(fromQueryFilter).length) {
57✔
79
      aggregateSteps.unshift({ $match: fromQueryFilter })
33✔
80
    }
33✔
81
    if (fromProjectBefore) {
57✔
82
      aggregateSteps.unshift({ $project: fromProjectBefore })
6✔
83
    }
6✔
84
    if (fromACLMatching) {
57!
85
      aggregateSteps.unshift({ $match: { fromACLMatching } })
×
86
    }
×
87
    if (fromProjectAfter) {
57✔
88
      aggregateSteps.push({ $project: fromProjectAfter })
6✔
89
    }
6✔
90

57✔
91
    if (toProjectBefore) {
57✔
92
      innerPipeline.unshift({ $project: toProjectBefore })
6✔
93
    }
6✔
94
    if (toACLMatching) {
57!
95
      innerPipeline.unshift({ $match: toACLMatching })
×
96
    }
×
97
    if (toProjectAfter) {
57✔
98
      innerPipeline.push({ $project: toProjectAfter })
6✔
99
    }
6✔
100

57✔
101
    if (toMerge) {
57✔
102
      aggregateSteps.push({
30✔
103
        $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: [`$${asField}`, 0] }, '$$ROOT'] } },
30✔
104
      })
30✔
105
      aggregateSteps.push({
30✔
106
        $project: { [asField]: 0 },
30✔
107
      })
30✔
108
    } else {
57✔
109
      aggregateSteps.push({
27✔
110
        $replaceRoot: {
27✔
111
          newRoot: {
27✔
112
            $mergeObjects: [
27✔
113
              '$$ROOT',
27✔
114
              {
27✔
115
                $cond: {
27✔
116
                  if: {
27✔
117
                    $gt: [
27✔
118
                      { $size: [`$${asField}`] },
27✔
119
                      0,
27✔
120
                    ],
27✔
121
                  },
27✔
122
                  then: {
27✔
123
                    stats: {
27✔
124
                      $arrayElemAt: [`$${asField}`, 0],
27✔
125
                    },
27✔
126
                  },
27✔
127
                  else: { [asField]: null },
27✔
128
                },
27✔
129
              },
27✔
130
            ],
27✔
131
          },
27✔
132
        },
27✔
133
      })
27✔
134
    }
27✔
135

57✔
136
    return collection.aggregate(aggregateSteps)
57✔
137
  }
57✔
138

91✔
139
  joinOneToMany(context, joinOptions) {
91✔
140
    const {
24✔
141
      from,
24✔
142
      to,
24✔
143
      fromQueryFilter,
24✔
144
      toQueryFilter,
24✔
145
      asField,
24✔
146
      localField,
24✔
147
      foreignField,
24✔
148
      fromProjectBefore,
24✔
149
      fromProjectAfter,
24✔
150
      toProjectBefore,
24✔
151
      toProjectAfter,
24✔
152
      fromACLMatching,
24✔
153
      toACLMatching,
24✔
154
    } = joinOptions
24✔
155
    const collection = this.database.collection(this.collections[from].name)
24✔
156
    const expectedLocalFieldType = this.collections[from].fields[localField]
24✔
157
    const innerPipeline = [
24✔
158
      {
24✔
159
        $match: {
24✔
160
          ...toQueryFilter,
24✔
161
          $expr: {
24✔
162
            $cond: {
24✔
163
              if: {
24✔
164
                $in: [
24✔
165
                  { $type: [`$$${variableName}`] },
24✔
166
                  expectedLocalFieldType,
24✔
167
                ],
24✔
168
              },
24✔
169
              then: { $eq: [`$${foreignField}`, `$$${variableName}`] },
24✔
170
              else: { $eq: [true, false] },
24✔
171
            },
24✔
172
          },
24✔
173
        },
24✔
174
      },
24✔
175
    ]
24✔
176
    const aggregateSteps = [
24✔
177
      {
24✔
178
        $lookup: {
24✔
179
          from: this.collections[to].name,
24✔
180
          as: asField,
24✔
181
          let: { [variableName]: `$${localField}` },
24✔
182
          pipeline: innerPipeline,
24✔
183
        },
24✔
184
      },
24✔
185
    ]
24✔
186
    if (Object.keys(fromQueryFilter).length) {
24✔
187
      aggregateSteps.unshift({ $match: fromQueryFilter })
15✔
188
    }
15✔
189
    if (fromProjectBefore) {
24✔
190
      aggregateSteps.unshift({ $project: fromProjectBefore })
3✔
191
    }
3✔
192
    if (fromACLMatching) {
24!
193
      aggregateSteps.unshift({ $match: { fromACLMatching } })
×
194
    }
×
195
    if (fromProjectAfter) {
24✔
196
      aggregateSteps.push({ $project: fromProjectAfter })
3✔
197
    }
3✔
198

24✔
199
    if (toProjectBefore) {
24✔
200
      innerPipeline.unshift({ $project: toProjectBefore })
3✔
201
    }
3✔
202
    if (toACLMatching) {
24!
203
      innerPipeline.unshift({ $match: toACLMatching })
×
204
    }
×
205
    if (toProjectAfter) {
24✔
206
      innerPipeline.push({ $project: toProjectAfter })
3✔
207
    }
3✔
208

24✔
209
    return collection.aggregate(aggregateSteps)
24✔
210
  }
24✔
211

91✔
212
  joinManyToMany(context, joinOptions) {
91✔
213
    const {
21✔
214
      from,
21✔
215
      to,
21✔
216
      fromQueryFilter,
21✔
217
      toQueryFilter,
21✔
218
      asField,
21✔
219
      localField,
21✔
220
      foreignField,
21✔
221
      fromProjectBefore,
21✔
222
      fromProjectAfter,
21✔
223
      toProjectBefore,
21✔
224
      toProjectAfter,
21✔
225
      fromACLMatching,
21✔
226
      toACLMatching,
21✔
227
    } = joinOptions
21✔
228
    const collection = this.database.collection(this.collections[from].name)
21✔
229
    const expectedLocalFieldType = this.collections[from].fields[localField]
21✔
230
    const innerPipeline = [
21✔
231
      {
21✔
232
        $match: {
21✔
233
          ...toQueryFilter,
21✔
234
          $expr: {
21✔
235
            $cond: {
21✔
236
              if: {
21✔
237
                $in: [
21✔
238
                  { $type: [`$$${variableName}`] },
21✔
239
                  expectedLocalFieldType,
21✔
240
                ],
21✔
241
              },
21✔
242
              then: { $in: [`$${foreignField}`, `$$${variableName}`] },
21✔
243
              else: { $eq: [true, false] },
21✔
244
            },
21✔
245
          },
21✔
246
        },
21✔
247
      },
21✔
248
    ]
21✔
249
    const aggregateSteps = [
21✔
250
      {
21✔
251
        $lookup: {
21✔
252
          from: this.collections[to].name,
21✔
253
          as: asField,
21✔
254
          let: { [variableName]: `$${localField}` },
21✔
255
          pipeline: innerPipeline,
21✔
256
        },
21✔
257
      },
21✔
258
    ]
21✔
259
    if (fromQueryFilter) {
21✔
260
      aggregateSteps.unshift({ $match: fromQueryFilter })
21✔
261
    }
21✔
262
    if (fromProjectBefore) {
21✔
263
      aggregateSteps.unshift({ $project: fromProjectBefore })
6✔
264
    }
6✔
265
    if (fromACLMatching) {
21!
266
      aggregateSteps.unshift({ $match: { fromACLMatching } })
×
267
    }
×
268
    if (fromProjectAfter) {
21✔
269
      aggregateSteps.push({ $project: fromProjectAfter })
6✔
270
    }
6✔
271

21✔
272
    if (toProjectBefore) {
21✔
273
      innerPipeline.unshift({ $project: toProjectBefore })
6✔
274
    }
6✔
275
    if (toACLMatching) {
21!
276
      innerPipeline.unshift({ $match: toACLMatching })
×
277
    }
×
278
    if (toProjectAfter) {
21✔
279
      innerPipeline.push({ $project: toProjectAfter })
6✔
280
    }
6✔
281

21✔
282
    return collection.aggregate(aggregateSteps)
21✔
283
  }
21✔
284
}
91✔
285

91✔
286
function getTypeCompatibility(type) {
39,495✔
287
  switch (type) {
39,495✔
288
  case 'string': {
39,495✔
289
    return ['string']
18,234✔
290
  }
18,234✔
291
  case 'number': {
39,495✔
292
    return ['double', 'int', 'long', 'decimal']
2,231✔
293
  }
2,231✔
294
  case 'ObjectId': {
39,495✔
295
    return ['objectId']
4,055✔
296
  }
4,055✔
297
  case 'boolean': {
39,495✔
298
    return ['bool']
808✔
299
  }
808✔
300
  case 'Date': {
39,495✔
301
    return ['date']
8,104✔
302
  }
8,104✔
303
  case 'GeoPoint': {
39,495✔
304
    return ['object']
808✔
305
  }
808✔
306
  case 'Array': {
39,495✔
307
    return ['array']
3,437✔
308
  }
3,437✔
309
  case 'RawObject': {
39,495✔
310
    return ['object']
1,818✔
311
  }
1,818✔
312
  default: {
39,495!
313
    throw new Error(`Unknown type: ${type}`)
×
314
  }
×
315
  }
39,495✔
316
}
39,495✔
317

91✔
318
function getType(jsonSchema) {
447✔
319
  switch (jsonSchema.type) {
447✔
320
  case 'string': {
447✔
321
    if (DATE_FORMATS.includes(jsonSchema.format)) { return ['Date'] }
273✔
322
    // eslint-disable-next-line no-underscore-dangle
198✔
323
    if (jsonSchema.__mia_configuration?.type === OBJECTID) { return ['objectId'] }
273✔
324
    return ['string']
162✔
325
  }
162✔
326
  case 'number': {
447✔
327
    return ['double', 'int', 'long', 'decimal']
27✔
328
  }
27✔
329
  case 'boolean': {
447✔
330
    return ['bool']
15✔
331
  }
15✔
332
  case 'array':
447✔
333
  case 'object': {
447✔
334
    return [jsonSchema.type]
132✔
335
  }
132✔
336
  default: {
447!
337
    throw new Error(`Unknown type: ${jsonSchema.type}`)
×
338
  }
×
339
  }
447✔
340
}
447✔
341

91✔
342
function getCollectionNameFromEndpoint(endpointBasePath) {
3,675✔
343
  return endpointBasePath.replace('/', '')
3,675✔
344
    .replace(/\//g, '-')
3,675✔
345
}
3,675✔
346

91✔
347
function getCollections(collections) {
211✔
348
  const ret = {}
211✔
349

211✔
350
  for (const collection of Object.values(collections)) {
211✔
351
    const { definition: { fields, schema, endpointBasePath, name: collectionName } } = collection
3,675✔
352
    const collectionHTTPName = getCollectionNameFromEndpoint(endpointBasePath)
3,675✔
353
    ret[collectionHTTPName] = {
3,675✔
354
      name: collectionName,
3,675✔
355
      fields: {},
3,675✔
356
    }
3,675✔
357

3,675✔
358
    // TODO: remove (compatibility)
3,675✔
359
    if (!schema) {
3,675✔
360
      for (const field of fields) {
3,648✔
361
        ret[collectionHTTPName].fields[field.name] = getTypeCompatibility(field.type)
39,495✔
362
      }
39,495✔
363
    } else {
3,675✔
364
      Object
27✔
365
        .entries(schema.properties)
27✔
366
        .forEach(([propertyName, jsonSchema]) => {
27✔
367
          ret[collectionHTTPName].fields[propertyName] = getType(jsonSchema)
447✔
368
        })
27✔
369
    }
27✔
370
  }
3,675✔
371

211✔
372
  return ret
211✔
373
}
211✔
374

91✔
375
module.exports = JoinService
91✔
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