Conversation
📝 WalkthroughWalkthroughAdds a complete "retail-store" Next.js + Prisma‑Next MongoDB example (app, API routes, UI, data layer, migrations, seed/reset scripts, and extensive tests) and introduces codec-aware value wrapping/resolution plus nullable value‑object JSON Schema handling in core Mongo packages. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Browser
participant Next as Next.js Server
participant Handler as Route/Page Handler
participant DB as MongoDB
Browser->>Next: HTTP request (page or /api/*)
Next->>Handler: invoke server component or route (auth, getDb)
Handler->>DB: ORM or raw pipeline call (find/create/update/aggregate)
DB-->>Handler: results
Handler-->>Next: render props or JSON response
Next-->>Browser: HTML or JSON
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/emitter
@prisma-next/migration-tools
@prisma-next/vite-plugin-contract-emit
@prisma-next/runtime-executor
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-pipeline-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
c6c3885 to
b13db34
Compare
…artifacts Extract consolidated framework limitations from code reviews into a standalone document at projects/mongo-example-apps/framework-limitations.md. Covers type ergonomics (FL-01–FL-03), query capabilities (FL-04–FL-08), schema/migration gaps (FL-09–FL-12), and missing capabilities (FL-13–FL-15). Tracks 6 items addressed by this branch (FL-A1–FL-A6). Also commits the PR #327 review artifacts (system-design-review, code-review, walkthrough), the round 2 spec, and the round 2 plan.
f6820e2 to
9ac7ac6
Compare
There was a problem hiding this comment.
Actionable comments posted: 20
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (12)
examples/retail-store/src/components/navbar-client.tsx-9-13 (1)
9-13:⚠️ Potential issue | 🟡 MinorGuard navigation on logout API failure.
If
/api/auth/logoutfails, the code still redirects to/login, which can leave the session active and create inconsistent UX.💡 Suggested fix
async function handleLogout() { - await fetch('/api/auth/logout', { method: 'POST' }); + const res = await fetch('/api/auth/logout', { method: 'POST' }); + if (!res.ok) return; router.push('/login'); router.refresh(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/src/components/navbar-client.tsx` around lines 9 - 13, The handleLogout function should await the POST to '/api/auth/logout' and only navigate when that request succeeds; update handleLogout to check the fetch response (e.g., response.ok) and handle non-OK or network errors by logging or showing an error instead of calling router.push('/login') and router.refresh(); keep the fetch to '/api/auth/logout' and the router calls (router.push, router.refresh) but gate them behind a successful response and handle failures (retry, toast, or console.error) to avoid redirecting when logout fails.examples/retail-store/app/api/auth/signup/route.ts-16-21 (1)
16-21:⚠️ Potential issue | 🟡 MinorAdd
secureflag to auth cookie for HTTPS enforcement.Auth cookies should include the
secureflag to prevent transmission over non-HTTPS connections. Even though this is an example app, including it demonstrates security best practices and prevents the cookie from being sent over plaintext HTTP in production deployments.Suggested change
cookieStore.set('userId', String(user._id), { path: '/', httpOnly: true, sameSite: 'lax', + secure: process.env['NODE_ENV'] === 'production', maxAge: 60 * 60 * 24 * 30, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/api/auth/signup/route.ts` around lines 16 - 21, The auth cookie set in cookieStore.set(...) (in route.ts) is missing the secure flag; update the options passed to cookieStore.set (the call that sets 'userId') to include secure: true so the cookie is only sent over HTTPS (optionally you can conditionally set secure based on NODE_ENV for local dev, but ensure production uses secure: true).examples/retail-store/app/checkout/page.tsx-55-55 (1)
55-55:⚠️ Potential issue | 🟡 MinorAvoid using array index in cart row keys.
Line 55 includes
iin the key, which can destabilize element identity during list mutations. Use a stable, unique identifier instead—ifproductIdalone is insufficient due to duplicate items, consider a compound key like${item.cartItemId}-${item.productId}or rely on a unique cart item ID.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/checkout/page.tsx` at line 55, The key for each cart row currently uses the array index (`i`) combined with `item.productId` which can break element identity on list mutations; update the key in the cart row render (the div using `key={`${item.productId}-${i}`}` in page.tsx) to use a stable unique identifier such as `item.cartItemId` or a compound of `item.cartItemId` and `item.productId` (e.g. `${item.cartItemId}-${item.productId}`) so each row has a deterministic, unique key instead of relying on the loop index.examples/retail-store/test/aggregation.test.ts-42-47 (1)
42-47:⚠️ Potential issue | 🟡 MinorAvoid index-based assertions on aggregation output order.
Lines 44-46 assume deterministic row order. Unless the pipeline explicitly sorts, this can be flaky. Prefer asserting by keyed counts.
Suggested fix
const result = await aggregateEventsByType(ctx.db, 'test-user'); expect(result).toHaveLength(3); - expect(result[0]).toMatchObject({ _id: 'view-product', count: 3 }); - expect(result[1]).toMatchObject({ _id: 'add-to-cart', count: 2 }); - expect(result[2]).toMatchObject({ _id: 'search', count: 1 }); + const counts = Object.fromEntries(result.map((row) => [row._id, row.count])); + expect(counts).toMatchObject({ + 'view-product': 3, + 'add-to-cart': 2, + search: 1, + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/test/aggregation.test.ts` around lines 42 - 47, The test for aggregateEventsByType is asserting on specific array indices which is flaky because Mongo aggregation results are unordered unless explicitly sorted; update the test to check counts by key instead of index — for example, transform the result from aggregateEventsByType(ctx.db, 'test-user') into a map keyed by _id and assert resultMap['view-product'] === 3, resultMap['add-to-cart'] === 2, and resultMap['search'] === 1 (or use expect.arrayContaining with the expected objects) so the assertions no longer depend on row order.examples/retail-store/app/cart/page.tsx-37-39 (1)
37-39:⚠️ Potential issue | 🟡 MinorUse a stable key for cart rows.
At Line 38, the key uses the array index (
${item.productId}-${i}), which becomes unstable when items are removed/reordered and can produce incorrect UI reconciliation.Suggested fix
- {items.map((item, i) => ( - <Card key={`${item.productId}-${i}`}> + {items.map((item) => ( + <Card key={String(item.productId)}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/cart/page.tsx` around lines 37 - 39, The cart row key is unstable because items.map currently uses `${item.productId}-${i}`; change it to a stable, unique identifier instead (e.g., use item.productId or item.id if present) on the Card component so React can reconcile rows correctly; locate the items.map(...) JSX that renders <Card key=...> and remove the array index from the key, ensuring the key value is a persistent unique property from the item object.examples/retail-store/app/orders/[id]/page.tsx-52-55 (1)
52-55:⚠️ Potential issue | 🟡 MinorUse a stable key for order item rows.
At Line 54, combining
productIdwith the array index can still cause unstable reconciliation when list membership changes. Prefer a stable domain key.Suggested fix
- {order.items.map((item, i) => ( + {order.items.map((item) => ( <div - key={`${item.productId}-${i}`} + key={String(item.productId)} className="flex justify-between py-2 border-b border-border last:border-0" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/orders/`[id]/page.tsx around lines 52 - 55, The list uses order.items.map with a key of `${item.productId}-${i}` which is unstable; replace the key prop on the mapped <div> in the order.items.map block with a stable domain identifier (e.g., use item.id or item.lineItemId if those exist, or item.productId alone only if productId is guaranteed unique per line) instead of combining with the array index, so React can reconcile rows correctly when membership/order changes.examples/retail-store/app/orders/[id]/page.tsx-82-82 (1)
82-82:⚠️ Potential issue | 🟡 MinorRemove the
as unknown as stringdouble-cast for timestamps.
entry.timestampis already typed asDate(fromstatusHistory: ReadonlyArray<{ status: string; timestamp: Date }>), so the cast is unnecessary. Simply usenew Date(entry.timestamp).toLocaleString()instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/orders/`[id]/page.tsx at line 82, Remove the redundant double-cast on the timestamp: replace the usage of new Date(entry.timestamp as unknown as string).toLocaleString() with a direct new Date(entry.timestamp).toLocaleString(), since entry.timestamp is already a Date (see statusHistory: ReadonlyArray<{ status: string; timestamp: Date }>) and the unnecessary "as unknown as string" cast should be removed from the rendering in the orders/[id]/page.tsx.examples/retail-store/app/api/orders/[id]/route.ts-39-48 (1)
39-48:⚠️ Potential issue | 🟡 MinorMissing input validation and error handling for status update.
Two issues:
body.statusis used without validation — could be undefined, non-string, or an invalid status valueupdateOrderStatuscan returnnull(per the test file), but the result isn't checked before fetching the updated order🛡️ Proposed fix
const body = await req.json(); + if (typeof body.status !== 'string') { + return NextResponse.json({ error: 'Invalid status' }, { status: 400 }); + } const db = await getDb(); const order = await getOrderById(db, id); if (!order || String(order.userId) !== userId) return NOT_FOUND; - await updateOrderStatus(db, id, { + const result = await updateOrderStatus(db, id, { status: body.status, timestamp: new Date(), }); + if (!result) return NOT_FOUND; const updated = await getOrderWithUser(db, id); return NextResponse.json(updated);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/api/orders/`[id]/route.ts around lines 39 - 48, Validate the incoming body.status before calling updateOrderStatus: ensure req.json() yields an object with a defined string status and that the value is one of the allowed statuses; if validation fails return a 400 error. After calling updateOrderStatus(db, id, {...}) check its return value for null and if null return an appropriate error response (e.g. 404 or 400 per business rules) instead of proceeding to call getOrderWithUser; update the route handler logic around getDb, getOrderById, updateOrderStatus, and getOrderWithUser to perform these checks and short-circuit with the proper NextResponse when validation or the update fails.examples/retail-store/app/api/orders/[id]/route.ts-11-12 (1)
11-12:⚠️ Potential issue | 🟡 MinorCreate fresh response objects for each request instead of reusing module-level constants.
NextResponse objects returned from route handlers must be created per-request, not reused across multiple handler invocations. Each request execution requires a fresh Response object. Replace the module-level
UNAUTHORIZEDandNOT_FOUNDconstants with inline responses in each handler, or move them into a helper function that returns a new instance:const createUnauthorized = () => NextResponse.json({ error: 'Not authenticated' }, { status: 401 }); const createNotFound = () => NextResponse.json({ error: 'Order not found' }, { status: 404 });Then return
createUnauthorized()andcreateNotFound()from each handler.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/api/orders/`[id]/route.ts around lines 11 - 12, The module-level NextResponse objects UNAUTHORIZED and NOT_FOUND are being reused across requests; change them to produce fresh responses per request by replacing those constants with factory functions (e.g., createUnauthorized and createNotFound) that return NextResponse.json(...) or by creating the NextResponse inline inside each handler where UNAUTHORIZED/NOT_FOUND are currently returned; ensure all handler code that references UNAUTHORIZED or NOT_FOUND calls the factory (or creates a new NextResponse) so a new Response object is created for every request.examples/retail-store/app/checkout/checkout-form.tsx-66-70 (1)
66-70:⚠️ Potential issue | 🟡 MinorHandle non-2xx order creation responses explicitly.
The current branch only handles
res.ok === true; failures are swallowed, leaving users without feedback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/checkout/checkout-form.tsx` around lines 66 - 70, The code currently only handles the res.ok path and swallows failures; update the checkout submission logic so that when fetch returns a non-2xx (res.ok === false) you parse the error response (await res.json() or text()), surface a user-friendly error (set an error state or call your toast/notification utility), and avoid calling invalidateCart() or router.push(`/orders/${order._id}`); also log the error for debugging. Specifically, modify the block around the fetch response handling where res.ok, invalidateCart, and router.push are used to add an else branch that extracts the server error message and displays it to the user while preserving the success flow that uses order._id on success.examples/retail-store/app/checkout/checkout-form.tsx-115-121 (1)
115-121:⚠️ Potential issue | 🟡 MinorFix label-to-control association for pickup location.
labeltargetspickup-location, butSelectTriggerhas no matchingid, so the label is not programmatically associated.💡 Suggested fix
- <Select value={locationId} onValueChange={setLocationId}> - <SelectTrigger> + <Select value={locationId} onValueChange={setLocationId}> + <SelectTrigger id="pickup-location"> <SelectValue placeholder="Select a store" /> </SelectTrigger>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/checkout/checkout-form.tsx` around lines 115 - 121, The label's htmlFor="pickup-location" has no matching control id, so add an id="pickup-location" to the SelectTrigger (or to the underlying native input rendered by the Select component) so the label is programmatically associated with the Select; update the SelectTrigger element (used with Select, SelectValue, SelectTrigger) to accept and include that id while keeping the existing value={locationId} and onValueChange={setLocationId} props.examples/retail-store/test/api-flows.test.ts-35-56 (1)
35-56:⚠️ Potential issue | 🟡 MinorThis test never exercises the auth guard it names.
getOrderWithUser()is called once without any actor context, and the assertions only restate that the stored order belongs to Alice. This still passes if Bob can read Alice's order. Use a user-scoped API/helper and assertnull/forbidden for Bob, or rename the test so it doesn't claim auth coverage.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/test/api-flows.test.ts` around lines 35 - 56, The test "user cannot access another user's order" never exercises authorization because it calls getOrderWithUser(orderId) without an actor; fix by invoking the user-scoped helper/API that enforces auth (or the function variant that accepts an acting user) using Bob as the actor and assert it returns null/forbidden for Bob, e.g., replace the unauthenticated getOrderWithUser call with the user-scoped call (the helper that takes an actor or userId) and add an assertion that Bob cannot fetch Alice's order; alternatively, if no user-scoped API exists, rename the test to remove the auth claim and add a new test that uses the correct actor-aware function to verify access control.
🧹 Nitpick comments (11)
docs/planning/mongo-target/next-steps.md (2)
3-3: Consider the longevity of cross-references to project artifacts.This planning document references artifacts under
projects/(lines 3, 59), which are typically transient. Whiledocs/planning/may be semi-transient itself, if this document is expected to remain stable beyond the current milestone, consider either:
- Consolidating the referenced content directly into this document
- Noting that the
projects/links are time-bound and may become staleBased on learnings, durable documentation should avoid linking to ephemeral project artifacts to aid long-term maintenance.
Also applies to: 59-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/planning/mongo-target/next-steps.md` at line 3, The planning doc references transient project artifacts (projects/mongo-example-apps/framework-limitations.md, projects/orm-consolidation/plan.md, projects/mongo-schema-migrations/plan.md) which may go stale; update the document to either inline the essential content from those referenced files into this document or add a clear, visible note stating that the links under projects/ are time-bound and may become stale, and list any critical excerpts you want preserved (e.g., framework gaps summary) so the doc remains durable long-term.
191-205: Prefer ASCII characters in the diagram.The diagram uses non-ASCII characters (
▼on lines 201, 203) which may render inconsistently across tools. Consider replacing with ASCII alternatives for better portability.Additionally, the code fence is missing a language specification, which triggers markdownlint warnings.
♻️ Proposed fix
-``` +```text ┌─ Area 5 (pipeline typing) ── parallel │ Area 1 (type ergonomics) ───┐ ├─ Area 6 (schema authoring) ── parallel ├───┤ Area 3 (migration bugs) ────┘ └─ Area 4 (migration completion) │ Area 2 (ORM ergonomics) ────────────── │ ── can overlap with Area 1 │ - ▼ + v TML-2186 (predictive maintenance app) │ - ▼ + v Shared ORM interface (M8, with Alexey)</details> As per coding guidelines, prefer ASCII characters in diagrams for consistency across rendering tools. <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@docs/planning/mongo-target/next-steps.mdaround lines 191 - 205, Replace the
non-ASCII triangle characters (the two occurrences of '▼') in the ASCII diagram
with an ASCII equivalent like 'v' and add a language specifier to the code fence
(e.g. change the openingtotext) so markdownlint stops warning; locate
the diagram by searching for the block containing "TML-2186 (predictive
maintenance app)" and "Shared ORM interface (M8, with Alexey)" and update those
symbols and the fence accordingly while preserving spacing/indentation.</details> </blockquote></details> <details> <summary>packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts (1)</summary><blockquote> `1462-1492`: **Move this case into a split validator/schema test file.** This suite is already far past the repo’s 500-line limit, so adding more validator-derivation coverage here makes it harder to navigate and maintain. A dedicated file like `interpreter.validator-derivation.test.ts` would keep this concern isolated. As per coding guidelines, “Keep test files under 500 lines to maintain readability and navigability. If a test file exceeds this limit, it should be split into multiple files.” <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts` around lines 1462 - 1492, Move the test case titled "handles nullable value object fields with oneOf null or object" out of the large interpreter.test.ts and into a new, focused test file (e.g., interpreter.validator-derivation.test.ts); copy the test body that uses interpretOk and getValidator and ensure the new file imports the same test helpers and test runner setup, then remove the original test from interpreter.test.ts so the original file falls under the 500-line limit. ``` </details> </blockquote></details> <details> <summary>examples/retail-store/src/components/navbar.tsx (1)</summary><blockquote> `35-35`: **Remove the unsafe `as string` cast for `user.name`.** Please narrow `user.name` before rendering `NavbarClient` instead of forcing the type. <details> <summary>♻️ Suggested change</summary> ```diff - <div className="ml-auto">{user && <NavbarClient userName={user.name as string} />}</div> + <div className="ml-auto"> + {user && typeof user.name === 'string' ? <NavbarClient userName={user.name} /> : null} + </div> ``` </details> As per coding guidelines, “Minimize type casts in TypeScript… Replace blind casts with type predicates.” <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/src/components/navbar.tsx` at line 35, Remove the unsafe cast "as string" on user.name in the Navbar render; instead narrow the type before passing it to NavbarClient by ensuring user.name is a string (e.g., check typeof user.name === 'string' or use a type guard/helper that validates and returns a string) and only render <NavbarClient userName={...} /> when that check passes, referencing the NavbarClient component and the user.name property so callers and types remain sound without blind casting. ``` </details> </blockquote></details> <details> <summary>examples/retail-store/test/search.test.ts (2)</summary><blockquote> `89-93`: **Make the case-insensitive test assert the matched record.** `toBeGreaterThanOrEqual(1)` is too loose; assert the expected product identity so the behavior is unambiguous. <details> <summary>✅ Tightened assertion example</summary> ```diff it('is case insensitive', async () => { await seedProducts(); const results = await searchProducts(ctx.db, 'LEATHER'); - expect(results.length).toBeGreaterThanOrEqual(1); + expect(results).toHaveLength(1); + expect(results[0]!.name).toBe('Leather Crossbody Bag'); }); ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/test/search.test.ts` around lines 89 - 93, The test "is case insensitive" currently only checks results.length; instead assert that searchProducts(ctx.db, 'LEATHER') returns the known seeded leather product from seedProducts by verifying a specific identifier/property (e.g., product id or name) is present in results. Modify the assertion to check results contains the expected product (use properties like id or name) rather than just length, referencing the test's seedProducts call and the searchProducts function and the results variable. ``` </details> --- `66-67`: **Drop optional chaining after length guarantees.** After `expect(results).toHaveLength(1)`, use non-null access (`results[0]!`) instead of defensive `?.`. As per coding guidelines: "Prefer assertions over defensive checks when data is guaranteed to be valid ... remove optional chaining ... access required properties directly." Also applies to: 73-74, 80-81 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/test/search.test.ts` around lines 66 - 67, Replace defensive optional chaining with non-null assertions where the test already asserts array length: after expect(results).toHaveLength(1) change expressions like results[0]?.name to results[0]!.name (and similarly results[0]?.id -> results[0]!.id) so the test accesses required properties directly; update the three occurrences corresponding to the assertions at/around the blocks that call expect(results).toHaveLength(1). ``` </details> </blockquote></details> <details> <summary>examples/retail-store/app/products/[id]/page.tsx (1)</summary><blockquote> `46-52`: **Prefer explicit value normalization over blind `as string` casts.** Lines 46-52 use assertion casts when building client props. Use `String(...)`/type guards instead so invalid shapes are handled explicitly. As per coding guidelines: `Avoid blind casts like \`as unknown as X\` in production TypeScript code. Use type predicates (\`value is X\`) or type guards instead to let the compiler narrow types safely.` <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/products/`[id]/page.tsx around lines 46 - 52, The code is using blind assertion casts (e.g., product.name as string, product.brand as string, product.price.currency as string, product.image.url as string) when creating the client props; replace these with explicit normalization and/or guards: validate or coerce each field (name, brand, price.amount, price.currency, image.url) using String(...) or a small type-guard/normalizer function before assigning to the props so malformed values are handled predictably (e.g., check product?.price?.amount is numeric and coerce with Number(...), and convert other fields with String(...) or reject/handle missing values), updating the code paths where product is used in the page component that constructs these objects to call the normalizer/guard instead of using `as string`. ``` </details> </blockquote></details> <details> <summary>examples/retail-store/app/checkout/page.tsx (1)</summary><blockquote> `35-35`: **Replace blind casts with explicit normalization/type guards.** Lines 35 and 73-78 rely on `as string` casts, which can hide invalid values at runtime. Prefer `String(...)` normalization or a local type predicate before building `CheckoutForm` props. As per coding guidelines: `Avoid blind casts like \`as unknown as X\` in production TypeScript code. Use type predicates (\`value is X\`) or type guards instead to let the compiler narrow types safely.` Also applies to: 73-78 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/checkout/page.tsx` at line 35, Replace the blind `as string` casts by normalizing or guarding the value before passing into the CheckoutForm props: add a local type guard (e.g., an isString predicate) or normalize with String(...) for `loc.name` and the values built for `CheckoutForm` (the properties constructed in the block around lines 73-78), then use the narrowed/normalized value when setting `name: ...` and other props—this removes the `as string` casts and ensures runtime safety while keeping the same field names (`loc.name`, the CheckoutForm prop keys). ``` </details> </blockquote></details> <details> <summary>examples/retail-store/src/data/invoices.ts (1)</summary><blockquote> `12-27`: **Extract and export a named input interface for `createInvoice`.** Using an inline structural type here makes reuse and API discoverability harder. A named exported interface keeps this public data-layer API cleaner. As per coding guidelines, "`{packages,examples,test}/**/*.{ts,tsx}`: Export interfaces and factory functions for public APIs; keep concrete classes as private implementation details." <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/src/data/invoices.ts` around lines 12 - 27, Extract the inline invoice parameter type into a named exported interface (e.g., export interface InvoiceInput) and use it as the type for the createInvoice second parameter; update the items nested type into the interface as well (exporting any nested item interface like InvoiceItem if desired) so createInvoice(db: Db, invoice: InvoiceInput) consumes the exported interface and the public data-layer API is discoverable and reusable. ``` </details> </blockquote></details> <details> <summary>examples/retail-store/test/order-lifecycle.test.ts (1)</summary><blockquote> `74-86`: **Consider using type assertions at the ORM call site instead of bracket notation.** The bracket notation (`shipped!['statusHistory']`) and subsequent type cast suggest the ORM return type doesn't include `statusHistory`. This works but is fragile. <details> <summary>♻️ Suggested improvement</summary> If the type from `updateOrderStatus` is incomplete, consider asserting at the call site: ```diff - const shipped = await updateOrderStatus(ctx.db, order._id as string, { + const shipped = await updateOrderStatus(ctx.db, order._id as string, { status: 'shipped', timestamp: new Date('2026-03-02T14:00:00Z'), - }); + }) as { statusHistory: Array<{ status: string; timestamp: Date }> } | null; expect(shipped).not.toBeNull(); - expect(shipped!['statusHistory']).toHaveLength(2); + expect(shipped!.statusHistory).toHaveLength(2); ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/test/order-lifecycle.test.ts` around lines 74 - 86, The test uses bracket notation because the return type from updateOrderStatus lacks statusHistory; instead assert the correct type at the call site (or update the function's return type) so you can use dot access safely: cast the result of updateOrderStatus (for example for variables shipped and delivered) to an interface/type that includes statusHistory (e.g., OrderWithHistory) and then replace shipped!['statusHistory'] and delivered!['statusHistory'] with shipped!.statusHistory and delivered!.statusHistory; reference updateOrderStatus, shipped, delivered, and order to locate the affected calls. ``` </details> </blockquote></details> <details> <summary>examples/retail-store/app/page.tsx (1)</summary><blockquote> `96-100`: **Pagination "Next" button may appear on the last full page.** When the last page has exactly `PAGE_SIZE` items, the "Next" button will still appear, leading to an empty next page. This is a common pattern but can be confusing to users. Consider fetching `PAGE_SIZE + 1` items and using `products.length > PAGE_SIZE` to determine if more pages exist, then slice to display only `PAGE_SIZE`. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/page.tsx` around lines 96 - 100, The Next button is shown when products.length === PAGE_SIZE which still shows it on a full final page; change the data fetch to request PAGE_SIZE + 1 items (wherever you populate products) and then in the component use a hasMore condition products.length > PAGE_SIZE, display products.slice(0, PAGE_SIZE) so only PAGE_SIZE are rendered, and update the Next button rendering to use hasMore (e.g., if (products.length > PAGE_SIZE) show <Button...><Link href={`/?page=${page + 1}`}>Next</Link></Button>). ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Inline comments:
In@examples/retail-store/app/api/cart/route.ts:
- Around line 17-20: Add Arktype validation in the POST handler: define an
Arktype schema describing the expected request shape (e.g., cart item fields)
and import the schema validator, then replace the blind usage of body with a
validated value by validating await req.json() against that schema; if
validation fails return a 400 Response (with errors) instead of calling
addToCart, and if it passes call addToCart(db, userId, validatedValue) so only a
typed/validated object reaches addToCart/getDb.In
@examples/retail-store/app/api/orders/route.ts:
- Around line 20-27: The order creation and cart clearing must be done
atomically: wrap the calls to createOrder and clearCart in a single database
transaction/session so both succeed or both roll back on error; modify the
calling site to start a transaction (e.g., startSession/transaction on the db),
pass the session/transaction object into createOrder and clearCart (or call
transaction-aware variants), and commit the transaction only after both
operations succeed, otherwise abort/rollback and surface the error from the
transaction; update createOrder and clearCart signatures to accept an optional
session/transaction parameter if needed.- Around line 18-24: Add runtime validation for the incoming order payload using
an Arktype schema: define an Arktype that matches the expected shape (items
array, shippingAddress object, optional type string with default 'home'), parse
the body from req.json(), run it through the Arktype validator, and if
validation fails return a 400 response with the validation errors; only after
successful validation call createOrder(db, { userId, items: validated.items,
shippingAddress: validated.shippingAddress, type: validated.type ?? 'home' }) so
that getDb(), createOrder(), and downstream code receive validated
runtime-checked data.In
@examples/retail-store/app/api/products/random/route.ts:
- Around line 6-8: Validate and sanitize the incoming count before calling
getRandomProducts: read the raw value from
req.nextUrl.searchParams.get('count'), parse it to an integer, handle NaN by
falling back to a safe default (e.g., 4), and clamp it to a sensible range
(e.g., min 1, max 100) to avoid extreme queries; then pass the sanitized/clamped
integer to getRandomProducts. Ensure you update the variable used (count) so
downstream code only sees the validated number.In
@examples/retail-store/app/cart/cart-actions.tsx:
- Around line 8-27: CartActionsProps currently makes productId optional even
when mode === 'remove', and handleAction blindly builds
/api/cart?productId=${productId}and ignores failed DELETE responses; change
the props to a discriminated union so productId is required when mode is
'remove' (e.g. { mode: 'remove'; productId: string } | { mode: 'clear' }) to
tighten typing in the CartActions component, and update handleAction to validate
productId before calling the API in remove mode, await the fetch response and
check response.ok (throw or handle non-ok responses) so invalidateCart() and
router.refresh() are only called on success; keep setLoading(false) in finally.In
@examples/retail-store/app/checkout/checkout-form.tsx:
- Around line 51-55: When orderType is the BOPIS/pickup path (i.e., not 'home')
the computed shippingAddress can be '' if locationId is missing or doesn't
match; update the submit flow where shippingAddress is used (and the related
code in the same file around the other block at lines ~118-129) to validate that
for pickup the selected locationId yields a non-empty shippingAddress and block
submission if it is empty—return early, set a form/field error, and surface a
user-facing message; reference the shippingAddress computation, orderType,
locationId, and locations to locate and enforce this validation.In
@examples/retail-store/app/orders/[id]/order-status-buttons.tsx:
- Around line 25-34: In handleAdvance(), the fetch call currently treats any
HTTP response (including 4xx/5xx) as success because fetch only throws on
network errors; update the code to capture the Response returned by fetch (e.g.,
const res = await fetch(...)), check res.ok, and only call router.refresh() when
res.ok is true; for non-ok responses, handle the failure by logging/setting an
error state or showing a user-facing message and avoid calling router.refresh();
ensure setLoading(false) still runs in the finally block and surface the
response status/body as appropriate for debugging or UX.In
@examples/retail-store/app/orders/[id]/page.tsx:
- Around line 19-26: OrderDetail currently loads an order by ID without checking
authentication or ownership; fetch the current user/session first (e.g. via your
auth helper like getCurrentUser or getSession) and ensure the user is
authenticated, then enforce ownership before rendering by either passing the
user id into getOrderWithUser or comparing order.userId to the authenticated
user's id; if no user or the order does not belong to them, call
notFound()/handle unauthorized access. Ensure checks occur before any sensitive
data is returned from getOrderWithUser.In
@examples/retail-store/middleware.ts:
- Around line 3-13: The middleware currently redirects all unauthenticated
requests (NextResponse.redirect) which will incorrectly redirect API consumers;
update the middleware (function middleware and its use of
request.cookies.get('userId')) to detect API calls (e.g., inspect
request.nextUrl.pathname or request.url for a leading '/api/') and for
unauthenticated API requests return an API-appropriate response (HTTP 401 or 403
with a minimal body or JSON) instead of redirecting, while preserving the
redirect behavior for non-API routes (redirect to '/login'); keep the existing
config.matcher but ensure API detection logic inside middleware differentiates
API vs browser routes before choosing NextResponse.redirect vs an error status
response.In
@examples/retail-store/migrations/20260413T0314_migration/migration.json:
- Around line 786-794: The JSON schema validator for the nested "price" object
currently only requires "currency" (see the schema block that defines "price"
with "required": ["currency"]); update the contract/schema emission pipeline so
that the validator also marks numeric fields as required and with appropriate
numeric bsonTypes for all affected places (e.g., ensure "price.amount" is
required and numeric, and similarly require "unitPrice", "lineTotal",
"subtotal", "tax", and "total" wherever those fields appear), then regenerate
the migration JSON so those emitted validators include the numeric fields and
types instead of only "currency".In
@examples/retail-store/migrations/20260413T0314_migration/ops.json:
- Around line 799-853: The precheck/postcheck for
"index.events.create(timestamp:1)" currently only matches index by key shape
(the "filter" comparing "key": { timestamp: 1 }) which allows option-mismatched
indexes to pass; update the checks that call "listIndexes" and the createIndex
execution to verify the full index specification (include and compare option
fields such as "expireAfterSeconds", "unique", "sparse", "collation", and for
text indexes "weights", and also consider matching by "name" when appropriate)
so the precheck fails if an index with the same key but different options exists
and the postcheck asserts those same option values after creation; apply the
same fix pattern to other index ops referenced (IDs around 914-969, 972-1035,
1095-1153, 1277-1342).- Around line 73-124: The JSON schema validators dropped required numeric money
fields — update the relevant $jsonSchema validators (the "validator" object
under the collection ops) so each Price object requires both "currency" and
"amount" (e.g., the "price" property inside "items"), and for Invoice-like
documents add "subtotal", "tax", and "total" as required numeric fields and make
each line item's "unitPrice" and "lineTotal" required; modify the "required"
arrays for the "price" property and for Invoice/item objects (references:
"price", "items", "validator", "$jsonSchema", "subtotal", "tax", "total",
"unitPrice", "lineTotal") to include these fields and ensure their "bsonType" is
an appropriate numeric type (int/decimal) in the schema.- Around line 3-46: The migration is creating standalone collections for event
variants (e.g., the "collection": "addToCartEvent" created by the op with id
"collection.addToCartEvent.create") whose validator omits inherited Event
fields; remove the additive ops that create variant collections (addToCartEvent,
ViewProductEvent, SearchEvent) so you do not materialize @@base event variants
as separate collections, or if you must keep an op, replace it with a no-op or a
check that the single canonical "events" collection exists and enforces the full
validator (including _id, userId, sessionId, timestamp, type) rather than
creating per-variant collections or validators that drop inherited fields.In
@examples/retail-store/prisma/contract.prisma:
- Around line 75-82: The Cart schema allows multiple carts per user because it
uses @@index([userId])—replace that index with a uniqueness constraint so the DB
enforces one cart per user; update the Cart model (referencing model Cart, field
userId and the current @@index([userId])) to use a unique constraint (e.g., make
userId unique or use @@unique([userId])) and run/prisma migrate to apply the
change.In
@examples/retail-store/src/components/add-to-cart-button.tsx:
- Around line 24-38: The add-to-cart flow currently treats any fetch response as
success; update the async handler in add-to-cart-button.tsx that calls
fetch('/api/cart', ...) to check the response.ok flag and handle non-2xx
responses as failures: after awaiting fetch, if (!response.ok) parse or read the
error message, setState('error') (or similar) and avoid calling invalidateCart()
or setState('added'); only call invalidateCart(), setState('added'), and the
setTimeout to revert to 'idle' when response.ok is true. Ensure you reference
the existing functions/variables invalidateCart, setState, and the product
payload (product._id, etc.) when making the change.In
@examples/retail-store/src/data/object-id-filter.ts:
- Around line 5-12: The objectIdEq and rawObjectIdFilter functions construct
ObjectId from strings without validation which can throw; add a centralized
validator helper (e.g., ensureValidObjectId or parseObjectId) that uses
ObjectId.isValid() to check string ids and either throws a controlled error (or
returns a Result/nullable) when invalid, then call that helper from objectIdEq
and rawObjectIdFilter instead of new ObjectId(id) so all invalid string IDs are
handled predictably rather than bubbling up as unhandled exceptions.In
@examples/retail-store/src/data/products.ts:
- Around line 14-15: The paginated read in findProductsPaginated is unstable
because it calls db.orm.products.skip(skip).take(take).all() without a
deterministic order; update findProductsPaginated to apply a stable sort (for
example by _id or a canonical catalog field like sku) before calling skip/take
so page boundaries are deterministic (i.e., add an order/sort step on
db.orm.products such as ordering by _id ascending or sku before .skip and
.take).In
@examples/retail-store/src/db-singleton.ts:
- Around line 7-11: The current logic stores the raw promise from createClient
in dbPromise so a transient failure permanently caches a rejected promise;
change it so that when calling createClient (symbol: createClient) you store a
wrapper promise that on rejection clears dbPromise (and rethrows the error) so
future getDb() calls will retry, i.e., catch failures from the createClient call
inside the initializer and reset dbPromise to undefined before rethrowing the
error; update the singleton getter (symbol: getDb) to keep using dbPromise but
rely on this wrapper behavior to avoid caching rejected promises.In
@packages/2-mongo-family/5-query-builders/orm/src/collection.ts:
- Around line 481-503: The scalar branch in
#wrapFieldValuecurrently ignores
field.many and wraps an entire array in a single MongoParamRef, causing the
scalar codec to be applied to the array rather than each element; update
#wrapFieldValueso that when field.type.kind === 'scalar' and field.many &&
Array.isArray(value) you map each element to a new MongoParamRef(element, {
codecId: field.type.codecId }) (otherwise keep the existing single MongoParamRef
behavior), ensuring scalar lists (e.g., String[]/ObjectId[]) and nested
value-object lists containing scalar arrays are wrapped per-element with the
codecId.In
@packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts:
- Around line 150-151: createMongoAdapter currently replaces the
defaultCodecRegistry when a custom MongoCodecRegistry is passed, causing
built-in codecs (e.g., "mongo/objectId@1") to be lost; change createMongoAdapter
to merge the provided codecs into the default registry instead of replacing it
(or adjust MongoAdapterImpl construction to accept a merged registry). Locate
createMongoAdapter and defaultCodecRegistry and implement registry augmentation:
start from defaultCodecRegistry(), then overlay/extend it with entries from the
supplied codecs (preserving existing built-ins unless explicitly overridden),
and pass the merged registry into new MongoAdapterImpl so built-in codecs remain
available alongside custom ones.
Minor comments:
In@examples/retail-store/app/api/auth/signup/route.ts:
- Around line 16-21: The auth cookie set in cookieStore.set(...) (in route.ts)
is missing the secure flag; update the options passed to cookieStore.set (the
call that sets 'userId') to include secure: true so the cookie is only sent over
HTTPS (optionally you can conditionally set secure based on NODE_ENV for local
dev, but ensure production uses secure: true).In
@examples/retail-store/app/api/orders/[id]/route.ts:
- Around line 39-48: Validate the incoming body.status before calling
updateOrderStatus: ensure req.json() yields an object with a defined string
status and that the value is one of the allowed statuses; if validation fails
return a 400 error. After calling updateOrderStatus(db, id, {...}) check its
return value for null and if null return an appropriate error response (e.g. 404
or 400 per business rules) instead of proceeding to call getOrderWithUser;
update the route handler logic around getDb, getOrderById, updateOrderStatus,
and getOrderWithUser to perform these checks and short-circuit with the proper
NextResponse when validation or the update fails.- Around line 11-12: The module-level NextResponse objects UNAUTHORIZED and
NOT_FOUND are being reused across requests; change them to produce fresh
responses per request by replacing those constants with factory functions (e.g.,
createUnauthorized and createNotFound) that return NextResponse.json(...) or by
creating the NextResponse inline inside each handler where
UNAUTHORIZED/NOT_FOUND are currently returned; ensure all handler code that
references UNAUTHORIZED or NOT_FOUND calls the factory (or creates a new
NextResponse) so a new Response object is created for every request.In
@examples/retail-store/app/cart/page.tsx:
- Around line 37-39: The cart row key is unstable because items.map currently
uses${item.productId}-${i}; change it to a stable, unique identifier instead
(e.g., use item.productId or item.id if present) on the Card component so React
can reconcile rows correctly; locate the items.map(...) JSX that renders and remove the array index from the key, ensuring the key value is a
persistent unique property from the item object.In
@examples/retail-store/app/checkout/checkout-form.tsx:
- Around line 66-70: The code currently only handles the res.ok path and
swallows failures; update the checkout submission logic so that when fetch
returns a non-2xx (res.ok === false) you parse the error response (await
res.json() or text()), surface a user-friendly error (set an error state or call
your toast/notification utility), and avoid calling invalidateCart() or
router.push(/orders/${order._id}); also log the error for debugging.
Specifically, modify the block around the fetch response handling where res.ok,
invalidateCart, and router.push are used to add an else branch that extracts the
server error message and displays it to the user while preserving the success
flow that uses order._id on success.- Around line 115-121: The label's htmlFor="pickup-location" has no matching
control id, so add an id="pickup-location" to the SelectTrigger (or to the
underlying native input rendered by the Select component) so the label is
programmatically associated with the Select; update the SelectTrigger element
(used with Select, SelectValue, SelectTrigger) to accept and include that id
while keeping the existing value={locationId} and onValueChange={setLocationId}
props.In
@examples/retail-store/app/checkout/page.tsx:
- Line 55: The key for each cart row currently uses the array index (
i)
combined withitem.productIdwhich can break element identity on list
mutations; update the key in the cart row render (the div using
key={${item.productId}-${i}}in page.tsx) to use a stable unique identifier
such asitem.cartItemIdor a compound ofitem.cartItemIdand
item.productId(e.g.${item.cartItemId}-${item.productId}) so each row has a
deterministic, unique key instead of relying on the loop index.In
@examples/retail-store/app/orders/[id]/page.tsx:
- Around line 52-55: The list uses order.items.map with a key of
${item.productId}-${i}which is unstable; replace the key prop on the mappedin the order.items.map block with a stable domain identifier (e.g., use item.id or item.lineItemId if those exist, or item.productId alone only if productId is guaranteed unique per line) instead of combining with the array index, so React can reconcile rows correctly when membership/order changes. - Line 82: Remove the redundant double-cast on the timestamp: replace the usage of new Date(entry.timestamp as unknown as string).toLocaleString() with a direct new Date(entry.timestamp).toLocaleString(), since entry.timestamp is already a Date (see statusHistory: ReadonlyArray<{ status: string; timestamp: Date }>) and the unnecessary "as unknown as string" cast should be removed from the rendering in the orders/[id]/page.tsx.In
@examples/retail-store/src/components/navbar-client.tsx:
- Around line 9-13: The handleLogout function should await the POST to
'/api/auth/logout' and only navigate when that request succeeds; update
handleLogout to check the fetch response (e.g., response.ok) and handle non-OK
or network errors by logging or showing an error instead of calling
router.push('/login') and router.refresh(); keep the fetch to '/api/auth/logout'
and the router calls (router.push, router.refresh) but gate them behind a
successful response and handle failures (retry, toast, or console.error) to
avoid redirecting when logout fails.In
@examples/retail-store/test/aggregation.test.ts:
- Around line 42-47: The test for aggregateEventsByType is asserting on specific
array indices which is flaky because Mongo aggregation results are unordered
unless explicitly sorted; update the test to check counts by key instead of
index — for example, transform the result from aggregateEventsByType(ctx.db,
'test-user') into a map keyed by _id and assert resultMap['view-product'] === 3,
resultMap['add-to-cart'] === 2, and resultMap['search'] === 1 (or use
expect.arrayContaining with the expected objects) so the assertions no longer
depend on row order.In
@examples/retail-store/test/api-flows.test.ts:
- Around line 35-56: The test "user cannot access another user's order" never
exercises authorization because it calls getOrderWithUser(orderId) without an
actor; fix by invoking the user-scoped helper/API that enforces auth (or the
function variant that accepts an acting user) using Bob as the actor and assert
it returns null/forbidden for Bob, e.g., replace the unauthenticated
getOrderWithUser call with the user-scoped call (the helper that takes an actor
or userId) and add an assertion that Bob cannot fetch Alice's order;
alternatively, if no user-scoped API exists, rename the test to remove the auth
claim and add a new test that uses the correct actor-aware function to verify
access control.
Nitpick comments:
In@docs/planning/mongo-target/next-steps.md:
- Line 3: The planning doc references transient project artifacts
(projects/mongo-example-apps/framework-limitations.md,
projects/orm-consolidation/plan.md, projects/mongo-schema-migrations/plan.md)
which may go stale; update the document to either inline the essential content
from those referenced files into this document or add a clear, visible note
stating that the links under projects/ are time-bound and may become stale, and
list any critical excerpts you want preserved (e.g., framework gaps summary) so
the doc remains durable long-term.- Around line 191-205: Replace the non-ASCII triangle characters (the two
occurrences of '▼') in the ASCII diagram with an ASCII equivalent like 'v' and
add a language specifier to the code fence (e.g. change the opening ``` toblock containing "TML-2186 (predictive maintenance app)" and "Shared ORM interface (M8, with Alexey)" and update those symbols and the fence accordingly while preserving spacing/indentation. In `@examples/retail-store/app/checkout/page.tsx`: - Line 35: Replace the blind `as string` casts by normalizing or guarding the value before passing into the CheckoutForm props: add a local type guard (e.g., an isString predicate) or normalize with String(...) for `loc.name` and the values built for `CheckoutForm` (the properties constructed in the block around lines 73-78), then use the narrowed/normalized value when setting `name: ...` and other props—this removes the `as string` casts and ensures runtime safety while keeping the same field names (`loc.name`, the CheckoutForm prop keys). In `@examples/retail-store/app/page.tsx`: - Around line 96-100: The Next button is shown when products.length === PAGE_SIZE which still shows it on a full final page; change the data fetch to request PAGE_SIZE + 1 items (wherever you populate products) and then in the component use a hasMore condition products.length > PAGE_SIZE, display products.slice(0, PAGE_SIZE) so only PAGE_SIZE are rendered, and update the Next button rendering to use hasMore (e.g., if (products.length > PAGE_SIZE) show <Button...><Link href={`/?page=${page + 1}`}>Next</Link></Button>). In `@examples/retail-store/app/products/`[id]/page.tsx: - Around line 46-52: The code is using blind assertion casts (e.g., product.name as string, product.brand as string, product.price.currency as string, product.image.url as string) when creating the client props; replace these with explicit normalization and/or guards: validate or coerce each field (name, brand, price.amount, price.currency, image.url) using String(...) or a small type-guard/normalizer function before assigning to the props so malformed values are handled predictably (e.g., check product?.price?.amount is numeric and coerce with Number(...), and convert other fields with String(...) or reject/handle missing values), updating the code paths where product is used in the page component that constructs these objects to call the normalizer/guard instead of using `as string`. In `@examples/retail-store/src/components/navbar.tsx`: - Line 35: Remove the unsafe cast "as string" on user.name in the Navbar render; instead narrow the type before passing it to NavbarClient by ensuring user.name is a string (e.g., check typeof user.name === 'string' or use a type guard/helper that validates and returns a string) and only render <NavbarClient userName={...} /> when that check passes, referencing the NavbarClient component and the user.name property so callers and types remain sound without blind casting. In `@examples/retail-store/src/data/invoices.ts`: - Around line 12-27: Extract the inline invoice parameter type into a named exported interface (e.g., export interface InvoiceInput) and use it as the type for the createInvoice second parameter; update the items nested type into the interface as well (exporting any nested item interface like InvoiceItem if desired) so createInvoice(db: Db, invoice: InvoiceInput) consumes the exported interface and the public data-layer API is discoverable and reusable. In `@examples/retail-store/test/order-lifecycle.test.ts`: - Around line 74-86: The test uses bracket notation because the return type from updateOrderStatus lacks statusHistory; instead assert the correct type at the call site (or update the function's return type) so you can use dot access safely: cast the result of updateOrderStatus (for example for variables shipped and delivered) to an interface/type that includes statusHistory (e.g., OrderWithHistory) and then replace shipped!['statusHistory'] and delivered!['statusHistory'] with shipped!.statusHistory and delivered!.statusHistory; reference updateOrderStatus, shipped, delivered, and order to locate the affected calls. In `@examples/retail-store/test/search.test.ts`: - Around line 89-93: The test "is case insensitive" currently only checks results.length; instead assert that searchProducts(ctx.db, 'LEATHER') returns the known seeded leather product from seedProducts by verifying a specific identifier/property (e.g., product id or name) is present in results. Modify the assertion to check results contains the expected product (use properties like id or name) rather than just length, referencing the test's seedProducts call and the searchProducts function and the results variable. - Around line 66-67: Replace defensive optional chaining with non-null assertions where the test already asserts array length: after expect(results).toHaveLength(1) change expressions like results[0]?.name to results[0]!.name (and similarly results[0]?.id -> results[0]!.id) so the test accesses required properties directly; update the three occurrences corresponding to the assertions at/around the blocks that call expect(results).toHaveLength(1). In `@packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts`: - Around line 1462-1492: Move the test case titled "handles nullable value object fields with oneOf null or object" out of the large interpreter.test.ts and into a new, focused test file (e.g., interpreter.validator-derivation.test.ts); copy the test body that uses interpretOk and getValidator and ensure the new file imports the same test helpers and test runner setup, then remove the original test from interpreter.test.ts so the original file falls under the 500-line limit.🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
examples/retail-store/README.md (1)
109-117: Trim README internals and link to canonical limitations docs.This section is valuable, but it’s implementation-heavy for an example README. Consider summarizing the key user-impacting caveats and linking to canonical framework docs for the detailed gaps.
As per coding guidelines: "
**/README.md: keep README.md focused on what the package does, when to use it, and a few concrete examples. Avoid internal implementation detail unless it materially affects usage" and "**/*.{ts,tsx,md,mdx}: Prefer links to canonical docs over long comments."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/README.md` around lines 109 - 117, The "Framework Gaps" section in the README is too implementation-heavy; replace the detailed bullet list with a short, user-focused summary of key caveats and add a link to the canonical limitations/docs page for full details. Keep a brief mention of high-impact items (e.g., custom float scalar via scalarTypeDescriptors, ObjectId filter workaround for MongoFieldFilter.eq, migration artifacts applied with pnpm migration:apply) but remove file-level implementation pointers like src/data/object-id-filter.ts and long operator or pipeline internals; instead point readers to the canonical docs page and any relevant CLI commands.examples/retail-store/src/seed.ts (2)
256-259: Avoid positional product picks for seed dependencies.Selecting
products[0],products[1], andproducts[11]is brittle; catalog reordering can silently change what cart/order/event records reference.♻️ Suggested change
- const p0 = products[0]; - const p1 = products[1]; - const p2 = products[11]; + const productsByCode = new Map(products.map((p) => [String(p.code), p])); + const p0 = productsByCode.get('HER-OXF-001'); + const p1 = productsByCode.get('HER-LIN-002'); + const p2 = productsByCode.get('CRA-BAG-017'); if (!p0 || !p1 || !p2) throw new Error('Failed to seed products');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/src/seed.ts` around lines 256 - 259, The current seed uses brittle positional picks (products[0], products[1], products[11] assigned to p0/p1/p2); replace these with lookups by a stable unique identifier (e.g., product.id or product.sku) using products.find(...) to locate each required product (referencing the variables p0, p1, p2), validate each was found and throw a descriptive error listing which identifiers are missing, and update any subsequent cart/order/event seed code that uses p0/p1/p2 to rely on these looked‑up objects instead of array positions.
362-377: Derive event metadata from seeded product instead of literals.
brandandsubCategoryare currently hardcoded ('Heritage','Topwear'), which can drift fromp0if catalog data changes.♻️ Suggested change
await createViewProductEvent(db, { userId: 'alice-session-1', sessionId: 'sess-001', timestamp: new Date('2026-03-01T09:00:00Z'), productId: String(p0._id), - subCategory: 'Topwear', - brand: 'Heritage', + subCategory: String(p0.subCategory), + brand: String(p0.brand), }); await createAddToCartEvent(db, { userId: 'alice-session-1', sessionId: 'sess-001', timestamp: new Date('2026-03-01T09:05:00Z'), productId: String(p0._id), - brand: 'Heritage', + brand: String(p0.brand), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/src/seed.ts` around lines 362 - 377, The events currently hardcode brand and subCategory; update the createViewProductEvent and createAddToCartEvent calls to read these values from the seeded product p0 (e.g., use p0.brand and p0.subCategory or p0.category.subCategory if your product shape nests category) instead of the literal strings so event metadata always reflects the catalog data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/retail-store/app/api/orders/`[id]/route.ts:
- Around line 15-16: The code currently trusts a client-supplied cookie via
getAuthUserId() to derive identity—this is tamperable; change authentication to
derive user identity from a verified server-side session or signed/encrypted
token instead: update getAuthUserId() usage (and any checks around it in the
route handlers that call unauthorized()) to validate a signed session cookie or
look up session state on the server (e.g., verify JWT signature or query session
store) and return the authenticated user id only after verification; ensure all
ownership checks use this verified identity rather than raw cookie values and
fail via unauthorized() when verification fails.
- Around line 39-45: The PATCH handler currently calls req.json() and writes
body.status directly via updateOrderStatus (and uses getOrderById/notFound)
without validation; wrap req.json() in try/catch to return 400 on invalid JSON,
add an Arktype schema (e.g., OrderStatusBody with a required non-empty string
status) and validate the parsed body before calling updateOrderStatus, returning
400 on validation failure, and pass the validated status (typed) into
updateOrderStatus to avoid implicit any and protect order state integrity.
In `@examples/retail-store/README.md`:
- Around line 79-90: The two fenced code blocks showing the schema/diagram
("Products ─── Price, Image..." block) and the file list
("prisma/contract.prisma PSL schema..." block) need language identifiers to
satisfy MD040; update each triple-backtick fence to include a language token
such as text (e.g., change ``` to ```text) so markdownlint passes and rendering
is consistent; locate the two fences surrounding the block that starts with
"Products ─── Price, Image" and the block that lists files like
"prisma/contract.prisma" and add the language specifier.
- Line 61: The README currently says users are redirected to "sign-up" but the
app middleware redirects to /login; update the README sentence so it matches the
actual redirect path (/login) and the onboarding flow (e.g., "Open
http://localhost:3000 — you'll be redirected to /login; click 'Sign Up' or 'Log
In' to create a user or authenticate") so documentation and the middleware
redirect (referenced as the middleware redirecting to /login) are consistent.
---
Nitpick comments:
In `@examples/retail-store/README.md`:
- Around line 109-117: The "Framework Gaps" section in the README is too
implementation-heavy; replace the detailed bullet list with a short,
user-focused summary of key caveats and add a link to the canonical
limitations/docs page for full details. Keep a brief mention of high-impact
items (e.g., custom float scalar via scalarTypeDescriptors, ObjectId filter
workaround for MongoFieldFilter.eq, migration artifacts applied with pnpm
migration:apply) but remove file-level implementation pointers like
src/data/object-id-filter.ts and long operator or pipeline internals; instead
point readers to the canonical docs page and any relevant CLI commands.
In `@examples/retail-store/src/seed.ts`:
- Around line 256-259: The current seed uses brittle positional picks
(products[0], products[1], products[11] assigned to p0/p1/p2); replace these
with lookups by a stable unique identifier (e.g., product.id or product.sku)
using products.find(...) to locate each required product (referencing the
variables p0, p1, p2), validate each was found and throw a descriptive error
listing which identifiers are missing, and update any subsequent
cart/order/event seed code that uses p0/p1/p2 to rely on these looked‑up objects
instead of array positions.
- Around line 362-377: The events currently hardcode brand and subCategory;
update the createViewProductEvent and createAddToCartEvent calls to read these
values from the seeded product p0 (e.g., use p0.brand and p0.subCategory or
p0.category.subCategory if your product shape nests category) instead of the
literal strings so event metadata always reflects the catalog data.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: c52ab1df-f7d9-44eb-85a5-5d427bfca22e
⛔ Files ignored due to path filters (24)
examples/retail-store/public/images/products/cra-bag-017.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/cra-blt-019.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/cra-tot-018.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-che-022.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-drs-021.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-jea-020.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-lin-002.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-mer-003.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-oxf-001.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-psq-023.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-bea-032.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-duf-031.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-hik-034.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-rai-035.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-sho-030.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-trl-033.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-chi-042.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-den-012.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-jog-013.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-pol-011.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-slp-016.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-snk-015.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-sun-014.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-tee-010.jpgis excluded by!**/*.jpg
📒 Files selected for processing (4)
.vscode/settings.jsonexamples/retail-store/README.mdexamples/retail-store/app/api/orders/[id]/route.tsexamples/retail-store/src/seed.ts
✅ Files skipped from review due to trivial changes (1)
- .vscode/settings.json
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
examples/retail-store/app/cart/cart-actions.tsx (1)
20-23:⚠️ Potential issue | 🟡 MinorAdd a runtime guard for empty
productIdin remove mode.
productIdis typed asstring, so''is still possible at runtime. With the current API handler (if (productId) ... else clearCart(...)),productId=can be interpreted as clear-cart. Guarding non-empty IDs here avoids accidental cart clears.🔧 Suggested fix
async function handleAction() { setLoading(true); try { + if (mode === 'remove' && productId.trim().length === 0) { + throw new Error('Missing productId for remove action'); + } const url = mode === 'remove' ? `/api/cart?${new URLSearchParams({ productId }).toString()}` : '/api/cart';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/cart/cart-actions.tsx` around lines 20 - 23, When building the request URL in cart-actions.tsx, guard against empty productId when mode === 'remove' by checking productId is non-empty (e.g., productId.trim() !== '' or Boolean(productId)) before using it in new URLSearchParams; if productId is empty, treat it as a non-remove operation (use '/api/cart' or abort) to avoid accidentally invoking the clearCart path on the API; update the const url construction that references mode and productId accordingly.
🧹 Nitpick comments (2)
examples/retail-store/app/cart/cart-actions.tsx (1)
33-50: Consider matching loading copy between remove and clear actions.
clearshows"Clearing..."whileremovealways shows"Remove". A"Removing..."state would make action feedback consistent.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/cart/cart-actions.tsx` around lines 33 - 50, The "remove" branch of the CartActions component doesn't show a loading label—when mode === 'remove' the Button always renders "Remove" even if loading is true; update the Remove button render (the JSX under if (mode === 'remove')) to conditionally display "Removing..." when loading is true (similar to the Clear button) while keeping the same props (variant="ghost", size="sm", onClick={handleAction}, disabled={loading}, className="text-destructive") so the labels are consistent across the remove and clear actions.examples/retail-store/app/api/products/random/route.ts (1)
6-13: Use strict numeric parsing forcount.
Number.parseIntaccepts trailing characters (e.g.,"4abc"→ 4), which weakens input validation. PreferNumber(...)+Number.isSafeInteger()to reject malformed values entirely.Suggested refactor
export async function GET(req: NextRequest) { const raw = req.nextUrl.searchParams.get('count'); - const count = raw === null ? 4 : Number.parseInt(raw, 10); - if (!Number.isInteger(count) || count < 1 || count > 24) { + const count = raw === null ? 4 : Number(raw); + if (!Number.isSafeInteger(count) || count < 1 || count > 24) { return NextResponse.json( { error: 'Invalid count. Expected an integer between 1 and 24.' }, { status: 400 }, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/api/products/random/route.ts` around lines 6 - 13, The current parsing uses Number.parseInt which allows trailing characters (e.g., "4abc"); change the parsing to use Number(raw) and validate with Number.isSafeInteger to reject malformed input: compute count = raw === null ? 4 : Number(raw), then check !Number.isSafeInteger(count) || count < 1 || count > 24 and respond with the same 400 JSON on failure; update references to raw and count in this route handler (the random products route) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/retail-store/app/orders/`[id]/page.tsx:
- Around line 33-34: The line that defines lastStatus using "as string" masks
undefined when order.statusHistory can be empty; instead, check
order.statusHistory.length > 0 (or use a safe getter) and compute lastStatus
only when present, e.g. set lastStatus to
order.statusHistory[order.statusHistory.length - 1]?.status ?? 'default' or
return/handle the empty case before rendering; remove the blind cast "as string"
and ensure the value you pass to the component prop currentStatus is a validated
string (or provide an explicit fallback) so the prop never receives undefined.
- Line 58: The list item key currently uses the unstable array index suffix
("key={`${item.productId}-${i}`}"), which can break reconciliation; change it to
a stable identifier such as "key={item.productId}" or "key={item.lineItemId}"
(or whichever unique id exists on the item object) inside the component
rendering the order items so the key relies only on a persistent unique value
(replace occurrences of `${item.productId}-${i}` with the stable id).
- Line 86: The expression new Date(entry.timestamp as unknown as
string).toLocaleString() uses a banned double-cast; remove the cast and ensure
timestamp is safely converted to a string (or a number) before calling Date.
Update the code around entry.timestamp in the page component to handle possible
types (e.g., check typeof, call String(entry.timestamp) or parseInt when
appropriate) and then pass that safe value into new Date(...).toLocaleString();
ensure any nullable cases are guarded so Date receives a valid input.
In `@examples/retail-store/prisma/contract.prisma`:
- Around line 1-4: The Price type (and any other money-related fields currently
declared as Float) should store amounts as integer minor units to avoid rounding
drift: change the amount field in type Price from Float to Int and update all
other money fields that used Float (e.g., unitPrice, price, taxAmount,
totalAmount or similarly named fields) to Int with a clear comment that these
represent minor units (cents). Ensure any application code that reads/writes
these fields converts between decimal currencies and minor units
(multiply/divide by 100 or currency-specific scale) and update any Prisma
input/output handling to use Int for those fields.
- Around line 35-38: Replace the loose String fields with Prisma enums: add
enums (e.g., OrderType and Status) enumerating the allowed values, change the
composite type StatusEntry to declare status as Status, and update the Order
model to declare type as OrderType (change references to StatusEntry.status and
Order.type accordingly). After updating the schema, run prisma generate and
apply any necessary migration steps so the client and database reflect the new
enums; also update any code that constructs or reads Order.type or
StatusEntry.status to use the enum values.
In `@examples/retail-store/src/components/add-to-cart-button.tsx`:
- Line 39: The timeout set in the AddToCartButton (where setTimeout(() =>
setState('idle'), 1500)) can be stale and flip state during a later add; fix by
storing the timeout ID (e.g., in a ref like timeoutRef) and clear any existing
timeout via clearTimeout(timeoutRef.current) before assigning a new one, also
clear it on unmount to avoid leaks so only the most recent success schedules the
state reset.
---
Duplicate comments:
In `@examples/retail-store/app/cart/cart-actions.tsx`:
- Around line 20-23: When building the request URL in cart-actions.tsx, guard
against empty productId when mode === 'remove' by checking productId is
non-empty (e.g., productId.trim() !== '' or Boolean(productId)) before using it
in new URLSearchParams; if productId is empty, treat it as a non-remove
operation (use '/api/cart' or abort) to avoid accidentally invoking the
clearCart path on the API; update the const url construction that references
mode and productId accordingly.
---
Nitpick comments:
In `@examples/retail-store/app/api/products/random/route.ts`:
- Around line 6-13: The current parsing uses Number.parseInt which allows
trailing characters (e.g., "4abc"); change the parsing to use Number(raw) and
validate with Number.isSafeInteger to reject malformed input: compute count =
raw === null ? 4 : Number(raw), then check !Number.isSafeInteger(count) || count
< 1 || count > 24 and respond with the same 400 JSON on failure; update
references to raw and count in this route handler (the random products route)
accordingly.
In `@examples/retail-store/app/cart/cart-actions.tsx`:
- Around line 33-50: The "remove" branch of the CartActions component doesn't
show a loading label—when mode === 'remove' the Button always renders "Remove"
even if loading is true; update the Remove button render (the JSX under if (mode
=== 'remove')) to conditionally display "Removing..." when loading is true
(similar to the Clear button) while keeping the same props (variant="ghost",
size="sm", onClick={handleAction}, disabled={loading},
className="text-destructive") so the labels are consistent across the remove and
clear actions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 4cc7ad0c-d9a2-441d-ab7b-b76bb9669f1d
📒 Files selected for processing (15)
examples/retail-store/app/api/cart/route.tsexamples/retail-store/app/api/orders/route.tsexamples/retail-store/app/api/products/random/route.tsexamples/retail-store/app/cart/cart-actions.tsxexamples/retail-store/app/checkout/checkout-form.tsxexamples/retail-store/app/orders/[id]/order-status-buttons.tsxexamples/retail-store/app/orders/[id]/page.tsxexamples/retail-store/middleware.tsexamples/retail-store/prisma/contract.prismaexamples/retail-store/src/components/add-to-cart-button.tsxexamples/retail-store/src/contract.d.tsexamples/retail-store/src/contract.jsonexamples/retail-store/src/data/object-id-filter.tsexamples/retail-store/src/data/products.tsexamples/retail-store/src/db-singleton.ts
✅ Files skipped from review due to trivial changes (2)
- examples/retail-store/src/contract.json
- examples/retail-store/src/contract.d.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- examples/retail-store/src/db-singleton.ts
- examples/retail-store/src/data/object-id-filter.ts
- examples/retail-store/app/orders/[id]/order-status-buttons.tsx
- examples/retail-store/middleware.ts
- examples/retail-store/app/checkout/checkout-form.tsx
- examples/retail-store/app/api/orders/route.ts
- examples/retail-store/app/api/cart/route.ts
Reset branch to tml-2220-m1-family-migration-spi-vertical-slice which provides the full Mongo framework stack (embedded value objects, ORM CRUD, pipeline builder, migrations). Carry forward the project spec and retail-store plan (revised for the new capabilities) and predictive-maintenance plan.
Add workspace package with Next.js, PN Mongo deps, and config files mirroring mongo-demo. No contract or source files yet.
Define 8 value object types (Price, Image, Address, CartItem, OrderLineItem, StatusEntry, InvoiceLineItem, EventMetadata) and 7 models (Product, User, Cart, Order, Location, Invoice, Event) with nested value objects and reference relations. Extend scalar type descriptors with Float -> mongo/double@1 since the default Mongo PSL interpreter does not include Float.
db.ts exposes orm, runtime, pipeline, raw, and contract. seed.ts populates all 7 collections with realistic e-commerce data. scripts/seed.ts provides a standalone entry point with .env support.
Implement typed data access functions for all 7 collections: products, users, carts, orders, locations, invoices, events. Add objectIdEq helper to handle BSON ObjectId filtering via MongoParamRef, since MongoFieldFilter.eq resolveValue corrupts raw ObjectId instances by treating them as plain objects. Integration tests cover: CRUD lifecycle, embedded value objects (Price, Image, Address, CartItem, OrderLineItem, StatusEntry, InvoiceLineItem, EventMetadata), upsert, clear cart, relation loading via $lookup (cart->user, order->user, invoice->order), and seed data population across all 7 collections.
Implement addToCart ($push), removeFromCart ($pull), and updateOrderStatus ($push to statusHistory) using raw MongoDB commands through mongoRaw. These bypass the ORM to use native array operators not yet supported in the typed ORM surface. Tests verify push/pull on cart items and status history append.
aggregateEventsByType uses $match -> $group -> $sort to count event types per user. getRandomProducts uses $sample to return a random subset from the products collection. Tests verify group counts and sample cardinality.
RESTful API routes backed by the data access layer: - GET/POST/DELETE /api/cart (add/remove items, clear) - GET /api/products, GET /api/products/[id], GET /api/products/random - GET/POST /api/orders, GET/DELETE/PATCH /api/orders/[id] - GET /api/locations db-singleton provides lazy-initialized shared db client.
Server-rendered pages using the data access layer directly: - Product catalog grid (/) with brand, category, and pricing - Product detail (/products/[id]) with full metadata - Cart view (/cart) with line items and total - Order history (/orders) with status badges - Order detail (/orders/[id]) with items and status timeline Uses inline styles for simplicity (no Tailwind dependency).
Add findSimilarProducts via $vectorSearch (requires Atlas). Fix instanceof checks on string params for typecheck. Document demonstrated features and known framework gaps in README.
Document .env configuration, seed workflow, DEMO_USER_ID requirement for cart/orders pages, and optional Atlas vector search setup.
seed() now returns the demo user ID. The seed script writes it into .env automatically, removing the manual mongosh step from the setup workflow.
- F01: fix objectIdEq param type from string|unknown to string|ObjectId - F02: replace as-string casts in seed.ts with String() calls - F03: guard cart/orders API routes against empty DEMO_USER_ID - F04: document why raw commands skip MongoParamRef wrapping - F06: document why findSimilarProducts uses raw aggregate - F07: document migration gap in README - F08: explain standalone biome config in biome.jsonc - F09: type pipeline return values (Product[], EventTypeCount[]) - F10: extract executeRaw/collectResults helpers
Install tailwindcss, @tailwindcss/postcss, class-variance-authority, clsx, tailwind-merge, lucide-react, and Radix UI primitives. Create postcss.config.mjs, Tailwind v4 globals.css with @theme, cn() utility, and 8 UI components (Button, Card, Input, Badge, Select, RadioGroup, Separator, DropdownMenu).
Next.js middleware redirects unauthenticated visitors to /login. Sign Up creates a user doc via ORM and sets a userId cookie. getAuthUser() server helper reads the cookie and fetches the user.
…write Products now span Apparel/Accessories/Footwear in Heritage, UrbanEdge, Craftsman, TrailMark brands. Added 4 store locations. Seed script no longer writes DEMO_USER_ID to .env since auth uses cookies.
…lation, sparse, and compound sort directions
…erivation Nullable embedded types (e.g. Address?) were emitted as bsonType: "object" which rejected null values at runtime. Now emits oneOf: [null, object] so MongoDB validators correctly accept both null and the object shape.
…d nullable validators
Audits the ORM consolidation and schema migrations project plans against actual completion status (Linear tickets + framework limitations from the retail store). Consolidates gaps into 6 work areas with Linear tickets: - Area 1: Type ergonomics (TML-2245) - Area 2: ORM query/mutation ergonomics (TML-2246) - Area 3: Migration planner bugs (TML-2247) - Area 4: Migration system completion (TML-2248) - Area 5: Pipeline result typing (TML-2249) - Area 6: Schema authoring gaps (TML-2250)
Review artifacts under projects/**/reviews/ are useful during development but should not ship in PRs. Added a gitignore rule to prevent re-addition.
The seed writes image URLs like /images/products/her-oxf-001.jpg but no files existed at those paths. Generate 24 flat illustrations (one per product) and place them in public/images/products/ so Next.js serves them as static assets, resolving the fabricated-URL issue (C01).
Replace removed EventMetadata reference with the polymorphic variant structure (ViewProductEvent, SearchEvent, AddToCartEvent). Update framework gaps to reflect that migrations and index support now work (C02).
Extract a lineItemFrom() function that converts an ORM product record into the common cart/order line item fields (productId, name, brand, image). This consolidates 12 String() calls into 4 inside the helper, making the cart and order construction sites cleaner (C03).
UNAUTHORIZED and NOT_FOUND were module-level NextResponse.json() instances, potentially unsafe to reuse across requests. Convert to factory functions that create a fresh Response per invocation (C04).
The order detail page loaded orders by ID without checking the current user, enabling IDOR access to other users orders. Add auth check and ownership verification. Also update middleware to return 401 JSON for unauthenticated API requests instead of redirecting.
fetch only throws on network failures; 4xx/5xx responses silently proceeded as success. Add res.ok checks to add-to-cart, cart remove/ clear, and order status advancement. Also tighten CartActionsProps to a discriminated union so productId is required for remove mode.
Validate count param on random products endpoint (1-24 range). Block BOPIS checkout when no pickup location is selected. Validate ObjectId strings before construction to prevent 500s. Add basic payload validation to cart POST and order POST routes.
…d DB init Change Cart.userId index from @@index to @@unique to enforce one cart per user at the database level. Switch paginated product queries to pipeline builder with _id sort for deterministic page boundaries. Clear cached DB promise on connection failure so subsequent calls can retry.
Add status field validation to PATCH handler — return 400 on invalid JSON or missing status. Fix README to say "login page" consistently and add language tags to fenced code blocks.
Remove prohibited as unknown as string double cast on timestamp, use String() instead. Guard against empty statusHistory with explicit check instead of as string cast. Clear previous timeout on rapid add-to-cart clicks to prevent stale timer from resetting state.
b5b77eb to
8459476
Compare
The test expected a plain @@index but the schema was changed to @@unique([userId]) to enforce one cart per user.
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (1)
packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts (1)
150-151:⚠️ Potential issue | 🟠 MajorMerge custom registry with defaults instead of replacing it.
On Line 151,
codecs ?? defaultCodecRegistry()means any providedcodecsfully replaces built-ins. That can drop standard codec IDs (e.g., ObjectId/string/date codecs) and break normal param-ref encoding when callers only add custom codecs.Run this read-only check to confirm API shape and missing coverage before implementing merge behavior:
#!/bin/bash set -euo pipefail echo "1) Inspect MongoCodecRegistry API surface" rg -n -C3 "export (interface|type) MongoCodecRegistry|createMongoCodecRegistry|register\\(" --type=ts packages echo echo "2) Inspect adapter factory behavior" rg -n -C4 "defaultCodecRegistry|createMongoAdapter\\(|new MongoAdapterImpl" --type=ts packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts echo echo "3) Inspect tests for custom-registry + built-in preservation" rg -n -C4 "createMongoAdapter\\(|codecId|custom codec|objectId|mongo/objectId@1" --type=ts packages/3-mongo-target/2-mongo-adapter/testExpected result: you should confirm whether custom registries are merged or replaced and whether a test exists asserting built-ins still encode when a custom registry is supplied.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts` around lines 150 - 151, The factory createMongoAdapter currently replaces built-in codecs when given a MongoCodecRegistry (codecs ?? defaultCodecRegistry()); change it to merge the provided MongoCodecRegistry into the default registry instead of replacing it: obtain the defaultCodecRegistry(), then copy or register each entry from the provided codecs into that default instance (or use a create/merge helper if one exists) before passing the merged registry into new MongoAdapterImpl; update any tests to assert that builtin codec IDs (e.g., ObjectId/string/date) still exist when a custom registry is supplied and reuse symbols createMongoAdapter, MongoAdapterImpl, defaultCodecRegistry, and MongoCodecRegistry to locate code to modify.
🧹 Nitpick comments (4)
examples/retail-store/src/components/ui/select.tsx (1)
12-71: AddforwardRefto wrapper components for ref forwarding support.
SelectTrigger,SelectContent, andSelectItemprevent ref forwarding by usingComponentPropsWithoutRef. This limits programmatic focus control and ref-based integrations, though the underlying Radix primitives support it. While no current usage attempts ref passing, wrapper components should enable this for forward compatibility and align with React best practices for Radix component wrapping.Wrap all three with
forwardRef<ElementRef<typeof SelectPrimitive.X>, ComponentPropsWithoutRef<typeof SelectPrimitive.X>>and adddisplayNameproperties to match Radix conventions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/src/components/ui/select.tsx` around lines 12 - 71, The three wrapper components SelectTrigger, SelectContent, and SelectItem should forward refs and expose displayName: change each from a plain function to a forwardRef wrapper using forwardRef<ElementRef<typeof SelectPrimitive.Trigger>, ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>> (and the corresponding ElementRef/ComponentPropsWithoutRef types for SelectPrimitive.Content and SelectPrimitive.Item), accept (props, ref) and pass ref into the underlying SelectPrimitive.Trigger/Content/Item, and set SelectTrigger.displayName = 'SelectTrigger' (and likewise for SelectContent and SelectItem) to match Radix conventions.docs/planning/mongo-target/next-steps.md (1)
112-112: Remove URL encoding from relative path.The ADR link uses
%20for spaces, which may not render correctly in all Markdown contexts. Use spaces directly in the relative path:🔗 Proposed fix
-- **FL-04**: Implement dot-path field accessor mutations — `$push`, `$pull`, `$inc`, `$set` on nested paths via `u("field.path")` (deferred Phase 1.5 M4). Maps to [ADR 180](../../architecture%20docs/adrs/ADR%20180%20-%20Dot-path%20field%20accessor.md). +- **FL-04**: Implement dot-path field accessor mutations — `$push`, `$pull`, `$inc`, `$set` on nested paths via `u("field.path")` (deferred Phase 1.5 M4). Maps to [ADR 180](../../architecture docs/adrs/ADR 180 - Dot-path field accessor.md).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/planning/mongo-target/next-steps.md` at line 112, Update the ADR link so it doesn't use URL-encoded spaces (%20); edit the relative path in the FL-04 line that points to "ADR 180" (the existing link to ADR%20180%20-%20Dot-path%20field%20accessor.md) and replace the URL-encoded segments with literal spaces so the link reads the normal filename "ADR 180 - Dot-path field accessor.md" in the relative path.examples/retail-store/test/api-flows.test.ts (1)
85-88: Use object/collection matchers for related assertions.These are related multi-field checks split across several
.toBe()calls. Consolidating improves intent and aligns with test matcher guidance.Suggested matcher consolidation
- expect(aliceOrders[0]!.shippingAddress).toBe('123 Main St'); - expect(bobOrders[0]!.shippingAddress).toBe('456 Oak Ave'); + expect(aliceOrders[0]).toMatchObject({ shippingAddress: '123 Main St' }); + expect(bobOrders[0]).toMatchObject({ shippingAddress: '456 Oak Ave' }); @@ - expect(cart!.items[0]!.productId).toBe('prod-1'); - expect(cart!.items[1]!.productId).toBe('prod-1'); + expect(cart!.items).toMatchObject([{ productId: 'prod-1' }, { productId: 'prod-1' }]);As per coding guidelines, "Prefer object matchers (toMatchObject) over multiple individual expect().toBe() calls when checking 2 or more related values in tests."
Also applies to: 285-287
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/test/api-flows.test.ts` around lines 85 - 88, Replace the multiple per-field assertions on order objects with object/collection matchers: instead of separate expects for aliceOrders[0]!.shippingAddress and bobOrders[0]!.shippingAddress, assert the entire order objects using toMatchObject or toEqual against an object that includes the expected fields (e.g., check aliceOrders and bobOrders lengths as already done, then assert aliceOrders[0] and bobOrders[0] match the expected shape); update both the assertions around aliceOrders/bobOrders (and the similar block at lines ~285-287) to use toMatchObject for clearer, consolidated intent.examples/retail-store/app/cart/page.tsx (1)
37-38: Array index in key is justified here, but consider a stable identifier.The Biome lint flags the array index in the key, but since the same
productIdcan appear multiple times (items are pushed via$push), usingproductIdalone would produce duplicate keys. The composite key${item.productId}-${i}works, but index-based keys can cause rendering issues if items are reordered.If cart items had a unique
_idor insertion timestamp, that would be a more stable key. For this demo, the current approach is acceptable given the data model constraints.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/app/cart/page.tsx` around lines 37 - 38, The key currently uses an array index in the composite key inside the items.map render (key={`${item.productId}-${i}`} on the Card) which can cause rendering issues if items are reordered; replace this with a stable identifier by ensuring each cart item has a unique id at insertion (e.g., add and use item._id or an insertion timestamp like item.insertedAt) and update the map to use that stable field as the Card key, or if you cannot change the data model, persist a generated UUID for each item when pushing into the cart and use that UUID as the key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/planning/mongo-target/next-steps.md`:
- Line 3: Replace the broken external link in the Context sentence that
currently points to projects/mongo-example-apps/framework-limitations.md with an
inline reference to the local "Framework limitations" section (which lists FL-01
through FL-15); specifically update the sentence that begins "The retail store
example app (PR `#327`) stress-tested..." to read something like "...surfaced
framework gaps (documented below in the Framework limitations section)..." so it
no longer references the non-existent file and instead points to the existing
FL-01–FL-15 entries in the same document.
In `@examples/retail-store/app/api/cart/route.ts`:
- Around line 17-26: The request body validation in route handler only checks
productId, name, and price but addToCart expects productId, name, brand, amount,
price, and image; update the validation in the route (before calling getDb and
addToCart) to ensure all six fields are present and valid (e.g., amount is a
positive number, price is numeric) and return a 400 NextResponse.json with a
clear error message listing missing/invalid fields if any; this prevents storing
undefined values when calling addToCart.
In `@examples/retail-store/app/checkout/page.tsx`:
- Line 35: Remove the redundant "as string" type assertions in the view-model
mapping in page.tsx: drop the cast on loc.name and the similar casts in the
mapping block around lines 73–79 so that the fields rely on the ORM-typed
strings (e.g., replace "name: loc.name as string" with "name: loc.name" and
similarly remove "as string" from the other mapped fields). Ensure no other
logic changes are made and the mapped object keys remain the same.
In `@examples/retail-store/src/components/cart-provider.tsx`:
- Around line 41-46: invalidateCart currently calls setCount after the fetch
without checking mountedRef, risking state updates on an unmounted component;
update the invalidateCart callback (function invalidateCart) to check
mountedRef.current before calling setCount inside the .then (and avoid calling
setCount in .catch), and ensure the useCallback dependency list accounts for any
external variables used (e.g., mountedRef and setCount) so the guard is
reliable.
In `@examples/retail-store/test/api-flows.test.ts`:
- Around line 269-273: The test title "API flow: cart add idempotency" mislabels
the behavior being asserted (the test actually verifies duplicate appends create
two line items); update the describe and/or it strings to remove "idempotency"
and use clearer wording such as "API flow: cart duplicate add behavior" or
similar, and adjust the it description (e.g., keep "adding same product twice
creates two separate line items") so the test text matches the asserted
non-idempotent behavior (locate the describe(...) and the it(...) around
setupTestDb('api_flows_cart_idempotency') and change only the human-readable
strings).
- Around line 35-56: The test currently only verifies the order's userId matches
Alice (tautology) but doesn't exercise auth enforcement; update the test to
simulate Bob attempting to retrieve the same order and assert access is denied:
after creating the order with createOrder and getting orderId, call the
retrieval path that enforces auth (e.g., getOrderWithUser or the API/DB method
that accepts a requester context) using Bob's identity or request context and
assert it returns null/throws/returns 403 as your auth contract specifies;
ensure you keep the existing assertions about Alice but add the explicit
Bob-access assertion so the auth-guard is actually validated.
In `@packages/2-mongo-family/5-query-builders/orm/src/collection.ts`:
- Around line 494-498: The return is blindly cast to MongoValue even though
value.map(...) already yields an array compatible with MongoArray/MongoValue;
remove the unnecessary "as unknown as MongoValue" cast so TypeScript can infer
the correct type. Locate the branch that checks field.many and calls
this.#wrapValueObject(item as Record<string, unknown>, voDef) and simply return
the mapped array result without the extra cast (leave the field.many check and
`#wrapValueObject` call unchanged). This removes the unsafe cast and relies on the
existing MongoArray/MongoValue union types for proper typing.
---
Duplicate comments:
In `@packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts`:
- Around line 150-151: The factory createMongoAdapter currently replaces
built-in codecs when given a MongoCodecRegistry (codecs ??
defaultCodecRegistry()); change it to merge the provided MongoCodecRegistry into
the default registry instead of replacing it: obtain the defaultCodecRegistry(),
then copy or register each entry from the provided codecs into that default
instance (or use a create/merge helper if one exists) before passing the merged
registry into new MongoAdapterImpl; update any tests to assert that builtin
codec IDs (e.g., ObjectId/string/date) still exist when a custom registry is
supplied and reuse symbols createMongoAdapter, MongoAdapterImpl,
defaultCodecRegistry, and MongoCodecRegistry to locate code to modify.
---
Nitpick comments:
In `@docs/planning/mongo-target/next-steps.md`:
- Line 112: Update the ADR link so it doesn't use URL-encoded spaces (%20); edit
the relative path in the FL-04 line that points to "ADR 180" (the existing link
to ADR%20180%20-%20Dot-path%20field%20accessor.md) and replace the URL-encoded
segments with literal spaces so the link reads the normal filename "ADR 180 -
Dot-path field accessor.md" in the relative path.
In `@examples/retail-store/app/cart/page.tsx`:
- Around line 37-38: The key currently uses an array index in the composite key
inside the items.map render (key={`${item.productId}-${i}`} on the Card) which
can cause rendering issues if items are reordered; replace this with a stable
identifier by ensuring each cart item has a unique id at insertion (e.g., add
and use item._id or an insertion timestamp like item.insertedAt) and update the
map to use that stable field as the Card key, or if you cannot change the data
model, persist a generated UUID for each item when pushing into the cart and use
that UUID as the key.
In `@examples/retail-store/src/components/ui/select.tsx`:
- Around line 12-71: The three wrapper components SelectTrigger, SelectContent,
and SelectItem should forward refs and expose displayName: change each from a
plain function to a forwardRef wrapper using forwardRef<ElementRef<typeof
SelectPrimitive.Trigger>, ComponentPropsWithoutRef<typeof
SelectPrimitive.Trigger>> (and the corresponding
ElementRef/ComponentPropsWithoutRef types for SelectPrimitive.Content and
SelectPrimitive.Item), accept (props, ref) and pass ref into the underlying
SelectPrimitive.Trigger/Content/Item, and set SelectTrigger.displayName =
'SelectTrigger' (and likewise for SelectContent and SelectItem) to match Radix
conventions.
In `@examples/retail-store/test/api-flows.test.ts`:
- Around line 85-88: Replace the multiple per-field assertions on order objects
with object/collection matchers: instead of separate expects for
aliceOrders[0]!.shippingAddress and bobOrders[0]!.shippingAddress, assert the
entire order objects using toMatchObject or toEqual against an object that
includes the expected fields (e.g., check aliceOrders and bobOrders lengths as
already done, then assert aliceOrders[0] and bobOrders[0] match the expected
shape); update both the assertions around aliceOrders/bobOrders (and the similar
block at lines ~285-287) to use toMatchObject for clearer, consolidated intent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: fe374d4c-28e4-420d-9815-fea7285ea416
⛔ Files ignored due to path filters (28)
examples/retail-store/public/images/products/cra-bag-017.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/cra-blt-019.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/cra-tot-018.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-che-022.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-drs-021.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-jea-020.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-lin-002.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-mer-003.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-oxf-001.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/her-psq-023.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-bea-032.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-duf-031.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-hik-034.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-rai-035.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-sho-030.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/tm-trl-033.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-chi-042.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-den-012.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-jog-013.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-pol-011.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-slp-016.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-snk-015.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-sun-014.jpgis excluded by!**/*.jpgexamples/retail-store/public/images/products/ue-tee-010.jpgis excluded by!**/*.jpgpnpm-lock.yamlis excluded by!**/pnpm-lock.yamlprojects/mongo-example-apps/plans/predictive-maintenance-plan.mdis excluded by!projects/**projects/mongo-example-apps/plans/retail-store-plan.mdis excluded by!projects/**projects/mongo-example-apps/spec.mdis excluded by!projects/**
📒 Files selected for processing (89)
.gitignore.vscode/settings.jsondocs/planning/mongo-target/next-steps.mdexamples/retail-store/.gitignoreexamples/retail-store/README.mdexamples/retail-store/app/api/auth/logout/route.tsexamples/retail-store/app/api/auth/signup/route.tsexamples/retail-store/app/api/cart/count/route.tsexamples/retail-store/app/api/cart/route.tsexamples/retail-store/app/api/locations/route.tsexamples/retail-store/app/api/orders/[id]/route.tsexamples/retail-store/app/api/orders/route.tsexamples/retail-store/app/api/products/[id]/route.tsexamples/retail-store/app/api/products/random/route.tsexamples/retail-store/app/api/products/route.tsexamples/retail-store/app/cart/cart-actions.tsxexamples/retail-store/app/cart/page.tsxexamples/retail-store/app/checkout/checkout-form.tsxexamples/retail-store/app/checkout/page.tsxexamples/retail-store/app/globals.cssexamples/retail-store/app/layout.tsxexamples/retail-store/app/login/page.tsxexamples/retail-store/app/orders/[id]/order-status-buttons.tsxexamples/retail-store/app/orders/[id]/page.tsxexamples/retail-store/app/orders/page.tsxexamples/retail-store/app/page.tsxexamples/retail-store/app/products/[id]/page.tsxexamples/retail-store/biome.jsoncexamples/retail-store/middleware.tsexamples/retail-store/migrations/20260413T0314_migration/migration.jsonexamples/retail-store/migrations/20260413T0314_migration/ops.jsonexamples/retail-store/next.config.jsexamples/retail-store/package.jsonexamples/retail-store/postcss.config.mjsexamples/retail-store/prisma-next.config.tsexamples/retail-store/prisma/contract.prismaexamples/retail-store/scripts/reset-db.tsexamples/retail-store/scripts/seed.tsexamples/retail-store/src/components/add-to-cart-button.tsxexamples/retail-store/src/components/cart-badge.tsxexamples/retail-store/src/components/cart-provider.tsxexamples/retail-store/src/components/navbar-client.tsxexamples/retail-store/src/components/navbar.tsxexamples/retail-store/src/components/ui/badge.tsxexamples/retail-store/src/components/ui/button.tsxexamples/retail-store/src/components/ui/card.tsxexamples/retail-store/src/components/ui/dropdown-menu.tsxexamples/retail-store/src/components/ui/input.tsxexamples/retail-store/src/components/ui/radio-group.tsxexamples/retail-store/src/components/ui/select.tsxexamples/retail-store/src/components/ui/separator.tsxexamples/retail-store/src/contract.d.tsexamples/retail-store/src/contract.jsonexamples/retail-store/src/data/carts.tsexamples/retail-store/src/data/events.tsexamples/retail-store/src/data/execute-raw.tsexamples/retail-store/src/data/invoices.tsexamples/retail-store/src/data/locations.tsexamples/retail-store/src/data/object-id-filter.tsexamples/retail-store/src/data/orders.tsexamples/retail-store/src/data/products.tsexamples/retail-store/src/data/users.tsexamples/retail-store/src/db-singleton.tsexamples/retail-store/src/db.tsexamples/retail-store/src/lib/auth.tsexamples/retail-store/src/lib/utils.tsexamples/retail-store/src/seed.tsexamples/retail-store/test/aggregation.test.tsexamples/retail-store/test/api-flows.test.tsexamples/retail-store/test/cart-lifecycle.test.tsexamples/retail-store/test/crud-lifecycle.test.tsexamples/retail-store/test/migration.test.tsexamples/retail-store/test/order-lifecycle.test.tsexamples/retail-store/test/polymorphism.test.tsexamples/retail-store/test/relations.test.tsexamples/retail-store/test/search.test.tsexamples/retail-store/test/seed.test.tsexamples/retail-store/test/setup.tsexamples/retail-store/test/update-operators.test.tsexamples/retail-store/tsconfig.jsonexamples/retail-store/vitest.config.tspackages/2-mongo-family/2-authoring/contract-psl/src/derive-json-schema.tspackages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.tspackages/2-mongo-family/5-query-builders/orm/src/collection.tspackages/2-mongo-family/5-query-builders/orm/test/collection.test.tspackages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.tspackages/3-mongo-target/2-mongo-adapter/src/resolve-value.tspackages/3-mongo-target/2-mongo-adapter/test/mongo-adapter.test.tspackages/3-mongo-target/2-mongo-adapter/test/resolve-value.test.ts
✅ Files skipped from review due to trivial changes (38)
- .vscode/settings.json
- .gitignore
- examples/retail-store/.gitignore
- examples/retail-store/biome.jsonc
- examples/retail-store/app/api/auth/logout/route.ts
- examples/retail-store/postcss.config.mjs
- examples/retail-store/app/api/locations/route.ts
- examples/retail-store/src/data/locations.ts
- examples/retail-store/src/lib/utils.ts
- examples/retail-store/src/components/cart-badge.tsx
- examples/retail-store/app/api/cart/count/route.ts
- examples/retail-store/src/components/ui/input.tsx
- examples/retail-store/tsconfig.json
- examples/retail-store/src/data/object-id-filter.ts
- examples/retail-store/scripts/reset-db.ts
- examples/retail-store/src/db-singleton.ts
- examples/retail-store/prisma-next.config.ts
- examples/retail-store/src/components/ui/badge.tsx
- examples/retail-store/src/components/ui/separator.tsx
- examples/retail-store/test/relations.test.ts
- examples/retail-store/src/components/ui/dropdown-menu.tsx
- packages/3-mongo-target/2-mongo-adapter/test/resolve-value.test.ts
- examples/retail-store/app/api/orders/route.ts
- examples/retail-store/test/search.test.ts
- examples/retail-store/README.md
- examples/retail-store/src/data/invoices.ts
- examples/retail-store/package.json
- examples/retail-store/migrations/20260413T0314_migration/ops.json
- examples/retail-store/next.config.js
- examples/retail-store/src/contract.json
- examples/retail-store/test/order-lifecycle.test.ts
- examples/retail-store/test/crud-lifecycle.test.ts
- examples/retail-store/migrations/20260413T0314_migration/migration.json
- examples/retail-store/test/update-operators.test.ts
- examples/retail-store/src/contract.d.ts
- examples/retail-store/src/components/ui/card.tsx
- examples/retail-store/src/data/users.ts
- examples/retail-store/prisma/contract.prisma
🚧 Files skipped from review as they are similar to previous changes (32)
- examples/retail-store/app/api/products/route.ts
- examples/retail-store/app/api/products/random/route.ts
- examples/retail-store/app/api/auth/signup/route.ts
- examples/retail-store/vitest.config.ts
- examples/retail-store/app/orders/[id]/order-status-buttons.tsx
- examples/retail-store/src/components/navbar-client.tsx
- examples/retail-store/app/layout.tsx
- examples/retail-store/src/components/navbar.tsx
- packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
- examples/retail-store/middleware.ts
- packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts
- packages/2-mongo-family/2-authoring/contract-psl/src/derive-json-schema.ts
- packages/3-mongo-target/2-mongo-adapter/test/mongo-adapter.test.ts
- packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts
- examples/retail-store/app/login/page.tsx
- examples/retail-store/test/cart-lifecycle.test.ts
- examples/retail-store/app/cart/cart-actions.tsx
- examples/retail-store/test/migration.test.ts
- examples/retail-store/app/products/[id]/page.tsx
- examples/retail-store/test/polymorphism.test.ts
- examples/retail-store/scripts/seed.ts
- examples/retail-store/src/db.ts
- examples/retail-store/test/seed.test.ts
- examples/retail-store/src/components/add-to-cart-button.tsx
- examples/retail-store/src/components/ui/radio-group.tsx
- examples/retail-store/app/page.tsx
- examples/retail-store/app/checkout/checkout-form.tsx
- examples/retail-store/src/seed.ts
- examples/retail-store/app/orders/page.tsx
- examples/retail-store/src/data/carts.ts
- examples/retail-store/src/lib/auth.ts
- examples/retail-store/test/aggregation.test.ts
Validate all 6 required cart fields (not just 3). Replace as-string casts with String() in checkout page. Add mountedRef guard to invalidateCart callback. Strengthen auth guard test to verify Bob cannot see Alice order list. Rename misleading "idempotency" describe.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
examples/retail-store/test/api-flows.test.ts (1)
271-289: Naming correctly reflects the asserted behavior.The rename from "idempotency" to "duplicate cart add behavior" accurately describes what's being tested (two separate line items created). The assertions at lines 287-288 could optionally be consolidated:
♻️ Optional: consolidate productId assertions
const cart = await getCartByUserId(ctx.db, userId); expect(cart!.items).toHaveLength(2); - expect(cart!.items[0]!.productId).toBe('prod-1'); - expect(cart!.items[1]!.productId).toBe('prod-1'); + expect(cart!.items.map((i) => i.productId)).toEqual(['prod-1', 'prod-1']); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/retail-store/test/api-flows.test.ts` around lines 271 - 289, Test name and behavior are fine; simplify the two separate productId assertions by asserting the array of productIds instead of two index checks. In the 'API flow: duplicate cart add behavior' test that calls addToCart(ctx.db, userId, ITEM_A) twice and then getCartByUserId(ctx.db, userId), replace the two expects on cart!.items[0]!.productId and cart!.items[1]!.productId with a single assertion that the list of productIds from cart!.items equals ['prod-1','prod-1'] (derive the expected value from ITEM_A if needed) to make the intent concise and reduce brittle index-based checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@examples/retail-store/test/api-flows.test.ts`:
- Around line 271-289: Test name and behavior are fine; simplify the two
separate productId assertions by asserting the array of productIds instead of
two index checks. In the 'API flow: duplicate cart add behavior' test that calls
addToCart(ctx.db, userId, ITEM_A) twice and then getCartByUserId(ctx.db,
userId), replace the two expects on cart!.items[0]!.productId and
cart!.items[1]!.productId with a single assertion that the list of productIds
from cart!.items equals ['prod-1','prod-1'] (derive the expected value from
ITEM_A if needed) to make the intent concise and reduce brittle index-based
checks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 440a108b-8310-4a25-8fce-05786f62a322
📒 Files selected for processing (4)
examples/retail-store/app/api/cart/route.tsexamples/retail-store/app/checkout/page.tsxexamples/retail-store/src/components/cart-provider.tsxexamples/retail-store/test/api-flows.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- examples/retail-store/src/components/cart-provider.tsx
- examples/retail-store/app/api/cart/route.ts
…artifacts Extract consolidated framework limitations from code reviews into a standalone document at projects/mongo-example-apps/framework-limitations.md. Covers type ergonomics (FL-01–FL-03), query capabilities (FL-04–FL-08), schema/migration gaps (FL-09–FL-12), and missing capabilities (FL-13–FL-15). Tracks 6 items addressed by this branch (FL-A1–FL-A6). Also commits the PR #327 review artifacts (system-design-review, code-review, walkthrough), the round 2 spec, and the round 2 plan.
…artifacts Extract consolidated framework limitations from code reviews into a standalone document at projects/mongo-example-apps/framework-limitations.md. Covers type ergonomics (FL-01–FL-03), query capabilities (FL-04–FL-08), schema/migration gaps (FL-09–FL-12), and missing capabilities (FL-13–FL-15). Tracks 6 items addressed by this branch (FL-A1–FL-A6). Also commits the PR #327 review artifacts (system-design-review, code-review, walkthrough), the round 2 spec, and the round 2 plan.
closes TML-2185
Intent
Build a working interactive e-commerce application — the "retail store" — that validates Prisma Next's MongoDB support against a real-world data model. The app exercises embedded value objects, referenced relations, polymorphic types, array update operators, aggregation pipelines, multi-field search, pagination, schema indexes, and migration artifacts. Along the way, fix the framework bugs discovered during development.
The story
Define the domain in PSL — A contract with 7 models, 3 polymorphic variants, and 8 embedded value object types establishes the retail domain: products, users, carts, orders, invoices, locations, and events. The PSL contract uses
@@discriminator/@@basefor polymorphic events,@@textIndexwith weights, compound/hashed/TTL/sparse/collation-aware indexes, and@uniquefor email.Build a typed data access layer — One module per collection under
src/data/wraps all database operations in typed functions that accept aDbhandle and return typed results. ORM CRUD for standard operations,mongoRawfor array update operators ($push/$pull), pipeline builder for aggregation, and$regexfor search. No raw MongoDB calls leak outside this layer.Make it interactive — Cookie-based auth (signup creates a user, sets a
userIdcookie), product catalog with pagination and search, add-to-cart with live badge updates, checkout with home delivery vs. BOPIS (store location picker), order management with status progression. Built with Next.js App Router, Tailwind CSS v4, and shadcn-style UI components.Fix the framework to make it work — Four framework bugs discovered during development were fixed in their proper packages: ORM codec attachment on mutations, adapter codec encoding via registry, nullable value object validator derivation, and optional codec.encode guard.
Validate with tests — 12 test files covering CRUD lifecycle, relations, polymorphism, aggregation, search, cart/order lifecycle, API-level flows, migration/indexes, and seeding. All tests run against
mongodb-memory-server.Behavior changes & evidence
Adds a complete e-commerce example app
Adds a Next.js retail store application under
examples/retail-store/that demonstrates the full range of PN's MongoDB capabilities through an interactive storefront.ORM mutations now encode values through the codec registry
Before:
MongoParamRefinstances created by the ORM carried no codec information. String-typed ObjectId fields were sent as plain strings, causing type mismatches when MongoDB expected BSON ObjectIds.After: The ORM's
#toDocument()and#toSetFields()look up each field'scodecIdfrom the contract and attach it toMongoParamRef. The adapter'sresolveValue()checks forcodecIdand callscodec.encode()before sending to the wire.#wrapFieldValue(),#wrapValueObject(),#modelFields()Nullable value object fields produce correct $jsonSchema validators
Before:
address Address?produced a required-object validator, rejectingnullvalues.After: Nullable value object fields produce
oneOf: [{ bsonType: "null" }, { bsonType: "object", ... }].Schema indexes authored in PSL flow through to migration operations
11 index definitions across 7 collections, including text search with weights, TTL, hashed, sparse, collation, and compound sort directions.
Polymorphic events via @@discriminator/@@base
Eventcollection with 3 variant models (ViewProductEvent,SearchEvent,AddToCartEvent). Tested: variant creation, base queries, discriminator filtering.Framework limitations documented
15 framework gaps surfaced by the app are cataloged in projects/mongo-example-apps/framework-limitations.md and tracked as Linear tickets (TML-2245 through TML-2250). A next-steps plan sequences the follow-up work.
Compatibility / migration / risk
MongoParamRefcarries acodecId. Existing code is unaffected.createMongoAdapter()signature: Now accepts an optionalMongoCodecRegistryparameter. Non-breaking — existing callers get a default registry.ops.jsonincludes incorrect collection creation for polymorphic variants. Tracked as TML-2247.Follow-ups
All tracked in the next-steps plan:
Summary by CodeRabbit
New Features
Documentation
Tests
Chores