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

CBIIT / crdc-datahub-ui / 17162208983

22 Aug 2025 05:46PM UTC coverage: 77.659% (+0.02%) from 77.64%
17162208983

Pull #842

github

web-flow
Merge 237e02706 into ddbf13ac6
Pull Request #842: Submission Request Import/Export Fixes

4866 of 5349 branches covered (90.97%)

Branch coverage included in aggregate %.

33 of 33 new or added lines in 9 files covered. (100.0%)

5 existing lines in 1 file now uncovered.

29126 of 38422 relevant lines covered (75.81%)

175.5 hits per line

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

93.08
/src/classes/QuestionnaireExcelMiddleware.ts
1
import { LazyQueryExecFunction } from "@apollo/client";
2
import ExcelJS from "exceljs";
1✔
3
import { cloneDeep, merge, union, some, values } from "lodash";
1✔
4

5
import cancerTypeOptions from "@/config/CancerTypesConfig";
1✔
6
import DataTypes from "@/config/DataTypesConfig";
1✔
7
import { fileTypeExtensions } from "@/config/FileTypeConfig";
1✔
8
import fundingOptions from "@/config/FundingConfig";
1✔
9
import { InitialQuestionnaire } from "@/config/InitialValues";
1✔
10
import { NotApplicableProgram, OtherProgram } from "@/config/ProgramConfig";
1✔
11
import { InitialSections } from "@/config/SectionConfig";
1✔
12
import speciesOptions from "@/config/SpeciesConfig";
1✔
13
import env from "@/env";
1✔
14
import { ListInstitutionsResp, ListOrgsInput, ListOrgsResp } from "@/graphql";
15
import { parseReleaseVersion } from "@/utils/envUtils";
1✔
16
import { Logger } from "@/utils/logger";
1✔
17
import { parseSchemaObject } from "@/utils/zodUtils";
1✔
18

19
import { SectionA, SectionAColumns, SectionASchema } from "./Excel/A/SectionA";
1✔
20
import { SectionB, SectionBColumns, SectionBSchema } from "./Excel/B/SectionB";
1✔
21
import { SectionC, SectionCColumns, SectionCSchema } from "./Excel/C/SectionC";
1✔
22
import { SectionD, SectionDColumns, SectionDSchema } from "./Excel/D/SectionD";
1✔
23
import { MetaKeys } from "./Excel/Metadata/Columns";
24
import { MetadataColumns, MetadataSection } from "./Excel/Metadata/MetadataSection";
1✔
25
import { SectionCtxBase } from "./Excel/SectionBase";
26

27
/**
28
 * An internal template version identifier.
29
 */
30
export const TEMPLATE_VERSION = "1.0";
1✔
31

32
/**
33
 * The names of the HIDDEN sheets used in the Excel workbook.
34
 * Primarily used for hidden lists.
35
 */
36
export const HIDDEN_SHEET_NAMES = {
1✔
37
  institutions: "InstitutionList",
1✔
38
  programs: "ProgramList",
1✔
39
  fileTypes: "FileTypeList",
1✔
40
  cancerTypes: "CancerTypeList",
1✔
41
  speciesOptions: "SubjectSpeciesList",
1✔
42
  fundingAgencies: "FundingAgencyList",
1✔
43
  repositoryDataTypes: "RepositoryDataTypeList",
1✔
44
} as const;
1✔
45

46
/**
47
 * The required dependencies to import or export a Submission Request.
48
 */
49
export type MiddlewareDependencies = {
50
  application?: Omit<Application, "QuestionnaireData">;
51
  getPrograms?: LazyQueryExecFunction<ListOrgsResp, ListOrgsInput>;
52
  getInstitutions?: LazyQueryExecFunction<ListInstitutionsResp, unknown>;
53
};
54

55
/**
56
 * A specialized class to handle importing and exporting of a Submission Request.
57
 * This class is designed to work with QuestionnaireData, allowing for the import and export of data
58
 * via Excel files.
59
 */
60
export class QuestionnaireExcelMiddleware {
1✔
61
  /**
62
   * The internal ExcelJS Workbook object.
63
   * This object is used to create and manipulate the Excel file during the import/export process.
64
   */
65
  private workbook: ExcelJS.Workbook;
71✔
66

67
  /**
68
   * The internal QuestionnaireData object.
69
   * This object is mutated during the import process, but remains immutable for export.
70
   */
71
  private data: QuestionnaireData | RecursivePartial<QuestionnaireData> | null;
71✔
72

73
  /**
74
   * The dependencies required for exporting or importing,
75
   * such as fetching Programs or Institutions.
76
   */
77
  private dependencies: MiddlewareDependencies;
71✔
78

79
  /**
80
   * Creates an instance of the QuestionnaireExcelMiddleware.
81
   *
82
   * @param data The QuestionnaireData object, if applicable, to be exported.
83
   * @param dependencies The import/export dependencies
84
   */
85
  constructor(data: QuestionnaireData | null, dependencies: MiddlewareDependencies) {
71✔
86
    this.workbook = new ExcelJS.Workbook();
71✔
87
    this.data = data ? cloneDeep(data) : null;
71✔
88
    this.dependencies = dependencies;
71✔
89
  }
71✔
90

91
  /**
92
   * A method to serialize the QuestionnaireData object to an Excel file.
93
   * If no `QuestionnaireData` is provided, the Excel file will be generated with no pre-filled values.
94
   *
95
   * @throws Will throw an error if the serialization fails significantly.
96
   * @returns A Promise that resolves to an ArrayBuffer containing the Excel file data.
97
   */
98
  public async serialize(): Promise<ArrayBuffer> {
71✔
99
    this.setMetadataProperties();
×
100

101
    await this.serializeMetadata();
×
102
    await this.serializeSectionA();
×
103
    await this.serializeSectionB();
×
104
    await this.serializeSectionC();
×
105
    await this.serializeSectionD();
×
106

107
    return this.workbook.xlsx.writeBuffer();
×
108
  }
×
109

110
  /**
111
   * A static method to parse the input file and return a QuestionnaireExcelMiddleware instance.
112
   * This method is the inverse of the `serialize` method.
113
   *
114
   * @param fileBuffer The Excel file data to be parsed.
115
   * @param dependencies The dependencies required for parsing the data.
116
   * @throws Will throw an error if the data cannot be parsed or is invalid.
117
   * @returns A new instance of QuestionnaireExcelMiddleware with the parsed data.
118
   */
119
  public static async parse(
71✔
120
    fileBuffer: ArrayBuffer,
×
121
    dependencies: MiddlewareDependencies
×
122
  ): Promise<QuestionnaireExcelMiddleware["data"]> {
×
123
    // Load the workbook from the ArrayBuffer
124
    const workbook = new ExcelJS.Workbook();
×
125
    await workbook.xlsx.load(fileBuffer);
×
126

127
    // Create an instance with null data, then assign the loaded workbook
128
    const middleware = new QuestionnaireExcelMiddleware(null, dependencies);
×
129
    middleware.workbook = workbook;
×
130
    middleware.data = { ...InitialQuestionnaire, sections: [...InitialSections] };
×
131

132
    await middleware.parseMetadata();
×
133
    await middleware.parseSectionA();
×
134
    await middleware.parseSectionB();
×
135
    await middleware.parseSectionC();
×
136
    await middleware.parseSectionD();
×
137

138
    return middleware.data;
×
139
  }
×
140

141
  /**
142
   * Sets the metadata properties for the Excel workbook.
143
   *
144
   * @returns A Promise that resolves when the properties have been set.
145
   */
146
  private async setMetadataProperties(): Promise<void> {
71✔
147
    this.workbook.creator = "crdc-datahub-ui";
1✔
148
    this.workbook.lastModifiedBy = "crdc-datahub-ui";
1✔
149
    this.workbook.title = "CRDC Submission Request";
1✔
150
    this.workbook.subject = "CRDC Submission Request Template";
1✔
151
    this.workbook.company = "National Cancer Institute";
1✔
152
    this.workbook.created = new Date();
1✔
153
    this.workbook.modified = new Date();
1✔
154
  }
1✔
155

156
  /**
157
   * Builds the metadata page for the Excel workbook.
158
   *
159
   * @returns A readonly reference to the created worksheet.
160
   */
161
  private async serializeMetadata(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
162
    const ctx: SectionCtxBase = {
6✔
163
      workbook: this.workbook,
6✔
164
      u: {
6✔
165
        header: (ws: ExcelJS.Worksheet, color?: string) => {
6✔
166
          const r1 = ws.getRow(1);
6✔
167
          r1.font = { bold: true };
6✔
168
          r1.alignment = { horizontal: "center" };
6✔
169
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
6✔
170
          ws.state = "veryHidden";
6✔
171
        },
6✔
172
      },
6✔
173
    };
6✔
174

175
    const metaSection = new MetadataSection({
6✔
176
      application: this.dependencies.application,
6✔
177
      templateVersion: TEMPLATE_VERSION,
6✔
178
      devTier: env.VITE_DEV_TIER || "N/A",
6!
179
      appVersion: parseReleaseVersion(),
6✔
180
    });
6✔
181

182
    return metaSection.serialize(ctx);
6✔
183
  }
6✔
184

185
  /**
186
   * Adds the form section A to the Excel workbook.
187
   *
188
   * @returns A readonly reference to the created worksheet.
189
   */
190
  private async serializeSectionA(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
191
    const ctx: SectionCtxBase = {
15✔
192
      workbook: this.workbook,
15✔
193
      u: {
15✔
194
        header: (ws: ExcelJS.Worksheet, color?: string) => {
15✔
195
          const r1 = ws.getRow(1);
15✔
196
          r1.font = { bold: true };
15✔
197
          r1.alignment = { horizontal: "center" };
15✔
198
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
15✔
199
        },
15✔
200
      },
15✔
201
    };
15✔
202

203
    const sectionA = new SectionA({
15✔
204
      data: this.data as QuestionnaireData,
15✔
205
      institutionSheet: await this.createInstitutionSheet(),
15✔
206
    });
15✔
207

208
    return sectionA.serialize(ctx);
15✔
209
  }
15✔
210

211
  /**
212
   * Serializes Section B data into the Excel workbook.
213
   *
214
   * @returns The serialized worksheet for Section B.
215
   */
216
  private async serializeSectionB(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
217
    const ctx: SectionCtxBase = {
19✔
218
      workbook: this.workbook,
19✔
219
      u: {
19✔
220
        header: (ws: ExcelJS.Worksheet, color?: string) => {
19✔
221
          const r1 = ws.getRow(1);
19✔
222
          r1.font = { bold: true };
19✔
223
          r1.alignment = { horizontal: "center" };
19✔
224
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
19✔
225
        },
19✔
226
      },
19✔
227
    };
19✔
228

229
    const sectionB = new SectionB({
19✔
230
      data: this.data as QuestionnaireData,
19✔
231
      programSheet: await this.createProgramsSheet(),
19✔
232
      fundingAgenciesSheet: await this.createFundingAgencySheet(),
19✔
233
      repositoryDataTypesSheet: await this.createRepositoryDataTypesSheet(),
19✔
234
    });
19✔
235

236
    const sheet = await sectionB.serialize(ctx);
19✔
237

238
    return sheet;
19✔
239
  }
19✔
240

241
  /**
242
   * Adds the form section C to the Excel workbook.
243
   *
244
   * @returns A readonly reference to the created worksheet.
245
   */
246
  private async serializeSectionC(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
247
    const ctx: SectionCtxBase = {
14✔
248
      workbook: this.workbook,
14✔
249
      u: {
14✔
250
        header: (ws: ExcelJS.Worksheet, color?: string) => {
14✔
251
          const r1 = ws.getRow(1);
14✔
252
          r1.font = { bold: true };
14✔
253
          r1.alignment = { horizontal: "center" };
14✔
254
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
14✔
255
        },
14✔
256
      },
14✔
257
    };
14✔
258

259
    const sectionC = new SectionC({
14✔
260
      data: this.data as QuestionnaireData,
14✔
261
      cancerTypesSheet: await this.createCancerTypesSheet(),
14✔
262
      speciesSheet: await this.createSpeciesListSheet(),
14✔
263
    });
14✔
264

265
    return sectionC.serialize(ctx);
14✔
266
  }
14✔
267

268
  private async serializeSectionD(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
269
    const ctx: SectionCtxBase = {
16✔
270
      workbook: this.workbook,
16✔
271
      u: {
16✔
272
        header: (ws: ExcelJS.Worksheet, color?: string) => {
16✔
273
          const r1 = ws.getRow(1);
16✔
274
          r1.font = { bold: true };
16✔
275
          r1.alignment = { horizontal: "center" };
16✔
276
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
16✔
277
        },
16✔
278
      },
16✔
279
    };
16✔
280

281
    const sectionD = new SectionD({
16✔
282
      data: this.data as QuestionnaireData,
16✔
283
      programSheet: await this.createProgramsSheet(),
16✔
284
      fileTypesSheet: await this.createFileTypesSheet(),
16✔
285
    });
16✔
286

287
    const sheet = await sectionD.serialize(ctx);
16✔
288

289
    return sheet;
16✔
290
  }
16✔
291

292
  /**
293
   * Parses the data from the metadata section.
294
   *
295
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
296
   */
297
  private async parseMetadata(): Promise<boolean> {
71✔
298
    const sheet = this.workbook.getWorksheet(MetadataSection.SHEET_NAME);
4✔
299
    if (!sheet) {
4✔
300
      return false;
1✔
301
    }
1✔
302

303
    const data = await this.extractValuesFromWorksheet(sheet);
3✔
304
    const newData = new Map<MetaKeys, Array<unknown>>();
3✔
305

306
    data.forEach((values, key) => {
3✔
307
      const colKey = MetadataColumns.find((col) => col.header === key)?.key;
13✔
308
      newData.set(
13✔
309
        colKey,
13✔
310
        values.map((value) => String(value).trim())
13✔
311
      );
13✔
312
    });
3✔
313

314
    if (newData?.get("devTier")?.[0] !== env.VITE_DEV_TIER) {
4✔
315
      Logger.info("QuestionnaireExcelMiddleware: Received mismatched devTier.", {
1✔
316
        expected: env.VITE_DEV_TIER,
1✔
317
        received: newData?.get("devTier")?.[0],
1✔
318
      });
1✔
319
    }
1✔
320
    if (newData?.get("templateVersion")?.[0] !== TEMPLATE_VERSION) {
4✔
321
      Logger.info("QuestionnaireExcelMiddleware: Received mismatched templateVersion.", {
1✔
322
        expected: TEMPLATE_VERSION,
1✔
323
        received: newData?.get("templateVersion")?.[0],
1✔
324
      });
1✔
325
    }
1✔
326
    if (
3✔
327
      newData?.get("submissionId")?.[0] &&
4✔
328
      newData?.get("submissionId")?.[0] !== this.dependencies?.application?._id
1✔
329
    ) {
4✔
330
      Logger.info("QuestionnaireExcelMiddleware: Received mismatched submissionId.", {
1✔
331
        expected: this.dependencies?.application?._id,
1✔
332
        received: newData?.get("submissionId")?.[0],
1✔
333
      });
1✔
334
    }
1✔
335

336
    // Compare App Version for traceability
337
    const importedAppVersion = newData?.get("appVersion")?.[0] as string;
4✔
338
    const currentAppVersion = parseReleaseVersion();
4✔
339
    if (importedAppVersion && importedAppVersion !== currentAppVersion) {
4✔
340
      Logger.info("QuestionnaireExcelMiddleware: Received mismatched appVersion.", {
1✔
341
        currentVersion: currentAppVersion,
1✔
342
        importedVersion: importedAppVersion,
1✔
343
      });
1✔
344
    }
1✔
345

346
    return true;
3✔
347
  }
4✔
348

349
  /**
350
   * Parses the data from Section A of the Excel workbook.
351
   *
352
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
353
   */
354
  private async parseSectionA(): Promise<boolean> {
71✔
355
    const sheet = this.workbook.getWorksheet(SectionA.SHEET_NAME);
9✔
356
    if (!sheet) {
9✔
357
      Logger.info(`parseSectionA: No sheet found for ${SectionA.SHEET_NAME}. Skipping`);
1✔
358
      return false;
1✔
359
    }
1✔
360

361
    const data = await this.extractValuesFromWorksheet(sheet);
8✔
362
    const newData = new Map();
8✔
363

364
    data.forEach((values, key) => {
8✔
365
      const colKey = SectionAColumns.find((col) => col.header === key)?.key;
40✔
366
      newData.set(
40✔
367
        colKey,
40✔
368
        values.map((value) => String(value).trim())
40✔
369
      );
40✔
370
    });
8✔
371

372
    const newMapping = SectionA.mapValues(newData, {
8✔
373
      institutionSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.institutions),
8✔
374
    });
8✔
375

376
    const result: RecursivePartial<QuestionnaireData> = parseSchemaObject(
8✔
377
      SectionASchema,
8✔
378
      newMapping
8✔
379
    );
8✔
380

381
    const hasPIFields = some(values(result?.pi), (v) => typeof v === "string" && v.trim() !== "");
9✔
382
    const hasPrimaryContactFields = some(
9✔
383
      values(result?.primaryContact),
9✔
384
      (v) => typeof v === "string" && v.trim() !== ""
9✔
385
    );
9✔
386
    const hasAdditionalContactFields = some(result?.additionalContacts || [], (contact) =>
9!
387
      some(values(contact), (v) => typeof v === "string" && v.trim() !== "")
3✔
388
    );
9✔
389

390
    this.data = merge({}, this.data, result);
9✔
391

392
    const isStarted = hasPIFields || hasPrimaryContactFields || hasAdditionalContactFields;
9✔
393
    this.data.sections.find((s) => s.name === "A").status = isStarted
9✔
394
      ? "In Progress"
5✔
395
      : "Not Started";
3✔
396

397
    return true;
9✔
398
  }
9✔
399

400
  /**
401
   * Parses the data from Section B of the Excel workbook.
402
   *
403
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
404
   */
405
  private async parseSectionB(): Promise<boolean> {
71✔
406
    const ws = this.workbook.getWorksheet(SectionB.SHEET_NAME);
9✔
407
    if (!ws) {
9✔
408
      Logger.info("parseSectionB: No sheet found for Section B. Skipping");
1✔
409
      return false;
1✔
410
    }
1✔
411

412
    const data = await this.extractValuesFromWorksheet(ws);
8✔
413
    const newData = new Map();
8✔
414
    // Swap the column headers for the column keys in the mapping
415
    data.forEach((values, key) => {
8✔
416
      const colKey = SectionBColumns.find((col) => col.header === key)?.key;
55✔
417
      newData.set(
55✔
418
        colKey,
55✔
419
        values.map((value) => String(value).trim())
55✔
420
      );
55✔
421
    });
8✔
422
    const newMapping = SectionB.mapValues(newData, {
8✔
423
      programSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs),
8✔
424
    });
8✔
425

426
    const result: RecursivePartial<QuestionnaireData> = parseSchemaObject(
8✔
427
      SectionBSchema,
8✔
428
      newMapping
8✔
429
    );
8✔
430

431
    this.data = merge({}, this.data, result);
8✔
432

433
    const hasProgramId = result?.program?._id?.length > 0;
9✔
434
    const hasProgramName = result?.program?.name?.length > 0;
9✔
435
    const hasProgramAbbreviation = result?.program?.abbreviation?.length > 0;
9✔
436
    const hasStudyName = result?.study?.name?.length > 0;
9✔
437
    const hasStudyAbbreviation = result?.study?.abbreviation?.length > 0;
9✔
438
    const hasStudyDescription = result?.study?.description?.length > 0;
9✔
439
    // SR form creates one funding entry by default
440
    const hasFundingAgency = result?.study?.funding?.[0]?.agency;
9✔
441
    const hasFundingGrantNumbers = result?.study?.funding?.[0]?.grantNumbers;
9✔
442
    const hasFundingNciProgramOfficer = result?.study?.funding?.[0]?.nciProgramOfficer;
9✔
443
    const hasPublications = result?.study?.publications?.length > 0;
9✔
444
    const hasPlannedPublications = result?.study?.plannedPublications?.length > 0;
9✔
445
    const hasRepositories = result?.study?.repositories?.length > 0;
9✔
446

447
    const isStarted =
9✔
448
      hasProgramId ||
9✔
449
      hasProgramName ||
1✔
450
      hasProgramAbbreviation ||
1✔
451
      hasStudyName ||
1✔
452
      hasStudyAbbreviation ||
1✔
453
      hasStudyDescription ||
1✔
454
      hasFundingAgency ||
1✔
455
      hasFundingGrantNumbers ||
1✔
456
      hasFundingNciProgramOfficer ||
1✔
457
      hasPublications ||
1✔
458
      hasPlannedPublications ||
1✔
459
      hasRepositories;
1✔
460
    this.data.sections.find((s) => s.name === "B").status = isStarted
9✔
461
      ? "In Progress"
7✔
462
      : "Not Started";
1✔
463

464
    return true;
9✔
465
  }
9✔
466

467
  /**
468
   * Parses the data from Section C of the Excel workbook.
469
   *
470
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
471
   */
472
  private async parseSectionC(): Promise<boolean> {
71✔
473
    const sheet = this.workbook.getWorksheet(SectionC.SHEET_NAME);
10✔
474
    if (!sheet) {
10✔
475
      Logger.info(`parseSectionC: No sheet found for ${SectionC.SHEET_NAME}. Skipping`);
1✔
476
      return false;
1✔
477
    }
1✔
478

479
    const data = await this.extractValuesFromWorksheet(sheet);
9✔
480
    const newData = new Map();
9✔
481

482
    data.forEach((values, key) => {
9✔
483
      const colKey = SectionCColumns.find((col) => col.header === key)?.key;
40✔
484
      newData.set(
40✔
485
        colKey,
40✔
486
        values.map((value) => String(value).trim())
40✔
487
      );
40✔
488
    });
9✔
489

490
    const newMapping = SectionC.mapValues(newData, {
9✔
491
      cancerTypesSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.cancerTypes),
9✔
492
      speciesSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.speciesOptions),
9✔
493
    });
9✔
494

495
    const result: RecursivePartial<QuestionnaireData> = parseSchemaObject(
9✔
496
      SectionCSchema,
9✔
497
      newMapping
9✔
498
    );
9✔
499

500
    const hasAccessTypes = result?.accessTypes?.length > 0;
10✔
501
    const hasStudyFields = some(
10✔
502
      values(result?.study || {}),
10!
503
      (v) => typeof v === "string" && v.trim() !== ""
10✔
504
    );
10✔
505
    const hasCancerTypes = result?.cancerTypes?.length > 0;
10✔
506
    const hasOtherCancerTypes = result?.otherCancerTypes?.length > 0;
10✔
507
    const hasPreCancerTypes = result?.preCancerTypes?.length > 0;
10✔
508
    const hasSpecies = result?.species?.length > 0;
10✔
509
    const hasOtherSpecies = result?.otherSpeciesOfSubjects?.length > 0;
10✔
510
    const hasNumberOfParticipants = !!result?.numberOfParticipants;
10✔
511

512
    this.data = merge({}, this.data, result);
10✔
513

514
    const isStarted =
10✔
515
      hasAccessTypes ||
10✔
516
      hasStudyFields ||
8✔
517
      hasCancerTypes ||
7✔
518
      hasOtherCancerTypes ||
5✔
519
      hasPreCancerTypes ||
5✔
520
      hasSpecies ||
5✔
521
      hasOtherSpecies ||
4✔
522
      hasNumberOfParticipants;
4✔
523
    this.data.sections.find((s) => s.name === "C").status = isStarted
10✔
524
      ? "In Progress"
6✔
525
      : "Not Started";
3✔
526

527
    return true;
10✔
528
  }
10✔
529

530
  private async parseSectionD(): Promise<boolean> {
71✔
531
    const ws = this.workbook.getWorksheet(SectionD.SHEET_NAME);
6✔
532
    if (!ws) {
6✔
533
      Logger.info("parseSectionD: No sheet found for Section D. Skipping");
1✔
534
      return false;
1✔
535
    }
1✔
536

537
    const data = await this.extractValuesFromWorksheet(ws);
5✔
538
    const newData = new Map();
5✔
539
    // Swap the column headers for the column keys in the mapping
540
    data.forEach((values, key) => {
5✔
541
      const colKey = SectionDColumns.find((col) => col.header === key)?.key;
80✔
542
      newData.set(
80✔
543
        colKey,
80✔
544
        values.map((value) => String(value).trim())
80✔
545
      );
80✔
546
    });
5✔
547
    const newMapping = SectionD.mapValues(newData, {
5✔
548
      programSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs),
5✔
549
      fileTypesSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fileTypes),
5✔
550
    });
5✔
551

552
    const result: RecursivePartial<QuestionnaireData> = parseSchemaObject(
5✔
553
      SectionDSchema,
5✔
554
      newMapping
5✔
555
    );
5✔
556

557
    this.data = merge({}, this.data, result);
5✔
558

559
    const hasTargetedSubmissionDate = result?.targetedSubmissionDate?.length > 0;
6✔
560
    const hasTargetedReleaseDate = result?.targetedReleaseDate?.length > 0;
6✔
561
    const hasDataTypes = result?.dataTypes?.length > 0;
6✔
562
    const hasOtherDataTypes = result?.otherDataTypes?.length > 0;
6✔
563
    const hasFiles = result?.files?.length > 0;
6✔
564
    const hasDataDeIdentified = !!result?.dataDeIdentified;
6✔
565
    const hasSubmitterComment = result?.submitterComment?.length > 0;
6✔
566
    const hasCellLines = !!result?.cellLines;
6✔
567
    const hasModelSystems = !!result?.modelSystems;
6✔
568

569
    const isStarted =
6✔
570
      hasTargetedSubmissionDate ||
6✔
571
      hasTargetedReleaseDate ||
3✔
572
      hasDataTypes ||
3!
573
      hasOtherDataTypes ||
×
574
      hasFiles ||
×
575
      hasDataDeIdentified ||
×
576
      hasSubmitterComment ||
×
577
      hasCellLines ||
×
UNCOV
578
      hasModelSystems;
×
579
    this.data.sections.find((s) => s.name === "D").status = isStarted
6✔
580
      ? "In Progress"
5!
UNCOV
581
      : "Not Started";
×
582

583
    return true;
6✔
584
  }
6✔
585

586
  /**
587
   * Adds a hidden sheet which contains the full list of API provided institutions at function call.
588
   *
589
   * Columns:
590
   * - `A` - Institution ID
591
   * - `B` - Institution Name
592
   *
593
   * @returns An immutable internal reference to the sheet.
594
   */
595
  private async createInstitutionSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
596
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.institutions);
15✔
597
    if (!sheet) {
15✔
598
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.institutions, { state: "veryHidden" });
15✔
599

600
      const institutions = await this.getAPIInstitutions();
15✔
601
      institutions?.forEach((institution, index) => {
15✔
602
        sheet.getCell(`A${index + 1}`).value = institution._id;
3✔
603
        sheet.getCell(`B${index + 1}`).value = institution.name;
3✔
604
      });
15✔
605
    }
15✔
606

607
    return sheet;
15✔
608
  }
15✔
609

610
  /**
611
   * Adds a hidden sheet which contains the full list of API provided programs at function call.
612
   *
613
   * Columns:
614
   * - `A` - Program ID
615
   * - `B` - Program Name
616
   *
617
   * @returns The created worksheet.
618
   */
619
  private async createProgramsSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
620
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs);
34✔
621
    if (!sheet) {
34✔
622
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.programs, { state: "veryHidden" });
34✔
623

624
      const programs = await this.getAPIPrograms();
34✔
625
      const fullPrograms: ProgramInput[] = [NotApplicableProgram, ...programs, OtherProgram];
34✔
626

627
      fullPrograms.forEach((program, index) => {
34✔
628
        const row = index + 1;
69✔
629

630
        sheet.getCell(`A${row}`).value = program._id || "";
69!
631
        sheet.getCell(`B${row}`).value = program.name || "";
69✔
632
        sheet.getCell(`C${row}`).value = program.abbreviation || "";
69✔
633
        sheet.getCell(`D${row}`).value = program.description || "";
69✔
634

635
        // Set the formula for the Program name to default to Program ID if empty
636
        sheet.getCell(`E${row}`).value = {
69✔
637
          formula: `IF(LEN(TRIM(B${row}))>0,B${row},A${row})`,
69✔
638
        };
69✔
639
      });
34✔
640
    }
34✔
641

642
    return sheet;
34✔
643
  }
34✔
644

645
  /**
646
   * Creates a hidden sheet containing the file types.
647
   *
648
   * @returns The created worksheet.
649
   */
650
  private async createFileTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
651
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fileTypes);
16✔
652
    if (!sheet) {
16✔
653
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.fileTypes, { state: "veryHidden" });
16✔
654

655
      Object.keys(fileTypeExtensions)?.forEach((file, index) => {
16✔
656
        sheet.getCell(`A${index + 1}`).value = file;
80✔
657
      });
16✔
658

659
      const allExtensions = union(...Object.values(fileTypeExtensions));
16✔
660
      allExtensions?.forEach((extension, index) => {
16✔
661
        sheet.getCell(`B${index + 1}`).value = extension;
144✔
662
      });
16✔
663
    }
16✔
664

665
    return sheet;
16✔
666
  }
16✔
667

668
  private async createFundingAgencySheet(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
669
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fundingAgencies);
19✔
670
    if (!sheet) {
19✔
671
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.fundingAgencies, {
19✔
672
        state: "veryHidden",
19✔
673
      });
19✔
674

675
      fundingOptions?.forEach((agency, index) => {
19✔
676
        sheet.getCell(`A${index + 1}`).value = agency;
57✔
677
      });
19✔
678
    }
19✔
679

680
    return sheet;
19✔
681
  }
19✔
682

683
  /**
684
   * Creates a sheet with the full list of Cancer Types
685
   *
686
   * @returns A readonly reference to the worksheet.
687
   */
688
  private async createCancerTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
689
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.cancerTypes);
14✔
690
    if (!sheet) {
14✔
691
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.cancerTypes, { state: "veryHidden" });
14✔
692

693
      cancerTypeOptions.forEach((file, index) => {
14✔
694
        sheet.getCell(`A${index + 1}`).value = file;
742✔
695
      });
14✔
696
    }
14✔
697

698
    return sheet;
14✔
699
  }
14✔
700

701
  /**
702
   * Creates a hidden sheet containing the species options.
703
   *
704
   * @returns A readonly reference to the worksheet.
705
   */
706
  private async createSpeciesListSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
707
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.speciesOptions);
14✔
708
    if (!sheet) {
14✔
709
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.speciesOptions, {
14✔
710
        state: "veryHidden",
14✔
711
      });
14✔
712

713
      speciesOptions.forEach((file, index) => {
14✔
714
        sheet.getCell(`A${index + 1}`).value = file;
56✔
715
      });
14✔
716
    }
14✔
717

718
    return sheet;
14✔
719
  }
14✔
720

721
  /**
722
   * Creates a hidden sheet containing the repository data types.
723
   *
724
   * @returns The created worksheet.
725
   */
726
  private async createRepositoryDataTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
71✔
727
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.repositoryDataTypes);
19✔
728
    if (!sheet) {
19✔
729
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.repositoryDataTypes, {
19✔
730
        state: "veryHidden",
19✔
731
      });
19✔
732

733
      const repositoryDataTypeOptions = [
19✔
734
        DataTypes.clinicalTrial.name,
19✔
735
        DataTypes.genomics.name,
19✔
736
        DataTypes.imaging.name,
19✔
737
        DataTypes.proteomics.name,
19✔
738
      ];
739
      repositoryDataTypeOptions.forEach((file, index) => {
19✔
740
        sheet.getCell(`A${index + 1}`).value = file;
76✔
741
      });
19✔
742
    }
19✔
743

744
    return sheet;
19✔
745
  }
19✔
746

747
  /**
748
   * An internal utility to retrieve the Institution List from the dependencies
749
   *
750
   * @note This is error-safe
751
   * @returns The array of institutions
752
   */
753
  private async getAPIInstitutions(): Promise<Institution[]> {
71✔
754
    try {
15✔
755
      const { data } = (await this.dependencies.getInstitutions?.()) || {};
15✔
756
      return data?.listInstitutions?.institutions || [];
15✔
757
    } catch (error) {
15!
758
      return [];
×
UNCOV
759
    }
×
760
  }
15✔
761

762
  /**
763
   * Retrieves the list of programs from the dependencies.
764
   *
765
   * @note This excludes 'readOnly' programs from the program list.
766
   * @returns The array of programs.
767
   */
768
  private async getAPIPrograms(): Promise<Organization[]> {
71✔
769
    try {
34✔
770
      const { data } = (await this.dependencies.getPrograms?.()) || {};
34✔
771
      return (
34✔
772
        (data?.listPrograms?.programs as Organization[])?.filter((program) => !program.readOnly) ||
34✔
773
        []
33✔
774
      );
775
    } catch (error) {
34!
776
      return [];
×
UNCOV
777
    }
×
778
  }
34✔
779

780
  // eslint-disable-next-line class-methods-use-this
781
  private async extractValuesFromWorksheet(
71✔
782
    ws: ExcelJS.Worksheet
33✔
783
  ): Promise<Map<string, Array<unknown>>> {
33✔
784
    const data = new Map<string, Array<unknown>>();
33✔
785
    const headerRow = ws.getRow(1);
33✔
786
    const headers = headerRow.values;
33✔
787

788
    ws.eachRow((row, rowNumber) => {
33✔
789
      if (rowNumber === 1) {
91✔
790
        return;
33✔
791
      }
33✔
792

793
      row.eachCell((cell, colNumber) => {
58✔
794
        const header = headers[colNumber];
289✔
795
        if (!header) {
289!
796
          // Invalid data, ignore
797
          return;
×
UNCOV
798
        }
×
799

800
        const existingValues = data.get(header) || [];
289✔
801
        data.set(header, [...existingValues, cell.value]);
289✔
802
      });
58✔
803
    });
33✔
804

805
    return data;
33✔
806
  }
33✔
807
}
71✔
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