Skip to content

feat(worker): add aggregate ratings API on D1 (test bucket) - #423

Closed
richardthe3rd wants to merge 6 commits into
mainfrom
claude/festival-rating-recommend-o6y71l
Closed

feat(worker): add aggregate ratings API on D1 (test bucket)#423
richardthe3rd wants to merge 6 commits into
mainfrom
claude/festival-rating-recommend-o6y71l

Conversation

@richardthe3rd

Copy link
Copy Markdown
Owner

First step towards an online "my festival". Clients submit a drink rating
and get back the shared aggregate (count + average + their own rating).

  • New /v1/ratings endpoints on the existing proxy worker:
    POST/DELETE upsert/remove a device's rating, GET single + batch aggregates.
  • D1-backed storage with upsert semantics (one row per device/drink) so
    re-rating never inflates counts. Anonymous device_id now; user_id column
    reserved for the sign-in upgrade.
  • Every row/query scoped by a bucket so test traffic stays isolated from
    production data; bucket derived from origin, overridable via RATINGS_BUCKET.
  • CORS extended to POST/DELETE.
  • Full vitest coverage against a simulated local D1 (no real database needed):
    pure-helper unit tests plus integration tests for upsert, aggregation,
    validation, deletion and bucket isolation. 78 worker tests pass.

The wrangler.toml database_id is a placeholder; local dev and tests use a
simulated D1. README documents the endpoints and the one-time
wrangler d1 create / migrations-apply provisioning before first deploy.

claude added 5 commits June 12, 2026 20:29
First step towards an online "my festival". Clients submit a drink rating
and get back the shared aggregate (count + average + their own rating).

- New /v1/ratings endpoints on the existing proxy worker:
  POST/DELETE upsert/remove a device's rating, GET single + batch aggregates.
- D1-backed storage with upsert semantics (one row per device/drink) so
  re-rating never inflates counts. Anonymous device_id now; user_id column
  reserved for the sign-in upgrade.
- Every row/query scoped by a `bucket` so test traffic stays isolated from
  production data; bucket derived from origin, overridable via RATINGS_BUCKET.
- CORS extended to POST/DELETE.
- Full vitest coverage against a simulated local D1 (no real database needed):
  pure-helper unit tests plus integration tests for upsert, aggregation,
  validation, deletion and bucket isolation. 78 worker tests pass.

The wrangler.toml database_id is a placeholder; local dev and tests use a
simulated D1. README documents the endpoints and the one-time
`wrangler d1 create` / migrations-apply provisioning before first deploy.
A yes/no "would recommend" signal, separate from the star rating, so each
drink can surface a "% would recommend".

- New /v1/recommendations endpoints mirroring ratings (POST/DELETE upsert,
  GET single + batch). Aggregate reports total responses, "yes" count and
  the recommend percentage, plus the caller's own answer.
- Stored in a new `recommendations` table in the same D1 database, with the
  same per-device upsert and bucket-isolation model.
- Extracted shared bucket/id-validation/JSON/REST-routing plumbing into
  shared.js so ratings and recommendations stay thin; ratings refactored to
  consume it (behaviour unchanged).
- 19 new tests (pure helpers + integration for upsert, aggregation,
  validation, deletion, bucket isolation). 97 worker tests pass.

README documents the new endpoints; migration 0002 adds the table.
Define the online "my festival" API as Protocol Buffers following Google's
AIPs, as the source of truth for the ratings/recommendations endpoints. An
OpenAPI v3 doc is generated from it via a buf BSR remote plugin.

- proto/cambeerfestival/myfestival/v1: Rating/RatingSummary,
  Recommendation/RecommendationSummary resources and MyFestivalService with
  google.api.http annotations.
- Resource-oriented design: nested resource names, Update+allow_missing
  upsert (AIP-134), bodyless Delete (AIP-135), paginated List of summaries
  (AIP-158), field_behavior + resource annotations.
- buf.yaml (googleapis dep, AIP-aware lint) and buf.gen.yaml (gnostic OpenAPI
  remote plugin); buf added to the dev mise env with proto:lint / format /
  dep-update / generate tasks. See proto/README.md.

Contract only — buf lint/build and OpenAPI generation, plus reworking the
worker to conform, are pending network access to buf.build.
Rework the /v1 API to conform to the proto contract and Google's AIPs.

BREAKING CHANGE: replaces the flat POST/DELETE /v1/ratings endpoints with
resource-oriented routes. Nothing consumes them yet (no client, placeholder
DB), so this is a safe pre-launch change.

- Resource names: PATCH/GET/DELETE on
  /v1/festivals/{f}/drinks/{d}/ratings/{device} (and .../recommendations/...).
- Upsert via PATCH with allow_missing semantics (AIP-134); bodyless DELETE
  that is NOT_FOUND when absent (AIP-135).
- Read aggregates as RatingSummary / RecommendationSummary resources:
  GET .../{f}/ratingSummaries/{d} and a paginated list
  GET .../{f}/ratingSummaries (page_size/page_token/next_page_token +
  total_size, keyset cursor) (AIP-158).
- Structured google.rpc.Status errors with ErrorInfo reason+domain (AIP-193).
- RFC3339 update_time; camelCase resource fields matching the proto JSON
  mapping; dropped redundant your_* (client is local-first and knows its own).
- Generic family engine in shared.js drives both resources; CORS now allows
  GET/PATCH/DELETE; unknown /v1 routes 404 instead of proxying upstream.
- pin buf 1.70.0 in the dev mise env (lockfile). 87 worker tests pass.
#422 added buf to the base mise.toml tools (pinned 1.70.0 in mise.lock), so
drop the redundant buf declaration from the dev env after rebasing on main.
The proto:* tasks remain in the dev env and use the base buf binary.
Copilot AI review requested due to automatic review settings June 12, 2026 20:30

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

This PR introduces a proto-first contract for the upcoming “my festival” API and implements initial Cloudflare Worker /v1 endpoints for per-device drink ratings and recommendations backed by D1, including pagination, CORS updates, migrations, and vitest coverage using a simulated D1.

Changes:

  • Added protobuf API contract (AIP-style resource model) plus buf tooling/tasks to lint/format/generate OpenAPI.
  • Implemented /v1 resource-family routing in the Cloudflare Worker for ratings + recommendations with D1 upsert/delete and aggregate summary reads (single + paginated list).
  • Added D1 schema migrations and comprehensive vitest coverage with migrations applied to a simulated per-test D1 instance.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
proto/README.md Documents the proto-first contract and buf workflows for generating OpenAPI.
proto/cambeerfestival/myfestival/v1/rating.proto Defines Rating and RatingSummary resources.
proto/cambeerfestival/myfestival/v1/recommendation.proto Defines Recommendation and RecommendationSummary resources.
proto/cambeerfestival/myfestival/v1/my_festival_service.proto Defines service RPCs and HTTP annotations for /v1 routes.
proto/buf.yaml Adds buf module, lint, and breaking-change config.
proto/buf.gen.yaml Configures OpenAPI generation via a remote buf plugin.
mise.dev.toml Adds mise tasks for proto lint/format/dep-update/generate.
cloudflare-worker/wrangler.toml Adds D1 binding configuration for ratings storage (placeholder DB id).
cloudflare-worker/worker.js Routes /v1 before proxying upstream; expands CORS preflight methods.
cloudflare-worker/shared.js Implements shared routing, validation, pagination, and structured error responses for resource families.
cloudflare-worker/ratings.js Implements ratings “family” config (validation + summary shaping).
cloudflare-worker/recommendations.js Implements recommendations “family” config (validation + summary shaping).
cloudflare-worker/migrations/0001_create_ratings_table.sql Creates the ratings table and aggregate index.
cloudflare-worker/migrations/0002_create_recommendations_table.sql Creates the recommendations table and aggregate index.
cloudflare-worker/vitest.config.js Loads migrations at config-time and exposes them as a test binding.
cloudflare-worker/test/apply-migrations.js Applies migrations to the simulated D1 before tests run.
cloudflare-worker/test/ratings.test.js Adds helper and integration tests for ratings CRUD, summaries, pagination, and bucket isolation.
cloudflare-worker/test/recommendations.test.js Adds helper and integration tests for recommendations CRUD, summaries, and bucket isolation.
cloudflare-worker/test/cors.test.js Updates expectations for expanded CORS allow-methods.
cloudflare-worker/README.md Documents the new /v1 API, bucket behavior, and one-time D1 provisioning steps.

Comment on lines +26 to +31
export function resolveBucket(origin, env) {
if (env && typeof env.RATINGS_BUCKET === "string" && env.RATINGS_BUCKET) {
return env.RATINGS_BUCKET;
}
return isProductionOrigin(origin) ? "prod" : "test";
}
Comment on lines +111 to +118
function parseV1Path(pathname) {
if (pathname !== "/v1" && !pathname.startsWith("/v1/")) return null;
return pathname
.slice("/v1/".length)
.split("/")
.filter((s) => s.length > 0)
.map((s) => decodeURIComponent(s));
}
Comment on lines +210 to +219
if (segments.length === 4) {
return getSummary({
db,
bucket,
family,
festivalId,
drinkId: segments[3],
corsHeaders,
});
}
Comment on lines +256 to +262
return errorResponse(
404,
"NOT_FOUND",
"No such rating",
"NOT_FOUND",
corsHeaders,
);
Comment on lines +327 to +333
return errorResponse(
404,
"NOT_FOUND",
"No such rating",
"NOT_FOUND",
ctx.corsHeaders,
);
Comment on lines +71 to +75
// "My festival" API (/v1/...). Handled before the proxy fall-through so
// these paths are never forwarded upstream.
const ratingsResponse = await handleRatings(
request,
url,
Comment on lines +132 to +133
// If true (the default for this API), create the rating when absent (upsert).
bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL];
Comment on lines +200 to +201
// If true (the default for this API), create the answer when absent (upsert).
bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL];
Run buf dep update to pin googleapis BSR dependency (buf.lock was
missing from the branch), then buf format -w to normalise whitespace —
collapsing multi-line option/field blocks onto single lines per buf's
default style. buf lint now passes cleanly.

https://claude.ai/code/session_01WX7GbU19M9fh3tAfAxzeET

Copy link
Copy Markdown
Owner Author

Closing in favour of two focused PRs:


Generated by Claude Code

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.

3 participants