feat(plugins): predicate-guarded atomic updateIf for plugin storage (no-oversell) - #5
feat(plugins): predicate-guarded atomic updateIf for plugin storage (no-oversell)#5vedanshujain wants to merge 454 commits into
Conversation
|
I have read the CLA Document and I hereby sign the CLA 8 out of 13 committers have signed the CLA. |
Scope checkThis PR changes 542 lines across 7 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
|
This PR has been inactive for 14 days. It will be closed automatically in 7 days if there is no further activity. If you're still working on this, please push an update or leave a comment. |
…-cms#2556) * fix(client): retry requests with a body after a token refresh refreshInterceptor derived its retry from the request it had already sent. A request body can only be read once, so every retried POST/PUT threw "Cannot construct a Request with a Request object that has already been used" -- which is what CLI users see on the first content create/update after the stored access token expires. Keep a clone taken before the first attempt and rebuild the retry from that. GET requests never hit this, which is why the existing test (a bodyless retry whose mock backend never reads the body) passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(client): trim the retry-clone comment to the non-obvious fact Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dash-cms#2629) EmDash-Run: 972b3c37-b688-440b-99f0-ad53c2a0e87b Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
…d rebuild (emdash-cms#2612) Migration 040 rebuilds `_emdash_content_bylines` on SQLite by staging a copy, dropping the old table and renaming the copy. When the run stops between the drop and the rename, a retry skips the rebuild: its guard reads `PRAGMA foreign_key_list` on a table that no longer exists and treats the empty result as "already rebuilt". The migration is then recorded as applied with only `_emdash_content_bylines_new` present, every byline subquery fails, and the site reports no entries although the rows are intact. Add migration 071, which renames the staged copy back when the live table is missing and re-creates the two indexes the drop removed. It is restart-safe, leaves a healthy table untouched, and is a no-op on Postgres, whose 040 path alters the table in place.
…cms#2598) * fix(auth): preserve CMS user on comment submissions * test(comments): cover anonymous and session flows
* fix(admin): align disclosure section drag handles * fix(admin): stabilize editor sidebar layout * chore: consolidate sidebar alignment changeset
* fix(admin): make image actions reliably selectable * chore: add changeset for reliable image actions
* i18n(ja): complete admin catalog translation * i18n(ja): use plain-language CMS terms * i18n(ja): clarify technical admin terminology * chore: add Japanese translation changeset * i18n(ja): refine admin terminology
…ms#2600) * fix(core): keep playground welcome dismissed across reloads * docs: remove issue 2595 technical spec
* docs: document media upload flows * docs: clarify media upload reference * Update docs/src/content/docs/reference/rest-api.mdx Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com> --------- Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
* docs: keep contributor guidance maintainable * docs: clarify architecture guide audience * docs: restore contributor context * docs: rebuild the EmDash front door
…3041) isAvailable() answers only true or false, so a missing worker_loaders binding, a missing PluginBridge export and a workerd binary that does not run all produced the same startup warning and the same SANDBOX_NOT_AVAILABLE refusal on install and update. SandboxRunner gains an optional unavailableReason(). The Cloudflare and workerd runners report their cause, and the startup warning and the four refusals append it; runners without the method keep the previous text.
…sh-cms#2934) * fix(portable-text): preserve table structure and identity * fix(editor): make tables responsive and lossless * feat(editor): add complete table controls and clipboard workflows * test(editor): verify table workflows across browsers * fix(editor): keep contextual table menus anchored * test(admin): wait for loaded content in save conflict tests * fix(core): preserve wide D1 entry loading * test(admin): retain live social settings save locators * test(admin): wait for autosave before clean unscheduling * fix(core): allow localized inline table placeholders * refactor(admin): remove spotlight toolbar control * fix(editor): keep table resizing scroll-stable * test(e2e): avoid range fragment layout flake * test(e2e): scope table insertion announcement * test(e2e): stabilize table deletion announcements * fix(editor): soften table overflow cues * ci: accept visual baselines (requested by @ascorbic) --------- Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
… review-thread comment Addresses the automated review on emdash-cms#2169. The 40001/40P01 docstrings and the StorageSerializationError message attributed both SQLSTATEs to an isolation level stricter than READ COMMITTED. That holds for 40001 but not for 40P01: a deadlock needs only two transactions taking row locks in opposite order, which is reachable at READ COMMITTED. The message told such a caller to "run it at READ COMMITTED" when they already were. The message is now SQLSTATE-aware and names the remedy that applies to each. Documents updateIf in the canonical plugin storage reference, which still described the StorageCollection interface without it, and syncs the template copies via scripts/sync-template-skills.sh. Removes a comment in storage-updateif.test.ts addressed to the review thread rather than to a future reader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NK2NYBPGxwwzgHf7KfQRGM
…o content hooks (emdash-cms#2881) (emdash-cms#3057) * fix(core): separate revision author from entry owner and pass actor to content hooks (emdash-cms#2881) EmDash-Run: 806a2348-849c-4be2-9529-757229e5c2a7 * fix(core): preserve content actor attribution --------- Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com> Co-authored-by: Matt Kane <mkane@cloudflare.com>
…s#2351) * fix(cloudflare): derive sandboxed plugins from Worker Loader * fix(cloudflare): respect named Wrangler sandbox environments * test: keep sandbox opt-in coverage accurate * test(e2e): count rendered publication label lines --------- Co-authored-by: Matt Kane <mkane@cloudflare.com>
Co-authored-by: logelog <194732487+logelog@users.noreply.github.com> Co-authored-by: Matt Kane <mkane@cloudflare.com>
…onomies (emdash-cms#3062) * feat: collapsible sidebar groups for collections and their taxonomies Adds a nullable nav_group column on _emdash_collections, exposed as group through the schema API, seed files, MCP tools and the admin manifest. The admin sidebar folds collections sharing a group into one collapsible entry at the first member's position; a taxonomy joins when every collection it is assigned to shares the group. Folder state persists per browser and the folder holding the active route opens on load. The content type editor gains a Navigation block with the group field. * fix: keep Content section rule and guard the nav_group rollback * test: cover folder rendering, rail fallback and stored folder choices
buildCondition pushes only the bounds that are defined, so a filter whose
every bound is undefined returned an empty SQL string. buildWhereClause
joined that empty string into the clause, which dropped the predicate
entirely.
The reachable shape is a bound built from an optional value:
const since = options.since; // string | undefined
await storage.query({ where: { timestamp: { gte: since } } });
That type-checks, because each bound on RangeFilter is optional. query()
and count() then returned every row in the collection, and updateIf()
applied its write with no guard — a guarded decrement could drive a
counter past the bound the caller asked for, which is the oversell
updateIf exists to prevent. A second field alongside the empty one
emitted a dangling AND and failed to parse.
buildCondition now throws StorageQueryError naming the field, and
buildWhereClause skips any condition contributing no SQL.
Callers that mean "match unconditionally" omit the field instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NK2NYBPGxwwzgHf7KfQRGM
* fix(admin): simplify theme toggle * ci: accept visual baselines (requested by @ascorbic) --------- Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
fix(plugins): reject a storage range filter with no defined bound
* fix marketplace update re-consent * fix plugin update consent feedback
* fix: surface registry configuration errors * fix: validate registry exclusion shape
…#3078) * fix(registry): exempt proven first releases from holdback * fix(admin): preserve registry policy evidence * test(admin): cover release policy fallback
* feat(admin): show registry plugin public names * fix(admin): normalize registry public-name handles * docs(admin): clarify registry detail routing
What does this PR do?
Adds
ctx.storage.<collection>.updateIf(id, { where, set?, delta? })— a predicate-guarded atomic update for plugin storage, and the no-oversell primitive. Today plugin storage offers only unconditionalput(whole-doc upsert), so "sell one iff in stock" is impossible in-process without a racy read-modify-write.updateIfruns the guard and the arithmetic in one statement:No read-then-write, no interactive transaction — so N concurrent guarded decrements serialize correctly (exactly M of N apply, final stock 0, never oversell).
wherereuses the numeric-correctWhereClausetranslation fromquery()(landed upstream in fix(core): plugin storage where-filters fail on Postgres with boolean = integer (#920) emdash-cms/emdash#1898 + the numeric fix), so a multi-digit guard likestock >= 10compares numerically on Postgres.setwrites wholesale field values;deltaapplies integerinc/decin-SQL overCOALESCE(base, 0)(a delta on a missing/null field starts from 0). Integer-only deltas enforced at runtime; a field may not appear in bothsetanddelta; at least one is required.set/deltaare separate args so{inc:n}is never mistaken for a value.{ applied: true, data }or{ applied: false }(row absent or guard failed — intentionally indistinguishable). Never inserts.Backed by
pluginDataWriteExpr(dialect-correctjson_set/jsonb_set, values bound as params). No new column, no migration.Tests
storage-updateif.test.ts— guard pass/fail/missing, integer inc/dec + integer round-trip, float-delta rejection,COALESCE-from-0 on missing/null, wholesaleset,set+delta, both-fields / neither-provided rejection, guard operator coverage (equality, multi-digitgte,in,startsWith, non-indexed field), empty-inno-op.storage-no-oversell.test.ts— M-of-N concurrent guarded decrements (the acceptance bar; Postgres is the real race, SQLite proves SQL correctness).Both dialects; Postgres via
EMDASH_TEST_PG. Full core plugin suites green (795 tests), typecheck / lint / format clean.Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change) — full core plugin suites, both dialectspnpm formathas been runAI-generated code disclosure