feat: native content filtering by byline credit - #1439
Conversation
Adds a reserved `byline` key to the `where` clause of getEmDashCollection / loadCollection that joins the _emdash_content_bylines junction table, matching entries credited to one or more byline translation groups in any credit position (not just primary_byline_id). Also adds a getEntriesByByline runtime helper mirroring getEntriesByTerm. Closes #1358
🦋 Changeset detectedLatest commit: ef37607 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 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. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | ef37607 | Jun 12 2026, 03:23 PM |
@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: |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | ef37607 | Jun 12 2026, 03:25 PM |
There was a problem hiding this comment.
Pull request overview
Adds first-class collection filtering by byline credits (via _emdash_content_bylines) so author/archive pages can query any credited byline (including secondary/co-author credits) without raw DB access.
Changes:
- Extend
wherefiltering in the loader/query layer with a reservedbylinekey that INNER JOINs_emdash_content_bylinesand matches by byline translation group(s). - Export a new runtime helper
getEntriesByByline(collection, byline, options)mirroringgetEntriesByTerm. - Add unit tests covering byline filtering behavior across dialects.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/loader.ts | Implements where.byline parsing and SQL join/condition composition with DISTINCT dedupe. |
| packages/core/src/query.ts | Updates public query filter docs to describe the reserved byline where-key. |
| packages/core/src/bylines/index.ts | Adds getEntriesByByline helper wrapping getEmDashCollection(where: { byline }). |
| packages/core/src/index.ts | Re-exports getEntriesByByline from the package entry point. |
| packages/core/tests/unit/loader-byline-filter.test.ts | Adds dialect-parameterized tests for byline credit filtering and composition with other filters. |
| .changeset/byline-content-filter.md | Declares a minor release with the new filter + helper API. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // A byline filter with no groups matches nothing — short-circuit | ||
| // before building SQL (an empty `IN ()` is invalid SQL anyway). | ||
| if (bylineFilter && bylineFilter.groups.length === 0) { | ||
| return { entries: [], cacheHint: { tags: [type] } }; | ||
| } |
| it("ignores range operators on the byline key", async () => { | ||
| const post = await createPost("Solo"); | ||
| await credit(post.id, "byline_alice", 0); | ||
|
|
||
| // A range on `byline` is meaningless; it is dropped with a warning and | ||
| // no byline filter is applied, so all published entries return. | ||
| const result = await load({ byline: { gte: "a" } }); | ||
|
|
||
| expect(result.entries).toHaveLength(1); | ||
| expect(result.entries[0]!.data.title).toBe("Solo"); | ||
| }); |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | ef37607 | Jun 12 2026, 03:24 PM |
There was a problem hiding this comment.
This PR adds a reserved byline key to the where filter in the content loader, INNER JOINing _emdash_content_bylines to match entries by explicit credit (including secondary/co-authored credits). It also adds a getEntriesByByline runtime helper.
Approach judgment: This is the right change solving the right problem. Filtering by primary_byline_id alone misses co-authored entries where a byline is a secondary credit — a genuine gap from #1358. The approach mirrors the existing taxonomy JOIN filter pattern, uses SELECT DISTINCT correctly for dedup, and matches by translation_group so credits span every locale variant. This fits EmDash's architecture well.
What I checked:
- SQL safety: All values are parameterized via Kysely's
sqltagged template. No string interpolation into SQL. Table names are validated before use. Nosql.raw()with variables. - Locale filtering: Both the joined path (byline/taxonomy) and the non-joined path apply
localecorrectly. The byline filter matchestranslation_group, so it spans locales correctly. - AGENTS.md conventions: Reserved key is already in
RESERVED_FIELD_SLUGS— no shadowing risk. Changeset present. Tests added. Junction table already hasidx_content_bylines_bylineonbyline_id. Internal imports use.jsextensions. No admin UI / Lingui / RTL concerns (server-side query API only). - Edge cases: Empty array short-circuits to empty result before emitting invalid
IN (). Range operators are rejected with a warning (consistent with taxonomy handling).null/undefinedvalues are skipped. - Tests: 9 dialect-aware tests covering primary credit, secondary credit, isolation, array OR, DISTINCT dedup, byline+field AND, byline+taxonomy AND, empty array, and range rejection.
- Type consistency:
getEntriesByBylineuses the sameRecord<string, unknown>wrapper pattern as the existinggetEntriesByTermhelper.
The implementation is clean, consistent with existing conventions, and thoroughly tested. I found no bugs, regressions, or convention violations.
The JOIN + SELECT DISTINCT approach threw on Postgres for collections with a json column, since Postgres has no equality operator for json and cannot DISTINCT such rows. Switch both taxonomy and byline filters to correlated EXISTS subqueries: no row fan-out, no DISTINCT, works on SQLite and Postgres. Also short-circuit empty taxonomy filter arrays (parity with byline) to avoid invalid IN () SQL, and strengthen the byline range-rejection test.
|
Pushed Root cause of the Postgres test failuresThe Fix: both taxonomy and byline filters are now applied as correlated Copilot review comments
|
What does this PR do?
Adds a first-class way to query content by byline credit.
getEmDashCollection(and the underlyingloadCollection) now accept a reservedbylinekey inwherethat INNER JOINs the_emdash_content_bylinesjunction table and matches entries credited to one or more byline translation groups.Unlike filtering on the content table's
primary_byline_idcolumn, this matches every explicit credit, so co-authored entries where the byline is a secondary credit are included. This makes author archive pages correct without dropping to rawgetDb()access, which was the only prior workaround.bylinewhere-key in the loader; accepts a single translation group or an array (OR). Composes with taxonomy, field, locale, status, and ordering filters via a sharedSELECT DISTINCTjoined path.byline_idis matched against the byline'stranslation_group(the value credits store since migration 040), so a credit spans every locale of a byline.bylineare rejected with a warning.getEntriesByByline(collection, byline, options)runtime helper, exported from the package entry point, mirroringgetEntriesByTerm.bylinewas already inRESERVED_FIELD_SLUGS, so no user-defined field can shadow the key.Closes #1358
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
New
loader-byline-filter.test.ts: 9 tests underdescribeEachDialect(SQLite + Postgres-in-CI) covering primary credit, secondary/co-authored credit, single-byline isolation, array OR, DISTINCT dedup, byline+field, byline+taxonomy, empty array, and range rejection. Full core suite: 3819 tests pass. Lint: 0 diagnostics. Typecheck: clean.Try this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
feat/byline-content-filter. Updated automatically when the playground redeploys.