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

alkem-io / server / #8923

19 Dec 2024 10:44AM UTC coverage: 13.948%. First build
#8923

Pull #4763

travis-ci

Pull Request #4763: Handle Visuals, References and attached images into markdowns on Create entities

95 of 4736 branches covered (2.01%)

Branch coverage included in aggregate %.

36 of 106 new or added lines in 16 files covered. (33.96%)

2142 of 11302 relevant lines covered (18.95%)

7.02 hits per line

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

11.49
/src/services/api/input-creator/input.creator.service.ts
1
import { LogContext } from '@common/enums/logging.context';
2
import { validateAndConvertVisualTypeName } from '@common/enums/visual.type';
54✔
3
import { RelationshipNotFoundException } from '@common/exceptions';
54✔
4
import { EntityNotInitializedException } from '@common/exceptions/entity.not.initialized.exception';
54✔
5
import { ICalloutContributionDefaults } from '@domain/collaboration/callout-contribution-defaults/callout.contribution.defaults.interface';
54✔
6
import { CreateCalloutContributionDefaultsInput } from '@domain/collaboration/callout-contribution-defaults/dto/callout.contribution.defaults.dto.create';
7
import { ICalloutFraming } from '@domain/collaboration/callout-framing/callout.framing.interface';
8
import { CreateCalloutFramingInput } from '@domain/collaboration/callout-framing/dto/callout.framing.dto.create';
9
import { ICallout } from '@domain/collaboration/callout/callout.interface';
10
import { CalloutService } from '@domain/collaboration/callout/callout.service';
11
import { CreateCalloutInput } from '@domain/collaboration/callout/dto/callout.dto.create';
54✔
12
import { ICalloutsSet } from '@domain/collaboration/callouts-set/callouts.set.interface';
13
import { CreateCalloutsSetInput } from '@domain/collaboration/callouts-set/dto/callouts.set.dto.create';
14
import { CollaborationService } from '@domain/collaboration/collaboration/collaboration.service';
15
import { CreateCollaborationInput } from '@domain/collaboration/collaboration/dto/collaboration.dto.create';
54✔
16
import { CreateInnovationFlowInput } from '@domain/collaboration/innovation-flow/dto/innovation.flow.dto.create';
17
import { IInnovationFlow } from '@domain/collaboration/innovation-flow/innovation.flow.interface';
54✔
18
import { CreateLocationInput } from '@domain/common/location/dto/location.dto.create';
19
import { ILocation } from '@domain/common/location/location.interface';
54✔
20
import { CreateProfileInput } from '@domain/common/profile/dto/profile.dto.create';
21
import { CreateVisualOnProfileInput } from '@domain/common/profile/dto/profile.dto.create.visual';
22
import { IProfile } from '@domain/common/profile/profile.interface';
23
import { CreateReferenceInput } from '@domain/common/reference/dto/reference.dto.create';
24
import { IReference } from '@domain/common/reference/reference.interface';
25
import { CreateTagsetInput } from '@domain/common/tagset/dto/tagset.dto.create';
26
import { ITagset } from '@domain/common/tagset/tagset.interface';
27
import { IVisual } from '@domain/common/visual';
28
import { CreateWhiteboardInput } from '@domain/common/whiteboard/dto/whiteboard.dto.create';
29
import { IWhiteboard } from '@domain/common/whiteboard/whiteboard.interface';
30
import { ICommunityGuidelines } from '@domain/community/community-guidelines/community.guidelines.interface';
31
import { CreateCommunityGuidelinesInput } from '@domain/community/community-guidelines/dto/community.guidelines.dto.create';
32
import { Injectable } from '@nestjs/common';
33
import { IClassification } from '@domain/common/classification/classification.interface';
34
import { CreateClassificationInput } from '@domain/common/classification/dto/classification.dto.create';
35
import { SpaceLookupService } from '@domain/space/space.lookup/space.lookup.service';
36
import { CreateTemplateContentSpaceInput } from '@domain/template/template-content-space/dto/template.content.space.dto.create';
54✔
37
import { CreateSpaceAboutInput, ISpaceAbout } from '@domain/space/space.about';
38
import { EntityManager } from 'typeorm';
39
import { InjectEntityManager } from '@nestjs/typeorm';
54✔
40
import { TemplateContentSpace } from '@domain/template/template-content-space/template.content.space.entity';
41
import { CreateInnovationFlowStateInput } from '@domain/collaboration/innovation-flow-state/dto';
×
42
import { IInnovationFlowState } from '@domain/collaboration/innovation-flow-state/innovation.flow.state.interface';
×
43
import { IMemo } from '@domain/common/memo/memo.interface';
×
44
import { CreateMemoInput } from '@domain/common/memo/dto/memo.dto.create';
×
45
import { yjsStateToMarkdown } from '@domain/common/memo/conversion';
46

47
@Injectable()
48
export class InputCreatorService {
49
  constructor(
50
    private collaborationService: CollaborationService,
×
51
    private spaceLookupService: SpaceLookupService,
×
52
    private calloutService: CalloutService,
×
53
    @InjectEntityManager('default')
54
    private entityManager: EntityManager
×
55
  ) {}
56

57
  public async buildCreateCalloutInputsFromCallouts(
58
    callouts: ICallout[]
59
  ): Promise<CreateCalloutInput[]> {
60
    const result: CreateCalloutInput[] = [];
×
61
    for (const callout of callouts) {
62
      result.push(await this.buildCreateCalloutInputFromCallout(callout.id));
63
    }
64
    return result;
65
  }
66

67
  public async buildCreateCalloutInputFromCallout(
68
    calloutID: string
69
  ): Promise<CreateCalloutInput> {
70
    const callout = await this.calloutService.getCalloutOrFail(calloutID, {
71
      relations: {
72
        contributionDefaults: true,
73
        classification: {
74
          tagsets: true,
75
        },
76
        framing: {
77
          profile: {
78
            tagsets: true,
×
79
            references: true,
×
80
            visuals: true,
81
          },
82
          whiteboard: {
83
            profile: {
84
              visuals: true,
85
            },
×
86
          },
87
          link: {
88
            profile: true,
89
          },
90
          memo: {
91
            profile: true,
92
          },
93
        },
94
      },
95
    });
×
96
    if (
97
      !callout.framing ||
98
      !callout.framing.profile ||
×
99
      !callout.framing.profile.tagsets ||
100
      !callout.contributionDefaults ||
101
      !callout.settings ||
102
      !callout.classification
103
    ) {
104
      throw new EntityNotInitializedException(
105
        `Missing relation on callout: ${calloutID}`,
106
        LogContext.INPUT_CREATOR,
107
        {
108
          cause: 'Relation for Callout not loaded',
109
          calloutId: calloutID,
110
        }
111
      );
112
    }
113

114
    return {
115
      nameID: callout.nameID,
116
      classification: this.buildCreateClassificationInputFromClassification(
117
        callout.classification
118
      ),
119
      framing: this.buildCreateCalloutFramingInputFromCalloutFraming(
120
        callout.framing
121
      ),
122
      settings: callout.settings,
×
123
      contributionDefaults:
124
        this.buildCreateCalloutContributionDefaultsInputFromCalloutContributionDefaults(
125
          callout.contributionDefaults
126
        ),
127
      sortOrder: callout.sortOrder,
128
    };
129
  }
130

131
  public async buildCreateCalloutsSetInputFromCalloutsSet(
132
    calloutsSet: ICalloutsSet
133
  ): Promise<CreateCalloutsSetInput> {
134
    if (!calloutsSet.callouts) {
135
      throw new RelationshipNotFoundException(
136
        `CalloutsSet ${calloutsSet.id} is missing a relation`,
137
        LogContext.INPUT_CREATOR
138
      );
139
    }
×
140

×
141
    const calloutInputs: CreateCalloutInput[] = [];
142
    for (const callout of calloutsSet.callouts) {
143
      calloutInputs.push(
144
        await this.buildCreateCalloutInputFromCallout(callout.id)
145
      );
146
    }
×
147

×
148
    const result: CreateCalloutsSetInput = {
×
149
      calloutsData: calloutInputs,
150
    };
151

152
    return result;
153
  }
×
154

×
155
  public async buildCreateTemplateContentSpaceInputFromSpace(
156
    spaceID: string,
157
    recursive: boolean = true
158
  ): Promise<CreateTemplateContentSpaceInput> {
159
    const space = await this.spaceLookupService.getSpaceOrFail(spaceID, {
160
      relations: {
161
        collaboration: true,
162
        subspaces: true,
163
        about: {
×
164
          profile: {
165
            references: true,
166
            visuals: true,
167
            location: true,
168
            tagsets: true,
169
          },
×
170
          guidelines: {
×
171
            profile: {
172
              references: true,
173
            },
174
          },
175
        },
176
      },
×
177
    });
178
    if (!space.collaboration || !space.about || !space.subspaces) {
179
      throw new RelationshipNotFoundException(
180
        `Space ${space.id} is missing a relation`,
181
        LogContext.INPUT_CREATOR
182
      );
183
    }
×
184

185
    const collaborationInput =
186
      await this.buildCreateCollaborationInputFromCollaboration(
187
        space.collaboration.id
188
      );
189
    const aboutInput = this.buildCreateSpaceAboutInputFromSpaceAbout(
×
190
      space.about
191
    );
192
    const subspacesInput: CreateTemplateContentSpaceInput[] = [];
193
    if (recursive) {
194
      for (const subspace of space.subspaces) {
×
195
        const subspaceInput =
196
          await this.buildCreateTemplateContentSpaceInputFromSpace(
197
            subspace.id,
198
            recursive
199
          );
200
        subspacesInput.push(subspaceInput);
×
201
      }
×
202
    }
203

204
    const result: CreateTemplateContentSpaceInput = {
205
      collaborationData: collaborationInput,
206
      about: aboutInput,
207
      level: space.level,
208
      settings: space.settings,
209
      subspaces: subspacesInput,
210
    };
×
211

×
212
    return result;
213
  }
214

215
  public async buildCreateTemplateContentSpaceInputFromContentSpace(
216
    contentSpaceID: string
217
  ): Promise<CreateTemplateContentSpaceInput> {
218
    const contentSpace = await this.entityManager.findOneOrFail(
219
      TemplateContentSpace,
220
      {
×
221
        where: {
222
          id: contentSpaceID,
223
        },
224
        relations: {
225
          subspaces: true,
226
          collaboration: true,
227
          about: {
228
            profile: {
229
              references: true,
230
              visuals: true,
231
              location: true,
×
232
              tagsets: true,
×
233
            },
234
            guidelines: {
×
235
              profile: {
236
                references: true,
237
              },
238
            },
×
239
          },
240
        },
241
      }
242
    );
243

244
    if (
×
245
      !contentSpace.collaboration ||
246
      !contentSpace.about ||
247
      !contentSpace.subspaces
248
    ) {
249
      throw new RelationshipNotFoundException(
250
        `ContentSpace ${contentSpace.id} is missing a relation`,
251
        LogContext.INPUT_CREATOR
252
      );
253
    }
254

×
255
    const collaborationInput =
256
      await this.buildCreateCollaborationInputFromCollaboration(
257
        contentSpace.collaboration.id
258
      );
259
    const aboutInput = this.buildCreateSpaceAboutInputFromSpaceAbout(
260
      contentSpace.about
261
    );
262

263
    const subspacesInput: CreateTemplateContentSpaceInput[] = [];
264
    for (const subspace of contentSpace.subspaces) {
265
      const subspaceInput =
266
        await this.buildCreateTemplateContentSpaceInputFromContentSpace(
267
          subspace.id
268
        );
269
      subspacesInput.push(subspaceInput);
270
    }
271

272
    const result: CreateTemplateContentSpaceInput = {
×
273
      collaborationData: collaborationInput,
×
274
      about: aboutInput,
275
      level: contentSpace.level,
276
      settings: contentSpace.settings,
277
      subspaces: subspacesInput,
278
    };
279

280
    return result;
281
  }
282

283
  public async buildCreateCollaborationInputFromCollaboration(
284
    collaborationID: string
285
  ): Promise<CreateCollaborationInput> {
286
    const collaboration =
×
287
      await this.collaborationService.getCollaborationOrFail(collaborationID, {
×
288
        relations: {
×
289
          calloutsSet: {
×
290
            callouts: true,
291
          },
×
292
          innovationFlow: {
293
            profile: true,
294
            states: true,
295
          },
296
        },
297
      });
×
298
    if (
299
      !collaboration.calloutsSet ||
300
      !collaboration.calloutsSet.callouts ||
301
      !collaboration.innovationFlow ||
302
      !collaboration.innovationFlow.states
303
    ) {
304
      throw new RelationshipNotFoundException(
305
        `Collaboration ${collaboration.id} is missing a relation`,
306
        LogContext.INPUT_CREATOR
NEW
307
      );
×
NEW
308
    }
×
NEW
309

×
NEW
310
    const calloutsSetInput =
×
311
      await this.buildCreateCalloutsSetInputFromCalloutsSet(
312
        collaboration.calloutsSet
313
      );
314

NEW
315
    const result: CreateCollaborationInput = {
×
316
      calloutsSetData: calloutsSetInput,
317
      innovationFlowData: this.buildCreateInnovationFlowInputFromInnovationFlow(
318
        collaboration.innovationFlow
319
      ),
×
320
    };
321

322
    return result;
323
  }
324

325
  public buildCreateInnovationFlowInputFromInnovationFlow(
326
    innovationFlow: IInnovationFlow
327
  ): CreateInnovationFlowInput {
328
    if (
329
      !innovationFlow.states ||
330
      !innovationFlow.settings ||
×
331
      !innovationFlow.profile
×
332
    ) {
×
333
      throw new EntityNotInitializedException(
×
334
        `Template ${innovationFlow.id} is missing relation`,
335
        LogContext.INPUT_CREATOR
×
336
      );
337
    }
338

339
    const currentState = innovationFlow.states.find(
340
      state => state.id === innovationFlow.currentStateID
341
    );
342
    // Note: no profile currently present, so use the one from the template for now
343
    const result: CreateInnovationFlowInput = {
344
      settings: innovationFlow.settings,
345
      profile: {
346
        displayName: innovationFlow.profile.displayName,
347
        description: innovationFlow.profile.description,
348
      },
349
      states: this.buildCreateInnovationFlowStateInputFromInnovationFlowState(
350
        innovationFlow.states
351
      ),
352
      currentStateDisplayName: currentState?.displayName ?? '',
353
    };
354
    return result;
355
  }
356

357
  public buildCreateInnovationFlowStateInputFromInnovationFlowState(
358
    states: IInnovationFlowState[]
359
  ): CreateInnovationFlowStateInput[] {
360
    const result: CreateInnovationFlowStateInput[] = [];
361
    for (const state of states) {
362
      result.push({
363
        displayName: state.displayName,
364
        description: state.description,
365
        settings: state.settings,
366
        sortOrder: state.sortOrder,
367
      });
368
    }
369
    return result;
370
  }
371

372
  public buildCreateCommunityGuidelinesInputFromCommunityGuidelines(
373
    communityGuidelines: ICommunityGuidelines
374
  ): CreateCommunityGuidelinesInput {
375
    const result: CreateCommunityGuidelinesInput = {
376
      profile: this.buildCreateProfileInputFromProfile(
377
        communityGuidelines.profile
378
      ),
379
    };
380
    return result;
381
  }
382

383
  public buildCreateWhiteboardInputFromWhiteboard(
384
    whiteboard?: IWhiteboard
385
  ): CreateWhiteboardInput | undefined {
386
    if (!whiteboard) return undefined;
387
    return {
388
      profile: this.buildCreateProfileInputFromProfile(whiteboard.profile),
389
      content: whiteboard.content,
390
      nameID: whiteboard.nameID,
391
      previewSettings: whiteboard.previewSettings,
392
    };
393
  }
394

395
  public buildCreateMemoInputFromMemo(memo: IMemo): CreateMemoInput {
396
    if (!memo.profile) {
397
      throw new EntityNotInitializedException(
398
        'Memo not fully initialised',
399
        LogContext.INPUT_CREATOR,
400
        {
401
          cause: 'Relation "profile" for memo not loaded',
402
          memoId: memo.id,
403
        }
404
      );
405
    }
406
    return {
407
      nameID: memo.nameID,
408
      markdown: memo.content ? yjsStateToMarkdown(memo.content) : undefined,
409
      profile: this.buildCreateProfileInputFromProfile(memo.profile),
410
    };
411
  }
412

413
  public buildCreateSpaceAboutInputFromSpaceAbout(
414
    spaceAbout: ISpaceAbout
415
  ): CreateSpaceAboutInput {
416
    const result: CreateSpaceAboutInput = {
417
      profileData: this.buildCreateProfileInputFromProfile(spaceAbout.profile),
418
      who: spaceAbout.who,
419
      why: spaceAbout.why,
420
      guidelines: spaceAbout.guidelines
421
        ? this.buildCreateCommunityGuidelinesInputFromCommunityGuidelines(
422
            spaceAbout.guidelines
423
          )
424
        : undefined,
425
    };
426

427
    return result;
428
  }
429

430
  private buildCreateCalloutFramingInputFromCalloutFraming(
431
    calloutFraming: ICalloutFraming
432
  ): CreateCalloutFramingInput {
433
    if (!calloutFraming.profile) {
434
      throw new EntityNotInitializedException(
435
        'CalloutFraming not fully initialised',
436
        LogContext.INPUT_CREATOR,
437
        {
438
          cause: 'Relation for callout framing not loaded',
439
          calloutFramingId: calloutFraming.id,
440
        }
441
      );
442
    }
443
    return {
444
      type: calloutFraming.type,
445
      profile: this.buildCreateProfileInputFromProfile(calloutFraming.profile),
446
      whiteboard: this.buildCreateWhiteboardInputFromWhiteboard(
447
        calloutFraming.whiteboard
448
      ),
449
      link: calloutFraming.link?.profile
450
        ? {
451
            profile: this.buildCreateProfileInputFromProfile(
452
              calloutFraming.link.profile
453
            ),
454
            uri: calloutFraming.link.uri,
455
          }
456
        : undefined,
457
      memo: calloutFraming.memo
458
        ? this.buildCreateMemoInputFromMemo(calloutFraming.memo)
459
        : undefined,
460
    };
461
  }
462

463
  private buildCreateCalloutContributionDefaultsInputFromCalloutContributionDefaults(
464
    calloutContributionDefaults?: ICalloutContributionDefaults
465
  ): CreateCalloutContributionDefaultsInput | undefined {
466
    if (!calloutContributionDefaults) {
467
      return undefined;
468
    }
469
    const result: CreateCalloutContributionDefaultsInput = {
470
      defaultDisplayName: calloutContributionDefaults.defaultDisplayName,
471
      postDescription: calloutContributionDefaults.postDescription,
472
      whiteboardContent: calloutContributionDefaults.whiteboardContent,
473
    };
474
    return result;
475
  }
476

477
  private buildCreateClassificationInputFromClassification(
478
    classification: IClassification
479
  ): CreateClassificationInput {
480
    return {
481
      tagsets: this.buildCreateTagsetsInputFromTagsets(classification.tagsets),
482
    };
483
  }
484

485
  public buildCreateProfileInputFromProfile(
486
    profile: IProfile
487
  ): CreateProfileInput {
488
    if (!profile) {
489
      throw new EntityNotInitializedException(
490
        'Profile not fully initialized',
491
        LogContext.INPUT_CREATOR,
492
        {
493
          cause: 'Profile relation not loaded',
494
        }
495
      );
496
    }
497
    return {
498
      description: profile.description,
499
      displayName: profile.displayName,
500
      location: this.buildCreateLocationInputFromLocation(profile.location),
501
      referencesData: this.buildCreateReferencesInputFromReferences(
502
        profile.references
503
      ),
504
      tagline: profile.tagline,
505
      tagsets: this.buildCreateTagsetsInputFromTagsets(profile.tagsets),
506
      visuals: this.buildCreateVisualsOnProfileInputFromVisuals(
507
        profile.visuals
508
      ),
509
    };
510
  }
511

512
  private buildCreateLocationInputFromLocation(
513
    location?: ILocation
514
  ): CreateLocationInput | undefined {
515
    if (!location) return undefined;
516
    return {
517
      city: location.city,
518
      country: location.country,
519
      addressLine1: location.addressLine1,
520
      addressLine2: location.addressLine2,
521
      postalCode: location.postalCode,
522
      stateOrProvince: location.stateOrProvince,
523
    };
524
  }
525

526
  private buildCreateReferencesInputFromReferences(
527
    references?: IReference[]
528
  ): CreateReferenceInput[] {
529
    const result: CreateReferenceInput[] = [];
530
    if (!references) return result;
531
    for (const reference of references) {
532
      result.push(this.buildCreateReferenceInputFromReference(reference));
533
    }
534
    return result;
535
  }
536

537
  private buildCreateReferenceInputFromReference(
538
    reference: IReference
539
  ): CreateReferenceInput {
540
    return {
541
      name: reference.name,
542
      uri: reference.uri,
543
      description: reference.description,
544
    };
545
  }
546

547
  private buildCreateVisualsOnProfileInputFromVisuals(
548
    visuals?: IVisual[]
549
  ): CreateVisualOnProfileInput[] {
550
    const result: CreateVisualOnProfileInput[] = [];
551
    if (!visuals) return result;
552
    for (const visual of visuals) {
553
      result.push({
554
        name: validateAndConvertVisualTypeName(visual.name),
555
        uri: visual.uri,
556
      });
557
    }
558
    return result;
559
  }
560

561
  public buildCreateTagsetInputFromTagset(tagset: ITagset): CreateTagsetInput {
562
    return {
563
      name: tagset.name,
564
      tags: tagset.tags,
565
      type: tagset.type,
566
      tagsetTemplate: tagset.tagsetTemplate,
567
    };
568
  }
569

570
  public buildCreateTagsetsInputFromTagsets(
571
    tagsets?: ITagset[]
572
  ): CreateTagsetInput[] {
573
    const tagsetInputs: CreateTagsetInput[] = [];
574
    if (!tagsets) return tagsetInputs;
575
    for (const tagset of tagsets) {
576
      tagsetInputs.push(this.buildCreateTagsetInputFromTagset(tagset));
577
    }
578
    return tagsetInputs;
579
  }
580
}
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