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

alkem-io / server / #8050

16 Aug 2024 11:21AM UTC coverage: 13.92%. First build
#8050

Pull #4411

travis-ci

Pull Request #4411: Type added to authorization policy entity

80 of 4158 branches covered (1.92%)

Branch coverage included in aggregate %.

61 of 116 new or added lines in 50 files covered. (52.59%)

1945 of 10389 relevant lines covered (18.72%)

3.01 hits per line

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

7.47
/src/domain/collaboration/callout/callout.service.ts
1
import { Injectable } from '@nestjs/common';
17✔
2
import { InjectRepository } from '@nestjs/typeorm';
17✔
3
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
17✔
4
import {
5
  EntityNotFoundException,
6
  EntityNotInitializedException,
7
  RelationshipNotFoundException,
8
  ValidationException,
9
} from '@common/exceptions';
17✔
10
import { LogContext } from '@common/enums';
11
import { AuthorizationPolicy } from '@domain/common/authorization-policy';
12
import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service';
13
import { Callout } from '@domain/collaboration/callout/callout.entity';
14
import { ICallout } from '@domain/collaboration/callout/callout.interface';
17✔
15
import { CreateCalloutInput } from '@domain/collaboration/callout/dto/index';
17✔
16
import { limitAndShuffle } from '@common/utils';
17✔
17
import { NamingService } from '@services/infrastructure/naming/naming.service';
17✔
18
import { UpdateCalloutVisibilityInput } from './dto/callout.dto.update.visibility';
19
import { RoomService } from '@domain/communication/room/room.service';
20
import { RoomType } from '@common/enums/room.type';
21
import { IRoom } from '@domain/communication/room/room.interface';
22
import { ITagsetTemplate } from '@domain/common/tagset-template';
23
import { CalloutFramingService } from '../callout-framing/callout.framing.service';
17✔
24
import { ICalloutFraming } from '../callout-framing/callout.framing.interface';
17✔
25
import { ICalloutSettings } from '../callout-settings/callout.settings.interface';
17✔
26
import { CalloutContributionDefaultsService } from '../callout-contribution-defaults/callout.contribution.defaults.service';
17✔
27
import { ICalloutContribution } from '../callout-contribution/callout.contribution.interface';
28
import { CreateContributionOnCalloutInput } from './dto/callout.dto.create.contribution';
17✔
29
import { CalloutContributionService } from '../callout-contribution/callout.contribution.service';
17✔
30
import { CreateWhiteboardInput } from '@domain/common/whiteboard/dto/whiteboard.dto.create';
17✔
31
import { CreatePostInput } from '../post/dto/post.dto.create';
32
import { ICalloutContributionDefaults } from '../callout-contribution-defaults/callout.contribution.defaults.interface';
33
import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface';
17✔
34
import { StorageAggregatorResolverService } from '@services/infrastructure/storage-aggregator-resolver/storage.aggregator.resolver.service';
17✔
35
import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type';
36
import { UpdateCalloutInput } from './dto/callout.dto.update';
17✔
37
import { UpdateContributionCalloutsSortOrderInput } from '../callout-contribution/dto/callout.contribution.dto.update.callouts.sort.order';
17✔
38
import { cloneDeep, keyBy, merge } from 'lodash';
39
import { IStorageBucket } from '@domain/storage/storage-bucket/storage.bucket.interface';
40
import { UserLookupService } from '@domain/community/user-lookup/user.lookup.service';
17✔
41
import { ClassificationService } from '@domain/common/classification/classification.service';
42
import { IClassification } from '@domain/common/classification/classification.interface';
43
import {
44
  AllCalloutContributionTypes,
45
  CalloutContributionType,
46
} from '@common/enums/callout.contribution.type';
47
import { CalloutFramingType } from '@common/enums/callout.framing.type';
17✔
48
import { DefaultCalloutSettings } from '../callout-settings/callout.settings.default';
17✔
49
import { CalloutContributionsCountOutput } from './dto/callout.contributions.count.dto';
50

51
@Injectable()
17✔
52
export class CalloutService {
53
  constructor(
×
54
    private authorizationPolicyService: AuthorizationPolicyService,
×
55
    private namingService: NamingService,
×
56
    private roomService: RoomService,
×
57
    private userLookupService: UserLookupService,
×
58
    private calloutFramingService: CalloutFramingService,
×
59
    private contributionDefaultsService: CalloutContributionDefaultsService,
×
60
    private contributionService: CalloutContributionService,
×
61
    private storageAggregatorResolverService: StorageAggregatorResolverService,
×
62
    private classificationService: ClassificationService,
63
    @InjectRepository(Callout)
×
64
    private calloutRepository: Repository<Callout>
65
  ) {}
66

67
  public async createCallout(
68
    calloutData: CreateCalloutInput,
69
    classificationTagsetTemplates: ITagsetTemplate[],
70
    storageAggregator: IStorageAggregator,
71
    userID?: string
72
  ): Promise<ICallout> {
×
73
    this.validateCreateCalloutData(calloutData);
74

×
75
    if (!calloutData.sortOrder) {
×
76
      calloutData.sortOrder = 10;
77
    }
78

×
NEW
79
    const callout: ICallout = Callout.create(calloutData);
×
80
    callout.authorization = new AuthorizationPolicy(
81
      AuthorizationPolicyType.CALLOUT
82
    );
×
83
    callout.createdBy = userID ?? undefined;
×
84
    callout.contributions = [];
×
85

86
    callout.framing = await this.calloutFramingService.createCalloutFraming(
×
87
      calloutData.framing,
88
      storageAggregator,
89
      userID
90
    );
91

92
    callout.settings = this.createCalloutSettings(calloutData.settings);
×
93

×
94
    callout.classification = this.classificationService.createClassification(
95
      classificationTagsetTemplates,
96
      calloutData.classification
97
    );
98

99
    callout.contributionDefaults =
×
100
      await this.contributionDefaultsService.createCalloutContributionDefaults(
101
        calloutData.contributionDefaults,
102
        callout.framing.profile.storageBucket
103
      );
104

105
    if (userID && calloutData.contributions && callout.settings.contribution) {
×
106
      callout.contributions =
107
        await this.contributionService.createCalloutContributions(
108
          calloutData.contributions,
109
          storageAggregator,
×
110
          callout.settings.contribution,
111
          userID
112
        );
113
    }
114

×
115
    if (!callout.isTemplate && callout.settings.framing.commentsEnabled) {
×
116
      callout.comments = await this.roomService.createRoom(
117
        `callout-comments-${callout.nameID}`,
118
        RoomType.CALLOUT
119
      );
120
    }
121

×
122
    return callout;
123
  }
124

125
  private createCalloutSettings(
×
126
    settingsData?: CreateCalloutInput['settings']
×
127
  ): ICalloutSettings {
×
128
    const calloutSettings = cloneDeep(DefaultCalloutSettings);
129
    if (settingsData) {
×
130
      merge(calloutSettings, settingsData);
131
    }
132
    return calloutSettings;
133
  }
134

135
  private validateCreateCalloutData(calloutData: CreateCalloutInput) {
×
136
    if (
×
137
      // If can contribute with whiteboard
138
      (calloutData.settings?.contribution?.allowedTypes ?? []).includes(
139
        CalloutContributionType.WHITEBOARD
×
140
      ) && //  but no whiteboard template provided
141
      !calloutData.contributionDefaults?.whiteboardContent
142
    ) {
143
      throw new ValidationException(
144
        'Please provide a whiteboard template',
145
        LogContext.COLLABORATION
146
      );
147
    }
148

149
    if (
×
150
      calloutData.framing.type == CalloutFramingType.WHITEBOARD &&
151
      !calloutData.framing.whiteboard
152
    ) {
153
      throw new ValidationException(
154
        'Please provide a whiteboard',
155
        LogContext.COLLABORATION
156
      );
157
    } else if (
158
      calloutData.framing.type !== CalloutFramingType.WHITEBOARD &&
×
159
      calloutData.framing.whiteboard
×
160
    ) {
×
161
      throw new ValidationException(
162
        'Whiteboard framing can only be used with whiteboard framing type',
163
        LogContext.COLLABORATION
164
      );
165
    }
×
166
  }
167

×
168
  private async getStorageAggregator(
169
    calloutID: string
170
  ): Promise<IStorageAggregator> {
171
    return await this.storageAggregatorResolverService.getStorageAggregatorForCallout(
172
      calloutID
×
173
    );
×
174
  }
175

176
  public async getCalloutOrFail(
177
    calloutID: string,
178
    options?: FindOneOptions<Callout>
179
  ): Promise<ICallout | never> {
×
180
    const callout = await this.calloutRepository.findOne({
181
      where: { id: calloutID },
182
      ...options,
183
    });
184

185
    if (!callout)
×
186
      throw new EntityNotFoundException(
187
        `No Callout found with the given id: ${calloutID}, using options: ${JSON.stringify(
×
188
          options
×
189
        )}`,
190
        LogContext.COLLABORATION
×
191
      );
192
    return callout;
193
  }
194

195
  public async updateCalloutVisibility(
196
    calloutVisibilityUpdateData: UpdateCalloutVisibilityInput
197
  ): Promise<ICallout> {
198
    const callout = await this.getCalloutOrFail(
×
199
      calloutVisibilityUpdateData.calloutID
×
200
    );
×
201

202
    if (calloutVisibilityUpdateData.visibility)
203
      callout.settings.visibility = calloutVisibilityUpdateData.visibility;
×
204

×
205
    return await this.calloutRepository.save(callout);
×
206
  }
207

208
  public async updateCalloutPublishInfo(
×
209
    callout: ICallout,
210
    publisherID?: string,
211
    publishedTimestamp?: number
212
  ): Promise<ICallout> {
213
    if (publisherID) {
214
      const publisher = await this.userLookupService.getUserByUUID(publisherID);
×
215
      callout.publishedBy = publisher?.id || '';
216
    }
217

218
    if (publishedTimestamp) {
219
      const date = new Date(publishedTimestamp);
220
      callout.publishedDate = date;
221
    }
222

×
223
    return await this.calloutRepository.save(callout);
×
224
  }
225

226
  public async updateCallout(
227
    calloutInput: ICallout,
228
    calloutUpdateData: UpdateCalloutInput,
229
    userID?: string
×
230
  ): Promise<ICallout> {
×
231
    const callout = await this.getCalloutOrFail(calloutInput.id, {
232
      relations: {
233
        contributionDefaults: true,
234
        framing: {
235
          profile: true,
236
          whiteboard: true,
×
237
          link: true,
×
238
          memo: true,
239
        },
240
        classification: {
241
          tagsets: true,
242
        },
243
        calloutsSet: true,
244
      },
×
245
    });
×
246
    const storageAggregator = await this.getStorageAggregator(callout.id);
247

248
    if (!callout.contributionDefaults || !callout.settings.contribution) {
249
      throw new EntityNotInitializedException(
250
        `Unable to load callout: ${callout.id}`,
251
        LogContext.COLLABORATION
252
      );
×
253
    }
×
254

255
    if (calloutUpdateData.framing) {
×
256
      callout.framing = await this.calloutFramingService.updateCalloutFraming(
×
257
        callout.framing,
258
        calloutUpdateData.framing,
259
        storageAggregator,
260
        callout.isTemplate,
261
        userID
262
      );
×
263
    }
264

265
    if (calloutUpdateData.settings) {
266
      callout.settings = merge(callout.settings, calloutUpdateData.settings);
×
267
    }
268

269
    if (calloutUpdateData.classification) {
270
      callout.classification = this.classificationService.updateClassification(
×
271
        callout.classification,
272
        calloutUpdateData.classification
273
      );
274
    }
275

276
    if (calloutUpdateData.contributionDefaults) {
277
      callout.contributionDefaults =
278
        this.contributionDefaultsService.updateCalloutContributionDefaults(
279
          callout.contributionDefaults,
280
          calloutUpdateData.contributionDefaults
×
281
        );
×
282
    }
283

284
    // Create the Matrix room for comments if it doesn't yet exist
285
    if (
×
286
      !callout.isTemplate &&
287
      callout.settings.framing.commentsEnabled &&
288
      !callout.comments
289
    ) {
290
      callout.comments = await this.roomService.createRoom(
291
        `callout-comments-${callout.nameID}`,
×
292
        RoomType.CALLOUT
×
293
      );
×
294
    }
295

296
    if (calloutUpdateData.sortOrder)
×
297
      callout.sortOrder = calloutUpdateData.sortOrder;
×
298

299
    return await this.calloutRepository.save(callout);
300
  }
×
301

×
302
  async save(callout: ICallout): Promise<ICallout> {
303
    return await this.calloutRepository.save(callout);
×
304
  }
×
305

306
  public async deleteCallout(calloutID: string): Promise<ICallout> {
×
307
    const callout = await this.getCalloutOrFail(calloutID, {
×
308
      relations: {
309
        comments: true,
×
310
        contributions: true,
311
        contributionDefaults: true,
312
        framing: true,
313
      },
×
314
    });
315

316
    if (
317
      !callout.contributionDefaults ||
318
      !callout.settings ||
319
      !callout.contributions
×
320
    ) {
×
321
      throw new EntityNotInitializedException(
322
        `Unable to load callout for deleting: ${callout.id}`,
323
        LogContext.COLLABORATION
324
      );
×
325
    }
326

327
    await this.calloutFramingService.delete(callout.framing);
328

329
    for (const contribution of callout.contributions) {
330
      await this.contributionService.delete(contribution.id);
331
    }
332

333
    if (callout.comments) {
NEW
334
      await this.roomService.deleteRoom(callout.comments);
×
335
    }
336

337
    await this.contributionDefaultsService.delete(callout.contributionDefaults);
×
338

339
    if (callout.authorization)
340
      await this.authorizationPolicyService.delete(callout.authorization);
341

342
    const result = await this.calloutRepository.remove(callout as Callout);
343
    result.id = calloutID;
344

345
    return result;
346
  }
347

348
  public getCallouts(options: FindManyOptions<Callout>): Promise<ICallout[]> {
349
    return this.calloutRepository.find(options);
350
  }
351

352
  /**
353
   *
354
   * @param callout
355
   * @returns a number, the number of messages or the number of contributions if the callout allows contributions
356
   */
357
  public async getActivityCount(callout: ICallout): Promise<number> {
358
    if (callout.settings.contribution.allowedTypes.length > 0) {
359
      return this.contributionService.getContributionsInCalloutCount(
×
360
        callout.id
×
361
      );
362
    } else {
363
      return this.getCommentsCount(callout.id);
364
    }
×
365
  }
366

367
  private async setNameIdOnPostData(
368
    postData: CreatePostInput,
369
    reservedNameIDs: string[],
×
370
    callout: ICallout
×
371
  ) {
×
372
    if (postData.nameID && postData.nameID.length > 0) {
373
      const nameTaken = reservedNameIDs.includes(postData.nameID);
374
      if (nameTaken)
×
375
        throw new ValidationException(
376
          `Unable to create Post: the provided nameID is already taken: ${postData.nameID}`,
377
          LogContext.SPACES
378
        );
379
    } else {
380
      postData.nameID = this.namingService.createNameIdAvoidingReservedNameIDs(
381
        postData.profileData.displayName,
382
        reservedNameIDs
×
383
      );
×
384
    }
×
385

×
386
    // Check that there isn't an post with the same title
387
    const displayName = postData.profileData.displayName;
388
    const existingPost = callout.posts?.find(
389
      post => post.profile.displayName === displayName
390
    );
×
391
    if (existingPost)
392
      throw new ValidationException(
393
        `Already have an post with the provided display name: ${displayName}`,
394
        LogContext.COLLABORATION
395
      );
396
  }
397

×
398
  public async getContributionDefaults(
×
399
    calloutID: string
×
400
  ): Promise<ICalloutContributionDefaults> {
401
    const callout = await this.getCalloutOrFail(calloutID, {
×
402
      relations: { contributionDefaults: true },
×
403
    });
404
    if (!callout.contributionDefaults)
405
      throw new EntityNotInitializedException(
406
        `Callout (${calloutID}) not initialised as no contribution defaults`,
407
        LogContext.COLLABORATION
408
      );
409
    return callout.contributionDefaults;
410
  }
411

×
412
  public async getComments(calloutID: string): Promise<IRoom | undefined> {
413
    const callout = await this.getCalloutOrFail(calloutID, {
414
      relations: { comments: true },
×
415
    });
×
416
    return callout.comments;
417
  }
418

419
  private async getCommentsCount(calloutID: string): Promise<number> {
×
420
    const comments = await this.getComments(calloutID);
421
    if (!comments) return 0;
422
    return comments.messagesCount;
423
  }
424

425
  private async setNameIdOnWhiteboardData(
×
426
    whiteboardData: CreateWhiteboardInput,
427
    reservedNameIDs: string[]
428
  ) {
×
429
    if (whiteboardData.nameID && whiteboardData.nameID.length > 0) {
×
430
      const nameIdTaken = reservedNameIDs.includes(whiteboardData.nameID);
431
      if (nameIdTaken)
432
        throw new ValidationException(
433
          `Unable to create Whiteboard: the provided nameID is already taken: ${whiteboardData.nameID}`,
×
434
          LogContext.SPACES
435
        );
436
    } else {
437
      whiteboardData.nameID =
×
438
        this.namingService.createNameIdAvoidingReservedNameIDs(
439
          `${whiteboardData.profile?.displayName ?? 'Whiteboard'}`,
440
          reservedNameIDs
×
441
        );
442
    }
443
  }
444

445
  public async createContributionOnCallout(
446
    contributionData: CreateContributionOnCalloutInput,
447
    userID: string
×
448
  ): Promise<ICalloutContribution> {
×
449
    const calloutID = contributionData.calloutID;
×
450
    const callout = await this.getCalloutOrFail(calloutID, {
×
451
      relations: {
452
        contributions: true,
453
      },
454
    });
455
    if (!callout.settings.contribution)
×
456
      throw new EntityNotInitializedException(
457
        `Callout (${calloutID}) not initialised as no contributions`,
458
        LogContext.COLLABORATION
459
      );
460

461
    const reservedNameIDs =
462
      await this.namingService.getReservedNameIDsInCalloutContributions(
463
        calloutID
464
      );
465
    if (contributionData.whiteboard) {
466
      await this.setNameIdOnWhiteboardData(
467
        contributionData.whiteboard,
×
468
        reservedNameIDs
×
469
      );
470
    }
471
    if (contributionData.post) {
×
472
      await this.setNameIdOnPostData(
×
473
        contributionData.post,
474
        reservedNameIDs,
475
        callout
476
      );
477
    }
478

×
479
    if (!callout.contributions) {
480
      throw new EntityNotInitializedException(
481
        'Not able to load Contributions for this callout',
×
482
        LogContext.COLLABORATION,
×
483
        { calloutId: calloutID }
484
      );
485
    }
486

487
    // set default sort order as the minimum of the existing contributions
×
488
    // we want the new one to be first
×
489
    if (contributionData.sortOrder === undefined) {
490
      const contributionsSortOrder = callout.contributions.map(
491
        c => c.sortOrder
492
      );
493
      const minOrder = Math.min(...contributionsSortOrder);
494
      // first contribution
495
      contributionData.sortOrder = !contributionsSortOrder.length
×
496
        ? 1
497
        : minOrder - 1;
×
498
    }
499

500
    const storageAggregator = await this.getStorageAggregator(callout.id);
501
    const contribution =
502
      await this.contributionService.createCalloutContribution(
503
        contributionData,
×
504
        storageAggregator,
×
505
        callout.settings.contribution,
506
        userID
507
      );
508
    contribution.callout = callout;
509

×
510
    return await this.contributionService.save(contribution);
511
  }
×
512

513
  public async getStorageBucket(calloutID: string): Promise<IStorageBucket> {
514
    const callout = await this.getCalloutOrFail(calloutID, {
×
515
      relations: {
×
516
        framing: {
517
          profile: {
518
            storageBucket: true,
519
          },
520
        },
×
521
      },
522
    });
523
    const storageBucket = callout?.framing?.profile?.storageBucket;
524
    if (!storageBucket) {
525
      throw new RelationshipNotFoundException(
526
        `Unable to find storage bucket to use for Callout: ${calloutID}`,
527
        LogContext.COLLABORATION
528
      );
529
    }
530
    return storageBucket;
531
  }
×
532

533
  public async getClassification(calloutID: string): Promise<IClassification> {
534
    const callout = await this.getCalloutOrFail(calloutID, {
535
      relations: {
536
        classification: true,
537
      },
538
    });
539
    const classification = callout?.classification;
×
540
    if (!classification) {
×
541
      throw new RelationshipNotFoundException(
542
        `Unable to find Classification to use for Callout: ${calloutID}`,
543
        LogContext.COLLABORATION
544
      );
545
    }
×
546
    return classification;
×
547
  }
×
548

×
549
  public async updateContributionCalloutsSortOrder(
550
    calloutId: string,
551
    sortOrderData: UpdateContributionCalloutsSortOrderInput
552
  ): Promise<ICalloutContribution[]> {
553
    const callout = await this.getCalloutOrFail(calloutId, {
×
554
      relations: {
555
        contributions: true,
556
      },
×
557
    });
×
558

×
559
    if (!callout.contributions)
560
      throw new EntityNotFoundException(
561
        `No collaborations found: ${calloutId}`,
562
        LogContext.COLLABORATION
563
      );
×
564

×
565
    const contributionsById = keyBy(callout.contributions, 'id');
×
566

567
    const contributionsInOrder: ICalloutContribution[] = [];
568
    let index = 1;
569
    for (const id of sortOrderData.contributionIDs) {
570
      const contribution = contributionsById[id];
×
571
      if (!contribution || !contribution.id) {
×
572
        throw new EntityNotFoundException(
×
573
          `Callout with requested ID (${id}) not located within current Contribution: ${calloutId}`,
574
          LogContext.COLLABORATION
575
        );
576
      }
×
577
      contribution.sortOrder = index;
578
      contributionsInOrder.push(contribution);
×
579
      index++;
580
    }
×
581

582
    return this.contributionService.save(contributionsInOrder);
583
  }
×
584

×
585
  public async getCalloutFraming(
×
586
    calloutID: string,
587
    relations: FindOneOptions<ICallout>[] = []
×
588
  ): Promise<ICalloutFraming> {
×
589
    const calloutLoaded = await this.getCalloutOrFail(calloutID, {
×
590
      relations: { framing: true, ...relations },
×
591
    });
592
    if (!calloutLoaded.framing)
593
      throw new EntityNotFoundException(
594
        `Callout not initialised, no framing: ${calloutID}`,
×
595
        LogContext.COLLABORATION
×
596
      );
×
597

×
598
    return calloutLoaded.framing;
599
  }
600

601
  public async getContributions(
×
602
    callout: ICallout,
×
603
    contributionIDs?: string[],
×
604
    types: readonly CalloutContributionType[] = AllCalloutContributionTypes,
×
605
    limit?: number,
606
    shuffle?: boolean
607
  ): Promise<ICalloutContribution[]> {
×
608
    const calloutLoaded = await this.getCalloutOrFail(callout.id, {
609
      relations: {
×
610
        contributions: {
611
          post: types.includes(CalloutContributionType.POST),
×
612
          whiteboard: types.includes(CalloutContributionType.WHITEBOARD),
613
          link: types.includes(CalloutContributionType.LINK),
614
          memo: types.includes(CalloutContributionType.MEMO),
615
        },
616
      },
617
      ...(!shuffle
618
        ? {
619
            order: {
620
              contributions: {
621
                sortOrder: 'ASC',
622
              },
623
            },
624
          }
625
        : undefined),
626
    });
627
    if (!calloutLoaded.contributions)
628
      throw new EntityNotFoundException(
629
        `Callout not initialized, no contributions: ${callout.id}`,
630
        LogContext.COLLABORATION
631
      );
632

633
    const results: ICalloutContribution[] = [];
634
    if (!contributionIDs) {
635
      results.push(...calloutLoaded.contributions);
636
    } else {
637
      for (const contributionID of contributionIDs) {
638
        const contribution = calloutLoaded.contributions.find(
639
          contribution => contribution.id === contributionID
640
        );
641
        if (!contribution) continue;
642

643
        results.push(contribution);
644
      }
645
    }
646

647
    const limitAndShuffled = limitAndShuffle(results, limit, shuffle);
648
    return limitAndShuffled;
649
  }
650

651
  public async getContributionsCount(
652
    callout: ICallout
653
  ): Promise<CalloutContributionsCountOutput> {
654
    const counts = await this.calloutRepository
655
      .createQueryBuilder('callout')
656
      .leftJoin('callout.contributions', 'contribution')
657
      .select('contribution.type', 'type')
658
      .addSelect('COUNT(contribution.id)', 'count')
659
      .where('callout.id = :calloutId', { calloutId: callout.id })
660
      .groupBy('contribution.type')
661
      .getRawMany<{ type: CalloutContributionType; count: string }>();
662

663
    const result: CalloutContributionsCountOutput = {
664
      post: 0,
665
      link: 0,
666
      whiteboard: 0,
667
      memo: 0,
668
    };
669

670
    for (const { type, count } of counts) {
671
      const numCount = parseInt(count, 10);
672
      if (type === CalloutContributionType.POST) {
673
        result.post = numCount;
674
      } else if (type === CalloutContributionType.LINK) {
675
        result.link = numCount;
676
      } else if (type === CalloutContributionType.WHITEBOARD) {
677
        result.whiteboard = numCount;
678
      } else if (type === CalloutContributionType.MEMO) {
679
        result.memo = numCount;
680
      }
681
    }
682

683
    return result;
684
  }
685
}
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