source-cooperative / source-cooperative/source.coop

Product writes have no optimistic locking (lost-update risk)

Open
#367 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
26
Forks
9
Avg merge
1d 7h
Merged PRs (30d)
42

Description

Summary

The product-mirror server actions — addProductMirror, removeProductMirror, setPrimaryMirror (src/lib/actions/product-mirrors.ts) — read a product, mutate metadata.mirrors / primary_mirror in memory, and write the whole product back via productsTable.update. There is no condition on the write, so two concurrent admin operations on the same product can silently clobber each other (lost update).

This was raised as a non-blocking finding in the #364 review.

Scope

This is not specific to mirrors: productsTable.update is a read-modify-write everywhere it's used, and Product has no version attribute. So a real fix is a small cross-cutting change, not a one-off — which is why it's split out of #364 rather than patched only in the mirror actions.

Impact

Low in practice: these are admin-only operations and concurrent edits of the same product are rare. But the race is real and silent when it happens.

Proposed fix

Add optimistic concurrency to productsTable.update:

  • Condition the UpdateCommand on the updated_at the caller read (ConditionExpression: "updated_at = :prev_updated_at"), or introduce a numeric version attribute and bump+condition it.
  • Surface ConditionalCheckFailedException to callers as a "this product changed, please retry" error.

Apply consistently across product mutations so the behavior is uniform.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/lib/actions/product-mirrors.ts and trace every use of productsTable.update, including addProductMirror, removeProductMirror, and setPrimaryMirror. Review how the caller-read updated_at or a version would be carried into the UpdateCommand and how ConditionalCheckFailedException is surfaced. Done means product mutations consistently reject stale writes with a retryable error instead of silently losing updates.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.