Skip to content

fix(core): save a blank url, datetime or number value as null - #3790

Open
DiogoDuart3 wants to merge 1 commit into
emdash-cms:mainfrom
DiogoDuart3:fix/blank-scalar-null
Open

DiogoDuart3 wants to merge 1 commit into
emdash-cms:mainfrom
DiogoDuart3:fix/blank-scalar-null

Conversation

@DiogoDuart3

Copy link
Copy Markdown
Contributor

What does this PR do?

If an editor fills in an optional url or datetime field and then clears it, the admin sends "" for that field. Validation accepts only null or a valid value for these types, so every save and autosave of the entry then fails. The toast reads "Website must be a valid URL." for a url field and "Starts at has an invalid value." for a datetime field. The entry cannot be saved again until the field is refilled.

#3354 added a blank-to-null step for portableText, multiSelect and repeater in normalizeFieldValues, which runs before validation on create and update. That PR named url, datetime, number and integer as uncovered, and this PR adds them to the same step. None of these types has a meaningful blank string. The fix applies to every client (admin, REST, MCP), and an entry that already stores "" is repaired on its next save.

  • Required fields still reject a blank value. The issue list is now a single required, where before a required url field reported both invalid_format and required.
  • string, text and slug fields still store "".
  • select is left out. fix(core): save a blank rich text, multi-select or repeater value as null #3354 listed it too, but a select without options validates as a plain string, where "" is a real value, so turning it into null would change stored data.

Reproduction on a fresh demos/simple:

  1. Sign in with /_emdash/api/setup/dev-bypass?redirect=/_emdash/admin.
  2. Create a collection events with optional fields website (URL) and starts_at (Date/time).
  3. Create an entry with both fields filled in and save it.
  4. Clear Website and click Save. The PUT returns 400 invalid_format and the toast says "Website must be a valid URL."
  5. Reload, clear Starts at and click Save. The PUT returns 400 invalid_union.

With this change, both saves return 200 and the fields are stored as null.

No existing issue.

Type of change

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

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Not applicable: no admin strings.
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: not applicable (bug fix).
  • I have included screenshots below if this PR changes the UI. Not applicable: no UI change.

AI-generated code disclosure

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

Screenshots / test output

Not applicable (no UI change).

The new test is packages/core/tests/integration/content/blank-scalar-field.test.ts, which runs on SQLite and Postgres. Without the runtime change, 4 of its 5 cases fail. The fifth, which checks that a blank string field is kept, passes either way.

  • Creating an entry with blank url, datetime, number (min: 1) and integer values stores NULL in all four columns.
  • An update that sends a cleared url and datetime stores null.
  • An entry with "" written directly into its url column is read back, re-sent as the admin does, and saved with null.
  • A required url sent as "" is rejected with exactly one required issue.

Results:

  • SQLite: packages/core full suite, 759 files and 9155 tests passed.
  • Postgres: the new file and blank-array-field.test.ts run with EMDASH_TEST_PG passed (15 passed, 1 skipped by design).
  • pnpm build, pnpm typecheck, pnpm lint and pnpm format:check are clean.
  • pnpm test:e2e field-widgets autosave content-crud form-data-loss: 28 passed. The full e2e suite was not run.

Note: #3129 (open) edits normalizeFieldValues too, so whichever PR merges second needs a small rebase.

https://claude.ai/code/session_011SKvTndrtRUPdctoaRyUw9

Once an optional url or datetime field had been filled in and then cleared,
the admin sent "" for it. The content schema accepts null or a valid value
for these types, so every save and autosave of the entry failed with
"<field> must be a valid URL." or "has an invalid value", and the editor
could not recover without the field being refilled.

The pre-validation normalizer that already turns a blank string into null
for array-valued fields now does the same for url, datetime, number and
integer fields. None of these types has a meaningful blank string, so this
covers every client (admin, REST, MCP) and repairs an entry that already
stores "" on its next save. Required fields still reject the value, now
reported only as `required` instead of also as an invalid URL. String,
text and slug fields keep accepting "".

Claude-Session: https://claude.ai/code/session_011SKvTndrtRUPdctoaRyUw9
@changeset-bot

changeset-bot Bot commented Oct 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ed5c63f

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

This PR includes changesets to release 12 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-test Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@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 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@DiogoDuart3

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Oct 2, 2026
@DiogoDuart3

Copy link
Copy Markdown
Contributor Author

The two red checks here are not from the change: "EmDashBot review" stopped with "model over capacity after 3 attempt(s)", and "label" failed removing a label that did not exist. The test suite has not run on this PR yet.

Could a maintainer reapply the bot:review label to retry? The branch still merges cleanly into main.

@CacheMeOwside CacheMeOwside added the bot:review Trigger an emdashbot code review on this PR label Oct 8, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix in the right place. Clearing an optional URL, datetime, or numeric field leaves the client with no meaningful way to express "empty" other than "", and the runtime’s existing normalizeFieldValues step is exactly where that translation already happens for array-valued fields. Expanding the same blank-to-null normalization to url, datetime, number, and integer is consistent, keeps validation rules unchanged, and applies to every write path because it sits inside the shared handleContentCreate / handleContentUpdate class methods (MCP, REST admin, and direct runtime calls all flow through those).

What I checked:

  • Traced normalizeFieldValues call sites in handleContentCreate and handleContentUpdate; all write paths hit it before validation.
  • Verified that zod-generator.ts uses .nullish() for non-required fields, so null round-trips cleanly after normalization.
  • Confirmed that required fields still produce a single required issue because the empty-string is turned into null before the required-field pass runs.
  • Verified BLANK_AS_NULL_FIELD_TYPES has no leftover references after the rename.
  • Review SQL in the new tests: uses sql.ref() and parameterized values, consistent with AGENTS.md.
  • Did not run the test suite; my review is static.

Headline issue: the renamed constant still carries its old doc comment, which is now false. I also flagged that the method docstring omits integer, and that a few test assertions assume item.data returns explicit null for cleared fields, while ContentRepository.mapRow strips null values from data. If the tests pass as reported, that serialization detail must be guaranteed elsewhere, but I’d tighten the assertions so they don’t depend on that invariant.

@@ -304,7 +304,15 @@ const DRAFT_ONLY_UPDATE_KEYS = new Set([
]);

/** Field types whose schema is an array, so a stored blank string can never validate. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The doc comment was left unchanged when ARRAY_FIELD_TYPES was renamed and expanded to BLANK_AS_NULL_FIELD_TYPES. The set now contains non-array scalar types, so the comment is factually wrong and will mislead future readers.

Suggested change
/** Field types whose schema is an array, so a stored blank string can never validate. */
/** Field types whose schema has no meaningful blank string, so blank input is normalized to null before validation. */

Comment on lines 5811 to 5814
* Normalize field values in content data before validation.
* Turns a blank string in an array-valued field into `null`, and fills
* Turns a blank string into `null` in a field whose type has no blank value
* (array, url, datetime and number fields), and fills
* missing image/file dimensions, storageKey, mimeType, and filename from providers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The docstring lists the affected types as “array, url, datetime and number fields” but omits integer, which is also in BLANK_AS_NULL_FIELD_TYPES. Update the parenthetical to be complete.

Suggested change
* Normalize field values in content data before validation.
* Turns a blank string in an array-valued field into `null`, and fills
* Turns a blank string into `null` in a field whose type has no blank value
* (array, url, datetime and number fields), and fills
* missing image/file dimensions, storageKey, mimeType, and filename from providers.
* (array, url, datetime, number and integer fields), and fills

Comment on lines +72 to +73
expect(updated.data.item.data.website).toBeNull();
expect(updated.data.item.data.starts_at).toBeNull();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] These assertions assume item.data returns an explicit null for a cleared scalar field. ContentRepository.mapRow intentionally excludes null values from data, so in the normal case data.website/data.starts_at will be undefined, which would make toBeNull() fail. If the tests pass currently, the response shape is preserving nulls through some other path I did not trace; to avoid coupling the test to that, consider asserting directly against the database (as the first case does) or using a looser check such as expect(value).not.toBe("") / expect(value).toBeFalsy().


expect(updated.success).toBe(true);
if (!updated.success) return;
expect(updated.data.item.data.website).toBeNull();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Same shape concern as lines 72–73: this relies on item.data.website being returned as explicit null. Consider asserting against the database row or using a check that does not depend on whether nulls are kept in the serialized data object.

@emdashbot emdashbot Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed bot:review Trigger an emdashbot code review on this PR review/needs-review No maintainer or bot review yet labels Oct 8, 2026
@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet and removed review/awaiting-author Reviewed; waiting on the author to respond labels Oct 9, 2026
@DiogoDuart3

Copy link
Copy Markdown
Contributor Author

Friendly ping: this still merges cleanly into main, and the test suite has not run on it yet (the two red checks are the bot review running out of capacity and the label step, not the change). 1.3.0 shipped #3791, which fixes the cleared number field in the admin; this PR is still needed for a cleared optional datetime or url sent through the API, MCP or any other client. Could a maintainer re-run the checks or reapply bot:review?

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.

2 participants