benchling_typescript_sdk
Version:
Typescript SDK for Benchling API
362 lines (344 loc) • 15.4 kB
text/typescript
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;
}
}