Skip to content

feat: add author_roles table, sync from author frontmatter (#177)#186

Merged
fennifith merged 3 commits into
playfulprogramming:mainfrom
bbornino:feat/author-roles-table
Jul 10, 2026
Merged

feat: add author_roles table, sync from author frontmatter (#177)#186
fennifith merged 3 commits into
playfulprogramming:mainfrom
bbornino:feat/author-roles-table

Conversation

@bbornino

@bbornino bbornino commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #177. Adds an author_roles table populated from sync-author, tracking author roles in a real table instead of profiles.meta.roles, so /content/profiles can eventually filter by role. This follows the same JSONB-to-join-table pattern as #81's post_tags/collection_tags work.

  • New author_roles table: composite PK on (profileSlug, role), FK to profiles.slug with cascade delete, no surrogate id — mirroring the shape of post_tags/collection_tags.
  • sync-author's processor now deletes all existing author_roles rows for the profile slug and re-inserts the current roles (if any) inside the existing db.transaction block, following the same delete-then-insert pattern sync-post uses for postTags. Authors aren't locale-scoped like posts, so there's no multi-locale aggregation concern here.
  • profiles.meta no longer includes roles — it now only contains socials. GET /content/profiles doesn't read meta.roles anywhere in this repo today, and since sync-author does a full onConflictDoUpdate on the whole meta object every sync, old meta.roles data on existing rows will self-heal away automatically the next time each profile is synced. No backfill needed.
  • Migration generated via drizzle-kit generate.

Open question: table placement

I placed author_roles in profiles.ts (alongside the existing profileAchievements table) rather than in tags.ts, since it's an owner-scoped role list similar to profileAchievements. That said, it's structurally identical to the post_tags/collection_tags (ownerSlug, value) composite-PK pattern in tags.ts — so James may prefer it live there instead for consistency. Left a comment in the code noting this; happy to move it if that's the preferred convention.

Heads up for James

meta.roles is no longer populated going forward. Nothing in this repo's API currently reads it, so this isn't blocking — but if the frontend repo reads raw profile meta directly anywhere, it would need to switch to the new author_roles table.

Test plan

  • Extended apps/worker/src/tasks/sync-author/processor.test.ts:
    • roles written to author_roles on sync
    • existing author's roles fully replaced on a subsequent sync with a different role set
    • empty roles array results in no rows inserted
  • pnpm test:unit passes (lint, knip, publint, sherif, vitest across all NX projects)

Paste that into the compare URL, double-check for truncation, then create the PR yourself.

Summary by CodeRabbit

  • New Features

    • Author profiles now support a separate list of roles, which is kept up to date during sync.
    • Profile images are now resized and stored automatically in a consistent JPEG format.
  • Bug Fixes

    • Improved role syncing so old roles are removed before new ones are saved.
    • Empty role lists are handled cleanly without creating extra entries.

Tracks author roles in a real table (mirroring playfulprogramming#81's tags pattern) instead
of embedding them in profiles.meta, so /content/profiles can eventually
filter by role.
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@bbornino, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c264281a-97e4-4367-a01e-557215baf0bc

📥 Commits

Reviewing files that changed from the base of the PR and between c50c4d4 and cd44752.

📒 Files selected for processing (2)
  • apps/worker/src/tasks/sync-author/processor.ts
  • packages/db/src/schema/profiles.ts
📝 Walkthrough

Walkthrough

Adds a new author_roles table (schema, migration, snapshot) linked to profiles via cascading foreign key. Updates the sync-author worker processor to persist author roles separately from profile meta via delete-then-insert, and adds profile image resizing/upload logic. Tests and mocks are updated accordingly.

Changes

Author Roles Persistence

Layer / File(s) Summary
authorRoles schema and migration
packages/db/src/schema/profiles.ts, packages/db/drizzle/20260709150348_shocking_expediter/migration.sql, packages/db/drizzle/20260709150348_shocking_expediter/snapshot.json
Adds authorRoles Drizzle table with composite primary key on profileSlug/role, cascading FK to profiles.slug, matching SQL migration, and schema snapshot.
Processor role sync and image resizing
apps/worker/src/tasks/sync-author/processor.ts
Adds processProfileImg helper to resize and upload profile images to S3, removes roles from profile meta, and syncs authorRoles by deleting existing rows then inserting current roles when non-empty.
Test mocks and processor tests
apps/worker/src/tasks/sync-author/processor.test.ts, apps/worker/test-utils/setup.ts
Extends mocked schema with authorRoles, reworks insert/delete mocking per table, updates fixture frontmatter with roles, and adds tests for role deletion/insertion, replacement across syncs, and empty-roles handling.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Processor
  participant DB
  participant S3

  Processor->>S3: processProfileImg (resize + upload profiles/authorId.jpeg)
  Processor->>DB: upsert profiles (meta without roles)
  Processor->>DB: delete authorRoles where profileSlug matches
  Processor->>DB: insert authorRoles (if roles list non-empty)
Loading

Possibly related PRs

  • playfulprogramming/hoof#169: Modifies the same apps/worker/src/tasks/sync-author/processor.ts for profileAchievements handling immediately preceding the author roles logic added here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding an author_roles table and syncing it from author frontmatter.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/worker/src/tasks/sync-author/processor.ts (1)

20-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Forward source stream errors to the sharp pipeline.

Readable.fromWeb(stream).pipe(pipeline) does not propagate source errors to the sharp destination. If the GitHub content stream errors mid-transfer, the sharp pipeline will hang waiting for data, and s3.upload will block indefinitely — potentially stalling the worker job until a timeout fires.

Attach an error listener to forward failures:

♻️ Proposed fix
 async function processProfileImg(
 	stream: ReadableStream<Uint8Array>,
 	uploadKey: string,
 ) {
 	const pipeline = sharp()
 		.resize({
 			width: PROFILE_IMAGE_SIZE_MAX,
 			height: PROFILE_IMAGE_SIZE_MAX,
 			fit: "inside",
 		})
 		.jpeg({ mozjpeg: true });
 
-	Readable.fromWeb(stream as never).pipe(pipeline);
+	const source = Readable.fromWeb(stream as never);
+	source.on("error", (err) => pipeline.destroy(err));
+	source.pipe(pipeline);
 
 	const bucket = await s3.ensureBucket(env.S3_BUCKET);
 	await s3.upload(bucket, uploadKey, undefined, pipeline, "image/jpeg");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/worker/src/tasks/sync-author/processor.ts` around lines 20 - 38, The
source stream errors are not being forwarded into the sharp pipeline in
processProfileImg, so a mid-transfer failure can leave the upload hanging.
Update the Readable.fromWeb(stream) handling to attach an error listener that
destroys or rejects the sharp pipeline when the source ReadableStream fails, and
make sure the async flow around s3.upload cannot wait indefinitely after a
stream error. Use processProfileImg and the Readable.fromWeb(...).pipe(pipeline)
section as the fix point.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/worker/src/tasks/sync-author/processor.ts`:
- Around line 20-38: The source stream errors are not being forwarded into the
sharp pipeline in processProfileImg, so a mid-transfer failure can leave the
upload hanging. Update the Readable.fromWeb(stream) handling to attach an error
listener that destroys or rejects the sharp pipeline when the source
ReadableStream fails, and make sure the async flow around s3.upload cannot wait
indefinitely after a stream error. Use processProfileImg and the
Readable.fromWeb(...).pipe(pipeline) section as the fix point.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e56d0b2-7631-4d20-b6d5-c2578ca07c95

📥 Commits

Reviewing files that changed from the base of the PR and between 086e182 and c50c4d4.

📒 Files selected for processing (6)
  • apps/worker/src/tasks/sync-author/processor.test.ts
  • apps/worker/src/tasks/sync-author/processor.ts
  • apps/worker/test-utils/setup.ts
  • packages/db/drizzle/20260709150348_shocking_expediter/migration.sql
  • packages/db/drizzle/20260709150348_shocking_expediter/snapshot.json
  • packages/db/src/schema/profiles.ts

…essProfileImg

Readable.fromWeb(stream).pipe(pipeline) doesn't forward source errors to
the destination — a mid-transfer failure from GitHub's content stream left
the pipeline hanging and s3.upload blocking indefinitely instead of
failing fast. Per CodeRabbit feedback on PR playfulprogramming#186.
Comment on lines +35 to +39

// Placed here for now since it's an owner-scoped role list like profileAchievements,
// but it's structurally identical to the post_tags/collection_tags (ownerSlug, value)
// composite-PK pattern in tags.ts, so this may belong there instead — open question for James.
export const authorRoles = pgTable(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// Placed here for now since it's an owner-scoped role list like profileAchievements,
// but it's structurally identical to the post_tags/collection_tags (ownerSlug, value)
// composite-PK pattern in tags.ts, so this may belong there instead — open question for James.
export const authorRoles = pgTable(
export const authorRoles = pgTable(

LGTM, just want to remove this comment.

TBH I'd rather have the tables in tags.ts moved into the post/collection files rather than their own file, since there's no central "tags" table.

James's review closed the open question about whether author_roles
belongs with the post_tags/collection_tags pattern in tags.ts instead
- it's staying in profiles.ts, so the comment flagging that as unresolved
no longer applies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@fennifith fennifith added this pull request to the merge queue Jul 10, 2026
Merged via the queue into playfulprogramming:main with commit 48c152f Jul 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create an author_roles table and populate it from sync-author

2 participants