Skip to content

Commit

Permalink
fix: pr comments
Browse files Browse the repository at this point in the history
  • Loading branch information
MacQSL committed Dec 4, 2024
1 parent 7cc6f4a commit 32b4c89
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 59 deletions.
71 changes: 55 additions & 16 deletions api/src/services/import-services/critter/critter-header-configs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { z } from 'zod';
import { CSVConfigUtils } from '../../../utils/csv-utils/csv-config-utils';
import { CSVCellSetter, CSVCellValidator, CSVParams } from '../../../utils/csv-utils/csv-config-validation.interface';
import {
CSVCellSetter,
CSVCellValidator,
CSVError,
CSVParams
} from '../../../utils/csv-utils/csv-config-validation.interface';
import { validateZodCell } from '../../../utils/csv-utils/csv-header-configs';
import { NestedRecord } from '../../../utils/nested-record';
import { CritterCSVStaticHeader } from './import-critters-service';
Expand All @@ -9,9 +14,10 @@ import { CritterCSVStaticHeader } from './import-critters-service';
* Get the critter alias cell validator.
*
* Rules:
* 1. The cell must be a string with a length between 1 and 50
* 2. The cell must be unique in the survey
* 3. The cell must be unique in the CSV
* 1. The cell can be a string with a length between 1 and 50
* 2. The cell can be a number with a min value of 0
* 3. The cell must be unique in the survey
* 4. The cell must be unique in the CSV
*
* @param {Set<string>} surveyAliases The survey aliases.
* @param {CSVConfigUtils<CritterCSVStaticHeader>} configUtils The CSV config utils.
Expand All @@ -22,7 +28,7 @@ export const getCritterAliasCellValidator = (
configUtils: CSVConfigUtils<CritterCSVStaticHeader>
): CSVCellValidator => {
return (params: CSVParams) => {
const cellErrors = validateZodCell(params, z.string().trim().min(1).max(50));
const cellErrors = validateZodCell(params, z.union([z.string().trim().min(1).max(50), z.number().min(0)]));
const isAliasUnique = configUtils.isCellUnique('ALIAS', params.cell);

if (cellErrors.length) {
Expand Down Expand Up @@ -69,18 +75,13 @@ export const getCritterCollectionUnitCellValidator = (
return [];
}

// The row TSN value
const rowTsn = Number(configUtils.getCellValue('ITIS_TSN', params.row));

// The collection unit cell value
const collectionUnitCellValue = String(params.cell);

// The collection category (for clarity)
const collectionCategory = params.header;
const rowTsn = Number(configUtils.getCellValue('ITIS_TSN', params.row)); // Row TSN
const collectionUnitCellValue = String(params.cell); // Cell value
const collectionCategory = params.header; // Current header ie: collection category

const rowDictionaryTsn = rowDictionary.get(rowTsn);

// Check if the row TSN has collection units
// Check if the row TSN has associated collection units
if (!rowDictionaryTsn) {
return [
{
Expand Down Expand Up @@ -160,8 +161,8 @@ export const getCritterSexCellValidator = (
configUtils: CSVConfigUtils<CritterCSVStaticHeader>
): CSVCellValidator => {
return (params: CSVParams) => {
const rowTsn = Number(configUtils.getCellValue('ITIS_TSN', params.row));
const sexCellValue = String(params.cell);
const rowTsn = Number(configUtils.getCellValue('ITIS_TSN', params.row)); // Row TSN
const sexCellValue = String(params.cell); // Cell value

const rowDictionaryTsn = rowDictionary.get(rowTsn);

Expand Down Expand Up @@ -210,3 +211,41 @@ export const getCritterSexCellSetter = (
return rowDictionary.get(rowTsn, sexCellValue);
};
};

/**
* Get the Wildlife Health ID header cell validator.
*
* Rules:
* 1. The Wildlife Health ID must be in the format 'XX-XXXX' or undefined
* 2. The Wildlife Health ID must be unique in the CSV
*
* @param {CSVConfigUtils<CritterCSVStaticHeader>} configUtils The CSV config utils.
* @returns {*} {CSVCellValidator} The validate cell callback
*/
export const getWlhIDCellValidator = (configUtils: CSVConfigUtils<CritterCSVStaticHeader>): CSVCellValidator => {
return (params: CSVParams) => {
const cellErrors: CSVError[] = [];

if (params.cell === undefined) {
return [];
}

const isWlhIdUnique = configUtils.isCellUnique('ALIAS', params.cell);

if (!/^\d{2}-.+/.exec(String(params.cell))) {
cellErrors.push({
error: `Invalid Wildlife Health ID format`,
solution: `Update the Wildlife Health ID to match the expected format 'XX-XXXX'`
});
}

if (!isWlhIdUnique) {
cellErrors.push({
error: `Wildlife Health ID already exists in the CSV`,
solution: `Update the Wildlife Health ID to be unique`
});
}

return cellErrors;
};
};
33 changes: 17 additions & 16 deletions api/src/services/import-services/critter/import-critters-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import {
CSVHeaderConfig,
CSVRowValidated
} from '../../../utils/csv-utils/csv-config-validation.interface';
import {
getDescriptionCellValidator,
getTsnCellValidator,
getWlhIDCellValidator
} from '../../../utils/csv-utils/csv-header-configs';
import { getDescriptionCellValidator, getTsnCellValidator } from '../../../utils/csv-utils/csv-header-configs';
import { getLogger } from '../../../utils/logger';
import { NestedRecord } from '../../../utils/nested-record';
import { CritterbaseService, IBulkCreate } from '../../critterbase-service';
Expand All @@ -27,7 +23,8 @@ import {
getCritterCollectionUnitCellSetter,
getCritterCollectionUnitCellValidator,
getCritterSexCellSetter,
getCritterSexCellValidator
getCritterSexCellValidator,
getWlhIDCellValidator
} from './critter-header-configs';

const defaultLog = getLogger('services/import/import-critters-service');
Expand Down Expand Up @@ -69,8 +66,8 @@ export class ImportCrittersService extends DBService {
ITIS_TSN: { aliases: ['TAXON', 'SPECIES', 'TSN'] },
ALIAS: { aliases: ['NICKNAME', 'NAME', 'ANIMAL_ID'] },
SEX: { aliases: ['TEST'] },
WLH_ID: { aliases: ['WILDLIFE_HEALTH_ID'], validateCell: getWlhIDCellValidator() },
DESCRIPTION: { aliases: ['COMMENTS', 'COMMENT', 'NOTES'], validateCell: getDescriptionCellValidator() }
WLH_ID: { aliases: ['WILDLIFE_HEALTH_ID', 'WILD LIFE HEALTH ID', 'WLHID'] },
DESCRIPTION: { aliases: ['COMMENTS', 'COMMENT', 'NOTES'] }
},
ignoreDynamicHeaders: false
};
Expand All @@ -96,7 +93,7 @@ export class ImportCrittersService extends DBService {
* @returns {*} {Promise<CSVError[]>} List of inserted survey critter ids
*/
async importCSVWorksheet(): Promise<CSVError[]> {
const config = await this._getCSVConfig();
const config = await this.getCSVConfig();

const { errors, rows } = validateCSVWorksheet(this.worksheet, config);

Expand Down Expand Up @@ -132,7 +129,7 @@ export class ImportCrittersService extends DBService {
*
* @returns {*} {Promise<CSVConfig<CritterCSVStaticHeader>>} The Critter CSV config
*/
async _getCSVConfig(): Promise<CSVConfig<CritterCSVStaticHeader>> {
async getCSVConfig(): Promise<CSVConfig<CritterCSVStaticHeader>> {
const [tsnHeaderConfig, aliasHeaderConfig, sexHeaderConfig, dynamicHeadersConfig] = await Promise.all([
this._getTsnHeaderConfig(),
this._getAliasHeaderConfig(),
Expand All @@ -144,10 +141,12 @@ export class ImportCrittersService extends DBService {
staticHeadersConfig: {
ITIS_TSN: tsnHeaderConfig,
ALIAS: aliasHeaderConfig,
SEX: sexHeaderConfig
SEX: sexHeaderConfig,
WLH_ID: getWlhIDCellValidator(this.configUtils),
DESCRIPTION: getDescriptionCellValidator()
},
dynamicHeadersConfig: dynamicHeadersConfig,
ignoreDynamicHeaders: dynamicHeadersConfig ? false : true
ignoreDynamicHeaders: !dynamicHeadersConfig
});
}

Expand Down Expand Up @@ -212,7 +211,8 @@ export class ImportCrittersService extends DBService {
const allowedTsns = new Set(taxonomy.map((taxon) => taxon.tsn));

return {
validateCell: getTsnCellValidator(allowedTsns)
validateCell: getTsnCellValidator(allowedTsns),
setCellValue: (params) => Number(params.cell)
};
}

Expand All @@ -230,7 +230,8 @@ export class ImportCrittersService extends DBService {
const surveyAliases = await this.surveyCritterService.getUniqueSurveyCritterAliases(this.surveyId);

return {
validateCell: getCritterAliasCellValidator(surveyAliases, this.configUtils)
validateCell: getCritterAliasCellValidator(surveyAliases, this.configUtils),
setCellValue: (params) => String(params.cell)
};
}

Expand All @@ -249,14 +250,14 @@ export class ImportCrittersService extends DBService {
const rowTsns = this.configUtils.getUniqueCellValues('ITIS_TSN');
const measurements = await Promise.all(rowTsns.map((tsn) => this.critterbaseService.getTaxonMeasurements(tsn)));

measurements.forEach((measurement) => {
measurements.forEach((measurement, index) => {
const sexMeasurement = measurement.qualitative.find(
(measurement) => measurement.measurement_name.toLowerCase() === 'sex'
);

if (sexMeasurement) {
sexMeasurement.options.forEach((option) => {
const tsn = Number(sexMeasurement.itis_tsn);
const tsn = Number(rowTsns[index]);
const sexLabel = option.option_label;

rowDictionary.set({ path: [tsn, sexLabel], value: option.qualitative_option_id });
Expand Down
8 changes: 4 additions & 4 deletions api/src/utils/csv-utils/csv-config-validation.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export interface CSVParams {
*
* @type {string | undefined}
*/
staticHeader?: string | undefined;
staticHeader?: string;
}

/**
Expand Down Expand Up @@ -140,19 +140,19 @@ export interface CSVError {
*
* @type {(string[] | number[]) | undefined}
*/
values?: string[] | number[] | undefined;
values?: string[] | number[];
/**
* The cell value.
*
* @type {unknown | undefined}
*/
cell?: unknown | undefined;
cell?: unknown;
/**
* The header name.
*
* @type {string | undefined}
*/
header?: string | undefined;
header?: string;
/**
* The row index the error occurred.
*
Expand Down
23 changes: 0 additions & 23 deletions api/src/utils/csv-utils/csv-header-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,3 @@ export const getDescriptionCellValidator = (): CSVCellValidator => {
return validateZodCell(params, z.string().trim().min(1).max(250).optional());
};
};

/**
* Get the Wildlife Health ID header cell validator.
*
* Rules:
* 1. The Wildlife Health ID must be in the format 'XX-XXXX' or undefined
*
* @returns {*} {CSVCellValidator} The validate cell callback
*/
export const getWlhIDCellValidator = (): CSVCellValidator => {
return (params: CSVParams) => {
if (params.cell === undefined || String(params.cell).match(/^\d{2}-.+/)) {
return [];
}

return [
{
error: `Invalid Wildlife Health ID format`,
solution: `Update the Wildlife Health ID to match the expected format 'XX-XXXX'`
}
];
};
};
6 changes: 6 additions & 0 deletions api/src/utils/nested-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ interface INestedRecord<TValue> {
/**
* NestedRecord - A class to handle nested records with case-insensitive keys
*
* @example
* const record = new NestedRecord({ a: { b: 'c' } });
* record.get('A', 'B'); // 'c'
* record.has('A', 'B'); // true
* record.set({ path: ['A', 'B'], value: 'd' });
*
* @class
* @exports
* @template TValue - The final value type
Expand Down

0 comments on commit 32b4c89

Please sign in to comment.