Skip to content

Staging -> Main#247

Merged
cirex-web merged 24 commits intomainfrom
staging
Feb 21, 2026
Merged

Staging -> Main#247
cirex-web merged 24 commits intomainfrom
staging

Conversation

@cirex-web
Copy link
Collaborator

No description provided.

Copilot AI review requested due to automatic review settings February 21, 2026 23:25
@cirex-web cirex-web merged commit 0db17c2 into main Feb 21, 2026
8 checks passed
@github-actions
Copy link

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 82.86% 474 / 572
🔵 Statements 82.05% 494 / 602
🔵 Functions 80.37% 127 / 158
🔵 Branches 72.8% 257 / 353
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/env.ts 80% 100% 50% 80% 21
src/db/dbQueryUtils.ts 82.92% 93.75% 77.77% 84.61% 149-152, 169-172, 216-227
src/db/getLocations.ts 100% 100% 100% 100%
src/db/schema.ts 62.16% 100% 33.33% 62.16% 32, 56, 72, 90, 103, 120, 153, 177-183, 208-211, 238-246
Generated in workflow #467 for commit 35a9f85 by the Vitest Coverage Report Action

return {};
async ({ cookie, body: { locationId, message } }) => {
const session = cookie["session_id"]!.value as string | undefined;
const userDetails = await fetchUserDetails(session);
Copy link

Choose a reason for hiding this comment

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

Bug: The /report endpoint schema was updated to remove locationName, but Elysia.js will reject requests from older clients that still send this extra field, causing a validation error.
Severity: HIGH

Suggested Fix

To maintain backward compatibility, configure the Elysia.js route to allow additional properties in the request body. This can be done by setting additionalProperties: true in the t.Object schema for the endpoint, which will instruct the validator to ignore extra fields like locationName.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/endpoints/misc.ts#L65

Potential issue: The change to the `/report` endpoint removes the `locationName`
parameter from the request body schema. However, the Elysia.js framework, by default,
strictly validates incoming requests and rejects any that contain fields not defined in
the schema. Since older clients will continue to send the `locationName` field, their
requests will fail with a 422 Unprocessable Entity validation error. This breaks
backward compatibility for the endpoint, preventing older clients from successfully
submitting reports.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Contributor

Copilot AI left a comment

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 merges staging changes into main, primarily adding persisted “reports” for locations and exposing rating aggregates (ratingsAvg/ratingsCount) in the locations API/schema.

Changes:

  • Add ratingsAvg + ratingsCount to location responses and update DB/query utilities + tests accordingly.
  • Add a new reports table/migration and update /report to persist reports (plus add a read endpoint for reports).
  • Update API schemas and README local setup instructions.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/reviews.test.ts Adds a test for the new ratings average/count aggregation helper.
tests/database.test.ts Updates expected location output to include ratingsAvg/ratingsCount.
src/env.ts Changes env validation for session cookie signing secret (now defaulted).
src/endpoints/schemas.ts Extends LocationSchema with ratingsAvg/ratingsCount.
src/endpoints/reviews.ts Adds GET /v2/locations/:locationId/reports to return stored reports.
src/endpoints/misc.ts Updates POST /report to look up location name, email/slack, and insert into reports.
src/db/schema.ts Adds reportsTable definition.
src/db/getLocations.ts Plumbs ratings aggregates into location objects returned by getAllLocationsFromDB.
src/db/dbQueryUtils.ts Adds getRatingsAvgsAndCounts() query helper.
drizzle/meta/_journal.json Registers the new migration in Drizzle’s journal.
drizzle/meta/0010_snapshot.json Drizzle snapshot for the new migration state.
drizzle/0010_shallow_doctor_strange.sql Migration creating the reports table + FKs.
README.md Improves local DB startup instructions formatting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +251 to +252
}
)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

reports will likely be queried by location_id (and possibly by time). Adding an index on locationId (and optionally createdAt) would prevent full table scans as reports grow.

Suggested change
}
)
},
(t) => [
index("reports_location_created_idx").on(t.locationId, t.createdAt),
],
);

Copilot uses AI. Check for mistakes.
Comment on lines +194 to +195
// since all ratings are from 0 to 5, the whole part of the average cannot
// exceeds one. also we make the decimal digits longer for other consumers
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The comment describing the avg rating cast is incorrect/unclear ("whole part ... cannot exceeds one"; the average can be up to 5) and has a grammar error. Update the comment to reflect the actual bounds and intent of the decimal(4,3) cast.

Suggested change
// since all ratings are from 0 to 5, the whole part of the average cannot
// exceeds one. also we make the decimal digits longer for other consumers
// Ratings are between 0 and 5, so their average is also in [0, 5] and fits
// in decimal(4,3) (one digit before the decimal, three after). We keep
// three decimal places for higher precision for downstream consumers.

Copilot uses AI. Check for mistakes.
.default(false),
ENV: z.enum(["dev", "staging", "prod"]),
SESSION_COOKIE_SIGNING_SECRET: z.string(),
SESSION_COOKIE_SIGNING_SECRET: z.string().default("lemon melon cookie"),
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

SESSION_COOKIE_SIGNING_SECRET is now given a hard-coded default. This weakens session signing if the env var is missing/misconfigured (especially in prod/staging), making cookies forgeable across deployments. Remove the default and require a secret to be explicitly set (or gate a test/dev-only default behind IN_TEST_MODE/ENV !== "prod" and validate minimum length/entropy).

Suggested change
SESSION_COOKIE_SIGNING_SECRET: z.string().default("lemon melon cookie"),
SESSION_COOKIE_SIGNING_SECRET: z
.string()
.min(32, "SESSION_COOKIE_SIGNING_SECRET must be at least 32 characters long"),

Copilot uses AI. Check for mistakes.
);
return {};
async ({ cookie, body: { locationId, message } }) => {
const session = cookie["session_id"]!.value as string | undefined;
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

cookie["session_id"]! will throw when the request has no session_id cookie, causing /report to 500 for anonymous users. Use optional access for the cookie and let fetchUserDetails handle undefined, or return a 401 only if you intend to require authentication for reporting.

Suggested change
const session = cookie["session_id"]!.value as string | undefined;
const session = cookie["session_id"]?.value as string | undefined;

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +82
const reports = await db.select().from(locationDataTable).where(eq(locationDataTable.id, locationId))
if (reports.length == 0) {
throw new Response(`Invalid location id ${locationId}`, {
status: 400,
});
}

if (reports.length > 1) {
throw new Response(`
Expected 1 restaurant corresponding to id=${locationId}. Somehow got 2.
`, { status: 500 }) // this should be unreachable
}

const locationName = reports[0]?.name ?? "Unnamed"
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The variable reports is actually the location lookup result, and the reports.length > 1 branch should be unreachable because location_data.id is a primary key. Consider renaming the variable (e.g., locations/locationRows) and simplifying the lookup to a single-row query (limit(1)/findFirst) with a single not-found check.

Suggested change
const reports = await db.select().from(locationDataTable).where(eq(locationDataTable.id, locationId))
if (reports.length == 0) {
throw new Response(`Invalid location id ${locationId}`, {
status: 400,
});
}
if (reports.length > 1) {
throw new Response(`
Expected 1 restaurant corresponding to id=${locationId}. Somehow got 2.
`, { status: 500 }) // this should be unreachable
}
const locationName = reports[0]?.name ?? "Unnamed"
const [location] = await db
.select()
.from(locationDataTable)
.where(eq(locationDataTable.id, locationId))
.limit(1);
if (!location) {
throw new Response(`Invalid location id ${locationId}`, {
status: 400,
});
}
const locationName = location.name ?? "Unnamed";

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +149
.get(
"/v2/locations/:locationId/reports",
async ({ params: { locationId } }) => {
const reports = await db.select().from(reportsTable).where(eq(reportsTable.locationId, locationId))

return reports
},
{
response: t.Array(
t.Object({
id: t.Number(),
userId: t.Nullable(t.Number()),
createdAt: t.Date(),
locationId: t.String(),
message: t.String()
})
)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

GET /v2/locations/:locationId/reports exposes user-submitted report messages (and userId) without any authentication/authorization and is included in the public schema. If these reports are meant for internal moderation/admin use, add an auth check (and ideally an admin/role check) and/or hide the endpoint from OpenAPI to avoid leaking potentially sensitive data.

Copilot uses AI. Check for mistakes.
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.

4 participants