feat(content): support indexed custom field sorting - #2212
Conversation
🦋 Changeset detectedLatest commit: 1e1bf1d The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 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 |
Scope checkThis PR changes 920 lines across 35 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. |
@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/registry-verification
@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. |
a3f5bcc to
9509332
Compare
9509332 to
3a15f15
Compare
138f0cd to
33ed15e
Compare
There was a problem hiding this comment.
The server-side sorting approach is sound: it adds physical SQLite indexes on real ec_* columns for scalar field types, keeps pagination cursor-stable with explicit null ordering, and validates the indexed flag against a whitelist of indexable types. That fits EmDash’s schema-in-the-database architecture.
Two things block a clean sign-off:
-
AGENTS.md Discussion requirement. The PR introduces a new user-facing feature (indexed custom-field sorting) but the description explicitly states that the linked Discussion #1717 only covers
admin.listColumns(#2194) and “no Discussion has been opened for indexed sorting yet.” EmDash requires a maintainer-approved Discussion before merging a feature. This needs to be opened/approved before merge. -
FieldEditor bug when changing an indexed field to a non-indexable type. The editor sends
indexed: undefinedwhen the selected type is not indexable. The backend interpretsundefinedas “preserve existing value,” so an already-indexed field that is changed to a non-indexable type (e.g.string→text) fails withFIELD_NOT_INDEXABLEinstead of dropping the index. Fix by explicitly sendingfalsefor non-indexable types.
Other notes:
- The diff still includes the stacked #2194
admin.listColumnscommits/changeset. Review comments here focus on the indexed-sorting additions; the list-columns pieces should land via #2194. - Server support for custom-field sorting is complete, but the admin content list does not yet expose sort controls on custom columns (the new columns are display-only). If admin UI for selecting a custom sort is intended for this PR, it is missing.
- I did not run tests/lint/typecheck (no shell/tooling). Test coverage looks reasonable for null ordering, equal-value stability, unindexed-field rejection, and index lifecycle; the query-plan assertions are brittle but directly verify the stated performance goal.
| required, | ||
| unique, | ||
| searchable: isSearchableType ? searchable : undefined, | ||
| indexed: isIndexableType ? indexed : undefined, |
There was a problem hiding this comment.
[needs fixing] When the selected type is not indexable, this sends indexed: undefined. In updateField the backend computes nextIndexed = input.indexed ?? field.indexed, so an already-indexed field that is changed to a non-indexable type (e.g. string → text, same column type) keeps indexed = true and assertIndexableField throws FIELD_NOT_INDEXABLE. The UI gives the user no way to recover because the Indexed switch has disappeared.
Send false explicitly for non-indexable types so the index is dropped and the type change can succeed:
| indexed: isIndexableType ? indexed : undefined, | |
| indexed: isIndexableType ? indexed : false, |
| "@emdash-cms/admin": minor | ||
| --- | ||
|
|
||
| Adds opt-in database indexes for scalar custom fields and stable cursor pagination when ordering content lists by those fields. |
There was a problem hiding this comment.
[needs fixing] This changeset describes a new user-facing feature, but AGENTS.md requires a prior maintainer-approved Discussion for features. The PR description acknowledges that Discussion #1717 covers admin.listColumns (#2194) only and that no Discussion has been opened for indexed custom-field sorting yet. Please open and get approval for a Discussion covering this feature before merging.
Three blocks defend sending `indexed: false` or narrate a Playwright workaround. The tests already name the contract they cover. The bind-budget note above the seed batch size stays: it records a limit a reader would otherwise raise.
The file moved to 061 when upstream took 059; this import still named 060, so the suite could not load the module at all.
0688023 to
3cdeddc
Compare
| const indexName = this.getFieldIndexName(fieldId); | ||
|
|
||
| await sql` | ||
| CREATE INDEX IF NOT EXISTS ${sql.ref(indexName)} |
There was a problem hiding this comment.
Localised list queries filter by locale, but this index orders every locale together. On a collection where the requested locale contains only a small fraction of entries, the database must walk and discard rows from other locales to fill each page, approaching a collection-wide index scan. Please make locale-scoped custom-field ordering seekable,e.g. put locale before the ordering tuple
Reconciles upstream's indexed custom field sorting (emdash-cms#2212) with this branch's storage-less reference fields. Conflict resolutions: - registry.ts deleteField: drop the field index when `field.indexed`, then drop the column behind the `columnExists` guard. A field being storage-less is a property of the row, not the type, so pre-existing reference columns still need the DDL. - seed/apply.ts existing-field path: keep `upsertSeedField`, which supersedes the inline update/create pair. - seed/apply.ts new-collection path: keep the reference-relation loop and carry `indexed` through to the created field. - upsertSeedField: pass `indexed` to both updateField and createField. Seeding an existing collection routes through here, so omitting it would drop the flag for every seeded field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What does this PR do?
Adds server-backed sorting for custom scalar fields that are explicitly marked
indexed.Collection fields gain an opt-in
indexedflag. EmDash validates that flag against supported scalar field types, maintains a normalized index table when content changes, and allows the existingorderBycontract to sort by those field slugs. Sorting remains database-backed and cursor-safe, so it applies to the complete result set instead of only the currently loaded admin page.This enables uses such as SEO score, ticket priority, workflow rank, or other numeric and textual metadata without introducing full-table scans.
The collection administration contract this builds on has landed in emdash-cms/emdash PR 2194.
Addresses the structured sorting portion of emdash-cms/emdash issue 2179.
Discussion: indexed sorting; configured content-list fields.
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
Validated on the current
mainbase at the time of the update:GitHub CI runs the complete branch matrix.