feat: add custom fields API support#359
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughAdds a complete custom fields system: Zod/OpenAPI schemas for field types and options, a ChangesCustom Fields Feature
Sequence Diagram(s)sequenceDiagram
participant Client
participant PostsRouter
participant resolveCustomFieldValuesByKey
participant FieldsRouter
participant DB
participant Cache
Note over Client,Cache: Custom Field Definition (one-time setup)
Client->>FieldsRouter: POST /v1/fields { key, type, options? }
FieldsRouter->>DB: check key conflict, create field + options
FieldsRouter->>Cache: invalidate "fields" and "posts"
FieldsRouter-->>Client: 201 { field }
Note over Client,Cache: Writing field values on a post
Client->>PostsRouter: POST /v1/posts { title, fields: { my_key: "value" } }
PostsRouter->>DB: fetch field definitions for workspace
PostsRouter->>resolveCustomFieldValuesByKey: resolve in "create" mode
resolveCustomFieldValuesByKey-->>PostsRouter: FieldValueWrite[] or 400 error
PostsRouter->>DB: $transaction create post + insert fieldValue rows
PostsRouter->>Cache: invalidate "posts"
PostsRouter-->>Client: 201 { post }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
marble-mcp | b075656 | Commit Preview URL Branch Preview URL |
Jun 19 2026, 07:38 PM |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
marble-api | b075656 | Commit Preview URL Branch Preview URL |
Jun 19 2026, 07:39 PM |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/lib/fields.ts`:
- Around line 194-201: The validation logic is checking if the value is empty
before sanitizing the HTML, but required rich-text fields should be validated
after sanitization since HTML sanitization can result in an empty string even if
the original input was non-empty. Reorder the logic to first sanitize the value
using sanitizeHtml, then check if the sanitized result is empty using
isRichTextContentEmpty, and only then return an error if the field is required
and the sanitized value is empty. This prevents required rich-text fields from
being stored as empty after sanitization.
In `@apps/api/src/schemas/fields.ts`:
- Around line 134-138: The name field validation in the Zod schema currently
allows whitespace-only strings because they pass the `.min(1)` check despite
being functionally empty. Add a `.trim()` method call before the `.min(1, "Name
cannot be empty")` validation in the name field schema to strip leading and
trailing whitespace, ensuring that whitespace-only names are properly rejected.
This same fix also needs to be applied to the similar name validation block
mentioned at lines 172-177.
In `@apps/docs/features/custom-fields.mdx`:
- Around line 55-90: The API documentation section "Writing fields through the
API" contains only raw JSON payload snippets without showing the complete
request/response workflow. Replace the first JSON code block (starting with the
field definition containing "key": "audience") with a RequestExample component
that includes the HTTP method (POST), endpoint path (/v1/fields), a curl command
with proper Authorization and Content-Type headers, and the JSON payload. Add
corresponding ResponseExample components for success (status 201), validation
errors (status 400), and rate limiting (status 429). Apply the same pattern to
the second JSON snippet (for creating/updating posts) by wrapping it in
RequestExample/ResponseExample components showing the appropriate endpoint,
method, headers, and response cases. Ensure all examples are executable curl
commands that readers can copy and test directly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 70e9b658-c075-423a-81df-90513209285d
📒 Files selected for processing (13)
apps/api/src/app.tsapps/api/src/lib/cache.tsapps/api/src/lib/fields.tsapps/api/src/routes/fields.tsapps/api/src/routes/posts.tsapps/api/src/schemas/fields.tsapps/api/src/schemas/posts.tsapps/docs/features/custom-fields.mdxapps/docs/tools/mcp.mdxapps/docs/tools/sdk.mdxapps/mcp/src/server.tsapps/mcp/src/tools/fields.tsapps/mcp/src/tools/posts.ts
| ## Writing fields through the API | ||
|
|
||
| Create field definitions first: | ||
|
|
||
| ```json | ||
| { | ||
| "key": "audience", | ||
| "name": "Audience", | ||
| "type": "multiselect", | ||
| "required": false, | ||
| "options": [ | ||
| { "value": "developers", "label": "Developers" }, | ||
| { "value": "founders", "label": "Founders" } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| Then pass values by field key when creating or updating posts: | ||
|
|
||
| ```json | ||
| { | ||
| "title": "Launch Notes", | ||
| "content": "<p>Hello world</p>", | ||
| "description": "What changed this week", | ||
| "slug": "launch-notes", | ||
| "categoryId": "cat_123", | ||
| "status": "draft", | ||
| "fields": { | ||
| "audience": ["developers", "founders"] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Unknown field keys, wrong value types, and option values that do not match a | ||
| configured `select` or `multiselect` option are rejected with `400` responses. | ||
|
|
There was a problem hiding this comment.
Replace payload-only JSON snippets with full API request/response examples.
This API section currently shows body fragments only, so readers can’t execute or verify the workflow end-to-end (auth, endpoint, status, success/error responses, rate-limit behavior).
Suggested docs structure update
## Writing fields through the API
-Create field definitions first:
-
-```json
-{
- "key": "audience",
- "name": "Audience",
- "type": "multiselect",
- "required": false,
- "options": [
- { "value": "developers", "label": "Developers" },
- { "value": "founders", "label": "Founders" }
- ]
-}
-```
+<RequestExample method="POST" path="/v1/fields">
+```bash title="create-field.sh"
+curl -X POST "https://api.marblecms.com/v1/fields" \
+ -H "Authorization: Bearer mb_private_xxx" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "key": "audience",
+ "name": "Audience",
+ "type": "multiselect",
+ "required": false,
+ "options": [
+ { "value": "developers", "label": "Developers" },
+ { "value": "founders", "label": "Founders" }
+ ]
+ }'
+```
+</RequestExample>
+
+<ResponseExample status={201}>
+```json
+{ "field": { "key": "audience", "type": "multiselect", "required": false } }
+```
+</ResponseExample>
+
+<ResponseExample status={400}>
+```json
+{ "error": "Invalid custom field value", "message": "..." }
+```
+</ResponseExample>
+
+<ResponseExample status={429}>
+```json
+{ "error": "Rate limit exceeded", "message": "Limit: 60 requests/minute" }
+```
+</ResponseExample>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/docs/features/custom-fields.mdx` around lines 55 - 90, The API
documentation section "Writing fields through the API" contains only raw JSON
payload snippets without showing the complete request/response workflow. Replace
the first JSON code block (starting with the field definition containing "key":
"audience") with a RequestExample component that includes the HTTP method
(POST), endpoint path (/v1/fields), a curl command with proper Authorization and
Content-Type headers, and the JSON payload. Add corresponding ResponseExample
components for success (status 201), validation errors (status 400), and rate
limiting (status 429). Apply the same pattern to the second JSON snippet (for
creating/updating posts) by wrapping it in RequestExample/ResponseExample
components showing the appropriate endpoint, method, headers, and response
cases. Ensure all examples are executable curl commands that readers can copy
and test directly.
Source: Coding guidelines
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a24733f720
ℹ️ 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 field = await db.$transaction(async (tx) => { | ||
| if (typeChanged || optionsChanged) { | ||
| const fieldValueCount = await tx.fieldValue.count({ |
There was a problem hiding this comment.
Use serializable isolation for schema-change checks
When the API changes a field's type or options, this transaction counts existing fieldValue rows and then updates the field under the default isolation level. If a post create/update writes a value for the same field concurrently after the count returns 0, the schema update can still commit, leaving saved values that were validated against the old type/options. Use the same serializable transaction/locking approach as the dashboard field update path so the “no saved values before schema changes” invariant holds under concurrent API traffic.
Useful? React with 👍 / 👎.
| .filter((field) => field !== undefined); | ||
|
|
||
| for (const field of fieldsToValidate) { | ||
| const validation = validateFieldValue(field, json[field.key]); |
There was a problem hiding this comment.
Use own-property checks for custom field lookup
For a workspace field whose key is allowed by the create schema but exists on Object.prototype (for example constructor or __proto__), omitting that key during post creation reads the inherited value from {} instead of undefined. In create mode that makes optional fields fail type validation even though the client did not provide them, so look up values only when the parsed fields object has the key as an own property.
Useful? React with 👍 / 👎.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
marble-jobs | b075656 | Commit Preview URL Branch Preview URL |
Jun 19 2026, 07:38 PM |
Description
Adds first-class custom field support to the public API and MCP server. This includes
/v1/fieldsread/write endpoints, OpenAPI schemas for field definitions and options, key-based custom field validation for post create/update, MCP field tools, and docs updates for the API/SDK/MCP workflow.Motivation and Context
Custom fields already existed in the data model and were returned on post reads, but field definitions and post field writes were not exposed through the public API or MCP server. This lets agents and SDK/API users deliberately create field definitions, validate post field payloads against existing schema, and write custom field values without implicit schema creation.
How to Test
pnpm lint.pnpm exec tsc -p apps/mcp/tsconfig.json --noEmit.400responses.null.Validation performed locally:
pnpm lintpnpm exec tsc -p apps/mcp/tsconfig.json --noEmit{ error, message, details }Screenshots (if applicable)
N/A
Video Demo (if applicable)
N/A
Types of Changes
Summary by CodeRabbit
New Features
Documentation
Bug Fixes