diff --git a/.gitignore b/.gitignore
index 889715250e..c4330741dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,9 @@ CLAUDE.local.md
**/reviews/code-review.md
**/reviews/walkthrough.md
+# Local review artifacts
+projects/**/reviews/
+
# Generated contract files in test fixtures
test/**/fixtures/generated/
!test/e2e/framework/test/fixtures/generated/
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2a296e4b6c..fad3077d66 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -8,5 +8,8 @@
"files.associations": {
"**/packages/0-config/tsconfig/base.json": "jsonc",
"**/packages/0-config/tsconfig/prod.json": "jsonc"
+ },
+ "[json]": {
+ "editor.defaultFormatter": "biomejs.biome"
}
}
diff --git a/docs/planning/mongo-target/next-steps.md b/docs/planning/mongo-target/next-steps.md
new file mode 100644
index 0000000000..5e3156ad61
--- /dev/null
+++ b/docs/planning/mongo-target/next-steps.md
@@ -0,0 +1,225 @@
+# MongoDB Workstream — Next Steps
+
+**Context**: The retail store example app (PR #327) stress-tested the Mongo target end-to-end and surfaced framework gaps documented in [projects/mongo-example-apps/framework-limitations.md](../../../projects/mongo-example-apps/framework-limitations.md). This plan consolidates those gaps with incomplete items from the [ORM consolidation](../../../projects/orm-consolidation/plan.md) and [schema migrations](../../../projects/mongo-schema-migrations/plan.md) project plans into a sequenced set of work areas.
+
+**Linear project**: [WS4: MongoDB & Cross-Family Architecture](https://linear.app/prisma-company/project/ws4-mongodb-and-cross-family-architecture-89d4dcdbcd9a)
+
+---
+
+## Status snapshot (as of 2026-04-13)
+
+### ORM Consolidation
+
+| Phase | Status | Linear | Notes |
+|---|---|---|---|
+| Phase 1 (Mongo Collection spike) | Done | TML-2189 | |
+| Phase 1.5 M1-M3 (write ops, demo, tests) | Done | TML-2194 | |
+| Phase 1.5 M4 (dot-path mutations `$push`/`$pull`/`$inc`) | **Deferred** | — | No ticket. Maps to FL-04. |
+| Phase 1.5 M5 (unified query plan) | Done | | |
+| Phase 1.6 (codec-owned value serialization) | Done | TML-2202 | |
+| Phase 1.75a (typed JSON simplification) | Done | TML-2204 | Sub-task TML-2229 (no-emit path) still open, Urgent. |
+| Phase 1.75b (polymorphism) | Done | TML-2205, TML-2227 | |
+| Phase 1.75c (value objects & embedded docs) | Done | TML-2206 | `@@owner` in PSL not implemented (FL-12). |
+| Phase 2 (shared interface extraction) | Not started | TML-2213 | Assigned to Alexey. |
+| Phase 2.5 (pipeline builder) | Done | M9 milestone | |
+| Layering reorganization | Done | TML-2201 | |
+
+### Schema Migrations
+
+| Milestone | Status | Linear | Notes |
+|---|---|---|---|
+| M1 tasks 1.1–1.8 (SPI, types, IR, DDL, planner) | Done | TML-2220 | |
+| M1 tasks 1.9–1.13 (runner, marker, wiring, E2E, demo) | **Not done** | — | Runner and target wiring never implemented. |
+| M2 (full vocabulary + validators + PSL) | Done per Linear | TML-2231 | Has bugs: FL-09, FL-10, FL-11. |
+| M3 (polymorphic indexes) | Not started | TML-2232 | |
+| M4 (online CLI + live introspection) | In Progress | TML-2233 | |
+| Data migrations | Not started | TML-2219 | Depends on WS1 (Saevar). |
+| Manual migrations | Not started | TML-2244 | Depends on WS1 (Saevar). |
+
+### Example Apps (M10)
+
+| App | Status | Linear | Notes |
+|---|---|---|---|
+| Retail store | Merging | TML-2185 | PR #327 ready to merge. |
+| Predictive maintenance | Not started | TML-2186 | Blocked on framework gaps. |
+
+### Closeout Backlog
+
+| Item | Status | Linear | Priority |
+|---|---|---|---|
+| No-emit path parameterized types | To-do | TML-2229 | Urgent |
+| DML visitor pattern | To-do | TML-2234 | Low |
+| Close out pipeline builder project | Backlog | TML-2236 | |
+| Close out ORM consolidation project | Backlog | TML-2237 | |
+
+---
+
+## Framework limitations discovered by retail store
+
+Full details in [projects/mongo-example-apps/framework-limitations.md](../../../projects/mongo-example-apps/framework-limitations.md).
+
+| ID | Issue | Area |
+|---|---|---|
+| FL-01 | Scalar codec output types not assignable to `string`/`number` | Type ergonomics |
+| FL-02 | `_id` codec output type not assignable to `string` | Type ergonomics |
+| FL-03 | Timestamp codec type incompatible with `Date`/`string` | Type ergonomics |
+| FL-04 | No typed `$push`/`$pull`/`$inc` | ORM mutations |
+| FL-05 | Pipeline/raw results untyped | Query results |
+| FL-06 | ObjectId filter requires manual `MongoParamRef` wrapping | ORM queries |
+| FL-07 | No `$vectorSearch` in pipeline builder | Extension (deferred) |
+| FL-08 | 1:N back-relation loading not available/tested | ORM queries |
+| FL-09 | Migration planner creates separate collections for variants | Migration bugs |
+| FL-10 | Variant collection validators incomplete | Migration bugs |
+| FL-11 | `$jsonSchema` drops Float fields | Migration bugs |
+| FL-12 | Embedded models via `@@owner` not in PSL | Schema authoring |
+| FL-13 | TS DSL for Mongo not available | Schema authoring (WS2) |
+| FL-14 | Change streams | Future (WS3 VP5) |
+| FL-15 | Atlas Search requires extension pack | Future |
+
+---
+
+## Work areas (sequenced)
+
+### Area 1: Mongo type ergonomics (FL-01, FL-02, FL-03, TML-2229)
+
+**Linear**: [TML-2245](https://linear.app/prisma-company/issue/TML-2245)
+
+**Priority: Highest.** ~60 type casts in the retail store. Every ORM-to-application boundary is broken.
+
+**Root cause**: The codec type map resolves `mongo/string@1`, `mongo/objectId@1`, and `mongo/dateTime@1` to opaque branded types instead of their underlying TypeScript primitives. The runtime values *are* the expected primitives — the types don't reflect that.
+
+**Scope**:
+
+- Fix Mongo scalar codec output types (`mongo/string@1` → `string`, `mongo/int32@1` → `number`, etc.)
+- Fix `mongo/objectId@1` output type to be assignable to `string`
+- Fix `mongo/dateTime@1` output type to be assignable to `Date` (or `string`, depending on what the codec actually returns at runtime)
+- Complete TML-2229: restore parameterized output types in the no-emit path
+
+**Proof**: The retail store compiles with zero `as string` / `as unknown as string` / `String()` casts on ORM results.
+
+**Depends on**: Nothing.
+
+**Blocks**: Area 2 benefits from this (fewer workarounds in where clauses).
+
+### Area 2: ORM query and mutation ergonomics (FL-04, FL-06, FL-08)
+
+**Linear**: [TML-2246](https://linear.app/prisma-company/issue/TML-2246)
+
+**Priority: High.** Users drop to `mongoRaw` for the most common mutation patterns.
+
+**Scope**:
+
+- **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-06**: ORM `where()` should auto-encode ObjectId-typed fields. When a contract field has `codecId: 'mongo/objectId@1'`, the ORM should wrap the value in `MongoParamRef` automatically instead of requiring the user to construct it manually.
+- **FL-08**: Validate and test 1:N back-relation loading via `include()`. If it works, add test coverage. If it doesn't, implement it.
+
+**Proof**: The retail store's `mongoRaw` calls for cart add/remove and order status update are replaced with ORM `update()` calls. ObjectId filter helpers (`objectIdEq()`) are removed.
+
+**Depends on**: Area 1 (type fixes reduce noise, but not a hard blocker).
+
+### Area 3: Migration planner bugs (FL-09, FL-10, FL-11)
+
+**Linear**: [TML-2247](https://linear.app/prisma-company/issue/TML-2247)
+
+**Priority: High.** Bugs in completed M2 work that produce incorrect migration operations.
+
+**Scope**:
+
+- **FL-09 + FL-10**: Fix polymorphic variant collection handling. Variant models that share their base model's collection (via `@@map` or implicit STI) must not produce separate `createCollection` operations. Validators for the shared collection must include all base + variant fields, not just variant-specific fields.
+- **FL-11**: Fix `$jsonSchema` derivation to recognize `Float` scalar type. Fields typed as `Float` should produce `{ bsonType: "double" }` in the validator.
+
+**Proof**: Running `migration plan` on the retail store contract produces correct operations — no spurious variant collections, validators include all fields, Float fields are present.
+
+**Depends on**: Nothing.
+
+**Blocks**: Area 4 (M3 polymorphic indexes depend on correct variant handling).
+
+### Area 4: Migration system completion (M1 runner + wiring, M3, M4)
+
+**Linear**: [TML-2248](https://linear.app/prisma-company/issue/TML-2248)
+
+**Priority: Medium.** The planner generates operations but they can't be applied.
+
+**Scope**:
+
+- **M1 tasks 1.9–1.11**: Implement `MongoMigrationRunner` (three-phase loop: precheck → execute → postcheck), marker/ledger in `_prisma_migrations` collection, wire Mongo target descriptor.
+- **M1 tasks 1.12–1.13**: End-to-end proof (plan + apply single index against `mongodb-memory-server`), add migrations to example app.
+- **M3** (TML-2232): Polymorphic partial index derivation — auto-generate `partialFilterExpression` scoped to discriminator values for variant-specific indexes.
+- **M4** (TML-2233, already in progress): Online CLI commands + live introspection — `db init`, `db update`, `db verify`, `db sign`, `db schema`, `migration status --db`, `migration show`.
+
+**Note**: Data migrations (TML-2219) and manual migrations (TML-2244) depend on WS1 (Saevar's migration system work on VP1/VP2). They are not sequenced here.
+
+**Proof**: `migration plan` + `migration apply` works end-to-end against a real MongoDB instance for the retail store contract. Polymorphic collections get partial indexes.
+
+**Depends on**: Area 3 (bug fixes must land first).
+
+### Area 5: Pipeline and raw query result typing (FL-05)
+
+**Linear**: [TML-2249](https://linear.app/prisma-company/issue/TML-2249)
+
+**Priority: Medium.** All pipeline/raw results are `unknown`, requiring manual type assertions.
+
+**Scope**:
+
+- Pipeline builder `build()` should produce a `MongoQueryPlan` that carries the inferred result type from the document shape tracking.
+- `runtime.execute()` should propagate the result type from the query plan.
+- `rawPipeline()` remains untyped (user asserts the return type) but should accept a type parameter for the result.
+
+**Proof**: A pipeline builder chain like `.match(...).group(...).build()` produces a plan whose execution returns typed results without `as` casts.
+
+**Depends on**: Nothing (can run in parallel with Areas 1-4).
+
+### Area 6: Schema authoring gaps (FL-12, FL-13)
+
+**Linear**: [TML-2250](https://linear.app/prisma-company/issue/TML-2250)
+
+**Priority: Lower.** Missing PSL features that limit what the example app can demonstrate.
+
+**Scope**:
+
+- **FL-12**: Add `@@owner` attribute to the Mongo PSL interpreter, enabling embedded entity declarations (as distinct from value objects). The contract schema and emitter already support `owner` — the gap is only in PSL authoring and ORM embedded entity CRUD.
+- **FL-13**: TypeScript DSL for Mongo contract authoring. This depends on WS2 (Alberto's contract authoring workstream) delivering the new TS authoring surface. Not actionable until that surface exists.
+
+**Proof (FL-12)**: A PSL schema can declare `model Comment { ... @@owner(Post) }` and the emitter produces a contract with the correct owner/embed relation.
+
+**Depends on**: FL-13 depends on WS2.
+
+---
+
+## Sequencing
+
+```
+ ┌─ 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
+ │
+ ▼
+ TML-2186 (predictive maintenance app)
+ │
+ ▼
+ Shared ORM interface (M8, with Alexey)
+```
+
+- **Areas 1 and 3** are independent and can start immediately in parallel.
+- **Area 2** benefits from Area 1 landing but is not blocked by it.
+- **Area 4** is blocked by Area 3 (migration bugs must be fixed first).
+- **Areas 5 and 6** are independent and can run in parallel with everything.
+- **TML-2186** (predictive maintenance app) validates fixes end-to-end; sequence after Areas 1-3.
+- **M8** (shared ORM interface, Alexey) remains the final phase.
+
+---
+
+## Deferred (not in this sequence)
+
+| Item | Reason |
+|---|---|
+| FL-07 ($vectorSearch) | Requires Atlas extension pack — separate project. |
+| FL-14 (change streams) | Requires streaming subscription support (WS3 VP5). |
+| FL-15 (Atlas Search) | Requires extension pack — separate project. |
+| TML-2234 (DML visitor pattern) | Low-priority cleanup. |
+| TML-2219 (data migrations) | Depends on WS1 VP1 (Saevar). |
+| TML-2244 (manual migrations) | Depends on WS1 VP2 (Saevar). |
diff --git a/examples/retail-store/.gitignore b/examples/retail-store/.gitignore
new file mode 100644
index 0000000000..d56a7011bc
--- /dev/null
+++ b/examples/retail-store/.gitignore
@@ -0,0 +1,2 @@
+next-env.d.ts
+.next
diff --git a/examples/retail-store/README.md b/examples/retail-store/README.md
new file mode 100644
index 0000000000..7e824cfb62
--- /dev/null
+++ b/examples/retail-store/README.md
@@ -0,0 +1,117 @@
+# Retail Store — Prisma Next MongoDB Example
+
+An interactive e-commerce example application demonstrating Prisma Next's MongoDB capabilities with a Next.js frontend.
+
+## What This Demonstrates
+
+| Feature | Implementation |
+|---|---|
+| **PSL contract with embedded value objects** | 8 `type` definitions (Price, Image, Address, CartItem, etc.) nested inside 7 models |
+| **ORM CRUD** | `create`, `createAll`, `update`, `delete`, `upsert` via `@prisma-next/mongo-orm` |
+| **Reference relations** | `include('user')` compiles to `$lookup` for cart→user, order→user, invoice→order |
+| **Array update operators** | `$push`/`$pull` via `mongoRaw` for cart items and order status history |
+| **Aggregation pipelines** | `$match`→`$group`→`$sort` for event analytics, `$sample` for random products |
+| **Pipeline search** | `$regex` via `MongoOrExpr` + `MongoFieldFilter.of` for multi-field text search |
+| **Pagination** | ORM `.skip(n).take(n)` for paginated product listing |
+| **Vector search** | `findSimilarProducts` via `$vectorSearch` (requires Atlas cluster) |
+| **Cookie-based auth** | Next.js middleware + signup/logout API routes with `userId` cookie |
+| **Interactive cart** | Add to Cart, remove, clear — all backed by PN data layer + CartProvider context |
+| **Checkout flow** | Home delivery vs BOPIS (Buy Online, Pick Up In Store) with location picker |
+| **Order lifecycle** | Status progression (placed → shipped → delivered) via `$push` status entries |
+| **Next.js integration** | Server-rendered pages, client components, REST API routes, Tailwind CSS v4 |
+
+## Quick Start (tests only — no external DB)
+
+```bash
+# 1. Build framework packages (from repo root)
+pnpm build
+
+# 2. Emit contract
+pnpm emit
+
+# 3. Run tests (uses mongodb-memory-server)
+pnpm test
+```
+
+## Running with a Remote MongoDB Instance
+
+To run the full app (UI + API) against a real MongoDB cluster:
+
+**1. Create `.env` in `examples/retail-store/`:**
+
+```env
+DB_URL=mongodb+srv://user:pass@your-cluster.mongodb.net
+MONGODB_DB=retail-store
+```
+
+**2. Seed the database:**
+
+```bash
+pnpm db:seed
+```
+
+This populates all 7 collections with 24 products across 5 brands, 4 store locations, and sample users/orders/events.
+
+**3. Start the dev server:**
+
+```bash
+pnpm dev
+```
+
+Open [http://localhost:3000](http://localhost:3000). You'll be redirected to the login page — click "Sign Up" to create a user and start browsing.
+
+### User Flow
+
+1. **Sign Up** → creates a user doc via ORM, sets `userId` cookie
+2. **Browse** → paginated product catalog (8 per page), search bar
+3. **Add to Cart** → click on a product, "Add to Cart" button
+4. **Cart** → view items, remove individual items, clear cart
+5. **Checkout** → choose home delivery or store pickup, place order
+6. **Orders** → view order history, advance status (placed → shipped → delivered)
+7. **Log Out** → clears cookie, redirects to sign-up
+
+### Vector Search (optional, Atlas only)
+
+To test `findSimilarProducts`, create a vector search index named `product_embedding_index` on the `products` collection's `embedding` field in Atlas, and populate the `embedding` arrays with actual vectors (the seed data sets `embedding: null` by default).
+
+## Domain Model
+
+```text
+Products ─── Price, Image (embedded value objects)
+Users ─── Address? (optional embedded)
+Carts ──→ User (reference relation), CartItem[] (embedded array)
+Orders ──→ User (reference relation), OrderLineItem[], StatusEntry[]
+Locations ─── flat fields
+Invoices ──→ Order (reference relation), InvoiceLineItem[]
+Events ─── polymorphic (@@discriminator on type)
+ ├── ViewProductEvent (productId, subCategory, brand)
+ ├── SearchEvent (query)
+ └── AddToCartEvent (productId, brand)
+```
+
+## Project Structure
+
+```text
+prisma/contract.prisma PSL schema with types and models
+src/contract.json Generated contract (machine-readable)
+src/contract.d.ts Generated types (compile-time safety)
+src/db.ts Database factory (orm, runtime, pipeline, raw)
+src/seed.ts Seed data for all 7 collections
+src/data/ Data access layer (typed functions per collection)
+src/lib/auth.ts Server-side auth helper (cookie-based)
+src/lib/utils.ts cn() utility for Tailwind class merging
+src/components/ Navbar, CartProvider, AddToCartButton, UI primitives
+test/ Integration tests against mongodb-memory-server
+app/ Next.js App Router (pages + API routes)
+middleware.ts Auth middleware (redirects to /login)
+```
+
+## Framework Gaps
+
+- **Float scalar type**: Not in default Mongo PSL scalar descriptors; added via custom `scalarTypeDescriptors` in config
+- **ObjectId in filters**: `MongoFieldFilter.eq` with ObjectId values requires wrapping in `MongoParamRef` (see `src/data/object-id-filter.ts`)
+- **Schema migrations**: Migration artifacts are committed under `migrations/`; run `pnpm migration:apply` to apply. The planner handles indexes and collection validators but not all schema-level operations.
+- **Typed `$push`/`$pull`**: ORM doesn't expose array update operators; use `mongoRaw` with untyped commands
+- **Pipeline output types**: The pipeline builder doesn't propagate output types through aggregation stages; results are cast to expected shapes at the call site
+- **Atlas Search**: Requires extension pack not yet available
+- **Change Streams**: Not yet supported in the framework
diff --git a/examples/retail-store/app/api/auth/logout/route.ts b/examples/retail-store/app/api/auth/logout/route.ts
new file mode 100644
index 0000000000..549ea74e08
--- /dev/null
+++ b/examples/retail-store/app/api/auth/logout/route.ts
@@ -0,0 +1,8 @@
+import { cookies } from 'next/headers';
+import { NextResponse } from 'next/server';
+
+export async function POST() {
+ const cookieStore = await cookies();
+ cookieStore.delete('userId');
+ return NextResponse.json({ ok: true });
+}
diff --git a/examples/retail-store/app/api/auth/signup/route.ts b/examples/retail-store/app/api/auth/signup/route.ts
new file mode 100644
index 0000000000..9e3c0a2ff3
--- /dev/null
+++ b/examples/retail-store/app/api/auth/signup/route.ts
@@ -0,0 +1,24 @@
+import { cookies } from 'next/headers';
+import { NextResponse } from 'next/server';
+import { createUser } from '../../../../src/data/users';
+import { getDb } from '../../../../src/db-singleton';
+
+export async function POST() {
+ const db = await getDb();
+ const shortId = Math.random().toString(36).slice(2, 8);
+ const user = await createUser(db, {
+ name: `User-${shortId}`,
+ email: `user-${shortId}@demo.local`,
+ address: null,
+ });
+
+ const cookieStore = await cookies();
+ cookieStore.set('userId', String(user._id), {
+ path: '/',
+ httpOnly: true,
+ sameSite: 'lax',
+ maxAge: 60 * 60 * 24 * 30,
+ });
+
+ return NextResponse.json({ id: String(user._id), name: user.name });
+}
diff --git a/examples/retail-store/app/api/cart/count/route.ts b/examples/retail-store/app/api/cart/count/route.ts
new file mode 100644
index 0000000000..e4bd227678
--- /dev/null
+++ b/examples/retail-store/app/api/cart/count/route.ts
@@ -0,0 +1,13 @@
+import { NextResponse } from 'next/server';
+import { getCartByUserId } from '../../../../src/data/carts';
+import { getDb } from '../../../../src/db-singleton';
+import { getAuthUserId } from '../../../../src/lib/auth';
+
+export async function GET() {
+ const userId = await getAuthUserId();
+ if (!userId) return NextResponse.json({ count: 0 });
+ const db = await getDb();
+ const cart = await getCartByUserId(db, userId);
+ const count = cart?.items.reduce((sum, item) => sum + item.amount, 0) ?? 0;
+ return NextResponse.json({ count });
+}
diff --git a/examples/retail-store/app/api/cart/route.ts b/examples/retail-store/app/api/cart/route.ts
new file mode 100644
index 0000000000..9789f25e4b
--- /dev/null
+++ b/examples/retail-store/app/api/cart/route.ts
@@ -0,0 +1,47 @@
+import { NextResponse } from 'next/server';
+import { addToCart, clearCart, getCartByUserId, removeFromCart } from '../../../src/data/carts';
+import { getDb } from '../../../src/db-singleton';
+import { getAuthUserId } from '../../../src/lib/auth';
+
+export async function GET() {
+ const userId = await getAuthUserId();
+ if (!userId) return NextResponse.json({ items: [] }, { status: 401 });
+ const db = await getDb();
+ const cart = await getCartByUserId(db, userId);
+ return NextResponse.json(cart ?? { items: [] });
+}
+
+export async function POST(req: Request) {
+ const userId = await getAuthUserId();
+ if (!userId) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
+ const body = await req.json();
+ if (!body.productId || !body.name || !body.brand || !body.amount || !body.price || !body.image) {
+ return NextResponse.json(
+ { error: 'Missing required fields: productId, name, brand, amount, price, image' },
+ { status: 400 },
+ );
+ }
+ const db = await getDb();
+
+ await addToCart(db, userId, body);
+
+ const cart = await getCartByUserId(db, userId);
+ return NextResponse.json(cart ?? { items: [] });
+}
+
+export async function DELETE(req: Request) {
+ const userId = await getAuthUserId();
+ if (!userId) return NextResponse.json({ items: [] }, { status: 401 });
+ const { searchParams } = new URL(req.url);
+ const productId = searchParams.get('productId');
+ const db = await getDb();
+
+ if (productId) {
+ await removeFromCart(db, userId, productId);
+ } else {
+ await clearCart(db, userId);
+ }
+
+ const cart = await getCartByUserId(db, userId);
+ return NextResponse.json(cart ?? { items: [] });
+}
diff --git a/examples/retail-store/app/api/locations/route.ts b/examples/retail-store/app/api/locations/route.ts
new file mode 100644
index 0000000000..de10679eac
--- /dev/null
+++ b/examples/retail-store/app/api/locations/route.ts
@@ -0,0 +1,9 @@
+import { NextResponse } from 'next/server';
+import { findLocations } from '../../../src/data/locations';
+import { getDb } from '../../../src/db-singleton';
+
+export async function GET() {
+ const db = await getDb();
+ const locations = await findLocations(db);
+ return NextResponse.json(locations);
+}
diff --git a/examples/retail-store/app/api/orders/[id]/route.ts b/examples/retail-store/app/api/orders/[id]/route.ts
new file mode 100644
index 0000000000..522af3b998
--- /dev/null
+++ b/examples/retail-store/app/api/orders/[id]/route.ts
@@ -0,0 +1,58 @@
+import { NextResponse } from 'next/server';
+import {
+ deleteOrder,
+ getOrderById,
+ getOrderWithUser,
+ updateOrderStatus,
+} from '../../../../src/data/orders';
+import { getDb } from '../../../../src/db-singleton';
+import { getAuthUserId } from '../../../../src/lib/auth';
+
+const unauthorized = () => NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
+const notFound = () => NextResponse.json({ error: 'Order not found' }, { status: 404 });
+
+export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const userId = await getAuthUserId();
+ if (!userId) return unauthorized();
+ const { id } = await params;
+ const db = await getDb();
+ const order = await getOrderWithUser(db, id);
+ if (!order || String(order.userId) !== userId) return notFound();
+ return NextResponse.json(order);
+}
+
+export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const userId = await getAuthUserId();
+ if (!userId) return unauthorized();
+ const { id } = await params;
+ const db = await getDb();
+ const order = await getOrderById(db, id);
+ if (!order || String(order.userId) !== userId) return notFound();
+ const deleted = await deleteOrder(db, id);
+ return NextResponse.json(deleted);
+}
+
+export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const userId = await getAuthUserId();
+ if (!userId) return unauthorized();
+ const { id } = await params;
+ let body: { status: string };
+ try {
+ const raw = await req.json();
+ if (typeof raw?.status !== 'string' || !raw.status) {
+ return NextResponse.json({ error: 'Missing required field: status' }, { status: 400 });
+ }
+ body = raw;
+ } catch {
+ return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
+ }
+ const db = await getDb();
+ const order = await getOrderById(db, id);
+ if (!order || String(order.userId) !== userId) return notFound();
+ await updateOrderStatus(db, id, {
+ status: body.status,
+ timestamp: new Date(),
+ });
+ const updated = await getOrderWithUser(db, id);
+ return NextResponse.json(updated);
+}
diff --git a/examples/retail-store/app/api/orders/route.ts b/examples/retail-store/app/api/orders/route.ts
new file mode 100644
index 0000000000..d962ec0e08
--- /dev/null
+++ b/examples/retail-store/app/api/orders/route.ts
@@ -0,0 +1,35 @@
+import { NextResponse } from 'next/server';
+import { clearCart } from '../../../src/data/carts';
+import { createOrder, getUserOrders } from '../../../src/data/orders';
+import { getDb } from '../../../src/db-singleton';
+import { getAuthUserId } from '../../../src/lib/auth';
+
+export async function GET() {
+ const userId = await getAuthUserId();
+ if (!userId) return NextResponse.json([], { status: 401 });
+ const db = await getDb();
+ const orders = await getUserOrders(db, userId);
+ return NextResponse.json(orders);
+}
+
+export async function POST(req: Request) {
+ const userId = await getAuthUserId();
+ if (!userId) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
+ const body = await req.json();
+ if (!Array.isArray(body.items) || body.items.length === 0 || !body.shippingAddress) {
+ return NextResponse.json(
+ { error: 'Missing required fields: items, shippingAddress' },
+ { status: 400 },
+ );
+ }
+ const db = await getDb();
+ const order = await createOrder(db, {
+ userId,
+ items: body.items,
+ shippingAddress: body.shippingAddress,
+ type: body.type ?? 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+ await clearCart(db, userId);
+ return NextResponse.json(order, { status: 201 });
+}
diff --git a/examples/retail-store/app/api/products/[id]/route.ts b/examples/retail-store/app/api/products/[id]/route.ts
new file mode 100644
index 0000000000..0b10a92e61
--- /dev/null
+++ b/examples/retail-store/app/api/products/[id]/route.ts
@@ -0,0 +1,13 @@
+import { NextResponse } from 'next/server';
+import { findProductById } from '../../../../src/data/products';
+import { getDb } from '../../../../src/db-singleton';
+
+export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const db = await getDb();
+ const product = await findProductById(db, id);
+ if (!product) {
+ return NextResponse.json({ error: 'Product not found' }, { status: 404 });
+ }
+ return NextResponse.json(product);
+}
diff --git a/examples/retail-store/app/api/products/random/route.ts b/examples/retail-store/app/api/products/random/route.ts
new file mode 100644
index 0000000000..33af52ae10
--- /dev/null
+++ b/examples/retail-store/app/api/products/random/route.ts
@@ -0,0 +1,17 @@
+import { type NextRequest, NextResponse } from 'next/server';
+import { getRandomProducts } from '../../../../src/data/products';
+import { getDb } from '../../../../src/db-singleton';
+
+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) {
+ return NextResponse.json(
+ { error: 'Invalid count. Expected an integer between 1 and 24.' },
+ { status: 400 },
+ );
+ }
+ const db = await getDb();
+ const products = await getRandomProducts(db, count);
+ return NextResponse.json(products);
+}
diff --git a/examples/retail-store/app/api/products/route.ts b/examples/retail-store/app/api/products/route.ts
new file mode 100644
index 0000000000..99b255a6e8
--- /dev/null
+++ b/examples/retail-store/app/api/products/route.ts
@@ -0,0 +1,9 @@
+import { NextResponse } from 'next/server';
+import { findProducts } from '../../../src/data/products';
+import { getDb } from '../../../src/db-singleton';
+
+export async function GET() {
+ const db = await getDb();
+ const products = await findProducts(db);
+ return NextResponse.json(products);
+}
diff --git a/examples/retail-store/app/cart/cart-actions.tsx b/examples/retail-store/app/cart/cart-actions.tsx
new file mode 100644
index 0000000000..9a65c2323d
--- /dev/null
+++ b/examples/retail-store/app/cart/cart-actions.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useCart } from '../../src/components/cart-provider';
+import { Button } from '../../src/components/ui/button';
+
+type CartActionsProps =
+ | { mode: 'remove'; productId: string }
+ | { mode: 'clear'; productId?: never };
+
+export function CartActions({ productId, mode }: CartActionsProps) {
+ const router = useRouter();
+ const { invalidateCart } = useCart();
+ const [loading, setLoading] = useState(false);
+
+ async function handleAction() {
+ setLoading(true);
+ try {
+ const url =
+ mode === 'remove'
+ ? `/api/cart?${new URLSearchParams({ productId }).toString()}`
+ : '/api/cart';
+ const res = await fetch(url, { method: 'DELETE' });
+ if (!res.ok) throw new Error(`Cart action failed (${res.status})`);
+ invalidateCart();
+ router.refresh();
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ if (mode === 'remove') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/examples/retail-store/app/cart/page.tsx b/examples/retail-store/app/cart/page.tsx
new file mode 100644
index 0000000000..7ab5eadbcd
--- /dev/null
+++ b/examples/retail-store/app/cart/page.tsx
@@ -0,0 +1,74 @@
+import Link from 'next/link';
+import { redirect } from 'next/navigation';
+import { Button } from '../../src/components/ui/button';
+import { Card, CardContent } from '../../src/components/ui/card';
+import { Separator } from '../../src/components/ui/separator';
+import { getCartByUserId } from '../../src/data/carts';
+import { getDb } from '../../src/db-singleton';
+import { getAuthUserId } from '../../src/lib/auth';
+import { CartActions } from './cart-actions';
+
+export const dynamic = 'force-dynamic';
+
+export default async function CartPage() {
+ const userId = await getAuthUserId();
+ if (!userId) redirect('/login');
+
+ const db = await getDb();
+ const cart = await getCartByUserId(db, userId);
+ const items = cart?.items ?? [];
+
+ const total = items.reduce((sum, item) => sum + Number(item.price.amount) * item.amount, 0);
+
+ return (
+
+
Shopping Cart
+
+ {items.length === 0 ? (
+
+
Your cart is empty.
+
+
+ ) : (
+ <>
+
+ {items.map((item, i) => (
+
+
+
+
{item.name}
+
+ {item.brand} · Qty: {item.amount}
+
+
+
+
+ ${(Number(item.price.amount) * item.amount).toFixed(2)}
+
+
+
+
+
+ ))}
+
+
+
+
+
+ Total
+ ${total.toFixed(2)}
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/examples/retail-store/app/checkout/checkout-form.tsx b/examples/retail-store/app/checkout/checkout-form.tsx
new file mode 100644
index 0000000000..b0c6f9618a
--- /dev/null
+++ b/examples/retail-store/app/checkout/checkout-form.tsx
@@ -0,0 +1,148 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { useCart } from '../../src/components/cart-provider';
+import { Button } from '../../src/components/ui/button';
+import { Input } from '../../src/components/ui/input';
+import { RadioGroup, RadioGroupItem } from '../../src/components/ui/radio-group';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '../../src/components/ui/select';
+import { Separator } from '../../src/components/ui/separator';
+
+interface CartItem {
+ productId: string;
+ name: string;
+ brand: string;
+ amount: number;
+ price: { amount: number; currency: string };
+ image: { url: string };
+}
+
+interface Location {
+ id: string;
+ name: string;
+ address: string;
+}
+
+interface CheckoutFormProps {
+ defaultAddress: string;
+ locations: Location[];
+ cartItems: CartItem[];
+}
+
+export function CheckoutForm({ defaultAddress, locations, cartItems }: CheckoutFormProps) {
+ const router = useRouter();
+ const { invalidateCart } = useCart();
+ const [orderType, setOrderType] = useState<'home' | 'bopis'>('home');
+ const [address, setAddress] = useState(defaultAddress);
+ const [locationId, setLocationId] = useState(locations[0]?.id ?? '');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setError(null);
+ setLoading(true);
+ try {
+ const shippingAddress =
+ orderType === 'home' ? address : locations.find((l) => l.id === locationId)?.address;
+
+ if (!shippingAddress?.trim()) {
+ setError(
+ orderType === 'home'
+ ? 'Please enter a shipping address.'
+ : 'Please select a pickup location.',
+ );
+ return;
+ }
+
+ const res = await fetch('/api/orders', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ items: cartItems,
+ shippingAddress,
+ type: orderType,
+ }),
+ });
+
+ if (res.ok) {
+ invalidateCart();
+ const order = await res.json();
+ router.push(`/orders/${order._id}`);
+ }
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/examples/retail-store/app/checkout/page.tsx b/examples/retail-store/app/checkout/page.tsx
new file mode 100644
index 0000000000..5bef828e2b
--- /dev/null
+++ b/examples/retail-store/app/checkout/page.tsx
@@ -0,0 +1,85 @@
+import Link from 'next/link';
+import { redirect } from 'next/navigation';
+import { Card, CardContent, CardHeader, CardTitle } from '../../src/components/ui/card';
+import { Separator } from '../../src/components/ui/separator';
+import { getCartByUserId } from '../../src/data/carts';
+import { findLocations } from '../../src/data/locations';
+import { getDb } from '../../src/db-singleton';
+import { getAuthUser, getAuthUserId } from '../../src/lib/auth';
+import { CheckoutForm } from './checkout-form';
+
+export const dynamic = 'force-dynamic';
+
+export default async function CheckoutPage() {
+ const userId = await getAuthUserId();
+ if (!userId) redirect('/login');
+
+ const db = await getDb();
+ const [cart, user, locations] = await Promise.all([
+ getCartByUserId(db, userId),
+ getAuthUser(),
+ findLocations(db),
+ ]);
+
+ const items = cart?.items ?? [];
+ if (items.length === 0) redirect('/cart');
+
+ const total = items.reduce((sum, item) => sum + Number(item.price.amount) * item.amount, 0);
+
+ const userAddress = user?.address
+ ? `${user.address.streetAndNumber}, ${user.address.city}, ${user.address.postalCode}, ${user.address.country}`
+ : '';
+
+ const locationList = locations.map((loc) => ({
+ id: String(loc._id),
+ name: String(loc.name),
+ address: `${loc.streetAndNumber}, ${loc.city}`,
+ }));
+
+ return (
+
+
+ ← Back to cart
+
+
+
+ Checkout
+
+
+ Order Summary
+
+ {items.map((item, i) => (
+
+
+ {item.name} ×{item.amount}
+
+ ${(Number(item.price.amount) * item.amount).toFixed(2)}
+
+ ))}
+
+
+
+ Total
+ ${total.toFixed(2)}
+
+
+ ({
+ productId: String(item.productId),
+ name: String(item.name),
+ brand: String(item.brand),
+ amount: item.amount,
+ price: { amount: Number(item.price.amount), currency: String(item.price.currency) },
+ image: { url: String(item.image.url) },
+ }))}
+ />
+
+
+
+ );
+}
diff --git a/examples/retail-store/app/globals.css b/examples/retail-store/app/globals.css
new file mode 100644
index 0000000000..13ef653036
--- /dev/null
+++ b/examples/retail-store/app/globals.css
@@ -0,0 +1,27 @@
+@import 'tailwindcss';
+
+@theme {
+ --color-background: #fafafa;
+ --color-foreground: #111;
+ --color-muted: #666;
+ --color-muted-foreground: #888;
+ --color-border: #e5e5e5;
+ --color-input: #e5e5e5;
+ --color-ring: #2563eb;
+ --color-accent: #2563eb;
+ --color-accent-foreground: #fff;
+ --color-card: #fff;
+ --color-card-foreground: #111;
+ --color-destructive: #dc2626;
+ --color-success: #16a34a;
+ --color-warning: #d97706;
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: var(--color-background);
+ color: var(--color-foreground);
+}
diff --git a/examples/retail-store/app/layout.tsx b/examples/retail-store/app/layout.tsx
new file mode 100644
index 0000000000..ed9fb4c9e2
--- /dev/null
+++ b/examples/retail-store/app/layout.tsx
@@ -0,0 +1,22 @@
+import type { ReactNode } from 'react';
+import { CartProvider } from '../src/components/cart-provider';
+import { Navbar } from '../src/components/navbar';
+import './globals.css';
+
+export const metadata = {
+ title: 'Retail Store — Prisma Next MongoDB Demo',
+ description: 'E-commerce example app powered by Prisma Next with MongoDB',
+};
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/examples/retail-store/app/login/page.tsx b/examples/retail-store/app/login/page.tsx
new file mode 100644
index 0000000000..a8e41c3c1c
--- /dev/null
+++ b/examples/retail-store/app/login/page.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { Button } from '../../src/components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '../../src/components/ui/card';
+
+export default function LoginPage() {
+ const router = useRouter();
+ const [loading, setLoading] = useState(false);
+
+ async function handleSignUp() {
+ setLoading(true);
+ try {
+ const res = await fetch('/api/auth/signup', { method: 'POST' });
+ if (res.ok) {
+ router.push('/');
+ router.refresh();
+ }
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+
+
+ Retail Store
+ Prisma Next MongoDB Demo
+
+
+
+ Sign up to browse products, manage your cart, and place orders.
+
+
+
+
+
+ );
+}
diff --git a/examples/retail-store/app/orders/[id]/order-status-buttons.tsx b/examples/retail-store/app/orders/[id]/order-status-buttons.tsx
new file mode 100644
index 0000000000..97a2531a1f
--- /dev/null
+++ b/examples/retail-store/app/orders/[id]/order-status-buttons.tsx
@@ -0,0 +1,45 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+import { Button } from '../../../src/components/ui/button';
+
+const statusFlow: Record = {
+ placed: 'shipped',
+ shipped: 'delivered',
+};
+
+export function OrderStatusButtons({
+ orderId,
+ currentStatus,
+}: {
+ orderId: string;
+ currentStatus: string;
+}) {
+ const router = useRouter();
+ const [loading, setLoading] = useState(false);
+ const nextStatus = statusFlow[currentStatus];
+
+ if (!nextStatus) return null;
+
+ async function handleAdvance() {
+ setLoading(true);
+ try {
+ const res = await fetch(`/api/orders/${orderId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status: nextStatus }),
+ });
+ if (!res.ok) throw new Error(`Failed to update order status (${res.status})`);
+ router.refresh();
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/examples/retail-store/app/orders/[id]/page.tsx b/examples/retail-store/app/orders/[id]/page.tsx
new file mode 100644
index 0000000000..c38c80af58
--- /dev/null
+++ b/examples/retail-store/app/orders/[id]/page.tsx
@@ -0,0 +1,100 @@
+import Link from 'next/link';
+import { notFound, redirect } from 'next/navigation';
+import { Badge } from '../../../src/components/ui/badge';
+import { Card, CardContent, CardHeader, CardTitle } from '../../../src/components/ui/card';
+import { Separator } from '../../../src/components/ui/separator';
+import { getOrderWithUser } from '../../../src/data/orders';
+import { getDb } from '../../../src/db-singleton';
+import { getAuthUserId } from '../../../src/lib/auth';
+import { OrderStatusButtons } from './order-status-buttons';
+
+export const dynamic = 'force-dynamic';
+
+const statusVariant: Record = {
+ placed: 'default',
+ shipped: 'warning',
+ delivered: 'success',
+ cancelled: 'destructive',
+};
+
+export default async function OrderDetail({ params }: { params: Promise<{ id: string }> }) {
+ const userId = await getAuthUserId();
+ if (!userId) redirect('/login');
+
+ const { id } = await params;
+ const db = await getDb();
+ const order = await getOrderWithUser(db, id);
+
+ if (!order || String(order.userId) !== userId) {
+ notFound();
+ }
+
+ const total = order.items.reduce((sum, item) => sum + Number(item.price.amount) * item.amount, 0);
+ const lastEntry = order.statusHistory[order.statusHistory.length - 1];
+ const lastStatus = lastEntry ? String(lastEntry.status) : 'placed';
+
+ return (
+
+
+ ← Back to orders
+
+
+
+ Order Detail
+
+
+
+
Shipping Address
+
{order.shippingAddress}
+
+
+
+
Items
+
+ {order.items.map((item, i) => (
+
+
+ {item.name}
+ ×{item.amount}
+
+
${(Number(item.price.amount) * item.amount).toFixed(2)}
+
+ ))}
+
+
Total: ${total.toFixed(2)}
+
+
+
+
+
+
Status History
+
+ {order.statusHistory.map((entry) => {
+ const s = entry.status as string;
+ return (
+
+ {s}
+
+ {new Date(String(entry.timestamp)).toLocaleString()}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/retail-store/app/orders/page.tsx b/examples/retail-store/app/orders/page.tsx
new file mode 100644
index 0000000000..438412febd
--- /dev/null
+++ b/examples/retail-store/app/orders/page.tsx
@@ -0,0 +1,69 @@
+import Link from 'next/link';
+import { redirect } from 'next/navigation';
+import { Badge } from '../../src/components/ui/badge';
+import { Card, CardContent } from '../../src/components/ui/card';
+import { getUserOrders } from '../../src/data/orders';
+import { getDb } from '../../src/db-singleton';
+import { getAuthUserId } from '../../src/lib/auth';
+
+export const dynamic = 'force-dynamic';
+
+const statusVariant: Record = {
+ placed: 'default',
+ shipped: 'warning',
+ delivered: 'success',
+ cancelled: 'destructive',
+};
+
+export default async function OrdersPage() {
+ const userId = await getAuthUserId();
+ if (!userId) redirect('/login');
+
+ const db = await getDb();
+ const orders = await getUserOrders(db, userId);
+
+ return (
+
+
Order History
+
+ {orders.length === 0 ? (
+
No orders yet.
+ ) : (
+
+ {orders.map((order) => {
+ const lastStatus = order.statusHistory[order.statusHistory.length - 1];
+ const total = order.items.reduce(
+ (sum, item) => sum + Number(item.price.amount) * item.amount,
+ 0,
+ );
+ const status = lastStatus?.status as string;
+ return (
+
+
+
+
+
+ {order.items.length} item{order.items.length !== 1 ? 's' : ''}
+
+
{order.shippingAddress}
+
+
+ {status && (
+ {status}
+ )}
+ ${total.toFixed(2)}
+
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/examples/retail-store/app/page.tsx b/examples/retail-store/app/page.tsx
new file mode 100644
index 0000000000..74d3ef7223
--- /dev/null
+++ b/examples/retail-store/app/page.tsx
@@ -0,0 +1,105 @@
+import Link from 'next/link';
+import { Badge } from '../src/components/ui/badge';
+import { Button } from '../src/components/ui/button';
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '../src/components/ui/card';
+import { findProductsPaginated, searchProducts } from '../src/data/products';
+import { getDb } from '../src/db-singleton';
+
+export const dynamic = 'force-dynamic';
+
+const PAGE_SIZE = 8;
+
+export default async function ProductCatalog({
+ searchParams,
+}: {
+ searchParams: Promise>;
+}) {
+ const params = await searchParams;
+ const page = Math.max(1, Number(params['page']) || 1);
+ const query = typeof params['q'] === 'string' ? params['q'] : '';
+
+ const db = await getDb();
+
+ const products = query
+ ? await searchProducts(db, query)
+ : await findProductsPaginated(db, (page - 1) * PAGE_SIZE, PAGE_SIZE);
+
+ return (
+
+
+
Product Catalog
+
+
+
+
+
+ {products.map((product) => (
+
+
+
+ {product.brand}
+ {product.name}
+
+
+
+ {product.articleType}
+ {product.subCategory}
+
+ {product.description}
+
+
+
+ ${Number(product.price.amount).toFixed(2)}
+
+
+
+
+ ))}
+
+
+ {products.length === 0 && (
+
+ {query
+ ? `No products matching "${query}".`
+ : 'No products found. Run the seed script first.'}
+
+ )}
+
+ {!query && (
+
+ {page > 1 && (
+
+ )}
+ Page {page}
+ {products.length === PAGE_SIZE && (
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/examples/retail-store/app/products/[id]/page.tsx b/examples/retail-store/app/products/[id]/page.tsx
new file mode 100644
index 0000000000..c678cfe971
--- /dev/null
+++ b/examples/retail-store/app/products/[id]/page.tsx
@@ -0,0 +1,60 @@
+import Link from 'next/link';
+import { notFound } from 'next/navigation';
+import { AddToCartButton } from '../../../src/components/add-to-cart-button';
+import { Badge } from '../../../src/components/ui/badge';
+import { Card, CardContent, CardHeader } from '../../../src/components/ui/card';
+import { findProductById } from '../../../src/data/products';
+import { getDb } from '../../../src/db-singleton';
+
+export const dynamic = 'force-dynamic';
+
+export default async function ProductDetail({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const db = await getDb();
+ const product = await findProductById(db, id);
+
+ if (!product) {
+ notFound();
+ }
+
+ return (
+
+
+ ← Back to catalog
+
+
+
+ {product.brand}
+ {product.name}
+
+
+ {product.description}
+
+ {product.masterCategory}
+ {product.subCategory}
+ {product.articleType}
+
+
+ ${Number(product.price.amount).toFixed(2)} {product.price.currency}
+
+
+ Code: {product.code}
+
+
+
+ );
+}
diff --git a/examples/retail-store/biome.jsonc b/examples/retail-store/biome.jsonc
new file mode 100644
index 0000000000..a6826cb827
--- /dev/null
+++ b/examples/retail-store/biome.jsonc
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
+ // Example apps use a standalone biome config rather than extending the root
+ // config, to keep example-specific settings self-contained (matches mongo-demo).
+ "extends": "//"
+}
diff --git a/examples/retail-store/middleware.ts b/examples/retail-store/middleware.ts
new file mode 100644
index 0000000000..1b0a486d23
--- /dev/null
+++ b/examples/retail-store/middleware.ts
@@ -0,0 +1,16 @@
+import { type NextRequest, NextResponse } from 'next/server';
+
+export function middleware(request: NextRequest) {
+ const userId = request.cookies.get('userId')?.value;
+ if (!userId) {
+ if (request.nextUrl.pathname.startsWith('/api/')) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+ return NextResponse.redirect(new URL('/login', request.url));
+ }
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: ['/((?!login|api/auth|_next/static|_next/image|favicon.ico).*)'],
+};
diff --git a/examples/retail-store/migrations/20260413T0314_migration/migration.json b/examples/retail-store/migrations/20260413T0314_migration/migration.json
new file mode 100644
index 0000000000..c30d48b3bc
--- /dev/null
+++ b/examples/retail-store/migrations/20260413T0314_migration/migration.json
@@ -0,0 +1,1272 @@
+{
+ "from": "sha256:empty",
+ "to": "sha256:e5cfc21670435e53a4af14a665d61d8ba716d5e2e67b63c1443affdcad86985d",
+ "migrationId": "sha256:e3960312bdd88da15123ae50c272af5fb45c6cc0cc3f0ff187937b50773b5d13",
+ "kind": "regular",
+ "fromContract": null,
+ "toContract": {
+ "schemaVersion": "1",
+ "targetFamily": "mongo",
+ "target": "mongo",
+ "profileHash": "sha256:840de65fba7eb950a31487f74ee420b9c21205f38bce58579026747e0264e840",
+ "roots": {
+ "carts": "Cart",
+ "events": "Event",
+ "invoices": "Invoice",
+ "locations": "Location",
+ "orders": "Order",
+ "products": "Product",
+ "users": "User"
+ },
+ "models": {
+ "AddToCartEvent": {
+ "base": "Event",
+ "fields": {
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "productId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "events"
+ }
+ },
+ "Cart": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "items": {
+ "many": true,
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "CartItem"
+ }
+ },
+ "userId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {
+ "user": {
+ "cardinality": "N:1",
+ "on": {
+ "localFields": ["userId"],
+ "targetFields": ["_id"]
+ },
+ "to": "User"
+ }
+ },
+ "storage": {
+ "collection": "carts"
+ }
+ },
+ "Event": {
+ "discriminator": {
+ "field": "type"
+ },
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "sessionId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "timestamp": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/date@1",
+ "kind": "scalar"
+ }
+ },
+ "type": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "userId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "events"
+ },
+ "variants": {
+ "AddToCartEvent": {
+ "value": "add-to-cart"
+ },
+ "SearchEvent": {
+ "value": "search"
+ },
+ "ViewProductEvent": {
+ "value": "view-product"
+ }
+ }
+ },
+ "Invoice": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "issuedAt": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/date@1",
+ "kind": "scalar"
+ }
+ },
+ "items": {
+ "many": true,
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "InvoiceLineItem"
+ }
+ },
+ "orderId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "subtotal": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "tax": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "total": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {
+ "order": {
+ "cardinality": "N:1",
+ "on": {
+ "localFields": ["orderId"],
+ "targetFields": ["_id"]
+ },
+ "to": "Order"
+ }
+ },
+ "storage": {
+ "collection": "invoices"
+ }
+ },
+ "Location": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "city": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "country": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "postalCode": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "streetAndNumber": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "locations"
+ }
+ },
+ "Order": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "items": {
+ "many": true,
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "OrderLineItem"
+ }
+ },
+ "shippingAddress": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "statusHistory": {
+ "many": true,
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "StatusEntry"
+ }
+ },
+ "type": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "userId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {
+ "invoices": {
+ "cardinality": "1:N",
+ "on": {
+ "localFields": ["_id"],
+ "targetFields": ["orderId"]
+ },
+ "to": "Invoice"
+ },
+ "user": {
+ "cardinality": "N:1",
+ "on": {
+ "localFields": ["userId"],
+ "targetFields": ["_id"]
+ },
+ "to": "User"
+ }
+ },
+ "storage": {
+ "collection": "orders"
+ }
+ },
+ "Product": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "articleType": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "code": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "description": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "embedding": {
+ "many": true,
+ "nullable": true,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "image": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Image"
+ }
+ },
+ "masterCategory": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "price": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Price"
+ }
+ },
+ "subCategory": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "products"
+ }
+ },
+ "SearchEvent": {
+ "base": "Event",
+ "fields": {
+ "query": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "events"
+ }
+ },
+ "User": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "address": {
+ "nullable": true,
+ "type": {
+ "kind": "valueObject",
+ "name": "Address"
+ }
+ },
+ "email": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {
+ "carts": {
+ "cardinality": "1:N",
+ "on": {
+ "localFields": ["_id"],
+ "targetFields": ["userId"]
+ },
+ "to": "Cart"
+ },
+ "orders": {
+ "cardinality": "1:N",
+ "on": {
+ "localFields": ["_id"],
+ "targetFields": ["userId"]
+ },
+ "to": "Order"
+ }
+ },
+ "storage": {
+ "collection": "users"
+ }
+ },
+ "ViewProductEvent": {
+ "base": "Event",
+ "fields": {
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "exitMethod": {
+ "nullable": true,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "productId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "subCategory": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "events"
+ }
+ }
+ },
+ "valueObjects": {
+ "Address": {
+ "fields": {
+ "city": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "country": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "postalCode": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "streetAndNumber": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "CartItem": {
+ "fields": {
+ "amount": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/int32@1",
+ "kind": "scalar"
+ }
+ },
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "image": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Image"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "price": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Price"
+ }
+ },
+ "productId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "Image": {
+ "fields": {
+ "url": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "InvoiceLineItem": {
+ "fields": {
+ "amount": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/int32@1",
+ "kind": "scalar"
+ }
+ },
+ "lineTotal": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "unitPrice": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "OrderLineItem": {
+ "fields": {
+ "amount": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/int32@1",
+ "kind": "scalar"
+ }
+ },
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "image": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Image"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "price": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Price"
+ }
+ },
+ "productId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "Price": {
+ "fields": {
+ "amount": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "currency": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "StatusEntry": {
+ "fields": {
+ "status": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "timestamp": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/date@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ }
+ },
+ "storage": {
+ "collections": {
+ "addToCartEvent": {
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "brand": {
+ "bsonType": "string"
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["brand", "productId"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "carts": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["url"]
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["currency"]
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["amount", "brand", "image", "name", "price", "productId"]
+ }
+ },
+ "userId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": ["_id", "items", "userId"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "events": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ },
+ {
+ "direction": -1,
+ "field": "timestamp"
+ }
+ ]
+ },
+ {
+ "expireAfterSeconds": 7776000,
+ "keys": [
+ {
+ "direction": 1,
+ "field": "timestamp"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "sessionId": {
+ "bsonType": "string"
+ },
+ "timestamp": {
+ "bsonType": "date"
+ },
+ "type": {
+ "bsonType": "string"
+ },
+ "userId": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["_id", "sessionId", "timestamp", "type", "userId"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "invoices": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "orderId"
+ }
+ ]
+ },
+ {
+ "keys": [
+ {
+ "direction": -1,
+ "field": "issuedAt"
+ }
+ ],
+ "sparse": true
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "issuedAt": {
+ "bsonType": "date"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "name": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["amount", "name"]
+ }
+ },
+ "orderId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": ["_id", "issuedAt", "items", "orderId"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "locations": {
+ "indexes": [
+ {
+ "collation": {
+ "locale": "en",
+ "strength": 2
+ },
+ "keys": [
+ {
+ "direction": 1,
+ "field": "city"
+ },
+ {
+ "direction": 1,
+ "field": "country"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "city": {
+ "bsonType": "string"
+ },
+ "country": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "postalCode": {
+ "bsonType": "string"
+ },
+ "streetAndNumber": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["_id", "city", "country", "name", "postalCode", "streetAndNumber"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "orders": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["url"]
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["currency"]
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["amount", "brand", "image", "name", "price", "productId"]
+ }
+ },
+ "shippingAddress": {
+ "bsonType": "string"
+ },
+ "statusHistory": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "status": {
+ "bsonType": "string"
+ },
+ "timestamp": {
+ "bsonType": "date"
+ }
+ },
+ "required": ["status", "timestamp"]
+ }
+ },
+ "type": {
+ "bsonType": "string"
+ },
+ "userId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": ["_id", "items", "shippingAddress", "statusHistory", "type", "userId"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "products": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": "text",
+ "field": "name"
+ },
+ {
+ "direction": "text",
+ "field": "description"
+ }
+ ],
+ "weights": {
+ "description": 1,
+ "name": 10
+ }
+ },
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "brand"
+ },
+ {
+ "direction": 1,
+ "field": "subCategory"
+ }
+ ]
+ },
+ {
+ "keys": [
+ {
+ "direction": "hashed",
+ "field": "code"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "articleType": {
+ "bsonType": "string"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "code": {
+ "bsonType": "string"
+ },
+ "description": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["url"]
+ },
+ "masterCategory": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["currency"]
+ },
+ "subCategory": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "_id",
+ "articleType",
+ "brand",
+ "code",
+ "description",
+ "image",
+ "masterCategory",
+ "name",
+ "price",
+ "subCategory"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "searchEvent": {
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "query": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["query"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "users": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "email"
+ }
+ ],
+ "unique": true
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "address": {
+ "oneOf": [
+ {
+ "bsonType": "null"
+ },
+ {
+ "bsonType": "object",
+ "properties": {
+ "city": {
+ "bsonType": "string"
+ },
+ "country": {
+ "bsonType": "string"
+ },
+ "postalCode": {
+ "bsonType": "string"
+ },
+ "streetAndNumber": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["city", "country", "postalCode", "streetAndNumber"]
+ }
+ ]
+ },
+ "email": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["_id", "email", "name"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "viewProductEvent": {
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "brand": {
+ "bsonType": "string"
+ },
+ "exitMethod": {
+ "bsonType": ["null", "string"]
+ },
+ "productId": {
+ "bsonType": "string"
+ },
+ "subCategory": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["brand", "productId", "subCategory"]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ }
+ },
+ "storageHash": "sha256:e5cfc21670435e53a4af14a665d61d8ba716d5e2e67b63c1443affdcad86985d"
+ },
+ "capabilities": {},
+ "extensionPacks": {},
+ "meta": {},
+ "_generated": {
+ "warning": "⚠️ GENERATED FILE - DO NOT EDIT",
+ "message": "This file is automatically generated by \"prisma-next contract emit\".",
+ "regenerate": "To regenerate, run: prisma-next contract emit"
+ }
+ },
+ "hints": {
+ "used": [],
+ "applied": [],
+ "plannerVersion": "1.0.0",
+ "planningStrategy": "diff"
+ },
+ "labels": [],
+ "createdAt": "2026-04-13T03:14:54.111Z"
+}
diff --git a/examples/retail-store/migrations/20260413T0314_migration/ops.json b/examples/retail-store/migrations/20260413T0314_migration/ops.json
new file mode 100644
index 0000000000..e8f695dd8e
--- /dev/null
+++ b/examples/retail-store/migrations/20260413T0314_migration/ops.json
@@ -0,0 +1,1345 @@
+[
+ {
+ "id": "collection.addToCartEvent.create",
+ "label": "Create collection addToCartEvent",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection addToCartEvent does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "addToCartEvent"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection addToCartEvent",
+ "command": {
+ "kind": "createCollection",
+ "collection": "addToCartEvent",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "brand": {
+ "bsonType": "string"
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["brand", "productId"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.carts.create",
+ "label": "Create collection carts",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection carts does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "carts"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection carts",
+ "command": {
+ "kind": "createCollection",
+ "collection": "carts",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["url"]
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["currency"]
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["amount", "brand", "image", "name", "price", "productId"]
+ }
+ },
+ "userId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": ["_id", "items", "userId"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.events.create",
+ "label": "Create collection events",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection events does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "events"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection events",
+ "command": {
+ "kind": "createCollection",
+ "collection": "events",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "sessionId": {
+ "bsonType": "string"
+ },
+ "timestamp": {
+ "bsonType": "date"
+ },
+ "type": {
+ "bsonType": "string"
+ },
+ "userId": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["_id", "sessionId", "timestamp", "type", "userId"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.invoices.create",
+ "label": "Create collection invoices",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection invoices does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "invoices"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection invoices",
+ "command": {
+ "kind": "createCollection",
+ "collection": "invoices",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "issuedAt": {
+ "bsonType": "date"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "name": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["amount", "name"]
+ }
+ },
+ "orderId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": ["_id", "issuedAt", "items", "orderId"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.locations.create",
+ "label": "Create collection locations",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection locations does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "locations"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection locations",
+ "command": {
+ "kind": "createCollection",
+ "collection": "locations",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "city": {
+ "bsonType": "string"
+ },
+ "country": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "postalCode": {
+ "bsonType": "string"
+ },
+ "streetAndNumber": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["_id", "city", "country", "name", "postalCode", "streetAndNumber"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.orders.create",
+ "label": "Create collection orders",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection orders does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "orders"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection orders",
+ "command": {
+ "kind": "createCollection",
+ "collection": "orders",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["url"]
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["currency"]
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["amount", "brand", "image", "name", "price", "productId"]
+ }
+ },
+ "shippingAddress": {
+ "bsonType": "string"
+ },
+ "statusHistory": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "status": {
+ "bsonType": "string"
+ },
+ "timestamp": {
+ "bsonType": "date"
+ }
+ },
+ "required": ["status", "timestamp"]
+ }
+ },
+ "type": {
+ "bsonType": "string"
+ },
+ "userId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": ["_id", "items", "shippingAddress", "statusHistory", "type", "userId"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.products.create",
+ "label": "Create collection products",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection products does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "products"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection products",
+ "command": {
+ "kind": "createCollection",
+ "collection": "products",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "articleType": {
+ "bsonType": "string"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "code": {
+ "bsonType": "string"
+ },
+ "description": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["url"]
+ },
+ "masterCategory": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["currency"]
+ },
+ "subCategory": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "_id",
+ "articleType",
+ "brand",
+ "code",
+ "description",
+ "image",
+ "masterCategory",
+ "name",
+ "price",
+ "subCategory"
+ ]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.searchEvent.create",
+ "label": "Create collection searchEvent",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection searchEvent does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "searchEvent"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection searchEvent",
+ "command": {
+ "kind": "createCollection",
+ "collection": "searchEvent",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "query": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["query"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.users.create",
+ "label": "Create collection users",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection users does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "users"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection users",
+ "command": {
+ "kind": "createCollection",
+ "collection": "users",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "address": {
+ "oneOf": [
+ {
+ "bsonType": "null"
+ },
+ {
+ "bsonType": "object",
+ "properties": {
+ "city": {
+ "bsonType": "string"
+ },
+ "country": {
+ "bsonType": "string"
+ },
+ "postalCode": {
+ "bsonType": "string"
+ },
+ "streetAndNumber": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["city", "country", "postalCode", "streetAndNumber"]
+ }
+ ]
+ },
+ "email": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["_id", "email", "name"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "collection.viewProductEvent.create",
+ "label": "Create collection viewProductEvent",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "collection viewProductEvent does not exist",
+ "source": {
+ "kind": "listCollections"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "name",
+ "op": "$eq",
+ "value": "viewProductEvent"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create collection viewProductEvent",
+ "command": {
+ "kind": "createCollection",
+ "collection": "viewProductEvent",
+ "validator": {
+ "$jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "brand": {
+ "bsonType": "string"
+ },
+ "exitMethod": {
+ "bsonType": ["null", "string"]
+ },
+ "productId": {
+ "bsonType": "string"
+ },
+ "subCategory": {
+ "bsonType": "string"
+ }
+ },
+ "required": ["brand", "productId", "subCategory"]
+ }
+ },
+ "validationLevel": "strict",
+ "validationAction": "error"
+ }
+ }
+ ],
+ "postcheck": []
+ },
+ {
+ "id": "index.carts.create(userId:1)",
+ "label": "Create index on carts (userId:1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on carts",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "carts"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "userId": 1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on carts",
+ "command": {
+ "kind": "createIndex",
+ "collection": "carts",
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ }
+ ],
+ "name": "userId_1"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on carts",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "carts"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "userId": 1
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.events.create(userId:1,timestamp:-1)",
+ "label": "Create index on events (userId:1, timestamp:-1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on events",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "events"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "userId": 1,
+ "timestamp": -1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on events",
+ "command": {
+ "kind": "createIndex",
+ "collection": "events",
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ },
+ {
+ "direction": -1,
+ "field": "timestamp"
+ }
+ ],
+ "name": "userId_1_timestamp_-1"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on events",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "events"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "userId": 1,
+ "timestamp": -1
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.events.create(timestamp:1)",
+ "label": "Create index on events (timestamp:1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on events",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "events"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "timestamp": 1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on events",
+ "command": {
+ "kind": "createIndex",
+ "collection": "events",
+ "keys": [
+ {
+ "direction": 1,
+ "field": "timestamp"
+ }
+ ],
+ "expireAfterSeconds": 7776000,
+ "name": "timestamp_1"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on events",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "events"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "timestamp": 1
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.invoices.create(orderId:1)",
+ "label": "Create index on invoices (orderId:1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on invoices",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "invoices"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "orderId": 1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on invoices",
+ "command": {
+ "kind": "createIndex",
+ "collection": "invoices",
+ "keys": [
+ {
+ "direction": 1,
+ "field": "orderId"
+ }
+ ],
+ "name": "orderId_1"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on invoices",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "invoices"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "orderId": 1
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.invoices.create(issuedAt:-1)",
+ "label": "Create index on invoices (issuedAt:-1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on invoices",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "invoices"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "issuedAt": -1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on invoices",
+ "command": {
+ "kind": "createIndex",
+ "collection": "invoices",
+ "keys": [
+ {
+ "direction": -1,
+ "field": "issuedAt"
+ }
+ ],
+ "sparse": true,
+ "name": "issuedAt_-1"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on invoices",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "invoices"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "issuedAt": -1
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.locations.create(city:1,country:1)",
+ "label": "Create index on locations (city:1, country:1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on locations",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "locations"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "city": 1,
+ "country": 1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on locations",
+ "command": {
+ "kind": "createIndex",
+ "collection": "locations",
+ "keys": [
+ {
+ "direction": 1,
+ "field": "city"
+ },
+ {
+ "direction": 1,
+ "field": "country"
+ }
+ ],
+ "name": "city_1_country_1",
+ "collation": {
+ "locale": "en",
+ "strength": 2
+ }
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on locations",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "locations"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "city": 1,
+ "country": 1
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.orders.create(userId:1)",
+ "label": "Create index on orders (userId:1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on orders",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "orders"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "userId": 1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on orders",
+ "command": {
+ "kind": "createIndex",
+ "collection": "orders",
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ }
+ ],
+ "name": "userId_1"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on orders",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "orders"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "userId": 1
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.products.create(name:text,description:text)",
+ "label": "Create index on products (name:text, description:text)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on products",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "products"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key._fts",
+ "op": "$eq",
+ "value": "text"
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on products",
+ "command": {
+ "kind": "createIndex",
+ "collection": "products",
+ "keys": [
+ {
+ "direction": "text",
+ "field": "name"
+ },
+ {
+ "direction": "text",
+ "field": "description"
+ }
+ ],
+ "name": "name_text_description_text",
+ "weights": {
+ "description": 1,
+ "name": 10
+ }
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on products",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "products"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key._fts",
+ "op": "$eq",
+ "value": "text"
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.products.create(brand:1,subCategory:1)",
+ "label": "Create index on products (brand:1, subCategory:1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on products",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "products"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "brand": 1,
+ "subCategory": 1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on products",
+ "command": {
+ "kind": "createIndex",
+ "collection": "products",
+ "keys": [
+ {
+ "direction": 1,
+ "field": "brand"
+ },
+ {
+ "direction": 1,
+ "field": "subCategory"
+ }
+ ],
+ "name": "brand_1_subCategory_1"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on products",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "products"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "brand": 1,
+ "subCategory": 1
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.products.create(code:hashed)",
+ "label": "Create index on products (code:hashed)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on products",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "products"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "code": "hashed"
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on products",
+ "command": {
+ "kind": "createIndex",
+ "collection": "products",
+ "keys": [
+ {
+ "direction": "hashed",
+ "field": "code"
+ }
+ ],
+ "name": "code_hashed"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on products",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "products"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "code": "hashed"
+ }
+ },
+ "expect": "exists"
+ }
+ ]
+ },
+ {
+ "id": "index.users.create(email:1)",
+ "label": "Create index on users (email:1)",
+ "operationClass": "additive",
+ "precheck": [
+ {
+ "description": "index does not already exist on users",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "users"
+ },
+ "filter": {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "email": 1
+ }
+ },
+ "expect": "notExists"
+ }
+ ],
+ "execute": [
+ {
+ "description": "create index on users",
+ "command": {
+ "kind": "createIndex",
+ "collection": "users",
+ "keys": [
+ {
+ "direction": 1,
+ "field": "email"
+ }
+ ],
+ "unique": true,
+ "name": "email_1"
+ }
+ }
+ ],
+ "postcheck": [
+ {
+ "description": "index exists on users",
+ "source": {
+ "kind": "listIndexes",
+ "collection": "users"
+ },
+ "filter": {
+ "kind": "and",
+ "exprs": [
+ {
+ "kind": "field",
+ "field": "key",
+ "op": "$eq",
+ "value": {
+ "email": 1
+ }
+ },
+ {
+ "kind": "field",
+ "field": "unique",
+ "op": "$eq",
+ "value": true
+ }
+ ]
+ },
+ "expect": "exists"
+ }
+ ]
+ }
+]
diff --git a/examples/retail-store/next.config.js b/examples/retail-store/next.config.js
new file mode 100644
index 0000000000..f9e95e7ce3
--- /dev/null
+++ b/examples/retail-store/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ serverExternalPackages: ['mongodb'],
+};
+
+export default nextConfig;
diff --git a/examples/retail-store/package.json b/examples/retail-store/package.json
new file mode 100644
index 0000000000..7b72d76f15
--- /dev/null
+++ b/examples/retail-store/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "retail-store",
+ "private": true,
+ "type": "module",
+ "engines": {
+ "node": ">=24"
+ },
+ "scripts": {
+ "emit": "prisma-next contract emit",
+ "migration:plan": "prisma-next migration plan",
+ "migration:apply": "prisma-next migration apply",
+ "db:seed": "tsx scripts/seed.ts",
+ "db:reset": "tsx scripts/reset-db.ts",
+ "dev": "next dev",
+ "build": "next build",
+ "test": "vitest run",
+ "typecheck": "tsc --project tsconfig.json --noEmit"
+ },
+ "dependencies": {
+ "@prisma-next/adapter-mongo": "workspace:*",
+ "@prisma-next/contract": "workspace:*",
+ "@prisma-next/driver-mongo": "workspace:*",
+ "@prisma-next/mongo-contract": "workspace:*",
+ "@prisma-next/mongo-orm": "workspace:*",
+ "@prisma-next/mongo-pipeline-builder": "workspace:*",
+ "@prisma-next/mongo-query-ast": "workspace:*",
+ "@prisma-next/mongo-runtime": "workspace:*",
+ "@prisma-next/mongo-value": "workspace:*",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-radio-group": "^1.3.8",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@tailwindcss/postcss": "^4.2.2",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.8.0",
+ "mongodb": "catalog:",
+ "next": "^15.3.1",
+ "postcss": "^8.5.9",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "tailwind-merge": "^3.5.0",
+ "tailwindcss": "^4.2.2"
+ },
+ "devDependencies": {
+ "@prisma-next/cli": "workspace:*",
+ "@prisma-next/family-mongo": "workspace:*",
+ "@prisma-next/mongo-contract-psl": "workspace:*",
+ "@prisma-next/test-utils": "workspace:*",
+ "@prisma-next/tsconfig": "workspace:*",
+ "@types/node": "catalog:",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "mongodb-memory-server": "^10.4.0",
+ "tsx": "^4.19.2",
+ "typescript": "catalog:",
+ "vitest": "catalog:"
+ }
+}
diff --git a/examples/retail-store/postcss.config.mjs b/examples/retail-store/postcss.config.mjs
new file mode 100644
index 0000000000..d1840a7c4e
--- /dev/null
+++ b/examples/retail-store/postcss.config.mjs
@@ -0,0 +1 @@
+export default { plugins: { '@tailwindcss/postcss': {} } };
diff --git a/examples/retail-store/prisma-next.config.ts b/examples/retail-store/prisma-next.config.ts
new file mode 100644
index 0000000000..7e0a8cd418
--- /dev/null
+++ b/examples/retail-store/prisma-next.config.ts
@@ -0,0 +1,25 @@
+import mongoAdapter from '@prisma-next/adapter-mongo/control';
+import { defineConfig } from '@prisma-next/cli/config-types';
+import mongoDriver from '@prisma-next/driver-mongo/control';
+import { mongoFamilyDescriptor, mongoTargetDescriptor } from '@prisma-next/family-mongo/control';
+import { createMongoScalarTypeDescriptors } from '@prisma-next/mongo-contract-psl';
+import { mongoContract } from '@prisma-next/mongo-contract-psl/provider';
+
+const scalarTypeDescriptors = new Map([
+ ...createMongoScalarTypeDescriptors(),
+ ['Float', 'mongo/double@1'],
+]);
+
+export default defineConfig({
+ family: mongoFamilyDescriptor,
+ target: mongoTargetDescriptor,
+ adapter: mongoAdapter,
+ driver: mongoDriver,
+ contract: mongoContract('./prisma/contract.prisma', {
+ output: 'src/contract.json',
+ scalarTypeDescriptors,
+ }),
+ db: {
+ connection: process.env['DB_URL'] ?? 'mongodb://localhost:27017/retail-store',
+ },
+});
diff --git a/examples/retail-store/prisma/contract.prisma b/examples/retail-store/prisma/contract.prisma
new file mode 100644
index 0000000000..c03cbe0247
--- /dev/null
+++ b/examples/retail-store/prisma/contract.prisma
@@ -0,0 +1,151 @@
+type Price {
+ amount Float
+ currency String
+}
+
+type Image {
+ url String
+}
+
+type Address {
+ streetAndNumber String
+ city String
+ postalCode String
+ country String
+}
+
+type CartItem {
+ productId String
+ name String
+ brand String
+ amount Int
+ price Price
+ image Image
+}
+
+type OrderLineItem {
+ productId String
+ name String
+ brand String
+ amount Int
+ price Price
+ image Image
+}
+
+type StatusEntry {
+ status String
+ timestamp DateTime
+}
+
+type InvoiceLineItem {
+ name String
+ amount Int
+ unitPrice Float
+ lineTotal Float
+}
+
+model Product {
+ id ObjectId @id @map("_id")
+ name String
+ brand String
+ code String
+ description String
+ masterCategory String
+ subCategory String
+ articleType String
+ price Price
+ image Image
+ embedding Float[]?
+ @@map("products")
+ @@textIndex([name, description], weights: "{\"name\": 10, \"description\": 1}")
+ @@index([brand, subCategory])
+ @@index([code], type: "hashed")
+}
+
+model User {
+ id ObjectId @id @map("_id")
+ name String
+ email String @unique
+ address Address?
+ carts Cart[]
+ orders Order[]
+ @@map("users")
+}
+
+model Cart {
+ id ObjectId @id @map("_id")
+ userId ObjectId
+ items CartItem[]
+ user User @relation(fields: [userId], references: [id])
+ @@map("carts")
+ @@unique([userId])
+}
+
+model Order {
+ id ObjectId @id @map("_id")
+ userId ObjectId
+ items OrderLineItem[]
+ shippingAddress String
+ type String
+ statusHistory StatusEntry[]
+ user User @relation(fields: [userId], references: [id])
+ invoices Invoice[]
+ @@map("orders")
+ @@index([userId])
+}
+
+model Location {
+ id ObjectId @id @map("_id")
+ name String
+ streetAndNumber String
+ city String
+ postalCode String
+ country String
+ @@map("locations")
+ @@index([city, country], collationLocale: "en", collationStrength: 2)
+}
+
+model Invoice {
+ id ObjectId @id @map("_id")
+ orderId ObjectId
+ items InvoiceLineItem[]
+ subtotal Float
+ tax Float
+ total Float
+ issuedAt DateTime
+ order Order @relation(fields: [orderId], references: [id])
+ @@map("invoices")
+ @@index([orderId])
+ @@index([issuedAt(sort: Desc)], sparse: true)
+}
+
+model Event {
+ id ObjectId @id @map("_id")
+ userId String
+ sessionId String
+ type String
+ timestamp DateTime
+ @@map("events")
+ @@discriminator(type)
+ @@index([userId, timestamp(sort: Desc)])
+ @@index([timestamp], expireAfterSeconds: 7776000)
+}
+
+model ViewProductEvent {
+ productId String
+ subCategory String
+ brand String
+ exitMethod String?
+ @@base(Event, "view-product")
+}
+
+model SearchEvent {
+ query String
+ @@base(Event, "search")
+}
+
+model AddToCartEvent {
+ productId String
+ brand String
+ @@base(Event, "add-to-cart")
+}
diff --git a/examples/retail-store/public/images/products/cra-bag-017.jpg b/examples/retail-store/public/images/products/cra-bag-017.jpg
new file mode 100644
index 0000000000..20886fa3c4
Binary files /dev/null and b/examples/retail-store/public/images/products/cra-bag-017.jpg differ
diff --git a/examples/retail-store/public/images/products/cra-blt-019.jpg b/examples/retail-store/public/images/products/cra-blt-019.jpg
new file mode 100644
index 0000000000..9bd091c023
Binary files /dev/null and b/examples/retail-store/public/images/products/cra-blt-019.jpg differ
diff --git a/examples/retail-store/public/images/products/cra-tot-018.jpg b/examples/retail-store/public/images/products/cra-tot-018.jpg
new file mode 100644
index 0000000000..6f229ab13e
Binary files /dev/null and b/examples/retail-store/public/images/products/cra-tot-018.jpg differ
diff --git a/examples/retail-store/public/images/products/her-che-022.jpg b/examples/retail-store/public/images/products/her-che-022.jpg
new file mode 100644
index 0000000000..45133c2a2a
Binary files /dev/null and b/examples/retail-store/public/images/products/her-che-022.jpg differ
diff --git a/examples/retail-store/public/images/products/her-drs-021.jpg b/examples/retail-store/public/images/products/her-drs-021.jpg
new file mode 100644
index 0000000000..a71188455d
Binary files /dev/null and b/examples/retail-store/public/images/products/her-drs-021.jpg differ
diff --git a/examples/retail-store/public/images/products/her-jea-020.jpg b/examples/retail-store/public/images/products/her-jea-020.jpg
new file mode 100644
index 0000000000..7a0e6f70c7
Binary files /dev/null and b/examples/retail-store/public/images/products/her-jea-020.jpg differ
diff --git a/examples/retail-store/public/images/products/her-lin-002.jpg b/examples/retail-store/public/images/products/her-lin-002.jpg
new file mode 100644
index 0000000000..e13f4cf467
Binary files /dev/null and b/examples/retail-store/public/images/products/her-lin-002.jpg differ
diff --git a/examples/retail-store/public/images/products/her-mer-003.jpg b/examples/retail-store/public/images/products/her-mer-003.jpg
new file mode 100644
index 0000000000..1a6dcfd2c2
Binary files /dev/null and b/examples/retail-store/public/images/products/her-mer-003.jpg differ
diff --git a/examples/retail-store/public/images/products/her-oxf-001.jpg b/examples/retail-store/public/images/products/her-oxf-001.jpg
new file mode 100644
index 0000000000..0937ed0482
Binary files /dev/null and b/examples/retail-store/public/images/products/her-oxf-001.jpg differ
diff --git a/examples/retail-store/public/images/products/her-psq-023.jpg b/examples/retail-store/public/images/products/her-psq-023.jpg
new file mode 100644
index 0000000000..1a6d92ade8
Binary files /dev/null and b/examples/retail-store/public/images/products/her-psq-023.jpg differ
diff --git a/examples/retail-store/public/images/products/tm-bea-032.jpg b/examples/retail-store/public/images/products/tm-bea-032.jpg
new file mode 100644
index 0000000000..a92e479fe2
Binary files /dev/null and b/examples/retail-store/public/images/products/tm-bea-032.jpg differ
diff --git a/examples/retail-store/public/images/products/tm-duf-031.jpg b/examples/retail-store/public/images/products/tm-duf-031.jpg
new file mode 100644
index 0000000000..cc3ccc4837
Binary files /dev/null and b/examples/retail-store/public/images/products/tm-duf-031.jpg differ
diff --git a/examples/retail-store/public/images/products/tm-hik-034.jpg b/examples/retail-store/public/images/products/tm-hik-034.jpg
new file mode 100644
index 0000000000..8efd3b5a11
Binary files /dev/null and b/examples/retail-store/public/images/products/tm-hik-034.jpg differ
diff --git a/examples/retail-store/public/images/products/tm-rai-035.jpg b/examples/retail-store/public/images/products/tm-rai-035.jpg
new file mode 100644
index 0000000000..ae2e06e908
Binary files /dev/null and b/examples/retail-store/public/images/products/tm-rai-035.jpg differ
diff --git a/examples/retail-store/public/images/products/tm-sho-030.jpg b/examples/retail-store/public/images/products/tm-sho-030.jpg
new file mode 100644
index 0000000000..852e960c52
Binary files /dev/null and b/examples/retail-store/public/images/products/tm-sho-030.jpg differ
diff --git a/examples/retail-store/public/images/products/tm-trl-033.jpg b/examples/retail-store/public/images/products/tm-trl-033.jpg
new file mode 100644
index 0000000000..5e0e521ff7
Binary files /dev/null and b/examples/retail-store/public/images/products/tm-trl-033.jpg differ
diff --git a/examples/retail-store/public/images/products/ue-chi-042.jpg b/examples/retail-store/public/images/products/ue-chi-042.jpg
new file mode 100644
index 0000000000..eebf920fcf
Binary files /dev/null and b/examples/retail-store/public/images/products/ue-chi-042.jpg differ
diff --git a/examples/retail-store/public/images/products/ue-den-012.jpg b/examples/retail-store/public/images/products/ue-den-012.jpg
new file mode 100644
index 0000000000..63200db1af
Binary files /dev/null and b/examples/retail-store/public/images/products/ue-den-012.jpg differ
diff --git a/examples/retail-store/public/images/products/ue-jog-013.jpg b/examples/retail-store/public/images/products/ue-jog-013.jpg
new file mode 100644
index 0000000000..96a6173344
Binary files /dev/null and b/examples/retail-store/public/images/products/ue-jog-013.jpg differ
diff --git a/examples/retail-store/public/images/products/ue-pol-011.jpg b/examples/retail-store/public/images/products/ue-pol-011.jpg
new file mode 100644
index 0000000000..39719373d7
Binary files /dev/null and b/examples/retail-store/public/images/products/ue-pol-011.jpg differ
diff --git a/examples/retail-store/public/images/products/ue-slp-016.jpg b/examples/retail-store/public/images/products/ue-slp-016.jpg
new file mode 100644
index 0000000000..55c092abb1
Binary files /dev/null and b/examples/retail-store/public/images/products/ue-slp-016.jpg differ
diff --git a/examples/retail-store/public/images/products/ue-snk-015.jpg b/examples/retail-store/public/images/products/ue-snk-015.jpg
new file mode 100644
index 0000000000..4dda45c6eb
Binary files /dev/null and b/examples/retail-store/public/images/products/ue-snk-015.jpg differ
diff --git a/examples/retail-store/public/images/products/ue-sun-014.jpg b/examples/retail-store/public/images/products/ue-sun-014.jpg
new file mode 100644
index 0000000000..34f070b606
Binary files /dev/null and b/examples/retail-store/public/images/products/ue-sun-014.jpg differ
diff --git a/examples/retail-store/public/images/products/ue-tee-010.jpg b/examples/retail-store/public/images/products/ue-tee-010.jpg
new file mode 100644
index 0000000000..6eb2f55c4d
Binary files /dev/null and b/examples/retail-store/public/images/products/ue-tee-010.jpg differ
diff --git a/examples/retail-store/scripts/reset-db.ts b/examples/retail-store/scripts/reset-db.ts
new file mode 100644
index 0000000000..967e66da05
--- /dev/null
+++ b/examples/retail-store/scripts/reset-db.ts
@@ -0,0 +1,42 @@
+import { existsSync } from 'node:fs';
+import { MongoClient } from 'mongodb';
+
+if (existsSync('.env')) {
+ process.loadEnvFile('.env');
+}
+
+async function main() {
+ const url = process.env['DB_URL'];
+ if (!url) {
+ console.error('DB_URL is required. Set it in your environment or .env file.');
+ process.exit(1);
+ }
+
+ const dbName = process.env['MONGODB_DB'] ?? 'retail-store';
+
+ const client = new MongoClient(url);
+ try {
+ await client.connect();
+ const db = client.db(dbName);
+
+ const collections = await db.listCollections().toArray();
+ if (collections.length === 0) {
+ console.log(`Database "${dbName}" has no collections — nothing to drop.`);
+ return;
+ }
+
+ console.log(`Dropping ${collections.length} collection(s) from "${dbName}"...`);
+ for (const col of collections) {
+ await db.dropCollection(col.name);
+ console.log(` ✔ ${col.name}`);
+ }
+ console.log('\nDatabase reset complete. Run `pnpm db:seed` to re-populate.');
+ } finally {
+ await client.close();
+ }
+}
+
+main().catch((err) => {
+ console.error('Fatal:', err);
+ process.exit(1);
+});
diff --git a/examples/retail-store/scripts/seed.ts b/examples/retail-store/scripts/seed.ts
new file mode 100644
index 0000000000..3bb5937933
--- /dev/null
+++ b/examples/retail-store/scripts/seed.ts
@@ -0,0 +1,33 @@
+import { existsSync } from 'node:fs';
+import { createClient } from '../src/db';
+import { seed } from '../src/seed';
+
+if (existsSync('.env')) {
+ process.loadEnvFile('.env');
+}
+
+async function main() {
+ const url = process.env['DB_URL'];
+ if (!url) {
+ console.error('DB_URL is required. Set it in your environment or .env file.');
+ process.exit(1);
+ }
+
+ const dbName = process.env['MONGODB_DB'] ?? 'retail-store';
+
+ console.log('Connecting to MongoDB...');
+ const db = await createClient(url, dbName);
+
+ try {
+ console.log('Seeding data...');
+ await seed(db);
+ console.log('Seed complete.');
+ } finally {
+ await db.runtime.close();
+ }
+}
+
+main().catch((err) => {
+ console.error('Fatal:', err);
+ process.exit(1);
+});
diff --git a/examples/retail-store/src/components/add-to-cart-button.tsx b/examples/retail-store/src/components/add-to-cart-button.tsx
new file mode 100644
index 0000000000..d7cb571b3e
--- /dev/null
+++ b/examples/retail-store/src/components/add-to-cart-button.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { useRef, useState } from 'react';
+import { useCart } from './cart-provider';
+import { Button } from './ui/button';
+
+interface AddToCartButtonProps {
+ product: {
+ _id: string;
+ name: string;
+ brand: string;
+ price: { amount: number; currency: string };
+ image: { url: string };
+ };
+}
+
+export function AddToCartButton({ product }: AddToCartButtonProps) {
+ const { invalidateCart } = useCart();
+ const [state, setState] = useState<'idle' | 'loading' | 'added'>('idle');
+ const timerRef = useRef>(undefined);
+
+ async function handleAdd() {
+ clearTimeout(timerRef.current);
+ setState('loading');
+ try {
+ const res = await fetch('/api/cart', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ productId: product._id,
+ name: product.name,
+ brand: product.brand,
+ amount: 1,
+ price: product.price,
+ image: product.image,
+ }),
+ });
+ if (!res.ok) throw new Error(`Add to cart failed (${res.status})`);
+ invalidateCart();
+ setState('added');
+ timerRef.current = setTimeout(() => setState('idle'), 1500);
+ } catch {
+ setState('idle');
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/examples/retail-store/src/components/cart-badge.tsx b/examples/retail-store/src/components/cart-badge.tsx
new file mode 100644
index 0000000000..ab3099d89d
--- /dev/null
+++ b/examples/retail-store/src/components/cart-badge.tsx
@@ -0,0 +1,14 @@
+'use client';
+
+import { useCart } from './cart-provider';
+
+export function CartBadge() {
+ const { count } = useCart();
+ if (count === 0) return null;
+
+ return (
+
+ {count}
+
+ );
+}
diff --git a/examples/retail-store/src/components/cart-provider.tsx b/examples/retail-store/src/components/cart-provider.tsx
new file mode 100644
index 0000000000..96695befae
--- /dev/null
+++ b/examples/retail-store/src/components/cart-provider.tsx
@@ -0,0 +1,51 @@
+'use client';
+
+import {
+ createContext,
+ type ReactNode,
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
+
+interface CartContextValue {
+ count: number;
+ invalidateCart: () => void;
+}
+
+const CartContext = createContext({ count: 0, invalidateCart: () => {} });
+
+export function useCart() {
+ return useContext(CartContext);
+}
+
+export function CartProvider({ children }: { children: ReactNode }) {
+ const [count, setCount] = useState(0);
+ const mountedRef = useRef(false);
+
+ useEffect(() => {
+ mountedRef.current = true;
+ fetch('/api/cart/count')
+ .then((res) => res.json())
+ .then((data: { count: number }) => {
+ if (mountedRef.current) setCount(data.count);
+ })
+ .catch(() => {});
+ return () => {
+ mountedRef.current = false;
+ };
+ }, []);
+
+ const invalidateCart = useCallback(() => {
+ fetch('/api/cart/count')
+ .then((res) => res.json())
+ .then((data: { count: number }) => {
+ if (mountedRef.current) setCount(data.count);
+ })
+ .catch(() => {});
+ }, []);
+
+ return {children};
+}
diff --git a/examples/retail-store/src/components/navbar-client.tsx b/examples/retail-store/src/components/navbar-client.tsx
new file mode 100644
index 0000000000..851c4ee610
--- /dev/null
+++ b/examples/retail-store/src/components/navbar-client.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { Button } from './ui/button';
+
+export function NavbarClient({ userName }: { userName: string }) {
+ const router = useRouter();
+
+ async function handleLogout() {
+ await fetch('/api/auth/logout', { method: 'POST' });
+ router.push('/login');
+ router.refresh();
+ }
+
+ return (
+
+ {userName}
+
+
+ );
+}
diff --git a/examples/retail-store/src/components/navbar.tsx b/examples/retail-store/src/components/navbar.tsx
new file mode 100644
index 0000000000..a09e410851
--- /dev/null
+++ b/examples/retail-store/src/components/navbar.tsx
@@ -0,0 +1,38 @@
+import Link from 'next/link';
+import { getAuthUser } from '../lib/auth';
+import { CartBadge } from './cart-badge';
+import { NavbarClient } from './navbar-client';
+
+export async function Navbar() {
+ const user = await getAuthUser();
+
+ return (
+
+ );
+}
diff --git a/examples/retail-store/src/components/ui/badge.tsx b/examples/retail-store/src/components/ui/badge.tsx
new file mode 100644
index 0000000000..144398364b
--- /dev/null
+++ b/examples/retail-store/src/components/ui/badge.tsx
@@ -0,0 +1,28 @@
+import { cva, type VariantProps } from 'class-variance-authority';
+import type { HTMLAttributes } from 'react';
+import { cn } from '../../lib/utils';
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-sm px-2 py-0.5 text-xs font-semibold uppercase',
+ {
+ variants: {
+ variant: {
+ default: 'bg-accent text-accent-foreground',
+ success: 'bg-success text-white',
+ warning: 'bg-warning text-white',
+ destructive: 'bg-destructive text-white',
+ outline: 'border border-border text-foreground',
+ muted: 'bg-muted/20 text-muted',
+ },
+ },
+ defaultVariants: { variant: 'default' },
+ },
+);
+
+export interface BadgeProps
+ extends HTMLAttributes,
+ VariantProps {}
+
+export function Badge({ className, variant, ...props }: BadgeProps) {
+ return ;
+}
diff --git a/examples/retail-store/src/components/ui/button.tsx b/examples/retail-store/src/components/ui/button.tsx
new file mode 100644
index 0000000000..4ea0f393dd
--- /dev/null
+++ b/examples/retail-store/src/components/ui/button.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+import type { ButtonHTMLAttributes } from 'react';
+import { cn } from '../../lib/utils';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 cursor-pointer',
+ {
+ variants: {
+ variant: {
+ default: 'bg-accent text-accent-foreground hover:bg-accent/90',
+ destructive: 'bg-destructive text-white hover:bg-destructive/90',
+ outline: 'border border-border bg-card hover:bg-muted/20',
+ ghost: 'hover:bg-muted/20',
+ link: 'text-accent underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-10 px-4 py-2',
+ sm: 'h-8 px-3 text-xs',
+ lg: 'h-12 px-6 text-base',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: { variant: 'default', size: 'default' },
+ },
+);
+
+export interface ButtonProps
+ extends ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+export function Button({ className, variant, size, asChild, ...props }: ButtonProps) {
+ const Comp = asChild ? Slot : 'button';
+ return ;
+}
diff --git a/examples/retail-store/src/components/ui/card.tsx b/examples/retail-store/src/components/ui/card.tsx
new file mode 100644
index 0000000000..c74314d09f
--- /dev/null
+++ b/examples/retail-store/src/components/ui/card.tsx
@@ -0,0 +1,31 @@
+import type { HTMLAttributes } from 'react';
+import { cn } from '../../lib/utils';
+
+export function Card({ className, ...props }: HTMLAttributes) {
+ return (
+
+ );
+}
+
+export function CardHeader({ className, ...props }: HTMLAttributes) {
+ return ;
+}
+
+export function CardTitle({ className, ...props }: HTMLAttributes) {
+ return ;
+}
+
+export function CardDescription({ className, ...props }: HTMLAttributes) {
+ return ;
+}
+
+export function CardContent({ className, ...props }: HTMLAttributes) {
+ return ;
+}
+
+export function CardFooter({ className, ...props }: HTMLAttributes) {
+ return ;
+}
diff --git a/examples/retail-store/src/components/ui/dropdown-menu.tsx b/examples/retail-store/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000000..8b471169c0
--- /dev/null
+++ b/examples/retail-store/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import type { ComponentPropsWithoutRef } from 'react';
+import { cn } from '../../lib/utils';
+
+export const DropdownMenu = DropdownMenuPrimitive.Root;
+export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+export function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return (
+
+
+
+ );
+}
+
+export function DropdownMenuItem({
+ className,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return (
+
+ );
+}
diff --git a/examples/retail-store/src/components/ui/input.tsx b/examples/retail-store/src/components/ui/input.tsx
new file mode 100644
index 0000000000..db201edbea
--- /dev/null
+++ b/examples/retail-store/src/components/ui/input.tsx
@@ -0,0 +1,14 @@
+import type { InputHTMLAttributes } from 'react';
+import { cn } from '../../lib/utils';
+
+export function Input({ className, ...props }: InputHTMLAttributes) {
+ return (
+
+ );
+}
diff --git a/examples/retail-store/src/components/ui/radio-group.tsx b/examples/retail-store/src/components/ui/radio-group.tsx
new file mode 100644
index 0000000000..5eccf3db2a
--- /dev/null
+++ b/examples/retail-store/src/components/ui/radio-group.tsx
@@ -0,0 +1,32 @@
+'use client';
+
+import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
+import { Circle } from 'lucide-react';
+import type { ComponentPropsWithoutRef } from 'react';
+import { cn } from '../../lib/utils';
+
+export function RadioGroup({
+ className,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return ;
+}
+
+export function RadioGroupItem({
+ className,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/examples/retail-store/src/components/ui/select.tsx b/examples/retail-store/src/components/ui/select.tsx
new file mode 100644
index 0000000000..ea9e72c0a9
--- /dev/null
+++ b/examples/retail-store/src/components/ui/select.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+import * as SelectPrimitive from '@radix-ui/react-select';
+import { ChevronDown } from 'lucide-react';
+import type { ComponentPropsWithoutRef } from 'react';
+import { cn } from '../../lib/utils';
+
+export const Select = SelectPrimitive.Root;
+export const SelectGroup = SelectPrimitive.Group;
+export const SelectValue = SelectPrimitive.Value;
+
+export function SelectTrigger({
+ className,
+ children,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return (
+
+ {children}
+
+
+
+
+ );
+}
+
+export function SelectContent({
+ className,
+ children,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export function SelectItem({
+ className,
+ children,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/examples/retail-store/src/components/ui/separator.tsx b/examples/retail-store/src/components/ui/separator.tsx
new file mode 100644
index 0000000000..f9d52418b6
--- /dev/null
+++ b/examples/retail-store/src/components/ui/separator.tsx
@@ -0,0 +1,25 @@
+'use client';
+
+import * as SeparatorPrimitive from '@radix-ui/react-separator';
+import type { ComponentPropsWithoutRef } from 'react';
+import { cn } from '../../lib/utils';
+
+export function Separator({
+ className,
+ orientation = 'horizontal',
+ decorative = true,
+ ...props
+}: ComponentPropsWithoutRef) {
+ return (
+
+ );
+}
diff --git a/examples/retail-store/src/contract.d.ts b/examples/retail-store/src/contract.d.ts
new file mode 100644
index 0000000000..6e93c4ac1f
--- /dev/null
+++ b/examples/retail-store/src/contract.d.ts
@@ -0,0 +1,962 @@
+// ⚠️ GENERATED FILE - DO NOT EDIT
+// This file is automatically generated by 'prisma-next contract emit'.
+// To regenerate, run: prisma-next contract emit
+import type { CodecTypes as MongoCodecTypes } from '@prisma-next/adapter-mongo/codec-types';
+import type { Vector } from '@prisma-next/adapter-mongo/codec-types';
+
+import type { MongoContractWithTypeMaps, MongoTypeMaps } from '@prisma-next/mongo-contract';
+import type {
+ Contract as ContractType,
+ ExecutionHashBase,
+ ProfileHashBase,
+ StorageHashBase,
+} from '@prisma-next/contract/types';
+
+export type StorageHash =
+ StorageHashBase<'sha256:5f5aee268e41d345787b7c5d396778443a977b10dc2ec9f14c913171fd6d1411'>;
+export type ExecutionHash = ExecutionHashBase;
+export type ProfileHash =
+ ProfileHashBase<'sha256:840de65fba7eb950a31487f74ee420b9c21205f38bce58579026747e0264e840'>;
+
+export type CodecTypes = MongoCodecTypes;
+export type OperationTypes = Record;
+
+export type Price = {
+ readonly amount: CodecTypes['mongo/double@1']['output'];
+ readonly currency: CodecTypes['mongo/string@1']['output'];
+};
+export type Image = { readonly url: CodecTypes['mongo/string@1']['output'] };
+export type Address = {
+ readonly streetAndNumber: CodecTypes['mongo/string@1']['output'];
+ readonly city: CodecTypes['mongo/string@1']['output'];
+ readonly postalCode: CodecTypes['mongo/string@1']['output'];
+ readonly country: CodecTypes['mongo/string@1']['output'];
+};
+export type CartItem = {
+ readonly productId: CodecTypes['mongo/string@1']['output'];
+ readonly name: CodecTypes['mongo/string@1']['output'];
+ readonly brand: CodecTypes['mongo/string@1']['output'];
+ readonly amount: CodecTypes['mongo/int32@1']['output'];
+ readonly price: Price;
+ readonly image: Image;
+};
+export type OrderLineItem = {
+ readonly productId: CodecTypes['mongo/string@1']['output'];
+ readonly name: CodecTypes['mongo/string@1']['output'];
+ readonly brand: CodecTypes['mongo/string@1']['output'];
+ readonly amount: CodecTypes['mongo/int32@1']['output'];
+ readonly price: Price;
+ readonly image: Image;
+};
+export type StatusEntry = {
+ readonly status: CodecTypes['mongo/string@1']['output'];
+ readonly timestamp: CodecTypes['mongo/date@1']['output'];
+};
+export type InvoiceLineItem = {
+ readonly name: CodecTypes['mongo/string@1']['output'];
+ readonly amount: CodecTypes['mongo/int32@1']['output'];
+ readonly unitPrice: CodecTypes['mongo/double@1']['output'];
+ readonly lineTotal: CodecTypes['mongo/double@1']['output'];
+};
+export type FieldOutputTypes = {
+ readonly AddToCartEvent: {
+ readonly productId: CodecTypes['mongo/string@1']['output'];
+ readonly brand: CodecTypes['mongo/string@1']['output'];
+ };
+ readonly Cart: {
+ readonly _id: CodecTypes['mongo/objectId@1']['output'];
+ readonly userId: CodecTypes['mongo/objectId@1']['output'];
+ readonly items: ReadonlyArray;
+ };
+ readonly Event: {
+ readonly _id: CodecTypes['mongo/objectId@1']['output'];
+ readonly userId: CodecTypes['mongo/string@1']['output'];
+ readonly sessionId: CodecTypes['mongo/string@1']['output'];
+ readonly type: CodecTypes['mongo/string@1']['output'];
+ readonly timestamp: CodecTypes['mongo/date@1']['output'];
+ };
+ readonly Invoice: {
+ readonly _id: CodecTypes['mongo/objectId@1']['output'];
+ readonly orderId: CodecTypes['mongo/objectId@1']['output'];
+ readonly items: ReadonlyArray;
+ readonly subtotal: CodecTypes['mongo/double@1']['output'];
+ readonly tax: CodecTypes['mongo/double@1']['output'];
+ readonly total: CodecTypes['mongo/double@1']['output'];
+ readonly issuedAt: CodecTypes['mongo/date@1']['output'];
+ };
+ readonly Location: {
+ readonly _id: CodecTypes['mongo/objectId@1']['output'];
+ readonly name: CodecTypes['mongo/string@1']['output'];
+ readonly streetAndNumber: CodecTypes['mongo/string@1']['output'];
+ readonly city: CodecTypes['mongo/string@1']['output'];
+ readonly postalCode: CodecTypes['mongo/string@1']['output'];
+ readonly country: CodecTypes['mongo/string@1']['output'];
+ };
+ readonly Order: {
+ readonly _id: CodecTypes['mongo/objectId@1']['output'];
+ readonly userId: CodecTypes['mongo/objectId@1']['output'];
+ readonly items: ReadonlyArray;
+ readonly shippingAddress: CodecTypes['mongo/string@1']['output'];
+ readonly type: CodecTypes['mongo/string@1']['output'];
+ readonly statusHistory: ReadonlyArray;
+ };
+ readonly Product: {
+ readonly _id: CodecTypes['mongo/objectId@1']['output'];
+ readonly name: CodecTypes['mongo/string@1']['output'];
+ readonly brand: CodecTypes['mongo/string@1']['output'];
+ readonly code: CodecTypes['mongo/string@1']['output'];
+ readonly description: CodecTypes['mongo/string@1']['output'];
+ readonly masterCategory: CodecTypes['mongo/string@1']['output'];
+ readonly subCategory: CodecTypes['mongo/string@1']['output'];
+ readonly articleType: CodecTypes['mongo/string@1']['output'];
+ readonly price: Price;
+ readonly image: Image;
+ readonly embedding: ReadonlyArray | null;
+ };
+ readonly SearchEvent: { readonly query: CodecTypes['mongo/string@1']['output'] };
+ readonly User: {
+ readonly _id: CodecTypes['mongo/objectId@1']['output'];
+ readonly name: CodecTypes['mongo/string@1']['output'];
+ readonly email: CodecTypes['mongo/string@1']['output'];
+ readonly address: Address | null;
+ };
+ readonly ViewProductEvent: {
+ readonly productId: CodecTypes['mongo/string@1']['output'];
+ readonly subCategory: CodecTypes['mongo/string@1']['output'];
+ readonly brand: CodecTypes['mongo/string@1']['output'];
+ readonly exitMethod: CodecTypes['mongo/string@1']['output'] | null;
+ };
+};
+export type TypeMaps = MongoTypeMaps;
+
+type ContractBase = ContractType<
+ {
+ readonly collections: {
+ readonly products: {
+ readonly indexes: readonly [
+ {
+ readonly keys: readonly [
+ { readonly field: 'name'; readonly direction: 'text' },
+ { readonly field: 'description'; readonly direction: 'text' },
+ ];
+ readonly weights: { readonly name: 10; readonly description: 1 };
+ },
+ {
+ readonly keys: readonly [
+ { readonly field: 'brand'; readonly direction: 1 },
+ { readonly field: 'subCategory'; readonly direction: 1 },
+ ];
+ },
+ { readonly keys: readonly [{ readonly field: 'code'; readonly direction: 'hashed' }] },
+ ];
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly _id: { readonly bsonType: 'objectId' };
+ readonly name: { readonly bsonType: 'string' };
+ readonly brand: { readonly bsonType: 'string' };
+ readonly code: { readonly bsonType: 'string' };
+ readonly description: { readonly bsonType: 'string' };
+ readonly masterCategory: { readonly bsonType: 'string' };
+ readonly subCategory: { readonly bsonType: 'string' };
+ readonly articleType: { readonly bsonType: 'string' };
+ readonly price: {
+ readonly bsonType: 'object';
+ readonly properties: { readonly currency: { readonly bsonType: 'string' } };
+ readonly required: readonly ['currency'];
+ };
+ readonly image: {
+ readonly bsonType: 'object';
+ readonly properties: { readonly url: { readonly bsonType: 'string' } };
+ readonly required: readonly ['url'];
+ };
+ };
+ readonly required: readonly [
+ '_id',
+ 'articleType',
+ 'brand',
+ 'code',
+ 'description',
+ 'image',
+ 'masterCategory',
+ 'name',
+ 'price',
+ 'subCategory',
+ ];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly users: {
+ readonly indexes: readonly [
+ {
+ readonly keys: readonly [{ readonly field: 'email'; readonly direction: 1 }];
+ readonly unique: true;
+ },
+ ];
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly _id: { readonly bsonType: 'objectId' };
+ readonly name: { readonly bsonType: 'string' };
+ readonly email: { readonly bsonType: 'string' };
+ readonly address: {
+ readonly oneOf: readonly [
+ { readonly bsonType: 'null' },
+ {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly streetAndNumber: { readonly bsonType: 'string' };
+ readonly city: { readonly bsonType: 'string' };
+ readonly postalCode: { readonly bsonType: 'string' };
+ readonly country: { readonly bsonType: 'string' };
+ };
+ readonly required: readonly [
+ 'city',
+ 'country',
+ 'postalCode',
+ 'streetAndNumber',
+ ];
+ },
+ ];
+ };
+ };
+ readonly required: readonly ['_id', 'email', 'name'];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly carts: {
+ readonly indexes: readonly [
+ {
+ readonly keys: readonly [{ readonly field: 'userId'; readonly direction: 1 }];
+ readonly unique: true;
+ },
+ ];
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly _id: { readonly bsonType: 'objectId' };
+ readonly userId: { readonly bsonType: 'objectId' };
+ readonly items: {
+ readonly bsonType: 'array';
+ readonly items: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly productId: { readonly bsonType: 'string' };
+ readonly name: { readonly bsonType: 'string' };
+ readonly brand: { readonly bsonType: 'string' };
+ readonly amount: { readonly bsonType: 'int' };
+ readonly price: {
+ readonly bsonType: 'object';
+ readonly properties: { readonly currency: { readonly bsonType: 'string' } };
+ readonly required: readonly ['currency'];
+ };
+ readonly image: {
+ readonly bsonType: 'object';
+ readonly properties: { readonly url: { readonly bsonType: 'string' } };
+ readonly required: readonly ['url'];
+ };
+ };
+ readonly required: readonly [
+ 'amount',
+ 'brand',
+ 'image',
+ 'name',
+ 'price',
+ 'productId',
+ ];
+ };
+ };
+ };
+ readonly required: readonly ['_id', 'items', 'userId'];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly orders: {
+ readonly indexes: readonly [
+ { readonly keys: readonly [{ readonly field: 'userId'; readonly direction: 1 }] },
+ ];
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly _id: { readonly bsonType: 'objectId' };
+ readonly userId: { readonly bsonType: 'objectId' };
+ readonly items: {
+ readonly bsonType: 'array';
+ readonly items: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly productId: { readonly bsonType: 'string' };
+ readonly name: { readonly bsonType: 'string' };
+ readonly brand: { readonly bsonType: 'string' };
+ readonly amount: { readonly bsonType: 'int' };
+ readonly price: {
+ readonly bsonType: 'object';
+ readonly properties: { readonly currency: { readonly bsonType: 'string' } };
+ readonly required: readonly ['currency'];
+ };
+ readonly image: {
+ readonly bsonType: 'object';
+ readonly properties: { readonly url: { readonly bsonType: 'string' } };
+ readonly required: readonly ['url'];
+ };
+ };
+ readonly required: readonly [
+ 'amount',
+ 'brand',
+ 'image',
+ 'name',
+ 'price',
+ 'productId',
+ ];
+ };
+ };
+ readonly shippingAddress: { readonly bsonType: 'string' };
+ readonly type: { readonly bsonType: 'string' };
+ readonly statusHistory: {
+ readonly bsonType: 'array';
+ readonly items: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly status: { readonly bsonType: 'string' };
+ readonly timestamp: { readonly bsonType: 'date' };
+ };
+ readonly required: readonly ['status', 'timestamp'];
+ };
+ };
+ };
+ readonly required: readonly [
+ '_id',
+ 'items',
+ 'shippingAddress',
+ 'statusHistory',
+ 'type',
+ 'userId',
+ ];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly locations: {
+ readonly indexes: readonly [
+ {
+ readonly keys: readonly [
+ { readonly field: 'city'; readonly direction: 1 },
+ { readonly field: 'country'; readonly direction: 1 },
+ ];
+ readonly collation: { readonly locale: 'en'; readonly strength: 2 };
+ },
+ ];
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly _id: { readonly bsonType: 'objectId' };
+ readonly name: { readonly bsonType: 'string' };
+ readonly streetAndNumber: { readonly bsonType: 'string' };
+ readonly city: { readonly bsonType: 'string' };
+ readonly postalCode: { readonly bsonType: 'string' };
+ readonly country: { readonly bsonType: 'string' };
+ };
+ readonly required: readonly [
+ '_id',
+ 'city',
+ 'country',
+ 'name',
+ 'postalCode',
+ 'streetAndNumber',
+ ];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly invoices: {
+ readonly indexes: readonly [
+ { readonly keys: readonly [{ readonly field: 'orderId'; readonly direction: 1 }] },
+ {
+ readonly keys: readonly [{ readonly field: 'issuedAt'; readonly direction: -1 }];
+ readonly sparse: true;
+ },
+ ];
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly _id: { readonly bsonType: 'objectId' };
+ readonly orderId: { readonly bsonType: 'objectId' };
+ readonly items: {
+ readonly bsonType: 'array';
+ readonly items: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly name: { readonly bsonType: 'string' };
+ readonly amount: { readonly bsonType: 'int' };
+ };
+ readonly required: readonly ['amount', 'name'];
+ };
+ };
+ readonly issuedAt: { readonly bsonType: 'date' };
+ };
+ readonly required: readonly ['_id', 'issuedAt', 'items', 'orderId'];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly events: {
+ readonly indexes: readonly [
+ {
+ readonly keys: readonly [
+ { readonly field: 'userId'; readonly direction: 1 },
+ { readonly field: 'timestamp'; readonly direction: -1 },
+ ];
+ },
+ {
+ readonly keys: readonly [{ readonly field: 'timestamp'; readonly direction: 1 }];
+ readonly expireAfterSeconds: 7776000;
+ },
+ ];
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly _id: { readonly bsonType: 'objectId' };
+ readonly userId: { readonly bsonType: 'string' };
+ readonly sessionId: { readonly bsonType: 'string' };
+ readonly type: { readonly bsonType: 'string' };
+ readonly timestamp: { readonly bsonType: 'date' };
+ };
+ readonly required: readonly ['_id', 'sessionId', 'timestamp', 'type', 'userId'];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly viewProductEvent: {
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly productId: { readonly bsonType: 'string' };
+ readonly subCategory: { readonly bsonType: 'string' };
+ readonly brand: { readonly bsonType: 'string' };
+ readonly exitMethod: { readonly bsonType: readonly ['null', 'string'] };
+ };
+ readonly required: readonly ['brand', 'productId', 'subCategory'];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly searchEvent: {
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: { readonly query: { readonly bsonType: 'string' } };
+ readonly required: readonly ['query'];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ readonly addToCartEvent: {
+ readonly validator: {
+ readonly jsonSchema: {
+ readonly bsonType: 'object';
+ readonly properties: {
+ readonly productId: { readonly bsonType: 'string' };
+ readonly brand: { readonly bsonType: 'string' };
+ };
+ readonly required: readonly ['brand', 'productId'];
+ };
+ readonly validationLevel: 'strict';
+ readonly validationAction: 'error';
+ };
+ };
+ };
+ readonly storageHash: StorageHash;
+ },
+ {
+ readonly AddToCartEvent: {
+ readonly fields: {
+ readonly productId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly brand: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ };
+ readonly relations: Record;
+ readonly storage: { readonly collection: 'events' };
+ readonly base: 'Event';
+ };
+ readonly Cart: {
+ readonly fields: {
+ readonly _id: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly userId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly items: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'CartItem' };
+ readonly many: true;
+ };
+ };
+ readonly relations: {
+ readonly user: {
+ readonly to: 'User';
+ readonly cardinality: 'N:1';
+ readonly on: {
+ readonly localFields: readonly ['userId'];
+ readonly targetFields: readonly ['_id'];
+ };
+ };
+ };
+ readonly storage: { readonly collection: 'carts' };
+ };
+ readonly Event: {
+ readonly fields: {
+ readonly _id: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly userId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly sessionId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly type: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly timestamp: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/date@1' };
+ };
+ };
+ readonly relations: Record;
+ readonly storage: { readonly collection: 'events' };
+ readonly discriminator: { readonly field: 'type' };
+ readonly variants: {
+ readonly ViewProductEvent: { readonly value: 'view-product' };
+ readonly SearchEvent: { readonly value: 'search' };
+ readonly AddToCartEvent: { readonly value: 'add-to-cart' };
+ };
+ };
+ readonly Invoice: {
+ readonly fields: {
+ readonly _id: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly orderId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly items: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'InvoiceLineItem' };
+ readonly many: true;
+ };
+ readonly subtotal: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' };
+ };
+ readonly tax: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' };
+ };
+ readonly total: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' };
+ };
+ readonly issuedAt: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/date@1' };
+ };
+ };
+ readonly relations: {
+ readonly order: {
+ readonly to: 'Order';
+ readonly cardinality: 'N:1';
+ readonly on: {
+ readonly localFields: readonly ['orderId'];
+ readonly targetFields: readonly ['_id'];
+ };
+ };
+ };
+ readonly storage: { readonly collection: 'invoices' };
+ };
+ readonly Location: {
+ readonly fields: {
+ readonly _id: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly name: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly streetAndNumber: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly city: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly postalCode: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly country: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ };
+ readonly relations: Record;
+ readonly storage: { readonly collection: 'locations' };
+ };
+ readonly Order: {
+ readonly fields: {
+ readonly _id: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly userId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly items: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'OrderLineItem' };
+ readonly many: true;
+ };
+ readonly shippingAddress: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly type: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly statusHistory: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'StatusEntry' };
+ readonly many: true;
+ };
+ };
+ readonly relations: {
+ readonly user: {
+ readonly to: 'User';
+ readonly cardinality: 'N:1';
+ readonly on: {
+ readonly localFields: readonly ['userId'];
+ readonly targetFields: readonly ['_id'];
+ };
+ };
+ readonly invoices: {
+ readonly to: 'Invoice';
+ readonly cardinality: '1:N';
+ readonly on: {
+ readonly localFields: readonly ['_id'];
+ readonly targetFields: readonly ['orderId'];
+ };
+ };
+ };
+ readonly storage: { readonly collection: 'orders' };
+ };
+ readonly Product: {
+ readonly fields: {
+ readonly _id: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly name: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly brand: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly code: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly description: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly masterCategory: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly subCategory: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly articleType: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly price: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'Price' };
+ };
+ readonly image: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'Image' };
+ };
+ readonly embedding: {
+ readonly nullable: true;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' };
+ readonly many: true;
+ };
+ };
+ readonly relations: Record;
+ readonly storage: { readonly collection: 'products' };
+ };
+ readonly SearchEvent: {
+ readonly fields: {
+ readonly query: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ };
+ readonly relations: Record;
+ readonly storage: { readonly collection: 'events' };
+ readonly base: 'Event';
+ };
+ readonly User: {
+ readonly fields: {
+ readonly _id: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/objectId@1' };
+ };
+ readonly name: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly email: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly address: {
+ readonly nullable: true;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'Address' };
+ };
+ };
+ readonly relations: {
+ readonly carts: {
+ readonly to: 'Cart';
+ readonly cardinality: '1:N';
+ readonly on: {
+ readonly localFields: readonly ['_id'];
+ readonly targetFields: readonly ['userId'];
+ };
+ };
+ readonly orders: {
+ readonly to: 'Order';
+ readonly cardinality: '1:N';
+ readonly on: {
+ readonly localFields: readonly ['_id'];
+ readonly targetFields: readonly ['userId'];
+ };
+ };
+ };
+ readonly storage: { readonly collection: 'users' };
+ };
+ readonly ViewProductEvent: {
+ readonly fields: {
+ readonly productId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly subCategory: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly brand: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly exitMethod: {
+ readonly nullable: true;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ };
+ readonly relations: Record;
+ readonly storage: { readonly collection: 'events' };
+ readonly base: 'Event';
+ };
+ }
+> & {
+ readonly target: 'mongo';
+ readonly targetFamily: 'mongo';
+ readonly roots: {
+ readonly products: 'Product';
+ readonly users: 'User';
+ readonly carts: 'Cart';
+ readonly orders: 'Order';
+ readonly locations: 'Location';
+ readonly invoices: 'Invoice';
+ readonly events: 'Event';
+ };
+ readonly capabilities: {};
+ readonly extensionPacks: {};
+ readonly meta: {};
+ readonly valueObjects: {
+ readonly Price: {
+ readonly fields: {
+ readonly amount: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' };
+ };
+ readonly currency: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ };
+ };
+ readonly Image: {
+ readonly fields: {
+ readonly url: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ };
+ };
+ readonly Address: {
+ readonly fields: {
+ readonly streetAndNumber: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly city: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly postalCode: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly country: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ };
+ };
+ readonly CartItem: {
+ readonly fields: {
+ readonly productId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly name: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly brand: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly amount: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' };
+ };
+ readonly price: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'Price' };
+ };
+ readonly image: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'Image' };
+ };
+ };
+ };
+ readonly OrderLineItem: {
+ readonly fields: {
+ readonly productId: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly name: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly brand: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly amount: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' };
+ };
+ readonly price: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'Price' };
+ };
+ readonly image: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'valueObject'; readonly name: 'Image' };
+ };
+ };
+ };
+ readonly StatusEntry: {
+ readonly fields: {
+ readonly status: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly timestamp: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/date@1' };
+ };
+ };
+ };
+ readonly InvoiceLineItem: {
+ readonly fields: {
+ readonly name: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/string@1' };
+ };
+ readonly amount: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/int32@1' };
+ };
+ readonly unitPrice: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' };
+ };
+ readonly lineTotal: {
+ readonly nullable: false;
+ readonly type: { readonly kind: 'scalar'; readonly codecId: 'mongo/double@1' };
+ };
+ };
+ };
+ };
+ readonly profileHash: ProfileHash;
+};
+
+export type Contract = MongoContractWithTypeMaps;
diff --git a/examples/retail-store/src/contract.json b/examples/retail-store/src/contract.json
new file mode 100644
index 0000000000..e45d72d787
--- /dev/null
+++ b/examples/retail-store/src/contract.json
@@ -0,0 +1,1364 @@
+{
+ "schemaVersion": "1",
+ "targetFamily": "mongo",
+ "target": "mongo",
+ "profileHash": "sha256:840de65fba7eb950a31487f74ee420b9c21205f38bce58579026747e0264e840",
+ "roots": {
+ "carts": "Cart",
+ "events": "Event",
+ "invoices": "Invoice",
+ "locations": "Location",
+ "orders": "Order",
+ "products": "Product",
+ "users": "User"
+ },
+ "models": {
+ "AddToCartEvent": {
+ "base": "Event",
+ "fields": {
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "productId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "events"
+ }
+ },
+ "Cart": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "items": {
+ "many": true,
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "CartItem"
+ }
+ },
+ "userId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {
+ "user": {
+ "cardinality": "N:1",
+ "on": {
+ "localFields": [
+ "userId"
+ ],
+ "targetFields": [
+ "_id"
+ ]
+ },
+ "to": "User"
+ }
+ },
+ "storage": {
+ "collection": "carts"
+ }
+ },
+ "Event": {
+ "discriminator": {
+ "field": "type"
+ },
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "sessionId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "timestamp": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/date@1",
+ "kind": "scalar"
+ }
+ },
+ "type": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "userId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "events"
+ },
+ "variants": {
+ "AddToCartEvent": {
+ "value": "add-to-cart"
+ },
+ "SearchEvent": {
+ "value": "search"
+ },
+ "ViewProductEvent": {
+ "value": "view-product"
+ }
+ }
+ },
+ "Invoice": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "issuedAt": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/date@1",
+ "kind": "scalar"
+ }
+ },
+ "items": {
+ "many": true,
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "InvoiceLineItem"
+ }
+ },
+ "orderId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "subtotal": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "tax": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "total": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {
+ "order": {
+ "cardinality": "N:1",
+ "on": {
+ "localFields": [
+ "orderId"
+ ],
+ "targetFields": [
+ "_id"
+ ]
+ },
+ "to": "Order"
+ }
+ },
+ "storage": {
+ "collection": "invoices"
+ }
+ },
+ "Location": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "city": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "country": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "postalCode": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "streetAndNumber": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "locations"
+ }
+ },
+ "Order": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "items": {
+ "many": true,
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "OrderLineItem"
+ }
+ },
+ "shippingAddress": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "statusHistory": {
+ "many": true,
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "StatusEntry"
+ }
+ },
+ "type": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "userId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {
+ "invoices": {
+ "cardinality": "1:N",
+ "on": {
+ "localFields": [
+ "_id"
+ ],
+ "targetFields": [
+ "orderId"
+ ]
+ },
+ "to": "Invoice"
+ },
+ "user": {
+ "cardinality": "N:1",
+ "on": {
+ "localFields": [
+ "userId"
+ ],
+ "targetFields": [
+ "_id"
+ ]
+ },
+ "to": "User"
+ }
+ },
+ "storage": {
+ "collection": "orders"
+ }
+ },
+ "Product": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "articleType": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "code": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "description": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "embedding": {
+ "many": true,
+ "nullable": true,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "image": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Image"
+ }
+ },
+ "masterCategory": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "price": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Price"
+ }
+ },
+ "subCategory": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "products"
+ }
+ },
+ "SearchEvent": {
+ "base": "Event",
+ "fields": {
+ "query": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "events"
+ }
+ },
+ "User": {
+ "fields": {
+ "_id": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/objectId@1",
+ "kind": "scalar"
+ }
+ },
+ "address": {
+ "nullable": true,
+ "type": {
+ "kind": "valueObject",
+ "name": "Address"
+ }
+ },
+ "email": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {
+ "carts": {
+ "cardinality": "1:N",
+ "on": {
+ "localFields": [
+ "_id"
+ ],
+ "targetFields": [
+ "userId"
+ ]
+ },
+ "to": "Cart"
+ },
+ "orders": {
+ "cardinality": "1:N",
+ "on": {
+ "localFields": [
+ "_id"
+ ],
+ "targetFields": [
+ "userId"
+ ]
+ },
+ "to": "Order"
+ }
+ },
+ "storage": {
+ "collection": "users"
+ }
+ },
+ "ViewProductEvent": {
+ "base": "Event",
+ "fields": {
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "exitMethod": {
+ "nullable": true,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "productId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "subCategory": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ },
+ "relations": {},
+ "storage": {
+ "collection": "events"
+ }
+ }
+ },
+ "valueObjects": {
+ "Address": {
+ "fields": {
+ "city": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "country": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "postalCode": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "streetAndNumber": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "CartItem": {
+ "fields": {
+ "amount": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/int32@1",
+ "kind": "scalar"
+ }
+ },
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "image": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Image"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "price": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Price"
+ }
+ },
+ "productId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "Image": {
+ "fields": {
+ "url": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "InvoiceLineItem": {
+ "fields": {
+ "amount": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/int32@1",
+ "kind": "scalar"
+ }
+ },
+ "lineTotal": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "unitPrice": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "OrderLineItem": {
+ "fields": {
+ "amount": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/int32@1",
+ "kind": "scalar"
+ }
+ },
+ "brand": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "image": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Image"
+ }
+ },
+ "name": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "price": {
+ "nullable": false,
+ "type": {
+ "kind": "valueObject",
+ "name": "Price"
+ }
+ },
+ "productId": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "Price": {
+ "fields": {
+ "amount": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/double@1",
+ "kind": "scalar"
+ }
+ },
+ "currency": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ },
+ "StatusEntry": {
+ "fields": {
+ "status": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/string@1",
+ "kind": "scalar"
+ }
+ },
+ "timestamp": {
+ "nullable": false,
+ "type": {
+ "codecId": "mongo/date@1",
+ "kind": "scalar"
+ }
+ }
+ }
+ }
+ },
+ "storage": {
+ "collections": {
+ "addToCartEvent": {
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "brand": {
+ "bsonType": "string"
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "brand",
+ "productId"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "carts": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ }
+ ],
+ "unique": true
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "url"
+ ]
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "currency"
+ ]
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "amount",
+ "brand",
+ "image",
+ "name",
+ "price",
+ "productId"
+ ]
+ }
+ },
+ "userId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": [
+ "_id",
+ "items",
+ "userId"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "events": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ },
+ {
+ "direction": -1,
+ "field": "timestamp"
+ }
+ ]
+ },
+ {
+ "expireAfterSeconds": 7776000,
+ "keys": [
+ {
+ "direction": 1,
+ "field": "timestamp"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "sessionId": {
+ "bsonType": "string"
+ },
+ "timestamp": {
+ "bsonType": "date"
+ },
+ "type": {
+ "bsonType": "string"
+ },
+ "userId": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "_id",
+ "sessionId",
+ "timestamp",
+ "type",
+ "userId"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "invoices": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "orderId"
+ }
+ ]
+ },
+ {
+ "keys": [
+ {
+ "direction": -1,
+ "field": "issuedAt"
+ }
+ ],
+ "sparse": true
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "issuedAt": {
+ "bsonType": "date"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "name": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "amount",
+ "name"
+ ]
+ }
+ },
+ "orderId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": [
+ "_id",
+ "issuedAt",
+ "items",
+ "orderId"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "locations": {
+ "indexes": [
+ {
+ "collation": {
+ "locale": "en",
+ "strength": 2
+ },
+ "keys": [
+ {
+ "direction": 1,
+ "field": "city"
+ },
+ {
+ "direction": 1,
+ "field": "country"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "city": {
+ "bsonType": "string"
+ },
+ "country": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "postalCode": {
+ "bsonType": "string"
+ },
+ "streetAndNumber": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "_id",
+ "city",
+ "country",
+ "name",
+ "postalCode",
+ "streetAndNumber"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "orders": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "userId"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "items": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "amount": {
+ "bsonType": "int"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "url"
+ ]
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "currency"
+ ]
+ },
+ "productId": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "amount",
+ "brand",
+ "image",
+ "name",
+ "price",
+ "productId"
+ ]
+ }
+ },
+ "shippingAddress": {
+ "bsonType": "string"
+ },
+ "statusHistory": {
+ "bsonType": "array",
+ "items": {
+ "bsonType": "object",
+ "properties": {
+ "status": {
+ "bsonType": "string"
+ },
+ "timestamp": {
+ "bsonType": "date"
+ }
+ },
+ "required": [
+ "status",
+ "timestamp"
+ ]
+ }
+ },
+ "type": {
+ "bsonType": "string"
+ },
+ "userId": {
+ "bsonType": "objectId"
+ }
+ },
+ "required": [
+ "_id",
+ "items",
+ "shippingAddress",
+ "statusHistory",
+ "type",
+ "userId"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "products": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": "text",
+ "field": "name"
+ },
+ {
+ "direction": "text",
+ "field": "description"
+ }
+ ],
+ "weights": {
+ "description": 1,
+ "name": 10
+ }
+ },
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "brand"
+ },
+ {
+ "direction": 1,
+ "field": "subCategory"
+ }
+ ]
+ },
+ {
+ "keys": [
+ {
+ "direction": "hashed",
+ "field": "code"
+ }
+ ]
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "articleType": {
+ "bsonType": "string"
+ },
+ "brand": {
+ "bsonType": "string"
+ },
+ "code": {
+ "bsonType": "string"
+ },
+ "description": {
+ "bsonType": "string"
+ },
+ "image": {
+ "bsonType": "object",
+ "properties": {
+ "url": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "url"
+ ]
+ },
+ "masterCategory": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ },
+ "price": {
+ "bsonType": "object",
+ "properties": {
+ "currency": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "currency"
+ ]
+ },
+ "subCategory": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "_id",
+ "articleType",
+ "brand",
+ "code",
+ "description",
+ "image",
+ "masterCategory",
+ "name",
+ "price",
+ "subCategory"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "searchEvent": {
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "query": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "query"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "users": {
+ "indexes": [
+ {
+ "keys": [
+ {
+ "direction": 1,
+ "field": "email"
+ }
+ ],
+ "unique": true
+ }
+ ],
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "address": {
+ "oneOf": [
+ {
+ "bsonType": "null"
+ },
+ {
+ "bsonType": "object",
+ "properties": {
+ "city": {
+ "bsonType": "string"
+ },
+ "country": {
+ "bsonType": "string"
+ },
+ "postalCode": {
+ "bsonType": "string"
+ },
+ "streetAndNumber": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "city",
+ "country",
+ "postalCode",
+ "streetAndNumber"
+ ]
+ }
+ ]
+ },
+ "email": {
+ "bsonType": "string"
+ },
+ "name": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "_id",
+ "email",
+ "name"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ },
+ "viewProductEvent": {
+ "validator": {
+ "jsonSchema": {
+ "bsonType": "object",
+ "properties": {
+ "brand": {
+ "bsonType": "string"
+ },
+ "exitMethod": {
+ "bsonType": [
+ "null",
+ "string"
+ ]
+ },
+ "productId": {
+ "bsonType": "string"
+ },
+ "subCategory": {
+ "bsonType": "string"
+ }
+ },
+ "required": [
+ "brand",
+ "productId",
+ "subCategory"
+ ]
+ },
+ "validationAction": "error",
+ "validationLevel": "strict"
+ }
+ }
+ },
+ "storageHash": "sha256:5f5aee268e41d345787b7c5d396778443a977b10dc2ec9f14c913171fd6d1411"
+ },
+ "capabilities": {},
+ "extensionPacks": {},
+ "meta": {},
+ "_generated": {
+ "warning": "⚠️ GENERATED FILE - DO NOT EDIT",
+ "message": "This file is automatically generated by \"prisma-next contract emit\".",
+ "regenerate": "To regenerate, run: prisma-next contract emit"
+ }
+}
\ No newline at end of file
diff --git a/examples/retail-store/src/data/carts.ts b/examples/retail-store/src/data/carts.ts
new file mode 100644
index 0000000000..120bac1cf3
--- /dev/null
+++ b/examples/retail-store/src/data/carts.ts
@@ -0,0 +1,64 @@
+import type { Db } from '../db';
+import { executeRaw } from './execute-raw';
+import { objectIdEq, rawObjectIdFilter } from './object-id-filter';
+
+export function getCartByUserId(db: Db, userId: string) {
+ return db.orm.carts.where(objectIdEq('userId', userId)).first();
+}
+
+export function getCartWithUser(db: Db, userId: string) {
+ return db.orm.carts.include('user').where(objectIdEq('userId', userId)).first();
+}
+
+export function upsertCart(
+ db: Db,
+ userId: string,
+ items: ReadonlyArray<{
+ productId: string;
+ name: string;
+ brand: string;
+ amount: number;
+ price: { amount: number; currency: string };
+ image: { url: string };
+ }>,
+) {
+ return db.orm.carts.where(objectIdEq('userId', userId)).upsert({
+ create: { userId, items: [...items] },
+ update: { items: [...items] },
+ });
+}
+
+export function clearCart(db: Db, userId: string) {
+ return db.orm.carts.where(objectIdEq('userId', userId)).update({ items: [] });
+}
+
+export async function addToCart(
+ db: Db,
+ userId: string,
+ item: {
+ productId: string;
+ name: string;
+ brand: string;
+ amount: number;
+ price: { amount: number; currency: string };
+ image: { url: string };
+ },
+) {
+ const plan = db.raw
+ .collection('carts')
+ .findOneAndUpdate(
+ rawObjectIdFilter('userId', userId),
+ { $push: { items: item }, $setOnInsert: rawObjectIdFilter('userId', userId) },
+ { upsert: true },
+ )
+ .build();
+ await executeRaw(db, plan);
+}
+
+export async function removeFromCart(db: Db, userId: string, productId: string) {
+ const plan = db.raw
+ .collection('carts')
+ .updateOne(rawObjectIdFilter('userId', userId), { $pull: { items: { productId } } })
+ .build();
+ await executeRaw(db, plan);
+}
diff --git a/examples/retail-store/src/data/events.ts b/examples/retail-store/src/data/events.ts
new file mode 100644
index 0000000000..ea1d81908e
--- /dev/null
+++ b/examples/retail-store/src/data/events.ts
@@ -0,0 +1,90 @@
+import { acc } from '@prisma-next/mongo-pipeline-builder';
+import { MongoFieldFilter } from '@prisma-next/mongo-query-ast/execution';
+import type { Db } from '../db';
+import { collectResults } from './execute-raw';
+
+export function createViewProductEvent(
+ db: Db,
+ event: {
+ userId: string;
+ sessionId: string;
+ timestamp: Date;
+ productId: string;
+ subCategory: string;
+ brand: string;
+ exitMethod?: string | null;
+ },
+) {
+ return db.orm.events.variant('ViewProductEvent').create({
+ userId: event.userId,
+ sessionId: event.sessionId,
+ timestamp: event.timestamp,
+ productId: event.productId,
+ subCategory: event.subCategory,
+ brand: event.brand,
+ exitMethod: event.exitMethod ?? null,
+ });
+}
+
+export function createSearchEvent(
+ db: Db,
+ event: {
+ userId: string;
+ sessionId: string;
+ timestamp: Date;
+ query: string;
+ },
+) {
+ return db.orm.events.variant('SearchEvent').create({
+ userId: event.userId,
+ sessionId: event.sessionId,
+ timestamp: event.timestamp,
+ query: event.query,
+ });
+}
+
+export function createAddToCartEvent(
+ db: Db,
+ event: {
+ userId: string;
+ sessionId: string;
+ timestamp: Date;
+ productId: string;
+ brand: string;
+ },
+) {
+ return db.orm.events.variant('AddToCartEvent').create({
+ userId: event.userId,
+ sessionId: event.sessionId,
+ timestamp: event.timestamp,
+ productId: event.productId,
+ brand: event.brand,
+ });
+}
+
+export function findEventsByUser(db: Db, userId: string) {
+ return db.orm.events.where(MongoFieldFilter.eq('userId', userId)).all();
+}
+
+export function findSearchEventsByUser(db: Db, userId: string) {
+ return db.orm.events.variant('SearchEvent').where(MongoFieldFilter.eq('userId', userId)).all();
+}
+
+interface EventTypeCount {
+ _id: string;
+ count: number;
+}
+
+export async function aggregateEventsByType(db: Db, userId: string): Promise {
+ const plan = db.pipeline
+ .from('events')
+ .match(MongoFieldFilter.eq('userId', userId))
+ .group((f) => ({
+ _id: f.type,
+ count: acc.count(),
+ }))
+ .sort({ count: -1 })
+ .build();
+
+ return collectResults(db, plan);
+}
diff --git a/examples/retail-store/src/data/execute-raw.ts b/examples/retail-store/src/data/execute-raw.ts
new file mode 100644
index 0000000000..f042a916fd
--- /dev/null
+++ b/examples/retail-store/src/data/execute-raw.ts
@@ -0,0 +1,23 @@
+import type { MongoQueryPlan } from '@prisma-next/mongo-query-ast/execution';
+import type { Db } from '../db';
+
+export async function executeRaw(db: Db, plan: MongoQueryPlan) {
+ for await (const _ of db.runtime.execute(plan)) {
+ /* drain the iterator to trigger execution */
+ }
+}
+
+export async function collectResults(db: Db, plan: MongoQueryPlan): Promise {
+ const results: T[] = [];
+ for await (const row of db.runtime.execute(plan)) {
+ results.push(row as T);
+ }
+ return results;
+}
+
+export async function collectFirstResult(db: Db, plan: MongoQueryPlan): Promise {
+ for await (const row of db.runtime.execute(plan)) {
+ return row as T;
+ }
+ return null;
+}
diff --git a/examples/retail-store/src/data/invoices.ts b/examples/retail-store/src/data/invoices.ts
new file mode 100644
index 0000000000..c6350a93a6
--- /dev/null
+++ b/examples/retail-store/src/data/invoices.ts
@@ -0,0 +1,36 @@
+import type { Db } from '../db';
+import { objectIdEq } from './object-id-filter';
+
+export function findInvoiceById(db: Db, id: string) {
+ return db.orm.invoices.where(objectIdEq('_id', id)).first();
+}
+
+export function findInvoiceWithOrder(db: Db, id: string) {
+ return db.orm.invoices.include('order').where(objectIdEq('_id', id)).first();
+}
+
+export function createInvoice(
+ db: Db,
+ invoice: {
+ orderId: string;
+ items: ReadonlyArray<{
+ name: string;
+ amount: number;
+ unitPrice: number;
+ lineTotal: number;
+ }>;
+ subtotal: number;
+ tax: number;
+ total: number;
+ issuedAt: Date;
+ },
+) {
+ return db.orm.invoices.create({
+ orderId: invoice.orderId,
+ items: [...invoice.items],
+ subtotal: invoice.subtotal,
+ tax: invoice.tax,
+ total: invoice.total,
+ issuedAt: invoice.issuedAt,
+ });
+}
diff --git a/examples/retail-store/src/data/locations.ts b/examples/retail-store/src/data/locations.ts
new file mode 100644
index 0000000000..8842d99dca
--- /dev/null
+++ b/examples/retail-store/src/data/locations.ts
@@ -0,0 +1,5 @@
+import type { Db } from '../db';
+
+export function findLocations(db: Db) {
+ return db.orm.locations.all();
+}
diff --git a/examples/retail-store/src/data/object-id-filter.ts b/examples/retail-store/src/data/object-id-filter.ts
new file mode 100644
index 0000000000..887bc84693
--- /dev/null
+++ b/examples/retail-store/src/data/object-id-filter.ts
@@ -0,0 +1,17 @@
+import { MongoFieldFilter } from '@prisma-next/mongo-query-ast/execution';
+import { MongoParamRef } from '@prisma-next/mongo-value';
+import { ObjectId } from 'mongodb';
+
+function toObjectId(id: string | ObjectId): ObjectId {
+ if (id instanceof ObjectId) return id;
+ if (!ObjectId.isValid(id)) throw new Error(`Invalid ObjectId: ${id}`);
+ return new ObjectId(id);
+}
+
+export function objectIdEq(field: string, id: string | ObjectId): MongoFieldFilter {
+ return MongoFieldFilter.eq(field, new MongoParamRef(toObjectId(id)));
+}
+
+export function rawObjectIdFilter(field: string, id: string | ObjectId): Record {
+ return { [field]: toObjectId(id) };
+}
diff --git a/examples/retail-store/src/data/orders.ts b/examples/retail-store/src/data/orders.ts
new file mode 100644
index 0000000000..a05d93d535
--- /dev/null
+++ b/examples/retail-store/src/data/orders.ts
@@ -0,0 +1,57 @@
+import type { Db } from '../db';
+import { collectFirstResult } from './execute-raw';
+import { objectIdEq, rawObjectIdFilter } from './object-id-filter';
+
+export function getUserOrders(db: Db, userId: string) {
+ return db.orm.orders.where(objectIdEq('userId', userId)).all();
+}
+
+export function getOrderById(db: Db, id: string) {
+ return db.orm.orders.where(objectIdEq('_id', id)).first();
+}
+
+export function getOrderWithUser(db: Db, id: string) {
+ return db.orm.orders.include('user').where(objectIdEq('_id', id)).first();
+}
+
+export function createOrder(
+ db: Db,
+ order: {
+ userId: string;
+ items: ReadonlyArray<{
+ productId: string;
+ name: string;
+ brand: string;
+ amount: number;
+ price: { amount: number; currency: string };
+ image: { url: string };
+ }>;
+ shippingAddress: string;
+ type: string;
+ statusHistory: ReadonlyArray<{ status: string; timestamp: Date }>;
+ },
+) {
+ return db.orm.orders.create({
+ userId: order.userId,
+ items: [...order.items],
+ shippingAddress: order.shippingAddress,
+ type: order.type,
+ statusHistory: [...order.statusHistory],
+ });
+}
+
+export function deleteOrder(db: Db, id: string) {
+ return db.orm.orders.where(objectIdEq('_id', id)).delete();
+}
+
+export async function updateOrderStatus(
+ db: Db,
+ orderId: string,
+ entry: { status: string; timestamp: Date },
+) {
+ const plan = db.raw
+ .collection('orders')
+ .findOneAndUpdate(rawObjectIdFilter('_id', orderId), { $push: { statusHistory: entry } })
+ .build();
+ return collectFirstResult>(db, plan);
+}
diff --git a/examples/retail-store/src/data/products.ts b/examples/retail-store/src/data/products.ts
new file mode 100644
index 0000000000..7105cc02d2
--- /dev/null
+++ b/examples/retail-store/src/data/products.ts
@@ -0,0 +1,75 @@
+import { MongoFieldFilter, MongoOrExpr } from '@prisma-next/mongo-query-ast/execution';
+import { MongoParamRef } from '@prisma-next/mongo-value';
+import type { FieldOutputTypes } from '../contract';
+import type { Db } from '../db';
+import { collectResults } from './execute-raw';
+import { objectIdEq } from './object-id-filter';
+
+type Product = FieldOutputTypes['Product'];
+
+export function findProducts(db: Db) {
+ return db.orm.products.all();
+}
+
+export async function findProductsPaginated(
+ db: Db,
+ skip: number,
+ take: number,
+): Promise {
+ const plan = db.pipeline.from('products').sort({ _id: 1 }).skip(skip).limit(take).build();
+ return collectResults(db, plan);
+}
+
+export function findProductById(db: Db, id: string) {
+ return db.orm.products.where(objectIdEq('_id', id)).first();
+}
+
+function escapeRegex(str: string) {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+export async function searchProducts(db: Db, query: string): Promise {
+ const regex = new MongoParamRef(new RegExp(escapeRegex(query), 'i'));
+ const filter = MongoOrExpr.of([
+ MongoFieldFilter.of('name', '$regex', regex),
+ MongoFieldFilter.of('brand', '$regex', regex),
+ MongoFieldFilter.of('articleType', '$regex', regex),
+ ]);
+ const plan = db.pipeline.from('products').match(filter).build();
+ return collectResults(db, plan);
+}
+
+export async function getRandomProducts(db: Db, count: number): Promise {
+ const plan = db.pipeline.from('products').sample(count).build();
+ return collectResults(db, plan);
+}
+
+/**
+ * Vector similarity search via $vectorSearch aggregation stage.
+ * Requires an Atlas cluster with a vector search index on the
+ * `products.embedding` field. Not available with mongodb-memory-server.
+ *
+ * Uses raw aggregate instead of the pipeline builder because $vectorSearch
+ * is an Atlas-specific stage not yet supported by the pipeline builder API.
+ */
+export async function findSimilarProducts(
+ db: Db,
+ embedding: number[],
+ limit: number,
+): Promise {
+ const plan = db.raw
+ .collection('products')
+ .aggregate([
+ {
+ $vectorSearch: {
+ index: 'product_embedding_index',
+ path: 'embedding',
+ queryVector: embedding,
+ numCandidates: limit * 10,
+ limit,
+ },
+ },
+ ])
+ .build();
+ return collectResults(db, plan);
+}
diff --git a/examples/retail-store/src/data/users.ts b/examples/retail-store/src/data/users.ts
new file mode 100644
index 0000000000..4e5ee44ae8
--- /dev/null
+++ b/examples/retail-store/src/data/users.ts
@@ -0,0 +1,14 @@
+import type { Db } from '../db';
+import { objectIdEq } from './object-id-filter';
+
+export function findUsers(db: Db) {
+ return db.orm.users.all();
+}
+
+export function findUserById(db: Db, id: string) {
+ return db.orm.users.where(objectIdEq('_id', id)).first();
+}
+
+export function createUser(db: Db, data: { name: string; email: string; address: null }) {
+ return db.orm.users.create(data);
+}
diff --git a/examples/retail-store/src/db-singleton.ts b/examples/retail-store/src/db-singleton.ts
new file mode 100644
index 0000000000..8d918bb31c
--- /dev/null
+++ b/examples/retail-store/src/db-singleton.ts
@@ -0,0 +1,16 @@
+import type { Db } from './db';
+import { createClient } from './db';
+
+let dbPromise: Promise | undefined;
+
+export function getDb(): Promise {
+ if (!dbPromise) {
+ const url = process.env['DB_URL'] ?? 'mongodb://localhost:27017';
+ const dbName = process.env['MONGODB_DB'] ?? 'retail-store';
+ dbPromise = createClient(url, dbName).catch((error) => {
+ dbPromise = undefined;
+ throw error;
+ });
+ }
+ return dbPromise;
+}
diff --git a/examples/retail-store/src/db.ts b/examples/retail-store/src/db.ts
new file mode 100644
index 0000000000..b91e642b58
--- /dev/null
+++ b/examples/retail-store/src/db.ts
@@ -0,0 +1,25 @@
+import { createMongoAdapter } from '@prisma-next/adapter-mongo';
+import { createMongoDriver } from '@prisma-next/driver-mongo';
+import { validateMongoContract } from '@prisma-next/mongo-contract';
+import { mongoOrm, mongoRaw } from '@prisma-next/mongo-orm';
+import { mongoPipeline } from '@prisma-next/mongo-pipeline-builder';
+import { createMongoRuntime, type MongoRuntime } from '@prisma-next/mongo-runtime';
+import type { Contract } from './contract';
+import contractJson from './contract.json' with { type: 'json' };
+
+const { contract } = validateMongoContract(contractJson);
+
+const pipeline = mongoPipeline({ contractJson });
+const raw = mongoRaw({ contract });
+
+export async function createClient(connectionUri: string, dbName: string) {
+ const adapter = createMongoAdapter();
+ const driver = await createMongoDriver(connectionUri, dbName);
+ const runtime = createMongoRuntime({ adapter, driver });
+ const orm = mongoOrm({ contract, executor: runtime });
+
+ return { orm, runtime, pipeline, raw, contract };
+}
+
+export type Db = Awaited>;
+export type { MongoRuntime };
diff --git a/examples/retail-store/src/lib/auth.ts b/examples/retail-store/src/lib/auth.ts
new file mode 100644
index 0000000000..28031feaa0
--- /dev/null
+++ b/examples/retail-store/src/lib/auth.ts
@@ -0,0 +1,15 @@
+import { cookies } from 'next/headers';
+import { findUserById } from '../data/users';
+import { getDb } from '../db-singleton';
+
+export async function getAuthUserId(): Promise {
+ const cookieStore = await cookies();
+ return cookieStore.get('userId')?.value ?? null;
+}
+
+export async function getAuthUser() {
+ const userId = await getAuthUserId();
+ if (!userId) return null;
+ const db = await getDb();
+ return findUserById(db, userId);
+}
diff --git a/examples/retail-store/src/lib/utils.ts b/examples/retail-store/src/lib/utils.ts
new file mode 100644
index 0000000000..9ad0df4269
--- /dev/null
+++ b/examples/retail-store/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/examples/retail-store/src/seed.ts b/examples/retail-store/src/seed.ts
new file mode 100644
index 0000000000..b87c4e9eda
--- /dev/null
+++ b/examples/retail-store/src/seed.ts
@@ -0,0 +1,385 @@
+import { createAddToCartEvent, createSearchEvent, createViewProductEvent } from './data/events';
+import type { Db } from './db';
+
+const productData = [
+ {
+ name: 'Classic Oxford Shirt',
+ brand: 'Heritage',
+ code: 'HER-OXF-001',
+ description: 'Timeless button-down oxford shirt in crisp white cotton',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Shirts',
+ price: { amount: 79.99, currency: 'USD' },
+ },
+ {
+ name: 'Linen Camp Collar Shirt',
+ brand: 'Heritage',
+ code: 'HER-LIN-002',
+ description: 'Relaxed linen camp collar shirt for warm weather',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Shirts',
+ price: { amount: 89.99, currency: 'USD' },
+ },
+ {
+ name: 'Merino Crew Sweater',
+ brand: 'Heritage',
+ code: 'HER-MER-003',
+ description: 'Lightweight merino wool crew neck sweater',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Sweaters',
+ price: { amount: 119.99, currency: 'USD' },
+ },
+ {
+ name: 'Graphic Tee - Mountain',
+ brand: 'UrbanEdge',
+ code: 'UE-TEE-010',
+ description: 'Soft cotton graphic tee with mountain print',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'T-Shirts',
+ price: { amount: 34.99, currency: 'USD' },
+ },
+ {
+ name: 'Performance Polo',
+ brand: 'UrbanEdge',
+ code: 'UE-POL-011',
+ description: 'Moisture-wicking performance polo for active days',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Shirts',
+ price: { amount: 54.99, currency: 'USD' },
+ },
+ {
+ name: 'Denim Jacket',
+ brand: 'UrbanEdge',
+ code: 'UE-DEN-012',
+ description: 'Classic medium-wash denim jacket',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Jackets',
+ price: { amount: 99.99, currency: 'USD' },
+ },
+ {
+ name: 'Slim Fit Chinos',
+ brand: 'UrbanEdge',
+ code: 'UE-CHI-042',
+ description: 'Modern slim-fit chinos in navy with stretch comfort',
+ masterCategory: 'Apparel',
+ subCategory: 'Bottomwear',
+ articleType: 'Trousers',
+ price: { amount: 59.99, currency: 'USD' },
+ },
+ {
+ name: 'Relaxed Fit Jeans',
+ brand: 'Heritage',
+ code: 'HER-JEA-020',
+ description: 'Relaxed fit jeans in dark indigo wash',
+ masterCategory: 'Apparel',
+ subCategory: 'Bottomwear',
+ articleType: 'Jeans',
+ price: { amount: 89.99, currency: 'USD' },
+ },
+ {
+ name: 'Cargo Shorts',
+ brand: 'TrailMark',
+ code: 'TM-SHO-030',
+ description: 'Durable cargo shorts with multiple pockets',
+ masterCategory: 'Apparel',
+ subCategory: 'Bottomwear',
+ articleType: 'Shorts',
+ price: { amount: 44.99, currency: 'USD' },
+ },
+ {
+ name: 'Jogger Pants',
+ brand: 'UrbanEdge',
+ code: 'UE-JOG-013',
+ description: 'Tapered jogger pants with elastic cuff',
+ masterCategory: 'Apparel',
+ subCategory: 'Bottomwear',
+ articleType: 'Trousers',
+ price: { amount: 49.99, currency: 'USD' },
+ },
+ {
+ name: 'Wool Dress Trousers',
+ brand: 'Heritage',
+ code: 'HER-DRS-021',
+ description: 'Tailored wool dress trousers with flat front',
+ masterCategory: 'Apparel',
+ subCategory: 'Bottomwear',
+ articleType: 'Trousers',
+ price: { amount: 129.99, currency: 'USD' },
+ },
+ {
+ name: 'Leather Crossbody Bag',
+ brand: 'Craftsman',
+ code: 'CRA-BAG-017',
+ description: 'Hand-stitched leather crossbody bag with adjustable strap',
+ masterCategory: 'Accessories',
+ subCategory: 'Bags',
+ articleType: 'Handbags',
+ price: { amount: 149.99, currency: 'USD' },
+ },
+ {
+ name: 'Canvas Tote Bag',
+ brand: 'Craftsman',
+ code: 'CRA-TOT-018',
+ description: 'Waxed canvas tote with leather handles',
+ masterCategory: 'Accessories',
+ subCategory: 'Bags',
+ articleType: 'Handbags',
+ price: { amount: 69.99, currency: 'USD' },
+ },
+ {
+ name: 'Weekender Duffle',
+ brand: 'TrailMark',
+ code: 'TM-DUF-031',
+ description: 'Water-resistant weekender duffle bag',
+ masterCategory: 'Accessories',
+ subCategory: 'Bags',
+ articleType: 'Handbags',
+ price: { amount: 119.99, currency: 'USD' },
+ },
+ {
+ name: 'Leather Belt',
+ brand: 'Craftsman',
+ code: 'CRA-BLT-019',
+ description: 'Full-grain leather belt with brass buckle',
+ masterCategory: 'Accessories',
+ subCategory: 'Belts',
+ articleType: 'Belts',
+ price: { amount: 59.99, currency: 'USD' },
+ },
+ {
+ name: 'Aviator Sunglasses',
+ brand: 'UrbanEdge',
+ code: 'UE-SUN-014',
+ description: 'Classic aviator sunglasses with polarized lenses',
+ masterCategory: 'Accessories',
+ subCategory: 'Eyewear',
+ articleType: 'Sunglasses',
+ price: { amount: 79.99, currency: 'USD' },
+ },
+ {
+ name: 'Wool Beanie',
+ brand: 'TrailMark',
+ code: 'TM-BEA-032',
+ description: 'Ribbed merino wool beanie',
+ masterCategory: 'Accessories',
+ subCategory: 'Headwear',
+ articleType: 'Caps',
+ price: { amount: 29.99, currency: 'USD' },
+ },
+ {
+ name: 'Leather Sneakers',
+ brand: 'UrbanEdge',
+ code: 'UE-SNK-015',
+ description: 'Minimalist white leather sneakers',
+ masterCategory: 'Footwear',
+ subCategory: 'Shoes',
+ articleType: 'Casual Shoes',
+ price: { amount: 129.99, currency: 'USD' },
+ },
+ {
+ name: 'Suede Chelsea Boots',
+ brand: 'Heritage',
+ code: 'HER-CHE-022',
+ description: 'Suede Chelsea boots with elastic side panel',
+ masterCategory: 'Footwear',
+ subCategory: 'Shoes',
+ articleType: 'Casual Shoes',
+ price: { amount: 189.99, currency: 'USD' },
+ },
+ {
+ name: 'Trail Running Shoes',
+ brand: 'TrailMark',
+ code: 'TM-TRL-033',
+ description: 'Lightweight trail running shoes with grip sole',
+ masterCategory: 'Footwear',
+ subCategory: 'Shoes',
+ articleType: 'Sports Shoes',
+ price: { amount: 109.99, currency: 'USD' },
+ },
+ {
+ name: 'Canvas Slip-Ons',
+ brand: 'UrbanEdge',
+ code: 'UE-SLP-016',
+ description: 'Casual canvas slip-on shoes',
+ masterCategory: 'Footwear',
+ subCategory: 'Shoes',
+ articleType: 'Casual Shoes',
+ price: { amount: 44.99, currency: 'USD' },
+ },
+ {
+ name: 'Hiking Boots',
+ brand: 'TrailMark',
+ code: 'TM-HIK-034',
+ description: 'Waterproof hiking boots with ankle support',
+ masterCategory: 'Footwear',
+ subCategory: 'Shoes',
+ articleType: 'Sports Shoes',
+ price: { amount: 159.99, currency: 'USD' },
+ },
+ {
+ name: 'Rain Jacket',
+ brand: 'TrailMark',
+ code: 'TM-RAI-035',
+ description: 'Packable waterproof rain jacket',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Jackets',
+ price: { amount: 89.99, currency: 'USD' },
+ },
+ {
+ name: 'Silk Pocket Square',
+ brand: 'Heritage',
+ code: 'HER-PSQ-023',
+ description: 'Hand-rolled silk pocket square',
+ masterCategory: 'Accessories',
+ subCategory: 'Scarves',
+ articleType: 'Scarves',
+ price: { amount: 39.99, currency: 'USD' },
+ },
+] as const;
+
+export async function seed(db: Db) {
+ const products = await db.orm.products.createAll(
+ productData.map((p) => ({
+ ...p,
+ image: { url: `/images/products/${p.code.toLowerCase()}.jpg` },
+ embedding: null,
+ })),
+ );
+
+ const p0 = products[0];
+ const p1 = products[1];
+ const p2 = products[11];
+ if (!p0 || !p1 || !p2) throw new Error('Failed to seed products');
+
+ function lineItemFrom(product: (typeof products)[number]) {
+ return {
+ productId: String(product._id),
+ name: String(product.name),
+ brand: String(product.brand),
+ image: { url: `/images/products/${String(product.code).toLowerCase()}.jpg` },
+ };
+ }
+
+ const users = await db.orm.users.createAll([
+ {
+ name: 'Alice Chen',
+ email: 'alice@example.com',
+ address: {
+ streetAndNumber: '123 Main St',
+ city: 'San Francisco',
+ postalCode: '94102',
+ country: 'US',
+ },
+ },
+ {
+ name: 'Bob Kumar',
+ email: 'bob@example.com',
+ address: null,
+ },
+ ]);
+
+ const alice = users[0];
+ const bob = users[1];
+ if (!alice || !bob) throw new Error('Failed to seed users');
+
+ await db.orm.carts.create({
+ userId: String(alice._id),
+ items: [
+ {
+ ...lineItemFrom(p0),
+ amount: 1,
+ price: { amount: 79.99, currency: 'USD' },
+ },
+ {
+ ...lineItemFrom(p1),
+ amount: 2,
+ price: { amount: 89.99, currency: 'USD' },
+ },
+ ],
+ });
+
+ const order = await db.orm.orders.create({
+ userId: String(bob._id),
+ items: [
+ {
+ ...lineItemFrom(p2),
+ amount: 1,
+ price: { amount: 149.99, currency: 'USD' },
+ },
+ ],
+ shippingAddress: '456 Oak Ave, Portland, OR 97201',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date('2026-03-01T10:00:00Z') }],
+ });
+
+ await db.orm.locations.createAll([
+ {
+ name: 'Downtown Flagship',
+ streetAndNumber: '100 Market St',
+ city: 'San Francisco',
+ postalCode: '94105',
+ country: 'US',
+ },
+ {
+ name: 'Portland Store',
+ streetAndNumber: '200 NW 23rd Ave',
+ city: 'Portland',
+ postalCode: '97210',
+ country: 'US',
+ },
+ {
+ name: 'Seattle Pike Place',
+ streetAndNumber: '85 Pike St',
+ city: 'Seattle',
+ postalCode: '98101',
+ country: 'US',
+ },
+ {
+ name: 'Austin South Congress',
+ streetAndNumber: '1400 S Congress Ave',
+ city: 'Austin',
+ postalCode: '78704',
+ country: 'US',
+ },
+ ]);
+
+ await db.orm.invoices.create({
+ orderId: String(order._id),
+ items: [{ name: 'Leather Crossbody Bag', amount: 1, unitPrice: 149.99, lineTotal: 149.99 }],
+ subtotal: 149.99,
+ tax: 12.75,
+ total: 162.74,
+ issuedAt: new Date('2026-03-01T10:05:00Z'),
+ });
+
+ 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',
+ });
+
+ await createAddToCartEvent(db, {
+ userId: 'alice-session-1',
+ sessionId: 'sess-001',
+ timestamp: new Date('2026-03-01T09:05:00Z'),
+ productId: String(p0._id),
+ brand: 'Heritage',
+ });
+
+ await createSearchEvent(db, {
+ userId: 'bob-session-1',
+ sessionId: 'sess-002',
+ timestamp: new Date('2026-03-01T09:30:00Z'),
+ query: 'leather bag',
+ });
+}
diff --git a/examples/retail-store/test/aggregation.test.ts b/examples/retail-store/test/aggregation.test.ts
new file mode 100644
index 0000000000..0059901deb
--- /dev/null
+++ b/examples/retail-store/test/aggregation.test.ts
@@ -0,0 +1,72 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import {
+ aggregateEventsByType,
+ createAddToCartEvent,
+ createSearchEvent,
+ createViewProductEvent,
+} from '../src/data/events';
+import { getRandomProducts } from '../src/data/products';
+import { setupTestDb } from './setup';
+
+describe('aggregation pipelines', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('aggregation_test');
+
+ it('aggregates events by type for a user', async () => {
+ for (let i = 0; i < 3; i++) {
+ await createViewProductEvent(ctx.db, {
+ userId: 'test-user',
+ sessionId: `sess-view-${i}`,
+ timestamp: new Date(),
+ productId: `prod-${i}`,
+ subCategory: 'Topwear',
+ brand: 'TestBrand',
+ });
+ }
+ for (let i = 0; i < 2; i++) {
+ await createAddToCartEvent(ctx.db, {
+ userId: 'test-user',
+ sessionId: `sess-cart-${i}`,
+ timestamp: new Date(),
+ productId: `prod-${i}`,
+ brand: 'TestBrand',
+ });
+ }
+ await createSearchEvent(ctx.db, {
+ userId: 'test-user',
+ sessionId: 'sess-search-0',
+ timestamp: new Date(),
+ query: 'test query',
+ });
+
+ 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 });
+ });
+
+ it('samples random products', async () => {
+ await ctx.db.orm.products.createAll(
+ Array.from({ length: 10 }, (_, i) => ({
+ name: `Product ${i}`,
+ brand: 'TestBrand',
+ code: `TB-${i}`,
+ description: `Description ${i}`,
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Shirts',
+ price: { amount: 10 + i, currency: 'USD' },
+ image: { url: `/img${i}.jpg` },
+ embedding: null,
+ })),
+ );
+
+ const sample = await getRandomProducts(ctx.db, 3);
+ expect(sample).toHaveLength(3);
+ for (const product of sample) {
+ expect(product).toHaveProperty('name');
+ expect(product).toHaveProperty('price');
+ }
+ });
+});
diff --git a/examples/retail-store/test/api-flows.test.ts b/examples/retail-store/test/api-flows.test.ts
new file mode 100644
index 0000000000..f555847351
--- /dev/null
+++ b/examples/retail-store/test/api-flows.test.ts
@@ -0,0 +1,290 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import { addToCart, clearCart, getCartByUserId } from '../src/data/carts';
+import {
+ createOrder,
+ deleteOrder,
+ getOrderById,
+ getOrderWithUser,
+ getUserOrders,
+ updateOrderStatus,
+} from '../src/data/orders';
+import { setupTestDb } from './setup';
+
+const ITEM_A = {
+ productId: 'prod-1',
+ name: 'Shirt',
+ brand: 'Heritage',
+ amount: 1,
+ price: { amount: 79.99, currency: 'USD' },
+ image: { url: '/images/products/shirt.jpg' },
+};
+
+const ITEM_B = {
+ productId: 'prod-2',
+ name: 'Chinos',
+ brand: 'UrbanEdge',
+ amount: 2,
+ price: { amount: 59.99, currency: 'USD' },
+ image: { url: '/images/products/chinos.jpg' },
+};
+
+describe('API flow: order ownership (auth guard)', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('api_flows_order_auth');
+
+ it('other user cannot see the order in their list', async () => {
+ const alice = await ctx.db.orm.users.create({
+ name: 'Alice',
+ email: 'a@test.com',
+ address: null,
+ });
+ const bob = await ctx.db.orm.users.create({ name: 'Bob', email: 'b@test.com', address: null });
+
+ const order = await createOrder(ctx.db, {
+ userId: alice._id as string,
+ items: [ITEM_A],
+ shippingAddress: '123 Main St',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ const orderId = order._id as string;
+ const fetched = await getOrderWithUser(ctx.db, orderId);
+ expect(fetched).not.toBeNull();
+ expect(String(fetched!.userId)).toBe(String(alice._id));
+
+ const bobOrders = await getUserOrders(ctx.db, bob._id as string);
+ expect(bobOrders).toHaveLength(0);
+ });
+
+ it('user can only see their own orders via getUserOrders', async () => {
+ const alice = await ctx.db.orm.users.create({
+ name: 'Alice',
+ email: 'a@test.com',
+ address: null,
+ });
+ const bob = await ctx.db.orm.users.create({ name: 'Bob', email: 'b@test.com', address: null });
+
+ await createOrder(ctx.db, {
+ userId: alice._id as string,
+ items: [ITEM_A],
+ shippingAddress: '123 Main St',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ await createOrder(ctx.db, {
+ userId: bob._id as string,
+ items: [ITEM_B],
+ shippingAddress: '456 Oak Ave',
+ type: 'bopis',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ const aliceOrders = await getUserOrders(ctx.db, alice._id as string);
+ const bobOrders = await getUserOrders(ctx.db, bob._id as string);
+
+ expect(aliceOrders).toHaveLength(1);
+ expect(bobOrders).toHaveLength(1);
+ expect(aliceOrders[0]!.shippingAddress).toBe('123 Main St');
+ expect(bobOrders[0]!.shippingAddress).toBe('456 Oak Ave');
+ });
+
+ it('deleteOrder only removes the targeted order', async () => {
+ const alice = await ctx.db.orm.users.create({
+ name: 'Alice',
+ email: 'a@test.com',
+ address: null,
+ });
+
+ const order1 = await createOrder(ctx.db, {
+ userId: alice._id as string,
+ items: [ITEM_A],
+ shippingAddress: '123 Main St',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ const order2 = await createOrder(ctx.db, {
+ userId: alice._id as string,
+ items: [ITEM_B],
+ shippingAddress: '456 Oak Ave',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ await deleteOrder(ctx.db, order1._id as string);
+
+ const remaining = await getUserOrders(ctx.db, alice._id as string);
+ expect(remaining).toHaveLength(1);
+ expect(remaining[0]!._id).toEqual(order2._id);
+ });
+});
+
+describe('API flow: checkout (cart → order → clear)', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('api_flows_checkout');
+
+ it('creates order from cart items and clears the cart server-side', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Carol',
+ email: 'c@test.com',
+ address: null,
+ });
+ const userId = user._id as string;
+
+ await addToCart(ctx.db, userId, ITEM_A);
+ await addToCart(ctx.db, userId, ITEM_B);
+
+ const cart = await getCartByUserId(ctx.db, userId);
+ expect(cart!.items).toHaveLength(2);
+
+ const order = await createOrder(ctx.db, {
+ userId,
+ items: cart!.items.map((item) => ({
+ productId: item.productId as string,
+ name: item.name as string,
+ brand: item.brand as string,
+ amount: item.amount,
+ price: { amount: Number(item.price.amount), currency: item.price.currency as string },
+ image: { url: item.image.url as string },
+ })),
+ shippingAddress: '789 Elm Blvd',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ await clearCart(ctx.db, userId);
+
+ expect(order.items).toHaveLength(2);
+ expect(order.statusHistory).toHaveLength(1);
+ expect(order.statusHistory[0]).toMatchObject({ status: 'placed' });
+
+ const clearedCart = await getCartByUserId(ctx.db, userId);
+ expect(clearedCart!.items).toHaveLength(0);
+ });
+
+ it('order retains items after cart is cleared', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Dave',
+ email: 'd@test.com',
+ address: null,
+ });
+ const userId = user._id as string;
+
+ await addToCart(ctx.db, userId, ITEM_A);
+ const cart = await getCartByUserId(ctx.db, userId);
+
+ const order = await createOrder(ctx.db, {
+ userId,
+ items: cart!.items.map((item) => ({
+ productId: item.productId as string,
+ name: item.name as string,
+ brand: item.brand as string,
+ amount: item.amount,
+ price: { amount: Number(item.price.amount), currency: item.price.currency as string },
+ image: { url: item.image.url as string },
+ })),
+ shippingAddress: '101 Pine',
+ type: 'bopis',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ await clearCart(ctx.db, userId);
+
+ const fetchedOrder = await getOrderById(ctx.db, order._id as string);
+ expect(fetchedOrder).not.toBeNull();
+ expect(fetchedOrder!.items).toHaveLength(1);
+ expect(fetchedOrder!.items[0]).toMatchObject({ name: 'Shirt' });
+ });
+});
+
+describe('API flow: order status progression', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('api_flows_order_status');
+
+ it('advances through placed → shipped → delivered', async () => {
+ const user = await ctx.db.orm.users.create({ name: 'Eve', email: 'e@test.com', address: null });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [ITEM_A],
+ shippingAddress: '200 Cedar',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date('2026-04-01T10:00:00Z') }],
+ });
+
+ await updateOrderStatus(ctx.db, order._id as string, {
+ status: 'shipped',
+ timestamp: new Date('2026-04-02T14:00:00Z'),
+ });
+
+ await updateOrderStatus(ctx.db, order._id as string, {
+ status: 'delivered',
+ timestamp: new Date('2026-04-04T09:00:00Z'),
+ });
+
+ const final = await getOrderById(ctx.db, order._id as string);
+ expect(final!.statusHistory).toHaveLength(3);
+ const statuses = final!.statusHistory.map((s) => s.status);
+ expect(statuses).toEqual(['placed', 'shipped', 'delivered']);
+ });
+
+ it('updateOrderStatus on non-existent order returns null', async () => {
+ const result = await updateOrderStatus(ctx.db, '000000000000000000000000', {
+ status: 'shipped',
+ timestamp: new Date(),
+ });
+ expect(result).toBeNull();
+ });
+
+ it('order with user relation includes user data after status update', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Frank',
+ email: 'f@test.com',
+ address: {
+ streetAndNumber: '300 Maple',
+ city: 'Springfield',
+ postalCode: '62701',
+ country: 'US',
+ },
+ });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [ITEM_A],
+ shippingAddress: '300 Maple',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ await updateOrderStatus(ctx.db, order._id as string, {
+ status: 'shipped',
+ timestamp: new Date(),
+ });
+
+ const withUser = await getOrderWithUser(ctx.db, order._id as string);
+ expect(withUser).not.toBeNull();
+ expect(withUser!.user).toMatchObject({ name: 'Frank', email: 'f@test.com' });
+ expect(withUser!.statusHistory).toHaveLength(2);
+ });
+});
+
+describe('API flow: duplicate cart add behavior', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('api_flows_cart_duplicate');
+
+ it('adding same product twice creates two separate line items', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Grace',
+ email: 'g@test.com',
+ address: null,
+ });
+ const userId = user._id as string;
+
+ await addToCart(ctx.db, userId, ITEM_A);
+ await addToCart(ctx.db, userId, ITEM_A);
+
+ 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');
+ });
+});
diff --git a/examples/retail-store/test/cart-lifecycle.test.ts b/examples/retail-store/test/cart-lifecycle.test.ts
new file mode 100644
index 0000000000..7a5931e0c6
--- /dev/null
+++ b/examples/retail-store/test/cart-lifecycle.test.ts
@@ -0,0 +1,115 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import { addToCart, clearCart, getCartByUserId, removeFromCart } from '../src/data/carts';
+import { setupTestDb } from './setup';
+
+const ITEM_SHIRT = {
+ productId: 'prod-1',
+ name: 'Shirt',
+ brand: 'Heritage',
+ amount: 1,
+ price: { amount: 79.99, currency: 'USD' },
+ image: { url: '/shirt.jpg' },
+};
+
+const ITEM_CHINOS = {
+ productId: 'prod-2',
+ name: 'Chinos',
+ brand: 'UrbanEdge',
+ amount: 1,
+ price: { amount: 59.99, currency: 'USD' },
+ image: { url: '/chinos.jpg' },
+};
+
+describe('cart lifecycle (integration)', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('cart_lifecycle_test');
+
+ it('addToCart creates a cart when none exists', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Alice',
+ email: 'alice@example.com',
+ address: null,
+ });
+
+ await addToCart(ctx.db, user._id as string, ITEM_SHIRT);
+
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart).not.toBeNull();
+ expect(cart!.items).toHaveLength(1);
+ expect(cart!.items[0]).toMatchObject({ name: 'Shirt', productId: 'prod-1' });
+ });
+
+ it('addToCart appends to an existing cart', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Bob',
+ email: 'bob@example.com',
+ address: null,
+ });
+
+ await addToCart(ctx.db, user._id as string, ITEM_SHIRT);
+ await addToCart(ctx.db, user._id as string, ITEM_CHINOS);
+
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart).not.toBeNull();
+ expect(cart!.items).toHaveLength(2);
+ const names = cart!.items.map((i) => i.name).sort();
+ expect(names).toEqual(['Chinos', 'Shirt']);
+ });
+
+ it('removeFromCart removes item by productId', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Carol',
+ email: 'carol@example.com',
+ address: null,
+ });
+
+ await addToCart(ctx.db, user._id as string, ITEM_SHIRT);
+ await addToCart(ctx.db, user._id as string, ITEM_CHINOS);
+ await removeFromCart(ctx.db, user._id as string, 'prod-1');
+
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart).not.toBeNull();
+ expect(cart!.items).toHaveLength(1);
+ expect(cart!.items[0]!.productId).toBe('prod-2');
+ });
+
+ it('clearCart empties the items array', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Dave',
+ email: 'dave@example.com',
+ address: null,
+ });
+
+ await addToCart(ctx.db, user._id as string, ITEM_SHIRT);
+ await clearCart(ctx.db, user._id as string);
+
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart).not.toBeNull();
+ expect(cart!.items).toHaveLength(0);
+ });
+
+ it('full lifecycle: add → add → remove → clear', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Eve',
+ email: 'eve@example.com',
+ address: null,
+ });
+
+ await addToCart(ctx.db, user._id as string, ITEM_SHIRT);
+ const afterFirst = await getCartByUserId(ctx.db, user._id as string);
+ expect(afterFirst!.items).toHaveLength(1);
+
+ await addToCart(ctx.db, user._id as string, ITEM_CHINOS);
+ const afterSecond = await getCartByUserId(ctx.db, user._id as string);
+ expect(afterSecond!.items).toHaveLength(2);
+
+ await removeFromCart(ctx.db, user._id as string, 'prod-1');
+ const afterRemove = await getCartByUserId(ctx.db, user._id as string);
+ expect(afterRemove!.items).toHaveLength(1);
+ expect(afterRemove!.items[0]!.name).toBe('Chinos');
+
+ await clearCart(ctx.db, user._id as string);
+ const afterClear = await getCartByUserId(ctx.db, user._id as string);
+ expect(afterClear!.items).toHaveLength(0);
+ });
+});
diff --git a/examples/retail-store/test/crud-lifecycle.test.ts b/examples/retail-store/test/crud-lifecycle.test.ts
new file mode 100644
index 0000000000..331e8abd54
--- /dev/null
+++ b/examples/retail-store/test/crud-lifecycle.test.ts
@@ -0,0 +1,256 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import { clearCart, getCartByUserId, upsertCart } from '../src/data/carts';
+import { createViewProductEvent, findEventsByUser } from '../src/data/events';
+import { createInvoice, findInvoiceById } from '../src/data/invoices';
+import { findLocations } from '../src/data/locations';
+import { createOrder, deleteOrder, getOrderById, getUserOrders } from '../src/data/orders';
+import { findProductById, findProducts } from '../src/data/products';
+import { findUserById, findUsers } from '../src/data/users';
+import { setupTestDb } from './setup';
+
+describe('CRUD lifecycle', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('crud_lifecycle_test');
+
+ it('creates and reads products with embedded Price and Image', async () => {
+ const product = await ctx.db.orm.products.create({
+ name: 'Test Shirt',
+ brand: 'TestBrand',
+ code: 'TB-001',
+ description: 'A test shirt',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Shirts',
+ price: { amount: 49.99, currency: 'USD' },
+ image: { url: '/test.jpg' },
+ embedding: null,
+ });
+
+ expect(product._id).toBeDefined();
+ expect(product.price).toEqual({ amount: 49.99, currency: 'USD' });
+ expect(product.image).toEqual({ url: '/test.jpg' });
+
+ const all = await findProducts(ctx.db);
+ expect(all).toHaveLength(1);
+ expect(all[0]).toMatchObject({ name: 'Test Shirt', brand: 'TestBrand' });
+
+ const found = await findProductById(ctx.db, product._id as string);
+ expect(found).not.toBeNull();
+ expect(found!.name).toBe('Test Shirt');
+ });
+
+ it('creates and reads users with embedded Address', async () => {
+ const address = {
+ streetAndNumber: '123 Test St',
+ city: 'TestCity',
+ postalCode: '12345',
+ country: 'US',
+ };
+ const user = await ctx.db.orm.users.create({
+ name: 'Test User',
+ email: 'test@example.com',
+ address,
+ });
+
+ expect(user.address).toEqual(address);
+
+ const found = await findUserById(ctx.db, user._id as string);
+ expect(found).not.toBeNull();
+ expect(found!.address).toEqual(address);
+ });
+
+ it('creates user with null address', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'No Address',
+ email: 'noaddr@example.com',
+ address: null,
+ });
+
+ expect(user.address).toBeNull();
+ const all = await findUsers(ctx.db);
+ expect(all).toHaveLength(1);
+ expect(all[0]!.address).toBeNull();
+ });
+
+ it('creates and reads locations', async () => {
+ await ctx.db.orm.locations.createAll([
+ {
+ name: 'Store A',
+ streetAndNumber: '1 Main St',
+ city: 'CityA',
+ postalCode: '11111',
+ country: 'US',
+ },
+ {
+ name: 'Store B',
+ streetAndNumber: '2 Oak Ave',
+ city: 'CityB',
+ postalCode: '22222',
+ country: 'US',
+ },
+ ]);
+
+ const locations = await findLocations(ctx.db);
+ expect(locations).toHaveLength(2);
+ const names = locations.map((l) => l.name).sort();
+ expect(names).toEqual(['Store A', 'Store B']);
+ });
+
+ it('creates order with embedded line items and status history, then deletes', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Buyer',
+ email: 'buyer@example.com',
+ address: null,
+ });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [
+ {
+ productId: 'prod-1',
+ name: 'Item 1',
+ brand: 'Brand1',
+ amount: 2,
+ price: { amount: 29.99, currency: 'USD' },
+ image: { url: '/item1.jpg' },
+ },
+ ],
+ shippingAddress: '456 Test Ave',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date('2026-01-01') }],
+ });
+
+ expect(order._id).toBeDefined();
+ expect(order.items).toHaveLength(1);
+ expect(order.items[0]).toMatchObject({ name: 'Item 1', amount: 2 });
+ expect(order.statusHistory).toHaveLength(1);
+ expect(order.statusHistory[0]).toMatchObject({ status: 'placed' });
+
+ const userOrders = await getUserOrders(ctx.db, user._id as string);
+ expect(userOrders).toHaveLength(1);
+
+ const found = await getOrderById(ctx.db, order._id as string);
+ expect(found).not.toBeNull();
+ expect(found!.shippingAddress).toBe('456 Test Ave');
+
+ const deleted = await deleteOrder(ctx.db, order._id as string);
+ expect(deleted).not.toBeNull();
+
+ const afterDelete = await getUserOrders(ctx.db, user._id as string);
+ expect(afterDelete).toHaveLength(0);
+ });
+
+ it('upserts cart (insert and update paths)', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Shopper',
+ email: 'shopper@example.com',
+ address: null,
+ });
+
+ const item = {
+ productId: 'prod-1',
+ name: 'Widget',
+ brand: 'Acme',
+ amount: 1,
+ price: { amount: 9.99, currency: 'USD' },
+ image: { url: '/widget.jpg' },
+ };
+
+ await upsertCart(ctx.db, user._id as string, [item]);
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart).not.toBeNull();
+ expect(cart!.items).toHaveLength(1);
+ expect(cart!.items[0]).toMatchObject({ name: 'Widget' });
+
+ const updatedItem = { ...item, amount: 3 };
+ await upsertCart(ctx.db, user._id as string, [updatedItem]);
+ const updatedCart = await getCartByUserId(ctx.db, user._id as string);
+ expect(updatedCart).not.toBeNull();
+ expect(updatedCart!.items).toHaveLength(1);
+ expect(updatedCart!.items[0]!.amount).toBe(3);
+ });
+
+ it('clears cart', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'ClearTest',
+ email: 'clear@example.com',
+ address: null,
+ });
+
+ await upsertCart(ctx.db, user._id as string, [
+ {
+ productId: 'prod-1',
+ name: 'Widget',
+ brand: 'Acme',
+ amount: 1,
+ price: { amount: 9.99, currency: 'USD' },
+ image: { url: '/widget.jpg' },
+ },
+ ]);
+
+ await clearCart(ctx.db, user._id as string);
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart).not.toBeNull();
+ expect(cart!.items).toHaveLength(0);
+ });
+
+ it('creates invoice with embedded line items', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'InvUser',
+ email: 'inv@example.com',
+ address: null,
+ });
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [
+ {
+ productId: 'prod-1',
+ name: 'Item',
+ brand: 'B',
+ amount: 1,
+ price: { amount: 100, currency: 'USD' },
+ image: { url: '/item.jpg' },
+ },
+ ],
+ shippingAddress: '123 St',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ const invoice = await createInvoice(ctx.db, {
+ orderId: order._id as string,
+ items: [{ name: 'Item', amount: 1, unitPrice: 100, lineTotal: 100 }],
+ subtotal: 100,
+ tax: 8.5,
+ total: 108.5,
+ issuedAt: new Date('2026-03-15'),
+ });
+
+ expect(invoice._id).toBeDefined();
+ expect(invoice.items).toHaveLength(1);
+ expect(invoice.total).toBe(108.5);
+
+ const found = await findInvoiceById(ctx.db, invoice._id as string);
+ expect(found).not.toBeNull();
+ expect(found!.items[0]).toMatchObject({ name: 'Item', unitPrice: 100 });
+ });
+
+ it('creates and reads polymorphic events', async () => {
+ await createViewProductEvent(ctx.db, {
+ userId: 'user-1',
+ sessionId: 'sess-1',
+ timestamp: new Date('2026-03-01'),
+ productId: 'prod-1',
+ subCategory: 'Topwear',
+ brand: 'TestBrand',
+ });
+
+ const events = await findEventsByUser(ctx.db, 'user-1');
+ expect(events).toHaveLength(1);
+ expect(events[0]).toMatchObject({
+ type: 'view-product',
+ productId: 'prod-1',
+ brand: 'TestBrand',
+ });
+ });
+});
diff --git a/examples/retail-store/test/migration.test.ts b/examples/retail-store/test/migration.test.ts
new file mode 100644
index 0000000000..0284f60d78
--- /dev/null
+++ b/examples/retail-store/test/migration.test.ts
@@ -0,0 +1,148 @@
+import { validateMongoContract } from '@prisma-next/mongo-contract';
+import { timeouts } from '@prisma-next/test-utils';
+import { type Db, MongoClient } from 'mongodb';
+import { MongoMemoryReplSet } from 'mongodb-memory-server';
+import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import type { Contract } from '../src/contract';
+import contractJson from '../src/contract.json' with { type: 'json' };
+
+describe('migration', { timeout: timeouts.spinUpDbServer }, () => {
+ let replSet: MongoMemoryReplSet;
+ let client: MongoClient;
+ let db: Db;
+ const dbName = 'migration_test';
+
+ beforeAll(async () => {
+ replSet = await MongoMemoryReplSet.create({
+ replSet: { count: 1, storageEngine: 'wiredTiger' },
+ });
+ client = new MongoClient(replSet.getUri());
+ await client.connect();
+ db = client.db(dbName);
+ }, timeouts.spinUpDbServer);
+
+ afterAll(async () => {
+ await Promise.allSettled([client?.close(), replSet?.stop()]);
+ }, timeouts.spinUpDbServer);
+
+ it('contract contains expected index definitions', () => {
+ const { contract } = validateMongoContract(contractJson);
+ const { collections } = contract.storage;
+
+ expect(collections.products.indexes).toEqual([
+ {
+ keys: [
+ { field: 'name', direction: 'text' },
+ { field: 'description', direction: 'text' },
+ ],
+ weights: { name: 10, description: 1 },
+ },
+ {
+ keys: [
+ { field: 'brand', direction: 1 },
+ { field: 'subCategory', direction: 1 },
+ ],
+ },
+ { keys: [{ field: 'code', direction: 'hashed' }] },
+ ]);
+
+ expect(collections.users.indexes).toEqual([
+ { keys: [{ field: 'email', direction: 1 }], unique: true },
+ ]);
+
+ expect(collections.carts.indexes).toEqual([
+ { keys: [{ field: 'userId', direction: 1 }], unique: true },
+ ]);
+
+ expect(collections.orders.indexes).toEqual([{ keys: [{ field: 'userId', direction: 1 }] }]);
+
+ expect(collections.events.indexes).toEqual([
+ {
+ keys: [
+ { field: 'userId', direction: 1 },
+ { field: 'timestamp', direction: -1 },
+ ],
+ },
+ { keys: [{ field: 'timestamp', direction: 1 }], expireAfterSeconds: 7776000 },
+ ]);
+
+ expect(collections.invoices.indexes).toEqual([
+ { keys: [{ field: 'orderId', direction: 1 }] },
+ { keys: [{ field: 'issuedAt', direction: -1 }], sparse: true },
+ ]);
+
+ expect(collections.locations.indexes).toEqual([
+ {
+ keys: [
+ { field: 'city', direction: 1 },
+ { field: 'country', direction: 1 },
+ ],
+ collation: { locale: 'en', strength: 2 },
+ },
+ ]);
+ });
+
+ it('creates indexes on real MongoDB and verifies they exist', async () => {
+ await db
+ .collection('products')
+ .createIndex(
+ { name: 'text', description: 'text' },
+ { weights: { name: 10, description: 1 } },
+ );
+ await db.collection('products').createIndex({ brand: 1, subCategory: 1 });
+ await db.collection('products').createIndex({ code: 'hashed' });
+ await db.collection('users').createIndex({ email: 1 }, { unique: true });
+ await db.collection('carts').createIndex({ userId: 1 }, { unique: true });
+ await db.collection('orders').createIndex({ userId: 1 });
+ await db.collection('events').createIndex({ userId: 1, timestamp: -1 });
+ await db.collection('events').createIndex({ timestamp: 1 }, { expireAfterSeconds: 7776000 });
+ await db.collection('invoices').createIndex({ orderId: 1 });
+ await db.collection('invoices').createIndex({ issuedAt: -1 }, { sparse: true });
+ await db
+ .collection('locations')
+ .createIndex({ city: 1, country: 1 }, { collation: { locale: 'en', strength: 2 } });
+
+ const productIndexes = await db.collection('products').indexes();
+ expect(productIndexes).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ key: { _fts: 'text', _ftsx: 1 },
+ weights: { name: 10, description: 1 },
+ }),
+ expect.objectContaining({ key: { brand: 1, subCategory: 1 } }),
+ expect.objectContaining({ key: { code: 'hashed' } }),
+ ]),
+ );
+
+ const userIndexes = await db.collection('users').indexes();
+ expect(userIndexes).toEqual(
+ expect.arrayContaining([expect.objectContaining({ key: { email: 1 }, unique: true })]),
+ );
+
+ const eventIndexes = await db.collection('events').indexes();
+ expect(eventIndexes).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ key: { userId: 1, timestamp: -1 } }),
+ expect.objectContaining({ key: { timestamp: 1 }, expireAfterSeconds: 7776000 }),
+ ]),
+ );
+
+ const invoiceIndexes = await db.collection('invoices').indexes();
+ expect(invoiceIndexes).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ key: { orderId: 1 } }),
+ expect.objectContaining({ key: { issuedAt: -1 }, sparse: true }),
+ ]),
+ );
+
+ const locationIndexes = await db.collection('locations').indexes();
+ expect(locationIndexes).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ key: { city: 1, country: 1 },
+ collation: expect.objectContaining({ locale: 'en', strength: 2 }),
+ }),
+ ]),
+ );
+ });
+});
diff --git a/examples/retail-store/test/order-lifecycle.test.ts b/examples/retail-store/test/order-lifecycle.test.ts
new file mode 100644
index 0000000000..ed6a09c421
--- /dev/null
+++ b/examples/retail-store/test/order-lifecycle.test.ts
@@ -0,0 +1,162 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import { addToCart, clearCart, getCartByUserId } from '../src/data/carts';
+import {
+ createOrder,
+ deleteOrder,
+ getOrderById,
+ getOrderWithUser,
+ getUserOrders,
+ updateOrderStatus,
+} from '../src/data/orders';
+import { setupTestDb } from './setup';
+
+const ITEM = {
+ productId: 'prod-1',
+ name: 'Shirt',
+ brand: 'Heritage',
+ amount: 2,
+ price: { amount: 79.99, currency: 'USD' },
+ image: { url: '/shirt.jpg' },
+};
+
+describe('order lifecycle (integration)', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('order_lifecycle_test');
+
+ it('creates an order from cart items and clears the cart', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Alice',
+ email: 'alice@example.com',
+ address: null,
+ });
+
+ await addToCart(ctx.db, user._id as string, ITEM);
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart!.items).toHaveLength(1);
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: cart!.items,
+ shippingAddress: '123 Main St',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date('2026-03-01T10:00:00Z') }],
+ });
+
+ expect(order._id).toBeDefined();
+ expect(order.items).toHaveLength(1);
+ expect(order.statusHistory[0]).toMatchObject({ status: 'placed' });
+
+ await clearCart(ctx.db, user._id as string);
+ const clearedCart = await getCartByUserId(ctx.db, user._id as string);
+ expect(clearedCart!.items).toHaveLength(0);
+ });
+
+ it('tracks order status through placed → shipped → delivered', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Bob',
+ email: 'bob@example.com',
+ address: null,
+ });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [ITEM],
+ shippingAddress: '456 Oak Ave',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date('2026-03-01T10:00:00Z') }],
+ });
+
+ const shipped = await updateOrderStatus(ctx.db, order._id as string, {
+ status: 'shipped',
+ timestamp: new Date('2026-03-02T14:00:00Z'),
+ });
+ expect(shipped).not.toBeNull();
+ expect(shipped!['statusHistory']).toHaveLength(2);
+
+ const delivered = await updateOrderStatus(ctx.db, order._id as string, {
+ status: 'delivered',
+ timestamp: new Date('2026-03-04T09:00:00Z'),
+ });
+ expect(delivered).not.toBeNull();
+ expect(delivered!['statusHistory']).toHaveLength(3);
+
+ const statuses = (delivered!['statusHistory'] as Array<{ status: string }>).map(
+ (s) => s.status,
+ );
+ expect(statuses).toEqual(['placed', 'shipped', 'delivered']);
+ });
+
+ it('updateOrderStatus returns null for non-existent order', async () => {
+ const result = await updateOrderStatus(ctx.db, '000000000000000000000000', {
+ status: 'shipped',
+ timestamp: new Date(),
+ });
+ expect(result).toBeNull();
+ });
+
+ it('retrieves order with user relation', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Carol',
+ email: 'carol@example.com',
+ address: { streetAndNumber: '789 Elm', city: 'TestCity', postalCode: '12345', country: 'US' },
+ });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [ITEM],
+ shippingAddress: '789 Elm',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ const withUser = await getOrderWithUser(ctx.db, order._id as string);
+ expect(withUser).not.toBeNull();
+ expect(withUser!.user).toMatchObject({ name: 'Carol', email: 'carol@example.com' });
+ });
+
+ it('getUserOrders returns all orders for a user', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Dave',
+ email: 'dave@example.com',
+ address: null,
+ });
+
+ await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [ITEM],
+ shippingAddress: 'Addr 1',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+ await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [{ ...ITEM, productId: 'prod-2', name: 'Chinos' }],
+ shippingAddress: 'Addr 2',
+ type: 'bopis',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ const orders = await getUserOrders(ctx.db, user._id as string);
+ expect(orders).toHaveLength(2);
+ });
+
+ it('deleteOrder removes the order', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Eve',
+ email: 'eve@example.com',
+ address: null,
+ });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [ITEM],
+ shippingAddress: '101 Pine',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ await deleteOrder(ctx.db, order._id as string);
+ const found = await getOrderById(ctx.db, order._id as string);
+ expect(found).toBeNull();
+ });
+});
diff --git a/examples/retail-store/test/polymorphism.test.ts b/examples/retail-store/test/polymorphism.test.ts
new file mode 100644
index 0000000000..ba72a3c674
--- /dev/null
+++ b/examples/retail-store/test/polymorphism.test.ts
@@ -0,0 +1,142 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import {
+ createAddToCartEvent,
+ createSearchEvent,
+ createViewProductEvent,
+ findEventsByUser,
+ findSearchEventsByUser,
+} from '../src/data/events';
+import { setupTestDb } from './setup';
+
+describe('polymorphic events', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('polymorphism_test');
+
+ it('creates variant events with auto-injected discriminator', async () => {
+ await createViewProductEvent(ctx.db, {
+ userId: 'user-1',
+ sessionId: 'sess-1',
+ timestamp: new Date('2026-03-01'),
+ productId: 'prod-1',
+ subCategory: 'Topwear',
+ brand: 'Heritage',
+ exitMethod: null,
+ });
+
+ await createSearchEvent(ctx.db, {
+ userId: 'user-1',
+ sessionId: 'sess-1',
+ timestamp: new Date('2026-03-01'),
+ query: 'leather bag',
+ });
+
+ await createAddToCartEvent(ctx.db, {
+ userId: 'user-1',
+ sessionId: 'sess-1',
+ timestamp: new Date('2026-03-01'),
+ productId: 'prod-2',
+ brand: 'Urban',
+ });
+
+ const events = await findEventsByUser(ctx.db, 'user-1');
+ expect(events).toHaveLength(3);
+
+ const types = events.map((e) => e.type);
+ expect(types).toContain('view-product');
+ expect(types).toContain('search');
+ expect(types).toContain('add-to-cart');
+ });
+
+ it('queries base collection returns all variants', async () => {
+ await createViewProductEvent(ctx.db, {
+ userId: 'user-a',
+ sessionId: 'sess-a',
+ timestamp: new Date('2026-03-01'),
+ productId: 'prod-1',
+ subCategory: 'Topwear',
+ brand: 'Heritage',
+ exitMethod: 'scroll',
+ });
+
+ await createSearchEvent(ctx.db, {
+ userId: 'user-a',
+ sessionId: 'sess-a',
+ timestamp: new Date('2026-03-02'),
+ query: 'sneakers',
+ });
+
+ const events = await findEventsByUser(ctx.db, 'user-a');
+ expect(events).toHaveLength(2);
+
+ const viewEvent = events.find((e) => e.type === 'view-product');
+ expect(viewEvent).toMatchObject({
+ productId: 'prod-1',
+ subCategory: 'Topwear',
+ brand: 'Heritage',
+ exitMethod: 'scroll',
+ });
+
+ const searchEvent = events.find((e) => e.type === 'search');
+ expect(searchEvent).toMatchObject({ query: 'sneakers' });
+ });
+
+ it('variant query filters by discriminator', async () => {
+ await createSearchEvent(ctx.db, {
+ userId: 'user-b',
+ sessionId: 'sess-b1',
+ timestamp: new Date('2026-03-01'),
+ query: 'bags',
+ });
+
+ await createSearchEvent(ctx.db, {
+ userId: 'user-b',
+ sessionId: 'sess-b2',
+ timestamp: new Date('2026-03-02'),
+ query: 'shoes',
+ });
+
+ await createViewProductEvent(ctx.db, {
+ userId: 'user-b',
+ sessionId: 'sess-b3',
+ timestamp: new Date('2026-03-03'),
+ productId: 'prod-5',
+ subCategory: 'Footwear',
+ brand: 'TrailCraft',
+ exitMethod: null,
+ });
+
+ const searchEvents = await findSearchEventsByUser(ctx.db, 'user-b');
+ expect(searchEvents).toHaveLength(2);
+ for (const event of searchEvents) {
+ expect(event.type).toBe('search');
+ expect(event).toHaveProperty('query');
+ }
+ });
+
+ it('variant fields are accessible on returned documents', async () => {
+ await createViewProductEvent(ctx.db, {
+ userId: 'user-c',
+ sessionId: 'sess-c',
+ timestamp: new Date('2026-04-01'),
+ productId: 'prod-10',
+ subCategory: 'Bags',
+ brand: 'LuxLine',
+ exitMethod: null,
+ });
+
+ const events = await findEventsByUser(ctx.db, 'user-c');
+ expect(events).toHaveLength(1);
+ const event = events[0]!;
+
+ expect(event).toMatchObject({
+ userId: 'user-c',
+ sessionId: 'sess-c',
+ type: 'view-product',
+ productId: 'prod-10',
+ subCategory: 'Bags',
+ brand: 'LuxLine',
+ });
+ expect(event).toHaveProperty('exitMethod', null);
+ expect(event).not.toHaveProperty('metadata');
+ });
+});
diff --git a/examples/retail-store/test/relations.test.ts b/examples/retail-store/test/relations.test.ts
new file mode 100644
index 0000000000..bece620c0e
--- /dev/null
+++ b/examples/retail-store/test/relations.test.ts
@@ -0,0 +1,110 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import { getCartWithUser, upsertCart } from '../src/data/carts';
+import { createInvoice, findInvoiceWithOrder } from '../src/data/invoices';
+import { createOrder, getOrderWithUser } from '../src/data/orders';
+import { setupTestDb } from './setup';
+
+describe('relation loading via $lookup', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('relations_test');
+
+ it('loads cart with user via include()', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Alice',
+ email: 'alice@example.com',
+ address: null,
+ });
+
+ await upsertCart(ctx.db, user._id as string, [
+ {
+ productId: 'prod-1',
+ name: 'Widget',
+ brand: 'Acme',
+ amount: 1,
+ price: { amount: 9.99, currency: 'USD' },
+ image: { url: '/widget.jpg' },
+ },
+ ]);
+
+ const cartWithUser = await getCartWithUser(ctx.db, user._id as string);
+ expect(cartWithUser).not.toBeNull();
+ expect(cartWithUser!.user).toMatchObject({
+ name: 'Alice',
+ email: 'alice@example.com',
+ });
+ expect(cartWithUser!.items).toHaveLength(1);
+ });
+
+ it('loads order with user via include()', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Bob',
+ email: 'bob@example.com',
+ address: null,
+ });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [
+ {
+ productId: 'prod-1',
+ name: 'Item',
+ brand: 'B',
+ amount: 1,
+ price: { amount: 50, currency: 'USD' },
+ image: { url: '/item.jpg' },
+ },
+ ],
+ shippingAddress: '123 St',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ const orderWithUser = await getOrderWithUser(ctx.db, order._id as string);
+ expect(orderWithUser).not.toBeNull();
+ expect(orderWithUser!.user).toMatchObject({
+ name: 'Bob',
+ email: 'bob@example.com',
+ });
+ });
+
+ it('loads invoice with order via include()', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Carol',
+ email: 'carol@example.com',
+ address: null,
+ });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [
+ {
+ productId: 'prod-1',
+ name: 'Item',
+ brand: 'B',
+ amount: 1,
+ price: { amount: 100, currency: 'USD' },
+ image: { url: '/item.jpg' },
+ },
+ ],
+ shippingAddress: '789 St',
+ type: 'bopis',
+ statusHistory: [{ status: 'placed', timestamp: new Date() }],
+ });
+
+ const invoice = await createInvoice(ctx.db, {
+ orderId: order._id as string,
+ items: [{ name: 'Item', amount: 1, unitPrice: 100, lineTotal: 100 }],
+ subtotal: 100,
+ tax: 8.5,
+ total: 108.5,
+ issuedAt: new Date('2026-03-15'),
+ });
+
+ const invoiceWithOrder = await findInvoiceWithOrder(ctx.db, invoice._id as string);
+ expect(invoiceWithOrder).not.toBeNull();
+ expect(invoiceWithOrder!.order).toMatchObject({
+ shippingAddress: '789 St',
+ type: 'bopis',
+ });
+ });
+});
diff --git a/examples/retail-store/test/search.test.ts b/examples/retail-store/test/search.test.ts
new file mode 100644
index 0000000000..81036a7e60
--- /dev/null
+++ b/examples/retail-store/test/search.test.ts
@@ -0,0 +1,111 @@
+import { describe, expect, it } from 'vitest';
+import { findProductsPaginated, searchProducts } from '../src/data/products';
+import { setupTestDb } from './setup';
+
+describe('product search and pagination', () => {
+ const ctx = setupTestDb('search-test');
+
+ async function seedProducts() {
+ await ctx.db.orm.products.createAll([
+ {
+ name: 'Classic Oxford Shirt',
+ brand: 'Heritage',
+ code: 'HER-001',
+ description: 'A shirt',
+ masterCategory: 'Apparel',
+ subCategory: 'Topwear',
+ articleType: 'Shirts',
+ price: { amount: 79.99, currency: 'USD' },
+ image: { url: '/img/1.jpg' },
+ embedding: null,
+ },
+ {
+ name: 'Slim Fit Chinos',
+ brand: 'UrbanEdge',
+ code: 'UE-042',
+ description: 'Pants',
+ masterCategory: 'Apparel',
+ subCategory: 'Bottomwear',
+ articleType: 'Trousers',
+ price: { amount: 59.99, currency: 'USD' },
+ image: { url: '/img/2.jpg' },
+ embedding: null,
+ },
+ {
+ name: 'Leather Crossbody Bag',
+ brand: 'Craftsman',
+ code: 'CRA-017',
+ description: 'A bag',
+ masterCategory: 'Accessories',
+ subCategory: 'Bags',
+ articleType: 'Handbags',
+ price: { amount: 149.99, currency: 'USD' },
+ image: { url: '/img/3.jpg' },
+ embedding: null,
+ },
+ {
+ name: 'Trail Running Shoes',
+ brand: 'TrailMark',
+ code: 'TM-033',
+ description: 'Shoes',
+ masterCategory: 'Footwear',
+ subCategory: 'Shoes',
+ articleType: 'Sports Shoes',
+ price: { amount: 109.99, currency: 'USD' },
+ image: { url: '/img/4.jpg' },
+ embedding: null,
+ },
+ ]);
+ }
+
+ describe('searchProducts', () => {
+ it('finds products by name substring', async () => {
+ await seedProducts();
+ const results = await searchProducts(ctx.db, 'oxford');
+ expect(results).toHaveLength(1);
+ expect(results[0]?.name).toBe('Classic Oxford Shirt');
+ });
+
+ it('finds products by brand', async () => {
+ await seedProducts();
+ const results = await searchProducts(ctx.db, 'heritage');
+ expect(results).toHaveLength(1);
+ expect(results[0]?.brand).toBe('Heritage');
+ });
+
+ it('finds products by articleType', async () => {
+ await seedProducts();
+ const results = await searchProducts(ctx.db, 'trousers');
+ expect(results).toHaveLength(1);
+ expect(results[0]?.articleType).toBe('Trousers');
+ });
+
+ it('returns empty for non-matching query', async () => {
+ await seedProducts();
+ const results = await searchProducts(ctx.db, 'nonexistent');
+ expect(results).toHaveLength(0);
+ });
+
+ it('is case insensitive', async () => {
+ await seedProducts();
+ const results = await searchProducts(ctx.db, 'LEATHER');
+ expect(results.length).toBeGreaterThanOrEqual(1);
+ });
+ });
+
+ describe('findProductsPaginated', () => {
+ it('returns paginated results', async () => {
+ await seedProducts();
+ const page1 = await findProductsPaginated(ctx.db, 0, 2);
+ const page2 = await findProductsPaginated(ctx.db, 2, 2);
+ expect(page1).toHaveLength(2);
+ expect(page2).toHaveLength(2);
+ });
+
+ it('returns empty when skip exceeds count', async () => {
+ await seedProducts();
+ const results = await findProductsPaginated(ctx.db, 100, 10);
+ expect(results).toHaveLength(0);
+ });
+ });
+});
diff --git a/examples/retail-store/test/seed.test.ts b/examples/retail-store/test/seed.test.ts
new file mode 100644
index 0000000000..dcc22a70a6
--- /dev/null
+++ b/examples/retail-store/test/seed.test.ts
@@ -0,0 +1,47 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import { seed } from '../src/seed';
+import { setupTestDb } from './setup';
+
+describe('seed', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('seed_test');
+
+ it('populates all 7 collections and data is queryable', async () => {
+ await seed(ctx.db);
+
+ const products = await ctx.db.orm.products.all();
+ expect(products.length).toBeGreaterThanOrEqual(3);
+ expect(products[0]!.price).toBeDefined();
+ expect(products[0]!.image).toBeDefined();
+
+ const users = await ctx.db.orm.users.all();
+ expect(users.length).toBeGreaterThanOrEqual(2);
+ const withAddress = users.find((u) => u.address !== null);
+ expect(withAddress).toBeDefined();
+ expect(withAddress!.address).toMatchObject({ city: 'San Francisco' });
+
+ const carts = await ctx.db.orm.carts.all();
+ expect(carts.length).toBeGreaterThanOrEqual(1);
+ expect(carts[0]!.items.length).toBeGreaterThan(0);
+ expect(carts[0]!.items[0]!.price).toBeDefined();
+
+ const orders = await ctx.db.orm.orders.all();
+ expect(orders.length).toBeGreaterThanOrEqual(1);
+ expect(orders[0]!.items.length).toBeGreaterThan(0);
+ expect(orders[0]!.statusHistory.length).toBeGreaterThan(0);
+
+ const locations = await ctx.db.orm.locations.all();
+ expect(locations.length).toBeGreaterThanOrEqual(2);
+
+ const invoices = await ctx.db.orm.invoices.all();
+ expect(invoices.length).toBeGreaterThanOrEqual(1);
+ expect(invoices[0]!.items.length).toBeGreaterThan(0);
+
+ const events = await ctx.db.orm.events.all();
+ expect(events.length).toBeGreaterThanOrEqual(3);
+ const types = events.map((e) => e.type);
+ expect(types).toContain('view-product');
+ expect(types).toContain('add-to-cart');
+ expect(types).toContain('search');
+ });
+});
diff --git a/examples/retail-store/test/setup.ts b/examples/retail-store/test/setup.ts
new file mode 100644
index 0000000000..4b8214c543
--- /dev/null
+++ b/examples/retail-store/test/setup.ts
@@ -0,0 +1,53 @@
+import { createMongoAdapter } from '@prisma-next/adapter-mongo';
+import { createMongoDriver } from '@prisma-next/driver-mongo';
+import { validateMongoContract } from '@prisma-next/mongo-contract';
+import { mongoOrm, mongoRaw } from '@prisma-next/mongo-orm';
+import { mongoPipeline } from '@prisma-next/mongo-pipeline-builder';
+import { createMongoRuntime, type MongoRuntime } from '@prisma-next/mongo-runtime';
+import { timeouts } from '@prisma-next/test-utils';
+import { MongoClient } from 'mongodb';
+import { MongoMemoryReplSet } from 'mongodb-memory-server';
+import { afterAll, beforeAll, beforeEach } from 'vitest';
+import type { Contract } from '../src/contract';
+import contractJson from '../src/contract.json' with { type: 'json' };
+import type { Db } from '../src/db';
+
+const { contract } = validateMongoContract(contractJson);
+const pipeline = mongoPipeline({ contractJson });
+const raw = mongoRaw({ contract });
+
+export function setupTestDb(dbName: string) {
+ let replSet: MongoMemoryReplSet;
+ let client: MongoClient;
+ let runtime: MongoRuntime;
+ let db: Db;
+
+ beforeAll(async () => {
+ replSet = await MongoMemoryReplSet.create({
+ replSet: { count: 1, storageEngine: 'wiredTiger' },
+ });
+ client = new MongoClient(replSet.getUri());
+ await client.connect();
+
+ const adapter = createMongoAdapter();
+ const driver = await createMongoDriver(replSet.getUri(), dbName);
+ runtime = createMongoRuntime({ adapter, driver });
+ const orm = mongoOrm({ contract, executor: runtime });
+
+ db = { orm, runtime, pipeline, raw, contract };
+ }, timeouts.spinUpDbServer);
+
+ beforeEach(async () => {
+ await client.db(dbName).dropDatabase();
+ });
+
+ afterAll(async () => {
+ await Promise.allSettled([runtime?.close(), client?.close(), replSet?.stop()]);
+ }, timeouts.spinUpDbServer);
+
+ return {
+ get db() {
+ return db;
+ },
+ };
+}
diff --git a/examples/retail-store/test/update-operators.test.ts b/examples/retail-store/test/update-operators.test.ts
new file mode 100644
index 0000000000..56efc51d36
--- /dev/null
+++ b/examples/retail-store/test/update-operators.test.ts
@@ -0,0 +1,118 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { describe, expect, it } from 'vitest';
+import { addToCart, getCartByUserId, removeFromCart, upsertCart } from '../src/data/carts';
+import { createOrder, getOrderById, updateOrderStatus } from '../src/data/orders';
+import { setupTestDb } from './setup';
+
+describe('array update operators', { timeout: timeouts.spinUpDbServer }, () => {
+ const ctx = setupTestDb('update_operators_test');
+
+ it('$push adds item to cart', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Alice',
+ email: 'alice@example.com',
+ address: null,
+ });
+
+ await upsertCart(ctx.db, user._id as string, [
+ {
+ productId: 'prod-1',
+ name: 'Shirt',
+ brand: 'Heritage',
+ amount: 1,
+ price: { amount: 79.99, currency: 'USD' },
+ image: { url: '/shirt.jpg' },
+ },
+ ]);
+
+ await addToCart(ctx.db, user._id as string, {
+ productId: 'prod-2',
+ name: 'Chinos',
+ brand: 'UrbanEdge',
+ amount: 1,
+ price: { amount: 59.99, currency: 'USD' },
+ image: { url: '/chinos.jpg' },
+ });
+
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart).not.toBeNull();
+ expect(cart!.items).toHaveLength(2);
+ const names = cart!.items.map((i) => i.name).sort();
+ expect(names).toEqual(['Chinos', 'Shirt']);
+ });
+
+ it('$pull removes item from cart by productId', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Bob',
+ email: 'bob@example.com',
+ address: null,
+ });
+
+ await upsertCart(ctx.db, user._id as string, [
+ {
+ productId: 'prod-1',
+ name: 'Shirt',
+ brand: 'Heritage',
+ amount: 1,
+ price: { amount: 79.99, currency: 'USD' },
+ image: { url: '/shirt.jpg' },
+ },
+ {
+ productId: 'prod-2',
+ name: 'Chinos',
+ brand: 'UrbanEdge',
+ amount: 2,
+ price: { amount: 59.99, currency: 'USD' },
+ image: { url: '/chinos.jpg' },
+ },
+ ]);
+
+ await removeFromCart(ctx.db, user._id as string, 'prod-1');
+
+ const cart = await getCartByUserId(ctx.db, user._id as string);
+ expect(cart).not.toBeNull();
+ expect(cart!.items).toHaveLength(1);
+ expect(cart!.items[0]!.productId).toBe('prod-2');
+ });
+
+ it('$push adds status entry to order statusHistory', async () => {
+ const user = await ctx.db.orm.users.create({
+ name: 'Carol',
+ email: 'carol@example.com',
+ address: null,
+ });
+
+ const order = await createOrder(ctx.db, {
+ userId: user._id as string,
+ items: [
+ {
+ productId: 'prod-1',
+ name: 'Item',
+ brand: 'B',
+ amount: 1,
+ price: { amount: 100, currency: 'USD' },
+ image: { url: '/item.jpg' },
+ },
+ ],
+ shippingAddress: '123 St',
+ type: 'home',
+ statusHistory: [{ status: 'placed', timestamp: new Date('2026-03-01T10:00:00Z') }],
+ });
+
+ await updateOrderStatus(ctx.db, order._id as string, {
+ status: 'shipped',
+ timestamp: new Date('2026-03-02T14:00:00Z'),
+ });
+
+ await updateOrderStatus(ctx.db, order._id as string, {
+ status: 'delivered',
+ timestamp: new Date('2026-03-04T09:00:00Z'),
+ });
+
+ const updated = await getOrderById(ctx.db, order._id as string);
+ expect(updated).not.toBeNull();
+ expect(updated!.statusHistory).toHaveLength(3);
+ const statuses = updated!.statusHistory.map((s) => s.status);
+ expect(statuses).toEqual(['placed', 'shipped', 'delivered']);
+ });
+});
diff --git a/examples/retail-store/tsconfig.json b/examples/retail-store/tsconfig.json
new file mode 100644
index 0000000000..a72fb2b0a7
--- /dev/null
+++ b/examples/retail-store/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "extends": ["@prisma-next/tsconfig/base"],
+ "compilerOptions": {
+ "outDir": "dist",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "jsx": "preserve",
+ "allowJs": true,
+ "noEmit": true,
+ "incremental": true,
+ "isolatedModules": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
+ },
+ "include": [
+ "app/**/*.ts",
+ "app/**/*.tsx",
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "test/**/*.ts",
+ "middleware.ts",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": ["dist", "test/fixtures/**/*"]
+}
diff --git a/examples/retail-store/vitest.config.ts b/examples/retail-store/vitest.config.ts
new file mode 100644
index 0000000000..666c651cb8
--- /dev/null
+++ b/examples/retail-store/vitest.config.ts
@@ -0,0 +1,13 @@
+import { timeouts } from '@prisma-next/test-utils';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ pool: 'threads',
+ maxWorkers: 1,
+ isolate: false,
+ testTimeout: timeouts.spinUpDbServer,
+ hookTimeout: timeouts.spinUpDbServer,
+ },
+});
diff --git a/packages/2-mongo-family/2-authoring/contract-psl/src/derive-json-schema.ts b/packages/2-mongo-family/2-authoring/contract-psl/src/derive-json-schema.ts
index 29ffe4b4d6..0b16508b49 100644
--- a/packages/2-mongo-family/2-authoring/contract-psl/src/derive-json-schema.ts
+++ b/packages/2-mongo-family/2-authoring/contract-psl/src/derive-json-schema.ts
@@ -35,6 +35,9 @@ function fieldToBsonSchema(
if ('many' in field && field.many) {
return { bsonType: 'array', items: voSchema };
}
+ if (field.nullable) {
+ return { oneOf: [{ bsonType: 'null' }, voSchema] };
+ }
return voSchema;
}
diff --git a/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts b/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
index 4ac5c96efe..1648af174d 100644
--- a/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
+++ b/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts
@@ -1458,5 +1458,37 @@ describe('interpretPslDocumentToMongoContract', () => {
},
});
});
+
+ it('handles nullable value object fields with oneOf null or object', () => {
+ const ir = interpretOk(`
+ type Address {
+ street String
+ city String
+ }
+
+ model User {
+ id ObjectId @id @map("_id")
+ address Address?
+ }
+ `);
+ const validator = getValidator(ir, 'user');
+ const schema = validator!['jsonSchema'] as Record;
+ const props = schema['properties'] as Record>;
+ expect(props['address']).toEqual({
+ oneOf: [
+ { bsonType: 'null' },
+ {
+ bsonType: 'object',
+ required: ['city', 'street'],
+ properties: {
+ street: { bsonType: 'string' },
+ city: { bsonType: 'string' },
+ },
+ },
+ ],
+ });
+ const required = schema['required'] as string[];
+ expect(required).not.toContain('address');
+ });
});
});
diff --git a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts
index 26a686d6be..171d9fe02e 100644
--- a/packages/2-mongo-family/5-query-builders/orm/src/collection.ts
+++ b/packages/2-mongo-family/5-query-builders/orm/src/collection.ts
@@ -1,4 +1,9 @@
-import type { ContractReferenceRelation, PlanMeta } from '@prisma-next/contract/types';
+import type {
+ ContractField,
+ ContractReferenceRelation,
+ ContractValueObject,
+ PlanMeta,
+} from '@prisma-next/contract/types';
import type {
MongoContract,
MongoContractWithTypeMaps,
@@ -468,27 +473,71 @@ class MongoCollectionImpl<
return rows;
}
+ #modelFields(): Record {
+ const model = this.#contract.models[this.#modelName] as MongoModelDefinition | undefined;
+ return model?.fields ?? {};
+ }
+
+ #wrapFieldValue(value: unknown, field: ContractField | undefined): MongoValue {
+ if (field === undefined) return new MongoParamRef(value);
+
+ if (field.type.kind === 'scalar') {
+ return new MongoParamRef(value, { codecId: field.type.codecId });
+ }
+
+ if (field.type.kind === 'valueObject') {
+ const voName = field.type.name;
+ const voDef = (this.#contract as { valueObjects?: Record })
+ .valueObjects?.[voName];
+ if (!voDef || value === null) return new MongoParamRef(value);
+
+ if (field.many && Array.isArray(value)) {
+ return value.map((item) =>
+ this.#wrapValueObject(item as Record, voDef),
+ ) as unknown as MongoValue;
+ }
+ return this.#wrapValueObject(value as Record, voDef);
+ }
+
+ return new MongoParamRef(value);
+ }
+
+ #wrapValueObject(
+ data: Record,
+ voDef: ContractValueObject,
+ ): Record {
+ const doc: Record = {};
+ for (const [key, value] of Object.entries(data)) {
+ if (value === undefined) continue;
+ const fieldDef = voDef.fields[key];
+ doc[key] = this.#wrapFieldValue(value, fieldDef);
+ }
+ return doc;
+ }
+
#toDocument(data: Record): Record {
+ const fields = this.#modelFields();
const doc: Record = {};
for (const [key, value] of Object.entries(data)) {
if (value !== undefined) {
- doc[key] = new MongoParamRef(value);
+ doc[key] = this.#wrapFieldValue(value, fields[key]);
}
}
return doc;
}
#toSetFields(data: Record): Record {
- const fields: Record = {};
+ const fields = this.#modelFields();
+ const result: Record = {};
for (const [key, value] of Object.entries(data)) {
if (key === '_id' && value !== undefined) {
throw new Error('Mutation payloads cannot modify `_id`');
}
if (value !== undefined) {
- fields[key] = new MongoParamRef(value);
+ result[key] = this.#wrapFieldValue(value, fields[key]);
}
}
- return fields;
+ return result;
}
#stripUndefined(data: Record): Record {
diff --git a/packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts b/packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts
index 21a76f9d00..1adfba0076 100644
--- a/packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts
+++ b/packages/2-mongo-family/5-query-builders/orm/test/collection.test.ts
@@ -9,6 +9,7 @@ import {
type MongoSkipStage,
type MongoSortStage,
} from '@prisma-next/mongo-query-ast/execution';
+import { MongoParamRef } from '@prisma-next/mongo-value';
import { AsyncIterableResult } from '@prisma-next/runtime-executor';
import { describe, expect, it } from 'vitest';
import type { Contract } from '../../../1-foundation/mongo-contract/test/fixtures/orm-contract';
@@ -309,6 +310,35 @@ describe('MongoCollection write methods', () => {
expect(executor.lastCommand!.kind).toBe('insertOne');
expect(executor.lastCommand!.collection).toBe('users');
});
+
+ it('attaches codecId from contract fields to MongoParamRef in document', async () => {
+ const executor = createMockExecutor([{ insertedId: 'id' }]);
+ const col = createMongoCollection(contract, 'User', executor);
+ await col.create({ name: 'Alice', email: 'a@b.c' });
+ const command = executor.lastCommand!;
+ expect(command.kind).toBe('insertOne');
+ if (command.kind === 'insertOne') {
+ const nameRef = command.document['name'] as MongoParamRef;
+ expect(nameRef).toBeInstanceOf(MongoParamRef);
+ expect(nameRef.codecId).toBe('mongo/string@1');
+ const emailRef = command.document['email'] as MongoParamRef;
+ expect(emailRef).toBeInstanceOf(MongoParamRef);
+ expect(emailRef.codecId).toBe('mongo/string@1');
+ }
+ });
+
+ it('attaches objectId codecId for ObjectId-typed fields', async () => {
+ const executor = createMockExecutor([{ insertedId: 'id' }]);
+ const col = createMongoCollection(contract, 'Task', executor);
+ await col.create({ title: 'Fix bug', assigneeId: 'abc123', type: 'bug' });
+ const command = executor.lastCommand!;
+ expect(command.kind).toBe('insertOne');
+ if (command.kind === 'insertOne') {
+ const assigneeRef = command.document['assigneeId'] as MongoParamRef;
+ expect(assigneeRef).toBeInstanceOf(MongoParamRef);
+ expect(assigneeRef.codecId).toBe('mongo/objectId@1');
+ }
+ });
});
describe('createAll()', () => {
@@ -376,6 +406,20 @@ describe('MongoCollection write methods', () => {
const result = await col.where(MongoFieldFilter.eq('_id', 'missing')).update({ name: 'X' });
expect(result).toBeNull();
});
+
+ it('attaches codecId to $set fields from contract', async () => {
+ const executor = createMockExecutor([{ _id: 'id-1', name: 'Updated', email: 'a@b.c' }]);
+ const col = createMongoCollection(contract, 'User', executor);
+ await col.where(MongoFieldFilter.eq('_id', 'id-1')).update({ name: 'Updated' });
+ const command = executor.lastCommand!;
+ expect(command.kind).toBe('findOneAndUpdate');
+ if (command.kind === 'findOneAndUpdate') {
+ const update = command.update as Record>;
+ const nameRef = update['$set']!['name']!;
+ expect(nameRef).toBeInstanceOf(MongoParamRef);
+ expect(nameRef.codecId).toBe('mongo/string@1');
+ }
+ });
});
describe('updateCount()', () => {
diff --git a/packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts b/packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts
index 0b5e7ec045..cbbd8370c0 100644
--- a/packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts
+++ b/packages/3-mongo-target/2-mongo-adapter/src/mongo-adapter.ts
@@ -1,3 +1,4 @@
+import { createMongoCodecRegistry, type MongoCodecRegistry } from '@prisma-next/mongo-codec';
import type { MongoAdapter } from '@prisma-next/mongo-lowering';
import type {
MongoQueryPlan,
@@ -20,49 +21,58 @@ import {
import { lowerFilter, lowerPipeline, lowerStage } from './lowering';
import { resolveValue } from './resolve-value';
-function resolveDocument(expr: MongoExpr): Document {
- const result: Record = {};
- for (const [key, val] of Object.entries(expr)) {
- result[key] = resolveValue(val);
- }
- return result;
-}
-
function isUpdatePipeline(
update: MongoUpdateSpec,
): update is ReadonlyArray {
return Array.isArray(update);
}
-function lowerUpdate(update: MongoUpdateSpec): Document | ReadonlyArray {
- if (isUpdatePipeline(update)) {
- return update.map((stage) => lowerStage(stage));
+class MongoAdapterImpl implements MongoAdapter {
+ readonly #codecs: MongoCodecRegistry | undefined;
+
+ constructor(codecs?: MongoCodecRegistry) {
+ this.#codecs = codecs;
+ }
+
+ #resolveDocument(expr: MongoExpr): Document {
+ const result: Record = {};
+ for (const [key, val] of Object.entries(expr)) {
+ result[key] = resolveValue(val, this.#codecs);
+ }
+ return result;
+ }
+
+ #lowerUpdate(update: MongoUpdateSpec): Document | ReadonlyArray {
+ if (isUpdatePipeline(update)) {
+ return update.map((stage) => lowerStage(stage));
+ }
+ return this.#resolveDocument(update);
}
- return resolveDocument(update);
-}
-class MongoAdapterImpl implements MongoAdapter {
lower(plan: MongoQueryPlan): AnyMongoWireCommand {
const { command } = plan;
switch (command.kind) {
case 'insertOne':
- return new InsertOneWireCommand(command.collection, resolveDocument(command.document));
+ return new InsertOneWireCommand(
+ command.collection,
+ this.#resolveDocument(command.document),
+ );
case 'updateOne':
return new UpdateOneWireCommand(
command.collection,
lowerFilter(command.filter),
- lowerUpdate(command.update),
+ this.#lowerUpdate(command.update),
);
case 'insertMany':
return new InsertManyWireCommand(
command.collection,
- command.documents.map((doc) => resolveDocument(doc)),
+ command.documents.map((doc) => this.#resolveDocument(doc)),
);
case 'updateMany':
return new UpdateManyWireCommand(
command.collection,
lowerFilter(command.filter),
- lowerUpdate(command.update),
+ this.#lowerUpdate(command.update),
);
case 'deleteOne':
return new DeleteOneWireCommand(command.collection, lowerFilter(command.filter));
@@ -72,7 +82,7 @@ class MongoAdapterImpl implements MongoAdapter {
return new FindOneAndUpdateWireCommand(
command.collection,
lowerFilter(command.filter),
- lowerUpdate(command.update),
+ this.#lowerUpdate(command.update),
command.upsert,
);
case 'findOneAndDelete':
@@ -111,6 +121,32 @@ class MongoAdapterImpl implements MongoAdapter {
}
}
-export function createMongoAdapter(): MongoAdapter {
- return new MongoAdapterImpl();
+import {
+ mongoBooleanCodec,
+ mongoDateCodec,
+ mongoDoubleCodec,
+ mongoInt32Codec,
+ mongoObjectIdCodec,
+ mongoStringCodec,
+ mongoVectorCodec,
+} from './core/codecs';
+
+function defaultCodecRegistry(): MongoCodecRegistry {
+ const registry = createMongoCodecRegistry();
+ for (const codec of [
+ mongoObjectIdCodec,
+ mongoStringCodec,
+ mongoDoubleCodec,
+ mongoInt32Codec,
+ mongoBooleanCodec,
+ mongoDateCodec,
+ mongoVectorCodec,
+ ]) {
+ registry.register(codec);
+ }
+ return registry;
+}
+
+export function createMongoAdapter(codecs?: MongoCodecRegistry): MongoAdapter {
+ return new MongoAdapterImpl(codecs ?? defaultCodecRegistry());
}
diff --git a/packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts b/packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts
index 8cd7771666..c659bc0d54 100644
--- a/packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts
+++ b/packages/3-mongo-target/2-mongo-adapter/src/resolve-value.ts
@@ -1,8 +1,13 @@
+import type { MongoCodecRegistry } from '@prisma-next/mongo-codec';
import type { MongoValue } from '@prisma-next/mongo-value';
import { MongoParamRef } from '@prisma-next/mongo-value';
-export function resolveValue(value: MongoValue): unknown {
+export function resolveValue(value: MongoValue, codecs?: MongoCodecRegistry): unknown {
if (value instanceof MongoParamRef) {
+ if (value.codecId && codecs) {
+ const codec = codecs.get(value.codecId);
+ if (codec?.encode) return codec.encode(value.value);
+ }
return value.value;
}
if (value === null || typeof value !== 'object') {
@@ -12,11 +17,11 @@ export function resolveValue(value: MongoValue): unknown {
return value;
}
if (Array.isArray(value)) {
- return value.map((v) => resolveValue(v));
+ return value.map((v) => resolveValue(v, codecs));
}
const result: Record = {};
for (const [key, val] of Object.entries(value)) {
- result[key] = resolveValue(val);
+ result[key] = resolveValue(val, codecs);
}
return result;
}
diff --git a/packages/3-mongo-target/2-mongo-adapter/test/mongo-adapter.test.ts b/packages/3-mongo-target/2-mongo-adapter/test/mongo-adapter.test.ts
index 78007998b7..27eb7e5bae 100644
--- a/packages/3-mongo-target/2-mongo-adapter/test/mongo-adapter.test.ts
+++ b/packages/3-mongo-target/2-mongo-adapter/test/mongo-adapter.test.ts
@@ -1,3 +1,4 @@
+import { createMongoCodecRegistry, mongoCodec } from '@prisma-next/mongo-codec';
import type { AnyMongoCommand } from '@prisma-next/mongo-query-ast/execution';
import {
AggregateCommand,
@@ -371,3 +372,59 @@ describe('MongoAdapter', () => {
});
});
});
+
+describe('MongoAdapter with codec registry', () => {
+ const uppercaseCodec = mongoCodec({
+ typeId: 'test/uppercase@1',
+ targetTypes: ['string'],
+ decode: (wire: string) => wire.toLowerCase(),
+ encode: (value: string) => value.toUpperCase(),
+ });
+
+ function registryWithUppercase() {
+ const registry = createMongoCodecRegistry();
+ registry.register(uppercaseCodec);
+ return registry;
+ }
+
+ const adapterWithCodecs = createMongoAdapter(registryWithUppercase());
+
+ it('encodes MongoParamRef with codecId in insertOne document', () => {
+ const command = new InsertOneCommand('users', {
+ name: new MongoParamRef('alice', { codecId: 'test/uppercase@1' }),
+ age: new MongoParamRef(30),
+ });
+ const wire = narrowWire(adapterWithCodecs.lower(plan('users', command)), 'insertOne');
+ expect(wire.document).toEqual({ name: 'ALICE', age: 30 });
+ });
+
+ it('encodes MongoParamRef with codecId in update $set', () => {
+ const command = new UpdateOneCommand(
+ 'users',
+ MongoFieldFilter.eq('_id', new MongoParamRef('id-1')),
+ { $set: { name: new MongoParamRef('bob', { codecId: 'test/uppercase@1' }) } },
+ );
+ const wire = narrowWire(adapterWithCodecs.lower(plan('users', command)), 'updateOne');
+ expect(wire.update).toEqual({ $set: { name: 'BOB' } });
+ });
+
+ it('encodes MongoParamRef with codecId in findOneAndUpdate', () => {
+ const command = new FindOneAndUpdateCommand(
+ 'users',
+ MongoFieldFilter.eq('_id', new MongoParamRef('id-1')),
+ { $set: { name: new MongoParamRef('charlie', { codecId: 'test/uppercase@1' }) } },
+ true,
+ );
+ const wire = narrowWire(adapterWithCodecs.lower(plan('users', command)), 'findOneAndUpdate');
+ expect(wire.update).toEqual({ $set: { name: 'CHARLIE' } });
+ });
+
+ it('encodes MongoParamRef with codecId in insertMany documents', () => {
+ const command = new InsertManyCommand('users', [
+ { name: new MongoParamRef('alice', { codecId: 'test/uppercase@1' }) },
+ { name: new MongoParamRef('bob', { codecId: 'test/uppercase@1' }) },
+ ]);
+ const wire = narrowWire(adapterWithCodecs.lower(plan('users', command)), 'insertMany');
+ expect(wire.documents).toEqual([{ name: 'ALICE' }, { name: 'BOB' }]);
+ });
+});
diff --git a/packages/3-mongo-target/2-mongo-adapter/test/resolve-value.test.ts b/packages/3-mongo-target/2-mongo-adapter/test/resolve-value.test.ts
new file mode 100644
index 0000000000..7dbd671dea
--- /dev/null
+++ b/packages/3-mongo-target/2-mongo-adapter/test/resolve-value.test.ts
@@ -0,0 +1,69 @@
+import { createMongoCodecRegistry, mongoCodec } from '@prisma-next/mongo-codec';
+import { MongoParamRef } from '@prisma-next/mongo-value';
+import { describe, expect, it } from 'vitest';
+import { resolveValue } from '../src/resolve-value';
+
+const uppercaseCodec = mongoCodec({
+ typeId: 'test/uppercase@1',
+ targetTypes: ['string'],
+ decode: (wire: string) => wire.toLowerCase(),
+ encode: (value: string) => value.toUpperCase(),
+});
+
+function testRegistry() {
+ const registry = createMongoCodecRegistry();
+ registry.register(uppercaseCodec);
+ return registry;
+}
+
+describe('resolveValue', () => {
+ it('unwraps MongoParamRef without codec registry', () => {
+ const ref = new MongoParamRef('hello');
+ expect(resolveValue(ref)).toBe('hello');
+ });
+
+ it('unwraps MongoParamRef without codecId even when registry is provided', () => {
+ const ref = new MongoParamRef('hello');
+ expect(resolveValue(ref, testRegistry())).toBe('hello');
+ });
+
+ it('applies codec encode when MongoParamRef has codecId and registry has codec', () => {
+ const ref = new MongoParamRef('hello', { codecId: 'test/uppercase@1' });
+ expect(resolveValue(ref, testRegistry())).toBe('HELLO');
+ });
+
+ it('falls back to raw value when codecId is set but registry is not provided', () => {
+ const ref = new MongoParamRef('hello', { codecId: 'test/uppercase@1' });
+ expect(resolveValue(ref)).toBe('hello');
+ });
+
+ it('falls back to raw value when codecId is not in registry', () => {
+ const ref = new MongoParamRef('hello', { codecId: 'test/unknown@1' });
+ expect(resolveValue(ref, testRegistry())).toBe('hello');
+ });
+
+ it('encodes nested MongoParamRef with codecId inside object', () => {
+ const doc = {
+ name: new MongoParamRef('alice'),
+ label: new MongoParamRef('greeting', { codecId: 'test/uppercase@1' }),
+ };
+ const result = resolveValue(doc, testRegistry()) as Record;
+ expect(result['name']).toBe('alice');
+ expect(result['label']).toBe('GREETING');
+ });
+
+ it('encodes MongoParamRef with codecId inside array', () => {
+ const arr = [new MongoParamRef('a', { codecId: 'test/uppercase@1' }), new MongoParamRef('b')];
+ const result = resolveValue(arr, testRegistry()) as unknown[];
+ expect(result[0]).toBe('A');
+ expect(result[1]).toBe('b');
+ });
+
+ it('preserves null, primitive, and Date values', () => {
+ expect(resolveValue(null)).toBeNull();
+ expect(resolveValue(42)).toBe(42);
+ expect(resolveValue('raw')).toBe('raw');
+ const d = new Date();
+ expect(resolveValue(d)).toBe(d);
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 72f5cd4364..2997df21cd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -63,7 +63,7 @@ importers:
version: 24.10.4
'@vitest/coverage-v8':
specifier: ^4.0.17
- version: 4.0.17(vitest@4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1))
+ version: 4.0.17(vitest@4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1))
dependency-cruiser:
specifier: ^16.3.3
version: 16.10.4
@@ -90,7 +90,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
examples/mongo-demo:
dependencies:
@@ -154,7 +154,7 @@ importers:
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react-swc':
specifier: ^4.2.3
- version: 4.2.3(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1))
+ version: 4.2.3(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1))
mongodb-memory-server:
specifier: ^10.4.0
version: 10.4.3
@@ -166,10 +166,10 @@ importers:
version: 5.9.3
vite:
specifier: 'catalog:'
- version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1)
+ version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
examples/prisma-next-demo:
dependencies:
@@ -281,7 +281,7 @@ importers:
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react-swc':
specifier: ^4.2.3
- version: 4.2.3(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1))
+ version: 4.2.3(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1))
jsdom:
specifier: ^28.1.0
version: 28.1.0(@noble/hashes@2.0.1)
@@ -293,10 +293,128 @@ importers:
version: 5.9.3
vite:
specifier: 'catalog:'
- version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1)
+ version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
+
+ examples/retail-store:
+ dependencies:
+ '@prisma-next/adapter-mongo':
+ specifier: workspace:*
+ version: link:../../packages/3-mongo-target/2-mongo-adapter
+ '@prisma-next/contract':
+ specifier: workspace:*
+ version: link:../../packages/1-framework/0-foundation/contract
+ '@prisma-next/driver-mongo':
+ specifier: workspace:*
+ version: link:../../packages/3-mongo-target/3-mongo-driver
+ '@prisma-next/mongo-contract':
+ specifier: workspace:*
+ version: link:../../packages/2-mongo-family/1-foundation/mongo-contract
+ '@prisma-next/mongo-orm':
+ specifier: workspace:*
+ version: link:../../packages/2-mongo-family/5-query-builders/orm
+ '@prisma-next/mongo-pipeline-builder':
+ specifier: workspace:*
+ version: link:../../packages/2-mongo-family/5-query-builders/pipeline-builder
+ '@prisma-next/mongo-query-ast':
+ specifier: workspace:*
+ version: link:../../packages/2-mongo-family/4-query/query-ast
+ '@prisma-next/mongo-runtime':
+ specifier: workspace:*
+ version: link:../../packages/2-mongo-family/7-runtime
+ '@prisma-next/mongo-value':
+ specifier: workspace:*
+ version: link:../../packages/2-mongo-family/1-foundation/mongo-value
+ '@radix-ui/react-dropdown-menu':
+ specifier: ^2.1.16
+ version: 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-label':
+ specifier: ^2.1.8
+ version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-radio-group':
+ specifier: ^1.3.8
+ version: 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-select':
+ specifier: ^2.2.6
+ version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-separator':
+ specifier: ^1.1.8
+ version: 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot':
+ specifier: ^1.2.4
+ version: 1.2.4(@types/react@19.2.14)(react@19.2.4)
+ '@tailwindcss/postcss':
+ specifier: ^4.2.2
+ version: 4.2.2
+ class-variance-authority:
+ specifier: ^0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ lucide-react:
+ specifier: ^1.8.0
+ version: 1.8.0(react@19.2.4)
+ mongodb:
+ specifier: 'catalog:'
+ version: 6.21.0
+ next:
+ specifier: ^15.3.1
+ version: 15.5.15(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ postcss:
+ specifier: ^8.5.9
+ version: 8.5.9
+ react:
+ specifier: ^19.2.4
+ version: 19.2.4
+ react-dom:
+ specifier: ^19.2.4
+ version: 19.2.4(react@19.2.4)
+ tailwind-merge:
+ specifier: ^3.5.0
+ version: 3.5.0
+ tailwindcss:
+ specifier: ^4.2.2
+ version: 4.2.2
+ devDependencies:
+ '@prisma-next/cli':
+ specifier: workspace:*
+ version: link:../../packages/1-framework/3-tooling/cli
+ '@prisma-next/family-mongo':
+ specifier: workspace:*
+ version: link:../../packages/2-mongo-family/9-family
+ '@prisma-next/mongo-contract-psl':
+ specifier: workspace:*
+ version: link:../../packages/2-mongo-family/2-authoring/contract-psl
+ '@prisma-next/test-utils':
+ specifier: workspace:*
+ version: link:../../test/utils
+ '@prisma-next/tsconfig':
+ specifier: workspace:*
+ version: link:../../packages/0-config/tsconfig
+ '@types/node':
+ specifier: 'catalog:'
+ version: 24.10.4
+ '@types/react':
+ specifier: ^19.2.14
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: ^19.2.3
+ version: 19.2.3(@types/react@19.2.14)
+ mongodb-memory-server:
+ specifier: ^10.4.0
+ version: 10.4.3
+ tsx:
+ specifier: ^4.19.2
+ version: 4.20.6
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+ vitest:
+ specifier: 'catalog:'
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/0-config/tsconfig: {}
@@ -338,7 +456,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/0-foundation/utils:
devDependencies:
@@ -356,7 +474,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/1-core/config:
dependencies:
@@ -387,7 +505,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/1-core/errors:
dependencies:
@@ -412,7 +530,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/1-core/framework-components:
dependencies:
@@ -440,7 +558,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/1-core/operations:
devDependencies:
@@ -461,7 +579,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/2-authoring/contract:
devDependencies:
@@ -479,7 +597,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/2-authoring/ids:
dependencies:
@@ -510,7 +628,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/2-authoring/psl-parser:
dependencies:
@@ -532,7 +650,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/2-authoring/psl-printer:
dependencies:
@@ -560,7 +678,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/3-tooling/cli:
dependencies:
@@ -660,7 +778,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/3-tooling/emitter:
dependencies:
@@ -703,7 +821,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/3-tooling/migration:
dependencies:
@@ -737,7 +855,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/3-tooling/vite-plugin-contract-emit:
dependencies:
@@ -762,10 +880,10 @@ importers:
version: 5.9.3
vite:
specifier: 'catalog:'
- version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1)
+ version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/1-framework/4-runtime/runtime-executor:
dependencies:
@@ -799,7 +917,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/1-foundation/mongo-codec:
dependencies:
@@ -827,7 +945,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/1-foundation/mongo-contract:
dependencies:
@@ -855,7 +973,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/1-foundation/mongo-value:
devDependencies:
@@ -873,7 +991,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/2-authoring/contract-psl:
dependencies:
@@ -910,7 +1028,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/2-authoring/contract-ts:
dependencies:
@@ -947,7 +1065,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/3-tooling/emitter:
dependencies:
@@ -981,7 +1099,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/3-tooling/mongo-schema-ir:
dependencies:
@@ -1006,7 +1124,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/4-query/query-ast:
dependencies:
@@ -1040,7 +1158,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/5-query-builders/orm:
dependencies:
@@ -1092,7 +1210,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/5-query-builders/pipeline-builder:
dependencies:
@@ -1123,7 +1241,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/6-transport/mongo-lowering:
dependencies:
@@ -1148,7 +1266,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/6-transport/mongo-wire:
dependencies:
@@ -1170,7 +1288,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/7-runtime:
dependencies:
@@ -1234,7 +1352,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-mongo-family/9-family:
dependencies:
@@ -1280,7 +1398,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/1-core/contract:
dependencies:
@@ -1311,7 +1429,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/1-core/errors:
devDependencies:
@@ -1332,7 +1450,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/1-core/operations:
dependencies:
@@ -1363,7 +1481,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/1-core/schema-ir:
dependencies:
@@ -1388,7 +1506,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/2-authoring/contract-psl:
dependencies:
@@ -1434,7 +1552,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/2-authoring/contract-ts:
dependencies:
@@ -1486,7 +1604,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/3-tooling/emitter:
dependencies:
@@ -1523,7 +1641,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/4-lanes/query-builder:
devDependencies:
@@ -1547,7 +1665,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/4-lanes/relational-core:
dependencies:
@@ -1596,7 +1714,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/4-lanes/sql-builder:
dependencies:
@@ -1669,7 +1787,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/5-runtime:
dependencies:
@@ -1727,7 +1845,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/2-sql/9-family:
dependencies:
@@ -1803,7 +1921,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-extensions/paradedb:
dependencies:
@@ -1828,7 +1946,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-extensions/pgvector:
dependencies:
@@ -1892,7 +2010,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-extensions/postgres:
dependencies:
@@ -1950,7 +2068,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-extensions/sql-orm-client:
dependencies:
@@ -2029,7 +2147,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-extensions/sqlite:
dependencies:
@@ -2081,7 +2199,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-mongo-target/1-mongo-target:
dependencies:
@@ -2103,7 +2221,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-mongo-target/2-mongo-adapter:
dependencies:
@@ -2170,7 +2288,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-mongo-target/3-mongo-driver:
dependencies:
@@ -2213,7 +2331,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-targets/3-targets/postgres:
dependencies:
@@ -2277,7 +2395,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-targets/3-targets/sqlite:
dependencies:
@@ -2317,7 +2435,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-targets/6-adapters/postgres:
dependencies:
@@ -2387,7 +2505,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-targets/6-adapters/sqlite:
dependencies:
@@ -2454,7 +2572,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-targets/7-drivers/postgres:
dependencies:
@@ -2518,7 +2636,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages/3-targets/7-drivers/sqlite:
dependencies:
@@ -2564,7 +2682,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
test/e2e/framework:
dependencies:
@@ -2652,7 +2770,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
test/integration:
dependencies:
@@ -2797,10 +2915,10 @@ importers:
version: 5.9.3
vite:
specifier: 7.3.1
- version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1)
+ version: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
test/integration/test/fixtures/cli/cli-e2e-test-app:
dependencies:
@@ -2923,7 +3041,7 @@ importers:
version: 5.9.3
vitest:
specifier: 'catalog:'
- version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ version: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
packages:
@@ -2933,6 +3051,10 @@ packages:
'@adobe/css-tools@4.4.4':
resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
+ '@alloc/quick-lru@5.2.0':
+ resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+ engines: {node: '>=10'}
+
'@ark/schema@0.56.0':
resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==}
@@ -3442,15 +3564,186 @@ packages:
'@noble/hashes':
optional: true
+ '@floating-ui/core@1.7.5':
+ resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
+
+ '@floating-ui/dom@1.7.6':
+ resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
+
+ '@floating-ui/react-dom@2.1.8':
+ resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/utils@0.2.11':
+ resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+
'@hono/node-server@1.19.7':
resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: 4.11.4
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.34.5':
+ resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linux-arm64@0.34.5':
+ resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-arm@0.34.5':
+ resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-ppc64@0.34.5':
+ resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-s390x@0.34.5':
+ resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-x64@0.34.5':
+ resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-wasm32@0.34.5':
+ resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-arm64@0.34.5':
+ resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.34.5':
+ resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.34.5':
+ resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [win32]
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
@@ -3471,6 +3764,61 @@ packages:
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
+ '@next/env@15.5.15':
+ resolution: {integrity: sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==}
+
+ '@next/swc-darwin-arm64@15.5.15':
+ resolution: {integrity: sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@next/swc-darwin-x64@15.5.15':
+ resolution: {integrity: sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@next/swc-linux-arm64-gnu@15.5.15':
+ resolution: {integrity: sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@next/swc-linux-arm64-musl@15.5.15':
+ resolution: {integrity: sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@next/swc-linux-x64-gnu@15.5.15':
+ resolution: {integrity: sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@next/swc-linux-x64-musl@15.5.15':
+ resolution: {integrity: sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@next/swc-win32-arm64-msvc@15.5.15':
+ resolution: {integrity: sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@next/swc-win32-x64-msvc@15.5.15':
+ resolution: {integrity: sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
'@noble/hashes@2.0.1':
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
engines: {node: '>= 20.19.0'}
@@ -3496,49 +3844,414 @@ packages:
'@quansync/fs@1.0.0':
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
- '@rolldown/binding-android-arm64@1.0.0-beta.57':
- resolution: {integrity: sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [android]
-
- '@rolldown/binding-android-arm64@1.0.0-beta.58':
- resolution: {integrity: sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [android]
+ '@radix-ui/number@1.1.1':
+ resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
- '@rolldown/binding-darwin-arm64@1.0.0-beta.57':
- resolution: {integrity: sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [darwin]
+ '@radix-ui/primitive@1.1.3':
+ resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
- '@rolldown/binding-darwin-arm64@1.0.0-beta.58':
- resolution: {integrity: sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm64]
- os: [darwin]
+ '@radix-ui/react-arrow@1.1.7':
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
- '@rolldown/binding-darwin-x64@1.0.0-beta.57':
- resolution: {integrity: sha512-6RsB8Qy4LnGqNGJJC/8uWeLWGOvbRL/KG5aJ8XXpSEupg/KQtlBEiFaYU/Ma5Usj1s+bt3ItkqZYAI50kSplBA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [darwin]
+ '@radix-ui/react-collection@1.1.7':
+ resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
- '@rolldown/binding-darwin-x64@1.0.0-beta.58':
- resolution: {integrity: sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [darwin]
+ '@radix-ui/react-compose-refs@1.1.2':
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
- '@rolldown/binding-freebsd-x64@1.0.0-beta.57':
- resolution: {integrity: sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [x64]
- os: [freebsd]
+ '@radix-ui/react-context@1.1.2':
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
- '@rolldown/binding-freebsd-x64@1.0.0-beta.58':
+ '@radix-ui/react-direction@1.1.1':
+ resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dismissable-layer@1.1.11':
+ resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-dropdown-menu@2.1.16':
+ resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-focus-guards@1.1.3':
+ resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-focus-scope@1.1.7':
+ resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-id@1.1.1':
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-label@2.1.8':
+ resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-menu@2.1.16':
+ resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popper@1.2.8':
+ resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-portal@1.1.9':
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-presence@1.1.5':
+ resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.3':
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.4':
+ resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-radio-group@1.3.8':
+ resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-roving-focus@1.1.11':
+ resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-select@2.2.6':
+ resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-separator@1.1.8':
+ resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.3':
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.4':
+ resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-callback-ref@1.1.1':
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-controllable-state@1.2.2':
+ resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-effect-event@0.0.2':
+ resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-escape-keydown@1.1.1':
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-layout-effect@1.1.1':
+ resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-previous@1.1.1':
+ resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-rect@1.1.1':
+ resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-size@1.1.1':
+ resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-visually-hidden@1.2.3':
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/rect@1.1.1':
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
+ '@rolldown/binding-android-arm64@1.0.0-beta.57':
+ resolution: {integrity: sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-android-arm64@1.0.0-beta.58':
+ resolution: {integrity: sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-beta.57':
+ resolution: {integrity: sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-beta.58':
+ resolution: {integrity: sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-beta.57':
+ resolution: {integrity: sha512-6RsB8Qy4LnGqNGJJC/8uWeLWGOvbRL/KG5aJ8XXpSEupg/KQtlBEiFaYU/Ma5Usj1s+bt3ItkqZYAI50kSplBA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-beta.58':
+ resolution: {integrity: sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-beta.57':
+ resolution: {integrity: sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-beta.58':
resolution: {integrity: sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
@@ -3884,43 +4597,138 @@ packages:
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+ '@swc/helpers@0.5.15':
+ resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+
'@swc/types@0.1.25':
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
- '@testing-library/dom@10.4.1':
- resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
- engines: {node: '>=18'}
+ '@tailwindcss/node@4.2.2':
+ resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==}
- '@testing-library/jest-dom@6.9.1':
- resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
- engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+ '@tailwindcss/oxide-android-arm64@4.2.2':
+ resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [android]
- '@testing-library/react@16.3.2':
- resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
- engines: {node: '>=18'}
- peerDependencies:
- '@testing-library/dom': ^10.0.0
- '@types/react': ^18.0.0 || ^19.0.0
- '@types/react-dom': ^18.0.0 || ^19.0.0
- react: ^18.0.0 || ^19.0.0
- react-dom: ^18.0.0 || ^19.0.0
- peerDependenciesMeta:
- '@types/react':
- optional: true
- '@types/react-dom':
- optional: true
+ '@tailwindcss/oxide-darwin-arm64@4.2.2':
+ resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [darwin]
- '@tybys/wasm-util@0.10.1':
- resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+ '@tailwindcss/oxide-darwin-x64@4.2.2':
+ resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [darwin]
- '@types/aria-query@5.0.4':
- resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+ '@tailwindcss/oxide-freebsd-x64@4.2.2':
+ resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [freebsd]
- '@types/chai@5.2.3':
- resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
+ resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm]
+ os: [linux]
- '@types/deep-eql@4.0.2':
- resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
+ resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
+ resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
+ resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.2':
+ resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.2':
+ resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
+ resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==}
+ engines: {node: '>= 20'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.2':
+ resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==}
+ engines: {node: '>= 20'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.2.2':
+ resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==}
+ engines: {node: '>= 20'}
+
+ '@tailwindcss/postcss@4.2.2':
+ resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==}
+
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/jest-dom@6.9.1':
+ resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+
+ '@testing-library/react@16.3.2':
+ resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@testing-library/dom': ^10.0.0
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@tybys/wasm-util@0.10.1':
+ resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
+ '@types/chai@5.2.3':
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -4054,6 +4862,10 @@ packages:
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@@ -4175,6 +4987,9 @@ packages:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
+ caniuse-lite@1.0.30001787:
+ resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==}
+
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
@@ -4193,6 +5008,9 @@ packages:
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
@@ -4201,9 +5019,16 @@ packages:
resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==}
engines: {node: '>=20'}
+ client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+
closest-match@1.3.3:
resolution: {integrity: sha512-RSdHrZwNOvt2uMQgqJDJdM/I+5MlJ1tQJEXYrbRjSMXWiCRo06g2hwObJ7+WKt2J9ySK9/pJ0Q2vbL+BPkofDA==}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -4292,6 +5117,13 @@ packages:
destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
discontinuous-range@1.0.0:
resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
@@ -4333,6 +5165,10 @@ packages:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
+ enhanced-resolve@5.20.1:
+ resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
+ engines: {node: '>=10.13.0'}
+
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -4457,6 +5293,10 @@ packages:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
+ get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+
get-port-please@3.2.0:
resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==}
@@ -4660,6 +5500,80 @@ packages:
resolution: {integrity: sha512-ksNxfzIW77OcZ+QWSAPC7yDqUSaIVwkTWnTPNiIy//vifNbwsSgQ57OkkncHxxpcBHM3LRfLAZVEh7kjq5twVA==}
engines: {node: '>=20.0.0'}
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -4692,6 +5606,11 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
+ lucide-react@1.8.0:
+ resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -4803,6 +5722,27 @@ packages:
resolution: {integrity: sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==}
engines: {node: '>=12.22.0'}
+ next@15.5.15:
+ resolution: {integrity: sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==}
+ engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ '@playwright/test': ^1.51.1
+ babel-plugin-react-compiler: '*'
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ '@playwright/test':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+ sass:
+ optional: true
+
node-fetch-native@1.6.7:
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
@@ -4964,10 +5904,18 @@ packages:
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
+ postcss@8.4.31:
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.5.9:
+ resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==}
+ engines: {node: ^10 || ^12 || >=14}
+
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
@@ -5025,6 +5973,36 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+ react-remove-scroll-bar@2.3.8:
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-remove-scroll@2.7.2:
+ resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-style-singleton@2.2.3:
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
@@ -5139,6 +6117,10 @@ packages:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
+ sharp@0.34.5:
+ resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -5219,6 +6201,19 @@ packages:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
+ styled-jsx@5.1.6:
+ resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
+ engines: {node: '>= 12.0.0'}
+ peerDependencies:
+ '@babel/core': '*'
+ babel-plugin-macros: '*'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ babel-plugin-macros:
+ optional: true
+
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -5230,6 +6225,12 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+ tailwind-merge@3.5.0:
+ resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
+
+ tailwindcss@4.2.2:
+ resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==}
+
tapable@2.3.0:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
@@ -5399,6 +6400,26 @@ packages:
synckit:
optional: true
+ use-callback-ref@1.3.3:
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
valibot@1.2.0:
resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
peerDependencies:
@@ -5560,6 +6581,8 @@ snapshots:
'@adobe/css-tools@4.4.4': {}
+ '@alloc/quick-lru@5.2.0': {}
+
'@ark/schema@0.56.0':
dependencies:
'@ark/util': 0.56.0
@@ -5888,83 +6911,566 @@ snapshots:
'@esbuild/win32-x64@0.27.2':
optional: true
- '@exodus/bytes@1.15.0(@noble/hashes@2.0.1)':
+ '@exodus/bytes@1.15.0(@noble/hashes@2.0.1)':
+ optionalDependencies:
+ '@noble/hashes': 2.0.1
+
+ '@floating-ui/core@1.7.5':
+ dependencies:
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/dom@1.7.6':
+ dependencies:
+ '@floating-ui/core': 1.7.5
+ '@floating-ui/utils': 0.2.11
+
+ '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@floating-ui/dom': 1.7.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
+ '@floating-ui/utils@0.2.11': {}
+
+ '@hono/node-server@1.19.7(hono@4.11.4)':
+ dependencies:
+ hono: 4.11.4
+
+ '@img/colour@1.1.0':
+ optional: true
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-darwin-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-arm@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-ppc64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-s390x@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-wasm32@0.34.5':
+ dependencies:
+ '@emnapi/runtime': 1.8.1
+ optional: true
+
+ '@img/sharp-win32-arm64@0.34.5':
+ optional: true
+
+ '@img/sharp-win32-ia32@0.34.5':
+ optional: true
+
+ '@img/sharp-win32-x64@0.34.5':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@mongodb-js/saslprep@1.4.6':
+ dependencies:
+ sparse-bitfield: 3.0.3
+
+ '@mrleebo/prisma-ast@0.13.1':
+ dependencies:
+ chevrotain: 10.5.0
+ lilconfig: 2.1.0
+
+ '@napi-rs/wasm-runtime@1.1.1':
+ dependencies:
+ '@emnapi/core': 1.8.1
+ '@emnapi/runtime': 1.8.1
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
+ '@next/env@15.5.15': {}
+
+ '@next/swc-darwin-arm64@15.5.15':
+ optional: true
+
+ '@next/swc-darwin-x64@15.5.15':
+ optional: true
+
+ '@next/swc-linux-arm64-gnu@15.5.15':
+ optional: true
+
+ '@next/swc-linux-arm64-musl@15.5.15':
+ optional: true
+
+ '@next/swc-linux-x64-gnu@15.5.15':
+ optional: true
+
+ '@next/swc-linux-x64-musl@15.5.15':
+ optional: true
+
+ '@next/swc-win32-arm64-msvc@15.5.15':
+ optional: true
+
+ '@next/swc-win32-x64-msvc@15.5.15':
+ optional: true
+
+ '@noble/hashes@2.0.1': {}
+
+ '@oxc-project/types@0.103.0': {}
+
+ '@oxc-project/types@0.106.0': {}
+
+ '@prisma/debug@7.1.0': {}
+
+ '@prisma/dev@0.19.1(typescript@5.9.3)':
+ dependencies:
+ '@electric-sql/pglite': 0.3.14
+ '@electric-sql/pglite-socket': 0.0.19(@electric-sql/pglite@0.3.14)
+ '@electric-sql/pglite-tools': 0.2.19(@electric-sql/pglite@0.3.14)
+ '@hono/node-server': 1.19.7(hono@4.11.4)
+ '@mrleebo/prisma-ast': 0.13.1
+ '@prisma/get-platform': 7.1.0
+ '@prisma/query-plan-executor': 7.1.0
+ foreground-child: 3.3.1
+ get-port-please: 3.2.0
+ hono: 4.11.4
+ http-status-codes: 2.3.0
+ pathe: 2.0.3
+ proper-lockfile: 4.1.2
+ remeda: 2.32.0
+ std-env: 3.10.0
+ valibot: 1.2.0(typescript@5.9.3)
+ zeptomatch: 2.1.0
+ transitivePeerDependencies:
+ - typescript
+
+ '@prisma/get-platform@7.1.0':
+ dependencies:
+ '@prisma/debug': 7.1.0
+
+ '@prisma/query-plan-executor@7.1.0': {}
+
+ '@quansync/fs@1.0.0':
+ dependencies:
+ quansync: 1.0.0
+
+ '@radix-ui/number@1.1.1': {}
+
+ '@radix-ui/primitive@1.1.3': {}
+
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/rect': 1.1.1
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
- '@noble/hashes': 2.0.1
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@hono/node-server@1.19.7(hono@4.11.4)':
+ '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- hono: 4.11.4
+ '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@jridgewell/gen-mapping@0.3.13':
- dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
- '@jridgewell/trace-mapping': 0.3.31
+ '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@jridgewell/resolve-uri@3.1.2': {}
+ '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@jridgewell/sourcemap-codec@1.5.5': {}
+ '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@radix-ui/number': 1.1.1
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ aria-hidden: 1.2.6
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@jridgewell/trace-mapping@0.3.31':
+ '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- '@jridgewell/resolve-uri': 3.1.2
- '@jridgewell/sourcemap-codec': 1.5.5
+ '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
- '@mongodb-js/saslprep@1.4.6':
+ '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- sparse-bitfield: 3.0.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@mrleebo/prisma-ast@0.13.1':
+ '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- chevrotain: 10.5.0
- lilconfig: 2.1.0
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@napi-rs/wasm-runtime@1.1.1':
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- '@emnapi/core': 1.8.1
- '@emnapi/runtime': 1.8.1
- '@tybys/wasm-util': 0.10.1
- optional: true
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@noble/hashes@2.0.1': {}
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@oxc-project/types@0.103.0': {}
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@oxc-project/types@0.106.0': {}
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@prisma/debug@7.1.0': {}
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@prisma/dev@0.19.1(typescript@5.9.3)':
+ '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- '@electric-sql/pglite': 0.3.14
- '@electric-sql/pglite-socket': 0.0.19(@electric-sql/pglite@0.3.14)
- '@electric-sql/pglite-tools': 0.2.19(@electric-sql/pglite@0.3.14)
- '@hono/node-server': 1.19.7(hono@4.11.4)
- '@mrleebo/prisma-ast': 0.13.1
- '@prisma/get-platform': 7.1.0
- '@prisma/query-plan-executor': 7.1.0
- foreground-child: 3.3.1
- get-port-please: 3.2.0
- hono: 4.11.4
- http-status-codes: 2.3.0
- pathe: 2.0.3
- proper-lockfile: 4.1.2
- remeda: 2.32.0
- std-env: 3.10.0
- valibot: 1.2.0(typescript@5.9.3)
- zeptomatch: 2.1.0
- transitivePeerDependencies:
- - typescript
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@prisma/get-platform@7.1.0':
+ '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)':
dependencies:
- '@prisma/debug': 7.1.0
+ '@radix-ui/rect': 1.1.1
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@prisma/query-plan-executor@7.1.0': {}
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4)
+ react: 19.2.4
+ optionalDependencies:
+ '@types/react': 19.2.14
- '@quansync/fs@1.0.0':
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- quansync: 1.0.0
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+ '@types/react-dom': 19.2.3(@types/react@19.2.14)
+
+ '@radix-ui/rect@1.1.1': {}
'@rolldown/binding-android-arm64@1.0.0-beta.57':
optional: true
@@ -6179,10 +7685,83 @@ snapshots:
'@swc/counter@0.1.3': {}
+ '@swc/helpers@0.5.15':
+ dependencies:
+ tslib: 2.8.1
+
'@swc/types@0.1.25':
dependencies:
'@swc/counter': 0.1.3
+ '@tailwindcss/node@4.2.2':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.20.1
+ jiti: 2.6.1
+ lightningcss: 1.32.0
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.2.2
+
+ '@tailwindcss/oxide-android-arm64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.2.2':
+ optional: true
+
+ '@tailwindcss/oxide@4.2.2':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.2.2
+ '@tailwindcss/oxide-darwin-arm64': 4.2.2
+ '@tailwindcss/oxide-darwin-x64': 4.2.2
+ '@tailwindcss/oxide-freebsd-x64': 4.2.2
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2
+ '@tailwindcss/oxide-linux-arm64-musl': 4.2.2
+ '@tailwindcss/oxide-linux-x64-gnu': 4.2.2
+ '@tailwindcss/oxide-linux-x64-musl': 4.2.2
+ '@tailwindcss/oxide-wasm32-wasi': 4.2.2
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2
+ '@tailwindcss/oxide-win32-x64-msvc': 4.2.2
+
+ '@tailwindcss/postcss@4.2.2':
+ dependencies:
+ '@alloc/quick-lru': 5.2.0
+ '@tailwindcss/node': 4.2.2
+ '@tailwindcss/oxide': 4.2.2
+ postcss: 8.5.9
+ tailwindcss: 4.2.2
+
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.29.0
@@ -6258,15 +7837,15 @@ snapshots:
dependencies:
'@types/webidl-conversions': 7.0.3
- '@vitejs/plugin-react-swc@4.2.3(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1))':
+ '@vitejs/plugin-react-swc@4.2.3(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.2
'@swc/core': 1.15.18
- vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1)
+ vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
transitivePeerDependencies:
- '@swc/helpers'
- '@vitest/coverage-v8@4.0.17(vitest@4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1))':
+ '@vitest/coverage-v8@4.0.17(vitest@4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.17
@@ -6278,7 +7857,7 @@ snapshots:
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
- vitest: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1)
+ vitest: 4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
'@vitest/expect@4.0.17':
dependencies:
@@ -6289,13 +7868,13 @@ snapshots:
chai: 6.2.2
tinyrainbow: 3.0.3
- '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1))':
+ '@vitest/mocker@4.0.17(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 4.0.17
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1)
+ vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
'@vitest/pretty-format@4.0.17':
dependencies:
@@ -6373,6 +7952,10 @@ snapshots:
dependencies:
sprintf-js: 1.0.3
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
@@ -6492,6 +8075,8 @@ snapshots:
camelcase@6.3.0: {}
+ caniuse-lite@1.0.30001787: {}
+
chai@6.2.2: {}
chalk@4.1.2:
@@ -6516,6 +8101,10 @@ snapshots:
dependencies:
consola: 3.4.2
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
@@ -6525,8 +8114,12 @@ snapshots:
slice-ansi: 7.1.2
string-width: 8.1.0
+ client-only@0.0.1: {}
+
closest-match@1.3.3: {}
+ clsx@2.1.1: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -6621,6 +8214,10 @@ snapshots:
destr@2.0.5: {}
+ detect-libc@2.1.2: {}
+
+ detect-node-es@1.1.0: {}
+
discontinuous-range@1.0.0: {}
dom-accessibility-api@0.5.16: {}
@@ -6648,6 +8245,11 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
+ enhanced-resolve@5.20.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
+
entities@6.0.1: {}
environment@1.1.0: {}
@@ -6800,6 +8402,8 @@ snapshots:
hasown: 2.0.2
math-intrinsics: 1.1.0
+ get-nonce@1.0.1: {}
+
get-port-please@3.2.0: {}
get-proto@1.0.1:
@@ -6992,6 +8596,55 @@ snapshots:
kysely@0.28.10:
optional: true
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
lilconfig@2.1.0: {}
lint-staged@16.2.6:
@@ -7033,6 +8686,10 @@ snapshots:
dependencies:
yallist: 4.0.0
+ lucide-react@1.8.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
lz-string@1.5.0: {}
magic-string@0.30.21:
@@ -7152,6 +8809,29 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ next@15.5.15(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ '@next/env': 15.5.15
+ '@swc/helpers': 0.5.15
+ caniuse-lite: 1.0.30001787
+ postcss: 8.4.31
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ styled-jsx: 5.1.6(react@19.2.4)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 15.5.15
+ '@next/swc-darwin-x64': 15.5.15
+ '@next/swc-linux-arm64-gnu': 15.5.15
+ '@next/swc-linux-arm64-musl': 15.5.15
+ '@next/swc-linux-x64-gnu': 15.5.15
+ '@next/swc-linux-x64-musl': 15.5.15
+ '@next/swc-win32-arm64-msvc': 15.5.15
+ '@next/swc-win32-x64-msvc': 15.5.15
+ sharp: 0.34.5
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+
node-fetch-native@1.6.7: {}
nypm@0.6.2:
@@ -7274,12 +8954,24 @@ snapshots:
exsolve: 1.0.8
pathe: 2.0.3
+ postcss@8.4.31:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
+ postcss@8.5.9:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
postgres-array@2.0.0: {}
postgres-bytea@1.0.0: {}
@@ -7332,6 +9024,33 @@ snapshots:
react-is@17.0.2: {}
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4)
+ react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4)
+ use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4)
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
react@19.2.4: {}
readdirp@4.1.2: {}
@@ -7487,6 +9206,38 @@ snapshots:
gopd: 1.2.0
has-property-descriptors: 1.0.2
+ sharp@0.34.5:
+ dependencies:
+ '@img/colour': 1.1.0
+ detect-libc: 2.1.2
+ semver: 7.7.3
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.34.5
+ '@img/sharp-darwin-x64': 0.34.5
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ '@img/sharp-linux-arm': 0.34.5
+ '@img/sharp-linux-arm64': 0.34.5
+ '@img/sharp-linux-ppc64': 0.34.5
+ '@img/sharp-linux-riscv64': 0.34.5
+ '@img/sharp-linux-s390x': 0.34.5
+ '@img/sharp-linux-x64': 0.34.5
+ '@img/sharp-linuxmusl-arm64': 0.34.5
+ '@img/sharp-linuxmusl-x64': 0.34.5
+ '@img/sharp-wasm32': 0.34.5
+ '@img/sharp-win32-arm64': 0.34.5
+ '@img/sharp-win32-ia32': 0.34.5
+ '@img/sharp-win32-x64': 0.34.5
+ optional: true
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -7559,6 +9310,11 @@ snapshots:
dependencies:
min-indent: 1.0.1
+ styled-jsx@5.1.6(react@19.2.4):
+ dependencies:
+ client-only: 0.0.1
+ react: 19.2.4
+
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -7567,6 +9323,10 @@ snapshots:
symbol-tree@3.2.4: {}
+ tailwind-merge@3.5.0: {}
+
+ tailwindcss@4.2.2: {}
+
tapable@2.3.0: {}
tar-stream@3.1.8:
@@ -7729,11 +9489,26 @@ snapshots:
dependencies:
rolldown: 1.0.0-beta.58
+ use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
+ use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.2.4
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.14
+
valibot@1.2.0(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
- vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1):
+ vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1):
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
@@ -7745,13 +9520,14 @@ snapshots:
'@types/node': 24.10.4
fsevents: 2.3.3
jiti: 2.6.1
+ lightningcss: 1.32.0
tsx: 4.20.6
yaml: 2.8.1
- vitest@4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(tsx@4.20.6)(yaml@2.8.1):
+ vitest@4.0.17(@types/node@24.10.4)(jiti@2.6.1)(jsdom@28.1.0(@noble/hashes@2.0.1))(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1):
dependencies:
'@vitest/expect': 4.0.17
- '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1))
+ '@vitest/mocker': 4.0.17(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1))
'@vitest/pretty-format': 4.0.17
'@vitest/runner': 4.0.17
'@vitest/snapshot': 4.0.17
@@ -7768,7 +9544,7 @@ snapshots:
tinyexec: 1.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
- vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.1)
+ vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.20.6)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.10.4
diff --git a/projects/mongo-example-apps/plans/predictive-maintenance-plan.md b/projects/mongo-example-apps/plans/predictive-maintenance-plan.md
new file mode 100644
index 0000000000..b9f0c109a2
--- /dev/null
+++ b/projects/mongo-example-apps/plans/predictive-maintenance-plan.md
@@ -0,0 +1,176 @@
+# Predictive Maintenance Port
+
+## Summary
+
+Port the [Leafy-Predictive-Maintenance](https://github.com/mongodb-industry-solutions/Leafy-Predictive-Maintenance) manufacturing/IoT application from raw MongoDB driver calls to Prisma Next's MongoDB support. This app stress-tests vector search as a core feature (repair manual RAG, equipment criticality analysis), handles sensor time-series data, and stores ML model binary artifacts. Success means the entire Node.js data access layer uses the PN ORM, vector searches go through the PN extension pack, and schema migrations create the correct vector search indexes.
+
+**Spec:** `projects/mongo-example-apps/spec.md`
+
+## Collaborators
+
+| Role | Person/Team | Context |
+|---|---|---|
+| Maker | Agent / Engineer | Drives execution |
+| Reviewer | Will | Architectural review, framework gap triage |
+| Collaborator | WS4 (Mongo) team | Framework features this plan depends on |
+
+## Source Repo Analysis
+
+Leafy-Predictive-Maintenance is a **Next.js Pages Router** JavaScript app with a companion **Express + Socket.IO** alerts server and a **Python** inference module (out of scope). Uses the **native `mongodb` driver** (no Mongoose). Key characteristics:
+
+- **6 collections**: `raw_data`, `transformed_data`, `machine_failures`, `ml_models`, `repair_manuals`, `maintenance_history`
+- **Embedded documents**: `source: { filename, page_number }` on RAG chunks; `data: { ... }` nested object on `transformed_data`
+- **No cross-collection references via ObjectId**: logical links via `machineID` (string) and `sessionID`
+- **No application-defined indexes**: vector search indexes are documented in README for manual Atlas creation (not in code)
+- **Vector search is the core feature**: `$vectorSearch` aggregation on `repair_manuals` (1024-dim Cohere / 1536-dim OpenAI embeddings) and `maintenance_history` (with `source.filename` pre-filter)
+- **Stream processing**: Atlas Stream Processing transforms `raw_data` → `transformed_data`; Python watches `transformed_data` change stream for inference
+- **Update operators**: only `$set` for acknowledging alerts on `machine_failures`
+- **ML model storage**: `ml_models` stores compressed pickle binaries in `model_ckpt` field (BinData)
+- **Dual embedding fields**: `embeddings` (Cohere) and `embeddings_openai` (OpenAI) on the same documents
+
+## Milestones
+
+### Milestone 1: Project scaffold and contract authoring
+
+Set up the example app as a workspace package and author the contract in both PSL and TypeScript DSL.
+
+**Validates:** dual authoring surface parity, Mongo contract support for vector embedding fields, embedded documents, BSON binary types.
+
+**Tasks:**
+
+- [ ] Create `examples/predictive-maintenance/` with `package.json` (workspace deps on PN packages), `tsconfig.json`, `biome.jsonc`, `vitest.config.ts` — following the `mongo-demo` example structure
+- [ ] Clone/download the Leafy source and analyze the full data model: document every collection's fields, embedded structures, and index requirements
+- [ ] Author the PSL contract (`schema.psl`) covering all collections:
+ - `raw_data`: flat sensor readings (`Product ID`, `Type`, temperatures, speeds, `Tool wear`, `Session ID`)
+ - `transformed_data`: `sessionID`, `machineID`, embedded `data` object with typed sensor fields
+ - `machine_failures`: `machineID`, `failure` (string), `ts` (DateTime), `isAcknowledged` (boolean), `repairSteps` (string)
+ - `ml_models`: `tag` (string), `model_ckpt` (Binary/BinData)
+ - `repair_manuals`: `text_chunk`, embedded `source: { filename, page_number }`, `embeddings` (vector array), `embeddings_openai` (vector array)
+ - `maintenance_history`: same shape as `repair_manuals`
+- [ ] Author the TypeScript DSL contract producing equivalent `contract.json`
+- [ ] Write a parity test: emit from both surfaces, assert structural equivalence
+- [ ] Generate `contract.json` and `contract.d.ts` via the emitter; commit artifacts
+
+### Milestone 2: Schema migrations
+
+The contract produces schema migrations that create MongoDB collections with vector search indexes (multiple dimensions) and filtered indexes.
+
+**Validates:** Mongo schema migration generation for vector search indexes, multi-dimensional embedding support, filtered indexes, migration runner.
+
+**Blocks on:** PN Mongo schema migration support for vector search indexes.
+
+**Tasks:**
+
+- [ ] Generate schema migrations from the contract
+- [ ] Verify migration creates a vector search index on `repair_manuals` for `embeddings` field (1024 dimensions, euclidean similarity)
+- [ ] Verify migration creates a vector search index on `repair_manuals` for `embeddings_openai` field (1536 dimensions) — or a single index covering both paths
+- [ ] Verify migration creates a filtered vector search index on `maintenance_history` with `source.filename` as a filter field
+- [ ] Write integration test: apply migrations against `mongodb-memory-server`, assert collections and vector indexes exist (note: Atlas Vector Search indexes may require Atlas — test creation API, not search functionality)
+- [ ] Write integration test: apply migrations idempotently
+
+### Milestone 3: Core ORM — CRUD operations
+
+Replace raw MongoDB driver calls for basic CRUD with PN ORM queries.
+
+**Validates:** `create`, `findMany`, `findFirst`, `update`, `delete` via ORM; embedded document inlining; BSON binary type handling; type safety.
+
+**Tasks:**
+
+- [ ] Create the PN database client (`db.ts`) following the `mongo-demo` pattern
+- [ ] Port `raw_data` writes: `create` for sensor data ingestion (high-volume insert pattern from machine simulator)
+- [ ] Port `transformed_data` reads: `findFirst` with sort by `_id` descending (latest reading), `findMany` by `sessionID`/`machineID`
+- [ ] Port `machine_failures` operations:
+ - `create` for inserting failure predictions
+ - `findMany` for listing active failures
+ - `update` with `$set` for acknowledging alerts (`isAcknowledged: true`, `repairSteps: "..."`)
+ - `deleteMany` for clearing failures before simulator run
+- [ ] Port `ml_models` reads: `findFirst` by `tag` field (e.g. `"RootCauseClassifier"`) — verify BSON binary field (`model_ckpt`) is handled correctly by the codec
+- [ ] Port `repair_manuals` reads: `findMany` for listing chunks (projecting out embedding vectors for display)
+- [ ] Port `maintenance_history` reads: `findMany` for listing chunks
+- [ ] Write integration tests for each collection's CRUD operations
+- [ ] Verify embedded `source: { filename, page_number }` appears inline in query results
+- [ ] Verify all query results are fully typed — add negative type tests
+
+### Milestone 4: Vector search — repair manual RAG
+
+Port the core RAG query for repair plan generation to use the PN vector search extension pack.
+
+**Validates:** PN vector search as a primary query mechanism (not a bolt-on); multi-provider embedding support; real-world RAG pipeline.
+
+**Blocks on:** PN Mongo vector search extension pack implementation.
+
+**Tasks:**
+
+- [ ] Add the PN vector search extension pack dependency
+- [ ] Port repair plan search: vector search on `repair_manuals` by `embeddings` field (Cohere, 1024-dim), returning top 10 results with 150 candidates
+- [ ] Port repair plan search (OpenAI variant): vector search on `repair_manuals` by `embeddings_openai` field (1536-dim), controlled by config/env
+- [ ] Verify the PN API supports switching the vector field path based on the AI provider
+- [ ] Write integration test: seed `repair_manuals` with embedding vectors, run vector search, assert results returned in relevance order (requires Atlas or test double)
+
+### Milestone 5: Vector search — criticality analysis with pre-filter
+
+Port the criticality analysis RAG query, which adds a pre-filter on `source.filename` to the vector search.
+
+**Validates:** filtered vector search (pre-filter on non-vector fields), compound vector + filter index usage.
+
+**Blocks on:** PN Mongo vector search extension pack with pre-filter support.
+
+**Tasks:**
+
+- [ ] Port criticality analysis search: vector search on `maintenance_history` with `filter: { "source.filename": { $in: selectedDocuments } }`
+- [ ] Verify the PN vector search API supports pre-filters alongside vector queries
+- [ ] Write integration test: seed `maintenance_history` with multiple source filenames, run filtered vector search, assert only matching source documents are returned
+- [ ] Verify the filtered vector search uses the correct index (the one with `source.filename` as a filter field)
+
+### Milestone 6: Sensor data and time-series patterns
+
+Validate that the PN runtime handles the sensor data ingestion and query patterns — high-volume inserts and time-ordered queries.
+
+**Validates:** PN performance with high-volume inserts; sort-by-insertion-order queries; bulk operations.
+
+**Tasks:**
+
+- [ ] Port machine simulator data path: bulk `create` operations for `raw_data` documents (simulating ~2s interval sensor readings)
+- [ ] Port "latest reading" query: `findFirst` on `transformed_data` sorted by `_id` descending
+- [ ] Port change stream watch on `transformed_data`: subscribe to new inserts for a given `sessionID` (this is the Python inference trigger path — port the subscription, not the Python inference)
+- [ ] Write integration test: insert a batch of sensor readings, query latest, assert correct document returned
+- [ ] Write integration test: subscribe to `transformed_data` changes, insert documents, assert change events received
+
+### Milestone 7: Close-out
+
+Verify all acceptance criteria, document gaps found, and clean up.
+
+**Tasks:**
+
+- [ ] Run full test suite against `mongodb-memory-server` — all tests pass
+- [ ] Run typecheck — no errors
+- [ ] Verify all acceptance criteria from the spec are met (checklist below)
+- [ ] Document any framework gaps discovered during the port
+- [ ] Write a brief README for `examples/predictive-maintenance/` explaining how to run the example
+
+## Test Coverage
+
+| Acceptance Criterion | Test Type | Milestone | Notes |
+|---|---|---|---|
+| Contract authored in both PSL and TS DSL | Integration (parity) | M1 | Assert equivalent `contract.json` output |
+| Both surfaces produce equivalent `contract.json` | Integration (parity) | M1 | Structural comparison test |
+| Contract emits valid `contract.json` and `contract.d.ts` | Integration | M1 | Emitter success + typecheck |
+| Schema migrations create correct collections and vector indexes | Integration | M2 | Apply against mongodb-memory-server |
+| Migration runner applies against real MongoDB | Integration | M2 | mongodb-memory-server test |
+| All CRUD operations use PN ORM | Integration | M3 | Per-collection CRUD tests |
+| Embedded documents inline in results | Integration | M3 | Assert `source` nested fields present |
+| Query results fully typed | Compile-time | M3 | Negative type tests + typecheck |
+| Vector search via PN extension pack (repair manuals) | Integration | M4 | Requires Atlas or test double |
+| Vector search via PN extension pack (criticality with pre-filter) | Integration | M5 | Requires Atlas or test double |
+| Time-series insert and query patterns work | Integration | M6 | Bulk insert + latest query |
+| Runs against mongodb-memory-server in CI | CI | M7 | Full suite green |
+| Demonstrates 3+ distinct MongoDB idioms | Manual | M7 | Checklist: embedded docs, vector search (core), update ops, time-series, change streams |
+
+## Open Items
+
+- **BSON Binary type support**: `ml_models` stores compressed pickle data as `BinData`. The PN Mongo codec registry needs a `binary` codec. If not available, this blocks M3.
+- **Dual embedding fields**: The same documents have both `embeddings` (Cohere, 1024-dim) and `embeddings_openai` (OpenAI, 1536-dim). The contract needs to model both fields, and the vector search API needs to support selecting which field to search at query time.
+- **Atlas Vector Search in CI**: Vector search indexes are an Atlas-only feature. Integration tests for M4 and M5 may need to be marked as Atlas-optional, with the core CRUD tests running against `mongodb-memory-server`.
+- **Stream Processing**: Atlas Stream Processing transforms `raw_data` → `transformed_data`. This is a server-side pipeline, not application code. The port doesn't replicate it — instead, the port validates that the PN ORM can read/write both collections and subscribe to changes on `transformed_data`.
+- **Python inference module**: Out of scope per spec. The Node.js data access layer is ported; the Python ML pipeline remains as-is or is documented as a future port.
+- **Field naming conventions**: Source uses string keys with spaces (`"Product ID"`, `"Air temperature [K]"`). These may need to be mapped to valid identifiers in the contract via the `storage.fields` mapping.
diff --git a/projects/mongo-example-apps/plans/retail-store-plan.md b/projects/mongo-example-apps/plans/retail-store-plan.md
new file mode 100644
index 0000000000..fc82079cc4
--- /dev/null
+++ b/projects/mongo-example-apps/plans/retail-store-plan.md
@@ -0,0 +1,582 @@
+# Retail Store Example App
+
+## Summary
+
+Build a contract-first e-commerce data access layer using Prisma Next's MongoDB support, inspired by the [retail-store-v2](https://github.com/mongodb-industry-solutions/retail-store-v2) domain model. This is **not a literal port** — it's an equivalent application designed from the contract outward, the way a PN user would build it. The domain model (products, carts, orders, users, locations) stays the same; the data access layer is idiomatic PN.
+
+The goal is to **prove real-world usage of Prisma Next** against a realistic e-commerce domain — embedded documents, referenced relations, update operators, vector search, change streams, aggregation, and schema migrations — all with full type safety. This is not a migration/upgrade-path validation (substituting PN for raw Mongo calls or Prisma ORM at existing call sites). We're building a greenfield PN application that happens to share a domain model with an existing MongoDB app.
+
+**Spec:** `projects/mongo-example-apps/spec.md`
+
+## Collaborators
+
+
+| Role | Person/Team | Context |
+| ------------ | ---------------- | ------------------------------------------ |
+| Maker | Agent / Engineer | Drives execution |
+| Reviewer | Will | Architectural review, framework gap triage |
+| Collaborator | WS4 (Mongo) team | Framework features this plan depends on |
+
+
+---
+
+## What we're building
+
+A working e-commerce application, living at `examples/retail-store/`. It consists of:
+
+1. **A PN contract** defining the domain model (PSL → `contract.json` + `contract.d.ts`) with embedded value objects via the PSL `type` keyword
+2. **A Next.js App Router application** with a UI and API routes — a representative e-commerce experience (browse products, manage cart, place orders, view order history)
+3. **A typed data access layer** — the API routes use the PN ORM, pipeline builder, and runtime for all database operations
+4. **Schema migrations** generated from the contract via `prisma-next migration plan` and applied via `prisma-next migration apply`
+5. **Integration tests** proving each PN capability against `mongodb-memory-server`
+6. **Seed data** for demonstrations and tests
+
+The app should function broadly like the original — a user can browse products, add them to a cart, check out, and view orders. The API routes don't need to be identical to the original, but they should be a representative sample of real-world usage patterns. The UI can be simplified.
+
+### What we're NOT building
+
+- **The chatbot integration** — validates Dataworkz, not PN
+- **Generic CRUD helpers** — the original's `findDocuments`/`insertDocument`/`updateDocument` are the anti-pattern PN replaces
+
+---
+
+## Base branch
+
+**`tml-2220-m1-family-migration-spi-vertical-slice-single-index-e2e`** (merging to main soon).
+
+This branch provides a comprehensive Mongo framework stack including: PSL interpreter with `type` keyword for value objects, ORM CRUD (`create`/`update`/`delete`/`upsert`), polymorphism (`@@discriminator`/`@@base`/`.variant()`), pipeline builder (`@prisma-next/mongo-pipeline-builder`), schema migration planner + runner, and the updated `mongo-demo` example as a template.
+
+### Previous work
+
+An initial implementation of M1–M2 was built on `tml-2185-port-retail-store-v2-e-commerce-app-to-prisma-next-mongodb` (branched from the earlier `tml-2187-mongo-psl-interpreter-3-11`). That implementation used flattened scalar fields as a stopgap for embedded documents, lacked ORM writes, and followed an older `db.ts` pattern. This plan supersedes it — **start fresh from the new base branch**.
+
+---
+
+## Source Repo Analysis
+
+The retail-store-v2 is a **Next.js App Router** JavaScript app using the **native `mongodb` driver** (no Mongoose). Key characteristics:
+
+- **10 collections**: `products`, `carts`, `orders`, `users`, `locations`, `invoices`, `sessions`, `events_ingest`, `session_signals`, `next_best_actions`
+- **Embedded documents**: cart `products[]` (with nested `price`, `image`), order `products[]` and `status_history[]`, event `tags` and `metadata`, user `lastRecommendations[]`
+- **Referenced relations**: `carts.user` → `users._id`, `orders.user` → `users._id` (no `$lookup` in original — uses denormalization)
+- **Indexes**: `carts` has `user_1` unique index; Atlas Search text index on products; vector embeddings on products (`vai_text_embedding`)
+- **Update operators**: `$push` (order status, cart items), `$pull` (cart item removal), `$set` (clear cart, embeddings, NBA redeemed), `$setOnInsert` (cart upsert)
+- **Change streams**: DB-level `watch()` with `$match` filter, SSE bridge to client
+- **Aggregation**: `$search` (text), `$sample`, generic aggregate endpoint, analytics pipelines with `$group`/`$unwind`/`$sort`
+- **No `$lookup`**: original app uses denormalization everywhere — our app will add `$lookup` via PN `include()` where appropriate
+
+---
+
+## Domain Model
+
+### Overview
+
+The original retail-store-v2 has 10 collections. We use the same core e-commerce domain but design the contract to showcase PN's strengths. We keep 7 collections that exercise distinct MongoDB patterns:
+
+
+| Collection | Purpose | Key PN patterns |
+| ----------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------- |
+| `products` | Product catalog | Embedded value objects (price, image), vector embedding field, text search index |
+| `users` | Customer accounts | Embedded value objects (address), referenced by carts and orders |
+| `carts` | Shopping carts | Embedded cart items (value objects), reference to user (1:1), update operators (`$push`, `$pull`, `$set`), upsert |
+| `orders` | Customer orders | Embedded line items and status history (value objects), reference to user (N:1), update operators (`$push`) |
+| `locations` | Store locations | Simple read-only collection |
+| `invoices` | Digital receipts | Embedded line items, linked to order |
+| `events` | Behavioral event stream | High-volume inserts, aggregation pipelines, change streams |
+
+
+**Dropped from original:** `sessions` (trivial insert, no PN value), `session_signals` and `next_best_actions` (CEP microservice concern — their aggregation patterns are better demonstrated through `events`).
+
+### Document Shapes
+
+#### Product
+
+```
+products {
+ _id: ObjectId
+ name: string
+ brand: string
+ code: string
+ description: string
+ masterCategory: string
+ subCategory: string
+ articleType: string
+ price: Price // embedded value object
+ image: Image // embedded value object
+ embedding: number[] // vector embedding for similarity search
+}
+
+Price { // value object (no identity)
+ amount: number
+ currency: string
+}
+
+Image { // value object
+ url: string
+}
+```
+
+**Mapping to original:** Same fields. Renamed `vai_text_embedding` → `embedding` (clearer). `price` and `image` are embedded subdocuments in both — modeled as value objects via PSL `type` keyword.
+
+**Indexes:**
+
+- Text search index on `name`, `articleType`, `subCategory`, `brand`
+- Vector search index on `embedding`
+
+#### User
+
+```
+users {
+ _id: ObjectId
+ name: string
+ email: string
+ address: Address? // embedded value object (optional)
+}
+
+Address { // value object
+ streetAndNumber: string
+ city: string
+ postalCode: string
+ country: string
+}
+```
+
+**Mapping to original:** The original `users` collection has minimal visible fields (the routes just `find({})` and `find({_id})`). We add an embedded `address` (inspired by the shipping address logic in `createOrder`) to exercise embedded value objects on users. The original's `lastRecommendations` array is dropped — recommendation state belongs in the vector search results, not denormalized on the user.
+
+#### Cart
+
+```
+carts {
+ _id: ObjectId
+ userId: ObjectId // reference to users (1:1)
+ items: CartItem[] // embedded array of value objects
+}
+
+CartItem { // value object
+ productId: ObjectId
+ name: string
+ brand: string
+ amount: number
+ price: Price // nested value object
+ image: Image // nested value object
+}
+```
+
+**Mapping to original:** Same structure. The original's `user` field is renamed `userId` for clarity. Cart items are denormalized product snapshots (same pattern — this is intentional denormalization for cart display). The `code` and `description` fields from the original cart items are dropped (not needed for cart display).
+
+**Indexes:**
+
+- Unique index on `userId` (1:1 relationship enforcement)
+
+**Data access patterns:**
+
+- **Upsert cart** — `orm.carts.upsert()` with initial items
+- **Add item** — `$push` to `items` array (via raw command, typed `$push` not yet on ORM)
+- **Remove item** — `$pull` from `items` array by `productId` (via raw command)
+- **Clear cart** — `orm.carts.where(...).update({ items: [] })`
+- **Get cart** — `orm.carts.where({ userId }).all()` → first
+- **Get cart with user** — `orm.carts.findMany({ where: { userId }, include: { user: true } })`
+
+#### Order
+
+```
+orders {
+ _id: ObjectId
+ userId: ObjectId // reference to users (N:1)
+ items: OrderLineItem[] // embedded array of value objects (snapshot at order time)
+ shippingAddress: string
+ type: string // "home" | "bopis"
+ statusHistory: StatusEntry[] // embedded array of value objects
+}
+
+OrderLineItem { // value object (identical to CartItem shape)
+ productId: ObjectId
+ name: string
+ brand: string
+ amount: number
+ price: Price
+ image: Image
+}
+
+StatusEntry { // value object
+ status: string
+ timestamp: Date
+}
+```
+
+**Mapping to original:** Same structure. `products` → `items` and `status_history` → `statusHistory` for TS naming conventions. The `user` ObjectId field → `userId`.
+
+**Data access patterns:**
+
+- **Create order** — `orm.orders.create({ ... })` with initial status entry
+- **List user orders** — `orm.orders.findMany({ where: { userId } })`
+- **Get order details** — `orm.orders.findMany({ where: { _id } })` → first
+- **Get order with user** — `findMany` with `include: { user: true }`
+- **Update order status** — `$push` to `statusHistory` array (via raw command)
+- **Delete order** — `orm.orders.delete({ _id })`
+
+#### Location
+
+```
+locations {
+ _id: ObjectId
+ name: string
+ streetAndNumber: string
+ city: string
+ postalCode: string
+ country: string
+}
+```
+
+**Mapping to original:** Same purpose, field names aligned with our `Address` value object. Simple read-only collection.
+
+#### Invoice
+
+```
+invoices {
+ _id: ObjectId
+ orderId: ObjectId // reference to orders
+ items: InvoiceLineItem[] // embedded array
+ subtotal: number
+ tax: number
+ total: number
+ issuedAt: Date
+}
+
+InvoiceLineItem { // value object
+ name: string
+ amount: number
+ unitPrice: number
+ lineTotal: number
+}
+```
+
+**Mapping to original:** The original's invoice structure isn't visible from routes (only `findFirst` by `_id`). We define a reasonable invoice shape that demonstrates embedded line items and a reference to orders.
+
+#### Event
+
+```
+events {
+ _id: ObjectId
+ userId: string
+ sessionId: string
+ type: string // "heartbeat" | "view-product" | "add-to-cart" | "search" | "exit-risk"
+ timestamp: Date
+ metadata: EventMetadata // embedded value object (polymorphic by type)
+}
+
+EventMetadata { // value object
+ productId?: string
+ subCategory?: string
+ brand?: string
+ query?: string
+ exitMethod?: string
+}
+```
+
+**Mapping to original:** Consolidates the original's `events_ingest`, `session_signals`, and `next_best_actions` into a single `events` collection. The aggregation patterns (group by type, compute percentages, sort) are preserved as queries against this collection. This simplification still exercises the same MongoDB features (high-volume inserts, aggregation pipelines, change streams) without the microservice complexity.
+
+**Data access patterns:**
+
+- **Insert event** — `orm.events.create({ ... })`
+- **Aggregate by type** — pipeline builder: `$match` → `$group` → `$unwind` → `$project` → `$sort`
+- **Watch events** — change stream subscription for real-time processing
+
+---
+
+## Feature Coverage
+
+Each feature we exercise maps to a PN capability we're validating:
+
+### Tier 1: Core (ready now)
+
+
+| Feature | MongoDB idiom | PN capability | Original app equivalent | Status |
+| ----------------------- | --------------------------------- | ------------------------ | ------------------------------ | ------------ |
+| Contract definition | — | PSL + contract emitter | (new — original has no schema) | Ready |
+| Embedded value objects | Subdocuments in results | PSL `type` keyword | `price`, `image` on products | Ready |
+| Product catalog queries | `find` with projection and filter | ORM `findMany` | `getProducts`, `findDocuments` | Ready |
+| User lookup | `find` by `_id` | ORM `findMany` + `where` | `getUsers` | Ready |
+| Cart → User relation | `$lookup` | ORM `include()` | (new — original denormalizes) | Ready |
+| Order → User relation | `$lookup` | ORM `include()` | (new — original denormalizes) | Ready |
+| Location listing | `find({})` | ORM `findMany` | `getStoreLocations` | Ready |
+| Create order | `insertOne` | ORM `create` | `createOrder` | Ready |
+| Delete order | `deleteOne` | ORM `delete` | `deleteOrder` | Ready |
+| Insert event | `insertOne` | ORM `create` | `events/route.js` | Ready |
+| Cart upsert | `findOneAndUpdate` + upsert | ORM `upsert` | `fillCart` | Ready |
+| Clear cart | `updateOne` with `$set` | ORM `update` | `clearCart` | Ready |
+| Schema migrations | `createIndex` | Migration planner/runner | — | Ready |
+| Event aggregation | `$group`/`$unwind`/`$sort` | Pipeline builder | `aggregate` | Ready |
+
+
+### Tier 2: Needs raw commands (ORM doesn't have typed array operators yet)
+
+
+| Feature | MongoDB idiom | PN surface | Original app equivalent |
+| ------------------- | --------------------------------------------- | ------------------------------- | ----------------------------- |
+| Add to cart | `$push` to embedded array | Raw `updateOne` with `$push` | `updateCartProducts` (add) |
+| Remove from cart | `$pull` from embedded array | Raw `updateOne` with `$pull` | `updateCartProducts` (remove) |
+| Update order status | `$push` to `statusHistory` | Raw `updateOne` with `$push` | `updateOrderStatus` |
+
+
+### Tier 3: Still blocked
+
+
+| Feature | MongoDB idiom | PN capability | Blocks on |
+| ------------------- | ----------------------------------- | ------------------------------------- | ---------------------------------------------- |
+| Product text search | `$search` stage | Atlas Search extension pack | Extension pack not built |
+| Product similarity | `$vectorSearch` | Pipeline builder has `$vectorSearch` | Atlas-only (needs Atlas cluster for testing) |
+| Event change stream | `db.watch()` | Runtime streaming interface | Change stream support not built |
+| Data migration | `updateMany` with field transform | Data migration runner | Data migration runner not built |
+
+
+---
+
+## Architecture
+
+A Next.js App Router application with a clear separation between the UI, API routes, and data access layer:
+
+```
+examples/retail-store/
+├── package.json
+├── next.config.js
+├── tsconfig.json
+├── biome.jsonc
+├── vitest.config.ts
+├── prisma-next.config.ts # PN config: family, target, adapter, driver, contract provider
+├── prisma/
+│ └── contract.prisma # PSL schema (source of truth for data model)
+├── migrations/ # generated by `prisma-next migration plan`
+│ └── _migration/
+│ ├── migration.json
+│ └── ops.json
+├── src/
+│ ├── contract.json # generated by `prisma-next contract emit`
+│ ├── contract.d.ts # generated
+│ ├── db.ts # database factory (createClient: uri, dbName → orm + pipeline)
+│ ├── data/ # data access layer (all PN ORM/pipeline calls live here)
+│ │ ├── products.ts
+│ │ ├── users.ts
+│ │ ├── carts.ts
+│ │ ├── orders.ts
+│ │ ├── locations.ts
+│ │ ├── invoices.ts
+│ │ └── events.ts
+│ └── seed.ts # seed data for tests and demos
+├── scripts/
+│ └── seed.ts # standalone seed script with .env support
+├── app/ # Next.js App Router
+│ ├── layout.tsx
+│ ├── page.tsx # product catalog / landing page
+│ ├── cart/
+│ │ └── page.tsx
+│ ├── orders/
+│ │ └── page.tsx
+│ ├── orders/[id]/
+│ │ └── page.tsx
+│ └── api/ # API routes — representative sample
+│ ├── products/route.ts
+│ ├── cart/route.ts
+│ ├── orders/route.ts
+│ └── ...
+└── test/
+ ├── crud-lifecycle.test.ts # full CRUD + upsert + embedded docs
+ ├── relations.test.ts # cross-collection $lookup tests
+ ├── aggregation.test.ts # pipeline builder tests
+ └── setup.ts # shared MongoMemoryReplSet setup
+```
+
+The data access layer (`src/data/`) is the boundary where PN is used — API routes call into it, never touching the MongoDB driver directly. The contract is authored in PSL (`prisma/contract.prisma`) and emitted via `prisma-next contract emit`. Tests use `mongodb-memory-server`.
+
+**Key patterns from the updated `mongo-demo`:**
+
+- `prisma-next.config.ts` includes `driver: mongoDriver` (from `@prisma-next/driver-mongo/control`) and `db: { connection: ... }`
+- `db.ts` creates a `pipeline` via `mongoPipeline({ contractJson })` alongside the ORM
+- `validateMongoContract` imports from `@prisma-next/mongo-contract` (not `mongo-core`)
+- `createMongoRuntime` no longer takes `loweringContext`
+
+---
+
+## Milestones
+
+The original plan had 8 milestones, most blocked on missing framework features. With the new base branch, the first 5 milestones (through aggregation) are fully unblocked and can be consolidated.
+
+### Milestone 1: Scaffold + Contract + Migrations
+
+Set up the workspace package, author the full contract with embedded value objects, and generate the initial schema migration.
+
+**Framework dependencies:** All available on the base branch.
+
+**Tasks:**
+
+- [ ] Create `examples/retail-store/` with `package.json`, `tsconfig.json`, `biome.jsonc`, `vitest.config.ts` — following the updated `mongo-demo` structure (including `@prisma-next/mongo-pipeline-builder`, `@prisma-next/mongo-query-ast`, `@prisma-next/mongo-contract`)
+- [ ] Write `prisma-next.config.ts` with `driver: mongoDriver`, `db: { connection }`, and `mongoContract()` provider
+- [ ] Write `prisma/contract.prisma` using the PSL `type` keyword for all embedded value objects:
+ - `type Price { amount Float; currency String }`
+ - `type Image { url String }`
+ - `type Address { streetAndNumber String; city String; postalCode String; country String }`
+ - `type CartItem { productId ObjectId; name String; brand String; amount Int; price Price; image Image }`
+ - `type OrderLineItem { productId ObjectId; name String; brand String; amount Int; price Price; image Image }`
+ - `type StatusEntry { status String; timestamp DateTime }`
+ - `type InvoiceLineItem { name String; amount Int; unitPrice Float; lineTotal Float }`
+ - `type EventMetadata { productId String?; subCategory String?; brand String?; query String?; exitMethod String? }`
+ - 7 models with proper relations, using these embedded types
+- [ ] Run `prisma-next contract emit`, commit `contract.json` + `contract.d.ts`
+- [ ] Run `prisma-next migration plan`, commit the initial migration (creates collections + indexes)
+- [ ] Write `src/db.ts` database factory with ORM + pipeline builder (following updated `mongo-demo` pattern)
+- [ ] Write `src/seed.ts` and `scripts/seed.ts` with realistic seed data for all 7 collections
+
+### Milestone 2: CRUD + Relations + Embedded Documents
+
+Build the full data access layer with reads, writes, and relation loading. This consolidates the old M2 (reads), M3 (writes), and the non-blocked parts of M4 (upsert, $set).
+
+**Framework dependencies:** ORM `findMany` + `include()` + `create` + `update` + `delete` + `upsert` — all available.
+
+**Tasks:**
+
+- [ ] Implement product queries: `findProducts()`, `findProductById(id)`
+- [ ] Implement user queries: `findUsers()`, `findUserById(id)` — verify embedded `address` appears inline
+- [ ] Implement location queries: `findLocations()`
+- [ ] Implement invoice queries: `findInvoiceById(id)`, `findInvoiceWithOrder(id)` — verify embedded `items` inline + `include({ order: true })`
+- [ ] Implement cart queries: `getCartByUserId(userId)`, `getCartWithUser(userId)` — verify embedded `items` inline + `include({ user: true })`
+- [ ] Implement order queries: `getUserOrders(userId)`, `getOrderById(id)`, `getOrderWithUser(id)` — verify embedded `items` + `statusHistory` inline + `include({ user: true })`
+- [ ] Implement event queries: `findEventsByUser(userId)` — verify embedded `metadata` inline
+- [ ] Implement `createOrder(order)` — `orm.orders.create(...)` with initial status entry and embedded line items
+- [ ] Implement `deleteOrder(id)` — `orm.orders.delete(...)`
+- [ ] Implement `createEvent(event)` — `orm.events.create(...)`
+- [ ] Implement `upsertCart(userId, items)` — `orm.carts.upsert(...)`
+- [ ] Implement `clearCart(userId)` — `orm.carts.where(...).update({ items: [] })`
+- [ ] Write integration tests covering: reads, creates, deletes, upsert, relation loading ($lookup), embedded documents inline in results
+- [ ] Write seed test to verify `seed()` populates all collections and data is queryable via ORM
+
+### Milestone 3: Array update operators (raw commands)
+
+Implement cart add/remove and order status updates using `$push`/`$pull` via raw commands. The ORM does not yet have typed array update operators, so these use the runtime's raw command surface — still PN framework code.
+
+**Framework dependencies:** Raw command execution via runtime (available). Typed `$push`/`$pull` on ORM (not available — tracked as a framework gap).
+
+**Tasks:**
+
+- [ ] Implement `addToCart(userId, item)` — raw `updateOne` with `$push: { items: item }`
+- [ ] Implement `removeFromCart(userId, productId)` — raw `updateOne` with `$pull: { items: { productId } }`
+- [ ] Implement `updateOrderStatus(orderId, entry)` — raw `updateOne` with `$push: { statusHistory: entry }`
+- [ ] Write integration tests for each update operator pattern
+- [ ] Document that these will migrate to ORM `$push`/`$pull` when the typed array operator surface ships
+
+### Milestone 4: Aggregation pipelines
+
+Add event analytics using the pipeline builder.
+
+**Framework dependencies:** `@prisma-next/mongo-pipeline-builder` (available).
+
+**Tasks:**
+
+- [ ] Implement `aggregateEventsByType(userId)` — pipeline builder: `$match` → `$group` → `$unwind` → `$project` → `$sort`
+- [ ] Implement `getRandomProducts(count)` — pipeline builder with `$sample` stage
+- [ ] Write integration tests for aggregation results
+
+### Milestone 5: Vector search (optional — Atlas required)
+
+Add product similarity queries using the pipeline builder's `$vectorSearch` stage.
+
+**Framework dependencies:** Pipeline builder has `$vectorSearch` stage support (available). Atlas cluster required for testing.
+
+**Tasks:**
+
+- [ ] Add `embedding` field to the Product model in the PSL schema (if not already included in M1)
+- [ ] Implement `findSimilarProducts(embedding, limit)` — pipeline builder with `$vectorSearch` stage
+- [ ] Write optional integration test (requires Atlas): seed products with embeddings, query by vector, assert results ordered by relevance
+- [ ] Document Atlas cluster requirement for this feature
+
+### Milestone 6: Close-out
+
+Verify all acceptance criteria, document gaps, and finalize.
+
+**Tasks:**
+
+- [ ] Run full test suite — all tests pass
+- [ ] Run typecheck — no errors
+- [ ] Verify all testable acceptance criteria from the spec
+- [ ] Document framework gaps discovered (with tickets if appropriate):
+ - Typed `$push`/`$pull` array operators on ORM
+ - Change stream support
+ - Atlas Search extension pack
+ - Data migration runner
+- [ ] Write README for `examples/retail-store/`
+- [ ] Verify migrations apply cleanly against `mongodb-memory-server`
+
+---
+
+## Mapping to Original App
+
+This section documents how each original retail-store-v2 API route maps to our design:
+
+
+| Original route | Original operation | Our equivalent | Notes |
+| ------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------ |
+| `getProducts` | `find` with brand/category filters, projection | `findProducts(filters)` | Same query, typed filters |
+| `search` | `$search` aggregation pipeline | `searchProducts(query)` | Blocks on Atlas Search extension |
+| `getProductId` | `find({_id})` | `findProductById(id)` | Direct mapping |
+| `getUsers` | `find({})` | `findUsers()` | Direct mapping |
+| `getCart` | `find({user: ObjectId})` | `getCartByUserId(userId)` | Direct mapping |
+| `fillCart` | `$sample` + `findOneAndUpdate` with upsert, `$setOnInsert`, `$push` | `upsertCart(userId, items)` | Split: random product selection is separate from cart upsert |
+| `updateCartProducts` | `$push` (add) or `$pull` (remove) via `findOneAndUpdate` | `addToCart(userId, item)` / `removeFromCart(userId, productId)` | Split into explicit add/remove |
+| `clearCart` | `updateOne` with `$set: {products: []}` | `clearCart(userId)` | Direct mapping |
+| `createOrder` | `insertOne` with constructed document | `createOrder(order)` | Direct mapping |
+| `getOrders` | `find({user}).sort({_id: -1})` | `getUserOrders(userId)` | Direct mapping |
+| `getOrderDetails` | `find({_id})` | `getOrderById(orderId)` | Direct mapping |
+| `updateOrderStatus` | `updateOne` with `$push: {status_history}` | `updateOrderStatus(orderId, entry)` | Direct mapping |
+| `deleteOrder` | `deleteOne({_id})` | `deleteOrder(orderId)` | Direct mapping |
+| `getStoreLocations` | `find({})` | `findLocations()` | Direct mapping |
+| `findDocuments` (invoices) | Generic `find` | `findInvoiceById(id)` | Typed, not generic |
+| `insertDocument` (sessions) | Generic `insertOne` | Dropped | No PN value in a trivial insert |
+| `events` | `insertOne` to `events_ingest` | `createEvent(event)` | Direct mapping |
+| `aggregate` | Generic aggregate endpoint | `aggregateEventsByType(userId)` | Typed, specific pipelines |
+| `sse` | `db.watch()` + SSE bridge | Deferred | Change stream support not yet built |
+| `updateDocument` (NBA redeem) | Generic `updateOne` with `$set` | Dropped (consolidated into events) | |
+| `findDocuments` (signals, NBAs) | Generic `find` | Dropped (consolidated into events) | |
+| `getAssistantResponse` | Chatbot integration | Dropped | Out of scope (validates Dataworkz) |
+| `getInvoiceUrl` | URL generation | Dropped | App-level concern |
+
+
+---
+
+## Framework Gaps (Remaining)
+
+Compared to the original plan, most gaps are now closed. These remain:
+
+
+| Gap | Impact | Status |
+| -------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------- |
+| ~~ORM `create`/`update`/`delete`~~ | ~~M3~~ | **Shipped** on base branch |
+| Typed `$push`/`$pull` array operators | M3 uses raw commands instead of ORM operators | Not on ORM; raw commands work as workaround |
+| ~~Upsert support~~ | ~~M4~~ | **Shipped** on base branch |
+| ~~Value objects in contract~~ | ~~M1~~ | **Shipped** — PSL `type` keyword |
+| ~~Mongo migration planner~~ | ~~M5~~ | **Shipped** on base branch |
+| ~~Aggregation pipeline builder~~ | ~~M7~~ | **Shipped** — `@prisma-next/mongo-pipeline-builder` |
+| Vector search extension pack | M5 uses pipeline builder `$vectorSearch` stage | Stage exists in pipeline builder; no dedicated extension pack for Mongo |
+| Atlas Search extension pack | Product text search not implementable | Not built |
+| Change stream support | Event change stream deferred | Not built (no evidence on base branch) |
+| Data migration runner | Data migration deferred | Schema migration runner exists; data migration runner unclear |
+
+
+---
+
+## Test Coverage
+
+
+| Acceptance Criterion | Test Type | Milestone |
+| -------------------------------------------------------- | -------------------------- | --------- |
+| Contract emits valid `contract.json` and `contract.d.ts` | Build | M1 |
+| Schema migrations create correct indexes | Integration | M1 |
+| Embedded value objects inline in results | Integration | M2 |
+| `$lookup` relations load via `include()` | Integration | M2 |
+| All CRUD operations work via ORM | Integration | M2 |
+| Cart upsert works | Integration | M2 |
+| `$push`/`$pull` array update operators work | Integration | M3 |
+| Aggregation pipeline produces correct analytics | Integration | M4 |
+| `$sample` returns random documents | Integration | M4 |
+| Vector search returns similarity-ordered results | Integration (Atlas-only) | M5 |
+| Full suite passes against `mongodb-memory-server` | CI | M6 |
+
+
diff --git a/projects/mongo-example-apps/spec.md b/projects/mongo-example-apps/spec.md
new file mode 100644
index 0000000000..f4db079a1a
--- /dev/null
+++ b/projects/mongo-example-apps/spec.md
@@ -0,0 +1,129 @@
+# Summary
+
+Port two real-world MongoDB Industry Solutions applications — an e-commerce platform ([retail-store-v2](https://github.com/mongodb-industry-solutions/retail-store-v2)) and a predictive maintenance system ([Leafy-Predictive-Maintenance](https://github.com/mongodb-industry-solutions/Leafy-Predictive-Maintenance)) — to Prisma Next's MongoDB support. These live as example apps in this repo, consuming framework packages directly, to validate that the PN Mongo implementation holds up under real industry conditions.
+
+# Description
+
+The MongoDB PoC validated the architecture with toy schemas. Before shipping, we need confidence that the implementation handles real-world data models with real-world complexity — embedded documents, referenced relations, polymorphic types, vector search indexes, change streams, update operators, aggregation pipelines, and schema migrations.
+
+The MongoDB Industry Solutions org maintains ~84 open-source demo apps across industries. Two were selected for their complementary feature coverage:
+
+1. **retail-store-v2** — A Next.js e-commerce platform with products, orders, customers, inventory, an agentic RAG chatbot, omnichannel ordering, personalized recommendations (via vector embeddings), and complex event processing via change streams. This exercises the widest range of PN Mongo features: embedded documents, referenced relations, multiple index types, vector search, change streams, and update operators.
+
+2. **Leafy-Predictive-Maintenance** — A Next.js predictive maintenance system for manufacturing, using Atlas Vector Search for repair manual search and equipment criticality analysis, Atlas Stream Processing for real-time failure detection, and ML model storage. This specifically stress-tests vector search as a core feature, time-series data patterns, and exotic BSON types.
+
+Both apps live as example projects under `examples/` in this repo on this branch. They consume PN framework packages directly via workspace dependencies, which keeps the framework code visible to agents working on the ports and ensures the examples stay up to date as the framework evolves.
+
+# Requirements
+
+## Functional Requirements
+
+### App 1: Retail Store (retail-store-v2)
+
+1. **Contract definition**: Author the retail domain contract in both PSL and TypeScript DSL — products, orders, customers, inventory, carts, recommendations — with embedded documents (order line items, addresses, cart items), referenced relations (customer ↔ orders, product ↔ category), and polymorphic types where the original data model uses them. Both authoring surfaces must produce equivalent `contract.json` output.
+2. **Schema migrations**: The contract produces schema migrations that create the necessary MongoDB collections, indexes (unique, compound, text, vector), and JSON Schema validators. The migration runner applies them against a real MongoDB instance.
+3. **ORM queries**: Replace raw MongoDB driver calls with PN ORM queries — `findMany`, `findFirst`, `create`, `update`, `delete` — using the fluent chaining API. Relation loading via `$lookup` works for referenced relations. Embedded documents are inlined in query results.
+4. **Update operators**: Cart and inventory mutations use Mongo-native update operators (`$inc`, `$push`, `$pull`, `$set`) through the PN mutation surface.
+5. **Vector search**: Product recommendation queries use the PN vector search extension pack to perform similarity searches against embedding vectors stored in the product/recommendation collections.
+6. **Change streams**: The customer retention CEP feature subscribes to collection changes through the PN runtime's streaming interface.
+7. **Data migrations**: At least one data migration (e.g. a product catalog restructuring or field rename) runs through the PN migration graph.
+
+### App 2: Predictive Maintenance (Leafy-Predictive-Maintenance)
+
+8. **Contract definition**: Author the maintenance domain contract in both PSL and TypeScript DSL — machines, sensor readings, failure predictions, ML models, repair manuals, maintenance history — with appropriate embedded documents and referenced relations. Both authoring surfaces must produce equivalent `contract.json` output.
+9. **Schema migrations**: The contract produces schema migrations that create collections with vector search indexes (multiple embedding dimensions: 1024 for Cohere, 1536 for OpenAI) and filtered indexes.
+10. **Vector search (core)**: Repair manual search and equipment criticality analysis use PN's vector search extension pack as their primary query mechanism, not a bolt-on.
+11. **ORM queries**: Replace raw MongoDB driver calls with PN ORM queries for CRUD operations on machines, failures, and maintenance history.
+12. **Time-series patterns**: Sensor data ingestion and failure prediction queries work through the PN runtime, validating how PN handles high-volume insert and time-ordered query patterns.
+
+## Non-Functional Requirements
+
+1. **In-repo examples**: Both apps live under `examples/` with workspace dependencies on PN packages. They do not use published npm packages.
+2. **Runnable against mongodb-memory-server**: Both apps can run their data access layer against `mongodb-memory-server` for local development and CI, without requiring Atlas. Atlas-specific features (Atlas Vector Search, Atlas Stream Processing) are exercised via optional integration tests that require Atlas credentials.
+3. **Contract-first**: Each app has a committed `contract.json` and `contract.d.ts`. The contract is the source of truth for the data model, not ad-hoc driver calls.
+4. **Type safety**: All queries are fully typed — the ORM infers row types from the contract, including embedded document fields, relation includes, and polymorphic narrowing.
+5. **Block on framework, never paper over**: If a PN feature required by the port is not yet implemented, the port blocks until the framework delivers it. The purpose of this project is to uncover gaps in the framework, not to work around them with raw driver calls or hand-rolled solutions. Every gap discovered is a signal that the framework needs work — file it, fix it in the framework, then continue the port.
+
+## Non-goals
+
+- **Full UI port**: The goal is porting the data access layer, not rewriting the frontend. The UI can remain unchanged or be simplified.
+- **Production deployment**: These are local example apps for validation, not production-ready deployments.
+- **Atlas-exclusive features as hard requirements**: Atlas Stream Processing and Atlas Vector Search are tested via optional integration tests. The core data access layer works without Atlas.
+- **Porting non-JS microservices**: The Python inference script in Leafy-Predictive-Maintenance is out of scope. Only the Node.js/Next.js data access layer is ported.
+- **Performance benchmarking**: We're validating correctness and API coverage, not measuring throughput.
+
+# Acceptance Criteria
+
+## Contract & Schema
+
+- [ ] Each app has a PN contract authored in both PSL and TypeScript DSL
+- [ ] Both authoring surfaces produce equivalent `contract.json` for each app
+- [ ] Each contract emits valid `contract.json` and `contract.d.ts` artifacts
+- [ ] Schema migrations create the correct MongoDB collections, indexes, and validators
+- [ ] The migration runner applies schema migrations against a real MongoDB instance
+
+## ORM & Queries
+
+- [ ] All CRUD operations in both apps use the PN ORM, not raw driver calls
+- [ ] Embedded documents appear inline in query results (no separate `include` needed)
+- [ ] Referenced relations load via `$lookup` through the ORM's `include` method
+- [ ] Query results are fully typed — the TypeScript compiler catches type errors in query consumption code
+
+## MongoDB-Specific Features
+
+- [ ] At least one mutation in the retail app uses Mongo-native update operators (`$inc`, `$push`, or `$pull`) through the PN mutation surface
+- [ ] Vector search queries work in both apps via the PN extension pack
+- [ ] At least one data migration runs through the PN migration graph in the retail app
+- [ ] Change stream subscription works through the PN runtime in the retail app (optional: requires Atlas or replica set)
+
+## Validation
+
+- [ ] Both apps run their data access layer against `mongodb-memory-server` in CI
+- [ ] Both apps demonstrate at least 3 distinct MongoDB idioms each (from: embedded docs, referenced relations, polymorphism, vector search, update operators, aggregation pipelines, change streams, time-series patterns)
+
+# Other Considerations
+
+## Security
+
+Not applicable — these are local example apps with no authentication or multi-tenancy concerns. The original apps' auth patterns are out of scope for the port.
+
+## Cost
+
+Zero incremental infrastructure cost. Both apps run against `mongodb-memory-server` locally. Atlas integration tests run against a shared development Atlas cluster if credentials are available.
+
+## Observability
+
+Not applicable beyond standard test output. The PN runtime's built-in telemetry plugin can be demonstrated if useful.
+
+## Data Protection
+
+Not applicable — all data is synthetic/demo data from the original repos.
+
+## Analytics
+
+Not applicable.
+
+# References
+
+- [retail-store-v2](https://github.com/mongodb-industry-solutions/retail-store-v2) — source repo (JavaScript, Next.js)
+- [Leafy-Predictive-Maintenance](https://github.com/mongodb-industry-solutions/Leafy-Predictive-Maintenance) — source repo (JavaScript, Next.js)
+- [MongoDB Family subsystem doc](../../docs/architecture%20docs/subsystems/10.%20MongoDB%20Family.md)
+- [MongoDB User Promise](../../docs/reference/mongodb-user-promise.md) — the developer experience we're validating against
+- [MongoDB Feature Support Priorities](../../docs/reference/mongodb-feature-support-priorities.md) — the MongoDB team's feature priority list
+- [MongoDB Status Update](../../docs/planning/mongo-target/mongodb-status-update.md) — current implementation status
+- ADRs 170, 172-180 — architecture decisions governing the implementation
+- [April milestone WS4](../../docs/planning/april-milestone.md) — the broader workstream these examples validate
+
+# Open Questions
+
+_All initial questions resolved._
+
+## Resolved
+
+1. **Contract authoring surface** — Author contracts in both PSL and TypeScript DSL. Both surfaces must produce equivalent output. Do not use hand-crafted JSON.
+
+2. **Scope of the retail app port** — Port the core e-commerce data model (products, orders, customers, inventory) plus recommendations (vector search) and CEP (change streams). The chatbot integration is out of scope (it validates Dataworkz, not PN).
+
+3. **Vector search implementation timing** — Block on the PN Mongo vector search implementation. Do not use raw aggregation pipelines as a workaround. This applies generally: if a PN feature is not ready, wait until it is.
+
+4. **Directory layout** — Two separate directories: `examples/retail-store/` and `examples/predictive-maintenance/`.