Skip to content

Choose Containers for export and statistics through Container selection (ADR 0011) - #101

Open
m-murphy wants to merge 34 commits into
docs/adr-coding-standardsfrom
feat/container-selection
Open

m-murphy wants to merge 34 commits into
docs/adr-coding-standardsfrom
feat/container-selection

Conversation

@m-murphy

@m-murphy m-murphy commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Container export and the statistics dashboard choose Containers through one Container selection module (ADR 0011).

 runSingleStudyContainerExport*  (#96)
-  buildContainerQuery → filterContainerIdsByType → enrichContainerData(type filter)
+  selectContainers({ kind: 'study' | 'subjects' }) → enrichContainerData
 runMultiStudy / runBarcode      (#97)
-  buildMultiStudyContainerQuery (one query per Study, fake MULTI study)
+  selectContainers({ kind: 'subjects' | 'micronixBarcodes' })
 getDashboardStatistics          (#98)
-  inline all-tags intersection, unreachable location-only branch
+  selectContainers({ kind: 'study' | 'database' })
 GET /containers?tag_ids         (#99)
-  any selected Tag
+  containerHasAllTags(tagIds)

Also:

  • GET /export/available-types reads only distinct types through selectContainerSelectionTypes, not every Specimen and Container row. A missing study is a typed ValidationError.
  • JSON POST container exports return their rows again (they returned data: []).
  • buildContainerQuery, lib/export/filter.ts, lib/container-tag-filter.ts and lib/statistics/location-filter.ts are deleted. enrichContainerData(database, containers) reads study fields from the Source.
  • Uploaded barcodes and subject names resolve a chunk of SQL_IN_CHUNK_SIZE at a time, as ADR 0011 requires.
  • A Subject's collection dates fold in one place, the contract's addSubjectDates. When a Subject is listed more than once, an undated entry wins, and ADR 0011 now says so.
  • Every container type union derives from one list, CONTAINER_SUBTYPES. The selection result says whether a Container-level filter was set, and parseStatisticsFilters parses the statistics id filters.

Evidence

  • Before: a subject list of 500 Subjects × 80 dates → expected 14964 values, received 80500. available-types for a Study with 70,000 Subjects → 500. A POST export with format: 'json' → data: []. subject_dates: { "S1": null } → 500. 70,000 uploaded barcodes or subject names → SQLite query expected 4466 values, received 70002.
    After: all pass. Container selection, export, statistics, identifier resolution and containers tests: 202 pass against the in-memory database, and the contract's 89 tests pass. bun run ci:verify passes: API 1211, web 949, contract 89.

Merge Danger

Door: two-way

Revertible per commit; no schema change.

Blast Radius: wire-semantics

GET /containers?tag_ids changes from any-Tag to all-Tags for external clients. GET study export returns 404 when every Container is filtered out by type. The dashboard's "Created" filter is relabelled "Specimen created". POST /export/containers answers 400 VALIDATION_ERROR for a malformed subject_dates, which it used to ignore or fail on with a 500. The available-types 400 body gains errorCode.

46 files changed, 2493 insertions(+), 2004 deletions(-).

Closes #94
Closes #95
Closes #96
Closes #97
Closes #98
Closes #99
Closes #100

🤖 Generated with Claude Code

m-murphy and others added 20 commits October 22, 2026 16:31
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Breaking: tag_ids now lists only Containers carrying all the selected
Tags, using containerHasAllTags from Container selection (ADR 0011).
Pagination and the total stay in SQL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion.

`GET /export/containers` now picks its Containers with `selectContainers`
(ADR 0011), so the container type filter runs once, in the selection. A
Study whose Containers are all of other types now returns 404 "No
containers found for this study" instead of an empty file. `created_from`
and `created_to` map to Container created.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The statistics created_from/created_to filter has always matched the
Specimen's created date. The filter fields and their chips now say so,
in the wording ADR 0011 uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
formatContainerExport returned the JSON rows as `json`, but
buildPostExportEnvelope reads `jsonData`, so every POST container export
in JSON format sent an empty `data` array. New executor tests for
multi-study and barcode export pin the rows, the per-Study summary and
the barcode summary before those paths move to Container selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion.

`POST /export/containers` now builds a subject-list selection from the resolved subject names and their per-subject dates, and selects with `selectContainers` (ADR 0011). Count-only and full export share the one selection, so neither calls `filterContainerIdsByType` any more. The summary is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…election.

getDashboardStatistics now asks selectContainers for its Containers and
Specimens. created_from/created_to map to Specimen created and
specimen_type_id to a one-item list. The inline all-Tags intersection,
the unreachable location- and tag-only branch, the statistics location
helpers and buildDateFilter are gone. The location filter no longer needs
the raw SQLite handle, so the statistics routes drop it.

The wire shape is unchanged. A filter that matches no Specimen returns
the all-zero statistics, without the storage summary, as an unknown Study
or Tag already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ainer selection.

A multi-study upload is now one subject-list selection instead of one
buildContainerQuery call per Study, followed by a container type filter
and enrichment per Study. The executor groups the enriched rows by Study
for the Export summary. The barcode export selects the found tubes with
a micronix barcode selection and applies no other filters; found and not
found still come from resolveMicronixBarcodesToContainers, since the
selection carries no barcodes.

buildMultiStudyContainerQuery, buildContainerQueryByMicronixBarcodes and
MultiStudyExportResult had no other callers and are deleted. The
multi-visit test moves to the executor tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/api/src/lib/export/container-export.ts
… the old selection code.

enrichContainerData now takes only the Containers. Each subject row's
study fields come from its Source's Study (ADR 0007) and control rows
have none, so the type filter, the unused specimen list, the study
argument and the subject-to-Study map are gone, along with
loadStudiesBySubject. The MULTI study record is now used only for JSON
export metadata, including the barcode export's.

buildContainerQuery, filterContainerIdsByType (lib/export/filter.ts) and
lib/container-tag-filter.ts had no callers outside their tests and are
deleted with them; Container selection's tests cover their cases.
query.ts keeps resolveMicronixBarcodesToContainers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every export and statistics path now selects through
src/lib/container-selection/, so the rule lists no unmigrated paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Container selection now owns the rules its callers re-derived:

- containerTypes is typed as ContainerSubtype. containerTypesFilter
  narrows wire values and drops unrecognised ones, so a list of only
  unrecognised values still matches nothing; 'unknown' matches
  Containers with no subtype row, as ADR 0011 now says.
- hasContainerLevelFilter replaces dashboard-stats' own rule, which
  left out Container created.
- SpecimenSourceType and specimenSourceType replace the ternary in the
  database selection and the check in statisticsSelection.

Export and statistics look up a Study through identifier-resolution
(resolveStudyRecordByShortCode backs resolveStudyByShortCode) instead of
their own queries. Export inverts subject name maps with one helper,
and multi-study JSON metadata uses MULTI_STUDY_RECORD, as barcode export
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /export/available-types picked a Study's Specimens and Containers
with unchunked IN lists that grew with the Study, and failed once its
Subjects outnumbered SQLite's bound-parameter limit. It now calls
selectContainers with a study selection and reads the specimen types and
containerTypes from the result. The response is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
createTestStorageContainer takes a created override, createTestSpecimen
takes collectionDate and created, and createTestContainerTags links
Tags to a Container. The Container selection tests use them instead of
raw inserts and updates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
subjectListScopes chunked by Subject, but each dated Subject added a
condition per date to one OR, so 500 Subjects with 80 dates each bound
80,500 parameters and the query failed. Each listed date is now its own
condition, and a query takes at most SQL_IN_CHUNK_SIZE of them; undated
Subjects get their own query. The list is named subjectDateConditions,
since it no longer mixes in the undated condition. Scopes can now
overlap, so selectContainers collects Containers by id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The filter table already ran each filter on study, subject-list and
whole-database selections. It now also runs them on a whole-database
selection narrowed to Subjects, and a subject-list test applies the
other filters to a Subject's own dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@m-murphy
m-murphy marked this pull request as ready for review October 23, 2026 00:24
m-murphy and others added 9 commits October 29, 2026 22:19
…ed subject_dates.

A null subject_dates entry reached Container selection as [null] and the
export returned 500. Null means undated again, as before the migration, and
the route checks the shape of subject_dates, answering 400 when it is wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… time.

resolveMicronixBarcodesToContainers and resolveSubjectNamesByStudy bound
every uploaded value in one IN list, so a long enough upload exceeded
SQLite's parameter cap. ADR 0011 keeps queries bounded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rror.

GET /export/available-types returned its 400 with c.json, which
CODING_STANDARDS.md rules out in changed route code (ADR 0004). It throws
ValidationError now; the body keeps its error message and gains errorCode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ule does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /export/available-types loaded every Specimen and Container row of the
Study to list their types. selectContainerSelectionTypes runs the same
selection with SELECT DISTINCT and returns only the type lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract's addSubjectDates now reads one listing of a Subject's dates
(a filter, a list, or none) and folds it into the Subject's selection, with
an undated listing winning. subjectDateSelections, the subject-list scopes
and the single-study POST export use it instead of their own copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GET export repeated the json/csv/xlsx dispatch of formatContainerExport,
and three callers passed the MULTI filters and study record pair by hand.
formatContainerExport returns the formatted result for both, and defaults its
JSON metadata to the multi-study placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Container selection's CONTAINER_TYPES re-spelled the ContainerSubtype union.
container-placement now exports CONTAINER_SUBTYPES and derives the type from
it, and the module filters on that list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
m-murphy and others added 5 commits October 29, 2026 22:26
hasContainerLevelFilter built every Container-level condition just to count
them, and dashboard statistics rebuilt the selection filters to call it. The
selection result now says whether one was set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s query.

dashboard-stats parsed specimen_type_id and location_id from wire strings
although parseStatisticsFilters parses the query. StatisticsFilters now
carries them as numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	CODING_STANDARDS.md
#	packages/api/src/__tests__/helpers/factories.ts
#	packages/api/src/routes/__tests__/export.test.ts
#	packages/api/src/routes/export.ts
…NER_SUBTYPES.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant