UNPKG

benchling_typescript_sdk

Version:

Typescript SDK for Benchling API

312 lines (281 loc) 9.85 kB
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 }; } }