UNPKG

benchling_typescript_sdk

Version:

Typescript SDK for Benchling API

362 lines (344 loc) 15.4 kB
import { UnexpectedBenchlingConfigError } from "../../utils/clientError"; import { validateSimpleTransferInstructions, updateTransferInstructions, } from "../../utils/validateTransferInstructions"; import { BenchlingClient } from "../BenchlingClient"; import type { Container, CreatePlatesAndTransferContentsRequest, ExtendedPlateTransfers, MultipleContainersTransfer, MultipleNewPlateContainersTransfer, Plate, PlateContentsTransferInstruction, PlateCreate, TransferedPlateIdentifiers, UserPlateContentsTransferInstruction, } from "../types"; /** * Transfers contents between plate or container based on barcode instructions * - if wells are inculded, barcode is assumed to be a plate barcode * - if well is not included, barcode is assumed to be a container barcode */ export class TransferContentsFromBarcodeInstructions { private benchling: BenchlingClient; constructor(client: BenchlingClient) { this.benchling = client; } private async validateUserTextInventoryIdentifiersAndInsertBarcodes( plateTransferInstructions: UserPlateContentsTransferInstruction[] ): Promise<UserPlateContentsTransferInstruction[]> { // first validate that all are barcodes. users might be using plate names instead! let [plateIdentifiers, containerIdentifiers]: [string[], string[]] = plateTransferInstructions.reduce( (acc, t) => { if (!t.sourceBarcode || !t.destinationBarcode) { throw new UnexpectedBenchlingConfigError({ message: `Both source and destination barcodes are required for transfer instructions.`, body: JSON.stringify(t), url: "transferContentsFromUserInstructions", }); } if (t.sourceWell) { acc[0].push(t.sourceBarcode); // source is a plate barcode } else { acc[1].push(t.sourceBarcode); // source is a container barcode } if (t.destinationWell) { acc[0].push(t.destinationBarcode); // destination is a plate barcode } else { acc[1].push(t.destinationBarcode); // destination is a container barcode } return acc; }, [[], []] as [string[], string[]] ); let registryId = this.benchling.registryId; // validate barcodes if (!registryId) { registryId = process.env.BENCHLING_REGISTRY_ID; if (!registryId) { throw new UnexpectedBenchlingConfigError({ message: `Registry ID is not set in BenchlingClient or environment variables.`, body: "", url: "", }); } } const plateResponse = await this.benchling.inventory.validate_barcodes( plateIdentifiers, registryId ); const containerResponse = await this.benchling.inventory.validate_barcodes( containerIdentifiers, registryId ); let [containerBarcodesInRegistry, containerBarcodesNotInRegistry] = containerResponse.reduce( (acc, item) => { if (!item.isValid) { // not valid means you cannot re-use this barcode it is already in the registry acc[0].push(item?.barcode || ""); } else { acc[1].push(item?.barcode || ""); } return acc; }, [[], []] as [string[], string[]] ); let [plateBarcodesInRegistry, plateBarcodesNotInRegistry] = plateResponse.reduce( (acc, item) => { if (!item.isValid) { // not valid means you cannot re-use this barcode it is already in the registry acc[0].push(item?.barcode || ""); } else { acc[1].push(item?.barcode || ""); } return acc; }, [[], []] as [string[], string[]] ); if ( containerBarcodesInRegistry.length + plateBarcodesInRegistry.length === plateIdentifiers.length + containerIdentifiers.length ) { return plateTransferInstructions; // all identifiers are barcodes } let containerIdentifierBarcodeMap: Record<string, string> = {}; let plateIdentifierBarcodeMap: Record<string, string> = {}; let possibleContainerNames = containerBarcodesNotInRegistry; let possiblePlateNames = plateBarcodesNotInRegistry; for (const n in possibleContainerNames) { containerIdentifierBarcodeMap[n] = ""; } for (const n in possiblePlateNames) { plateIdentifierBarcodeMap[n] = ""; } if (containerBarcodesNotInRegistry.length > 0) { let containersGenerator = await this.benchling.containers.listContainersNoLimits( { "names.anyOf": possibleContainerNames.join(",") } // get containers by names ); for await (const page of containersGenerator) { for (const container of page) { if (!container.barcode || !container.id || !container.name) { throw new UnexpectedBenchlingConfigError({ body: JSON.stringify(container), message: `Container without name, barcode or id from listContainers response`, url: "containers", }); } containerIdentifierBarcodeMap[container.name] = container.barcode; // map barcode to id } } } if (plateBarcodesNotInRegistry.length > 0) { // throws error if name is found on more that one plate // users must ensure plate names are unique let { ids, barcodes } = await this.benchling.plates.getSingleBarcodeAndIdMapsFromPlateNames( possiblePlateNames ); plateIdentifierBarcodeMap = barcodes; // this is already compiled for us (TODO make sure??!) } // inject barcodes where the names u sed to be and return all barcode instructions // if all barcodes are already in registry valid, we can continue. they are all barcodes and thus unique identifiers return plateTransferInstructions; } /* * Transfers contents from barcode / UNIQUE name instructions * - if wells are included, barcode is assumed to be a plate barcode * - if well is not included, barcode is assumed to be a container barcode * This method will transfer contents based on user provided barcodes. */ public async transferContentsFromUserInstructions( plateTransferInstructions: UserPlateContentsTransferInstruction[] ): Promise<TransferedPlateIdentifiers> { let results: TransferedPlateIdentifiers = { source: [], destination: [] }; let barocdeTransferInstructions = await this.validateUserTextInventoryIdentifiersAndInsertBarcodes(plateTransferInstructions); let transferInstructions: PlateContentsTransferInstruction[] = await this.convertBarcodeUserTransferInstructions(barocdeTransferInstructions); return results; // TODO - implement transfer logic WITH ordering considered } public async convertBarcodeUserTransferInstructions( transferInstructions: UserPlateContentsTransferInstruction[] ): Promise<PlateContentsTransferInstruction[]> { let updatedInstructions: PlateContentsTransferInstruction[] = []; const barcodeToIDMap: Record<string, string> = {}; const barcodeToVolumeMap: Record<string, number> = {}; const barcodeRole: Record<string, Set<"source" | "destination">> = {}; // validate that barcodes only act as a source or destinatnion let missingAnyVolumes: boolean = false; // to track barcodes that are missing volumes const { plateBarcodes, containerBarcodes } = transferInstructions.reduce( // we infer them to be containers IF there is no well id (acc, t) => { if (!barcodeRole[t.sourceBarcode]) { barcodeRole[t.sourceBarcode] = new Set(); // Initialize with a new Set if it doesn't exist } if (!barcodeRole[t.destinationBarcode]) { barcodeRole[t.destinationBarcode] = new Set(); // Initialize with a new Set if it doesn't exist } barcodeRole[t.sourceBarcode].add("source"); // Mark role as source barcodeRole[t.destinationBarcode].add("destination"); // Mark role as destination if (!t.transferVolume && t.transferVolume != 0) { // 0 is valid! missingAnyVolumes = true; } if (t.sourceWell) { // If there is a wellId, it's a plateBarcode acc.plateBarcodes.add(t.sourceBarcode); } else { // Otherwise, it's a containerBarcode acc.containerBarcodes.add(t.sourceBarcode); } if (t.destinationWell) { // If there is a wellId, it's a plateBarcode acc.plateBarcodes.add(t.destinationBarcode); } else { // Otherwise, it's a containerBarcode acc.containerBarcodes.add(t.destinationBarcode); } return acc; }, { plateBarcodes: new Set<string>(), containerBarcodes: new Set<string>() } ); // Convert sets to arrays const uniquePlateBarcodes = Array.from(plateBarcodes); const uniqueContainerBarcodes = Array.from(containerBarcodes); for (const barcode of uniquePlateBarcodes) { barcodeToIDMap[barcode] = ""; // initialize with empty string } for (const barcode of uniqueContainerBarcodes) { barcodeToIDMap[barcode] = ""; // initialize with empty string } try { // get all containers with the barcodes in transferInstructions // we assume they are containers if there is no well attribute const pageGeneratorContainers = await this.benchling.containers.listContainersNoLimits({ barcodes: uniqueContainerBarcodes.join(","), }); for await (const page of pageGeneratorContainers) { for (const container of page) { if (!container.barcode || !container.id) { throw new UnexpectedBenchlingConfigError({ body: JSON.stringify(container), message: `Container without barcode or id found in listContainers response`, url: "containers", }); } // determine total volume of container to enable default 'transfer all' functionality. // user does not need to indicate the volume if ( // find source container volume if it is empty and we need to move it all missingAnyVolumes && // we don't need to know the volume if user indicated volumes to transfer barcodeRole[container.barcode].has("source") && !barcodeRole[container.barcode].has("destination") ) { if (container.quantity && container.quantity.value) { if (container.quantity.units === "uL") { barcodeToVolumeMap[container.barcode] = container.quantity.value; } else if (container.quantity.units === "mL") { barcodeToVolumeMap[container.barcode] = container.quantity.value * 1000; // convert mL to uL } else { throw new UnexpectedBenchlingConfigError({ message: `Unexpected quantity units ${container.quantity.units} for container ${container.barcode}. Expected 'uL' or 'mL'.`, body: JSON.stringify(container), url: "containers", }); } } else { throw new UnexpectedBenchlingConfigError({ message: `No 'quantity' found conatiner response: ${container.barcode}`, body: JSON.stringify(container), url: "containers", }); } } barcodeToIDMap[container.barcode] = container.id; } } } catch (error) { console.error("Could not get container Ids for barcodes:", error); console.error(uniqueContainerBarcodes); throw error; } // get all plates with the barcodes in transferInstructions try { const pageGeneratorBarcodes = this.benchling.plates.listPlatesNoLimits({ barcodes: uniquePlateBarcodes.join(","), }); for await (const page of pageGeneratorBarcodes) { for (const plateEntity of page) { barcodeToIDMap[plateEntity.barcode] = plateEntity.id; // map barcode to id } } } catch (error) { console.error("Could not get container Ids for barcodes:", error); console.error(uniqueContainerBarcodes); throw error; } // / TODO - this logical can be performed inside previous call to list plates!! type PlateBarcode = string; type WellID = string; type ContainerId = string; let plateWellsContainerIds: { [key: PlateBarcode]: { [key: WellID]: ContainerId } } = {}; // to get container ids for all wells by barcodes for (const plateBarcode in uniquePlateBarcodes) { plateWellsContainerIds[plateBarcode] = {}; let plateId = barcodeToIDMap[plateBarcode]; let plateEntity = await this.benchling.plates.getPlate(plateId); let wells = plateEntity.wells || []; for (const wellname in wells) { let container = wells[wellname]; if ("id" in container && "barcode" in container) { plateWellsContainerIds[plateBarcode][wellname.toUpperCase()] = container.id || ""; // map wellId to containerId // TODO if ( missingAnyVolumes && barcodeRole[plateBarcode].has("source") && !barcodeRole[plateBarcode].has("destination") ) { // TODO -- Find the volume to transfer!! if (container.quantity && container.quantity.value) { if (container.quantity.units === "uL") { barcodeToVolumeMap[container.barcode || ""] = container.quantity.value; } else if (container.quantity.units === "mL") { barcodeToVolumeMap[container.barcode || ""] = container.quantity.value * 1000; // convert mL to uL } else { throw new UnexpectedBenchlingConfigError({ message: `Unexpected quantity units ${container.quantity.units} for container ${container.barcode}. Expected 'uL' or 'mL'.`, body: JSON.stringify(container), url: "containers", }); } } else { throw new UnexpectedBenchlingConfigError({ message: `No 'quantity' found conatiner response: ${container.barcode}`, body: JSON.stringify(container), url: "containers", }); } } } else { throw new UnexpectedBenchlingConfigError({ message: `Cannot view ${wellname} in plate ${plateEntity.name} (${plateEntity.barcode}). Check your permissions.`, body: JSON.stringify(container), url: `plates/${plateId}`, }); } } } const validatedData = validateSimpleTransferInstructions(transferInstructions); if (validatedData.erroredInstructions.length > 0) { throw new Error( `Invalid transfer instructions found: ${JSON.stringify(validatedData.erroredInstructions)}` ); } transferInstructions = validatedData.validInstructions; // well ids are accurate updatedInstructions = updateTransferInstructions( transferInstructions, plateWellsContainerIds, // map of plate barcodes to well ids to container ids barcodeToIDMap // map of plate and container barcodes to container ids or plate ids ); return updatedInstructions; } }