• 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.93
/src/domain/common/profile/profile.service.ts
1
import { Inject, Injectable, LoggerService } from '@nestjs/common';
41✔
2
import { InjectRepository } from '@nestjs/typeorm';
41✔
3
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
41✔
4
import { FindOneOptions, Repository } from 'typeorm';
41✔
5
import {
41✔
6
  EntityNotFoundException,
7
  EntityNotInitializedException,
8
  NotSupportedException,
9
  ValidationException,
10
} from '@common/exceptions';
41✔
11
import { LogContext, ProfileType } from '@common/enums';
12
import { IReference } from '@domain/common/reference/reference.interface';
41✔
13
import { ReferenceService } from '@domain/common/reference/reference.service';
14
import { ITagset } from '@domain/common/tagset/tagset.interface';
41✔
15
import { TagsetService } from '@domain/common/tagset/tagset.service';
41✔
16
import { Profile } from '@domain/common/profile/profile.entity';
17
import { IProfile } from '@domain/common/profile/profile.interface';
41✔
18
import { AuthorizationPolicy } from '@domain/common/authorization-policy';
41✔
19
import { AuthorizationPolicyService } from '@domain/common/authorization-policy/authorization.policy.service';
41✔
20
import { VisualService } from '@domain/common/visual/visual.service';
21
import { IVisual } from '@domain/common/visual/visual.interface';
22
import { CreateProfileInput, UpdateProfileInput } from './dto';
23
import { CreateReferenceOnProfileInput } from './dto/profile.dto.create.reference';
41✔
24
import { ILocation, LocationService } from '@domain/common/location';
41✔
25
import { VisualType } from '@common/enums/visual.type';
26
import { CreateTagsetInput } from '../tagset';
27
import { ITagsetTemplate } from '../tagset-template/tagset.template.interface';
41✔
28
import { StorageBucketService } from '@domain/storage/storage-bucket/storage.bucket.service';
29
import { IStorageAggregator } from '@domain/storage/storage-aggregator/storage.aggregator.interface';
41✔
30
import { AuthorizationPolicyType } from '@common/enums/authorization.policy.type';
31
import { CreateVisualOnProfileInput } from './dto/profile.dto.create.visual';
32
import { CreateReferenceInput } from '../reference';
41✔
33
import { ProfileDocumentsService } from '@domain/profile-documents/profile.documents.service';
34
import { DEFAULT_AVATAR_SERVICE_URL } from '@services/external/avatar-creator/avatar.creator.service';
35

41✔
36
@Injectable()
37
export class ProfileService {
×
38
  constructor(
×
39
    private authorizationPolicyService: AuthorizationPolicyService,
×
40
    private storageBucketService: StorageBucketService,
×
41
    private tagsetService: TagsetService,
×
42
    private referenceService: ReferenceService,
×
43
    private visualService: VisualService,
×
44
    private locationService: LocationService,
45
    private profileDocumentsService: ProfileDocumentsService,
×
46
    @InjectRepository(Profile)
×
47
    private profileRepository: Repository<Profile>,
48
    @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService
49
  ) {}
50

51
  // Create an empty profile, that the creating entity then has to
52
  // add tagets / visuals to.
53
  public async createProfile(
54
    profileData: CreateProfileInput,
55
    profileType: ProfileType,
56
    storageAggregator: IStorageAggregator
×
57
  ): Promise<IProfile> {
×
58
    const profile: IProfile = Profile.create({
×
59
      description: profileData?.description,
×
60
      tagline: profileData?.tagline,
61
      displayName: profileData?.displayName,
NEW
62
      type: profileType,
×
63
    });
64
    profile.authorization = new AuthorizationPolicy(
65
      AuthorizationPolicyType.PROFILE
66
    );
×
67
    // the next statement fails if it's not saved
68
    profile.storageBucket = this.storageBucketService.createStorageBucket({
69
      storageAggregator: storageAggregator,
70
    });
×
71
    profile.description =
×
72
      await this.profileDocumentsService.reuploadDocumentsInMarkdownToStorageBucket(
×
73
        profile.description ?? '',
74
        profile.storageBucket
75
      );
×
76
    profile.visuals = [];
77
    profile.location = await this.locationService.createLocation(
78
      profileData?.location
×
79
    );
80
    await this.createReferencesOnProfile(profileData?.referencesData, profile);
×
81

×
82
    const tagsetsFromInput = profileData?.tagsets?.map(tagsetData =>
83
      this.tagsetService.createTagsetWithName([], tagsetData)
×
84
    );
85
    profile.tagsets = tagsetsFromInput ?? [];
×
86

87
    return profile;
88
  }
89

90
  private async createReferencesOnProfile(
91
    references: CreateReferenceInput[] | undefined,
92
    profile: IProfile
×
93
  ) {
94
    if (!profile.storageBucket) {
95
      throw new EntityNotInitializedException(
96
        `Storage bucket not initialized on profile: ${profile.id}`,
97
        LogContext.PROFILE
98
      );
99
    }
100
    const newReferences = [];
101
    for (const reference of references ?? []) {
102
      const newReference = this.referenceService.createReference(reference);
×
103
      const newUrl =
×
104
        await this.profileDocumentsService.reuploadFileOnStorageBucket(
105
          newReference.uri,
106
          profile.storageBucket,
×
107
          false
×
108
        );
109
      if (newUrl) {
110
        newReference.uri = newUrl;
×
111
      }
×
112
      newReferences.push(newReference);
113
    }
114
    profile.references = newReferences;
×
115
  }
×
116

117
  async updateProfile(
118
    profileOrig: IProfile,
119
    profileData: UpdateProfileInput
120
  ): Promise<IProfile> {
121
    const profile = await this.getProfileOrFail(profileOrig.id, {
×
122
      relations: {
×
123
        references: true,
124
        tagsets: true,
125
        authorization: true,
126
        location: true,
127
        visuals: true,
128
      },
×
129
    });
×
130

131
    if (profileData.description !== undefined) {
132
      profile.description = profileData.description;
133
    }
134

135
    if (profileData.displayName !== undefined) {
×
136
      profile.displayName = profileData.displayName;
137
    }
138

139
    if (profileData.tagline !== undefined) {
140
      profile.tagline = profileData.tagline;
×
141
    }
142

143
    if (profileData.references) {
144
      profile.references = this.referenceService.updateReferences(
145
        profile.references,
146
        profileData.references
147
      );
148
    }
149

150
    if (profileData.tagsets) {
151
      profile.tagsets = this.tagsetService.updateTagsets(
×
152
        profile.tagsets,
×
153
        profileData.tagsets
×
154
      );
155
    }
156

157
    if (profileData.location && profile.location) {
×
158
      profile.location = await this.locationService.updateLocation(
×
159
        profile.location,
×
160
        profileData.location
161
      );
162
    }
163

164
    return await this.profileRepository.save(profile);
165
  }
×
166

×
167
  async deleteProfile(profileID: string): Promise<IProfile> {
168
    // Note need to load it in with all contained entities so can remove fully
169
    const profile = await this.getProfileOrFail(profileID, {
170
      relations: {
171
        references: true,
×
172
        location: true,
×
173
        tagsets: true,
×
174
        authorization: true,
175
        visuals: true,
176
        storageBucket: true,
177
      },
×
178
    });
×
179

180
    if (profile.tagsets) {
181
      for (const tagset of profile.tagsets) {
×
182
        await this.tagsetService.removeTagset(tagset.id);
×
183
      }
184
    }
×
185

186
    if (profile.references) {
187
      for (const reference of profile.references) {
188
        await this.referenceService.deleteReference({
×
189
          ID: reference.id,
190
        });
191
      }
192
    }
193

194
    if (profile.storageBucket) {
195
      await this.storageBucketService.deleteStorageBucket(
196
        profile.storageBucket.id
197
      );
×
198
    }
199

×
200
    if (profile.visuals) {
×
201
      for (const visual of profile.visuals) {
202
        await this.visualService.deleteVisual({ ID: visual.id });
×
203
      }
×
204
    }
205

×
206
    if (profile.location) {
×
207
      await this.locationService.removeLocation(profile.location);
208
    }
×
209

×
210
    if (profile.authorization)
211
      await this.authorizationPolicyService.delete(profile.authorization);
212

×
213
    return await this.profileRepository.remove(profile as Profile);
214
  }
215

216
  async save(profile: IProfile): Promise<IProfile> {
×
217
    return await this.profileRepository.save(profile);
×
218
  }
219

×
220
  public async addVisualsOnProfile(
×
221
    profile: IProfile,
222
    visualsData: CreateVisualOnProfileInput[] | undefined,
223
    visualTypes: VisualType[]
224
  ): Promise<IProfile> {
225
    if (!profile.visuals || !profile.storageBucket) {
×
226
      throw new EntityNotInitializedException(
227
        `No visuals or no storageBucket found on profile: ${profile.id}`,
228
        LogContext.COMMUNITY
229
      );
230
    }
231
    let visual: IVisual;
232
    for (const visualType of visualTypes) {
×
233
      switch (visualType) {
×
234
        case VisualType.AVATAR:
235
          visual = this.visualService.createVisualAvatar();
236
          break;
×
237
        case VisualType.BANNER:
238
          visual = this.visualService.createVisualBanner();
239
          break;
240
        case VisualType.WHITEBOARD_PREVIEW:
×
241
          visual = this.visualService.createVisualWhiteboardPreview();
242
          break;
×
243
        case VisualType.CARD:
244
          visual = this.visualService.createVisualCard();
245
          break;
246
        case VisualType.BANNER_WIDE:
247
          visual = this.visualService.createVisualBannerWide();
248
          break;
×
249

250
        default:
251
          throw new NotSupportedException(
252
            `Unable to recognise type of visual requested: ${visualTypes}`,
×
253
            LogContext.PROFILE
×
254
          );
255
      }
256
      const providedVisual = visualsData?.find(v => v.name === visualType);
257
      if (providedVisual && providedVisual.uri.length > 0) {
258
        // Only allow external URL if we are creating an Avatar and if it comes from https://eu.ui-avatars.com
×
259
        const allowExternalUrl =
×
260
          visualType === VisualType.AVATAR &&
×
261
          providedVisual.uri.startsWith(DEFAULT_AVATAR_SERVICE_URL);
262

263
        const url =
264
          await this.profileDocumentsService.reuploadFileOnStorageBucket(
265
            providedVisual.uri,
266
            profile.storageBucket,
267
            !allowExternalUrl
268
          );
×
269
        if (url) {
×
270
          visual.uri = url;
271
        } else {
×
272
          this.logger.warn(
273
            `Visual with URL '${providedVisual.uri}' ignored when creating profile ${profile.id}`,
274
            LogContext.PROFILE
275
          );
276
        }
277
      }
278
      profile.visuals.push(visual);
×
279
    }
280
    return profile;
×
281
  }
282

×
283
  async addOrUpdateTagsetOnProfile(
×
284
    profile: IProfile,
285
    tagsetData: CreateTagsetInput
286
  ): Promise<ITagset> {
287
    if (!profile.tagsets) {
×
288
      profile.tagsets = await this.getTagsets(profile);
289
    }
290

291
    const index = profile.tagsets.findIndex(
×
292
      tagset => tagset.name === tagsetData.name
×
293
    );
294

295
    if (index !== -1) {
296
      const newTags = tagsetData.tags ?? [];
×
297
      profile.tagsets[index].tags = Array.from(
298
        new Set([...profile.tagsets[index].tags, ...newTags])
299
      );
×
300

×
301
      return profile.tagsets[index];
302
    } else {
303
      const tagset = this.tagsetService.createTagsetWithName(
304
        profile.tagsets,
305
        tagsetData
×
306
      );
307
      profile.tagsets.push(tagset);
308

309
      return tagset;
×
310
    }
311
  }
312

×
313
  async createReference(
×
314
    referenceInput: CreateReferenceOnProfileInput
315
  ): Promise<IReference> {
316
    const profile = await this.getProfileOrFail(referenceInput.profileID, {
317
      relations: { references: true },
318
    });
×
319

320
    if (!profile.references)
321
      throw new EntityNotInitializedException(
322
        'References not defined',
323
        LogContext.COMMUNITY
324
      );
325
    // check there is not already a reference with the same name
×
326
    for (const reference of profile.references) {
×
327
      if (reference.name === referenceInput.name) {
328
        throw new ValidationException(
329
          `Reference with the provided name already exists: ${referenceInput.name}`,
330
          LogContext.SPACE_ABOUT
331
        );
332
      }
333
    }
×
334
    // If get here then no ref with the same name
335
    const newReference =
336
      await this.referenceService.createReference(referenceInput);
337
    newReference.profile = profile;
×
338

339
    return await this.referenceService.save(newReference);
340
  }
×
341

×
342
  async deleteAllReferencesFromProfile(profileId: string): Promise<void> {
343
    const profile = await this.getProfileOrFail(profileId, {
344
      relations: { references: true },
345
    });
346

×
347
    if (!profile.references)
348
      throw new EntityNotInitializedException(
349
        'References not defined',
350
        LogContext.COMMUNITY
×
351
      );
352

353
    for (const reference of profile.references) {
×
354
      await this.referenceService.deleteReference({
×
355
        ID: reference.id,
356
      });
357
    }
358
  }
359

×
360
  async getProfileOrFail(
361
    profileID: string,
362
    options?: FindOneOptions<Profile>
363
  ): Promise<IProfile | never> {
×
364
    const profile = await Profile.findOne({
365
      ...options,
366
      where: { ...options?.where, id: profileID },
×
367
    });
×
368
    if (!profile)
369
      throw new EntityNotFoundException(
370
        `Profile with id(${profileID}) not found!`,
371
        LogContext.COMMUNITY
372
      );
×
373
    return profile;
374
  }
375

376
  async getReferences(profileInput: IProfile): Promise<IReference[]> {
377
    const profile = await this.getProfileOrFail(profileInput.id, {
378
      relations: { references: true },
379
    });
380
    if (!profile.references) {
381
      throw new EntityNotInitializedException(
×
382
        `Profile not initialized: ${profile.id}`,
383
        LogContext.COMMUNITY
384
      );
385
    }
386
    return profile.references;
×
387
  }
388

389
  async getVisuals(profileInput: IProfile): Promise<IVisual[]> {
390
    const profile = await this.getProfileOrFail(profileInput.id, {
×
391
      relations: { visuals: true },
392
    });
393
    if (!profile.visuals) {
394
      throw new EntityNotInitializedException(
395
        `Profile not initialized: ${profile.id}`,
396
        LogContext.COMMUNITY
397
      );
398
    }
399
    return profile.visuals;
400
  }
×
401

402
  async getVisual(
403
    profileInput: IProfile,
404
    visualType: VisualType
405
  ): Promise<IVisual | undefined> {
406
    const visuals = await this.getVisuals(profileInput);
×
407
    const visual = visuals.find(v => v.name === visualType);
408
    // if (!visual) {
409
    //   throw new EntityNotInitializedException(
410
    //     `Unable to find visual with name '${visualType}' on ${profileInput.id}`,
×
411
    //     LogContext.COMMUNITY
412
    //   );
413
    // }
414
    return visual;
415
  }
416

417
  async getTagsets(profileInput: IProfile): Promise<ITagset[]> {
418
    const profile = await this.getProfileOrFail(profileInput.id, {
419
      relations: { tagsets: true },
×
420
    });
×
421
    if (!profile.tagsets) {
×
422
      throw new EntityNotInitializedException(
423
        `Profile not initialized: ${profile.id}`,
424
        LogContext.COMMUNITY
425
      );
×
426
    }
427
    return profile.tagsets;
428
  }
429

×
430
  async getLocation(profileInput: IProfile): Promise<ILocation> {
431
    const profile = await this.getProfileOrFail(profileInput.id, {
×
432
      relations: { location: true },
433
    });
434
    if (!profile.location) {
435
      throw new EntityNotInitializedException(
436
        `Profile not initialized: ${profile.id}`,
437
        LogContext.COMMUNITY
438
      );
439
    }
×
440
    return profile.location;
441
  }
×
442

443
  async getTagset(profileID: string, tagsetName: string): Promise<ITagset> {
×
444
    const profile = await this.getProfileOrFail(profileID, {
×
445
      relations: { tagsets: true },
×
446
    });
447
    if (!profile.tagsets) {
×
448
      throw new EntityNotInitializedException(
×
449
        `Profile not initialized: ${profile.id}`,
450
        LogContext.COMMUNITY
451
      );
×
452
    }
453
    return this.tagsetService.getTagsetByNameOrFail(
454
      profile.tagsets,
×
455
      tagsetName
456
    );
457
  }
458

×
459
  public convertTagsetTemplatesToCreateTagsetInput(
460
    tagsetTemplates: ITagsetTemplate[]
461
  ): CreateTagsetInput[] {
462
    const result: CreateTagsetInput[] = [];
463
    for (const tagsetTemplate of tagsetTemplates) {
464
      const input: CreateTagsetInput = {
465
        name: tagsetTemplate.name,
466
        type: tagsetTemplate.type,
467
        tagsetTemplate: tagsetTemplate,
468
        tags: tagsetTemplate.defaultSelectedValue
469
          ? [tagsetTemplate.defaultSelectedValue]
470
          : undefined,
471
      };
472
      result.push(input);
473
    }
474
    return result;
475
  }
476
}
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