feat(worker): add aggregate ratings API on D1 (test bucket) - #423
Closed
richardthe3rd wants to merge 6 commits into
Closed
feat(worker): add aggregate ratings API on D1 (test bucket)#423richardthe3rd wants to merge 6 commits into
richardthe3rd wants to merge 6 commits into
Conversation
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.
Contributor
There was a problem hiding this comment.
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
/v1resource-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
This was referenced Jun 12, 2026
Owner
Author
|
Closing in favour of two focused PRs:
Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First step towards an online "my festival". Clients submit a drink rating
and get back the shared aggregate (count + average + their own rating).
POST/DELETE upsert/remove a device's rating, GET single + batch aggregates.
re-rating never inflates counts. Anonymous device_id now; user_id column
reserved for the sign-in upgrade.
bucketso test traffic stays isolated fromproduction data; bucket derived from origin, overridable via RATINGS_BUCKET.
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.