fix(formulus): attachment synchronization refactor#612
Merged
najuna-brian merged 6 commits intoOpenDataEnsemble:devfrom Apr 17, 2026
Merged
Conversation
najuna-brian
approved these changes
Apr 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Restores attachment synchronization end-to-end and hardens a few adjacent regressions that surfaced alongside it.
Primary bug — drafts never promoted.
FormplayerModal.handleSubmissionstopped callingcommitDraftAttachmentsAfterSaveafter a prior refactor, so freshly captured attachments remained inattachments/draft/and were never queued for upload. Submission now flows through a newpersistObservationWithAttachmentshelper that saves the observation and promotes draft attachments atomically. A best-effort stale-draft sweep also runs on app start.Attachment folder layout v2. The on-disk layout is now explicitly
attachments/{draft,pending,synced}/with a one-shot migration (runAttachmentLayoutMigrationV2) that relocates legacy top-level committed files intosynced/andpending_upload/intopending/. All path helpers, the WebView URL resolver (draft → synced → pendinglookup order),RepositoryRecoveryService,ServerSwitchService,AttachmentExportService, andSyncScreenpending-upload counter were updated accordingly.FormulusInterfaceDefinitiondocstrings forgetAttachmentUri/getAttachmentsUriand the generatedformulus-api.jsshim reflect the new layout (breaking change forgetAttachmentsUri, which now returnssynced/).Efficiency & robustness.
@last_attachment_versioncursor so the happy path never re-scans synced files, butdownloadRawFile(s)now accepts{ overwrite: true }so corrupted/stale locals are always replaced.saveNewAttachment,downloadAttachments).Side quest 1 — false "server was reset" on fresh install. Formulus now omits
x-repository-generationwhen the AsyncStorage key is missing and adopts the server's value on first response. Synkronus handlers (sync,attachment_manifest,attachment) use a newParseClientRepositoryGenerationSenthelper to distinguish "not sent" from "explicit 1" and skip the 409 for fresh clients.SyncScreenalso silently recovers on 409 when local state is empty.Side quest 2 — missing server-URL change warning.
SettingsScreen.handleServerSwitchIfNeedednow reads the previous URL fromserverConfigService.getServerUrl()(authoritative) instead of un-hydrated component state. NewServerSwitchDecisionhelpers (resolvePreviousServerUrl,classifyServerChange) encapsulate the logic and are unit tested.E2E design artifacts (no wired tests yet). Adds
e2e/README.md(three-layer plan: Synkronus contract, headless Formulus, full-stack RN),e2e/docker-compose.e2e.ymlskeleton (Postgres + Synkronus with test-only creds on distinct ports), and aworkflow_dispatch-onlye2e-attachments.ymlGitHub Actions stub.Type of Change
Component(s) Affected
FormulusInterfaceDefinition+formulus-api.js)Related Issue(s)
Closes/Fixes/Resolves:
Testing
Details:
attachmentStorage(incl. v2 migration),ServerSwitchDecision,FormplayerModalsubmission promotion.pkg/sync/generation_test.go(ParseClientRepositoryGenerationSent),internal/handlers/sync_repository_generation_test.go(missing header → 200),pkg/attachment/manifest_split_cursor_test.go(latest-op, split cursor, hard reset wipes ops+files).go build ./...clean; no new TypeScript or lint errors introduced (pre-existingconfig.basePathanduploadAttachmentTS errors noted but unchanged).Breaking Changes
If breaking changes, please describe migration steps:
FormulusAPI.getAttachmentsUrinow points atattachments/synced/rather than the rootattachments/folder. Custom apps that enumerated the old root must update to the new lookup order (draft → synced → pending) viagetAttachmentUri(id).attachments/*+attachments/pending_upload/toattachments/{draft,pending,synced}/. A one-shot migration runs automatically on next app start (guarded byATTACHMENTS_LAYOUT_V2_KEY); no user action required.Documentation Updates
(
FormulusInterfaceDefinitiondocstrings,formulus-api.jsJSDoc,e2e/README.mddesign doc.)Checklist
Thank you for contributing to Open Data Ensemble (ODE)!