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

CBIIT / crdc-datahub-ui / 20439596599

22 Dec 2025 05:43PM UTC coverage: 78.972% (+0.8%) from 78.178%
20439596599

push

github

web-flow
Merge pull request #924 from CBIIT/3.5.0

3.5.0 Release

5158 of 5669 branches covered (90.99%)

Branch coverage included in aggregate %.

806 of 844 new or added lines in 46 files covered. (95.5%)

9 existing lines in 5 files now uncovered.

30666 of 39694 relevant lines covered (77.26%)

231.87 hits per line

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

94.27
/src/classes/QuestionnaireExcelMiddleware.ts
1
import { LazyQueryExecFunction } from "@apollo/client";
2
import ExcelJS from "exceljs";
1✔
3
import { cloneDeep, merge, union, toString } 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 {
1✔
16
  SectionASchema,
17
  SectionBSchema,
18
  SectionCSchema,
19
  SectionDSchema,
20
} from "@/schemas/ApplicationSections";
21
import { isFormulaValue, isHyperlinkValue, isRichTextValue, isSharedFormulaValue } from "@/utils";
1✔
22
import { parseReleaseVersion } from "@/utils/envUtils";
1✔
23
import { determineSectionStatus, parseSchemaObject, sectionHasData } from "@/utils/formUtils";
1✔
24
import { Logger } from "@/utils/logger";
1✔
25

26
import { SectionA, SectionAColumns } from "./Excel/A/SectionA";
1✔
27
import { SectionB, SectionBColumns } from "./Excel/B/SectionB";
1✔
28
import { SectionC, SectionCColumns } from "./Excel/C/SectionC";
1✔
29
import { SectionD, SectionDColumns } from "./Excel/D/SectionD";
1✔
30
import { InstructionsSection } from "./Excel/Instructions/InstructionsSection";
1✔
31
import { MetaKeys } from "./Excel/Metadata/Columns";
32
import { MetadataColumns, MetadataSection } from "./Excel/Metadata/MetadataSection";
1✔
33
import { SectionCtxBase } from "./Excel/SectionBase";
34

35
/**
36
 * An internal template version identifier.
37
 */
38
export const TEMPLATE_VERSION = "1.5";
1✔
39

40
/**
41
 * The names of the HIDDEN sheets used in the Excel workbook.
42
 * Primarily used for hidden lists.
43
 */
44
export const HIDDEN_SHEET_NAMES = {
1✔
45
  institutions: "InstitutionList",
1✔
46
  programs: "ProgramList",
1✔
47
  fileTypes: "FileTypeList",
1✔
48
  cancerTypes: "CancerTypeList",
1✔
49
  speciesOptions: "SubjectSpeciesList",
1✔
50
  fundingAgencies: "FundingAgencyList",
1✔
51
  repositoryDataTypes: "RepositoryDataTypeList",
1✔
52
} as const;
1✔
53

54
/**
55
 * The required dependencies to import or export a Submission Request.
56
 */
57
export type MiddlewareDependencies = {
58
  application?: Omit<Application, "QuestionnaireData">;
59
  getPrograms?: LazyQueryExecFunction<ListOrgsResp, ListOrgsInput>;
60
  getInstitutions?: LazyQueryExecFunction<ListInstitutionsResp, unknown>;
61
};
62

63
/**
64
 * A specialized class to handle importing and exporting of a Submission Request.
65
 * This class is designed to work with QuestionnaireData, allowing for the import and export of data
66
 * via Excel files.
67
 */
68
export class QuestionnaireExcelMiddleware {
1✔
69
  /**
70
   * The internal ExcelJS Workbook object.
71
   * This object is used to create and manipulate the Excel file during the import/export process.
72
   */
73
  private workbook: ExcelJS.Workbook;
90✔
74

75
  /**
76
   * The internal QuestionnaireData object.
77
   * This object is mutated during the import process, but remains immutable for export.
78
   */
79
  private data: QuestionnaireData | null;
90✔
80

81
  /**
82
   * The dependencies required for exporting or importing,
83
   * such as fetching Programs or Institutions.
84
   */
85
  private dependencies: MiddlewareDependencies;
90✔
86

87
  /**
88
   * Creates an instance of the QuestionnaireExcelMiddleware.
89
   *
90
   * @param data The QuestionnaireData object, if applicable, to be exported.
91
   * @param dependencies The import/export dependencies
92
   */
93
  constructor(data: QuestionnaireData | null, dependencies: MiddlewareDependencies) {
90✔
94
    this.workbook = new ExcelJS.Workbook();
90✔
95
    this.data = data ? cloneDeep(data) : null;
90✔
96
    this.dependencies = dependencies;
90✔
97
  }
90✔
98

99
  /**
100
   * A method to serialize the QuestionnaireData object to an Excel file.
101
   * If no `QuestionnaireData` is provided, the Excel file will be generated with no pre-filled values.
102
   *
103
   * @throws Will throw an error if the serialization fails significantly.
104
   * @returns A Promise that resolves to an ArrayBuffer containing the Excel file data.
105
   */
106
  public async serialize(): Promise<ArrayBuffer> {
90✔
107
    this.setMetadataProperties();
×
108

109
    await this.serializeMetadata();
×
110
    await this.serializeInstructions();
×
111
    await this.serializeSectionA();
×
112
    await this.serializeSectionB();
×
113
    await this.serializeSectionC();
×
114
    await this.serializeSectionD();
×
115

116
    return this.workbook.xlsx.writeBuffer();
×
117
  }
×
118

119
  /**
120
   * A static method to parse the input file and return a QuestionnaireExcelMiddleware instance.
121
   * This method is the inverse of the `serialize` method.
122
   *
123
   * @param file The Excel file data to be parsed.
124
   * @param dependencies The dependencies required for parsing the data.
125
   * @throws Will throw an error if the data cannot be parsed or is invalid.
126
   * @returns A new instance of QuestionnaireExcelMiddleware with the parsed data.
127
   */
128
  public static async parse(
90✔
NEW
129
    file: File,
×
130
    dependencies: MiddlewareDependencies
×
NEW
131
  ): Promise<QuestionnaireData> {
×
132
    // Load the workbook from the File
133
    const workbook = new ExcelJS.Workbook();
×
NEW
134
    await workbook.xlsx.load(await file?.arrayBuffer());
×
135

136
    // Create an instance with null data, then assign the loaded workbook
137
    const middleware = new QuestionnaireExcelMiddleware(null, dependencies);
×
138
    middleware.workbook = workbook;
×
139
    middleware.data = { ...InitialQuestionnaire, sections: [...InitialSections] };
×
140

141
    await middleware.parseMetadata();
×
142
    await middleware.parseSectionA();
×
143
    await middleware.parseSectionB();
×
144
    await middleware.parseSectionC();
×
145
    await middleware.parseSectionD();
×
146

147
    return middleware.data;
×
148
  }
×
149

150
  /**
151
   * Sets the metadata properties for the Excel workbook.
152
   *
153
   * @returns A Promise that resolves when the properties have been set.
154
   */
155
  private async setMetadataProperties(): Promise<void> {
90✔
156
    this.workbook.creator = "crdc-datahub-ui";
1✔
157
    this.workbook.lastModifiedBy = "crdc-datahub-ui";
1✔
158
    this.workbook.title = "CRDC Submission Request";
1✔
159
    this.workbook.subject = "CRDC Submission Request Template";
1✔
160
    this.workbook.company = "National Cancer Institute";
1✔
161
    this.workbook.created = new Date();
1✔
162
    this.workbook.modified = new Date();
1✔
163
  }
1✔
164

165
  /**
166
   * Builds the metadata page for the Excel workbook.
167
   *
168
   * @returns A readonly reference to the created worksheet.
169
   */
170
  private async serializeMetadata(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
171
    const ctx: SectionCtxBase = {
6✔
172
      workbook: this.workbook,
6✔
173
      u: {
6✔
174
        header: (ws: ExcelJS.Worksheet, color?: string) => {
6✔
175
          const r1 = ws.getRow(1);
6✔
176
          r1.font = { bold: true };
6✔
177
          r1.alignment = { horizontal: "center" };
6✔
178
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
6✔
179
          ws.state = "veryHidden";
6✔
180
        },
6✔
181
      },
6✔
182
    };
6✔
183

184
    const metaSection = new MetadataSection({
6✔
185
      application: this.dependencies.application,
6✔
186
      templateVersion: TEMPLATE_VERSION,
6✔
187
      devTier: env.VITE_DEV_TIER || "N/A",
6!
188
      appVersion: parseReleaseVersion(),
6✔
189
    });
6✔
190

191
    return metaSection.serialize(ctx);
6✔
192
  }
6✔
193

194
  /**
195
   * Serializes the instructions section into the Excel workbook.
196
   *
197
   * @returns A readonly reference to the created worksheet.
198
   */
199
  private async serializeInstructions(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
200
    const ctx: SectionCtxBase = {
1✔
201
      workbook: this.workbook,
1✔
202
      u: {
1✔
203
        header: (ws: ExcelJS.Worksheet, color?: string) => {
1✔
204
          const r1 = ws.getRow(1);
×
205
          r1.font = { bold: true };
×
206
          r1.alignment = { horizontal: "center" };
×
207
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
×
208
        },
×
209
      },
1✔
210
    };
1✔
211

212
    const instructionsSection = new InstructionsSection();
1✔
213

214
    return instructionsSection.serialize(ctx);
1✔
215
  }
1✔
216

217
  /**
218
   * Adds the form section A to the Excel workbook.
219
   *
220
   * @returns A readonly reference to the created worksheet.
221
   */
222
  private async serializeSectionA(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
223
    const ctx: SectionCtxBase = {
15✔
224
      workbook: this.workbook,
15✔
225
      u: {
15✔
226
        header: (ws: ExcelJS.Worksheet, color?: string) => {
15✔
227
          const r1 = ws.getRow(1);
15✔
228
          r1.font = { bold: true };
15✔
229
          r1.alignment = { horizontal: "center" };
15✔
230
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
15✔
231
        },
15✔
232
      },
15✔
233
    };
15✔
234

235
    const sectionA = new SectionA({
15✔
236
      data: this.data as QuestionnaireData,
15✔
237
      institutionSheet: await this.createInstitutionSheet(),
15✔
238
    });
15✔
239

240
    return sectionA.serialize(ctx);
15✔
241
  }
15✔
242

243
  /**
244
   * Serializes Section B data into the Excel workbook.
245
   *
246
   * @returns The serialized worksheet for Section B.
247
   */
248
  private async serializeSectionB(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
249
    const ctx: SectionCtxBase = {
21✔
250
      workbook: this.workbook,
21✔
251
      u: {
21✔
252
        header: (ws: ExcelJS.Worksheet, color?: string) => {
21✔
253
          const r1 = ws.getRow(1);
21✔
254
          r1.font = { bold: true };
21✔
255
          r1.alignment = { horizontal: "center" };
21✔
256
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
21✔
257
        },
21✔
258
      },
21✔
259
    };
21✔
260

261
    const sectionB = new SectionB({
21✔
262
      data: this.data as QuestionnaireData,
21✔
263
      programSheet: await this.createProgramsSheet(),
21✔
264
      fundingAgenciesSheet: await this.createFundingAgencySheet(),
21✔
265
      repositoryDataTypesSheet: await this.createRepositoryDataTypesSheet(),
21✔
266
    });
21✔
267

268
    const sheet = await sectionB.serialize(ctx);
21✔
269

270
    return sheet;
21✔
271
  }
21✔
272

273
  /**
274
   * Adds the form section C to the Excel workbook.
275
   *
276
   * @returns A readonly reference to the created worksheet.
277
   */
278
  private async serializeSectionC(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
279
    const ctx: SectionCtxBase = {
25✔
280
      workbook: this.workbook,
25✔
281
      u: {
25✔
282
        header: (ws: ExcelJS.Worksheet, color?: string) => {
25✔
283
          const r1 = ws.getRow(1);
25✔
284
          r1.font = { bold: true };
25✔
285
          r1.alignment = { horizontal: "center" };
25✔
286
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
25✔
287
        },
25✔
288
      },
25✔
289
    };
25✔
290

291
    const sectionC = new SectionC({
25✔
292
      data: this.data as QuestionnaireData,
25✔
293
      cancerTypesSheet: await this.createCancerTypesSheet(),
25✔
294
      speciesSheet: await this.createSpeciesListSheet(),
25✔
295
    });
25✔
296

297
    return sectionC.serialize(ctx);
25✔
298
  }
25✔
299

300
  private async serializeSectionD(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
301
    const ctx: SectionCtxBase = {
18✔
302
      workbook: this.workbook,
18✔
303
      u: {
18✔
304
        header: (ws: ExcelJS.Worksheet, color?: string) => {
18✔
305
          const r1 = ws.getRow(1);
18✔
306
          r1.font = { bold: true };
18✔
307
          r1.alignment = { horizontal: "center" };
18✔
308
          r1.fill = { type: "pattern", pattern: "solid", fgColor: { argb: color } };
18✔
309
        },
18✔
310
      },
18✔
311
    };
18✔
312

313
    const sectionD = new SectionD({
18✔
314
      data: this.data as QuestionnaireData,
18✔
315
      programSheet: await this.createProgramsSheet(),
18✔
316
      fileTypesSheet: await this.createFileTypesSheet(),
18✔
317
    });
18✔
318

319
    const sheet = await sectionD.serialize(ctx);
18✔
320

321
    return sheet;
18✔
322
  }
18✔
323

324
  /**
325
   * Parses the data from the metadata section.
326
   *
327
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
328
   */
329
  private async parseMetadata(): Promise<boolean> {
90✔
330
    const sheet = this.workbook.getWorksheet(MetadataSection.SHEET_NAME);
4✔
331
    if (!sheet) {
4✔
332
      return false;
1✔
333
    }
1✔
334

335
    const data = await this.extractValuesFromWorksheet(sheet);
3✔
336
    const newData = new Map<MetaKeys, Array<unknown>>();
3✔
337

338
    data.forEach((values, key) => {
3✔
339
      const colKey = MetadataColumns.find((col) => col.header === key)?.key;
33✔
340
      newData.set(
33✔
341
        colKey,
33✔
342
        values.map((value) => toString(value).trim())
33✔
343
      );
33✔
344
    });
3✔
345

346
    if (newData?.get("devTier")?.[0] !== env.VITE_DEV_TIER) {
4✔
347
      Logger.info("QuestionnaireExcelMiddleware: Received mismatched devTier.", {
1✔
348
        expected: env.VITE_DEV_TIER,
1✔
349
        received: newData?.get("devTier")?.[0],
1✔
350
      });
1✔
351
    }
1✔
352
    if (newData?.get("templateVersion")?.[0] !== TEMPLATE_VERSION) {
4✔
353
      Logger.info("QuestionnaireExcelMiddleware: Received mismatched templateVersion.", {
1✔
354
        expected: TEMPLATE_VERSION,
1✔
355
        received: newData?.get("templateVersion")?.[0],
1✔
356
      });
1✔
357
    }
1✔
358
    if (
3✔
359
      newData?.get("submissionId")?.[0] &&
4✔
360
      newData?.get("submissionId")?.[0] !== this.dependencies?.application?._id
1✔
361
    ) {
4✔
362
      Logger.info("QuestionnaireExcelMiddleware: Received mismatched submissionId.", {
1✔
363
        expected: this.dependencies?.application?._id,
1✔
364
        received: newData?.get("submissionId")?.[0],
1✔
365
      });
1✔
366
    }
1✔
367
    if (
3✔
368
      newData?.get("appVersion")?.[0] &&
4✔
369
      newData?.get("appVersion")?.[0] !== parseReleaseVersion()
3✔
370
    ) {
4✔
371
      Logger.info("QuestionnaireExcelMiddleware: Received mismatched appVersion.", {
1✔
372
        expected: parseReleaseVersion(),
1✔
373
        received: newData?.get("appVersion")?.[0],
1✔
374
      });
1✔
375
    }
1✔
376

377
    return true;
3✔
378
  }
4✔
379

380
  /**
381
   * Parses the data from Section A of the Excel workbook.
382
   *
383
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
384
   */
385
  private async parseSectionA(): Promise<boolean> {
90✔
386
    const sheet = this.workbook.getWorksheet(SectionA.SHEET_NAME);
9✔
387
    if (!sheet) {
9✔
388
      Logger.info(`parseSectionA: No sheet found for ${SectionA.SHEET_NAME}. Skipping`);
1✔
389
      return false;
1✔
390
    }
1✔
391

392
    const data = await this.extractValuesFromWorksheet(sheet);
8✔
393
    const newData = new Map();
8✔
394

395
    data.forEach((values, key) => {
8✔
396
      const colKey = SectionAColumns.find((col) => col.header === key)?.key;
160✔
397
      newData.set(
160✔
398
        colKey,
160✔
399
        values.map((value) => toString(value).trim())
160✔
400
      );
160✔
401
    });
8✔
402

403
    const newMapping = SectionA.mapValues(newData, {
8✔
404
      institutionSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.institutions),
8✔
405
    });
8✔
406

407
    const result = parseSchemaObject(SectionASchema, newMapping);
8✔
408

409
    this.data = merge({}, this.data, result.data);
8✔
410
    this.data.sections.find((s) => s.name === "A").status = determineSectionStatus(
8✔
411
      result.passed,
8✔
412
      sectionHasData(SectionA.SHEET_ID, result.data)
8✔
413
    );
8✔
414

415
    return true;
8✔
416
  }
9✔
417

418
  /**
419
   * Parses the data from Section B of the Excel workbook.
420
   *
421
   * @returns A Promise that resolves to a boolean indicating success or failure of the parsing.
422
   */
423
  private async parseSectionB(): Promise<boolean> {
90✔
424
    const ws = this.workbook.getWorksheet(SectionB.SHEET_NAME);
11✔
425
    if (!ws) {
11✔
426
      Logger.info("parseSectionB: No sheet found for Section B. Skipping");
1✔
427
      return false;
1✔
428
    }
1✔
429

430
    const data = await this.extractValuesFromWorksheet(ws);
10✔
431
    const newData = new Map();
10✔
432
    // Swap the column headers for the column keys in the mapping
433
    data.forEach((values, key) => {
10✔
434
      const colKey = SectionBColumns.find((col) => col.header === key)?.key;
190✔
435
      newData.set(
190✔
436
        colKey,
190✔
437
        values.map((value) => toString(value).trim())
190✔
438
      );
190✔
439
    });
10✔
440
    const newMapping = SectionB.mapValues(newData, {
10✔
441
      programSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs),
10✔
442
    });
10✔
443

444
    const result = parseSchemaObject(SectionBSchema, newMapping);
10✔
445

446
    this.data = merge({}, this.data, result.data);
10✔
447
    this.data.sections.find((s) => s.name === "B").status = determineSectionStatus(
10✔
448
      result.passed,
10✔
449
      sectionHasData(SectionB.SHEET_ID, result.data)
10✔
450
    );
10✔
451

452
    return true;
10✔
453
  }
11✔
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> {
90✔
461
    const sheet = this.workbook.getWorksheet(SectionC.SHEET_NAME);
21✔
462
    if (!sheet) {
21✔
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);
20✔
468
    const newData = new Map();
20✔
469

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

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

483
    const result = parseSchemaObject(SectionCSchema, newMapping);
20✔
484

485
    this.data = merge({}, this.data, result.data);
20✔
486
    this.data.sections.find((s) => s.name === "C").status = determineSectionStatus(
20✔
487
      result.passed,
20✔
488
      sectionHasData(SectionC.SHEET_ID, result.data)
20✔
489
    );
20✔
490

491
    return true;
20✔
492
  }
21✔
493

494
  private async parseSectionD(): Promise<boolean> {
90✔
495
    const ws = this.workbook.getWorksheet(SectionD.SHEET_NAME);
8✔
496
    if (!ws) {
8✔
497
      Logger.info("parseSectionD: No sheet found for Section D. Skipping");
1✔
498
      return false;
1✔
499
    }
1✔
500

501
    const data = await this.extractValuesFromWorksheet(ws);
7✔
502
    const newData = new Map();
7✔
503

504
    data.forEach((values, key) => {
7✔
505
      const colKey = SectionDColumns.find((col) => col.header === key)?.key;
168✔
506
      newData.set(
168✔
507
        colKey,
168✔
508
        values.map((value) => toString(value).trim())
168✔
509
      );
168✔
510
    });
7✔
511

512
    const newMapping = SectionD.mapValues(newData, {
7✔
513
      programSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs),
7✔
514
      fileTypesSheet: this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fileTypes),
7✔
515
    });
7✔
516

517
    const result = parseSchemaObject(SectionDSchema, newMapping);
7✔
518

519
    this.data = merge({}, this.data, result.data);
7✔
520
    this.data.sections.find((s) => s.name === "D").status = determineSectionStatus(
7✔
521
      result.passed,
7✔
522
      sectionHasData(SectionD.SHEET_ID, result.data)
7✔
523
    );
7✔
524

525
    return true;
7✔
526
  }
8✔
527

528
  /**
529
   * Adds a hidden sheet which contains the full list of API provided institutions at function call.
530
   *
531
   * Columns:
532
   * - `A` - Institution ID
533
   * - `B` - Institution Name
534
   *
535
   * @returns An immutable internal reference to the sheet.
536
   */
537
  private async createInstitutionSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
538
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.institutions);
15✔
539
    if (!sheet) {
15✔
540
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.institutions, { state: "veryHidden" });
15✔
541

542
      const institutions = await this.getAPIInstitutions();
15✔
543
      institutions?.forEach((institution, index) => {
15✔
544
        sheet.getCell(`A${index + 1}`).value = institution._id;
3✔
545
        sheet.getCell(`B${index + 1}`).value = institution.name;
3✔
546
      });
15✔
547
    }
15✔
548

549
    return sheet;
15✔
550
  }
15✔
551

552
  /**
553
   * Adds a hidden sheet which contains the full list of API provided programs at function call.
554
   *
555
   * Columns:
556
   * - `A` - Program ID
557
   * - `B` - Program Name
558
   *
559
   * @returns The created worksheet.
560
   */
561
  private async createProgramsSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
562
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.programs);
38✔
563
    if (!sheet) {
38✔
564
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.programs, { state: "veryHidden" });
38✔
565

566
      const programs = await this.getAPIPrograms();
38✔
567
      const fullPrograms: ProgramInput[] = [NotApplicableProgram, ...programs, OtherProgram];
38✔
568

569
      fullPrograms.forEach((program, index) => {
38✔
570
        const row = index + 1;
77✔
571

572
        sheet.getCell(`A${row}`).value = program._id || "";
77!
573
        sheet.getCell(`B${row}`).value = program.name || "";
77✔
574
        sheet.getCell(`C${row}`).value = program.abbreviation || "";
77✔
575
        sheet.getCell(`D${row}`).value = program.description || "";
77✔
576

577
        // Set the formula for the Program name to default to Program ID if empty
578
        sheet.getCell(`E${row}`).value = {
77✔
579
          formula: `IF(LEN(TRIM(B${row}))>0,B${row},A${row})`,
77✔
580
        };
77✔
581
      });
38✔
582
    }
38✔
583

584
    return sheet;
38✔
585
  }
38✔
586

587
  /**
588
   * Creates a hidden sheet containing the file types.
589
   *
590
   * @returns The created worksheet.
591
   */
592
  private async createFileTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
593
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fileTypes);
18✔
594
    if (!sheet) {
18✔
595
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.fileTypes, { state: "veryHidden" });
18✔
596

597
      Object.keys(fileTypeExtensions)?.forEach((file, index) => {
18✔
598
        sheet.getCell(`A${index + 1}`).value = file;
90✔
599
      });
18✔
600

601
      const allExtensions = union(...Object.values(fileTypeExtensions));
18✔
602
      allExtensions?.forEach((extension, index) => {
18✔
603
        sheet.getCell(`B${index + 1}`).value = extension;
162✔
604
      });
18✔
605
    }
18✔
606

607
    return sheet;
18✔
608
  }
18✔
609

610
  /**
611
   * Creates a hidden sheet containing the funding agency options.
612
   *
613
   * @returns The created worksheet.
614
   */
615
  private async createFundingAgencySheet(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
616
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.fundingAgencies);
21✔
617
    if (!sheet) {
21✔
618
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.fundingAgencies, {
21✔
619
        state: "veryHidden",
21✔
620
      });
21✔
621

622
      fundingOptions?.forEach((agency, index) => {
21✔
623
        sheet.getCell(`A${index + 1}`).value = agency;
63✔
624
      });
21✔
625
    }
21✔
626

627
    return sheet;
21✔
628
  }
21✔
629

630
  /**
631
   * Creates a sheet with the full list of Cancer Types
632
   *
633
   * @returns A readonly reference to the worksheet.
634
   */
635
  private async createCancerTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
636
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.cancerTypes);
25✔
637
    if (!sheet) {
25✔
638
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.cancerTypes, { state: "veryHidden" });
25✔
639

640
      cancerTypeOptions.forEach((file, index) => {
25✔
641
        sheet.getCell(`A${index + 1}`).value = file;
1,325✔
642
      });
25✔
643
    }
25✔
644

645
    return sheet;
25✔
646
  }
25✔
647

648
  /**
649
   * Creates a hidden sheet containing the species options.
650
   *
651
   * @returns A readonly reference to the worksheet.
652
   */
653
  private async createSpeciesListSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
654
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.speciesOptions);
25✔
655
    if (!sheet) {
25✔
656
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.speciesOptions, {
25✔
657
        state: "veryHidden",
25✔
658
      });
25✔
659

660
      speciesOptions.forEach((file, index) => {
25✔
661
        sheet.getCell(`A${index + 1}`).value = file;
100✔
662
      });
25✔
663
    }
25✔
664

665
    return sheet;
25✔
666
  }
25✔
667

668
  /**
669
   * Creates a hidden sheet containing the repository data types.
670
   *
671
   * @returns The created worksheet.
672
   */
673
  private async createRepositoryDataTypesSheet(): Promise<Readonly<ExcelJS.Worksheet>> {
90✔
674
    let sheet = this.workbook.getWorksheet(HIDDEN_SHEET_NAMES.repositoryDataTypes);
21✔
675
    if (!sheet) {
21✔
676
      sheet = this.workbook.addWorksheet(HIDDEN_SHEET_NAMES.repositoryDataTypes, {
21✔
677
        state: "veryHidden",
21✔
678
      });
21✔
679

680
      const repositoryDataTypeOptions = [
21✔
681
        DataTypes.clinicalTrial.name,
21✔
682
        DataTypes.genomics.name,
21✔
683
        DataTypes.imaging.name,
21✔
684
        DataTypes.proteomics.name,
21✔
685
      ];
686
      repositoryDataTypeOptions.forEach((file, index) => {
21✔
687
        sheet.getCell(`A${index + 1}`).value = file;
84✔
688
      });
21✔
689
    }
21✔
690

691
    return sheet;
21✔
692
  }
21✔
693

694
  /**
695
   * An internal utility to retrieve the Institution List from the dependencies
696
   *
697
   * @note This is error-safe
698
   * @returns The array of institutions
699
   */
700
  private async getAPIInstitutions(): Promise<Institution[]> {
90✔
701
    try {
15✔
702
      const { data } = (await this.dependencies.getInstitutions?.()) || {};
15✔
703
      return data?.listInstitutions?.institutions || [];
15✔
704
    } catch (error) {
15!
705
      return [];
×
706
    }
×
707
  }
15✔
708

709
  /**
710
   * Retrieves the list of programs from the dependencies.
711
   *
712
   * @note This excludes 'readOnly' programs from the program list.
713
   * @returns The array of programs.
714
   */
715
  private async getAPIPrograms(): Promise<Organization[]> {
90✔
716
    try {
38✔
717
      const { data } = (await this.dependencies.getPrograms?.()) || {};
38✔
718
      return (
38✔
719
        (data?.listPrograms?.programs as Organization[])?.filter((program) => !program.readOnly) ||
38✔
720
        []
37✔
721
      );
722
    } catch (error) {
38!
723
      return [];
×
724
    }
×
725
  }
38✔
726

727
  /**
728
   * Normalizes the cell value for consistent processing.
729
   *
730
   * @param value The cell value to normalize.
731
   * @returns The normalized value.
732
   */
733
  private normalizeCellValue(value: ExcelJS.CellValue | undefined): unknown {
90✔
734
    if (value === undefined || value === null) {
132,889✔
735
      return null;
66,070✔
736
    }
66,070✔
737

738
    if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
132,889✔
739
      return value;
66,806✔
740
    }
66,806✔
741
    if (value instanceof Date) {
132,889✔
742
      return new Intl.DateTimeFormat("en-US", { timeZone: "UTC" }).format(value);
3✔
743
    }
3✔
744

745
    if (isHyperlinkValue(value)) {
132,889✔
746
      return value.text;
2✔
747
    }
2✔
748
    if (isFormulaValue(value)) {
132,889✔
749
      return value.result !== undefined ? this.normalizeCellValue(value.result) : null;
2✔
750
    }
2✔
751
    if (isSharedFormulaValue(value)) {
132,889✔
752
      return value.result !== undefined ? this.normalizeCellValue(value.result) : null;
2✔
753
    }
2✔
754
    if (isRichTextValue(value)) {
132,889✔
755
      return value.richText.map((seg) => seg.text).join("");
2✔
756
    }
2✔
757

758
    Logger.error(
2✔
759
      "QuestionnaireExcelMiddleware: Found unknown value while normalizing data:",
2✔
760
      value
2✔
761
    );
2✔
762
    return null;
2✔
763
  }
132,889✔
764

765
  /**
766
   * Extracts the header key from a cell value.
767
   *
768
   * @param value The cell value to extract the header key from.
769
   * @returns The extracted header key or null if not found.
770
   */
771
  private headerKey(value: ExcelJS.CellValue | undefined): string | null {
90✔
772
    const normalized = this.normalizeCellValue(value);
66,440✔
773
    if (normalized === null || normalized === undefined) {
66,440✔
774
      return null;
3✔
775
    }
3✔
776

777
    return toString(normalized);
66,437✔
778
  }
66,440✔
779

780
  /**
781
   * Extracts values from the specified worksheet.
782
   *
783
   * @param ws The worksheet to extract values from.
784
   * @returns A map of header names to their corresponding cell values.
785
   */
786
  private async extractValuesFromWorksheet(
90✔
787
    ws: ExcelJS.Worksheet
49✔
788
  ): Promise<Map<string, Array<unknown>>> {
49✔
789
    const data = new Map<string, Array<unknown>>();
49✔
790
    const headerRow = ws.getRow(1);
49✔
791
    const headers = headerRow.values;
49✔
792

793
    ws.eachRow({ includeEmpty: true }, (row, rowNumber) => {
49✔
794
      if (rowNumber === 1) {
4,508✔
795
        return;
49✔
796
      }
49✔
797

798
      row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
4,459✔
799
        const header = this.headerKey(headers[colNumber]);
66,434✔
800
        if (!header) {
66,434✔
801
          // Invalid data, ignore
802
          return;
1✔
803
        }
1✔
804

805
        const allValues = data.get(header) || [];
66,434✔
806
        const normalizedValue = this.normalizeCellValue(cell.value);
66,434✔
807

808
        allValues[rowNumber - 2] = normalizedValue;
66,434✔
809

810
        data.set(header, allValues);
66,434✔
811
      });
4,459✔
812
    });
49✔
813

814
    return data;
49✔
815
  }
49✔
816
}
90✔
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