benchling_typescript_sdk
Version:
Typescript SDK for Benchling API
312 lines (281 loc) • 9.85 kB
text/typescript
import { UnexpectedBenchlingConfigError } from "../../utils/clientError";
import { BaseClient } from "../BaseClient";
import { chunkQueries } from "../superModules/chunkQueries";
import type {
Plate,
PlateCreate,
PlateUpdate,
PlatesArchive,
PlatePathGetParamQuery,
PlatesArchivalChange,
SmallPlate,
WellOrInaccessibleResource,
ContainerContent,
} from "../types";
type PlateName = string;
type Barode = string;
type Id = string;
type LimitedListQueries = keyof NonNullable<PlatePathGetParamQuery>;
const limitedSearches: LimitedListQueries[] = [
"names.anyOf",
"names.anyOf.caseSensitive",
"barcodes",
"ids",
"storageContentsIds",
"creatorIds",
];
export class Plates {
private client: BaseClient;
constructor(client: BaseClient) {
this.client = client;
}
public async *listPlatesNoLimits(paramQuery: PlatePathGetParamQuery): AsyncGenerator<Plate[]> {
if (!paramQuery || typeof paramQuery !== "object") {
throw new Error("Invalid query parameters provided.");
}
// Generate the list of chunked queries so that no list of queries is longer than 100
let paramQueries: PlatePathGetParamQuery[] = chunkQueries<NonNullable<PlatePathGetParamQuery>>(
paramQuery,
100,
limitedSearches
);
// Iterate over each chunked query
for (const query of paramQueries) {
// Call list for the current query
for await (const batch of this.listPlates(query)) {
// Yield each batch
yield batch;
}
}
}
static makeSmallContent(contents: ContainerContent[]) {
if (!contents || contents.length === 0) {
return [];
}
return contents.map((content) => {
if (
"entity" in content &&
content.entity &&
"id" in content.entity &&
"schema" in content.entity
) {
return {
entity: {
id: content.entity.id,
schema: { id: content.entity.schema.id },
},
concentration: content.concentration,
};
} else {
throw new Error("Invalid content structure provided.");
}
});
}
static makeSmallWells(wells: Plate["wells"]) {
let smallWells: SmallPlate["wells"] = {};
for (const [wellId, well] of Object.entries(wells)) {
if ("id" in well && "contents" in well) {
smallWells[wellId] = {
id: well.id || "",
barcode: well.barcode || "",
quantity: well.quantity,
contents: Plates.makeSmallContent(well.contents || []),
};
}
}
return smallWells;
}
static makeSmallPlate(plate: Plate): SmallPlate {
if (!plate || !plate.id || !plate.schema || !plate.schema.id || !plate.wells) {
throw new Error("Invalid plate data provided.");
}
return {
id: plate.id,
barcode: plate.barcode || "",
name: plate.name,
schema: { id: plate.schema.id },
wells: this.makeSmallWells(plate.wells),
};
}
public async *listSmallPlates(paramQuery: PlatePathGetParamQuery): AsyncGenerator<SmallPlate[]> {
if (!paramQuery || typeof paramQuery !== "object") {
throw new Error("Invalid query parameters provided.");
}
// Generate the list of chunked queries so that no list of queries is longer than 100
let paramQueries: PlatePathGetParamQuery[] = chunkQueries<NonNullable<PlatePathGetParamQuery>>(
paramQuery,
100,
limitedSearches
);
// Iterate over each chunked query
for (const query of paramQueries) {
// Call list for the current query
for await (const batch of this.listPlates(query)) {
// Yield each batch
let smallBatch: SmallPlate[] = batch.map((plate) => Plates.makeSmallPlate(plate));
yield smallBatch;
}
}
}
public async *listPlatesByBarcodes(
barcodes: string[],
schemaId: string | null,
chunkSize: number = 100 // Default chunk size
): AsyncGenerator<Plate[]> {
// Remove duplicates and trim barcodes
const uniqueBarcodes = [...new Set(barcodes.map((barcode) => barcode.trim()))];
if (uniqueBarcodes.length === 0) {
return; // No barcodes to process
}
// Process barcodes in chunks of `chunkSize`
for (let i = 0; i < uniqueBarcodes.length; i += chunkSize) {
const chunk = uniqueBarcodes.slice(i, i + chunkSize);
// Call listPlates for the current chunk
const parameters: PlatePathGetParamQuery = {
barcodes: chunk.join(","),
};
if (schemaId) {
parameters.schemaId = schemaId; // Add schemaId if provided
}
// Yield results from the inner generator
for await (const plates of this.listPlates(parameters)) {
yield plates;
}
}
}
public listPlates(parameters: PlatePathGetParamQuery): AsyncGenerator<Plate[]> {
return this.client.fetchPagesIterator<Plate>("plates", parameters);
}
public getPlate(plate_id: string): Promise<Plate> {
return this.client.fetchData<Plate>(`plates/${plate_id}`);
}
public patchPlate(plate_id: string, plate_update: PlateUpdate): Promise<Plate> {
return this.client.patchData<Plate>(`plates/${plate_id}`, plate_update, {});
}
public async archivePlates(platesArchive: PlatesArchive): Promise<PlatesArchivalChange> {
return await this.client.postData<PlatesArchivalChange>("plates:archive", platesArchive, {});
}
public async createPlate(plate: PlateCreate): Promise<Plate> {
return await this.client.postData<Plate>("plates", plate, {});
}
/**
* Returns name: Plate[] lookup for all plates with names provided.
* No Error if no plates found, just empty array.
* */
// public async getPlatesFromNames(names: string[]): Promise<{ [key: string]: Plate[] }> {
// let uniqueNames = [...new Set(names.map((d) => d.trim()))];
// if (uniqueNames.length === 0) {
// return {};
// }
// let plateNamesPlates: Record<string, Plate[]> = {};
// for (const name of uniqueNames) {
// plateNamesPlates[name] = [];
// }
// let platesPageGenerator = await this.client.fetchPagesIterator<Plate>("plates", {
// "names.anyOf": uniqueNames.join(","),
// });
// for await (const plates of platesPageGenerator) {
// plates.forEach((plate) => {
// plateNamesPlates[plate.name].push(plate);
// });
// }
// return plateNamesPlates;
// }
public async getPlatesFromNamesV2(
names: string[],
chunkSize: number = 100 // Default chunk size
): Promise<{ [key: string]: Plate[] }> {
// Remove duplicates and trim names
let uniqueNames = [...new Set(names.map((d) => d.trim()))];
if (uniqueNames.length === 0) {
return {};
}
let plateNamesPlates: Record<string, Plate[]> = {};
for (const name of uniqueNames) {
plateNamesPlates[name] = [];
}
// Process names in chunks of `chunkSize`
for (let i = 0; i < uniqueNames.length; i += chunkSize) {
const chunk = uniqueNames.slice(i, i + chunkSize);
// Fetch plates for the current chunk
let platesPageGenerator = await this.client.fetchPagesIterator<Plate>("plates", {
"names.anyOf": chunk.join(","),
});
// Process plates and map them to their names
for await (const plates of platesPageGenerator) {
plates.forEach((plate) => {
plateNamesPlates[plate.name].push(plate);
});
}
}
return plateNamesPlates;
}
/**
* Returns lookups for all matched plates: barcodes lookup and ids (api_key) lookup
* */
public async getBarcodeAndIdMapsFromPlateNames(
names: string[]
): Promise<{ barcodes: { [key: PlateName]: Barode[] }; ids: { [key: PlateName]: Id[] } }> {
let uniqueNames = [...new Set(names.map((d) => d.trim()))];
const result: { barcodes: { [key: PlateName]: Barode[] }; ids: { [key: PlateName]: Id[] } } = {
ids: {},
barcodes: {},
};
for (const name of uniqueNames) {
result["barcodes"][name] = [];
result["ids"][name] = [];
}
let namePlateMap = await this.getPlatesFromNamesV2(names);
for (const [name, plates] of Object.entries(namePlateMap)) {
for (const plate of plates) {
if (plate.barcode) {
if (!result.barcodes[name]) {
result.barcodes[name] = [];
}
result.barcodes[name].push(plate.barcode);
}
if (plate.id) {
if (!result.ids[name]) {
result.ids[name] = [];
}
result.ids[name].push(plate.id);
}
}
}
return result;
}
/**
* Returns a barcode and id (api_key) lookup for each plate name provided
* Throws UnexpectedBenchlingConfigError if any plate name isn't unique
* */
public async getSingleBarcodeAndIdMapsFromPlateNames(
names: string[]
): Promise<{ barcodes: { [key: PlateName]: Barode }; ids: { [key: PlateName]: Id } }> {
let maps = await this.getBarcodeAndIdMapsFromPlateNames(names);
let barcodes: { [key: PlateName]: Barode } = {};
let ids: { [key: PlateName]: Id } = {};
let nonUniquePlates = [];
for (const [name, plateBarcodes] of Object.entries(maps.barcodes)) {
if (maps.barcodes[name].length === 1) {
barcodes[name] = maps.barcodes[name][0];
ids[name] = maps.ids[name][0];
} else if (maps.barcodes[name].length === 0) {
// were not found
barcodes[name] = "";
ids[name] = "";
} else {
nonUniquePlates.push(name);
}
}
if (nonUniquePlates.length > 0) {
throw new UnexpectedBenchlingConfigError({
message: `Multiple plates found for names: ${nonUniquePlates.join(
", "
)}. Please ensure unique plate names.`,
body: nonUniquePlates.join(","),
url: "plates",
});
}
return { ids, barcodes };
}
}