Skip to content

refactor(website): refactor file mapping on submit to fix issues for CI and compressed metadata files - #7299

Open
tombch wants to merge 16 commits into
mainfrom
file-mapping-metadata-parse-improvements
Open

tombch wants to merge 16 commits into
mainfrom
file-mapping-metadata-parse-improvements

Conversation

@tombch

@tombch tombch commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

resolves #7105
resolves #7120
resolves CI flake outlined in #7270

Summary

This PR refactors the file mapping functions to use RawFile and VirtualFile types from fileProcessing.ts, that both implement the ProcessedFile interface. With this we remove the issues related to compressed metadata files in issue #7120 as now the ProcessedFile.text()is called rather than the raw File.text(). We also now have error handling for the metadata file in parseMetadataFile to address #7105 , as well as adding a call to parseSubmissionFileMapping when a user clicks submit, to remove the CI flake mentioned in #7270.

Changes per file:

fileMapping.ts

  • Updated parseSubmissionFileMapping and applyFileMappings functions to take ProcessedFile types rather than raw File objects.
  • applyFileMappings now returns a VirtualFile (implements ProcessedFile) instead of a raw File.
  • Changed the parseMetadataText function into a parseMetadataFile function, which accepts a ProcessedFile, and does error handling if anything goes wrong in reading the file.

ColumnMapping.ts

  • Updated the return type of the applyTo function to return a neverthrow Result containing a VirtualFile (implements ProcessedFile) instead of a raw File object, and added neverthrow error handling.
  • Added a check in applyTo for empty metadata files, and return a Please provide a non-empty Metadata file rather than TypeError: Cannot read properties of undefined (reading 'findIndex')

EditPage.tsx

  • Updated to have RawFile (implements ProcessedFile) input type for applyFileMappings.
  • Renamed variables related to file mapping.

DataUploadForm.tsx

  • Moved all file mapping operations from the DataUploadForm to the FormOrUploadWrapper.
  • Moved validateFileUploadStates to check extra files upload states before file factory call.

FormOrUploadWrapper.tsx

  • The FormOrUploadWrapper file factory now has all metadata/sequence file operations (column mapping, file mapping) done in here rather than in two stages, and returns the final metadata and sequence file objects. With the column mapping updated to return a ProcessedFile it also means these can be simply applied one after the other.
  • Added a parseSubmissionFileMapping step within the submit handling, to fix CI flakes where submit was clicked before the file mapping had finished being parsed in the metadata.
  • Updated file factory to also return a Result instead of custom ok/error types, and renamed variables related to file mapping.

Screenshot

For #7105: Better error message with corrupted metadata file (raises on adding metadata file, and blocks submit):

image

For #7120:

File mappings for compressed metadata are displayed:

image

Uploads in progress block:

image

Files are mapped correctly after submit:

image

PR Checklist

  • All necessary documentation has been adapted.
  • The implemented feature is covered by appropriate, automated tests.
  • Any manual testing that has been done is documented (i.e. what exactly was tested?)

🚀 Preview: https://file-mapping-metadata-par.loculus.org

@claude claude Bot added the website Tasks related to the web application label Sep 10, 2026
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

This PR may be related to: #7120 ("If file sharing is enabled, bulk submission of compressed metadata with files is broken"), which matches the compressed metadata file mapping issue described in this PR's title.

@tombch
tombch marked this pull request as ready for review September 14, 2026 10:03
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Claude finished @tombch's task in 2m 50s —— View job


Claude Code Review

  • Read PR diff
  • Review fileMapping.ts / ColumnMapping.ts
  • Review FormOrUploadWrapper.tsx / DataUploadForm.tsx / EditPage.tsx
  • Review test coverage
  • Post review

The refactor is a good direction. Threading ProcessedFile through parseSubmissionFileMapping/applyFileMappings instead of raw File is the right fix for #7120 — the compressed-metadata bug came from calling File.text() on a still-compressed blob, and routing everything through ProcessedFile.text() removes the whole class of problem. Consolidating the mapping pipeline into FormOrUploadWrapper's file factory also removes the real duplication between form and bulk modes in DataUploadForm, and having ColumnMapping.applyTo return a ProcessedFile makes the two transforms composable.

I left 5 inline comments. The one worth acting on before merge:

  • Error ordering regression (FormOrUploadWrapper.tsx:198) — because handleSubmit calls fileFactory() before validateFileUploadStates, submitting while an upload is still in flight now surfaces a "files missing" linkage error rather than "Please wait for all files to finish uploading". Moving the upload-state check above the fileFactory() call in DataUploadForm.handleSubmit fixes it.

The rest: a swallowed error in ColumnMapping.applyTo, repeated synchronous decompression on the submit path, missing useEffect deps, and the test-coverage gap.

Two smaller notes not worth inlining:

  • applyFileMappings now returns new VirtualFile(content, 'metadata.tsv') instead of new File([content], 'metadata.tsv', { type: 'text/tab-separated-values' }), so the resulting File.type is ''. I grepped and nothing in the website reads .type off the metadata file, and the backend keys off the filename, so this looks safe — flagging only so it's a deliberate choice.
  • mFile as a name reads as an abbreviation of nothing in particular; metadataFileToSubmit or similar would be clearer now that it's threaded through four distinct transforms. Pre-existing in the bulk branch, so entirely optional.

Not verified: node_modules isn't installed in this checkout, so I did not run CI=1 npm run test, npm run check-types or npm run format. The spec changes look type-consistent with the new async signatures on inspection, but please confirm CI is green.
· branch file-mapping-metadata-parse-improvements

Comment thread website/src/components/Submission/FormOrUploadWrapper.tsx
Comment thread website/src/components/Submission/FileUpload/ColumnMapping.ts Outdated
Comment on lines +200 to +232
const submissionFileMapping = await parseSubmissionFileMapping(
mFile,
submissionDataTypes.files?.categories?.map((category) => category.name) ?? [],
);
if (submissionFileMapping.isErr()) {
return { type: 'error', errorMessage: submissionFileMapping.error.message };
}

// Validate the submission file mapping
const validation = validateSubmissionFileMapping(
submissionFileMapping.value,
fileSharingConfig,
);
if (validation.isErr()) {
return { type: 'error', errorMessage: validation.error.message };
}

// Resolve the submission file mapping against file mapping of uploads
const { submissionFileMapping: resolvedFileMapping, fileLinkage } = resolveFileMappings(
submissionFileMapping.value,
fileMapping,
);
const linkageErrorMessage = getLinkageErrors(fileLinkage);
if (linkageErrorMessage !== undefined) {
return { type: 'error', errorMessage: linkageErrorMessage };
}

// Apply resolved mapping to metadata file
const metadataWithFileMapping = await applyFileMappings(mFile, resolvedFileMapping);
if (metadataWithFileMapping.isErr()) {
return { type: 'error', errorMessage: metadataWithFileMapping.error.message };
}
mFile = metadataWithFileMapping.value;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When columnMapping === null (the common case), mFile is still the original ProcessedFile, so parseSubmissionFileMapping (line 200) and applyFileMappings (line 228) each call .text() on it independently. For a CompressedFile that means decompressing the whole metadata file twice on submit — and fflate.decompressSync/fzstd.decompress are synchronous, so both block the main thread. The useEffect above has usually already decompressed it a third time.

Converting once up front would avoid this, e.g. mFile = new VirtualFile(await mFile.text(), 'metadata.tsv') before the extraFilesEnabled block (wrapped in try/catch, or via a small helper that returns a Result). Not a correctness issue, but it's a user-visible freeze on large compressed uploads — exactly the files #7120 is about.

Comment thread website/src/components/Submission/FormOrUploadWrapper.tsx
setSubmissionFileMapping={vi.fn()}
onError={vi.fn()}
fileSharingConfig={{ disableStrictFilenameValidation: false }}
fileMapping={undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec only gets the two new props added to keep it compiling — none of the new behaviour is covered. The three things this PR claims to fix all live in the file factory and are untested:

  1. A compressed (.tsv.zst/.gz) metadata file with a files.* column produces a correctly rewritten metadata file (If file sharing is enabled, bulk submission of compressed metadata with files is broken #7120) — currently nothing exercises parseSubmissionFileMapping/applyFileMappings against a CompressedFile.
  2. Submitting before the useEffect has settled still yields the right mapping (fix(website): wait for the metadata file when submitting, don't reject #7270) — the whole point of adding parseSubmissionFileMapping to the submit path.
  3. The factory returns { type: 'error' } with the right message when the metadata can't be read or the linkage is broken.

At minimum a test for (1) would be valuable, since it's the regression the PR is named after and it's cheap to write with a gziped fixture through METADATA_FILE_KIND.processRawFile.

@anna-parker anna-parker added the preview Triggers a deployment to argocd label Sep 14, 2026
return async (): Promise<Result<SequenceData, Error>> => {
switch (inputMode) {
case 'form': {
const submissionId = editableMetadata.getSubmissionId();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anya thoughts: can we split these out into subfunctions for form and bulk (its hard to see what is in the form and bulk case as the function is getting quite long)

if (enableConsensusSequences && sFile === undefined) {
return { type: 'error', errorMessage: 'Please specify a sequences file.' };
if (extraFilesEnabled) {
// Parse submission file mapping from the metadata

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mayeb explain this is to prevent a race condition

const newFileContent = Papa.unparse([headers, ...newRows], { delimiter: '\t', newline: '\n' });

return ok(new VirtualFile(newFileContent, 'remapped.tsv'));
} catch (error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to cover this error in a test


return ok({ columns, rows });
return ok({ columns, rows });
} catch (error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Triggers a deployment to argocd website Tasks related to the web application

Projects

None yet

2 participants