Chapter 4
The three REST endpoints of WIA-BIO-019, the BioBankingSDK reference implementation, the wia-bio-019 command-line tool, and the error model that ties them together — read against the actual source, including the places where the documents disagree.
Chapter 3 defined what a biobank record looks like. This chapter is about how records get in and out: the HTTP interface that a LIMS, a clinical system or a research portal would call, the TypeScript SDK that ships with the standard, and the shell tool that exposes a subset of the same operations. The scope is deliberately narrow. What the states mean, who approves a distribution request and how custody must be recorded belong to the lifecycle protocol in Chapter 5; consent semantics belong to Chapter 6. Here the question is simpler and more mechanical: which fields go where, what comes back, and what fails.
Three sources describe the interface, and they do not agree with one another. Specification §9.1.2 shows JSON examples for three endpoints. The package @wia/bio-019 (files api/typescript/src/types.ts and index.ts) defines the types and an in-memory SDK. The script cli/wia-bio-019.sh is a standalone command-line tool. The README contains a fourth set of examples that matches none of the other three. Wherever they diverge, this chapter quotes what the code actually does, because that is what a developer will meet first, and states which version it recommends.
Section 9 of the specification places the interface inside a LIMS context: sample tracking with unique IDs, barcodes and freezer/rack/box/position locations; inventory with real-time availability and quality flags; de-identified donor data; and a workflow for requests, retrieval and shipment. Of all that, the specification fixes exactly three HTTP operations. Its heading for them is numbered “9.9.2” although it sits under 9.1; this book refers to it as §9.1.2.
| Method | Path | Purpose | Main request fields | Response fields |
|---|---|---|---|---|
POST | /api/v1/samples | Register a new specimen | sample_id, type, donor_id, collection_date, volume, volume_unit, storage_condition, location{}, quality{}, consent_id | success, sample_id, barcode, registered_at |
GET | /api/v1/inventory | Search what is on the shelves | query string: type, available, min_volume | total_count, available_count, samples[] |
POST | /api/v1/requests | Ask for material for a project | requester_id, project_id, samples[], purpose, ethics_approval, return_date | request_id, status, estimated_fulfillment |
There is no endpoint for aliquoting, custody events, consent records, temperature excursions or updates to an existing sample, even though the SDK has methods for all of them. An implementer who needs those over HTTP must design them. The simplest defensible course is to mirror the SDK method names under the same /api/v1 prefix and to state in the service's own documentation that those routes are local extensions, not part of v1.0.
Registration turns a tube in someone's hand into a record with an identity, a location and a barcode. The request below follows the specification's example field for field, with two values changed: the specification writes "type": "blood_plasma" and "storage_condition": "minus_80", but neither string exists in the standard's own enumerations. SampleType has plasma, and StorageCondition has ultra_low_freezer. The book uses the enumeration values on the wire, because a server that validates against the types would reject the specification's spelling. The sample ID is also changed to a random-looking SAM-7K2Q9X, in keeping with §8.3.1 (random alphanumerics, no embedded date or sequence).
// POST /api/v1/samples (book example: enum vocabulary on the wire)
Request
{
"sample_id": "SAM-7K2Q9X",
"type": "plasma",
"donor_id": "D-789012",
"collection_date": "2025-12-26T08:30:00Z",
"volume": 10.5,
"volume_unit": "mL",
"storage_condition": "ultra_low_freezer",
"location": { "freezer": "F-01", "rack": "R-03", "box": "B-25", "position": "A5" },
"quality": { "hemolysis_index": 25, "total_protein": 72 },
"consent_id": "CNS-456789"
}
Response (shape as in spec §9.1.2)
{
"success": true,
"sample_id": "SAM-7K2Q9X",
"barcode": "data:image/png;base64,iVBOR...",
"registered_at": "2025-12-26T10:45:30Z"
}
The wire format is snake_case and deliberately flat: the donor is reduced to donor_id, consent to consent_id, and the blood quality indices sit directly under quality. The SDK's Sample type nests all three. The mapping is not hard, but it has to be written down, because nothing in the package does it for you.
| Wire field (§9.1.2) | SDK field | Notes |
|---|---|---|
sample_id | id | Required. Missing or duplicate → B001. |
type | type: SampleType | Use one of the 14 enum values; blood_plasma is not one. |
donor_id | donor.donorId | De-identified code only; the linkage key lives elsewhere (Chapter 6). |
collection_date | collectionDate: Date | ISO 8601 with an explicit UTC offset. The SDK silently substitutes “now” if it is missing — reject such requests at the API instead. |
volume, volume_unit | volume, volumeUnit | Both required. The unit is never implied. |
storage_condition | storageCondition | minus_80 is not an enum value; use ultra_low_freezer. |
location{freezer, rack, box, position} | location: StorageLocation | The wire omits fullLocation; the server should compose F-01/R-03/B-25/A5. Without a location the SDK stores UNASSIGNED. |
quality.hemolysis_index, quality.total_protein | quality.blood.hemolysisIndex, quality.blood.totalProtein | Flat on the wire, nested under blood in the SDK. |
consent_id | donor.consentId | See the consent caveat in 4.3.2. |
The response carries the barcode as a data:image/png;base64,… URI. Section 3.3 of the specification allows Code128 or DataMatrix symbologies and requires cryo-resistant labels; the API does not say which symbology the PNG encodes, so a client that prints labels should treat the image as a rendering and keep sample_id as the authoritative value. registered_at is the server's timestamp and should not be confused with collection_date. The difference between the two is the very time-to-processing interval that Chapter 2 treats as a quality variable.
The inventory endpoint is a filtered search over what the bank can distribute. It takes three query parameters in the specification's example and returns counts plus a list of matching samples.
GET /api/v1/inventory?type=plasma&available=true&min_volume=5
Response (spec example, with "total_count" written as valid JSON)
{
"total_count": 2450,
"available_count": 2380,
"samples": [
{
"sample_id": "SAM-7K2Q9X",
"type": "plasma",
"volume": 10.5,
"collection_date": "2025-12-26",
"storage_duration_days": 0,
"quality_score": 0.98,
"location": "F-01/R-03/B-25/A5"
}
]
}
Two corrections are applied above. The specification prints "total_count": 2,450, which is not valid JSON; a strict parser stops at the comma. It also uses blood_plasma in the query string, which again has been replaced by the enum value. The counts themselves are illustrative, not reference data.
The item shape is a summary, not a full Sample. quality_score corresponds to quality.integrityScore (the SIS from Chapter 3); location is the composed fullLocation string; storage_duration_days is derived at query time and has no SDK counterpart. The example omits volume_unit, which is a real defect given that the rest of the standard insists units are always explicit. A service should include it. The three query parameters correspond to InventoryQuery as follows: type → sampleType, available → availableOnly, min_volume → minVolume. InventoryQuery also defines storageCondition, collectionDateRange, minQualityScore, donorAgeRange, donorSex, limit and offset, none of which the specification exposes on HTTP. If a service adds them, it should use the snake_case forms (storage_condition, min_quality_score and so on) for consistency with the other wire fields.
Pagination is the other gap. The response has totals but no page metadata, while types.ts defines PaginatedResponse<T> with items and a meta object (total, limit, offset, hasMore). Returning that envelope from the inventory route costs nothing and lets clients page without guessing.
The third endpoint opens a distribution request. It records who is asking, for which project, under which ethics approval, and how much of each sample is wanted.
// POST /api/v1/requests
Request
{
"requester_id": "RES-001",
"project_id": "PRJ-2025-042",
"samples": [
{ "sample_id": "SAM-7K2Q9X", "volume_requested": 0.5, "volume_unit": "mL" }
],
"purpose": "Biomarker analysis",
"ethics_approval": "IRB-2025-123",
"return_date": "2025-12-30"
}
Response (as in spec §9.1.2)
{
"request_id": "REQ-987654",
"status": "pending_approval",
"estimated_fulfillment": "2025-12-27T14:00:00Z"
}
Three mismatches with the type definitions are worth fixing in any implementation. First, the response status pending_approval is not a RequestStatus value; the enum starts at pending and continues through under_review, approved, rejected, fulfilled and cancelled. A client that switches on the enum will not recognize the specification's string. Second, return_date sits at the request level on the wire, whereas RequestedSample carries returnDate per sample together with a required boolean permanent. A request that mixes a returnable tissue block with consumed plasma cannot be expressed in the wire form, so the per-sample fields should be sent. Third, SampleRequest requires organization and projectTitle and has an optional mta reference, none of which appear in the example. The access committee described in §8.2.2 reviews MTAs, so the reference belongs in the request.
The SDK has no method for requests at all. The SampleRequest, RequestedSample and RequestStatus types exist, but BioBankingSDK never uses them. The review workflow that moves a request from pending to fulfilled is described in Chapter 5, and it has to be built by the implementer.
Specification §9 says nothing about authentication or transport. The only security statements in the package come from the PHASE-* documents in the same folder. Those files are template text that carries the wrong identifier (WIA-BIO-018), so they are used here only for their generic, uncontroversial lines, and as non-normative context:
The same PHASE document also gives a base URL (api.wia-bio.org/bio-banking/v1) and a rate limit of 1000 requests per hour. Neither is corroborated anywhere else in the standard. The base URL does not match the /api/v1/… paths in §9.1.2, and a single fixed rate limit is not something a standard can sensibly impose on every deployment. Treat both as unverified. Each deployment publishes its own host name and quota in its OpenAPI document.
An audit log and a chain of custody are different things and should not be merged. The audit log records who called which endpoint and whether it was allowed; it serves security review. The custody chain records what physically happened to a tube; it serves sample provenance and is the subject of Chapter 5. A rejected POST /requests belongs in the audit log and never in the custody chain. A successful aliquoting belongs in both, in different forms. RBAC roles should follow the same line: registering and aliquoting are technician actions, approving requests belongs to the access committee, and reading inventory summaries can be open to researchers. Failures should map to B012 UNAUTHORIZED_ACCESS, the error code reserved for this case (4.4).
The npm package @wia/bio-019 (version 1.0.0, Node.js 16 or later) exports a class BioBankingSDK, all the types of Chapter 3, and a handful of standalone helpers. Before using it, it is important to know what kind of SDK it is. It is not an HTTP client. It holds samples, custody chains and consents in three in-memory Map objects, and nothing is persisted or sent anywhere. It is best understood as an executable reference model of the standard's behaviour: something to test against, to embed in a simulator, or to wrap behind a real storage layer and API. Everything a process registers disappears when the process exits.
One practical obstacle comes before any of that. As published, index.ts exports the class twice (export class BioBankingSDK and, at the end, export { BioBankingSDK }), and types.ts re-exports its enums and interfaces in trailing export lists after declaring them with export. When we loaded the source with Node.js's built-in TypeScript support, it failed with “Duplicate export of 'BioBankingSDK'”. Removing the redundant export lists was enough to make it run. The examples in this section were executed against that minimally patched copy, and the outputs shown in comments are the ones we observed.
| Method | Returns | Behaviour worth knowing | Errors raised |
|---|---|---|---|
registerSample(data) | Sample | Requires id, type, donor, volume, volumeUnit, storageCondition. Forces status = collected, empties aliquots, ignores parentSampleId, and opens a custody chain with a collection event by operator SYSTEM. | B001, B005 |
getSample(id) | Sample | Looks up registered samples only; aliquots are not registered as samples. | B002 |
updateSample(id, patch) | Sample | Shallow merge; protects id, refreshes updatedAt. Can change status or location without writing a custody event. | B002 |
createAliquot(parentId, volume, id?) | Aliquot | Default ID <parent>-A<n>; copies the parent's location; status stored; subtracts the volume from the parent; appends an aliquoting event. | B002, B003 |
addCustodyEvent(id, event) | CustodyEvent | Links to the previous hash and updates currentCustodian. | B010 |
getChainOfCustody(id) | ChainOfCustody | Returns the live object, not a copy. | B010 |
verifyChainOfCustody(id) | boolean | Recomputes every hash and checks linkage; returns false (does not throw) on failure or unknown ID. | — |
registerConsent(consent) | void | Stores or overwrites by consentId; no validation. | — |
validateConsent(id, purpose) | boolean | Checks validity, withdrawal, expiry and purpose (Chapter 6). | B005 if unknown |
queryInventory(query) | Sample[] | AND-combined filters, then offset/limit; returns a bare array. | — |
getInventorySummary() | InventorySummary | Counts by type and storage condition; sums every volume as if it were mL. | — |
recordTemperatureExcursion(id, exc) | void | Assigns an EXC-… ID, appends to quality.temperatureExcursions, adds a quality_check custody event. | B002 |
calculateSampleIntegrity(p), calculateStorageQuality(p) | IntegrityResult, number | The Chapter 3 metrics, also exported as standalone functions. | B004 |
getVersion() | string | '1.0.0'. | — |
registerSample accepts a Partial<Sample> with six mandatory fields and fills in the rest. Three of its choices matter to integrators. First, the status is always collected, whatever the caller passes. A LIMS that registers a sample after it has already been processed and shelved must follow up with custody events and a status update, or the record will claim that a stored tube is still at the collection site. Second, a missing location becomes { freezer: 'UNASSIGNED', fullLocation: 'UNASSIGNED' }. That is reasonable at collection time, but an inventory service should refuse to report such samples as available. Third, consent is checked only weakly. If donor.consentId refers to a consent that has been registered and whose isValid is false, registration fails with B005. If the consent was never registered, registration succeeds silently. Services that must not accept material without consent should call validateConsent themselves before registering.
updateSample is a shallow object merge. It is convenient, but it is also the easiest way to break the custody model: setting status: 'in_use' or a new location through it leaves no custody event behind. In a real service, status and location changes should go through dedicated operations that write the event and the state change together. The SDK leaves that discipline to the caller.
createAliquot is the most complete operation in the SDK. It checks the parent's remaining volume (B003 when the request exceeds it), derives an ID such as SAM-7K2Q9X-A1, subtracts the volume from the parent, and logs an aliquoting custody event whose condition.volume is the parent's new volume. Its shortcuts are equally instructive. The aliquot inherits the parent's exact location, although two tubes cannot occupy position A5 of the same box. The aliquot is not added to the sample registry, so getSample('SAM-7K2Q9X-A1') fails with B002, and it has no custody chain of its own. The parent is never marked depleted when its volume reaches zero, even though queryInventory's availableOnly filter depends on that status. The aliquot inherits the parent's unit and cannot be expressed in another one. An implementation built on the SDK should assign a location to each aliquot, register it as a Sample with parentSampleId set (the genealogy field from Chapter 3), and set the parent to depleted when its volume reaches zero.
queryInventory applies each supplied filter in turn and combines them with AND. Two filters behave in ways that are easy to miss. availableOnly excludes only depleted samples, so destroyed, in-transit and in-use samples still count as available. And minQualityScore drops every sample that has no integrityScore at all, rather than treating “unknown” as a separate case. Pagination is applied after filtering, with limit defaulting to “everything”, and the result is a plain array without the total that a caller would need for a page control. getInventorySummary returns counts by type and by storage condition, but its totalVolume adds up millilitres of plasma and milligrams of tissue and labels the sum mL. It is useful as a count; the volume figure should not be displayed as it stands.
recordTemperatureExcursion takes a TemperatureExcursion without its ID, appends it to the sample's quality record, and writes a quality_check custody event whose note contains the severity and duration. It raises no error even for a critical excursion. Deciding whether such a sample may still be distributed is part of the excursion protocol in Chapter 5; B008 exists for that purpose but is not used by the SDK.
The package also exports free functions. calculateSampleIntegrity and calculateStorageQuality wrap the class methods from Chapter 3; the latter carries the Arrhenius sign error discussed there and should not be used for decisions. celsiusToKelvin and kelvinToCelsius use 273.15, while BIO_CONSTANTS.STORAGE_TEMPS uses rounded integers (−80 °C = 193 K). That difference is harmless, but it explains small mismatches in test vectors. formatVolume(value, unit) assumes the unit is a base unit such as litres: it turns 0.5 L into “500.00 mL” but 0.5 mL into “500.00 mmL”. generateSampleId(prefix) produces IDs like SAM-1790301482493-JH63K1WIN, which embed a millisecond timestamp. That contradicts §8.3.1, which asks for random codes with no embedded date or sequence, precisely because a timestamp can be matched against clinic visit records and so weaken the coding. Services that care about re-identification risk should generate IDs from a cryptographic random source and check them for uniqueness instead.
The following script registers the plasma sample from 4.1.1, takes one aliquot, tries to over-draw, queries the inventory in the same way as the GET example, and inspects the custody trail. The comments show the values we observed.
import {
BioBankingSDK, SampleType, StorageCondition, BioBankingError,
} from '@wia/bio-019';
const sdk = new BioBankingSDK();
// 1. Register: id, type, donor, volume, volumeUnit, storageCondition are required
const plasma = sdk.registerSample({
id: 'SAM-7K2Q9X',
type: SampleType.PLASMA,
donor: { donorId: 'D-789012', consentId: 'CNS-456789' },
collectionDate: new Date('2025-12-26T08:30:00Z'),
volume: 10.5,
volumeUnit: 'mL',
storageCondition: StorageCondition.ULTRA_LOW_FREEZER,
location: {
freezer: 'F-01', rack: 'R-03', box: 'B-25', position: 'A5',
fullLocation: 'F-01/R-03/B-25/A5',
},
});
console.log(plasma.status); // 'collected' (always, whatever you pass)
// 2. Aliquot 0.5 mL: the parent volume drops, a custody event is appended
const a1 = sdk.createAliquot(plasma.id, 0.5);
console.log(a1.id, a1.status); // 'SAM-7K2Q9X-A1' 'stored'
console.log(sdk.getSample(plasma.id).volume); // 10
// 3. Over-draw is refused with B003
try {
sdk.createAliquot(plasma.id, 50);
} catch (e) {
if (e instanceof BioBankingError) console.log(e.code); // 'B003'
}
// 4. Query: the SDK equivalent of GET /inventory?type=plasma&available=true&min_volume=5
const hits = sdk.queryInventory({
sampleType: SampleType.PLASMA,
availableOnly: true,
minVolume: 5,
limit: 20,
offset: 0,
});
console.log(hits.length); // 1
// 5. Custody trail created as a side effect of steps 1 and 2
console.log(sdk.getChainOfCustody(plasma.id).events.map((e) => e.action));
// [ 'collection', 'aliquoting' ]
console.log(sdk.verifyChainOfCustody(plasma.id)); // true
Two details in that output repay attention. The parent's volume after one 0.5 mL aliquot is 10, not 10.5, as expected. The custody trail, however, contains only collection and aliquoting, although the sample was registered with a freezer position. The SDK recorded no storage event, so a service built on it has to add one at the moment the tube is actually shelved.
Every failure the SDK reports is a BioBankingError, a subclass of Error with a code from the BioErrorCode enumeration and an optional details object. Twelve codes are defined. Only six of them are ever raised by the v1.0 SDK, and two of those are used for purposes that their names do not suggest. The table separates what the code does today from what the book recommends a service should do. The HTTP status column is the book's proposal, not part of the standard.
| Code | Name | Raised by the v1.0 SDK | Recommended use in a service | HTTP (proposal) |
|---|---|---|---|---|
| B001 | INVALID_SAMPLE_ID | Registration without an ID, or with an existing ID | Also for IDs that fail the format rule | 400 / 409 |
| B002 | SAMPLE_NOT_FOUND | Any lookup of an unknown sample | As is | 404 |
| B003 | INSUFFICIENT_VOLUME | createAliquot above the remaining volume | Also for requests whose volume_requested exceeds what remains | 409 |
| B004 | QUALITY_BELOW_THRESHOLD | SIS input outside 0–1 (input validation) | A sample below the quality floor of the requested use; use 400 for bad input | 422 |
| B005 | CONSENT_EXPIRED | Registered consent with isValid = false; unknown consent ID in validateConsent | Expired or withdrawn consent only | 403 |
| B006 | CONSENT_NOT_COMPATIBLE | Never | Valid consent that does not cover the requested purpose | 403 |
| B007 | STORAGE_FULL | Never | No free position in the target equipment | 409 |
| B008 | TEMPERATURE_EXCURSION | Never | Distribution blocked pending excursion assessment | 409 |
| B009 | INVALID_LOCATION | Never | Unknown freezer/rack/box or occupied position | 400 / 409 |
| B010 | CHAIN_OF_CUSTODY_BROKEN | No custody chain for the sample | Also when verification fails | 409 |
| B011 | EQUIPMENT_FAILURE | Never | Target equipment in alarm or offline status | 503 |
| B012 | UNAUTHORIZED_ACCESS | Never | Authentication or RBAC failure | 401 / 403 |
Two misuses in the SDK are worth correcting rather than copying. B004 is thrown when a caller passes a viability of 1.85 into the integrity calculation. That is malformed input, not a sample below a quality threshold, and a client that treats B004 as “discard this sample” would misread it. B005 is thrown by validateConsent when the consent ID is unknown. A missing record is not an expired one, and conflating them hides data-entry errors. A service should use a separate validation error for bad input and report unknown consent as its own condition. Callers should branch on code, not on the message text; the messages interpolate IDs and volumes and are meant for people.
types.ts also defines Result<T, E> (a success/error union) and AsyncResult<T, E>, although no SDK method returns them; every method throws. They become useful at the boundary where an API handler must turn an operation into a response body. A small adapter keeps domain errors as values and lets genuine programming errors propagate:
import { BioBankingError, BioErrorCode, type Result } from '@wia/bio-019';
// The SDK throws; an API layer usually wants a value it can serialize.
export function attempt<T>(fn: () => T): Result<T, BioBankingError> {
try {
return { success: true, data: fn() };
} catch (e) {
if (e instanceof BioBankingError) return { success: false, error: e };
throw e; // programming errors stay loud
}
}
const r = attempt(() => sdk.createAliquot('SAM-7K2Q9X', 50));
if (!r.success && r.error.code === BioErrorCode.INSUFFICIENT_VOLUME) {
// e.g. respond 409 with { code: 'B003', message: r.error.message }
}
On the wire, a consistent error body — for example { "success": false, "code": "B003", "message": "…", "details": { … } } — mirrors the success flag that the registration response already uses. Declaring it once in the OpenAPI document as a shared schema keeps all three endpoints consistent.
wia-bio-019 is a single Bash script that uses bc for arithmetic. It does not call the SDK and keeps no state. Only two of its commands compute anything from their input; the others print demonstration text. That makes it useful for exploring the SIS formula and for showing an operator what the workflow looks like, but not for managing a real inventory.
| Command | Flags (as parsed by the script) | What it actually does |
|---|---|---|
register | --id --type --donor --volume --temp | Maps the temperature to a storage band (≤ −150 °C LN₂, ≤ −70 °C ULT, ≤ −15 °C freezer, ≤ 8 °C refrigerated, else ambient) and prints a confirmation. Default ID SAM-<epoch seconds>. Nothing is stored. |
integrity | --initial --current --temp-dev --max-temp-dev --time --max-time | Computes the SIS and its band, exactly as in Chapter 3. Defaults 0.98, 0.95, 2, 5, 365, 3650. |
storage-quality | --type (dna, rna, plasma, cells) --time (years) --temp (kelvin) | Evaluates the v1.0 SQI formula with its sign error. On our test system bc aborted (“scale too large”, “Out of memory”), and the tool printed an empty SQI followed by “DEGRADED”. |
retrieve | --id --purpose --requester | Echoes the inputs, then prints a fixed checklist (5.5 mL available, SIS 0.92, location F-01/R-03/B-25/A5) whatever the ID. |
inventory | --storage | Prints a fixed demonstration report; the filter is echoed but not applied. |
quality | --id --type (dna, rna, plasma, cells) | Prints fixed example metrics for the type. |
version, help | — | Version 1.0.0 and usage text. |
A short session shows both the useful and the misleading parts. Output is abbreviated and colour codes are removed.
$ wia-bio-019 register --type plasma --donor D-789012 --volume 10.5 --temp -80
... (banner and echoed inputs omitted)
✓ Sample registered: SAM-1790301495
Storage Condition: Ultra-Low Freezer (-80°C)
Status: COLLECTED
Chain of Custody: Initialized
$ wia-bio-019 integrity --initial 0.98 --current 0.95 --temp-dev 2 \
--max-temp-dev 5 --time 365 --max-time 3650
Viability Factor: .9693
Temperature Factor: .6000
Time Factor: .9000
✓ Sample Integrity Score (SIS): .5233
✗ Interpretation: POOR
$ wia-bio-019 integrity --id SAM-001 --current-viability 0.50 # README flags
Current Viability: 0.95 # unknown flags ignored, default used
✓ Sample Integrity Score (SIS): .5233
$ wia-bio-019 retrieve --id SAM-7K2Q9X --purpose research
✓ ✓ Volume available: 5.5 mL # fixed demo text, not a lookup
✓ ✓ Quality check: SIS 0.92 (Excellent)
Removed from: F-01/R-03/B-25/A5
The third command is the trap. The README documents integrity --id SAM-001 --current-viability 0.95 and inventory --storage-type liquid-nitrogen, but the script's argument loops end in *) shift ;;, which discards any flag they do not recognize. The README's flags are therefore ignored without a warning, and the computation runs on defaults. Asking for a current viability of 0.50 above still produced the default result, calculated from 0.95. The help text itself shows integrity … --time 365 without --max-temp-dev, so the default maximum deviation of 5 °C applies; with those values the score is the 0.52 (“poor”) shown in the transcript, and the temperature factor of 0.6 accounts for most of the loss. When scripting the tool, always pass every flag explicitly and check the echoed inputs.
Because the retrieve, inventory and quality outputs are fixed text, screenshots of them must not be presented as evidence of a real bank's state. If the CLI is to be more than a demonstration, the natural next step is to make it a thin client of the HTTP API in 4.1, and to have it exit with a non-zero status when a flag is unknown.
SampleType.BLOOD and SampleType.TISSUE are not members of the enum; the closest members are WHOLE_BLOOD (or PLASMA, SERUM, BUFFY_COAT) and TISSUE_FRESH_FROZEN or TISSUE_FFPE. The same applies to the wire strings blood_plasma and minus_80 in §9.1.2.registerSample examples pass donorId, storageTemp or a consent object at the top level, and use await on a synchronous method. The actual signature takes donor: DonorInfo, storageCondition and volumeUnit, and consent is registered separately. The README also multiplies the result of calculateSampleIntegrity by 100, but the function returns an IntegrityResult object; the number is result.score."total_count": 2,450 must be written 2450. Validate every example against the OpenAPI schema before publishing it.Date objects and would compare a string with a date incorrectly in collectionDateRange filters, so parse at the boundary.pending_approval (spec) versus pending (enum). Pick the enum.The adapter below turns a §9.1.2 registration body into the SDK's input. It accepts the specification's legacy spellings on input, so that older clients keep working, but it never emits them. It also composes fullLocation and moves the flat quality fields into their nested places.
import { SampleType, StorageCondition, BioBankingSDK } from '@wia/bio-019';
// Wire shape of POST /api/v1/samples (snake_case, flat donor and quality)
interface RegisterSampleWire {
sample_id: string;
type: string;
donor_id: string;
collection_date: string; // ISO 8601, UTC ("Z")
volume: number;
volume_unit: string;
storage_condition: string;
location?: { freezer: string; rack?: string; box?: string; position?: string };
quality?: { hemolysis_index?: number; total_protein?: number };
consent_id?: string;
}
type RegisterInput = Parameters<BioBankingSDK['registerSample']>[0];
// Legacy spellings seen in spec §9.1.2, accepted on input only
const TYPE_ALIASES: Record<string, SampleType> = { blood_plasma: SampleType.PLASMA };
const STORAGE_ALIASES: Record<string, StorageCondition> = {
minus_80: StorageCondition.ULTRA_LOW_FREEZER,
};
function asEnum<T extends string>(value: string, allowed: readonly T[],
aliases: Record<string, T>, field: string): T {
if ((allowed as readonly string[]).includes(value)) return value as T;
if (value in aliases) return aliases[value];
throw new Error(`${field}: unknown value "${value}"`);
}
export function fromWire(w: RegisterSampleWire): RegisterInput {
const loc = w.location;
return {
id: w.sample_id,
type: asEnum(w.type, Object.values(SampleType), TYPE_ALIASES, 'type'),
donor: { donorId: w.donor_id, consentId: w.consent_id },
collectionDate: new Date(w.collection_date),
volume: w.volume,
volumeUnit: w.volume_unit,
storageCondition: asEnum(w.storage_condition, Object.values(StorageCondition),
STORAGE_ALIASES, 'storage_condition'),
location: loc && {
...loc,
fullLocation: [loc.freezer, loc.rack, loc.box, loc.position]
.filter(Boolean).join('/'),
},
quality: w.quality && {
blood: {
hemolysisIndex: w.quality.hemolysis_index,
totalProtein: w.quality.total_protein,
},
},
};
}
The same pattern, with a toWire function going the other way, covers the inventory items and the request body. Keeping both directions in one module, and testing each with a round trip, is the cheapest way to make sure that a field added to Sample in a future revision does not silently disappear from the API.
WIA-BIO-019 v1.0 fixes three HTTP operations: register, query inventory and request samples. It supplies an in-memory TypeScript model that implements considerably more — aliquoting, custody, consent and excursions — and a Bash tool whose integrity calculation is real while most of its other output is demonstration text. The pieces are close enough to build on, but not close enough to connect without an adapter. The wire uses snake_case and flat objects; the SDK uses camelCase and nesting. Several example values are not valid enum members, one example is not valid JSON, and the README describes an API that the package does not have.
A robust integration therefore does four things: it validates every wire value against the enumerations; it maps between the two naming schemes in exactly one place; it adds the operations the specification leaves out (custody events, status changes, location assignment for aliquots) as explicit, event-writing endpoints; and it reports failures with the B001–B012 codes, using them for what their names say. Chapter 5 now builds on these interfaces, describing the order in which the operations are allowed to happen and how each step is recorded in the custody chain.