Skip to content

feat(core): write and read reference edges with the content entry - #2496

Closed
MA2153 wants to merge 2 commits into
emdash-cms:mainfrom
MA2153:feat/reference-edges-with-entry
Closed

feat(core): write and read reference edges with the content entry#2496
MA2153 wants to merge 2 commits into
emdash-cms:mainfrom
MA2153:feat/reference-edges-with-entry

Conversation

@MA2153

@MA2153 MA2153 commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Stacked on #2492. Base branches can't live on a fork, so this PR targets main and its diff currently includes #2492's commit. Review a3791bc4 on its own — that's the whole of this change. The diff collapses to just that commit once #2492 merges.

Reference selections now save with the entry.

  • references on the content create/update body{ relationTranslationGroup: [childEntryId, ...] }, in display order — written inside the same transaction as the entry. A relation or child id that fails to resolve throws a structured error that aborts the whole save, so a bad reference can't leave behind an entry whose taxonomies, bylines and SEO already committed.
  • Editor GET hydration, opt-in via a referenceOptions argument. It stays opt-in because a resolved child carries a draft or scheduled entry's id and slug; the REST route passes the caller's own content:read_drafts through, so draft children are visible to exactly the people already allowed to see them.
  • Duplicate carries references. Edges are keyed by translation_group and live outside data, so they don't ride along in a row copy — copyParentEdges copies the original's outgoing edges (backlinks pointing at the original are deliberately left alone).
  • Purge clears edges, but only when the group is gone. permanentDelete removes one row; edges belong to the whole translation group. Clearing unconditionally would wipe a multi-locale entry's entire reference set — incoming and outgoing — the moment any one translation was purged from the trash. Instead it checks for a surviving sibling first, counting trashed ones, since those are still restorable and their references have to come back with them.

The lookup that finds a collection's storage-less fields is shared with the read-side strip added in #2492 and memoized per request, so hydration doesn't add a second schema round-trip to the editor GET.

Part of #386; supersedes part of the core half of #1928.

Stack

  1. feat(core): make reference fields storage-less #2492 — storage-less field types + relation lifecycle + seed
  2. This PR — reference edges written and read with the content entry
  3. Server-side required / multiple enforcement
  4. Public content query API hydration
  5. MCP content tools
  6. OpenAPI documentation for the reference surface
  7. Admin UI + browser e2e

Part of the reference-field stack; no single PR closes the Discussion — the admin PR does.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5 (Claude Code)

Screenshots / test output

$ pnpm exec vitest run          # packages/core
 Test Files  466 passed | 2 skipped (468)
      Tests  5793 passed | 9 skipped (5802)

$ pnpm typecheck                # all packages — clean
$ pnpm lint:json | jq '.diagnostics | length'
0

tests/integration/content/content-references-write.test.ts, both dialects: edges written atomically on create in the caller's order; a bad child rolls the entry back (asserted by row count, not just the error code); opt-in hydration returns the first page and is absent without referenceOptions; duplicate copies edges; purging the last row of a group clears edges on both sides; purging one locale while a sibling survives leaves them intact.

Added in this PR beyond the original branch: a cross-locale case — an fr parent writing an edge to a child that exists only in en, read back from the en sibling row, and read from fr where the child resolves to its en variant and reports locale: "en" rather than presenting itself as French.

MA2153 and others added 2 commits August 16, 2026 16:42
A reference field's selections are edges in _emdash_content_references,
not a column on the collection's table. The registry skips column DDL for
storage-less field types, the schema handlers own the backing relation's
lifecycle (created with the field, destroyed with it, target collection
immutable), and the previously unregistered relation and reference-edge
routes are wired into injectCoreRoutes.

A storage-less field never appears in `data` in either direction: it is
excluded from the generated Zod shape (so a required reference field is
satisfiable at all), rejected with a VALIDATION_ERROR when a caller sends
one, and filtered out of reads so a column left behind by an older version
cannot round-trip back into a save. That replaces the reference-target
existence pass in validateContentData, which validated a column-backed
value that no longer exists.

Seeds apply a reference field's $ref: value as an edge, so seed files keep
working unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reference selections ride in the content create/update body under a
`references` key and are written in the same transaction as the entry, so
a child that fails to resolve aborts the whole save rather than leaving an
entry with taxonomies, bylines and SEO already committed. The editor GET
opts into hydrating the first page of each reference field's children.

Two paths that used to lose edges now carry them: duplicating an entry
copies its outgoing references onto the copy, and purging a row clears the
group's edges only once no sibling — trashed ones included, they are still
restorable — is left to own them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a3791bc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Major
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/core size/XL labels Aug 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 2,906 lines across 32 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.

@pkg-pr-new

pkg-pr-new Bot commented Aug 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2496

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2496

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2496

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2496

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2496

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2496

emdash

npm i https://pkg.pr.new/emdash@2496

create-emdash

npm i https://pkg.pr.new/create-emdash@2496

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2496

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2496

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2496

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2496

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2496

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2496

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2496

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2496

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2496

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2496

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2496

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2496

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2496

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2496

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2496

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2496

commit: a3791bc

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant