• 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

9.88
/src/domain/community/virtual-contributor/virtual.contributor.service.ts
1
import { Inject, Injectable } from '@nestjs/common';
18✔
2
import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';
18✔
3
import { WINSTON_MODULE_NEST_PROVIDER, WinstonLogger } from 'nest-winston';
18✔
4
import { EntityManager, FindOneOptions, Repository } from 'typeorm';
18✔
5
import {
18✔
6
  EntityNotFoundException,
7
  EntityNotInitializedException,
8
  RelationshipNotFoundException,
9
  ValidationException,
10
} from '@common/exceptions';
18✔
11
import { LogContext, ProfileType } from '@common/enums';
18✔
12
import { ProfileService } from '@domain/common/profile/profile.service';
18✔
13
import { AuthorizationPolicy } from '@domain/common/authorization-policy';
18✔
14
import { IAgent } from '@domain/agent/agent';
15
import { AgentService } from '@domain/agent/agent/agent.service';
18✔
16
import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service';
18✔
17
import { CredentialsSearchInput } from '@domain/agent/credential/dto/credentials.dto.search';
18
import { ContributorQueryArgs } from '../contributor/dto/contributor.query.args';
19
import { VirtualContributor } from './virtual.contributor.entity';
18✔
20
import { IVirtualContributor } from './virtual.contributor.interface';
21
import { TagsetReservedName } from '@common/enums/tagset.reserved.name';
18✔
22
import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface';
18✔
23
import { CreateVirtualContributorInput } from './dto/virtual.contributor.dto.create';
24
import { UpdateVirtualContributorInput } from './dto/virtual.contributor.dto.update';
25
import { limitAndShuffle } from '@common/utils/limitAndShuffle';
26
import { CommunicationAdapter } from '@services/adapters/communication-adapter/communication.adapter';
18✔
27
import { AiPersonaService } from '@services/ai-server/ai-persona/ai.persona.service';
18✔
28
import { CreateAiPersonaInput } from '@services/ai-server/ai-persona/dto';
18✔
29
import { AgentInfo } from '@core/authentication.agent.info/agent.info';
18✔
30
import { AiServerAdapter } from '@services/adapters/ai-server-adapter/ai.server.adapter';
31
import { SearchVisibility } from '@common/enums/search.visibility';
32
import { IAiPersona } from '@services/ai-server/ai-persona/ai.persona.interface';
33
import { IContributor } from '../contributor/contributor.interface';
18✔
34
import { AgentType } from '@common/enums/agent.type';
35
import { ContributorService } from '../contributor/contributor.service';
18✔
36
import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type';
37
import { Invitation } from '@domain/access/invitation/invitation.entity';
38
import { IStorageBucket } from '@domain/storage/storage-bucket/storage.bucket.interface';
39
import { IKnowledgeBase } from '@domain/common/knowledge-base/knowledge.base.interface';
18✔
40
import { KnowledgeBaseService } from '@domain/common/knowledge-base/knowledge.base.service';
41
import { AccountLookupService } from '@domain/space/account.lookup/account.lookup.service';
18✔
42
import { VirtualContributorLookupService } from '../virtual-contributor-lookup/virtual.contributor.lookup.service';
18✔
43
import { VirtualContributorDefaultsService } from '../virtual-contributor-defaults/virtual.contributor.defaults.service';
18✔
44
import { virtualContributorSettingsDefault } from './definition/virtual.contributor.settings.default';
45
import { UpdateVirtualContributorSettingsEntityInput } from '../virtual-contributor-settings';
46
import { VirtualContributorSettingsService } from '../virtual-contributor-settings/virtual.contributor.settings.service';
18✔
47
import { CreateCalloutInput } from '@domain/collaboration/callout/dto/callout.dto.create';
48
import { VirtualContributorBodyOfKnowledgeType } from '@common/enums/virtual.contributor.body.of.knowledge.type';
×
49

×
50
@Injectable()
×
51
export class VirtualContributorService {
×
52
  constructor(
×
53
    private authorizationPolicyService: AuthorizationPolicyService,
×
54
    private agentService: AgentService,
×
55
    private profileService: ProfileService,
×
56
    private contributorService: ContributorService,
57
    private communicationAdapter: CommunicationAdapter,
×
58
    private aiPersonaService: AiPersonaService,
59
    private aiServerAdapter: AiServerAdapter,
×
60
    private knowledgeBaseService: KnowledgeBaseService,
61
    private virtualContributorLookupService: VirtualContributorLookupService,
×
62
    private virtualContributorSettingsService: VirtualContributorSettingsService,
63
    private accountLookupService: AccountLookupService,
64
    private virtualContributorDefaultsService: VirtualContributorDefaultsService,
65
    @InjectEntityManager('default')
66
    private entityManager: EntityManager,
67
    @InjectRepository(VirtualContributor)
68
    private virtualContributorRepository: Repository<VirtualContributor>,
×
69
    @Inject(WINSTON_MODULE_NEST_PROVIDER)
70
    private readonly logger: WinstonLogger
×
71
  ) {}
72

×
73
  public async createVirtualContributor(
74
    virtualContributorData: CreateVirtualContributorInput,
×
75
    knowledgeBaseDefaultCallouts: CreateCalloutInput[],
×
76
    storageAggregator: IStorageAggregator,
77
    agentInfo?: AgentInfo
78
  ): Promise<IVirtualContributor> {
79
    if (virtualContributorData.nameID) {
×
80
      // Convert nameID to lower case
81
      virtualContributorData.nameID =
82
        virtualContributorData.nameID.toLowerCase();
83
      await this.checkNameIdOrFail(virtualContributorData.nameID);
×
84
    } else {
×
85
      virtualContributorData.nameID =
NEW
86
        await this.virtualContributorDefaultsService.createVirtualContributorNameID(
×
87
          virtualContributorData.profileData?.displayName || ''
88
        );
89
    }
×
90

91
    let virtualContributor: IVirtualContributor = VirtualContributor.create(
92
      virtualContributorData
×
93
    );
×
94

95
    virtualContributor.listedInStore = true;
96
    virtualContributor.searchVisibility = SearchVisibility.ACCOUNT;
×
97

×
98
    virtualContributor.authorization = new AuthorizationPolicy(
99
      AuthorizationPolicyType.VIRTUAL_CONTRIBUTOR
100
    );
101

×
102
    // Pull the settings from a defaults file
103
    virtualContributor.settings = virtualContributorSettingsDefault;
104

×
105
    const knowledgeBaseData =
106
      await this.virtualContributorDefaultsService.createKnowledgeBaseInput(
107
        virtualContributorData.knowledgeBaseData,
108
        knowledgeBaseDefaultCallouts,
109
        virtualContributorData.bodyOfKnowledgeType
×
110
      );
111

112
    virtualContributor.knowledgeBase =
113
      await this.knowledgeBaseService.createKnowledgeBase(
×
114
        knowledgeBaseData,
115
        storageAggregator,
116
        agentInfo?.userID
117
      );
118

×
119
    const kb = await this.knowledgeBaseService.save(
×
120
      virtualContributor.knowledgeBase
×
121
    );
122

123
    const communicationID = await this.communicationAdapter.tryRegisterNewUser(
124
      `virtual-contributor-${virtualContributor.nameID}@alkem.io`
125
    );
×
126

127
    if (communicationID) {
128
      virtualContributor.communicationID = communicationID;
129
    }
130

131
    if (
×
132
      virtualContributorData.bodyOfKnowledgeType ===
133
      VirtualContributorBodyOfKnowledgeType.ALKEMIO_KNOWLEDGE_BASE
134
    ) {
135
      virtualContributor.bodyOfKnowledgeID = kb.id;
×
136
    }
×
137

138
    const aiPersonaInput: CreateAiPersonaInput = {
139
      ...virtualContributorData.aiPersona,
140
    };
141

×
142
    // Get the default AI server
143
    const aiServer = await this.aiServerAdapter.getAiServer();
144
    const aiPersona = await this.aiPersonaService.createAiPersona(
145
      aiPersonaInput,
×
146
      aiServer
147
    );
148
    virtualContributor.aiPersonaID = aiPersona.id;
×
149

×
150
    virtualContributor.profile = await this.profileService.createProfile(
151
      virtualContributorData.profileData,
152
      ProfileType.VIRTUAL_CONTRIBUTOR,
153
      storageAggregator
154
    );
155
    await this.profileService.addOrUpdateTagsetOnProfile(
156
      virtualContributor.profile,
157
      {
158
        name: TagsetReservedName.KEYWORDS,
159
        tags: [],
×
160
      }
×
161
    );
162
    await this.profileService.addOrUpdateTagsetOnProfile(
×
163
      virtualContributor.profile,
×
164
      {
165
        name: TagsetReservedName.CAPABILITIES,
×
166
        tags: [],
167
      }
168
    );
169

170
    this.contributorService.addAvatarVisualToContributorProfile(
×
171
      virtualContributor.profile,
×
172
      virtualContributorData.profileData
173
    );
174

175
    virtualContributor.agent = await this.agentService.createAgent({
176
      type: AgentType.VIRTUAL_CONTRIBUTOR,
177
    });
178

179
    virtualContributor = await this.save(virtualContributor);
180

×
181
    const userID = agentInfo ? agentInfo.userID : '';
182
    await this.contributorService.ensureAvatarIsStoredInLocalStorageBucket(
183
      virtualContributor.profile.id,
184
      userID
185
    );
186

187
    // Reload to ensure have the updated avatar URL
×
188
    virtualContributor = await this.getVirtualContributorOrFail(
×
189
      virtualContributor.id
190
    );
191
    this.logger.verbose?.(
192
      `Created new virtual with id ${virtualContributor.id}`,
193
      LogContext.COMMUNITY
×
194
    );
×
195

196
    return virtualContributor;
197
  }
198

199
  public async updateVirtualContributorSettings(
200
    virtualContributor: IVirtualContributor,
×
201
    settingsData: UpdateVirtualContributorSettingsEntityInput
×
202
  ): Promise<IVirtualContributor> {
203
    virtualContributor.settings =
204
      this.virtualContributorSettingsService.updateSettings(
205
        virtualContributor.settings,
206
        settingsData
×
207
      );
×
208
    return await this.save(virtualContributor);
209
  }
210

211
  private async checkNameIdOrFail(nameID: string) {
×
212
    const virtualCount = await this.virtualContributorRepository.countBy({
×
213
      nameID: nameID,
214
    });
215
    if (virtualCount >= 1)
×
216
      throw new ValidationException(
×
217
        `Virtual: the provided nameID is already taken: ${nameID}`,
218
        LogContext.COMMUNITY
219
      );
×
220
  }
221

222
  private async checkDisplayNameOrFail(
223
    newDisplayName?: string,
224
    existingDisplayName?: string
225
  ) {
×
226
    if (!newDisplayName) {
227
      return;
228
    }
229
    if (newDisplayName === existingDisplayName) {
230
      return;
231
    }
232
    const virtualCount = await this.virtualContributorRepository.countBy({
233
      profile: {
234
        displayName: newDisplayName,
235
      },
×
236
    });
×
237
    if (virtualCount >= 1)
238
      throw new ValidationException(
239
        `Virtual: the provided displayName is already taken: ${newDisplayName}`,
×
240
        LogContext.COMMUNITY
×
241
      );
242
  }
243

244
  public async updateVirtualContributor(
245
    virtualContributorData: UpdateVirtualContributorInput
×
246
  ): Promise<IVirtualContributor> {
×
247
    const virtual = await this.getVirtualContributorOrFail(
248
      virtualContributorData.ID,
249
      {
×
250
        relations: {
251
          profile: true,
252
          knowledgeBase: {
×
253
            profile: true,
254
          },
×
255
        },
×
256
      }
257
    );
258

259
    await this.checkDisplayNameOrFail(
260
      virtualContributorData.profileData?.displayName,
×
261
      virtual.profile.displayName
262
    );
×
263

264
    // Check the tagsets
265
    if (virtualContributorData.profileData && virtual.profile) {
266
      virtual.profile = await this.profileService.updateProfile(
267
        virtual.profile,
268
        virtualContributorData.profileData
269
      );
270
    }
×
271

×
272
    if (virtualContributorData.nameID) {
273
      if (
×
274
        virtualContributorData.nameID.toLowerCase() !==
275
        virtual.nameID.toLowerCase()
276
      ) {
277
        // updating the nameID, check new value is allowed
×
278
        await this.checkNameIdOrFail(virtualContributorData.nameID);
279
        virtual.nameID = virtualContributorData.nameID;
×
280
      }
281
    }
282

×
283
    if (typeof virtualContributorData.listedInStore === 'boolean') {
284
      virtual.listedInStore = !!virtualContributorData.listedInStore;
285
    }
286

287
    if (virtualContributorData.searchVisibility) {
288
      virtual.searchVisibility = virtualContributorData.searchVisibility;
289
    }
×
290

×
291
    if (
×
292
      virtualContributorData.knowledgeBaseData?.profile?.description &&
293
      virtual?.knowledgeBase?.profile
294
    ) {
295
      virtual.knowledgeBase.profile.description =
×
296
        virtualContributorData.knowledgeBaseData.profile?.description;
297
    }
298

299
    return await this.save(virtual);
300
  }
301

×
302
  async deleteVirtualContributor(
303
    virtualContributorID: string
304
  ): Promise<IVirtualContributor> {
305
    const virtualContributor = await this.getVirtualContributorOrFail(
306
      virtualContributorID,
307
      {
308
        relations: {
×
309
          profile: true,
×
310
          agent: true,
311
          knowledgeBase: true,
312
        },
313
      }
314
    );
×
315

316
    if (
317
      !virtualContributor.profile ||
318
      !virtualContributor.agent ||
319
      !virtualContributor.knowledgeBase
320
    ) {
321
      throw new RelationshipNotFoundException(
322
        `Unable to load entities for virtual: ${virtualContributor.id} `,
323
        LogContext.COMMUNITY
324
      );
×
325
    }
×
326

327
    await this.profileService.deleteProfile(virtualContributor.profile.id);
328

329
    if (virtualContributor.authorization) {
330
      await this.authorizationPolicyService.delete(
×
331
        virtualContributor.authorization
332
      );
333
    }
334

335
    await this.agentService.deleteAgent(virtualContributor.agent.id);
×
336

337
    const result = await this.virtualContributorRepository.remove(
×
338
      virtualContributor as VirtualContributor
339
    );
340
    result.id = virtualContributorID;
341

342
    if (virtualContributor.aiPersonaID) {
343
      try {
344
        await this.aiPersonaService.deleteAiPersona({
345
          ID: virtualContributor.aiPersonaID,
×
346
        });
347
      } catch (error: any) {
348
        this.logger.error(
349
          {
350
            message: 'Failed to delete external AI Persona.',
351
            aiPersonaID: virtualContributor.aiPersonaID,
352
            virtualContributorID,
353
          },
354
          error?.stack,
355
          LogContext.AI_PERSONA
×
356
        );
×
357
      }
358
    }
359

360
    await this.knowledgeBaseService.delete(virtualContributor.knowledgeBase);
361
    await this.deleteVCInvitations(virtualContributorID);
×
362

363
    return result;
364
  }
365

×
366
  async getVirtualContributor(
367
    virtualContributorID: string,
368
    options?: FindOneOptions<VirtualContributor>
369
  ): Promise<IVirtualContributor | null> {
370
    const virtualContributor = await this.virtualContributorRepository.findOne({
371
      ...options,
372
      where: { ...options?.where, id: virtualContributorID },
373
    });
374

×
375
    return virtualContributor;
376
  }
377

378
  async getVirtualContributorOrFail(
379
    virtualID: string,
380
    options?: FindOneOptions<VirtualContributor>
381
  ): Promise<IVirtualContributor | never> {
×
382
    const virtual = await this.getVirtualContributor(virtualID, options);
×
383
    if (!virtual)
×
384
      throw new EntityNotFoundException(
385
        `Unable to find Virtual with ID: ${virtualID}`,
386
        LogContext.COMMUNITY
387
      );
388
    return virtual;
×
389
  }
×
390

×
391
  async getVirtualContributorAndAgent(
×
392
    virtualID: string
393
  ): Promise<{ virtualContributor: IVirtualContributor; agent: IAgent }> {
394
    const virtualContributor = await this.getVirtualContributorOrFail(
395
      virtualID,
396
      {
397
        relations: { agent: true },
398
      }
399
    );
400

401
    if (!virtualContributor.agent) {
×
402
      throw new EntityNotInitializedException(
403
        `Virtual Contributor Agent not initialized: ${virtualID}`,
404
        LogContext.AUTH
×
405
      );
406
    }
407
    return {
408
      virtualContributor: virtualContributor,
409
      agent: virtualContributor.agent,
410
    };
×
411
  }
412

413
  public async getStorageBucket(
414
    virtualContributorID: string
415
  ): Promise<IStorageBucket> {
416
    const virtualContributor = await this.getVirtualContributorOrFail(
×
417
      virtualContributorID,
418
      {
419
        relations: {
420
          profile: {
421
            storageBucket: true,
422
          },
×
423
        },
×
424
      }
×
425
    );
426
    const storageBucket = virtualContributor?.profile?.storageBucket;
427
    if (!storageBucket) {
428
      throw new RelationshipNotFoundException(
429
        `Unable to find storage bucket to use for Virtual Contributor: ${virtualContributorID}`,
×
430
        LogContext.VIRTUAL_CONTRIBUTOR
431
      );
432
    }
433
    return storageBucket;
434
  }
435

436
  public async refreshBodyOfKnowledge(
×
437
    virtualContributor: IVirtualContributor,
438
    agentInfo: AgentInfo
439
  ): Promise<boolean> {
×
440
    this.logger.verbose?.(
×
441
      `refreshing the body of knowledge ${virtualContributor.id}, by ${agentInfo.userID}`,
×
442
      LogContext.VIRTUAL_CONTRIBUTOR
443
    );
444

445
    // no refresh needed for these types
446
    if (
×
447
      [
×
448
        VirtualContributorBodyOfKnowledgeType.NONE,
449
        VirtualContributorBodyOfKnowledgeType.OTHER,
450
      ].includes(virtualContributor.bodyOfKnowledgeType)
451
    ) {
452
      return Promise.resolve(false);
453
    }
454

×
455
    return this.aiServerAdapter.refreshBodyOfKnowledge(
456
      // Guidance engine doens't have BoK ID for now, so fallback to empty string
457
      // next layer knows what to do
×
458
      virtualContributor.bodyOfKnowledgeID ?? '',
×
459
      virtualContributor.bodyOfKnowledgeType,
×
460
      virtualContributor.aiPersonaID
461
    );
462
  }
463

464
  public async refreshAllBodiesOfKnowledge(agentInfo: AgentInfo) {
465
    const virtualContributors = await this.getVirtualContributors();
×
466
    for (const vc of virtualContributors) {
×
467
      try {
468
        await this.refreshBodyOfKnowledge(vc, agentInfo);
469
      } catch (error: any) {
470
        this.logger.error(
471
          {
472
            message: 'Failed to refresh body of knowledge for VC.',
×
473
            virtualContributorID: vc.id,
×
474
          },
475
          error?.stack,
476
          LogContext.VIRTUAL_CONTRIBUTOR
×
477
        );
478
      }
479
    }
480
    return true;
481
  }
×
482

483
  // TODO: move to store
×
484
  async getVirtualContributors(
×
485
    args: ContributorQueryArgs = {}
486
  ): Promise<IVirtualContributor[]> {
487
    const limit = args.limit;
488
    const shuffle = args.shuffle || false;
489
    this.logger.verbose?.(
490
      `Querying all virtual contributors with limit: ${limit} and shuffle: ${shuffle}`,
×
491
      LogContext.COMMUNITY
492
    );
493

494
    const credentialsFilter = args.filter?.credentials;
495
    let virtualContributors: IVirtualContributor[] = [];
496
    if (credentialsFilter) {
×
497
      virtualContributors = await this.virtualContributorRepository
×
498
        .createQueryBuilder('virtual_contributor')
499
        .leftJoinAndSelect('virtual_contributor.agent', 'agent')
500
        .leftJoinAndSelect('agent.credentials', 'credential')
501
        .where('credential.type IN (:credentialsFilter)')
502
        .setParameters({
503
          credentialsFilter: credentialsFilter,
504
        })
505
        .getMany();
506
    } else {
507
      virtualContributors = await this.virtualContributorRepository.find();
508
    }
509

510
    return limitAndShuffle(virtualContributors, limit, shuffle);
×
511
  }
×
512

×
513
  async save(
514
    virtualContributor: IVirtualContributor
515
  ): Promise<IVirtualContributor> {
×
516
    return this.virtualContributorRepository.save(virtualContributor);
517
  }
×
518

519
  public async getAgent(
520
    virtualContributor: IVirtualContributor
521
  ): Promise<IAgent> {
522
    const virtualContributorWithAgent = await this.getVirtualContributorOrFail(
523
      virtualContributor.id,
×
524
      {
525
        relations: { agent: true },
×
526
      }
×
527
    );
528
    const agent = virtualContributorWithAgent.agent;
529
    if (!agent)
530
      throw new EntityNotInitializedException(
531
        `Virtual Contributor Agent not initialized: ${virtualContributor.id}`,
532
        LogContext.AUTH
533
      );
534

535
    return agent;
×
536
  }
537

×
538
  public async getProvider(
539
    virtualContributor: IVirtualContributor
540
  ): Promise<IContributor> {
541
    const account = await this.virtualContributorLookupService.getAccountOrFail(
542
      virtualContributor.id
543
    );
544

545
    const host = await this.accountLookupService.getHostOrFail(account);
546
    return host;
547
  }
548

549
  async getKnowledgeBaseOrFail(
×
550
    virtualContributor: IVirtualContributor
551
  ): Promise<IKnowledgeBase | never> {
552
    if (virtualContributor.knowledgeBase) {
553
      return virtualContributor.knowledgeBase;
554
    }
×
555
    const virtualContributorWithKnowledgeBase =
556
      await this.getVirtualContributorOrFail(virtualContributor.id, {
557
        relations: {
×
558
          knowledgeBase: true,
×
559
        },
×
560
      });
561
    const knowledgeBase = virtualContributorWithKnowledgeBase.knowledgeBase;
×
562

563
    if (!knowledgeBase) {
564
      throw new EntityNotFoundException(
565
        `Unable to find knowledge base for VirtualContributor: ${virtualContributor.id}`,
566
        LogContext.VIRTUAL_CONTRIBUTOR
567
      );
568
    }
569

570
    return knowledgeBase;
571
  }
572

573
  async getAiPersonaOrFail(
574
    virtualContributor: IVirtualContributor
575
  ): Promise<IAiPersona> {
576
    return await this.aiPersonaService.getAiPersonaOrFail(
577
      virtualContributor.aiPersonaID
578
    );
579
  }
580

581
  async countVirtualContributorsWithCredentials(
582
    credentialCriteria: CredentialsSearchInput
583
  ): Promise<number> {
584
    const credResourceID = credentialCriteria.resourceID || '';
585
    const virtualContributorMatchesCount =
586
      await this.virtualContributorRepository
587
        .createQueryBuilder('virtual')
588
        .leftJoinAndSelect('virtual.agent', 'agent')
589
        .leftJoinAndSelect('agent.credentials', 'credential')
590
        .where('credential.type = :type')
591
        .andWhere('credential.resourceID = :resourceID')
592
        .setParameters({
593
          type: `${credentialCriteria.type}`,
594
          resourceID: credResourceID,
595
        })
596
        .getCount();
597

598
    return virtualContributorMatchesCount;
599
  }
600

601
  async getBodyOfKnowledgeLastUpdated(virtualContributor: IVirtualContributor) {
602
    const aiPersona = await this.getAiPersonaOrFail(virtualContributor);
603
    return aiPersona.bodyOfKnowledgeLastUpdated;
604
  }
605

606
  //adding this to avoid circular dependency between VirtualContributor, Room, and Invitation
607
  private async deleteVCInvitations(contributorID: string) {
608
    const invitations = await this.entityManager.find(Invitation, {
609
      where: { invitedContributorID: contributorID },
610
    });
611
    for (const invitation of invitations) {
612
      if (invitation.authorization) {
613
        await this.authorizationPolicyService.delete(invitation.authorization);
614
      }
615
      await this.entityManager.remove(invitation);
616
    }
617
  }
618
}
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