feat(core): write and read reference edges with the content entry - #2496
Closed
MA2153 wants to merge 2 commits into
Closed
feat(core): write and read reference edges with the content entry#2496MA2153 wants to merge 2 commits into
MA2153 wants to merge 2 commits into
Conversation
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 detectedLatest commit: a3791bc The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
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 |
Contributor
Scope checkThis 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. |
18 tasks
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
18 tasks
18 tasks
Contributor
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 was referenced Aug 16, 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.
What does this PR do?
Reference selections now save with the entry.
referenceson 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.referenceOptionsargument. 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 owncontent:read_draftsthrough, so draft children are visible to exactly the people already allowed to see them.translation_groupand live outsidedata, so they don't ride along in a row copy —copyParentEdgescopies the original's outgoing edges (backlinks pointing at the original are deliberately left alone).permanentDeleteremoves 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
required/multipleenforcementPart of the reference-field stack; no single PR closes the Discussion — the admin PR does.
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges are included.emdash: minor)AI-generated code disclosure
Screenshots / test output
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 withoutreferenceOptions; 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
frparent writing an edge to a child that exists only inen, read back from theensibling row, and read fromfrwhere the child resolves to itsenvariant and reportslocale: "en"rather than presenting itself as French.