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

dondi / GRNsight / 25699375138

11 May 2026 09:49PM UTC coverage: 80.785% (-1.0%) from 81.764%
25699375138

Pull #1581

github

ntran18
Remove network option arguments when fetching and populate data from database
Pull Request #1581: Remove network option arguments when fetching and populate for database

458 of 573 branches covered (79.93%)

Branch coverage included in aggregate %.

1270 of 1566 relevant lines covered (81.1%)

8958.9 hits per line

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

94.89
/server/controllers/additional-sheet-parser.js
1
// Parses "optimization_parameters" and 2-column sheets
2
// from GRNmap input or output workbook
3

4
var constants = require(__dirname + "/workbook-constants");
3✔
5

6
const getSheetHeader = (sheetName, column, row) => {
3✔
7
    if (row === 0) {
2,259✔
8
        if (sheetName === "production_rates" || sheetName === "optimized_production_rates") {
2,259✔
9
            return column === 0 ? "id" : "production_rate";
591✔
10
        } else if (sheetName === "degradation_rates") {
1,668✔
11
            return column === 0 ? "id" : "degradation_rate";
495✔
12
        } else if (sheetName === "threshold_b" || sheetName === "optimized_threshold_b") {
1,173✔
13
            return column === 0 ? "id" : "threshold_b";
591✔
14
        } else if (sheetName === "optimization_parameters") {
582✔
15
            return column === 0 ? "optimization_parameter" : "value";
504✔
16
        } else if (sheetName === "optimization_diagnostics") {
78✔
17
            return column === 0 ? "Parameter" : "Value";
78✔
18
        }
19
    }
20
};
21

22
const optimizationParametersTypeKey = {
3✔
23
    alpha: "number",
24
    kk_max: "number",
25
    MaxIter: "number",
26
    TolFun: "number",
27
    MaxFunEval: "number",
28
    TolX: "number",
29
    production_function: "string",
30
    L_curve: "number",
31
    estimate_params: "number",
32
    make_graphs: "number",
33
    fix_P: "number",
34
    fix_b: "number",
35
    expression_timepoints: "object",
36
    Strain: "object",
37
    species: "string",
38
    taxon_id: "number",
39
    workbookType: "string",
40
    simulation_timepoints: "object",
41
    b_or_tau: "number",
42
};
43

44
const optimizationDiagnosticsParameters = ["LSE", "Penalty", "min LSE", "iteration count"];
3✔
45

46
const optimizationParametersObjectKey = {
3✔
47
    expression_timepoints: "number",
48
    Strain: "string",
49
    simulation_timepoints: "number",
50
};
51

52
const addWarning = (workbook, message) => {
3✔
53
    let warningsCount;
54
    if (!Object.keys(workbook).includes("warnings")) {
330!
55
        warningsCount = 0;
×
56
        workbook.warnings = [];
×
57
    } else {
58
        warningsCount = workbook.warnings.length;
330✔
59
    }
60
    const MAX_WARNINGS = 75;
330✔
61
    if (warningsCount < MAX_WARNINGS) {
330!
62
        const exists = workbook.warnings.some(w => w.errorDescription === message.errorDescription);
330✔
63
        if (exists) {
330!
64
            return false;
×
65
        }
66
        workbook.warnings.push(message);
330✔
67
    } else {
68
        workbook.errors.push(constants.errors.warningsCountError);
×
69
        return false;
×
70
    }
71
};
72

73
const addError = (output, message) => {
3✔
74
    const errorsCount = output.errors.length;
54✔
75
    const MAX_ERRORS = 20;
54✔
76
    if (errorsCount < MAX_ERRORS) {
54!
77
        output.errors.push(message);
54✔
78
    } else {
79
        output.errors.push(constants.errors.errorsCountError);
×
80
        return false;
×
81
    }
82
};
83

84
const validGeneName = (output, sheetName, gene, row) => {
3✔
85
    var maxGeneLength = 12;
6,717✔
86
    var regex = /[^a-z0-9\_\-]/gi;
6,717✔
87

88
    // Allow missing gene id
89
    if (gene === undefined || gene === null || (typeof gene === "string" && gene.trim() === "")) {
6,717✔
90
        return false;
42✔
91
    }
92

93
    if (typeof gene !== "string") {
6,675✔
94
        addError(output, constants.errors.invalidGeneTypeError(sheetName, gene, row));
15✔
95
        return false;
15✔
96
    } else if (gene.length > maxGeneLength) {
6,660✔
97
        addError(output, constants.errors.invalidGeneLengthError(sheetName, gene, row));
15✔
98
        return false;
15✔
99
    } else if (gene.match(regex) !== null) {
6,645✔
100
        addError(output, constants.errors.specialCharacterError(sheetName, gene, row));
15✔
101
        return false;
15✔
102
    }
103
    return true;
6,630✔
104
};
105
// Optimization Parameters Parser
106
const parseMetaDataSheet = sheet => {
3✔
107
    let meta = {
252✔
108
        data: {},
109
        errors: [],
110
        warnings: [],
111
    };
112
    let paramType;
113
    checkValidHeaderAndAddWarnings(meta, sheet);
252✔
114

115
    const isHeaderMissing = meta.warnings.some(
252✔
116
        w => w.warningCode === `MISSING_COLUMN_HEADER_${sheet.name.toUpperCase()}`
6✔
117
    );
118
    sheet.data.forEach(function (element, index) {
252✔
119
        if (!isHeaderMissing && index === 0) {
4,713✔
120
            return;
249✔
121
        }
122
        const value = element.slice(1);
4,464✔
123
        // Extract element from array if array contains only 1 value
124
        meta.data[element[0]] = value.length > 1 ? value : value[0];
4,464✔
125
    });
126
    for (let key in meta.data) {
252✔
127
        paramType = optimizationParametersTypeKey[key];
4,464✔
128
        if (paramType === "object") {
4,464✔
129
            paramType = `list of ${optimizationParametersObjectKey[key]}s`;
756✔
130
        }
131
        if (meta.data[key] === undefined) {
4,464✔
132
            addWarning(meta, constants.warnings.unknownOptimizationParameter(sheet.name, key));
3✔
133
        } else if (typeof meta.data[key] !== optimizationParametersTypeKey[key]) {
4,461✔
134
            if (
183✔
135
                optimizationParametersTypeKey[key] !== "object" ||
363✔
136
                typeof meta.data[key] !== optimizationParametersObjectKey[key]
137
            ) {
138
                addWarning(
3✔
139
                    meta,
140
                    constants.warnings.invalidOptimizationParameter(sheet.name, key, paramType)
141
                );
142
            }
143
        } else if (optimizationParametersTypeKey[key] === "object") {
4,278✔
144
            for (let val of meta.data[key]) {
6,843✔
145
                if (typeof val !== optimizationParametersObjectKey[key]) {
6,843✔
146
                    // throw error once per object. Makes sure that errors list is not flooded
147
                    addWarning(
3✔
148
                        meta,
149
                        constants.warnings.invalidOptimizationParameter(sheet.name, key, paramType)
150
                    );
151
                    break;
3✔
152
                }
153
            }
576✔
154
        }
155
    }
156
    return meta;
252✔
157
};
158

159
// check header method
160
const checkValidHeaderAndAddWarnings = (output, sheet) => {
3✔
161
    const sheetName = sheet.name;
1,125✔
162
    const expectedCellA1 = getSheetHeader(sheetName, 0, 0);
1,125✔
163
    const expectedCellB1 = getSheetHeader(sheetName, 1, 0);
1,125✔
164

165
    const hasData = sheet.data && sheet.data[0];
1,125✔
166
    const cellA1 = hasData ? sheet.data[0][0] : undefined;
1,125!
167
    const cellB1 = hasData && sheet.data[0].length > 1 ? sheet.data[0][1] : undefined;
1,125✔
168

169
    const isMissing =
170
        cellA1 === null ||
1,125✔
171
        cellA1 === undefined ||
172
        cellB1 === null ||
173
        cellB1 === undefined ||
174
        String(cellA1).trim() === "" ||
175
        String(cellB1).trim() === "" ||
176
        (typeof cellA1 === "string" && typeof cellB1 === "number");
177

178
    if (isMissing) {
1,125✔
179
        addWarning(
27✔
180
            output,
181
            constants.warnings.additionalSheetMissingColumnHeaderWarning(
182
                sheetName,
183
                expectedCellA1,
184
                expectedCellB1
185
            )
186
        );
187
        return;
27✔
188
    }
189

190
    if (cellA1 !== expectedCellA1 || cellB1 !== expectedCellB1) {
1,098✔
191
        addWarning(
27✔
192
            output,
193
            constants.warnings.additionalSheetIncorrectColumnHeaderWarning(
194
                sheetName,
195
                expectedCellA1,
196
                expectedCellB1
197
            )
198
        );
199
    }
200
};
201

202
const parseOptimizationDiagnosticsSheet = sheet => {
3✔
203
    let output = {
39✔
204
        data: {
205
            Parameters: {},
206
            MSE: {
207
                "column-headers": [],
208
                Genes: {},
209
            },
210
        },
211
        errors: [],
212
        warnings: [],
213
    };
214
    let currentParameter;
215
    let currentValue;
216
    let currentGene;
217
    let currentMSE = [];
39✔
218
    // Check Headers
219
    checkValidHeaderAndAddWarnings(output, sheet);
39✔
220
    // Check Parameter Section
221
    let row = 1;
39✔
222
    // a missing row is the indicator to move onto the MSE
223
    while (sheet.data[row].length > 0) {
39✔
224
        currentParameter = sheet.data[row][0];
162✔
225
        currentValue = sheet.data[row][1];
162✔
226
        if (currentParameter === undefined || currentParameter.replace(/\s+/g, "") === "") {
162✔
227
            if (currentValue === undefined || currentValue.replace(/\s+/g, "") === "") {
3!
228
                // if there is no parameter or value assume that its time to move on
229
                row++;
3✔
230
                break;
3✔
231
            }
232
        }
233
        if (sheet.data[row].length > 2) {
159✔
234
            addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
3✔
235
        }
236
        if (!optimizationDiagnosticsParameters.includes(currentParameter)) {
159✔
237
            if (currentParameter === "Gene") {
3!
238
                row--;
×
239
                break;
×
240
            }
241
            addWarning(
3✔
242
                output,
243
                constants.warnings.unknownOptimizationDiagnosticsParameter(
244
                    sheet.name,
245
                    currentParameter
246
                )
247
            );
248
        } else if (typeof currentValue !== "number") {
156✔
249
            addWarning(
3✔
250
                output,
251
                constants.warnings.invalidOptimizationDiagnosticsValue(sheet.name, currentParameter)
252
            );
253
        } else {
254
            output.data.Parameters[currentParameter] = currentValue;
153✔
255
        }
256
        row++;
159✔
257
    }
258
    // Skip until Gene section
259
    while (sheet.data[row] !== undefined && sheet.data[row].length < 1) {
39✔
260
        row++;
36✔
261
    }
262
    // Check Gene section MSE's
263
    if (sheet.data[row].length > 1) {
39✔
264
        if (sheet.data[row][0] !== "Gene") {
39✔
265
            addWarning(
6✔
266
                output,
267
                constants.warnings.incorrectMSEGeneHeaderWarning(sheet.name, row + 1)
268
            );
269
        }
270
        for (let col = 1; col < sheet.data[row].length; col++) {
39✔
271
            if (!sheet.data[row][col].includes("MSE")) {
234✔
272
                addWarning(
9✔
273
                    output,
274
                    constants.warnings.incorrectMSEHeaderWarning(
275
                        sheet.name,
276
                        sheet.data[row][col],
277
                        row + 1,
278
                        constants.numbersToLetters[col]
279
                    )
280
                );
281
            }
282
            // we still push the header (even tho it's sus) because the gene MSE's are
283
            // dependent on the order of the column headers
284
            output.data.MSE["column-headers"].push(sheet.data[row][col]);
234✔
285
        }
286
        row++;
39✔
287
        // on to the actual genes
288
        while (row < sheet.data.length) {
39✔
289
            if (sheet.data[row].length > output.data.MSE["column-headers"].length + 1) {
585✔
290
                addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
6✔
291
            }
292
            currentGene = sheet.data[row][0];
585✔
293
            // if it's a valid gene set the key = MSE value
294
            if (validGeneName(output, sheet.name, currentGene, row)) {
585✔
295
                for (let col = 1; col <= output.data.MSE["column-headers"].length; col++) {
585✔
296
                    if (typeof sheet.data[row][col] === "number") {
3,510✔
297
                        currentMSE.push(sheet.data[row][col]);
3,468✔
298
                    } else if (sheet.data[row][col] === undefined) {
42✔
299
                        addWarning(
30✔
300
                            output,
301
                            constants.warnings.missingMSEDataWarning(
302
                                sheet.name,
303
                                row + 1,
304
                                constants.numbersToLetters[col]
305
                            )
306
                        );
307
                    } else {
308
                        addWarning(
12✔
309
                            output,
310
                            constants.warnings.invalidMSEDataWarning(
311
                                sheet.name,
312
                                row + 1,
313
                                constants.numbersToLetters[col]
314
                            )
315
                        );
316
                    }
317
                }
318
                output.data.MSE.Genes[currentGene] = currentMSE;
585✔
319
                currentMSE = [];
585✔
320
            }
321
            row++;
585✔
322
        }
323
    }
324
    return output;
39✔
325
};
326

327
const checkValidGenesAndValuesInTwoColumnSheet = (
3✔
328
    output,
329
    sheet,
330
    row,
331
    genesMissingValue,
332
    valuesMissingGene
333
) => {
334
    if (!sheet.data[row]) return;
6,132!
335

336
    const currentGene = sheet.data[row][0];
6,132✔
337
    const currentValue = sheet.data[row][1];
6,132✔
338

339
    const isValueEmpty =
340
        currentValue === null ||
6,132✔
341
        currentValue === undefined ||
342
        (typeof currentValue === "string" && currentValue.trim() === "");
343

344
    if (validGeneName(output, sheet.name, currentGene, row + 1)) {
6,132✔
345
        if (isValueEmpty) {
6,045✔
346
            genesMissingValue.push(currentGene);
27✔
347
            output.data[currentGene] = undefined;
27✔
348
        } else {
349
            if (typeof currentValue === "number") {
6,018✔
350
                output.data[currentGene] = currentValue;
6,009✔
351
            } else {
352
                addError(
9✔
353
                    output,
354
                    constants.errors.invalidValueError(
355
                        sheet.name,
356
                        currentValue,
357
                        row + 1,
358
                        getSheetHeader(sheet.name, 1, 0)
359
                    )
360
                );
361
            }
362
        }
363
    } else if (!isValueEmpty) {
87✔
364
        valuesMissingGene.push(currentValue);
87✔
365
    }
366
};
367

368
const checkOrderOfGenesInTwoColumnSheet = (output, genesInNetwork, sheetName) => {
3✔
369
    const genesInSheet = Object.keys(output.data);
465✔
370
    const presentNetworkGenesInSheet = genesInNetwork.filter(gene => genesInSheet.includes(gene));
2,439✔
371

372
    const isWrongGeneOrder =
373
        genesInSheet.length > 0 &&
465✔
374
        !presentNetworkGenesInSheet.every((gene, index) => gene === genesInSheet[index]);
2,322✔
375

376
    if (isWrongGeneOrder) {
465✔
377
        addWarning(output, constants.warnings.wrongGeneOrderInTwoColumnSheet(sheetName));
9✔
378

379
        // Matching order with genes in network
380
        const sortedData = {};
9✔
381
        genesInNetwork.forEach(gene => {
9✔
382
            sortedData[gene] = output.data[gene];
27✔
383
        });
384
        output.data = sortedData;
9✔
385
    }
386
};
387

388
const parseTwoColumnSheet = (sheet, genesInNetwork) => {
3✔
389
    let output = {
843✔
390
        data: {},
391
        errors: [],
392
        warnings: [],
393
    };
394

395
    if (sheet.data.length === 0) {
843✔
396
        return output;
9✔
397
    }
398

399
    const genesMissingValue = [];
834✔
400
    const valuesMissingGene = [];
834✔
401

402
    // check to see if the genes are strings and the values are numbers
403

404
    for (let row = 0; row < sheet.data.length; row++) {
834✔
405
        if (sheet.data[row].length > 2) {
6,945✔
406
            addWarning(output, constants.warnings.extraneousDataWarning(sheet.name, row + 1));
15✔
407
        }
408
        if (row === 0) {
6,945✔
409
            checkValidHeaderAndAddWarnings(output, sheet);
834✔
410

411
            const isHeaderMissing = output.warnings.some(
834✔
412
                w => w.warningCode === `MISSING_COLUMN_HEADER_${sheet.name.toUpperCase()}`
45✔
413
            );
414

415
            if (!isHeaderMissing) {
834✔
416
                continue;
813✔
417
            }
418
        }
419

420
        checkValidGenesAndValuesInTwoColumnSheet(
6,132✔
421
            output,
422
            sheet,
423
            row,
424
            genesMissingValue,
425
            valuesMissingGene
426
        );
427
    }
428

429
    // Check whether all genes are missing values
430
    const isAllGenesMissingValues =
431
        genesInNetwork && genesInNetwork.every(gene => genesMissingValue.includes(gene));
834✔
432
    if (isAllGenesMissingValues) {
834✔
433
        addWarning(
9✔
434
            output,
435
            constants.warnings.missingAllGenesAndValuesInTwoColumnSheet(
436
                sheet.name,
437
                /*isAllGenesMissing=*/ false
438
            )
439
        );
440
    }
441

442
    // Check for values that are missing genes
443
    if (valuesMissingGene.length > 0) {
834✔
444
        addWarning(
72✔
445
            output,
446
            constants.warnings.missingGeneIdsWithValuesInTwoColumnSheet(
447
                sheet.name,
448
                valuesMissingGene
449
            )
450
        );
451
    }
452

453
    // Check for missing genes in sheet
454
    if (genesInNetwork) {
834✔
455
        //  Check if the output data keys (genes in sheet) include all genes in the network
456
        const missingGenes = genesInNetwork.filter(g => !Object.keys(output.data).includes(g));
2,439✔
457
        if (missingGenes.length > 0) {
465✔
458
            if (missingGenes.length === genesInNetwork.length) {
90✔
459
                addWarning(
9✔
460
                    output,
461
                    constants.warnings.missingAllGenesAndValuesInTwoColumnSheet(
462
                        sheet.name,
463
                        /*isAllGenesMissing=*/ true
464
                    )
465
                );
466
            } else {
467
                addWarning(
81✔
468
                    output,
469
                    constants.warnings.missingGenesAndValuesInTwoColumnSheetWarningWhenImporting(
470
                        sheet.name,
471
                        missingGenes.join(", ")
472
                    )
473
                );
474
            }
475
        }
476

477
        checkOrderOfGenesInTwoColumnSheet(output, genesInNetwork, sheet.name);
465✔
478
    }
479

480
    return output;
834✔
481
};
482

483
module.exports = function (workbookFile, genesInNetwork) {
3✔
484
    let output = {
270✔
485
        meta: {
486
            data: {},
487
            errors: [],
488
            warnings: [],
489
        }, // optimization_parameters only
490
        twoColumnSheets: {}, // 2-column data
491
        meta2: {}, // optimation_diagnostics only //temporary until where it goes is decided
492
    };
493
    workbookFile.forEach(function (sheet) {
270✔
494
        if (sheet.name === "optimization_parameters") {
2,505✔
495
            output.meta = parseMetaDataSheet(sheet);
252✔
496
            // above line creates an object from the optimization parameters sheet
497
            // these are part of the "meta" property
498
        } else if (constants.TWO_COL_SHEET_NAMES.includes(sheet.name)) {
2,253✔
499
            output.twoColumnSheets[sheet.name] = parseTwoColumnSheet(sheet, genesInNetwork);
843✔
500
        } else if (sheet.name === "optimization_diagnostics") {
1,410✔
501
            output.meta2 = parseOptimizationDiagnosticsSheet(sheet);
39✔
502
        }
503
    });
504
    return output;
270✔
505
};
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