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

GrottoCenter / grottocenter-api / 5123216243

pending completion
5123216243

push

github

vmarseguerra
feat(entities): list latest contributions

774 of 2004 branches covered (38.62%)

Branch coverage included in aggregate %.

123 of 123 new or added lines in 11 files covered. (100.0%)

2775 of 5089 relevant lines covered (54.53%)

14.54 hits per line

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

33.79
/api/services/DocumentService.js
1
const RECURSIVE_GET_CHILD_DOC = `
2✔
2
  WITH RECURSIVE recursiveChildren AS (SELECT *
3
                                       FROM t_document
4
                                       WHERE id_parent = $1
5
                                       UNION ALL
6
                                       SELECT td.*
7
                                       FROM t_document td
8
                                              INNER JOIN recursiveChildren
9
                                                         ON td.id_parent = recursiveChildren.id)
10
  SELECT *
11
  FROM recursiveChildren
12
  WHERE is_validated = true
13
    AND is_deleted = false;
14
`;
15

16
// Doc types needing a parent in order to be created
17
// (ex; an issue needs a collection, an article needs an issue)
18
const MANDATORY_PARENT_TYPES = ['article', 'issue'];
2✔
19
const oldTopoFilesUrl = 'https://www.grottocenter.org/upload/topos/';
2✔
20
const ramda = require('ramda');
2✔
21
const CommonService = require('./CommonService');
2✔
22
const DescriptionService = require('./DescriptionService');
2✔
23
const ElasticsearchService = require('./ElasticsearchService');
2✔
24
const FileService = require('./FileService');
2✔
25
const NameService = require('./NameService');
2✔
26
const NotificationService = require('./NotificationService');
2✔
27
const RecentChangeService = require('./RecentChangeService');
2✔
28
const {
29
  NOTIFICATION_TYPES,
30
  NOTIFICATION_ENTITIES,
31
} = require('./NotificationService');
2✔
32

33
const getAdditionalESIndexFromDocumentType = (document) => {
2✔
34
  if (document.type.name === 'Issue') {
×
35
    return 'document-issues';
×
36
  }
37
  if (document.type.name === 'Collection') {
×
38
    return 'document-collections';
×
39
  }
40
  return '';
×
41
};
42

43
module.exports = {
2✔
44
  // TO DO: proper update
45
  updateDocumentInElasticSearchIndexes: async (document) => {
46
    await module.exports.deleteDocumentFromElasticsearchIndexes(document);
×
47
    await module.exports.addDocumentToElasticSearchIndexes(document);
×
48
  },
49

50
  deleteDocumentFromElasticsearchIndexes: async (document) => {
51
    await ElasticsearchService.deleteResource('documents', document.id);
×
52
    const additionalIndex = getAdditionalESIndexFromDocumentType(document);
×
53

54
    // Delete in document-collections-index or document-issues-index
55
    if (additionalIndex !== '') {
×
56
      await ElasticsearchService.deleteResource(additionalIndex, document.id);
×
57
    }
58
  },
59

60
  /**
61
   * Based on the logstash.conf file.
62
   * The document must be fully populated and with all its names set
63
   *    (@see DocumentService.setNamesOfPopulatedDocument).
64
   */
65
  addDocumentToElasticSearchIndexes: async (document) => {
66
    const esBody = module.exports.getElasticsearchBody(document);
×
67
    await ElasticsearchService.create('documents', document.id, {
×
68
      ...esBody,
69
      tags: ['document'],
70
    });
71
    // Create in document-collections-index or document-issues-index
72
    const additionalIndex = getAdditionalESIndexFromDocumentType(document);
×
73

74
    if (additionalIndex !== '') {
×
75
      await ElasticsearchService.create(additionalIndex, document.id, {
×
76
        ...esBody,
77
        tags: [`document-${document.type.name.toLowerCase()}`],
78
      });
79
    }
80
  },
81

82
  getElasticsearchBody: (document) => {
83
    const { type, modifiedDocJson, ...docWithoutJsonAndType } = document;
×
84
    return {
×
85
      ...docWithoutJsonAndType,
86
      authors: document.authors
×
87
        ? document.authors.map((a) => a.nickname).join(', ')
×
88
        : null,
89
      'contributor id': document.author.id,
90
      'contributor nickname': document.author.nickname,
91
      date_part: document.datePublication
×
92
        ? new Date(document.datePublication).getFullYear()
93
        : null,
94
      description: document.descriptions[0].body,
95
      'editor id': ramda.pathOr(null, ['editor', 'id'], document),
96
      'editor name': ramda.pathOr(null, ['editor', 'name'], document),
97
      'library id': ramda.pathOr(null, ['library', 'id'], document),
98
      'library name': ramda.pathOr(null, ['library', 'name'], document),
99
      regions: document.regions
×
100
        ? document.regions.map((r) => r.name).join(', ')
×
101
        : null,
102
      subjects: document.subjects
×
103
        ? document.subjects.map((s) => s.subject).join(', ')
×
104
        : null,
105
      title: document.descriptions[0].title,
106
      'type id': ramda.propOr(null, 'id', type),
107
      'type name': ramda.propOr(null, 'name', type),
108
      deleted: document.isDeleted,
109
    };
110
  },
111

112
  getLangDescDataFromClient: (req) => {
113
    let langDescData = {
4✔
114
      author: req.token.id,
115
      description: req.body.description,
116
      title: req.body.title,
117
    };
118

119
    if (ramda.pathOr(false, ['documentMainLanguage', 'id'], req.body)) {
4✔
120
      langDescData = {
2✔
121
        ...langDescData,
122
        documentMainLanguage: {
123
          id: req.body.documentMainLanguage.id,
124
        },
125
      };
126
    }
127
    if (ramda.pathOr(false, ['titleAndDescriptionLanguage', 'id'], req.body)) {
4✔
128
      langDescData = {
2✔
129
        ...langDescData,
130
        titleAndDescriptionLanguage: {
131
          id: req.body.titleAndDescriptionLanguage.id,
132
        },
133
      };
134
    }
135

136
    return langDescData;
4✔
137
  },
138

139
  // Extract everything from the request body except id and dateInscription
140
  getConvertedDataFromClient: async (req) => {
141
    // Remove id if present to avoid null id (and an error)
142
    const { id, option, ...reqBodyWithoutId } = req.body;
4✔
143

144
    const optionFound = option
4!
145
      ? await TOption.findOne({ name: option })
146
      : undefined;
147

148
    // Massif will be deleted in the future (a document can be about many massifs and a massif can be the subject of many documents): use massifs
149
    const massif = ramda.pathOr(undefined, ['massif', 'id'], req.body);
4✔
150
    const massifs = [
4✔
151
      ...ramda.propOr([], 'massifs', req.body),
152
      ...(massif ? [massif] : []),
4!
153
    ];
154

155
    return {
4✔
156
      ...reqBodyWithoutId,
157
      authorizationDocument: ramda.pathOr(
158
        undefined,
159
        ['authorizationDocument', 'id'],
160
        req.body
161
      ),
162
      authors: req.body.authors ? req.body.authors.map((a) => a.id) : undefined,
2✔
163
      datePublication:
164
        req.body.publicationDate === '' ? null : req.body.publicationDate,
4✔
165
      editor: ramda.pathOr(undefined, ['editor', 'id'], req.body),
166
      identifierType: ramda.pathOr(
167
        undefined,
168
        ['identifierType', 'id'],
169
        req.body
170
      ),
171
      issue:
172
        req.body.issue && req.body.issue !== '' ? req.body.issue : undefined,
8!
173
      library: ramda.pathOr(undefined, ['library', 'id'], req.body),
174
      license: ramda.pathOr(1, ['license', 'id'], req.body),
175
      massif,
176
      massifs,
177
      option: optionFound ? optionFound.id : undefined,
4!
178
      parent: ramda.pathOr(undefined, ['partOf', 'id'], req.body),
179
      regions: req.body.regions ? req.body.regions.map((r) => r.id) : undefined,
✔
180
      subjects: req.body.subjects
4✔
181
        ? req.body.subjects.map((s) => s.code)
2✔
182
        : undefined,
183
      type: ramda.pathOr(undefined, ['documentType', 'id'], req.body),
184
    };
185
  },
186

187
  // Set name of cave, entrance, massif, editor and library if present
188
  setNamesOfPopulatedDocument: async (document) => {
189
    if (!ramda.isNil(document.entrance))
8!
190
      await NameService.setNames([document.entrance], 'entrance');
×
191
    if (!ramda.isNil(document.cave)) {
8!
192
      await NameService.setNames([document.cave], 'cave');
×
193
    }
194
    if (!ramda.isNil(document.massif)) {
8!
195
      await NameService.setNames([document.massif], 'massif');
×
196
    }
197
    if (!ramda.isNil(document.library)) {
8!
198
      await NameService.setNames([document.library], 'grotto');
×
199
    }
200
    if (!ramda.isNil(document.editor)) {
8!
201
      await NameService.setNames([document.editor], 'grotto');
×
202
    }
203
    await DescriptionService.setDocumentDescriptions(document);
8✔
204
    if (!ramda.isNil(document.authorizationDocument)) {
8!
205
      await DescriptionService.setDocumentDescriptions(
×
206
        document.authorizationDocument
207
      );
208
    }
209
    return document;
8✔
210
  },
211
  /**
212
   * Deep populate children and sub-children only (not recursive currently)
213
   * @param {*} doc
214
   * @returns {*} the doc with its children and sub-children populated
215
   */
216
  deepPopulateChildren: async (doc) => {
217
    const result = await CommonService.query(RECURSIVE_GET_CHILD_DOC, [doc.id]);
×
218
    const childIds = result.rows.map((d) => d.id);
×
219

220
    // Populate
221
    const childrenAndGrandChildren = await TDocument.find({
×
222
      id: { in: childIds },
223
    }).populate('descriptions');
224
    const children = childrenAndGrandChildren.filter(
×
225
      (c) => c.parent === doc.id
×
226
    );
227
    const grandChildren = childrenAndGrandChildren.filter(
×
228
      (c) => c.parent !== doc.id
×
229
    );
230

231
    const formattedChildren = [];
×
232
    // Format children
233
    for (const childDoc of children) {
×
234
      // Is a direct child ?
235
      if (childDoc.parent === doc.id) {
×
236
        formattedChildren.push(childDoc);
×
237
      }
238
    }
239
    // Format grand children
240
    for (const grandChildDoc of grandChildren) {
×
241
      const childIdx = formattedChildren.findIndex(
×
242
        (c) => c.id === grandChildDoc.parent
×
243
      );
244
      if (childIdx !== -1) {
×
245
        const alreadyPickedChild = formattedChildren[childIdx];
×
246
        if (alreadyPickedChild.children) {
×
247
          alreadyPickedChild.children.push(grandChildDoc);
×
248
        } else {
249
          alreadyPickedChild.children = [grandChildDoc];
×
250
        }
251
      }
252
    }
253
    doc.children = formattedChildren; // eslint-disable-line no-param-reassign
×
254
    return doc;
×
255
  },
256

257
  /**
258
   * Depending on the number of languages, return the document main language.
259
   * @param {TLanguage[]} languages
260
   * @returns main language of the document
261
   */
262
  getMainLanguage: (languages) => {
263
    if (languages) {
8!
264
      if (languages.length === 0) {
8!
265
        return undefined;
8✔
266
      }
267
      if (languages.length === 1) {
×
268
        return languages[0];
×
269
      }
270
      return languages.filter((l) => l.isMain);
×
271
    }
272
    return undefined;
×
273
  },
274

275
  getTopoFiles: async (docId) => {
276
    const files = await TFile.find({ document: docId });
×
277
    // Build path old
278
    return files.map((f) => ({
×
279
      ...f,
280
      pathOld: oldTopoFilesUrl + f.path,
281
    }));
282
  },
283

284
  updateDocument: async (req, newData, newDescriptionData, newFiles) => {
285
    const jsonData = {
2✔
286
      ...newData,
287
      ...newDescriptionData,
288
      id: req.param('id'),
289
      author: req.token.id,
290
      newFiles: ramda.isEmpty(newFiles) ? undefined : newFiles,
2!
291
    };
292
    const updatedDocument = await TDocument.updateOne(req.param('id')).set({
2✔
293
      isValidated: false,
294
      dateValidation: null,
295
      dateReviewed: new Date(),
296
      reviewer: req.token.id,
297
      modifiedDocJson: jsonData,
298
    });
299

300
    await NotificationService.notifySubscribers(
2✔
301
      req,
302
      updatedDocument,
303
      req.token.id,
304
      NOTIFICATION_TYPES.UPDATE,
305
      NOTIFICATION_ENTITIES.DOCUMENT
306
    );
307

308
    await DescriptionService.setDocumentDescriptions(updatedDocument, false);
2✔
309

310
    return updatedDocument;
2✔
311
  },
312

313
  createDocument: async (
314
    req,
315
    documentData,
316
    langDescData,
317
    shouldDownloadDistantFile = false
2✔
318
  ) => {
319
    const document = await sails.getDatastore().transaction(async (db) => {
2✔
320
      // Perform some checks
321
      const docType =
322
        documentData.type && (await TType.findOne(documentData.type));
2✔
323
      if (docType) {
2!
324
        const docTypeName = docType.name.toLowerCase();
2✔
325
        // Parent doc is mandatory for articles and issues
326
        if (
2!
327
          MANDATORY_PARENT_TYPES.includes(docTypeName) &&
2!
328
          !documentData.parent
329
        ) {
330
          throw Error(
×
331
            `Your document being an ${docType.name.toLowerCase()}, you must provide a document parent.`
332
          );
333
        }
334
      }
335

336
      const createdDocument = await TDocument.create(documentData)
2✔
337
        .fetch()
338
        .usingConnection(db);
339

340
      // Create associated data not handled by TDocument manually
341
      if (ramda.pathOr(null, ['documentMainLanguage', 'id'], langDescData)) {
2!
342
        await JDocumentLanguage.create({
2✔
343
          document: createdDocument.id,
344
          language: langDescData.documentMainLanguage.id,
345
          isMain: true,
346
        }).usingConnection(db);
347
      }
348

349
      await TDescription.create({
2✔
350
        author: langDescData.author,
351
        body: langDescData.description,
352
        dateInscription: ramda.propOr(
353
          new Date(),
354
          'dateInscription',
355
          langDescData
356
        ),
357
        dateReviewed: ramda.propOr(undefined, 'dateReviewed', langDescData),
358
        document: createdDocument.id,
359
        language: langDescData.titleAndDescriptionLanguage.id,
360
        title: langDescData.title,
361
      }).usingConnection(db);
362

363
      return createdDocument;
2✔
364
    });
365

366
    await RecentChangeService.setNameCreate(
2✔
367
      'document',
368
      document.id,
369
      req.token.id,
370
      langDescData.title
371
    );
372

373
    const populatedDocument = await module.exports.getDocument(
2✔
374
      document.id,
375
      false
376
    );
377

378
    if (
2!
379
      populatedDocument.identifier &&
3✔
380
      ramda.pathOr('', ['identifierType', 'id'], populatedDocument).trim() ===
381
        'url'
382
    ) {
383
      sails.log.info(`Downloading ${populatedDocument.identifier}...`);
×
384
      const acceptedFileFormats = await TFileFormat.find();
×
385

386
      if (shouldDownloadDistantFile) {
×
387
        // Download distant file & tolerate error
388
        const file = await sails.helpers.distantFileDownload
×
389
          .with({
390
            url: populatedDocument.identifier,
391
            acceptedFileFormats: acceptedFileFormats.map((f) =>
392
              f.extension.trim()
×
393
            ),
394
            refusedFileFormats: ['html'], // don't download html page, they are not a valid file for GC
395
          })
396
          .tolerate((error) =>
397
            sails.log.error(
×
398
              `Failed to download ${populatedDocument.identifier}: ${error.message}`
399
            )
400
          );
401
        if (file) {
×
402
          await FileService.create(file, document.id);
×
403
        }
404
      }
405
    }
406

407
    await NotificationService.notifySubscribers(
2✔
408
      req,
409
      populatedDocument,
410
      req.token.id,
411
      NOTIFICATION_TYPES.CREATE,
412
      NOTIFICATION_ENTITIES.DOCUMENT
413
    );
414

415
    return populatedDocument;
2✔
416
  },
417

418
  /**
419
   * Populate any document-like object.
420
   * Avoid using when possible. Mainly used for json column that cannot be populated
421
   * using waterline query language.
422
   * @param {*} document
423
   * @returns populated document
424
   */
425
  populateJSON: async (document) => {
426
    const {
427
      author,
428
      authors,
429
      cave,
430
      editor,
431
      entrance,
432
      identifierType,
433
      languages,
434
      library,
435
      license,
436
      massif,
437
      parent,
438
      regions,
439
      reviewer,
440
      subjects,
441
      type,
442
      option,
443
      authorizationDocument,
444
      ...cleanedData
445
    } = document;
×
446

447
    // Join the tables
448
    const doc = { ...cleanedData };
×
449
    doc.authorizationDocument = authorizationDocument
×
450
      ? await TDocument.findOne(authorizationDocument)
451
      : null;
452
    doc.cave = cave ? await TCave.findOne(cave) : null;
×
453
    doc.editor = editor ? await TGrotto.findOne(editor) : null;
×
454
    doc.entrance = entrance ? await TEntrance.findOne(entrance) : null;
×
455
    doc.identifierType = identifierType
×
456
      ? await TIdentifierType.findOne(identifierType)
457
      : null;
458
    doc.library = library ? await TGrotto.findOne(library) : null;
×
459
    doc.license = license ? await TLicense.findOne(license) : null;
×
460
    doc.massif = massif ? await TMassif.findOne(massif) : null;
×
461
    doc.option = option ? await TOption.findOne(option) : null;
×
462
    doc.parent = parent
×
463
      ? await TDocument.findOne(parent).populate('descriptions')
464
      : null;
465
    doc.reviewer = reviewer ? await TCaver.findOne(reviewer) : null;
×
466
    doc.type = type ? await TType.findOne(type) : null;
×
467

468
    doc.author = author ? await TCaver.findOne(author) : null;
×
469
    doc.authors = authors
×
470
      ? await Promise.all(
471
          authors.map(async (a) => {
472
            const res = await TCaver.findOne(a);
×
473
            return res;
×
474
          })
475
        )
476
      : [];
477
    doc.languages = languages
×
478
      ? await Promise.all(
479
          languages.map(async (lang) => {
480
            const res = await TLanguage.findOne(lang);
×
481
            return res;
×
482
          })
483
        )
484
      : [];
485
    doc.regions = regions
×
486
      ? await Promise.all(
487
          regions.map(async (region) => {
488
            const res = await TRegion.findOne(region);
×
489
            return res;
×
490
          })
491
        )
492
      : [];
493
    doc.subjects = subjects
×
494
      ? await Promise.all(
495
          subjects.map(async (subject) => {
496
            const res = await TSubject.findOne(subject);
×
497
            return res;
×
498
          })
499
        )
500
      : [];
501
    return doc;
×
502
  },
503

504
  getDocument: async (documentId, setParentDescriptions = false) => {
6✔
505
    // Simple function currently but will be extended depending on needs
506
    const result = await TDocument.findOne(documentId)
8✔
507
      .populate('cave')
508
      .populate('entrance')
509
      .populate('files')
510
      .populate('identifierType')
511
      .populate('license')
512
      .populate('type');
513
    await DescriptionService.setDocumentDescriptions(
8✔
514
      result,
515
      setParentDescriptions
516
    );
517
    return result;
8✔
518
  },
519

520
  getHDocumentById: async (documentId) =>
521
    HDocument.find({ t_id: documentId })
×
522
      .populate('author')
523
      .populate('reviewer')
524
      .populate('massif')
525
      .populate('cave')
526
      .populate('editor')
527
      .populate('entrance')
528
      .populate('identifierType')
529
      .populate('library')
530
      .populate('license')
531
      .populate('type'),
532

533
  populateHDocumentsWithDescription: async (documentId, hDocuments) => {
534
    const descriptions = await DescriptionService.getHDescriptionsOfDocument(
×
535
      documentId
536
    );
537
    hDocuments.forEach((document) => {
×
538
      if (Object.keys(descriptions).length > 0) {
×
539
        // eslint-disable-next-line no-param-reassign
540
        document.description = descriptions[0];
×
541
        descriptions.forEach((desc) => {
×
542
          if (
×
543
            // Return true if the description should be associate to this document according to her dateReviewed
544
            DescriptionService.compareDescriptionDate(
545
              // Id represents here the dateReviewed like in the H models
546
              new Date(document.id),
547
              new Date(desc.id),
548
              new Date(document.description.id)
549
            )
550
          ) {
551
            // eslint-disable-next-line no-param-reassign
552
            document.description = desc;
×
553
          }
554
        });
555
      }
556
    });
557
    return hDocuments;
×
558
  },
559

560
  getIdDocumentByEntranceId: async (entranceId) => {
561
    let documentsId = [];
×
562
    if (entranceId) {
×
563
      documentsId = await TDocument.find({
×
564
        where: { entrance: entranceId },
565
        select: ['id'],
566
      });
567
    }
568
    return documentsId;
×
569
  },
570
};
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