Skip to content

feat: native content filtering by byline credit - #1439

Merged
ascorbic merged 4 commits into
mainfrom
feat/byline-content-filter
Jun 12, 2026
Merged

feat: native content filtering by byline credit#1439
ascorbic merged 4 commits into
mainfrom
feat/byline-content-filter

Conversation

@ascorbic

@ascorbic ascorbic commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds a first-class way to query content by byline credit. getEmDashCollection (and the underlying loadCollection) now accept a reserved byline key in where that INNER JOINs the _emdash_content_bylines junction table and matches entries credited to one or more byline translation groups.

Unlike filtering on the content table's primary_byline_id column, 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 raw getDb() access, which was the only prior workaround.

  • Reserved byline where-key in the loader; accepts a single translation group or an array (OR). Composes with taxonomy, field, locale, status, and ordering filters via a shared SELECT DISTINCT joined path.
  • byline_id is matched against the byline's translation_group (the value credits store since migration 040), so a credit spans every locale of a byline.
  • Empty-array filter short-circuits to an empty result; range operators on byline are rejected with a warning.
  • New getEntriesByByline(collection, byline, options) runtime helper, exported from the package entry point, mirroring getEntriesByTerm.

byline was already in RESERVED_FIELD_SLUGS, so no user-defined field can shadow the key.

Closes #1358

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 (n/a — no admin UI strings; server-side runtime query API only)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: filed against issue Add native content filtering by byline credits #1358 (enhancement); opening at maintainer direction.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (OpenCode). Adversarial review pass run via GPT-5.5.

Screenshots / test output

New loader-byline-filter.test.ts: 9 tests under describeEachDialect (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.

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
Copilot AI review requested due to automatic review settings June 12, 2026 14:06
@changeset-bot

changeset-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ef37607

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

This PR includes changesets to release 14 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@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/admin Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@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 added area/core size/L review/needs-review No maintainer or bot review yet labels Jun 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR template validation failed

Please fix the following issues by editing your PR description:

See CONTRIBUTING.md for the full contribution policy.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs ef37607 Jun 12 2026, 03:23 PM
@pkg-pr-new

pkg-pr-new Bot commented Jun 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: ef37607

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground ef37607 Jun 12 2026, 03:25 PM

Copilot AI 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.

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 where filtering in the loader/query layer with a reserved byline key that INNER JOINs _emdash_content_bylines and matches by byline translation group(s).
  • Export a new runtime helper getEntriesByByline(collection, byline, options) mirroring getEntriesByTerm.
  • 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.

Comment thread packages/core/src/loader.ts Outdated
Comment on lines +730 to +734
// 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] } };
}
Comment on lines +183 to +193
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");
});
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache ef37607 Jun 12 2026, 03:24 PM

@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 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 sql tagged template. No string interpolation into SQL. Table names are validated before use. No sql.raw() with variables.
  • Locale filtering: Both the joined path (byline/taxonomy) and the non-joined path apply locale correctly. The byline filter matches translation_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 has idx_content_bylines_byline on byline_id. Internal imports use .js extensions. 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/undefined values 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: getEntriesByByline uses the same Record<string, unknown> wrapper pattern as the existing getEntriesByTerm helper.

The implementation is clean, consistent with existing conventions, and thoroughly tested. I found no bugs, regressions, or convention violations.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Jun 12, 2026
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.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/approved Approved; no new commits since labels Jun 12, 2026
@ascorbic

Copy link
Copy Markdown
Collaborator Author

Pushed ea2acac9 addressing the failing Tests check and the review comments.

Root cause of the Postgres test failures

The Tests job failed only on the [postgres] dialect with could not identify an equality operator for type json. The JOIN + SELECT DISTINCT table.* approach is invalid on Postgres when a selected column is json (Postgres has no equality operator for json, so it can't dedupe such rows). SQLite is permissive, so it passed locally and in the SQLite suite. This was a latent bug in the existing taxonomy filter path too, only never exercised on Postgres (the taxonomy tests were SQLite-only).

Fix: both taxonomy and byline filters are now applied as correlated EXISTS semi-joins instead of INNER JOIN ... DISTINCT. No row fan-out, no DISTINCT, so the json equality problem disappears and it works on both dialects. Verified the full byline + taxonomy + field-filter suites pass on SQLite and a local Postgres 17.

Copilot review comments

  1. Empty taxonomy array → t.slug IN () (loader.ts) — valid, and still latent after the refactor. Empty taxonomy filter arrays now short-circuit to an empty result, matching the byline filter's existing behavior. Added a regression test.
  2. Weak range-rejection test — valid. The test now seeds a second uncredited published entry and asserts both rows return, proving the byline range filter is actually dropped rather than coincidentally passing with a single row.
@ascorbic ascorbic added bot:review Trigger an emdashbot code review on this PR and removed cla: signed labels Jun 12, 2026
@ascorbic
ascorbic enabled auto-merge (squash) June 12, 2026 15:34
@ascorbic
ascorbic merged commit 023893a into main Jun 12, 2026
44 of 45 checks passed
@ascorbic
ascorbic deleted the feat/byline-content-filter branch June 12, 2026 15:36
@emdashbot emdashbot Bot mentioned this pull request Jun 12, 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 review/needs-rereview Author pushed changes since the last review size/L

2 participants