benchling_typescript_sdk
Version:
Typescript SDK for Benchling API
216 lines (200 loc) • 8.89 kB
text/typescript
import * as fs from "fs";
import { BenchlingClient } from "../BenchlingClient";
import { DeleteBarcodes } from "./DeleteInventory";
import type {
Container,
CreatePlatesAndTransferContentsRequest,
ExtendedPlateTransfers,
MultipleContainersTransfer,
MultipleNewPlateContainersTransfer,
Plate,
PlateCreate,
} from "../types";
function pruneToPlate(input: Record<string, any>): PlateCreate {
const plateKeys: (keyof PlateCreate)[] = [
"barcode",
"fields",
"name",
"parentStorageId",
"projectId",
"schemaId",
"wells",
];
// Filter the input object to include only PlateCreate keys
const result: Partial<PlateCreate> = {};
for (const key of plateKeys) {
if (key in input) {
result[key] = input[key];
}
}
return result as PlateCreate;
}
/**
* Creates plates and transfers contents into them.
* - plate barcode and name are optional
* - entites must be containable with "transferQuantity" and "sourceConcentration"
*/
export class CreatePlatesWithContainableEntities {
private benchling: BenchlingClient;
constructor(client: BenchlingClient) {
this.benchling = client;
}
/**
* Creates plates and transfers contents into them.
* - plate barcode and name are optional
* - entites must be containable with "transferQuantity" and "sourceConcentration"
* @param {CreatePlatesAndTransferContentsRequest[]} - An array of requests specifying the plates to create and their transfer details.
* @returns {Promise<{ plates: Plate[], filledWells: Container[] }>} A promise resolving to an object containing the plates and all the plates' filled wells.
*/
public async createPlatesAndTransferContents(
platesToCreateWithTransfers: CreatePlatesAndTransferContentsRequest[]
): Promise<Plate[]> {
// User asks to create X plates with contents to transfer
// Create Plates, then fill the wells, then return the plates and the final filled wells with contents
// will create or update Plates with Transfered Contents
const createdPlateIds: string[] = []; // must be able to archive all created plates if anything fails!!
const platesToCreate: ExtendedPlateTransfers[] = platesToCreateWithTransfers.map((d) => ({
...d,
id: "",
}));
try {
const createdPlates = await this.injectPlateIdentifiers(platesToCreate, createdPlateIds); // create plates in benchling and then update plate.id, and possibly plate.barcode to created plates
const wellTransfers: MultipleContainersTransfer[] = await this.compileTransfersForNewWells(
platesToCreate
); // return containers with transfered contents
// !isLambdaRuntime() &&
// fs.writeFileSync(
// "logs/wellTransfers.json",
// JSON.stringify(wellTransfers, null, 2),
// "utf-8"
// ); // save the well transfers to a file for debugging
await this.benchling.containers.waitForAllTransfers(wellTransfers, 1000); // it is possible but unlikely
return createdPlates; // return the created plates and the filled containers
} catch (error) {
//why is this error different suddenly?
console.error(error);
console.error("Error creating plates and transferring contents:", error);
console.error("Archiving all previously created plates:");
console.error(createdPlateIds.join(", "));
// let deletePlates = await new DeleteBarcodes(this.benchling).deletePlates(
// createdPlateIds, // ids or barcodes
// "Made in error",
// true
// );
// TODO archive all previously created plates and throw error to user
throw error;
}
}
private async injectPlateIdentifiers(
platesToCreate: ExtendedPlateTransfers[],
createdPlateIds: string[]
): Promise<Plate[]> {
const createdPlates: Plate[] = [];
for (const plateToCreate of platesToCreate) {
let onlyPlate: PlateCreate = pruneToPlate(plateToCreate);
const createdPlate = await this.benchling.plates.createPlate(onlyPlate);
if ("error" in createdPlate) {
console.error("Error creating plate:", createdPlate);
console.error("Archiving all previously created plates:");
console.error(createdPlateIds.join(", "));
throw new Error(
"\nFailed to create plate. Still have not implemented archiving previous plates: " +
createdPlateIds.join(", ") +
".\n"
); // r96wp175:D1 vs con_9ENpL3BU
// TODO archive all previously created plates and throw error to user
} else if ("id" in createdPlate) {
createdPlates.push(createdPlate);
createdPlateIds.push(createdPlate.id);
plateToCreate.id = createdPlate.id; // update plateToCreate with the created plate id
plateToCreate.name = createdPlate.name; // update plateToCreate with the created plate name
plateToCreate.barcode = createdPlate.barcode; // update plateToCreate with the created plate barcode
} else {
throw new Error(
"Internal Program Error: Contact Nicole Cannon. Failed to create a plate for unknown reason"
);
}
}
return createdPlates;
}
private async compileTransfersForNewWells(
platesToFill: ExtendedPlateTransfers[]
): Promise<MultipleContainersTransfer[]> {
const wellTransfers: MultipleContainersTransfer[] = []; // combine all transfers from all plates
// throw new Error("Not yet implemented");
for (const plate of platesToFill) {
const plateId = plate.id;
let plateWellTransfers = plate.transfers;
let plateWellGenerator = this.benchling.containers.listContainers({
ancestorStorageId: plateId,
pageSize: 500, // max page size - will always all wells of a plate in first page! important!
returning: "containers.id, containers.barcode", // should be empty
});
// for each page in idGenerator
for await (const page of plateWellGenerator) {
for (const container of page) {
if (!container.barcode || !container.id) {
throw new Error(
`Internal Program Error: Container without barcode or id found in plate ${plateId}\nContainer: ${JSON.stringify(
container
)}`
);
}
let destinationContainerId = container.id; // this is the id of the container (aka the well) in benchling
let wellId = container.barcode.split(":")[1]; // this is the wellId the user wants to transfer to in plateId
const matchingTransfers = plateWellTransfers.filter(
(pwt) => pwt.wellId.toUpperCase() === wellId.toUpperCase()
); // get all transfers into the same container
// Remove all matching transfers from plateWellTransfers
plateWellTransfers = plateWellTransfers.filter(
(pwt) => pwt.wellId.toUpperCase() !== wellId.toUpperCase()
); // reduce search space for next iteration
// Convert each matching transfer into MultipleContainersTransfer and add to wellTransfers
matchingTransfers.forEach((transfer) => {
const transferToAdd: MultipleContainersTransfer =
this.convertToMultipleContainersTransfer(transfer, destinationContainerId);
wellTransfers.push(transferToAdd);
});
}
}
}
return wellTransfers;
}
private convertToMultipleContainersTransfer(
transfer: MultipleNewPlateContainersTransfer,
destinationContainerId: string
): MultipleContainersTransfer {
let transferToAdd: MultipleContainersTransfer = {
destinationContainerId: destinationContainerId,
};
// Conditionally add keys if they are defined in `transfer`
if (transfer.transferQuantity !== undefined) {
transferToAdd.transferQuantity = transfer.transferQuantity;
}
if (transfer.sourceContainerId !== undefined) {
transferToAdd.sourceContainerId = transfer.sourceContainerId;
}
if (transfer.sourceEntityId !== undefined) {
transferToAdd.sourceEntityId = transfer.sourceEntityId;
}
if (transfer.sampleOwnerIds !== undefined) {
transferToAdd.sampleOwnerIds = transfer.sampleOwnerIds;
}
if (transfer.sourceBatchId !== undefined) {
transferToAdd.sourceBatchId = transfer.sourceBatchId;
}
if (transfer.sourceConcentration !== undefined) {
transferToAdd.sourceConcentration = transfer.sourceConcentration;
}
if (transfer.transferVolume !== undefined) {
transferToAdd.transferVolume = transfer.transferVolume;
}
if (transfer.restrictedSamplePartyIds !== undefined) {
transferToAdd.restrictedSamplePartyIds = transfer.restrictedSamplePartyIds;
}
if (transfer.restrictionStatus !== undefined) {
transferToAdd.restrictionStatus = transfer.restrictionStatus;
}
return transferToAdd;
}
}