Skip to content

fix(core): apply or reject field type changes on update instead of dropping them (#1397)#1556

Merged
ascorbic merged 4 commits into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1397-updatefield-type-change
Jun 30, 2026
Merged

fix(core): apply or reject field type changes on update instead of dropping them (#1397)#1556
ascorbic merged 4 commits into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1397-updatefield-type-change

Conversation

@marcusbellamyshaw-cell

@marcusbellamyshaw-cell marcusbellamyshaw-cell commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

SchemaRegistry.updateField built its SET clause without type / column_type, and UpdateFieldInput had no type field, so emdash seed --on-conflict update (and the seed update branch in seed/apply.ts) couldn't express a field-type change. Re-seeding after changing a field's type in seed.json reported success and bumped fields.updated, while _emdash_fields.type / column_type kept their old values — typegen then regenerated stale TS types with no warning.

This adds type to UpdateFieldInput and writes type / column_type in updateField:

  • A type change that keeps the same column affinity (e.g. stringslug, both TEXT) is applied.
  • A change that would alter the column type (e.g. textportableText, TEXTJSON) is rejected with a FIELD_TYPE_COLUMN_CHANGE SchemaError, because there is no in-place column migration and silently rewriting only the metadata would desync column_type from the real ec_* column.

The seed update branch now passes the field type through, so a re-seed either applies the change or fails loudly instead of silently dropping it. Backwards-compatible: callers that don't pass type get a no-op write of the existing value.

Closes #1397

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (targeted: schema/registry.test.ts + seed-on-conflict + seed/schema suites)
  • pnpm format has been run
  • I have added/updated tests for my changes
  • User-visible strings in the admin UI are wrapped for translation (n/a — server-side English error message, stable SCREAMING_SNAKE_CASE code)
  • I have added a changeset

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 ultracode

Screenshots / test output

New tests cover both layers: updateField applies a same-affinity change and rejects an affinity-changing one; applySeed --on-conflict update persists a same-affinity change and rejects an affinity-changing one instead of silently reporting success. Schema + seed suites green (248 + 85 tests).

…opping them (emdash-cms#1397)

SchemaRegistry.updateField built its SET clause without type/column_type,
and UpdateFieldInput had no `type` field, so `emdash seed --on-conflict
update` (seed/apply.ts) couldn't express a field-type change. Re-seeding
after changing a field's type reported success and bumped fields.updated
while _emdash_fields.type/column_type kept their old values — stale typegen
with no warning.

Add `type` to UpdateFieldInput and write type/column_type in updateField.
A type change that keeps the same column affinity (string -> slug, both
TEXT) is applied; one that would change the column type (text -> portableText,
TEXT -> JSON) is rejected with a FIELD_TYPE_COLUMN_CHANGE SchemaError, since
no in-place column migration exists and rewriting only the metadata would
desync column_type from the real ec_* column. The seed update branch now
passes the field type through.

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

changeset-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c91fc32

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

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@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 Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash 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

@emdashbot

emdashbot Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Could not push formatting changes to this fork. The contributor may have "Allow edits by maintainers" disabled.

Please run the formatter locally:

pnpm format
Collapse multi-line .innerJoin() calls onto single lines per the repo's
oxfmt config. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@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.

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jun 24, 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.

The approach is sound and targets the right problem: SchemaRegistry.updateField previously ignored field-type changes, so emdash seed --on-conflict update silently dropped them. Adding type to UpdateFieldInput, applying it only when the SQLite column affinity is unchanged, and rejecting affinity-changing updates with a clear FIELD_TYPE_COLUMN_CHANGE error is the right fix for #1397 and avoids corrupting the metadata/column mapping.

I checked the diff, the full registry implementation, the API schema/route layer, seed validation/apply, the error-code catalog, and the new tests. The registry and seed layers are correctly implemented and well-tested for both the success and rejection paths. However, the HTTP API contract was not kept in sync: the Zod body schema for PUT /_emdash/api/schema/collections/{slug}/fields/{fieldSlug} still omits type, so the new capability is unreachable from the admin UI or any API caller. That is a blocking inconsistency introduced by the PR.

Other than that, the only notable gap is a minor catalog omission in ErrorCode. Once the API schema accepts type, this should be good to merge.


Findings

  • [needs fixing] packages/core/src/api/schemas/schema.ts:126-137

    The updateFieldBody schema does not include type, so Zod strips it from the request body before the route casts the validated body to UpdateFieldInput and passes it to handleSchemaFieldUpdate. Even though UpdateFieldInput and SchemaRegistry.updateField now support type changes, API callers (and the admin UI) cannot actually use them.

    Add type as an optional field-type value:

    export const updateFieldBody = z
    	.object({
    		label: z.string().min(1).optional(),
    		type: fieldTypeValues.optional(),
    		required: z.boolean().optional(),
    		unique: z.boolean().optional(),
    		defaultValue: z.unknown().optional(),
    		validation: fieldValidation.nullable(),
    		widget: z.string().optional(),
    		options: fieldWidgetOptions,
    		sortOrder: z.number().int().min(0).optional(),
    		searchable: z.boolean().optional(),
    		translatable: z.boolean().optional(),
    	})
    	.meta({ id: "UpdateFieldBody" });
    

    Please also add a unit test in packages/core/tests/unit/api/schemas.test.ts asserting that updateFieldBody.parse preserves a valid type value.

  • [suggestion] packages/core/src/api/errors.ts:63

    FIELD_TYPE_COLUMN_CHANGE is returned from SchemaRegistry.updateField but is not listed in the ErrorCode constant alongside the other schema codes. Adding it keeps the catalog complete; mapErrorStatus already falls through to 400 for it, so no mapping change is required unless you want to make that explicit.

@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: c91fc32

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jun 24, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator

@marcusbellamyshaw-cell can you look at the review feedback?

@ascorbic

Copy link
Copy Markdown
Collaborator

/review

@ask-bonk ask-bonk 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.

The core fix is correct and well-tested at the registry and seed layers. However, the previous review (emdashbot, 2026-06-24) flagged a blocking inconsistency that has not been addressed in the two later commits: the PUT /_emdash/api/schema/collections/{slug}/fields/{fieldSlug} Zod body schema still omits type, so the new capability is unreachable from the admin UI and any other HTTP caller. Re-raising that finding plus one minor observation.

Findings

[blocking] packages/core/src/api/schemas/schema.ts:125-138updateFieldBody does not accept type.

Zod's default .object() strips unknown keys before the route's as UpdateFieldInput cast in packages/core/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts:58. So even though SchemaRegistry.updateField now supports a type change, every HTTP caller — including the admin UI's updateField client in packages/admin/src/lib/api/schema.ts:253, which already serializes the full input — has its type silently stripped before it reaches handleSchemaFieldUpdate. The fix in this PR therefore only works via direct registry/seed callers; the public API surface still has the original #1397 bug.

Suggested change:

export const updateFieldBody = z
	.object({
		label: z.string().min(1).optional(),
		type: fieldTypeValues.optional(),
		required: z.boolean().optional(),
		// ...rest unchanged
	})
	.meta({ id: "UpdateFieldBody" });

Please also add a unit test in packages/core/tests/unit/api/schemas.test.ts confirming updateFieldBody.parse({ type: "slug" }) preserves type, and ideally an integration test that PUTs { type: "slug" } through the HTTP route and reads back the persisted _emdash_fields.type. Without an end-to-end test exercising the route, future regressions in the API layer will keep hiding under registry-level coverage.

[minor] packages/core/src/api/errors.ts:63FIELD_TYPE_COLUMN_CHANGE missing from the ErrorCode catalog.

FIELD_TYPE_COLUMN_CHANGE is thrown by SchemaRegistry.updateField (registry.ts line 549) but isn't listed in the ErrorCode constant alongside the other schema codes. mapErrorStatus's default branch already returns 400 for non-_ERROR codes so HTTP behavior is correct, but adding it here keeps the catalog complete and discoverable for admin-side message mapping.

}
nextType = input.type;
nextColumnType = newColumnType;
}

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.

Optional / minor: when input.type === field.type (no-op type write — what the existing tests for unrelated updates exercise), the new code falls through to writing type/column_type back to their existing values. That's harmless but means every updateField call — including ones that only flip required or widget — now writes two more columns than before. If you want to preserve the previous SET-clause shape for the common case, you can guard the new columns inside if (input.type !== undefined).

Not blocking; flagging only because the PR description claims a no-op write of the existing value, which is accurate but worth confirming is intentional.

@ask-bonk

ask-bonk Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Summary of what I posted:

  • Blocking finding (re-raised): updateFieldBody Zod schema in packages/core/src/api/schemas/schema.ts:125-138 still omits type, so the new capability is unreachable from the admin UI / any HTTP caller — Zod strips type before the as UpdateFieldInput cast in the route handler. The previous emdashbot review flagged this 5 days ago and the maintainer pinged the contributor today, but the two follow-up commits didn't address it.
  • Minor: FIELD_TYPE_COLUMN_CHANGE is thrown but missing from the ErrorCode catalog in api/errors.ts (HTTP status is still correctly 400 via the default branch).
  • Inline (optional): Note on registry.ts that the new code unconditionally writes type/column_type back to existing values on every updateField call, even when type isn't being changed — harmless but slightly more write churn than before.

github run

…pe changes reach the registry

The updateFieldBody Zod schema omitted `type`, so it was stripped from the
request body before the route cast to UpdateFieldInput — making the new
field-type-change capability unreachable from the admin UI and any HTTP caller.
Add `type` to the schema, add FIELD_TYPE_COLUMN_CHANGE to the ErrorCode catalog,
and cover type preservation in the schemas unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jun 29, 2026
@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor Author

Addressed both findings in 88a9484:

  • [blocking] Added type: fieldTypeValues.optional() to updateFieldBody so a field-type change survives Zod and reaches handleSchemaFieldUpdateSchemaRegistry.updateField. Added a unit test in schemas.test.ts asserting updateFieldBody.parse({ type: "slug" }) preserves type.
  • [minor] Added FIELD_TYPE_COLUMN_CHANGE to the ErrorCode catalog (HTTP status was already 400 via the default branch).

Targeted suites green (schemas.test.ts 38, registry.test.ts 39); typecheck, lint, and format clean.

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor Author

@ascorbic — unrelated to this PR, a quick logistics ask while I have you. I'm capped at 10 open PRs against the repo, and I've been at the cap for about a week, so I can't promote my draft (#1588) to a real PR or open any new bug fixes until some of the current ones land. They're all small, single-issue fixes against tracked issues (#1397, #1480, #1178, #1370, #1416, #1368, #1415, #1551, #1463, plus the #1588 draft for the Workers afterDelete/afterUnpublish hook cancellation).

Would it be possible to raise my open-PR limit a bit, or is there anything I can do on my end to help get the queue moving? Happy to rebase, split, or reprioritize whichever are easiest to review first. Thanks!

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ascorbic

Copy link
Copy Markdown
Collaborator

@marcusbellamyshaw-cell the limit is to keep it manageable for me to review the PRs. When there's a huge number of PRs it makes it hard to review them and to keep them deconflicted. I'm the only person reviewing PRs right now, and I am reviewing them as quickly as I can. The best thing you can do to get them merged quickly is to respond to review comments from me or bots. Thanks for your patience.

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor Author

Will do 👍

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

@ascorbic ascorbic merged commit 0f8d1ff into emdash-cms:main Jun 30, 2026
45 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core bot:review Trigger an emdashbot code review on this PR cla: signed overlap review/needs-rereview Author pushed changes since the last review size/M

2 participants