Repository navigation
feat: Add publicWrite feature for member-created content - #874
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
React Doctor found 17 new issues in 13 files · 17 warnings · score 80 / 100 (Needs work) · 0 fixed · vs 17 warnings
Reviewed by React Doctor for commit |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0e811e442a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| // Always structured: a public write route is a new contract, so a unique | ||
| // conflict answers with the same JSON shape as a version conflict. | ||
| return rethrowAsHttpError(error, { |
There was a problem hiding this comment.
Map fallback errors into the public-write contract
When an otherwise valid write throws a generic ContentInputError—for example, a punctuation-only title whose derived slug normalizes to empty—this fallback produces the legacy plain 400 body. Likewise, an editorial content type with delivery redirects can throw ContentDeliverySlugReserved, which becomes a delivery-conflict 409. The generated routes declare ContentPublicWriteRejection for 400 and zodContentConflict for 409, so fetcher clients infer bodies that these responses do not satisfy and parseContentPublicWriteRejection fails. Map these errors into the declared public-write shapes or widen the response schemas.
AGENTS.md reference: AGENTS.md:L40-L40
Useful? React with 👍 / 👎.
| const reachable = new Set( | ||
| rows | ||
| .filter(row => isContentBaseRowPublic(row)) | ||
| .map(row => contentIdKey(row.id as ContentId)), |
There was a problem hiding this comment.
Verify localized relation targets through their translations
For a writable relation whose target content type is localized, filtering only with isContentBaseRowPublic accepts a published base row even when it has no published translation in any enabled language. Such a record cannot be returned by the target's localized public API, yet callers can link to it and probe its existence through successful writes, contradicting the public-readability check. Include the localized translation publication and enabled-language predicates, or reject localized relation targets.
Useful? React with 👍 / 👎.
0e811e4 to
e244d7b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e244d7bba2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const isContentWriteRequest = (c: Context): boolean => | ||
| (c.req.method === "POST" || c.req.method === "PATCH") && | ||
| c.req.path.includes("/content/") && | ||
| !c.req.path.includes("/admin/"); |
There was a problem hiding this comment.
Match the admin module rather than any admin segment
For a valid plugin ID such as admin or @scope/admin, every public write URL contains /admin/ as part of the plugin ID (for example, /api/@scope/admin/content/topics), so this condition exempts its POST and PATCH routes from the new content-write budget. Identify the module segment after the complete plugin ID instead of searching the entire path for /admin/.
Useful? React with 👍 / 👎.
| .limit(1); | ||
| await authorize(actor, row ?? null); | ||
|
|
||
| const full = await model.service(c).findById(id); |
There was a problem hiding this comment.
Authorize the same snapshot returned for editing
When an AdminCP update reassigns or hides a record between the authorization query and this second query, PostgreSQL can return the newer row even though the actor was authorized against the older owner/visibility state. That can expose newly changed, non-public writable fields to the former owner; fetch the full row used for authorization in one statement or keep both reads in a transaction with an appropriate lock.
Useful? React with 👍 / 👎.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…edit - add publicWrite to content types for authenticated public create and edit - answer public write unique conflicts with structured JSON - give public write issues a translatable code - add discussions to the example plugin as the publicWrite reference - cover publicWrite configuration, routes and permissions - document publicWrite - introduce the Frame UI component for structured settings display Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52693af to
cab4e01
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
| return ( | ||
| <Frame className="not-prose w-full"> | ||
| <FrameHeader> | ||
| <FrameTitle render={<h3 />}>Publishing</FrameTitle> |
There was a problem hiding this comment.
React Doctor · react-doctor/heading-has-content (warning)
Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.
Fix → Put readable text in every heading.
| <div className="not-prose grid w-full gap-4 md:grid-cols-2"> | ||
| <Frame> | ||
| <FrameHeader> | ||
| <FrameTitle render={<h3 />}>Personal information</FrameTitle> |
There was a problem hiding this comment.
React Doctor · react-doctor/heading-has-content (warning)
Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.
Fix → Put readable text in every heading.
| </Frame> | ||
| <Frame> | ||
| <FrameHeader> | ||
| <FrameTitle render={<h3 />}>Account</FrameTitle> |
There was a problem hiding this comment.
React Doctor · react-doctor/heading-has-content (warning)
Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.
Fix → Put readable text in every heading.
| return ( | ||
| <Frame className="not-prose w-full"> | ||
| <FrameHeader> | ||
| <FrameTitle render={<h3 />}>AI features</FrameTitle> |
There was a problem hiding this comment.
React Doctor · react-doctor/heading-has-content (warning)
Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.
Fix → Put readable text in every heading.
| known: readonly string[], | ||
| hints: Record<string, string> = {}, | ||
| ): void => { | ||
| const unknown = Object.keys(value).find(key => !known.includes(key)); |
There was a problem hiding this comment.
React Doctor · react-doctor/js-set-map-lookups (warning)
This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.
Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time
| ); | ||
| }; | ||
|
|
||
| export const ProfileSummary = ({ |
There was a problem hiding this comment.
React Doctor · react-doctor/no-high-complexity-react-function (warning)
ProfileSummary has cyclomatic complexity 14, cognitive complexity 17, and maximum nesting depth 2, so its React logic is hard to understand and change. Extract independent branches into components or hooks.
Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.
| value={user.showRealName ? t("realNameShown") : t("realNameHidden")} | ||
| <Frame> | ||
| <FrameHeader> | ||
| <FrameTitle render={<h2 />}>{t("personalTitle")}</FrameTitle> |
There was a problem hiding this comment.
React Doctor · react-doctor/heading-has-content (warning)
Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.
Fix → Put readable text in every heading.
| <FrameTitle | ||
| className="flex items-center gap-2" | ||
| id={titleId} | ||
| render={<h2 />} |
There was a problem hiding this comment.
React Doctor · react-doctor/heading-has-content (warning)
Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.
Fix → Put readable text in every heading.
| /> | ||
| <Frame aria-labelledby={titleId} render={<section />}> | ||
| <FrameHeader> | ||
| <FrameTitle id={titleId} render={<h2 />}> |
There was a problem hiding this comment.
React Doctor · react-doctor/heading-has-content (warning)
Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.
Fix → Put readable text in every heading.
| <Empty className="border"> | ||
| <EmptyHeader> | ||
| <EmptyMedia variant="icon">{icon}</EmptyMedia> | ||
| <EmptyTitle render={<h1 />}>{title}</EmptyTitle> |
There was a problem hiding this comment.
React Doctor · react-doctor/heading-has-content (warning)
Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.
Fix → Put readable text in every heading.
Description
What?
Implements the
publicWritefeature for the Content Engine, enabling signed-in members to create and edit their own content records through public API routes. This includes:publicWriteconfiguration option for content types with server-stamped ownership, moderator permissions, and strict field allowlists/api/{pluginId}/content/{path}for create and edit operations with full conflict/error handlingcontentWriteRateLimiterMiddlewareto prevent abuse of write operationsWhy?
This feature enables community-driven content creation while maintaining security and moderation control. Members can contribute discussions, comments, or other content types without admin intervention, with automatic ownership tracking and moderator oversight capabilities. The implementation follows Content Engine patterns for consistency and includes full type safety through the fetcher.
Test Plan
public-write-service.test.ts,public-write-routes.test.ts, anddefine-public-write.test.tspublic-write.test-d.tsand updatedfetcher.test-d.tshttps://claude.ai/code/session_0191AwoSVjnuPQZsoi1wUy9s