feat(schema): field constraint improvements — unique, required, indexed, validation - #1512
feat(schema): field constraint improvements — unique, required, indexed, validation#1512Glacier-Luo wants to merge 20 commits into
Conversation
🦋 Changeset detectedLatest commit: c492c6b The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 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 |
PR template validation failedPlease fix the following issues by editing your PR description:
See CONTRIBUTING.md for the full contribution policy. |
Scope checkThis PR changes 2,155 lines across 29 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. |
Lunaria Status Overview🌕 This pull request will trigger status changes. Learn moreBy default, every PR changing files present in the Lunaria configuration's You can change this by adding one of the keywords present in the Tracked Files
Warnings reference
|
@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/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: |
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. |
Adds `locale`, `translation_group`, and 9 index-collision slugs (e.g. `author`, `scheduled`) to RESERVED_FIELD_SLUGS. Prevents ADD COLUMN conflicts and index name collisions with built-in indexes created by createContentTable().
Creates a partial unique index (scoped by locale, excluding soft-deleted rows) when a field has unique=true. Validates type compatibility (rejects JSON/image/file) and the non-translatable+unique contradiction. Handles unique constraint changes on updateField (with duplicate-value pre-check), cleans up indexes on deleteField before DROP COLUMN. Adds unique-aware conflict detection to content restore and unique-field clearing on content duplicate to prevent constraint violations.
…ations Adds onError handlers to createMutation (ContentNewPage), addFieldMutation, updateFieldMutation, and deleteFieldMutation so API errors surface as toast notifications instead of being silently swallowed. Fixes the floating Promise bug in ContentTypeEditor.handleFieldSave by awaiting onAddField/onUpdateField — on error the dialog stays open for retry instead of closing immediately. Adds cross-status unique constraint tests confirming the DB index enforces uniqueness regardless of content status (draft vs published).
Extracts unique field logic into api/handlers/unique-check.ts and adds a pre-check in emdash-runtime before draft revision or content table writes. Checks both content table columns and draft revision JSON data to catch published and draft-vs-draft conflicts.
Backfill NULL rows with type-appropriate defaults when a field changes from optional to required; on Postgres also SET/DROP NOT NULL.
Adds a declarative `indexed` boolean to custom fields. When enabled, a B-tree index is created on the content table column to accelerate WHERE/ORDER BY queries. Non-indexable types (JSON, image, file, repeater) are rejected at the API layer. Unique supersedes plain indexed — when both are set, only the unique index is created. Includes migration 044, Admin UI toggle, CLI/MCP/seed passthrough, and seed `translatable` passthrough fix.
…untime Adds a repeater case to getBaseSchema (was falling through to z.unknown()) and a ZodArray branch to applyValidation so minItems and maxItems constraints are enforced for both repeater and multiSelect fields.
…tion delete Content hard-delete now also removes content_taxonomies and _emdash_content_bylines rows. Collection delete now cleans up revisions, content_taxonomies, _emdash_comments, _emdash_seo, and _emdash_content_bylines before dropping the content table.
Expose min/max selection constraints in the field editor for multiSelect fields, matching the existing repeater minItems/maxItems UI. The Zod runtime validation already supported this; this commit adds the admin UI surface with localized labels (Min Selections / Max Selections).
These slugs collide with locale composite index names created by
migration 041 (idx_{table}_loc_upd, idx_{table}_loc_crt).
Replace sql.raw(fillValue) with Kysely parameterized interpolation in the UPDATE statement. Adds toColumnValue() and getEmptyColumnValue() that return JS values instead of SQL literals, eliminating the injection surface from formatDefaultValue's manual quote-escaping.
…tent Add missing indexed field to the Zod response schema. Guard migration 044 with columnExists check so it can be safely re-run. Add 044 to the trailing migrations test list.
Add tests verifying that _emdash_seo, _emdash_comments, and revisions rows are cleaned up when content is permanently deleted. These three tables were cleaned up by the orphan data commit but lacked test coverage.
- checkDuplicateValues: replace HAVING cnt with HAVING COUNT(*) > 1 (Postgres does not support SELECT alias in HAVING) - checkUniqueFieldConflicts: use dialect-aware json_extract/->>'path' instead of SQLite-only json_extract() - syncRequiredConstraint: fall back to empty default when toColumnValue returns null (explicit null defaultValue edge case) - Migration 044 down(): guard with columnExists for idempotency - Add 12 tests for checkUniqueFieldConflicts covering content-table conflicts, draft-revision conflicts, self-exclusion, soft-delete exclusion, and locale scoping
- duplicate(): keep required non-string unique values instead of deleting them (which caused NOT NULL violations for integer/number) - restore(): add validateIdentifier() before sql.ref(field.slug) for defense-in-depth consistency - Fix false-positive locale-scope test that used wrong collection slug and never exercised the locale filtering logic
When a unique field changes from optional to required, the NULL backfill would set all NULL rows to the same default value, violating the unique index. Add a pre-check that rejects the operation when multiple NULLs exist in the same locale. Also add behavioral tests for required=true unique index enforcement and duplicate-allowed verification after unique→plain index switch.
- Rebuild unique index predicate when only `required` changes on a unique field - Prevent double "(Copy)" suffix when title/name is also a unique field - Move validateIdentifier before tableName construction in unique-check - Add tests for index predicate rebuild and duplicate dedup
- Add min={0} on Min Selections/Min Items to prevent negative values
- Add min={1} on Max Selections/Max Items to prevent zero upper bound
- Format migration 044 (oxfmt, no logic change)
131a50a to
c492c6b
Compare
|
Rebased onto latest main and resolved migration numbering conflict (044 → 045). All tests pass — ready for review when you get a chance. @ascorbic |
|
Hi @Glacier-Luo The indexed fields part is a new feature4 and should have a discussion first. There are also some translation changes that have slipped into this PR and should land separately too. |
OK, i'll do that.:) |
|
Closing per my comment. To be replaced with individual PRs |
What does this PR do?
Strengthens field constraints across the schema layer, fixing several gaps where constraints were declared but not enforced at the database level.
Unique constraints
Required field sync
NOT NULLcolumn constraintNOT NULLconstraintIndexed fields (new feature)
indexedboolean option to custom fieldsidx_{table}_{column})indexedcolumn to_emdash_fieldsValidation
minItems/maxItemsfor repeater and multiSelect fields at runtime (Zod schema generation)Reserved field slugs
RESERVED_FIELD_SLUGSto cover all internal columns (locale,translation_group,live_revision_id,draft_revision_id,primary_byline_id,scheduled_at,deleted_at,version,loc_upd,loc_crt)Orphan cleanup
datacolumn when content is permanently deleted or a collection is droppedType of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
Test coverage added for: unique constraint enforcement, required sync backfill, indexed field creation/removal, duplicate/restore with unique fields, orphan cleanup on hard-delete, minItems/maxItems validation, and reserved slug rejection.
🤖 Generated with Claude Code