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

GrottoCenter / grottocenter-api / 10996210778

23 Sep 2024 02:17PM UTC coverage: 46.158% (-2.8%) from 48.952%
10996210778

Pull #1306

github

vmarseguerra
feat(entities): adds delete / restore for document
Pull Request #1306: Adds delete / restore for entrance sub entities

740 of 2203 branches covered (33.59%)

Branch coverage included in aggregate %.

526 of 1293 new or added lines in 114 files covered. (40.68%)

23 existing lines in 18 files now uncovered.

2462 of 4734 relevant lines covered (52.01%)

4.5 hits per line

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

35.46
/api/services/DocumentService.js
1
const DescriptionService = require('./DescriptionService');
5✔
2
const ElasticsearchService = require('./ElasticsearchService');
5✔
3
const FileService = require('./FileService');
5✔
4
const NameService = require('./NameService');
5✔
5
const NotificationService = require('./NotificationService');
5✔
6
const RecentChangeService = require('./RecentChangeService');
5✔
7
const {
8
  valIfTruthyOrNull,
9
  distantFileDownload,
10
} = require('../utils/csvHelper');
5✔
11

12
const getAdditionalESIndexFromDocumentType = (document) => {
5✔
NEW
13
  if (document.type === 'Issue') {
×
14
    return 'document-issues';
×
15
  }
NEW
16
  if (document.type === 'Collection') {
×
17
    return 'document-collections';
×
18
  }
19
  return '';
×
20
};
21

22
// Used to create or update a document in elasticsearch
23
// Should match the the same format than the logstash document sql query
24
const getElasticsearchBody = (doc) => ({
5✔
25
  id: doc.id,
26
  identifier: doc.identifier ?? null,
×
27
  id_identifier_type: doc.identifierType?.id ?? null,
×
28
  deleted: doc.isDeleted,
29
  id_db_import: doc.idDbImport ?? null,
×
30
  name_db_import: doc.nameDbImport ?? null,
×
31
  date_publication: doc.datePublication ?? null,
×
32
  'contributor id': doc.author.id,
33
  'contributor nickname': doc.author.nickname,
NEW
34
  subjects: doc.subjects?.map((e) => e.id)?.join(', ') ?? null,
×
NEW
35
  authors: doc.authors?.map((a) => a.nickname).join(', ') ?? null,
×
36
  'type id': doc.type?.id,
37
  'type name': doc.type?.name,
38
  title: doc.descriptions?.[0].title,
39
  description: doc.descriptions?.[0].body,
40
  issue: doc.issue ?? null,
×
NEW
41
  countries: doc?.countries?.map((e) => e.id)?.join(', ') ?? [],
×
NEW
42
  iso_regions: doc?.isoRegions?.map((e) => e.id)?.join(', ') ?? [],
×
43
  'editor id': doc.editor?.id ?? null,
×
44
  'editor name': doc.editor?.name ?? null,
×
45
  'library id': doc.library?.id ?? null,
×
46
  'library name': doc.library?.name ?? null,
×
47
});
48

49
module.exports = {
5✔
50
  updateESDocument: async (document) => {
51
    // TODO: proper update
NEW
52
    await module.exports.deleteESDocument(document);
×
NEW
53
    await module.exports.createESDocument(document);
×
54
  },
55

56
  deleteESDocument: async (document) => {
57
    await ElasticsearchService.deleteResource('documents', document.id);
×
58
    const additionalIndex = getAdditionalESIndexFromDocumentType(document);
×
59

60
    // Delete in document-collections-index or document-issues-index
61
    if (additionalIndex !== '') {
×
62
      await ElasticsearchService.deleteResource(additionalIndex, document.id);
×
63
    }
64
  },
65

66
  /**
67
   * Based on the logstash.conf file.
68
   * The document must be fully populated and with all its names set
69
   *    (@see DocumentService.populateFullDocumentSubEntities).
70
   */
71
  createESDocument: async (document) => {
NEW
72
    const esBody = getElasticsearchBody(document);
×
UNCOV
73
    await ElasticsearchService.create('documents', document.id, {
×
74
      ...esBody,
75
      tags: ['document'],
76
    });
77
    // Create in document-collections-index or document-issues-index
78
    const additionalIndex = getAdditionalESIndexFromDocumentType(document);
×
79

80
    if (additionalIndex !== '') {
×
81
      await ElasticsearchService.create(additionalIndex, document.id, {
×
82
        ...esBody,
83
        tags: [`document-${document.type.name.toLowerCase()}`],
84
      });
85
    }
86
  },
87

88
  getDescriptionDataFromClient: (body, authorId) => ({
4✔
89
    author: authorId,
90
    body: body.description,
91
    title: body.title,
92
    language: body.mainLanguage,
93
  }),
94

95
  getChangedFileFromClient: (fileObjectArray) =>
96
    fileObjectArray.map((e) => ({
4✔
97
      id: e.id,
98
      fileName: e.fileName,
99
    })),
100

101
  // Extract everything from the request body except id and dateInscription
102
  // Used when creating or editing an existing document
103
  getConvertedDataFromClient: async (body) => {
104
    // 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
105
    const massif = body.massif?.id;
4✔
106
    const massifs = [...(body.massifs ?? []), ...(massif ? [massif] : [])];
4!
107

108
    let optionFound;
109
    // eslint-disable-next-line no-param-reassign
110
    if (body.option) optionFound = await TOption.findOne({ name: body.option });
4!
111
    let typeFound;
112
    if (body.type) typeFound = await TType.findOne({ name: body.type });
4✔
113

114
    return {
4✔
115
      identifier: body.identifier,
116
      identifierType: body.identifierType?.id,
117

118
      // dateInscription is added only at document creation
119
      // dateReviewed will be updated automaticly by the SQL historisation trigger
120
      datePublication: valIfTruthyOrNull(body.datePublication),
121
      // author are added only at document creation (done after if needed)
122
      authors: body.authors?.map((a) => a.id),
2✔
123
      authorsGrotto: body.authorsGrotto?.map((a) => a.id),
×
124
      editor: body.editor?.id,
125
      library: body.library?.id,
126
      authorComment: body.creatorComment,
127

128
      type: typeFound?.id,
129
      // descriptions is changed independently
130
      subjects: body.subjects?.map((s) => s.id ?? s.code),
2✔
131
      issue: valIfTruthyOrNull(body.issue),
132
      pages: valIfTruthyOrNull(body.pages),
133
      license: body.license?.id ?? 1,
8✔
134
      option: optionFound?.id,
135
      languages: body.mainLanguage ? [body.mainLanguage] : [],
4!
136
      // massif, // Deprecated, use massifs instead
137
      massifs,
138
      // cave is linked with the cave/add-document controller
139
      // entrance is linked with the entrance/add-document controller
140
      // files changes are handled independently
141
      // regions: body.regions?.map((r) => r.id), // Deprecated
142
      isoRegions: body.iso3166?.map((s) => s.iso)?.filter((e) => e.length > 2),
×
143
      countries: body.iso3166?.map((s) => s.iso)?.filter((e) => e.length <= 2),
×
144
      parent: body.parent?.id,
145
      // children cannot be set. The parent child relation can only be changed in one direction
146
      authorizationDocument: body.authorizationDocument?.id,
147
    };
148
  },
149

150
  appendPopulateForSimpleDocument: (docQuery) => {
151
    docQuery
8✔
152
      .populate('identifierType')
153
      .populate('author')
154
      .populate('authors')
155
      .populate('authorsGrotto')
156
      .populate('reviewer')
157
      .populate('validator')
158
      .populate('editor')
159
      .populate('library')
160
      .populate('type')
161
      .populate('descriptions')
162
      .populate('subjects')
163
      .populate('license')
164
      .populate('languages')
165
      .populate('option')
166
      // .populate('massif') // deprecated, replaced by countries and isoRegions
167
      .populate('countries')
168
      .populate('isoRegions')
169
      // .populate('massif') // deprecated, replaced by massifs
170
      .populate('massifs')
171
      .populate('files', { where: { isValidated: true } });
172
    return docQuery;
8✔
173
  },
174

175
  appendPopulateForFullDocument: (docQuery) => {
176
    module.exports
4✔
177
      .appendPopulateForSimpleDocument(docQuery)
178
      .populate('cave')
179
      .populate('entrance');
180
    // .populate('parent') // resolved in populateFullDocumentSubEntities()
181
    // .populate('children') // resolved in populateFullDocumentSubEntities()
182
    // .populate('authorizationDocument'); // resolved in populateFullDocumentSubEntities()
183

184
    return docQuery;
4✔
185
  },
186

187
  // Set name of cave, entrance, massif, editor and library if present
188
  populateFullDocumentSubEntities: async (document) => {
189
    const asyncQueue = [];
4✔
190

191
    // eslint-disable-next-line no-param-reassign
192
    document.mainLanguage = module.exports.getMainLanguage(document.languages);
4✔
193

194
    if (document.entrance) {
4!
195
      asyncQueue.push(NameService.setNames([document.entrance], 'entrance'));
×
196
    }
197
    if (document.cave) {
4!
198
      asyncQueue.push(NameService.setNames([document.cave], 'cave'));
×
199
    }
200
    const allMassifs = document.massifs;
4✔
201
    if (allMassifs.length > 0) {
4!
202
      asyncQueue.push(NameService.setNames(allMassifs, 'massif'));
×
203
    }
204
    const allGrottos = [];
4✔
205
    if (document.library) allGrottos.push(document.library);
4!
206
    if (document.editor) allGrottos.push(document.editor);
4✔
207
    if (document.authorsGrotto) allGrottos.push(...document.authorsGrotto);
4!
208
    if (allGrottos.length > 0) {
4✔
209
      asyncQueue.push(NameService.setNames(allGrottos, 'grotto'));
2✔
210
    }
211

212
    async function resolveDocument(doc, key) {
213
      // eslint-disable-next-line no-param-reassign
214
      doc[key] = (await module.exports.getDocuments([doc[key]]))[0];
2✔
215
    }
216

217
    if (document.parent) asyncQueue.push(resolveDocument(document, 'parent'));
4✔
218
    if (document.authorizationDocument)
4!
219
      asyncQueue.push(resolveDocument(document, 'authorizationDocument'));
×
220

221
    await Promise.all(asyncQueue);
4✔
222

223
    return document;
4✔
224
  },
225

226
  async getPopulatedDocument(documentId) {
227
    const doc = await module.exports.appendPopulateForFullDocument(
4✔
228
      TDocument.findOne(documentId)
229
    );
230
    if (!doc) return null;
4!
231
    await module.exports.populateFullDocumentSubEntities(doc);
4✔
232
    return doc;
4✔
233
  },
234

235
  /**
236
   * Depending on the number of languages, return the document main language.
237
   * @param {TLanguage[]} languages
238
   * @returns main language of the document
239
   */
240
  getMainLanguage: (languages) => {
241
    if (!languages) return undefined;
12!
242
    if (languages.length === 0) return undefined;
12!
243
    if (languages.length === 1) return languages[0];
×
244
    return languages.filter((l) => l.isMain);
×
245
  },
246

247
  async updateDocument({
×
248
    documentId,
249
    reviewerId,
250
    documentData,
251
    descriptionData,
252
    newFiles,
253
    modifiedFiles,
254
    deletedFiles,
255
  } = {}) {
256
    return TDocument.updateOne(documentId).set({
2✔
257
      dateReviewed: new Date(), // Avoid an uniqueness error
258
      isValidated: false,
259
      dateValidation: null,
260
      modifiedDocJson: {
261
        reviewerId,
262
        documentData,
263
        descriptionData,
264
        newFiles,
265
        modifiedFiles,
266
        deletedFiles,
267
      },
268
    });
269
  },
270

271
  createDocument: async (
272
    req,
273
    documentData,
274
    descriptionData,
275
    shouldDownloadDistantFile = false
2✔
276
  ) => {
277
    // Doc types needing a parent in order to be created
278
    // (ex; an issue needs a collection, an article needs an issue)
279
    const MANDATORY_PARENT_TYPES = ['article', 'issue'];
2✔
280

281
    const document = await sails.getDatastore().transaction(async (db) => {
2✔
282
      // Perform some checks
283
      const docType =
284
        documentData.type && (await TType.findOne(documentData.type));
2!
285
      if (docType) {
2!
286
        const docTypeName = docType.name.toLowerCase();
×
287
        // Parent doc is mandatory for articles and issues
288
        if (
×
289
          MANDATORY_PARENT_TYPES.includes(docTypeName) &&
×
290
          !documentData.parent
291
        ) {
292
          throw Error(
×
293
            `Your document being an ${docType.name.toLowerCase()}, you must provide a document parent.`
294
          );
295
        }
296
      }
297

298
      const createdDocument = await TDocument.create(documentData)
2✔
299
        .fetch()
300
        .usingConnection(db);
301

302
      await TDescription.create({
2✔
303
        dateInscription: descriptionData.dateInscription ?? new Date(),
4✔
304
        dateReviewed: descriptionData?.dateReviewed,
305
        author: descriptionData.author,
306
        title: descriptionData.title,
307
        body: descriptionData.body,
308
        document: createdDocument.id,
309
        language: descriptionData.language,
310
      }).usingConnection(db);
311

312
      return createdDocument;
2✔
313
    });
314

315
    await RecentChangeService.setNameCreate(
2✔
316
      'document',
317
      document.id,
318
      req.token.id,
319
      descriptionData.title
320
    );
321

322
    const populatedDocuments = await module.exports.getDocuments([document.id]);
2✔
323
    const populatedDocument = populatedDocuments[0];
2✔
324

325
    const documentType = populatedDocument?.identifierType?.id?.trim() ?? '';
2✔
326
    if (documentType === 'url' && shouldDownloadDistantFile) {
2!
327
      const url = populatedDocument.identifier;
×
328
      sails.log.info(`Downloading ${url}...`);
×
329
      const acceptedFileFormats = await TFileFormat.find();
×
330
      const allowedExtentions = acceptedFileFormats.map((f) =>
×
331
        f.extension.trim()
×
332
      );
333

334
      const file = await distantFileDownload({
×
335
        url,
336
        allowedExtentions,
337
      }).catch((err) => {
338
        sails.log.error(`Failed to download ${url}: ${err}`);
×
339
      });
340

341
      if (file) {
×
342
        await FileService.document.create(file, document.id);
×
343
      }
344
    }
345

346
    await NotificationService.notifySubscribers(
2✔
347
      req,
348
      populatedDocument,
349
      req.token.id,
350
      NotificationService.NOTIFICATION_TYPES.CREATE,
351
      NotificationService.NOTIFICATION_ENTITIES.DOCUMENT
352
    );
353

354
    return populatedDocument;
2✔
355
  },
356

357
  /**
358
   * Populate document-like object for a csv duplicate import or a modified document
359
   * Mainly used for json column that cannot be populated using waterline query language.
360
   * @param {*} documentData format from getConvertedDataFromClient()
361
   * @returns populated document
362
   */
363
  populateJSON: async (documentId, documentData) => {
364
    const {
365
      identifierType,
366
      author,
367
      authors,
368
      authorsGrotto,
369
      reviewer,
370
      editor,
371
      library,
372
      type,
373
      subjects,
374
      license,
375
      option,
376
      languages,
377
      countries,
378
      isoRegions,
379
      cave,
380
      entrance,
381
      massifs,
382
      parent,
383
      authorizationDocument,
384
      ...otherSimpleData
385
    } = documentData;
×
386

387
    // Join the tables
388
    const doc = { ...otherSimpleData, id: documentId };
×
389
    doc.identifierType = identifierType
×
390
      ? await TIdentifierType.findOne(identifierType)
391
      : null;
392
    doc.author = author ? await TCaver.findOne(author) : null;
×
393
    doc.authors = authors ? await TCaver.find({ id: authors }) : [];
×
394
    doc.authorsGrotto = authorsGrotto
×
395
      ? await TGrotto.find({ id: authorsGrotto })
396
      : [];
397
    doc.reviewer = reviewer ? await TCaver.findOne(reviewer) : null;
×
398
    doc.editor = editor ? await TGrotto.findOne(editor) : null;
×
399
    doc.library = library ? await TGrotto.findOne(library) : null;
×
400

401
    doc.type = type ? await TType.findOne(type) : null;
×
402
    // descriptions is a special case
403
    doc.subjects = subjects ? await TSubject.find({ id: subjects }) : [];
×
404
    doc.license = license ? await TLicense.findOne(license) : null;
×
405
    doc.option = option ? await TOption.findOne(option) : null;
×
406
    doc.languages = languages ? await TLanguage.find({ id: languages }) : [];
×
407

408
    // TODO files ?
409
    doc.countries = countries ? await TCountry.find({ id: countries }) : [];
×
410
    doc.isoRegions = isoRegions ? await TISO31662.find({ id: isoRegions }) : [];
×
411
    doc.cave = cave ? await TCave.findOne(cave) : null;
×
412
    doc.entrance = entrance ? await TEntrance.findOne(entrance) : null;
×
413
    doc.massifs = massifs ? await TCountry.find({ id: massifs }) : [];
×
414
    doc.parent = parent
×
415
      ? (await module.exports.getDocuments([parent]))[0]
416
      : null;
417
    doc.authorizationDocument = authorizationDocument
×
418
      ? await TDocument.findOne(authorizationDocument)
419
      : null;
420
    return doc;
×
421
  },
422

423
  /**
424
   * Get basic informations for a list of document ids
425
   * The result is intended to be passed to the toSimpleDocument converter
426
   * @param {Array} documentIds
427
   * @returns
428
   */
429
  getDocuments: async (documentIds) => {
430
    if (documentIds.length === 0) return [];
34✔
431
    return TDocument.find({ id: documentIds })
17✔
432
      .populate('descriptions')
433
      .populate('type')
434
      .populate('files', { where: { isValidated: true } });
435
  },
436

437
  getDocumentChildren: async (documentId) =>
438
    TDocument.find({ parent: documentId })
×
439
      .populate('descriptions')
440
      .populate('type'),
441

442
  getHDocumentById: async (documentId) =>
443
    HDocument.find({ t_id: documentId })
×
444
      .populate('author')
445
      .populate('reviewer')
446
      .populate('massif')
447
      .populate('cave')
448
      .populate('editor')
449
      .populate('entrance')
450
      .populate('identifierType')
451
      .populate('library')
452
      .populate('license')
453
      .populate('type'),
454

455
  populateHDocumentsWithDescription: async (documentId, hDocuments) => {
456
    const descriptions =
457
      await DescriptionService.getHDescriptionsOfDocument(documentId);
×
458
    hDocuments.forEach((document) => {
×
459
      if (Object.keys(descriptions).length > 0) {
×
460
        // eslint-disable-next-line no-param-reassign
461
        document.description = descriptions[0];
×
462
        descriptions.forEach((desc) => {
×
463
          if (
×
464
            // Return true if the description should be associate to this document according to her dateReviewed
465
            DescriptionService.compareDescriptionDate(
466
              // Id represents here the dateReviewed like in the H models
467
              new Date(document.id),
468
              new Date(desc.id),
469
              new Date(document.description.id)
470
            )
471
          ) {
472
            // eslint-disable-next-line no-param-reassign
473
            document.description = desc;
×
474
          }
475
        });
476
      }
477
    });
478
    return hDocuments;
×
479
  },
480

481
  getIdDocumentByEntranceId: async (entranceId) => {
482
    let documentsId = [];
×
483
    if (entranceId) {
×
484
      documentsId = await TDocument.find({
×
485
        where: { entrance: entranceId },
486
        select: ['id'],
487
      });
488
    }
489
    return documentsId;
×
490
  },
491
};
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