Skip to content

feat(quote): quote wall data layer for campaigns#4842

Merged
mashbean merged 7 commits into
thematters:developfrom
yingshinlee:feat/quote-wall
Jun 13, 2026
Merged

feat(quote): quote wall data layer for campaigns#4842
mashbean merged 7 commits into
thematters:developfrom
yingshinlee:feat/quote-wall

Conversation

@yingshinlee

Copy link
Copy Markdown
Collaborator

這是什麼

金句牆(Memo Wall)的資料層與站內 API:讀者在文章頁選一句金句「上牆」,金句存進資料庫,活動頁的金句牆與(未來)博物館從這裡讀取。階段 3,承接金句產生器(前端 #5965)。

前端對應分支:yingshinlee/matters-webfeat/quote-wall(依賴本 PR 的 schema)。

改動

檔案 內容
db/migrations/*_create_quote_table.js(新) quote 表(content / article_id / campaign_id / user_id / state)+ entity_type;軟刪除狀態 active / archived / banned
src/common/enums/quote.ts(新) QUOTE_STATEMAX_QUOTE_LENGTH = 80QUOTE_DAILY_LIMIT = 5QUOTE_PER_ARTICLE_LIMIT = 2
src/definitions/quote.d.ts(新)+ index / atomService 註冊 Quote 型別、表登記
src/types/quote.ts(新) putQuote / deleteQuote mutation;WritingChallenge.quotes(隨機抽樣)/ quoteCountQuote type
src/mutations/quote/putQuote.ts(新) 上牆,完整規則(見下)
src/mutations/quote/deleteQuote.ts(新) 收回(軟刪除)
src/queries/campaign/quotes.tsquoteCount.ts(新) 活動牆查詢,random: trueorder by random()
src/queries/quote/index.ts(新) Quote 欄位 resolver(article / poster 走 dataloader)

putQuote 的規則(防濫用+版權)

  • 必須是文章的節錄(去除空白後做 substring 比對)→ 不能自由打字,這是最強的防濫用機制。
  • 80 字上限版權 gate:文章授權為 ARR(保留所有權利)時僅作者本人可上牆,CC 系列放行。
  • 活動範圍:文章必須屬於某 campaign(查 campaign_article)才能上牆,自動歸到該活動的牆。
  • 額度(產品定案、可調):每人每日 5 則、同篇每人 2 則、同人同句去重。
  • 收回不退當日額度(計「當日上牆動作數」,防「貼 5 收 5 再貼 5」)。

deleteQuote(收回)

允許:貼的人原文作者(句子的著作權在作者)、或 admin。軟刪除(state → archived,紀錄保留),呼應「不刪除,只是不再被看見」。

⚠️ Merge 前

需 node ≥ 22 跑 codegen / typecheck(schema 有新型別)。Draft。

部署

Merge 進 develop 後先上測試站 https://matters.icu/ 驗收(建議 QA:上牆規則四種錯誤路徑、隨機抽樣、收回三種角色),再上正式站。

🤖 Generated with Claude Code

yingshinlee and others added 4 commits June 11, 2026 22:14
…ssion board

- new COMMENT_TYPE campaignDiscussion bound to campaign via targetId/targetTypeId
- putComment: accept campaignId; only succeeded participants (campaign_user.state)
  or campaign organizers/managers may comment; cap content at 240 chars
- WritingChallenge.discussion / discussionCount: public read resolvers
- Comment.node resolves campaign comments to WritingChallenge
- campaignService.isParticipant helper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new quote table (content, article_id, campaign_id, user_id, state) +
  entity_type row; soft-delete states active/archived/banned
- putQuote mutation with full rule set:
  * quote must be an excerpt of the article (primary anti-abuse: no free
    typing; whitespace-normalized substring check against article content)
  * 80-char cap; license gate (ARR -> author only)
  * campaign-scoped: only campaign articles can be quoted onto the wall
  * caps: 5/day per user, 2 per article per user, exact-duplicate rejection
- deleteQuote (retraction): poster, source article author, or admin;
  soft delete, daily quota not refunded
- WritingChallenge.quotes (random sampling for shuffle) + quoteCount
- Quote type resolvers (article / poster via dataloaders)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- codegen: add Quote mapper so resolver parent is the DB model
- regenerate schema.graphql / schema.d.ts (quote, campaignDiscussion)
- putComment: guard article/moment notification blocks with targetAuthor
- lint:fix import order in resolver index files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 27.07182% with 132 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.01%. Comparing base (85bdbe1) to head (a118ea1).
⚠️ Report is 19 commits behind head on develop.

Files with missing lines Patch % Lines
src/mutations/quote/putQuote.ts 4.08% 36 Missing and 11 partials ⚠️
src/queries/comment/campaign/discussion.ts 3.70% 19 Missing and 7 partials ⚠️
src/mutations/comment/putComment.ts 34.37% 20 Missing and 1 partial ⚠️
src/mutations/quote/deleteQuote.ts 6.25% 13 Missing and 2 partials ⚠️
src/queries/campaign/quotes.ts 20.00% 8 Missing ⚠️
src/queries/quote/index.ts 20.00% 4 Missing ⚠️
src/queries/campaign/quoteCount.ts 25.00% 3 Missing ⚠️
src/queries/comment/campaign/discussionCount.ts 25.00% 3 Missing ⚠️
src/queries/comment/node.ts 0.00% 3 Missing ⚠️
src/connectors/campaignService.ts 33.33% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #4842      +/-   ##
===========================================
- Coverage    72.82%   72.01%   -0.82%     
===========================================
  Files         1054     1064      +10     
  Lines        21263    21089     -174     
  Branches      4671     4588      -83     
===========================================
- Hits         15485    15187     -298     
+ Misses        5706     5407     -299     
- Partials        72      495     +423     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

mashbean and others added 3 commits June 13, 2026 09:55
…nd id validation

- add partial unique index on quote (user_id, article_id, content) for
  active quotes as a concurrency backstop, and surface PG unique
  violation as UserInputError in putQuote
- order campaign_article lookup by created_at desc so attribution is
  deterministic when an article belongs to multiple campaigns
- validate fromGlobalId type in putQuote and deleteQuote

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uote

- add `after` cursor to QuotesInput and wire the quotes resolver to the
  shared offset pagination util; `after` is ignored under `random`
- make deleteQuote idempotent: an already-retracted quote returns success,
  with the permission check kept ahead of the idempotent path so it cannot
  be used to probe quote existence

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cover the quote-wall feature's highest-regression-risk paths: dedupe,
per-article and daily caps, deleteQuote permission matrix (without
leaking existence of archived quotes), idempotent retraction, and the
campaign quotes query (active-only filtering, after pagination, random).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants