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

CBIIT / crdc-datahub-ui / 17136790327

21 Aug 2025 07:19PM UTC coverage: 77.635% (+1.7%) from 75.941%
17136790327

push

github

web-flow
Merge pull request #806 from CBIIT/feat/Submission-Request-Export

Submission Request Excel Import & Export CRDCDH-3033, CRDCDH-3045, CRDCDH-3063

4854 of 5337 branches covered (90.95%)

Branch coverage included in aggregate %.

3214 of 3490 new or added lines in 33 files covered. (92.09%)

7 existing lines in 3 files now uncovered.

29088 of 38383 relevant lines covered (75.78%)

175.29 hits per line

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

92.91
/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 { Logger } from "@/utils/logger";
1✔
16
import { parseSchemaObject } from "@/utils/zodUtils";
1✔
17

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
137
    return middleware.data;
×
NEW
138
  }
×
139

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

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

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

180
    return metaSection.serialize(ctx);
4✔
181
  }
4✔
182

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

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

206
    return sectionA.serialize(ctx);
15✔
207
  }
15✔
208

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

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

234
    const sheet = await sectionB.serialize(ctx);
19✔
235

236
    return sheet;
19✔
237
  }
19✔
238

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

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

263
    return sectionC.serialize(ctx);
14✔
264
  }
14✔
265

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

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

285
    const sheet = await sectionD.serialize(ctx);
16✔
286

287
    return sheet;
16✔
288
  }
16✔
289

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

301
    const data = await this.extractValuesFromWorksheet(sheet);
1✔
302
    const newData = new Map<MetaKeys, Array<unknown>>();
1✔
303

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

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

334
    return true;
1✔
335
  }
2✔
336

337
  /**
338
   * Parses the data from Section A of the Excel workbook.
339
   *
340
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
341
   */
342
  private async parseSectionA(): Promise<boolean> {
69✔
343
    const sheet = this.workbook.getWorksheet(SectionA.SHEET_NAME);
9✔
344
    if (!sheet) {
9✔
345
      Logger.info(`parseSectionA: No sheet found for ${SectionA.SHEET_NAME}. Skipping`);
1✔
346
      return false;
1✔
347
    }
1✔
348

349
    const data = await this.extractValuesFromWorksheet(sheet);
8✔
350
    const newData = new Map();
8✔
351

352
    data.forEach((values, key) => {
8✔
353
      const colKey = SectionAColumns.find((col) => col.header === key)?.key;
40✔
354
      newData.set(
40✔
355
        colKey,
40✔
356
        values.map((value) => String(value).trim())
40✔
357
      );
40✔
358
    });
8✔
359

360
    const newMapping = SectionA.mapValues(newData, {
8✔
361
      institutionSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.institutions),
8✔
362
    });
8✔
363

364
    const result: RecursivePartial<QuestionnaireData> = parseSchemaObject(
8✔
365
      SectionASchema,
8✔
366
      newMapping
8✔
367
    );
8✔
368

369
    const hasPIFields = some(values(result?.pi), (v) => typeof v === "string" && v.trim() !== "");
9✔
370
    const hasPrimaryContactFields = some(
9✔
371
      values(result?.primaryContact),
9✔
372
      (v) => typeof v === "string" && v.trim() !== ""
9✔
373
    );
9✔
374
    const hasAdditionalContactFields = some(result?.additionalContacts || [], (contact) =>
9!
375
      some(values(contact), (v) => typeof v === "string" && v.trim() !== "")
3✔
376
    );
9✔
377

378
    this.data = merge({}, this.data, result);
9✔
379

380
    const isStarted = hasPIFields || hasPrimaryContactFields || hasAdditionalContactFields;
9✔
381
    this.data.sections.find((s) => s.name === "A").status = isStarted
9✔
382
      ? "In Progress"
5✔
383
      : "Not Started";
3✔
384

385
    return true;
9✔
386
  }
9✔
387

388
  /**
389
   * Parses the data from Section B of the Excel workbook.
390
   *
391
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
392
   */
393
  private async parseSectionB(): Promise<boolean> {
69✔
394
    const ws = this.workbook.getWorksheet(SectionB.SHEET_NAME);
9✔
395
    if (!ws) {
9✔
396
      Logger.info("parseSectionB: No sheet found for Section B. Skipping");
1✔
397
      return false;
1✔
398
    }
1✔
399

400
    const data = await this.extractValuesFromWorksheet(ws);
8✔
401
    const newData = new Map();
8✔
402
    // Swap the column headers for the column keys in the mapping
403
    data.forEach((values, key) => {
8✔
404
      const colKey = SectionBColumns.find((col) => col.header === key)?.key;
55✔
405
      newData.set(
55✔
406
        colKey,
55✔
407
        values.map((value) => String(value).trim())
55✔
408
      );
55✔
409
    });
8✔
410
    const newMapping = SectionB.mapValues(newData, {
8✔
411
      programSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs),
8✔
412
    });
8✔
413

414
    const result: RecursivePartial<QuestionnaireData> = parseSchemaObject(
8✔
415
      SectionBSchema,
8✔
416
      newMapping
8✔
417
    );
8✔
418

419
    this.data = merge({}, this.data, result);
8✔
420

421
    const hasProgramId = result?.program?._id?.length > 0;
9✔
422
    const hasProgramName = result?.program?.name?.length > 0;
9✔
423
    const hasProgramAbbreviation = result?.program?.abbreviation?.length > 0;
9✔
424
    const hasStudyName = result?.study?.name?.length > 0;
9✔
425
    const hasStudyAbbreviation = result?.study?.abbreviation?.length > 0;
9✔
426
    const hasStudyDescription = result?.study?.description?.length > 0;
9✔
427
    // SR form creates one funding entry by default
428
    const hasFundingAgency = result?.study?.funding?.[0]?.agency;
9✔
429
    const hasFundingGrantNumbers = result?.study?.funding?.[0]?.grantNumbers;
9✔
430
    const hasFundingNciProgramOfficer = result?.study?.funding?.[0]?.nciProgramOfficer;
9✔
431
    const hasPublications = result?.study?.publications?.length > 0;
9✔
432
    const hasPlannedPublications = result?.study?.plannedPublications?.length > 0;
9✔
433
    const hasRepositories = result?.study?.repositories?.length > 0;
9✔
434

435
    const isStarted =
9✔
436
      hasProgramId ||
9✔
437
      hasProgramName ||
1✔
438
      hasProgramAbbreviation ||
1✔
439
      hasStudyName ||
1✔
440
      hasStudyAbbreviation ||
1✔
441
      hasStudyDescription ||
1✔
442
      hasFundingAgency ||
1✔
443
      hasFundingGrantNumbers ||
1✔
444
      hasFundingNciProgramOfficer ||
1✔
445
      hasPublications ||
1✔
446
      hasPlannedPublications ||
1✔
447
      hasRepositories;
1✔
448
    this.data.sections.find((s) => s.name === "B").status = isStarted
9✔
449
      ? "In Progress"
7✔
450
      : "Not Started";
1✔
451

452
    return true;
9✔
453
  }
9✔
454

455
  /**
456
   * Parses the data from Section C of the Excel workbook.
457
   *
458
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
459
   */
460
  private async parseSectionC(): Promise<boolean> {
69✔
461
    const sheet = this.workbook.getWorksheet(SectionC.SHEET_NAME);
10✔
462
    if (!sheet) {
10✔
463
      Logger.info(`parseSectionC: No sheet found for ${SectionC.SHEET_NAME}. Skipping`);
1✔
464
      return false;
1✔
465
    }
1✔
466

467
    const data = await this.extractValuesFromWorksheet(sheet);
9✔
468
    const newData = new Map();
9✔
469

470
    data.forEach((values, key) => {
9✔
471
      const colKey = SectionCColumns.find((col) => col.header === key)?.key;
40✔
472
      newData.set(
40✔
473
        colKey,
40✔
474
        values.map((value) => String(value).trim())
40✔
475
      );
40✔
476
    });
9✔
477

478
    const newMapping = SectionC.mapValues(newData, {
9✔
479
      cancerTypesSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.cancerTypes),
9✔
480
      speciesSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.speciesOptions),
9✔
481
    });
9✔
482

483
    const result: RecursivePartial<QuestionnaireData> = parseSchemaObject(
9✔
484
      SectionCSchema,
9✔
485
      newMapping
9✔
486
    );
9✔
487

488
    const hasAccessTypes = result?.accessTypes?.length > 0;
10✔
489
    const hasStudyFields = some(
10✔
490
      values(result?.study || {}),
10!
491
      (v) => typeof v === "string" && v.trim() !== ""
10✔
492
    );
10✔
493
    const hasCancerTypes = result?.cancerTypes?.length > 0;
10✔
494
    const hasOtherCancerTypes = result?.otherCancerTypes?.length > 0;
10✔
495
    const hasPreCancerTypes = result?.preCancerTypes?.length > 0;
10✔
496
    const hasSpecies = result?.species?.length > 0;
10✔
497
    const hasOtherSpecies = result?.otherSpeciesOfSubjects?.length > 0;
10✔
498
    const hasNumberOfParticipants = !!result?.numberOfParticipants;
10✔
499

500
    this.data = merge({}, this.data, result);
10✔
501

502
    const isStarted =
10✔
503
      hasAccessTypes ||
10✔
504
      hasStudyFields ||
8✔
505
      hasCancerTypes ||
7✔
506
      hasOtherCancerTypes ||
5✔
507
      hasPreCancerTypes ||
5✔
508
      hasSpecies ||
5✔
509
      hasOtherSpecies ||
4✔
510
      hasNumberOfParticipants;
4✔
511
    this.data.sections.find((s) => s.name === "C").status = isStarted
10✔
512
      ? "In Progress"
6✔
513
      : "Not Started";
3✔
514

515
    return true;
10✔
516
  }
10✔
517

518
  private async parseSectionD(): Promise<boolean> {
69✔
519
    const ws = this.workbook.getWorksheet(SectionD.SHEET_NAME);
6✔
520
    if (!ws) {
6✔
521
      Logger.info("parseSectionD: No sheet found for Section D. Skipping");
1✔
522
      return false;
1✔
523
    }
1✔
524

525
    const data = await this.extractValuesFromWorksheet(ws);
5✔
526
    const newData = new Map();
5✔
527
    // Swap the column headers for the column keys in the mapping
528
    data.forEach((values, key) => {
5✔
529
      const colKey = SectionDColumns.find((col) => col.header === key)?.key;
80✔
530
      newData.set(
80✔
531
        colKey,
80✔
532
        values.map((value) => String(value).trim())
80✔
533
      );
80✔
534
    });
5✔
535
    const newMapping = SectionD.mapValues(newData, {
5✔
536
      programSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs),
5✔
537
      fileTypesSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fileTypes),
5✔
538
    });
5✔
539

540
    const result: RecursivePartial<QuestionnaireData> = parseSchemaObject(
5✔
541
      SectionDSchema,
5✔
542
      newMapping
5✔
543
    );
5✔
544

545
    this.data = merge({}, this.data, result);
5✔
546

547
    const hasTargetedSubmissionDate = result?.targetedSubmissionDate?.length > 0;
6✔
548
    const hasTargetedReleaseDate = result?.targetedReleaseDate?.length > 0;
6✔
549
    const hasDataTypes = result?.dataTypes?.length > 0;
6✔
550
    const hasOtherDataTypes = result?.otherDataTypes?.length > 0;
6✔
551
    const hasFiles = result?.files?.length > 0;
6✔
552
    const hasDataDeIdentified = !!result?.dataDeIdentified;
6✔
553
    const hasSubmitterComment = result?.submitterComment?.length > 0;
6✔
554
    const hasCellLines = !!result?.cellLines;
6✔
555
    const hasModelSystems = !!result?.modelSystems;
6✔
556

557
    const isStarted =
6✔
558
      hasTargetedSubmissionDate ||
6✔
559
      hasTargetedReleaseDate ||
3✔
560
      hasDataTypes ||
3!
NEW
561
      hasOtherDataTypes ||
×
NEW
562
      hasFiles ||
×
NEW
563
      hasDataDeIdentified ||
×
NEW
564
      hasSubmitterComment ||
×
NEW
565
      hasCellLines ||
×
NEW
566
      hasModelSystems;
×
567
    this.data.sections.find((s) => s.name === "D").status = isStarted
6✔
568
      ? "In Progress"
5!
NEW
569
      : "Not Started";
×
570

571
    return true;
6✔
572
  }
6✔
573

574
  /**
575
   * Adds a hidden sheet which contains the full list of API provided institutions at function call.
576
   *
577
   * Columns:
578
   * - `A` - Institution ID
579
   * - `B` - Institution Name
580
   *
581
   * @returns An immutable internal reference to the sheet.
582
   */
583
  private async createInstitutionSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
69✔
584
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.institutions);
15✔
585
    if (!sheet) {
15✔
586
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.institutions, { state: "veryHidden" });
15✔
587

588
      const institutions = await this.getAPIInstitutions();
15✔
589
      institutions?.forEach((institution, index) => {
15✔
590
        sheet.getCell(`A${index + 1}`).value = institution._id;
3✔
591
        sheet.getCell(`B${index + 1}`).value = institution.name;
3✔
592
      });
15✔
593
    }
15✔
594

595
    return sheet;
15✔
596
  }
15✔
597

598
  /**
599
   * Adds a hidden sheet which contains the full list of API provided programs at function call.
600
   *
601
   * Columns:
602
   * - `A` - Program ID
603
   * - `B` - Program Name
604
   *
605
   * @returns The created worksheet.
606
   */
607
  private async createProgramsSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
69✔
608
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs);
34✔
609
    if (!sheet) {
34✔
610
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.programs, { state: "veryHidden" });
34✔
611

612
      const programs = await this.getAPIPrograms();
34✔
613
      const fullPrograms: ProgramInput[] = [NotApplicableProgram, ...programs, OtherProgram];
34✔
614

615
      fullPrograms.forEach((program, index) => {
34✔
616
        const row = index + 1;
69✔
617

618
        sheet.getCell(`A${row}`).value = program._id || "";
69!
619
        sheet.getCell(`B${row}`).value = program.name || "";
69✔
620
        sheet.getCell(`C${row}`).value = program.abbreviation || "";
69✔
621
        sheet.getCell(`D${row}`).value = program.description || "";
69✔
622

623
        // Set the formula for the Program name to default to Program ID if empty
624
        sheet.getCell(`E${row}`).value = {
69✔
625
          formula: `IF(LEN(TRIM(B${row}))>0,B${row},A${row})`,
69✔
626
        };
69✔
627
      });
34✔
628
    }
34✔
629

630
    return sheet;
34✔
631
  }
34✔
632

633
  /**
634
   * Creates a hidden sheet containing the file types.
635
   *
636
   * @returns The created worksheet.
637
   */
638
  private async createFileTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
69✔
639
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fileTypes);
16✔
640
    if (!sheet) {
16✔
641
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.fileTypes, { state: "veryHidden" });
16✔
642

643
      Object.keys(fileTypeExtensions)?.forEach((file, index) => {
16✔
644
        sheet.getCell(`A${index + 1}`).value = file;
80✔
645
      });
16✔
646

647
      const allExtensions = union(...Object.values(fileTypeExtensions));
16✔
648
      allExtensions?.forEach((extension, index) => {
16✔
649
        sheet.getCell(`B${index + 1}`).value = extension;
144✔
650
      });
16✔
651
    }
16✔
652

653
    return sheet;
16✔
654
  }
16✔
655

656
  private async createFundingAgencySheet(): Promise<Readonly<ExcelJS.Worksheet>> {
69✔
657
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fundingAgencies);
19✔
658
    if (!sheet) {
19✔
659
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.fundingAgencies, {
19✔
660
        state: "veryHidden",
19✔
661
      });
19✔
662

663
      fundingOptions?.forEach((agency, index) => {
19✔
664
        sheet.getCell(`A${index + 1}`).value = agency;
57✔
665
      });
19✔
666
    }
19✔
667

668
    return sheet;
19✔
669
  }
19✔
670

671
  /**
672
   * Creates a sheet with the full list of Cancer Types
673
   *
674
   * @returns A readonly reference to the worksheet.
675
   */
676
  private async createCancerTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
69✔
677
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.cancerTypes);
14✔
678
    if (!sheet) {
14✔
679
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.cancerTypes, { state: "veryHidden" });
14✔
680

681
      cancerTypeOptions.forEach((file, index) => {
14✔
682
        sheet.getCell(`A${index + 1}`).value = file;
742✔
683
      });
14✔
684
    }
14✔
685

686
    return sheet;
14✔
687
  }
14✔
688

689
  /**
690
   * Creates a hidden sheet containing the species options.
691
   *
692
   * @returns A readonly reference to the worksheet.
693
   */
694
  private async createSpeciesListSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
69✔
695
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.speciesOptions);
14✔
696
    if (!sheet) {
14✔
697
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.speciesOptions, {
14✔
698
        state: "veryHidden",
14✔
699
      });
14✔
700

701
      speciesOptions.forEach((file, index) => {
14✔
702
        sheet.getCell(`A${index + 1}`).value = file;
56✔
703
      });
14✔
704
    }
14✔
705

706
    return sheet;
14✔
707
  }
14✔
708

709
  /**
710
   * Creates a hidden sheet containing the repository data types.
711
   *
712
   * @returns The created worksheet.
713
   */
714
  private async createRepositoryDataTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
69✔
715
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.repositoryDataTypes);
19✔
716
    if (!sheet) {
19✔
717
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.repositoryDataTypes, {
19✔
718
        state: "veryHidden",
19✔
719
      });
19✔
720

721
      const repositoryDataTypeOptions = [
19✔
722
        DataTypes.clinicalTrial.name,
19✔
723
        DataTypes.genomics.name,
19✔
724
        DataTypes.imaging.name,
19✔
725
        DataTypes.proteomics.name,
19✔
726
      ];
727
      repositoryDataTypeOptions.forEach((file, index) => {
19✔
728
        sheet.getCell(`A${index + 1}`).value = file;
76✔
729
      });
19✔
730
    }
19✔
731

732
    return sheet;
19✔
733
  }
19✔
734

735
  /**
736
   * An internal utility to retrieve the Institution List from the dependencies
737
   *
738
   * @note This is error-safe
739
   * @returns The array of institutions
740
   */
741
  private async getAPIInstitutions(): Promise<Institution[]> {
69✔
742
    try {
15✔
743
      const { data } = (await this.dependencies.getInstitutions?.()) || {};
15✔
744
      return data?.listInstitutions?.institutions || [];
15✔
745
    } catch (error) {
15!
NEW
746
      return [];
×
NEW
747
    }
×
748
  }
15✔
749

750
  /**
751
   * Retrieves the list of programs from the dependencies.
752
   *
753
   * @note This excludes 'readOnly' programs from the program list.
754
   * @returns The array of programs.
755
   */
756
  private async getAPIPrograms(): Promise<Organization[]> {
69✔
757
    try {
34✔
758
      const { data } = (await this.dependencies.getPrograms?.()) || {};
34✔
759
      return (
34✔
760
        (data?.listPrograms?.programs as Organization[])?.filter((program) => !program.readOnly) ||
34✔
761
        []
33✔
762
      );
763
    } catch (error) {
34!
NEW
764
      return [];
×
NEW
765
    }
×
766
  }
34✔
767

768
  // eslint-disable-next-line class-methods-use-this
769
  private async extractValuesFromWorksheet(
69✔
770
    ws: ExcelJS.Worksheet
31✔
771
  ): Promise<Map<string, Array<unknown>>> {
31✔
772
    const data = new Map<string, Array<unknown>>();
31✔
773
    const headerRow = ws.getRow(1);
31✔
774
    const headers = headerRow.values;
31✔
775

776
    ws.eachRow((row, rowNumber) => {
31✔
777
      if (rowNumber === 1) {
87✔
778
        return;
31✔
779
      }
31✔
780

781
      row.eachCell((cell, colNumber) => {
56✔
782
        const header = headers[colNumber];
280✔
783
        if (!header) {
280!
784
          // Invalid data, ignore
NEW
785
          return;
×
NEW
786
        }
×
787

788
        const existingValues = data.get(header) || [];
280✔
789
        data.set(header, [...existingValues, cell.value]);
280✔
790
      });
56✔
791
    });
31✔
792

793
    return data;
31✔
794
  }
31✔
795
}
69✔
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