Skip to content

Commit

Permalink
feat: marking strategy now validating correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
MacQSL committed Aug 8, 2024
1 parent 1fa3a8f commit 44b6535
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 59 deletions.
6 changes: 4 additions & 2 deletions api/src/services/critterbase-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { ApiError, ApiErrorType } from '../errors/api-error';
import { getLogger } from '../utils/logger';
import { KeycloakService } from './keycloak-service';

// TODO: TechDebt: Audit the existing types / return types in this file.

export interface ICritterbaseUser {
username: string;
keycloak_guid: string;
Expand Down Expand Up @@ -518,9 +520,9 @@ export class CritterbaseService {
* Fetches body location information for the specified taxon.
*
* @param {string} tsn - The taxon serial number (TSN).
* @returns {Promise<any>} - The response data containing body location information.
* @returns {Promise<IAsSelectLookup[]>} - The response data containing body location information.
*/
async getTaxonBodyLocations(tsn: string): Promise<any> {
async getTaxonBodyLocations(tsn: string): Promise<IAsSelectLookup[]> {
return this._makeGetRequest(CbRoutes['taxon-marking-body-locations'], [
{ key: 'tsn', value: tsn },
{ key: 'format', value: 'asSelect' }
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { z } from 'zod';
import { IAsSelectLookup } from '../../critterbase-service';

/**
* Get CSV Marking schema.
*
* Note: This getter allows custom values to be injected for validation.
*
* @param {IAsSelectLookup[]} colours - Array of supported Critterbase colours
* @returns {*} Custom Zod schema for CSV Markings
*/
export const getCsvMarkingSchema = (
colours: IAsSelectLookup[],
markingTypes: IAsSelectLookup[],
critterBodyLocationsMap: Map<string, IAsSelectLookup[]>
) => {
const colourNames = colours.map((colour) => colour.value.toLowerCase());
const markingTypeNames = markingTypes.map((markingType) => markingType.value.toLowerCase());

const coloursSet = new Set(colourNames);
const markingTypesSet = new Set(markingTypeNames);

return z
.object({
critter_id: z.string({ required_error: 'Unable to find matching survey critter with alias' }).uuid(),
capture_id: z.string({ required_error: 'Unable to find matching capture with date and time' }).uuid(),
body_location: z.string(),
marking_type: z
.string()
.refine(
(val) => markingTypesSet.has(val.toLowerCase()),
`Marking type not supported. Allowed values: ${markingTypeNames.join(', ')}`
)
.optional(),
identifier: z.string().optional(),
primary_colour: z
.string()
.refine(
(val) => coloursSet.has(val.toLowerCase()),
`Colour not supported. Allowed values: ${colourNames.join(', ')}`
)
.optional(),
secondary_colour: z
.string()
.refine(
(val) => coloursSet.has(val.toLowerCase()),
`Colour not supported. Allowed values: ${colourNames.join(', ')}`
)
.optional(),
comment: z.string().optional()
})
.superRefine((schema, ctx) => {
const bodyLocations = critterBodyLocationsMap.get(schema.critter_id);
if (!bodyLocations) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'No taxon body locations found for Critter'
});
} else if (!bodyLocations.filter((location) => location.value === schema.body_location).length) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid body location for Critter. Allowed values: ${bodyLocations
.map((bodyLocation) => bodyLocation.value)
.join(', ')}`
});
}
});
};

/**
* A validated CSV Marking object
*
*/
export type CsvMarking = z.infer<ReturnType<typeof getCsvMarkingSchema>>;
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ import { IDBConnection } from '../../../database/db';
import { CSV_COLUMN_ALIASES } from '../../../utils/xlsx-utils/column-aliases';
import { generateCellGetterFromColumnValidator } from '../../../utils/xlsx-utils/column-validator-utils';
import { IXLSXCSVValidator } from '../../../utils/xlsx-utils/worksheet-utils';
import { IBulkCreateMarking } from '../../critterbase-service';
import { IAsSelectLookup, IBulkCreateMarking, ICritterDetailed } from '../../critterbase-service';
import { DBService } from '../../db-service';
import { SurveyCritterService } from '../../survey-critter-service';
import { CSVImportService, Row } from '../csv-import-strategy.interface';
import { findCaptureIdFromDateTime } from '../utils/datetime';
import { CsvMarking, getCsvMarkingSchema } from './import-markings-service.interface';
import { CsvMarking, getCsvMarkingSchema } from './import-markings-strategy.interface';

// TODO: Update all import services to use language Import<import-name>Strategy
// TODO: Update CSVImportService interface -> CSVImportStrategy

/**
*
* @class ImportMarkingsService
* @extends DBService
* @see CSVImportStrategy
* @see CSVImport
*
*/
export class ImportMarkingsService extends DBService implements CSVImportService {
export class ImportMarkingsStrategy extends DBService implements CSVImportService {
surveyCritterService: SurveyCritterService;
surveyId: number;

Expand Down Expand Up @@ -53,6 +56,40 @@ export class ImportMarkingsService extends DBService implements CSVImportService
this.surveyCritterService = new SurveyCritterService(connection);
}

/**
* Get taxon body locations Map from a list of Critters.
*
* @async
* @param {ICritterDetailed[]} critters - List of detailed critters
* @returns {Promise<Map<string, IAsSelectLookup[]>>} Critter id -> taxon body locations Map
*/
async getTaxonBodyLocationsCritterIdMap(critters: ICritterDetailed[]): Promise<Map<string, IAsSelectLookup[]>> {
const tsnBodyLocationsMap = new Map<number, IAsSelectLookup[]>();
const critterBodyLocationsMap = new Map<string, IAsSelectLookup[]>();

const uniqueTsns = Array.from(new Set(critters.map((critter) => critter.itis_tsn)));

// Only fetch body locations for unique tsns
const bodyLocations = await Promise.all(
uniqueTsns.map((tsn) => this.surveyCritterService.critterbaseService.getTaxonBodyLocations(String(tsn)))
);

// Loop through the flattened responses and set the body locations for each tsn
bodyLocations.flatMap((bodyLocationValues, idx) => {
tsnBodyLocationsMap.set(uniqueTsns[idx], bodyLocationValues);
});

// Now loop through the critters and assign the body locations to the critter id
for (const critter of critters) {
const tsnBodyLocations = tsnBodyLocationsMap.get(critter.itis_tsn);
if (tsnBodyLocations) {
critterBodyLocationsMap.set(critter.critter_id, tsnBodyLocations);
}
}

return critterBodyLocationsMap;
}

/**
* Validate the CSV rows against zod schema.
*
Expand All @@ -63,12 +100,16 @@ export class ImportMarkingsService extends DBService implements CSVImportService
// Generate type-safe cell getter from column validator
const getCellValue = generateCellGetterFromColumnValidator(this.columnValidator);

// Get reference values
// Get validation reference data
const critterAliasMap = await this.surveyCritterService.getSurveyCritterIdAliasMap(this.surveyId);
const colours = await this.surveyCritterService.critterbaseService.getColours();
const markingTypes = await this.surveyCritterService.critterbaseService.getMarkingTypes();

const rowsToValidate: Partial<CsvMarking>[] = [];
// Used to find critter_id -> body location map
const rowCritters: ICritterDetailed[] = [];

// Rows passed to validator
const rowsToValidate: Partial<CsvMarking & { _tsn?: number }>[] = [];

for (const row of rows) {
let critterId, captureId;
Expand All @@ -82,9 +123,12 @@ export class ImportMarkingsService extends DBService implements CSVImportService

const critter = critterAliasMap.get(alias);

// Find the capture_id from the date time columns
captureId = findCaptureIdFromDateTime(critter?.captures ?? [], captureDate, captureTime);
critterId = critter?.critter_id;
if (critter) {
// Find the capture_id from the date time columns
captureId = findCaptureIdFromDateTime(critter.captures, captureDate, captureTime);
critterId = critter.critter_id;
rowCritters.push(critter);
}
}

rowsToValidate.push({
Expand All @@ -98,9 +142,11 @@ export class ImportMarkingsService extends DBService implements CSVImportService
comment: getCellValue(row, 'COMMENT')
});
}
// Get the critter_id -> taxonBodyLocations[] mapping
const critterBodyLocationsMap = await this.getTaxonBodyLocationsCritterIdMap(rowCritters);

// Generate the zod schema with injected lookup values
return z.array(getCsvMarkingSchema(colours, markingTypes)).safeParseAsync(rowsToValidate);
return z.array(getCsvMarkingSchema(colours, markingTypes, critterBodyLocationsMap)).safeParseAsync(rowsToValidate);
}

/**
Expand Down

0 comments on commit 44b6535

Please sign in to comment.