ConusAI vendor 81941888f

This commit is contained in:
ConusAI vendor 81941888f
2026-10-07 08:47:51 +00:00
commit c3c6526952
135 changed files with 13010 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules/
.next/
.env*
!.env.example
next-env.d.ts
tsconfig.tsbuildinfo
playwright-report/
test-results/
+78
View File
@@ -0,0 +1,78 @@
# ConusAI Showcase
Notes with attachments on Postgres and Blob Storage, audited, observed and
metered: every ConusAI SDK in one Next.js app built with Feature-Sliced Design
(showcase plan D11). The template is proven in this repository by
`scripts/vendor/test-template.sh` at the same commit as the SDKs it uses (D18).
## Pinned versions
Pinned on 2026-10-06 with `npm view <package> version` (showcase §2.7):
| Package | Version |
|---|---|
| next | 16.3.8 |
| react, react-dom | 19.3.0 |
| typescript | 5.9.3 (5.x: the `@conusai-sdk/plugin` peer requires ^5) |
| drizzle-orm, drizzle-kit | 0.45.3, 0.31.11 |
| postgres | 3.4.9 |
| zod | 4.6.5 |
| server-only | 0.0.1 |
| vitest | 5.0.3 |
| eslint, eslint-config-next, @eslint/eslintrc | 9.39.5 (ESLint 10 breaks the React plugin bundled by eslint-config-next), 16.3.8, 3.3.7 |
| steiger, @feature-sliced/steiger-plugin | 0.7.0, 0.8.0 |
| @playwright/test | 1.63.0 |
| @types/node, @types/react, @types/react-dom | 26.6.4, 19.3.0, 19.3.0 |
The `@conusai-sdk/*` packages are not listed: `test-template.sh` installs them
from the bundle built from this commit.
## Layout
- `app/` routes only; each file re-exports from `src/views` or `src/app`.
- `src/app`, `src/views`, `src/widgets`, `src/features`, `src/entities`,
`src/shared`: the FSD layers, imports point downward only (checked by `steiger`).
- `src/shared/config/env.ts`: the environment, parsed once with zod (D12).
- `src/shared/api/*/server.ts`: server-only SDK clients (`server-only`).
- `src/shared/lib/visitor.ts`: the signed `sc_vid` visitor cookie (D16).
## What each slice demonstrates
| Slice | Demonstrates |
|---|---|
| `features/create-note` | A note in Postgres, audited as `note.created`; NATS announces it and Qdrant indexes it when those are linked. |
| `features/edit-note`, `features/delete-note` | Writes scoped to the author, each audited (`note.updated`, `note.deleted`). |
| `features/upload-file`, `features/delete-file` | Attachments in Blob Storage, one usage event per upload with an idempotency key; a per-minute quota when KV is linked. |
| `features/publish-note-event` | An author re-announces a note on NATS (`note.announced` in the audit log); another visitor is refused. |
| `features/search-notes` | Similar-note search in Qdrant, when Qdrant is linked. |
| `features/export-notes` | The visitor's notes written as one JSON object to the dedicated S3 bucket, when S3 is linked. |
| `features/rate-limit` | The uploads left this minute, read from KV, when KV is linked. |
| `features/simulate-traffic` | A scripted visitor journey that produces observed page views, a started journey and an error. |
| `widgets/note-list`, `widgets/attachments` | The visitor's notes and their attachments, with the controls each feature provides. |
| `widgets/audit-timeline`, `widgets/usage-meter` | The platform's audit events for this project, and the visitor's metered uploads and bytes. |
| `widgets/enable-card`, `widgets/module-panel` | One slot per optional module: its feature when linked, its enable card (text from the platform's catalog) when not (D13). |
| `views/dashboard`, `views/audit`, `views/platform` | The composed pages; the platform page lists each optional service as linked or not. |
## Optional modules
The showcase reads the variables the platform injects for each linked service
(D8, D13). A module without its variables shows its enable card instead of its
feature. Its variables are `REDIS_URL` (KV), `NATS_URL` and `NATS_TOKEN`,
`QDRANT_URL` and `QDRANT_API_KEY`, `GIT_URL`, and `AWS_ENDPOINT_URL`, `S3_BUCKET`,
`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. The S3 names are the ones the platform
injects for an S3 service.
## Commands
- `npm run typecheck`, `npm run steiger`, `npm run lint`, `npm test`, `npm run build`
- `npm run e2e` runs the Playwright suite against a production build. It starts
`e2e/fake-platform.mjs` in place of the platform and withholds the optional
module variables, so every enable card shows. The suite covers creating a note
and its audit entry, an attachment moving the usage meter, the enable cards,
axe (no serious or critical issues on each page) and the Core Web Vitals
budgets (LCP at most 2.5 s, INP at most 200 ms) measured in Chromium.
- `npm test` also runs `src/shared/lib/optional.live.test.ts` against live NATS,
Qdrant and S3 when `NATS_URL`, `QDRANT_URL` and `AWS_ENDPOINT_URL` are set; without
them it is skipped.
- `bash scripts/vendor/test-template.sh` from the repository root runs all of them against the bundle;
`bash scripts/vendor/test-template.sh --e2e` adds the Playwright suite.
+17
View File
@@ -0,0 +1,17 @@
import { sql } from 'drizzle-orm'
import { db, serverErrors, withRequest } from '@/shared/api'
// 200 when Postgres answers; the platform probes this path (health_check_path).
export const dynamic = 'force-dynamic'
export async function GET(request: Request) {
return withRequest(request, async () => {
try {
await db().execute(sql`select 1`)
return Response.json({ status: 'ok' })
} catch (error) {
serverErrors().captureException(error)
return Response.json({ status: 'unavailable' }, { status: 503 })
}
})
}
+3
View File
@@ -0,0 +1,3 @@
export const dynamic = 'force-dynamic'
export { AuditPage as default } from '@/views/audit'
+1
View File
@@ -0,0 +1 @@
export { RootLayout as default, metadata } from '@/app/layouts/root'
+1
View File
@@ -0,0 +1 @@
export { DashboardPage as default } from '@/views/dashboard'
+3
View File
@@ -0,0 +1,3 @@
export const dynamic = 'force-dynamic'
export { PlatformPage as default } from '@/views/platform'
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
dialect: 'postgresql',
schema: ['./src/entities/note/model/schema.ts', './src/entities/file/model/schema.ts'],
out: './drizzle',
})
+8
View File
@@ -0,0 +1,8 @@
CREATE TABLE "notes" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"body" text DEFAULT '' NOT NULL,
"author_id" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
+13
View File
@@ -0,0 +1,13 @@
CREATE TABLE "files" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"note_id" uuid NOT NULL,
"author_id" text NOT NULL,
"name" text NOT NULL,
"content_type" text NOT NULL,
"size" integer NOT NULL,
"url" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "files" ADD CONSTRAINT "files_note_id_notes_id_fk" FOREIGN KEY ("note_id") REFERENCES "notes"("id") ON DELETE cascade ON UPDATE no action;
+72
View File
@@ -0,0 +1,72 @@
{
"id": "cbd1580b-e469-4710-a530-13305ad7a627",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.notes": {
"name": "notes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"author_id": {
"name": "author_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+135
View File
@@ -0,0 +1,135 @@
{
"id": "6c019c3b-9836-4d91-99a6-a1f20b9fe445",
"prevId": "cbd1580b-e469-4710-a530-13305ad7a627",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.notes": {
"name": "notes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"body": {
"name": "body",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"author_id": {
"name": "author_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.files": {
"name": "files",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"note_id": {
"name": "note_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"author_id": {
"name": "author_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content_type": {
"name": "content_type",
"type": "text",
"primaryKey": false,
"notNull": true
},
"size": {
"name": "size",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1791259122531,
"tag": "0000_init",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1791259684816,
"tag": "0001_files",
"breakpoints": true
}
]
}
+96
View File
@@ -0,0 +1,96 @@
// A stand-in for the ConusAI platform's routes the showcase calls (plan S4.8), so the
// end-to-end run needs no platform. It keeps what the app sends in memory, reads it
// back the way the platform does, and exposes that state under /__e2e/state for the
// tests. It is not a platform: no tenancy, no auth beyond the bearer header's presence.
import { createServer } from 'node:http'
import { gunzipSync } from 'node:zlib'
const PORT = 4010
const state = { audit: [], usage: [], blobs: [] }
const catalog = {
redis: { display_name: 'Redis', description: 'Counts uploads per visitor each minute.' },
nats: { display_name: 'NATS', description: 'Announces new notes live to subscribers.' },
qdrant: { display_name: 'Qdrant', description: 'Finds similar notes by vector search.' },
git: { display_name: 'Git & Packages', description: 'Installs packages from the platform registry.' },
s3: { display_name: 'S3', description: 'Exports your notes to an S3 bucket.' },
}
function send(res, status, body) {
res.writeHead(status, { 'content-type': 'application/json' })
res.end(JSON.stringify(body))
}
async function readBody(req) {
const chunks = []
for await (const chunk of req) chunks.push(chunk)
const raw = Buffer.concat(chunks)
return req.headers['content-encoding'] === 'gzip' ? gunzipSync(raw) : raw
}
const server = createServer(async (req, res) => {
const url = new URL(req.url ?? '/', `http://127.0.0.1:${PORT}`)
// CONUSAI_API_URL is the platform base ending in /api, and the SDKs append only their
// route. Strip exactly one /api: a request for /api/api/... then matches no route.
const path = url.pathname.startsWith('/api/') ? url.pathname.slice(4) : url.pathname
const method = req.method ?? 'GET'
if (method === 'GET' && path === '/__e2e/health') return send(res, 200, { ok: true })
if (method === 'GET' && path === '/__e2e/state') return send(res, 200, state)
if (method === 'GET' && path === '/.well-known/conusai.json') {
return send(res, 200, { name: 'ConusAI e2e platform', version: 'e2e' })
}
const service = path.match(/^\/v1\/service-types\/([a-z0-9_-]+)$/)
if (method === 'GET' && service) {
const entry = catalog[service[1]]
if (!entry) return send(res, 404, { title: 'unknown service type' })
return send(res, 200, { service_type: service[1], category: 'e2e', color: '#000000', capabilities: {}, ...entry })
}
if (method === 'POST' && path === '/_conusai/audit') {
const body = JSON.parse((await readBody(req)).toString('utf8'))
for (const event of body.events) {
state.audit.push({
id: state.audit.length + 1,
action: event.action,
outcome: event.outcome ?? 'success',
occurred_at: event.occurred_at ?? new Date().toISOString(),
resource_type: event.target?.type ?? null,
resource_id: event.target?.id ?? null,
})
}
return send(res, 202, { accepted: body.events.length, duplicates: 0, redacted: [] })
}
if (method === 'GET' && path === '/_conusai/audit/events') {
const items = [...state.audit].reverse()
return send(res, 200, { items, next_cursor: null, has_more: false })
}
if (method === 'POST' && path === '/_conusai/usage/events') {
const body = JSON.parse((await readBody(req)).toString('utf8'))
state.usage.push(...body.events)
return send(res, 202, { accepted: body.events.length, duplicates: 0, rejected: [] })
}
if (method === 'POST' && path === '/blob') {
const bytes = await readBody(req)
const pathname = url.searchParams.get('pathname') ?? 'blob'
const info = {
url: `http://127.0.0.1:${PORT}/api/blob/${pathname}`,
pathname,
contentType: req.headers['content-type'] ?? 'application/octet-stream',
size: bytes.length,
uploadedAt: new Date().toISOString(),
}
state.blobs.push(info)
return send(res, 200, info)
}
return send(res, 404, { title: 'not found', path })
})
server.listen(PORT, '127.0.0.1', () => {
console.log(`fake platform listening on ${PORT}`)
})
+114
View File
@@ -0,0 +1,114 @@
import AxeBuilder from '@axe-core/playwright'
import { expect, test, type Page } from '@playwright/test'
// The end-to-end run of the showcase (plan S4.8): a note, an attachment, the audit
// entry and usage on the platform, the enable cards, accessibility and the Core Web
// Vitals budgets. The platform is the fake in e2e/fake-platform.mjs.
const platform = 'http://127.0.0.1:4010'
type PlatformState = {
audit: Array<{ action: string; resource_id: string | null }>
usage: Array<{ idempotency_key: string; meter: string }>
}
async function platformState(): Promise<PlatformState> {
const response = await fetch(`${platform}/__e2e/state`)
return (await response.json()) as PlatformState
}
function uniqueTitle(label: string): string {
return `${label} ${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
}
async function createNote(page: Page, title: string): Promise<void> {
await page.goto('/')
await page.getByLabel('Title').fill(title)
await page.getByLabel('Body').fill('written by the end-to-end run')
await page.getByRole('button', { name: 'Create note' }).click()
await expect(page.getByRole('heading', { name: title })).toBeVisible()
}
test('a note is audited on the platform and listed on the audit page', async ({ page }) => {
const title = uniqueTitle('audited')
await createNote(page, title)
await expect
.poll(async () => (await platformState()).audit.some((event) => event.action === 'note.created'), { timeout: 15_000 })
.toBe(true)
await page.goto('/audit')
await expect(page.getByText('note.created').first()).toBeVisible()
})
test('an attachment moves the usage meter and is metered on the platform', async ({ page }) => {
const title = uniqueTitle('attached')
await createNote(page, title)
const meter = page.getByText(/uploads, \d+ bytes stored/)
await expect(meter).toHaveText(/^0 uploads, 0 bytes stored$/)
const usageBefore = (await platformState()).usage.length
const note = page.getByRole('listitem').filter({ hasText: title })
await note.getByLabel('Attachment').setInputFiles({
name: 'notes.txt',
mimeType: 'text/plain',
buffer: Buffer.from('sixteen bytes!!!'),
})
await note.getByRole('button', { name: 'Attach', exact: true }).click()
await expect(meter).toHaveText(/^1 uploads, 16 bytes stored$/)
await expect
.poll(async () => (await platformState()).usage.length, { timeout: 15_000 })
.toBeGreaterThan(usageBefore)
})
test('each optional module without its variables shows its enable card', async ({ page }) => {
await page.goto('/')
await expect(page.getByText(/to enable this\.$/)).toHaveCount(5)
await expect(page.getByText('Link Redis to enable this.')).toBeVisible()
await expect(page.getByText(/uploads left this minute/)).toHaveCount(0)
})
for (const path of ['/', '/audit', '/platform']) {
test(`axe finds no serious or critical issues on ${path}`, async ({ page }) => {
await page.goto(path)
const results = await new AxeBuilder({ page }).analyze()
const blocking = results.violations
.filter((violation) => violation.impact === 'serious' || violation.impact === 'critical')
.map((violation) => `${violation.id} (${violation.nodes.length} node(s))`)
expect(blocking).toEqual([])
})
}
test('the dashboard meets the Core Web Vitals budgets: LCP at most 2.5 s, INP at most 200 ms', async ({ page }) => {
await page.addInitScript(() => {
const vitals = window as unknown as { lcp: number; inp: number }
vitals.lcp = 0
vitals.inp = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) vitals.lcp = entry.startTime
}).observe({ type: 'largest-contentful-paint', buffered: true })
// Event Timing entries carry the interaction id and the duration INP is built from.
new PerformanceObserver((list) => {
for (const entry of list.getEntries() as Array<PerformanceEntry & { interactionId?: number }>) {
if ((entry.interactionId ?? 0) > 0) vitals.inp = Math.max(vitals.inp, entry.duration)
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 } as PerformanceObserverInit)
})
await page.goto('/', { waitUntil: 'load' })
await page.getByLabel('Title').fill(uniqueTitle('vitals'))
await page.getByLabel('Body').fill('an interaction for the input delay budget')
await page.getByRole('button', { name: 'Create note' }).click()
await expect(page.getByText('an interaction for the input delay budget')).toBeVisible()
const vitals = await page.evaluate(() => {
const measured = window as unknown as { lcp: number; inp: number }
return { lcp: measured.lcp, inp: measured.inp }
})
expect(vitals.lcp).toBeGreaterThan(0)
expect(vitals.lcp).toBeLessThanOrEqual(2500)
expect(vitals.inp).toBeLessThanOrEqual(200)
})
+10
View File
@@ -0,0 +1,10 @@
import nextVitals from 'eslint-config-next/core-web-vitals'
import nextTypescript from 'eslint-config-next/typescript'
const config = [
{ ignores: ['.next/**', 'node_modules/**', 'drizzle/**'] },
...nextVitals,
...nextTypescript,
]
export default config
+5
View File
@@ -0,0 +1,5 @@
// Runs in the browser before the app. On the platform the page's own origin is
// the collector, so no configuration is needed here.
import { instrumentationClient } from '@conusai-sdk/observe-next'
instrumentationClient()
+2
View File
@@ -0,0 +1,2 @@
// Next calls this once per server process. The Node error client starts here (plan §4.4).
export { register } from '@conusai-sdk/observe-next'
+16
View File
@@ -0,0 +1,16 @@
// Applies the SQL migrations in drizzle/ before the app starts (plan D15).
// `npm start` runs it first, so a deploy never serves against an old schema.
import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres'
const url = process.env.POSTGRES_URL
if (!url) {
console.error('POSTGRES_URL is required to apply migrations')
process.exit(1)
}
const client = postgres(url, { max: 1 })
await migrate(drizzle(client), { migrationsFolder: 'drizzle' })
await client.end()
console.log('migrations applied')
+8
View File
@@ -0,0 +1,8 @@
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactStrictMode: true,
poweredByHeader: false,
}
export default nextConfig
+10013
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
{
"name": "conusai-showcase",
"version": "0.1.0",
"private": true,
"description": "ConusAI Showcase: notes with attachments on Postgres and Blob Storage, every ConusAI SDK in one Next.js app.",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "node migrate.mjs && next start",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"steiger": "steiger src",
"test": "vitest run",
"e2e": "playwright test",
"migrate": "node migrate.mjs",
"db:generate": "drizzle-kit generate"
},
"dependencies": {
"@aws-sdk/client-s3": "3.1146.0",
"drizzle-orm": "0.45.3",
"nats": "2.29.3",
"next": "16.3.8",
"postgres": "3.4.9",
"react": "19.3.0",
"react-dom": "19.3.0",
"server-only": "0.0.1",
"zod": "4.6.5"
},
"devDependencies": {
"@axe-core/playwright": "4.13.0",
"@eslint/eslintrc": "3.3.7",
"@feature-sliced/steiger-plugin": "0.8.0",
"@playwright/test": "1.63.0",
"@types/node": "26.6.4",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"drizzle-kit": "0.31.11",
"eslint": "9.39.5",
"eslint-config-next": "16.3.8",
"steiger": "0.7.0",
"typescript": "5.9.3",
"vitest": "5.0.3"
}
}
+48
View File
@@ -0,0 +1,48 @@
import { defineConfig } from '@playwright/test'
// The app runs against the fake platform in e2e/fake-platform.mjs. The optional
// module variables are withheld, so the dashboard shows every enable card (plan D13).
const platform = 'http://127.0.0.1:4010'
// The variables of the optional modules, withheld from the app under test.
const optional = new Set([
'NATS_URL',
'NATS_TOKEN',
'QDRANT_URL',
'QDRANT_API_KEY',
'REDIS_URL',
'GIT_URL',
'AWS_ENDPOINT_URL',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
])
const base = Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined && !optional.has(entry[0])),
)
export default defineConfig({
testDir: 'e2e',
workers: 1,
reporter: [['list']],
use: { baseURL: 'http://localhost:3000' },
webServer: [
{
command: 'node e2e/fake-platform.mjs',
url: `${platform}/__e2e/health`,
reuseExistingServer: false,
},
{
command: 'npm run start',
url: 'http://localhost:3000/api/health',
reuseExistingServer: false,
timeout: 120_000,
env: {
...base,
CONUSAI_API_URL: `${platform}/api`,
CONUSAI_API_TOKEN: 'e2e-deployment-token',
CONUSAI_PROJECT_ID: 'e2e',
CONUSAI_DSN: "http://e2e@127.0.0.1:4010/1",
CONUSAI_RELEASE: 'e2e',
},
},
],
})
+19
View File
@@ -0,0 +1,19 @@
import type { Metadata } from 'next'
import type { ReactNode } from 'react'
import { Observe } from '@/app/observability/observe'
/** The document title every page inherits (axe: document-title). */
export const metadata: Metadata = {
title: 'ConusAI Showcase',
description: 'Notes, attachments, audit and usage on the ConusAI platform',
}
export function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Observe>{children}</Observe>
</body>
</html>
)
}
+13
View File
@@ -0,0 +1,13 @@
'use client'
import type { ReactNode } from 'react'
import { ErrorBoundary, ObserveProvider } from '@conusai-sdk/observe-react'
/** The browser observability root: the SDK instance, and a boundary that reports what React catches. */
export function Observe({ children }: { children: ReactNode }) {
return (
<ObserveProvider>
<ErrorBoundary fallback={<p>Something went wrong. The error has been reported.</p>}>{children}</ErrorBoundary>
</ObserveProvider>
)
}
+40
View File
@@ -0,0 +1,40 @@
import { and, eq } from 'drizzle-orm'
import type { drizzle } from 'drizzle-orm/postgres-js'
import { files, type FileRow } from '@/entities/file/model/schema'
/** The handle the shared `db()` returns (`@/shared/api`). */
type Db = ReturnType<typeof drizzle>
export type NewFile = Omit<FileRow, 'id' | 'createdAt'>
/**
* Attachment persistence. Writes name their author, as the notes repository does:
* a visitor removes only the files they attached.
*/
export function fileRepository(db: Db) {
return {
async create(input: NewFile): Promise<FileRow> {
const [row] = await db.insert(files).values(input).returning()
if (!row) throw new Error('the insert returned no row')
return row
},
async listByNote(noteId: string): Promise<FileRow[]> {
return db.select().from(files).where(eq(files.noteId, noteId))
},
/** Every attachment the visitor made, across their notes. */
async listByAuthor(authorId: string): Promise<FileRow[]> {
return db.select().from(files).where(eq(files.authorId, authorId))
},
/** The removed file, or `undefined` when `authorId` did not attach it. */
async remove(id: string, authorId: string): Promise<FileRow | undefined> {
const [row] = await db
.delete(files)
.where(and(eq(files.id, id), eq(files.authorId, authorId)))
.returning()
return row
},
}
}
+3
View File
@@ -0,0 +1,3 @@
export { files, type FileRow } from './model/schema'
export { MAX_UPLOAD_BYTES, ALLOWED_CONTENT_TYPES, uploadLimits, type UploadCandidate } from './model/limits'
export { fileRepository, type NewFile } from './api/repository'
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { MAX_UPLOAD_BYTES, uploadLimits } from '@/entities/file'
describe('uploadLimits', () => {
const ok = { name: 'plan.pdf', size: 1024, contentType: 'application/pdf' }
it('accepts an allowed type within the size limit', () => {
expect(uploadLimits.parse(ok)).toEqual(ok)
})
it('refuses a file over the size limit', () => {
expect(() => uploadLimits.parse({ ...ok, size: MAX_UPLOAD_BYTES + 1 })).toThrow(/5 MiB/)
})
it('refuses an empty file', () => {
expect(() => uploadLimits.parse({ ...ok, size: 0 })).toThrow(/empty/)
})
it('refuses a content type that is not on the list', () => {
expect(() => uploadLimits.parse({ ...ok, contentType: 'application/x-msdownload' })).toThrow(/not accepted/)
})
})
+22
View File
@@ -0,0 +1,22 @@
import { z } from 'zod'
/** The largest attachment accepted, in bytes (5 MiB). */
export const MAX_UPLOAD_BYTES = 5 * 1024 * 1024
/** The content types an attachment may have. Anything else is refused before upload. */
export const ALLOWED_CONTENT_TYPES = [
'image/png',
'image/jpeg',
'image/gif',
'application/pdf',
'text/plain',
'text/markdown',
] as const
export const uploadLimits = z.object({
name: z.string().trim().min(1, 'A file needs a name').max(255),
size: z.number().int().min(1, 'The file is empty').max(MAX_UPLOAD_BYTES, 'The file is larger than 5 MiB'),
contentType: z.enum(ALLOWED_CONTENT_TYPES, { error: 'That file type is not accepted' }),
})
export type UploadCandidate = z.infer<typeof uploadLimits>
+19
View File
@@ -0,0 +1,19 @@
import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
/**
* An attachment on a note, stored in Blob Storage. `note_id` is a foreign key to
* notes(id) with ON DELETE CASCADE, added by the migration: an entity cannot
* import another entity's table (FSD), so the schema here carries the column only.
*/
export const files = pgTable('files', {
id: uuid('id').primaryKey().defaultRandom(),
noteId: uuid('note_id').notNull(),
authorId: text('author_id').notNull(),
name: text('name').notNull(),
contentType: text('content_type').notNull(),
size: integer('size').notNull(),
url: text('url').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export type FileRow = typeof files.$inferSelect
+68
View File
@@ -0,0 +1,68 @@
import { randomUUID } from 'node:crypto'
import { drizzle } from 'drizzle-orm/postgres-js'
import { migrate } from 'drizzle-orm/postgres-js/migrator'
import postgres from 'postgres'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { noteRepository, type NoteDb } from '@/entities/note'
// Runs against a real Postgres: POSTGRES_URL must name one. Each run migrates a
// schema of its own and drops it afterwards, so runs never share rows.
const url = process.env.POSTGRES_URL
const schema = `note_test_${randomUUID().replaceAll('-', '')}`
let admin: postgres.Sql
let client: postgres.Sql
let repo: ReturnType<typeof noteRepository>
const ALICE = 'visitor-alice'
const BOB = 'visitor-bob'
beforeAll(async () => {
expect(url, 'POSTGRES_URL must name a test Postgres').toBeTruthy()
admin = postgres(url as string, { max: 1 })
await admin.unsafe(`create schema "${schema}"`)
client = postgres(url as string, { max: 1, connection: { search_path: schema } })
const db = drizzle(client) as NoteDb
await migrate(db, { migrationsFolder: 'drizzle', migrationsSchema: schema })
repo = noteRepository(db)
})
afterAll(async () => {
await client?.end()
await admin?.unsafe(`drop schema if exists "${schema}" cascade`)
await admin?.end()
})
describe('noteRepository', () => {
it('creates a note and reads it back with its author', async () => {
const created = await repo.create({ title: 'Plan', body: 'draft', authorId: ALICE })
const read = await repo.get(created.id)
expect(read).toMatchObject({ title: 'Plan', body: 'draft', authorId: ALICE })
})
it('lists only the notes of the asked-for author', async () => {
await repo.create({ title: 'Bob note', body: '', authorId: BOB })
const aliceNotes = await repo.listByAuthor(ALICE)
expect(aliceNotes.length).toBeGreaterThan(0)
expect(aliceNotes.every((note) => note.authorId === ALICE)).toBe(true)
})
it('lets the author update their own note', async () => {
const created = await repo.create({ title: 'Old', body: '', authorId: ALICE })
const updated = await repo.update(created.id, ALICE, { title: 'New', body: 'edited' })
expect(updated).toMatchObject({ id: created.id, title: 'New', body: 'edited' })
})
it('does not let another visitor update the note', async () => {
const created = await repo.create({ title: 'Mine', body: '', authorId: ALICE })
const attempt = await repo.update(created.id, BOB, { title: 'Hijacked', body: '' })
expect(attempt).toBeUndefined()
expect((await repo.get(created.id))?.title).toBe('Mine')
})
it('removes only for the author', async () => {
const created = await repo.create({ title: 'Doomed', body: '', authorId: ALICE })
expect(await repo.remove(created.id, BOB)).toBe(false)
expect(await repo.remove(created.id, ALICE)).toBe(true)
expect(await repo.get(created.id)).toBeUndefined()
})
})
+59
View File
@@ -0,0 +1,59 @@
import { and, desc, eq } from 'drizzle-orm'
import type { drizzle } from 'drizzle-orm/postgres-js'
import { notes, type Note } from '@/entities/note/model/schema'
import type { NoteInput } from '@/entities/note/model/input'
/** The handle the shared `db()` returns (`@/shared/api/db/server`). */
export type NoteDb = ReturnType<typeof drizzle>
/**
* Note persistence over one database handle. The handle is a parameter so a test
* can run the same code against its own Postgres.
*
* Every write names its author in the `where`: a visitor can change only the
* notes they wrote (tenancy, D16).
*/
export function noteRepository(db: NoteDb) {
return {
async create(input: NoteInput & { authorId: string }): Promise<Note> {
const [row] = await db.insert(notes).values(input).returning()
if (!row) throw new Error('the insert returned no row')
return row
},
async get(id: string): Promise<Note | undefined> {
const [row] = await db.select().from(notes).where(eq(notes.id, id)).limit(1)
return row
},
async listByAuthor(authorId: string, limit = 50): Promise<Note[]> {
return db
.select()
.from(notes)
.where(eq(notes.authorId, authorId))
.orderBy(desc(notes.createdAt))
.limit(limit)
},
/** The updated note, or `undefined` when `authorId` did not write it. */
async update(id: string, authorId: string, input: NoteInput): Promise<Note | undefined> {
const [row] = await db
.update(notes)
.set({ ...input, updatedAt: new Date() })
.where(and(eq(notes.id, id), eq(notes.authorId, authorId)))
.returning()
return row
},
/** `true` when the note existed and `authorId` wrote it. */
async remove(id: string, authorId: string): Promise<boolean> {
const rows = await db
.delete(notes)
.where(and(eq(notes.id, id), eq(notes.authorId, authorId)))
.returning({ id: notes.id })
return rows.length > 0
},
}
}
export type NoteRepository = ReturnType<typeof noteRepository>
+3
View File
@@ -0,0 +1,3 @@
export { notes, type Note } from './model/schema'
export { noteInput, noteId, type NoteInput } from './model/input'
export { noteRepository, type NoteDb, type NoteRepository } from './api/repository'
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { noteId, noteInput } from '@/entities/note'
describe('noteInput', () => {
it('trims the title and defaults the body', () => {
expect(noteInput.parse({ title: ' Plan ' })).toEqual({ title: 'Plan', body: '' })
})
it('refuses a blank title', () => {
expect(() => noteInput.parse({ title: ' ' })).toThrow(/title/)
})
it('refuses a title past 200 characters', () => {
expect(() => noteInput.parse({ title: 'x'.repeat(201) })).toThrow()
})
})
describe('noteId', () => {
it('accepts a UUID and refuses anything else', () => {
expect(noteId.parse('00000000-0000-4000-8000-000000000001')).toBe('00000000-0000-4000-8000-000000000001')
expect(() => noteId.parse('not-a-uuid')).toThrow()
})
})
+12
View File
@@ -0,0 +1,12 @@
import { z } from 'zod'
/** The fields a visitor writes. Parsed at the boundary, before any write (plan §4.4). */
export const noteInput = z.object({
title: z.string().trim().min(1, 'A note needs a title').max(200),
body: z.string().max(20000).default(''),
})
export type NoteInput = z.infer<typeof noteInput>
/** A note id as it arrives in a form or a URL: a UUID, nothing else. */
export const noteId = z.uuid()
+13
View File
@@ -0,0 +1,13 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
/** A note. `author_id` is the visitor's id from the signed `sc_vid` cookie (D16). */
export const notes = pgTable('notes', {
id: uuid('id').primaryKey().defaultRandom(),
title: text('title').notNull(),
body: text('body').notNull().default(''),
authorId: text('author_id').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
})
export type Note = typeof notes.$inferSelect
@@ -0,0 +1,71 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// The action is the unit under test: its collaborators are replaced, so each
// test asserts what the action wrote and audited, and nothing else.
const mocks = vi.hoisted(() => {
const row = { id: '00000000-0000-4000-8000-000000000001', title: 'Plan', body: '', authorId: 'visitor-1' }
const values = vi.fn(() => ({ returning: async () => [row] }))
const insert = vi.fn(() => ({ values }))
return {
row,
values,
insert,
log: vi.fn(async () => {}),
publishNoteCreated: vi.fn(async () => {}),
indexNote: vi.fn(async () => {}),
revalidatePath: vi.fn(),
}
})
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({
db: () => ({ insert: mocks.insert }),
audit: () => ({ log: mocks.log }),
serverErrors: () => ({ captureException: vi.fn() }),
}))
vi.mock('@/shared/lib/optional', () => ({
publishNoteCreated: mocks.publishNoteCreated,
indexNote: mocks.indexNote,
}))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { createNote } from '@/features/create-note/api/actions'
function form(fields: Record<string, string>): FormData {
const data = new FormData()
for (const [key, value] of Object.entries(fields)) data.set(key, value)
return data
}
describe('createNote', () => {
beforeEach(() => {
mocks.values.mockClear()
mocks.insert.mockClear()
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
})
it('writes the note under the visitor and audits exactly one note.created', async () => {
await createNote(form({ title: 'Plan', body: '' }))
expect(mocks.values).toHaveBeenCalledWith({ title: 'Plan', body: '', authorId: 'visitor-1' })
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('note.created', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: mocks.row.id },
})
expect(mocks.revalidatePath).toHaveBeenCalledWith('/')
})
it('refuses an invalid title before any write or audit', async () => {
await expect(createNote(form({ title: ' ' }))).rejects.toThrow()
expect(mocks.insert).not.toHaveBeenCalled()
expect(mocks.log).not.toHaveBeenCalled()
})
it('announces and indexes the new note when the optional modules are linked', async () => {
await createNote(form({ title: 'Plan', body: 'draft' }))
expect(mocks.publishNoteCreated).toHaveBeenCalledWith({ id: mocks.row.id, title: 'Plan' })
expect(mocks.indexNote).toHaveBeenCalledWith({ id: mocks.row.id, title: 'Plan', body: '' })
})
})
+22
View File
@@ -0,0 +1,22 @@
'use server'
import { revalidatePath } from 'next/cache'
import { noteInput, noteRepository } from '@/entities/note'
import { audit, db, serverErrors } from '@/shared/api'
import { indexNote, publishNoteCreated } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Parse, write, audit, revalidate (plan §4.4): the shape every mutation follows. */
export async function createNote(form: FormData): Promise<void> {
const input = noteInput.parse({ title: form.get('title'), body: form.get('body') ?? '' })
const actor = await ensureVisitor()
const note = await noteRepository(db()).create({ ...input, authorId: actor })
await audit().log('note.created', { actor: { kind: 'human', id: actor }, target: { id: note.id } })
// Optional modules: NATS hears of the note and Qdrant indexes it when linked. A failure is
// reported, not raised: the note is saved.
await Promise.all([
publishNoteCreated({ id: note.id, title: note.title }),
indexNote({ id: note.id, title: note.title, body: note.body }),
]).catch((error: unknown) => serverErrors().captureException(error))
revalidatePath('/')
}
+2
View File
@@ -0,0 +1,2 @@
export { createNote } from './api/actions'
export { CreateNoteForm } from './ui/CreateNoteForm'
@@ -0,0 +1,17 @@
import { createNote } from '@/features/create-note/api/actions'
export function CreateNoteForm() {
return (
<form action={createNote}>
<label>
Title
<input name="title" required maxLength={200} />
</label>
<label>
Body
<textarea name="body" rows={4} />
</label>
<button type="submit">Create note</button>
</form>
)
}
@@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const FILE = '00000000-0000-4000-8000-0000000000ff'
const mocks = vi.hoisted(() => ({
del: vi.fn(async () => {}),
log: vi.fn(async () => {}),
revalidatePath: vi.fn(),
removed: [] as Array<{ id: string; url: string }>,
}))
const fakeDb = {
delete: () => ({
where: () => ({
returning: async () => mocks.removed,
}),
}),
}
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({
db: () => fakeDb,
audit: () => ({ log: mocks.log }),
blob: () => ({ del: mocks.del }),
}))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { deleteFile } from '@/features/delete-file/api/actions'
describe('deleteFile', () => {
beforeEach(() => {
mocks.del.mockClear()
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.removed.length = 0
})
it('removes the stored bytes and audits file.deleted once', async () => {
mocks.removed.push({ id: FILE, url: 'https://blob.example.com/notes/n/a.png' })
await deleteFile(FILE)
expect(mocks.del).toHaveBeenCalledWith('https://blob.example.com/notes/n/a.png')
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('file.deleted', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: FILE },
})
})
it('touches nothing when the visitor did not attach the file', async () => {
await deleteFile(FILE)
expect(mocks.del).not.toHaveBeenCalled()
expect(mocks.log).not.toHaveBeenCalled()
})
})
+18
View File
@@ -0,0 +1,18 @@
'use server'
import { revalidatePath } from 'next/cache'
import { fileRepository } from '@/entities/file'
import { noteId } from '@/entities/note'
import { audit, blob, db } from '@/shared/api'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Remove an attachment the visitor attached: its row first, then its stored bytes. */
export async function deleteFile(id: string): Promise<void> {
const key = noteId.parse(id)
const actor = await ensureVisitor()
const removed = await fileRepository(db()).remove(key, actor)
if (!removed) return
await blob().del(removed.url)
await audit().log('file.deleted', { actor: { kind: 'human', id: actor }, target: { id: key } })
revalidatePath('/')
}
+2
View File
@@ -0,0 +1,2 @@
export { deleteFile } from './api/actions'
export { DeleteFileButton } from './ui/DeleteFileButton'
@@ -0,0 +1,9 @@
import { deleteFile } from '@/features/delete-file/api/actions'
export function DeleteFileButton({ id }: { id: string }) {
return (
<form action={deleteFile.bind(null, id)}>
<button type="submit">Remove attachment</button>
</form>
)
}
@@ -0,0 +1,42 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const NOTE = '00000000-0000-4000-8000-0000000000aa'
const mocks = vi.hoisted(() => ({
log: vi.fn(async () => {}),
revalidatePath: vi.fn(),
removed: [] as Array<{ id: string }>,
}))
const fakeDb = {
delete: () => ({ where: () => ({ returning: async () => mocks.removed }) }),
}
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({ db: () => fakeDb, audit: () => ({ log: mocks.log }) }))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { deleteNote } from '@/features/delete-note/api/actions'
describe('deleteNote', () => {
beforeEach(() => {
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.removed.length = 0
})
it('audits note.deleted exactly once with the actor and the note', async () => {
mocks.removed.push({ id: NOTE })
await deleteNote(NOTE)
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('note.deleted', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: NOTE },
})
})
it('audits nothing when the visitor did not write the note', async () => {
await deleteNote(NOTE)
expect(mocks.log).not.toHaveBeenCalled()
})
})
+17
View File
@@ -0,0 +1,17 @@
'use server'
import { revalidatePath } from 'next/cache'
import { noteId, noteRepository } from '@/entities/note'
import { audit } from '@/shared/api'
import { db } from '@/shared/api'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Delete a note the visitor wrote. A note they did not write is left untouched. */
export async function deleteNote(id: string): Promise<void> {
const key = noteId.parse(id)
const actor = await ensureVisitor()
const removed = await noteRepository(db()).remove(key, actor)
if (!removed) return
await audit().log('note.deleted', { actor: { kind: 'human', id: actor }, target: { id: key } })
revalidatePath('/')
}
+2
View File
@@ -0,0 +1,2 @@
export { deleteNote } from './api/actions'
export { DeleteNoteButton } from './ui/DeleteNoteButton'
@@ -0,0 +1,9 @@
import { deleteNote } from '@/features/delete-note/api/actions'
export function DeleteNoteButton({ id }: { id: string }) {
return (
<form action={deleteNote.bind(null, id)}>
<button type="submit">Delete</button>
</form>
)
}
@@ -0,0 +1,53 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const NOTE = '00000000-0000-4000-8000-0000000000aa'
const mocks = vi.hoisted(() => ({
log: vi.fn(async () => {}),
revalidatePath: vi.fn(),
updated: undefined as { id: string } | undefined,
}))
const fakeDb = {
update: () => ({
set: () => ({
where: () => ({ returning: async () => (mocks.updated ? [mocks.updated] : []) }),
}),
}),
}
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({ db: () => fakeDb, audit: () => ({ log: mocks.log }) }))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { updateNote } from '@/features/edit-note/api/actions'
function edit(title: string): FormData {
const data = new FormData()
data.set('title', title)
data.set('body', '')
return data
}
describe('updateNote', () => {
beforeEach(() => {
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.updated = undefined
})
it('audits note.updated exactly once with the actor and the note', async () => {
mocks.updated = { id: NOTE }
await updateNote(NOTE, edit('Renamed'))
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('note.updated', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: NOTE },
})
})
it('audits nothing when the visitor did not write the note', async () => {
await updateNote(NOTE, edit('Renamed'))
expect(mocks.log).not.toHaveBeenCalled()
})
})
+18
View File
@@ -0,0 +1,18 @@
'use server'
import { revalidatePath } from 'next/cache'
import { noteId, noteInput, noteRepository } from '@/entities/note'
import { audit } from '@/shared/api'
import { db } from '@/shared/api'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Edit a note the visitor wrote. A note they did not write is left untouched. */
export async function updateNote(id: string, form: FormData): Promise<void> {
const key = noteId.parse(id)
const input = noteInput.parse({ title: form.get('title'), body: form.get('body') ?? '' })
const actor = await ensureVisitor()
const note = await noteRepository(db()).update(key, actor, input)
if (!note) return
await audit().log('note.updated', { actor: { kind: 'human', id: actor }, target: { id: note.id } })
revalidatePath('/')
}
+2
View File
@@ -0,0 +1,2 @@
export { updateNote } from './api/actions'
export { EditNoteForm } from './ui/EditNoteForm'
@@ -0,0 +1,18 @@
import type { Note } from '@/entities/note'
import { updateNote } from '@/features/edit-note/api/actions'
export function EditNoteForm({ note }: { note: Note }) {
return (
<form action={updateNote.bind(null, note.id)}>
<label>
Title
<input name="title" defaultValue={note.title} required maxLength={200} />
</label>
<label>
Body
<textarea name="body" rows={3} defaultValue={note.body} />
</label>
<button type="submit">Save</button>
</form>
)
}
@@ -0,0 +1,28 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
exportToS3: vi.fn(async () => 'exports/visitor-1/now.json' as string | null),
notes: [{ id: 'n-1', title: 'Plan' }],
}))
vi.mock('@/shared/api', () => ({
db: () => ({
select: () => ({ from: () => ({ where: () => ({ orderBy: () => ({ limit: async () => mocks.notes }) }) }) }),
}),
}))
vi.mock('@/shared/lib/optional', () => ({ exportToS3: mocks.exportToS3 }))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
import { exportNotes } from '@/features/export-notes/api/actions'
describe('exportNotes', () => {
beforeEach(() => {
mocks.exportToS3.mockClear()
})
it('writes the visitor’s own notes to S3 once', async () => {
await exportNotes()
expect(mocks.exportToS3).toHaveBeenCalledTimes(1)
expect(mocks.exportToS3).toHaveBeenCalledWith('visitor-1', mocks.notes)
})
})
+13
View File
@@ -0,0 +1,13 @@
'use server'
import { noteRepository } from '@/entities/note'
import { db } from '@/shared/api'
import { exportToS3 } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/** Writes the visitor's notes to the S3 bucket as one JSON object (S3 module). */
export async function exportNotes(): Promise<void> {
const actor = await ensureVisitor()
const notes = await noteRepository(db()).listByAuthor(actor)
await exportToS3(actor, notes)
}
+2
View File
@@ -0,0 +1,2 @@
export { exportNotes } from './api/actions'
export { ExportNotesButton } from './ui/ExportNotesButton'
@@ -0,0 +1,9 @@
import { exportNotes } from '@/features/export-notes/api/actions'
export function ExportNotesButton() {
return (
<form action={exportNotes}>
<button type="submit">Export my notes to S3</button>
</form>
)
}
@@ -0,0 +1,43 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const publishNoteCreated = vi.fn<(note: { id: string; title: string }) => Promise<void>>(async () => undefined)
const log = vi.fn<(...args: unknown[]) => Promise<void>>(async () => undefined)
const get = vi.fn<(id: string) => Promise<{ id: string; title: string; authorId: string } | undefined>>()
let visitor = 'visitor-a'
vi.mock('@/shared/lib/optional', () => ({ publishNoteCreated: (note: { id: string; title: string }) => publishNoteCreated(note) }))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => visitor }))
vi.mock('@/entities/note', () => ({ noteRepository: () => ({ get }) }))
vi.mock('@/shared/api', () => ({ db: () => ({}), audit: () => ({ log }) }))
const { announceNote } = await import('@/features/publish-note-event/api/actions')
describe('announceNote', () => {
beforeEach(() => {
publishNoteCreated.mockClear()
log.mockClear()
get.mockReset()
visitor = 'visitor-a'
})
it('publishes the author\'s note once and audits the announcement', async () => {
get.mockResolvedValue({ id: 'note-1', title: 'Budget', authorId: 'visitor-a' })
await announceNote('note-1')
expect(publishNoteCreated).toHaveBeenCalledTimes(1)
expect(publishNoteCreated).toHaveBeenCalledWith({ id: 'note-1', title: 'Budget' })
expect(log).toHaveBeenCalledWith('note.announced', expect.objectContaining({ target: { id: 'note-1' } }))
})
it('refuses another visitor\'s note and publishes nothing', async () => {
get.mockResolvedValue({ id: 'note-1', title: 'Budget', authorId: 'visitor-b' })
await expect(announceNote('note-1')).rejects.toThrow('only the author')
expect(publishNoteCreated).not.toHaveBeenCalled()
expect(log).not.toHaveBeenCalled()
})
it('refuses a note that does not exist', async () => {
get.mockResolvedValue(undefined)
await expect(announceNote('missing')).rejects.toThrow('only the author')
expect(publishNoteCreated).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,24 @@
'use server'
import { noteRepository } from '@/entities/note'
import { audit, db } from '@/shared/api'
import { publishNoteCreated } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/**
* Announces one of the visitor's own notes on NATS again, as a `note.created`
* event (plan §4.4). Only the author may announce a note; anyone else gets a refusal
* and no event. A no-op when NATS is not linked.
*/
export async function announceNote(noteId: string): Promise<void> {
const actor = await ensureVisitor()
const note = await noteRepository(db()).get(noteId)
if (!note || note.authorId !== actor) throw new Error('only the author can announce this note')
await publishNoteCreated({ id: note.id, title: note.title })
await audit().log('note.announced', { actor: { kind: 'human', id: actor }, target: { id: note.id } })
}
/** The form action behind the button: the note id travels as a hidden field. */
export async function announceFromForm(form: FormData): Promise<void> {
await announceNote(String(form.get('id') ?? ''))
}
+2
View File
@@ -0,0 +1,2 @@
export { AnnounceNoteButton } from './ui/AnnounceNoteButton'
export { announceNote } from './api/actions'
@@ -0,0 +1,11 @@
import { announceFromForm } from '@/features/publish-note-event/api/actions'
/** "Announce" on one of the visitor's notes: publishes its `note.created` event again. */
export function AnnounceNoteButton({ id }: { id: string }) {
return (
<form action={announceFromForm}>
<input type="hidden" name="id" value={id} />
<button type="submit">Announce</button>
</form>
)
}
+1
View File
@@ -0,0 +1 @@
export { UploadsLeft } from './ui/UploadsLeft'
@@ -0,0 +1,12 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/shared/lib/optional', () => ({ uploadsLeft: vi.fn(async () => 3) }))
vi.mock('@/shared/lib/visitor-session', () => ({ currentVisitor: vi.fn(async () => 'visitor-a') }))
describe('UploadsLeft', () => {
it('renders the uploads the visitor has left this minute', async () => {
const { UploadsLeft } = await import('@/features/rate-limit/ui/UploadsLeft')
expect(renderToStaticMarkup(await UploadsLeft())).toContain('3 uploads left this minute')
})
})
@@ -0,0 +1,9 @@
import { uploadsLeft } from '@/shared/lib/optional'
import { currentVisitor } from '@/shared/lib/visitor-session'
/** How many uploads the visitor has left this minute (KV module). */
export async function UploadsLeft() {
const visitor = await currentVisitor()
const left = visitor ? await uploadsLeft(visitor) : null
return <p>{left ?? 0} uploads left this minute</p>
}
+1
View File
@@ -0,0 +1 @@
export { SearchNotes } from './ui/SearchNotes'
@@ -0,0 +1,26 @@
import { similarNotes } from '@/shared/lib/optional'
/** Similar-notes search (Qdrant module): a GET form, so a search is a plain link. */
export async function SearchNotes({ query }: { query: string | undefined }) {
const hits: Awaited<ReturnType<typeof similarNotes>> = query ? await similarNotes(query) : []
return (
<section>
<form method="get">
<label>
Find similar notes
<input name="q" defaultValue={query ?? ''} />
</label>
<button type="submit">Search</button>
</form>
{hits && hits.length > 0 ? (
<ol>
{hits.map((hit) => (
<li key={hit.id}>
{hit.payload.title} ({hit.score.toFixed(2)})
</li>
))}
</ol>
) : null}
</section>
)
}
+2
View File
@@ -0,0 +1,2 @@
export { SimulateTrafficButton } from './ui/SimulateTrafficButton'
export { runJourney } from './model/journey'
@@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { createTestObserve } from '@conusai-sdk/observe-testing'
import { runJourney } from '@/features/simulate-traffic/model/journey'
describe('runJourney', () => {
it('sends the page view, the custom event and the captured error', async () => {
const observe = createTestObserve({ modules: { analytics: true } })
runJourney(observe)
await observe.flush()
const kinds = observe.mock.sent.map(({ item }) => item.kind)
// Custom events travel under their app. name; a captured error is kind `error`.
expect(kinds).toContain('app.page_view')
expect(kinds).toContain('app.journey_started')
expect(kinds).toContain('error')
const error = observe.mock.sent.find(({ item }) => item.kind === 'error')?.item
expect(JSON.stringify(error)).toContain('simulated journey failure')
})
})
@@ -0,0 +1,8 @@
import type { Observe } from '@conusai-sdk/observe'
/** What one simulated journey reports: a page view, a custom event and a captured error. */
export function runJourney(observe: Pick<Observe, 'track' | 'captureException'>): void {
observe.track('page_view', { path: '/' })
observe.track('journey_started', { steps: 3 })
observe.captureException(new Error('simulated journey failure'), { tags: { journey: 'simulate-traffic' } })
}
@@ -0,0 +1,14 @@
'use client'
import { useObserve } from '@conusai-sdk/observe-react'
import { runJourney } from '@/features/simulate-traffic/model/journey'
/** Runs one simulated journey, so a visitor sees the signals land in the console. */
export function SimulateTrafficButton() {
const observe = useObserve()
return (
<button type="button" onClick={() => runJourney(observe)}>
Simulate a user journey
</button>
)
}
@@ -0,0 +1,124 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// The action runs against fakes: a Blob Storage client that records what it was
// given, and a database whose note and attachment rows are set per test.
const NOTE = '00000000-0000-4000-8000-0000000000aa'
const mocks = vi.hoisted(() => ({
put: vi.fn(async (pathname: string) => ({ url: `https://blob.example.com/${pathname}` })),
log: vi.fn(async () => {}),
recordUsage: vi.fn(async () => ({ accepted: 1, duplicates: 0, rejected: [] })),
consumeUploadQuota: vi.fn(async () => null as { allowed: boolean; remaining: number } | null),
revalidatePath: vi.fn(),
note: undefined as { id: string; authorId: string } | undefined,
inserted: [] as Record<string, unknown>[],
}))
const fakeDb = {
select: () => ({ from: () => ({ where: () => ({ limit: async () => (mocks.note ? [mocks.note] : []) }) }) }),
insert: () => ({
values: (row: Record<string, unknown>) => ({
returning: async () => {
mocks.inserted.push(row)
return [{ id: '00000000-0000-4000-8000-0000000000ff', ...row, createdAt: new Date() }]
},
}),
}),
}
vi.mock('next/cache', () => ({ revalidatePath: mocks.revalidatePath }))
vi.mock('@/shared/api', () => ({
db: () => fakeDb,
audit: () => ({ log: mocks.log }),
blob: () => ({ put: mocks.put }),
recordUsage: mocks.recordUsage,
serverErrors: () => ({ captureException: vi.fn() }),
}))
vi.mock('@/shared/config', () => ({
getEnv: () => ({ CONUSAI_API_URL: 'https://console.example.com', CONUSAI_API_TOKEN: 'dt_fixture' }),
}))
vi.mock('@/shared/lib/visitor-session', () => ({ ensureVisitor: async () => 'visitor-1' }))
vi.mock('@/shared/lib/optional', () => ({ consumeUploadQuota: mocks.consumeUploadQuota }))
import { uploadFile } from '@/features/upload-file/api/actions'
function attach(file: File): FormData {
const data = new FormData()
data.set('file', file)
return data
}
describe('uploadFile', () => {
beforeEach(() => {
mocks.put.mockClear()
mocks.log.mockClear()
mocks.revalidatePath.mockClear()
mocks.consumeUploadQuota.mockReset()
mocks.consumeUploadQuota.mockResolvedValue(null)
mocks.inserted.length = 0
mocks.note = { id: NOTE, authorId: 'visitor-1' }
})
it('refuses a file over the size limit before any upload', async () => {
const big = new File([new Uint8Array(5 * 1024 * 1024 + 1)], 'big.png', { type: 'image/png' })
await expect(uploadFile(NOTE, attach(big))).rejects.toThrow(/5 MiB/)
expect(mocks.put).not.toHaveBeenCalled()
expect(mocks.log).not.toHaveBeenCalled()
})
it('refuses a disallowed type before any upload', async () => {
const exe = new File(['MZ'], 'run.exe', { type: 'application/x-msdownload' })
await expect(uploadFile(NOTE, attach(exe))).rejects.toThrow(/not accepted/)
expect(mocks.put).not.toHaveBeenCalled()
})
it('refuses a note the visitor did not write, without uploading', async () => {
mocks.note = { id: NOTE, authorId: 'visitor-2' }
const png = new File(['png'], 'a.png', { type: 'image/png' })
await expect(uploadFile(NOTE, attach(png))).rejects.toThrow(/not yours/)
expect(mocks.put).not.toHaveBeenCalled()
expect(mocks.inserted).toHaveLength(0)
})
it('stores the bytes, records the attachment, and audits file.uploaded once', async () => {
const png = new File(['png'], 'a.png', { type: 'image/png' })
await uploadFile(NOTE, attach(png))
expect(mocks.put).toHaveBeenCalledTimes(1)
expect(mocks.put.mock.calls[0]?.[0]).toBe(`notes/${NOTE}/a.png`)
expect(mocks.inserted[0]).toMatchObject({
noteId: NOTE,
authorId: 'visitor-1',
name: 'a.png',
contentType: 'image/png',
size: 3,
url: `https://blob.example.com/notes/${NOTE}/a.png`,
})
expect(mocks.log).toHaveBeenCalledTimes(1)
expect(mocks.log).toHaveBeenCalledWith('file.uploaded', {
actor: { kind: 'human', id: 'visitor-1' },
target: { id: '00000000-0000-4000-8000-0000000000ff' },
})
})
it('sends exactly one usage event, keyed by the attachment, for one upload', async () => {
const png = new File(['png'], 'a.png', { type: 'image/png' })
await uploadFile(NOTE, attach(png))
expect(mocks.recordUsage).toHaveBeenCalledTimes(1)
const [config, events] = mocks.recordUsage.mock.calls[0] as unknown as [
{ baseUrl: string; token: string },
Array<{ idempotency_key: string; meter: string; quantity: string }>,
]
expect(config).toEqual({ baseUrl: 'https://console.example.com', token: 'dt_fixture' })
expect(events).toHaveLength(1)
expect(events[0]).toMatchObject({ idempotency_key: 'upload:00000000-0000-4000-8000-0000000000ff', meter: 'uploads', quantity: '1' })
})
it('refuses an upload over the minute allowance before any write', async () => {
mocks.consumeUploadQuota.mockResolvedValue({ allowed: false, remaining: 0 })
const png = new File(['png'], 'a.png', { type: 'image/png' })
await expect(uploadFile(NOTE, attach(png))).rejects.toThrow(/limit/)
expect(mocks.put).not.toHaveBeenCalled()
expect(mocks.inserted).toHaveLength(0)
})
})
+57
View File
@@ -0,0 +1,57 @@
'use server'
import { revalidatePath } from 'next/cache'
import { fileRepository, uploadLimits } from '@/entities/file'
import { noteId, noteRepository } from '@/entities/note'
import { getEnv } from '@/shared/config'
import { audit, blob, db, recordUsage, serverErrors } from '@/shared/api'
import { consumeUploadQuota } from '@/shared/lib/optional'
import { ensureVisitor } from '@/shared/lib/visitor-session'
/**
* Attach a file to a note the visitor wrote. The limits are checked before any
* upload, so a refused file never reaches Blob Storage.
*/
export async function uploadFile(note: string, form: FormData): Promise<void> {
const key = noteId.parse(note)
const upload = form.get('file')
if (!(upload instanceof File)) throw new Error('choose a file to upload')
const limits = uploadLimits.parse({
name: upload.name,
size: upload.size,
contentType: upload.type,
})
const actor = await ensureVisitor()
// With KV linked, each visitor has a per-minute upload allowance; without it, none.
const quota = await consumeUploadQuota(actor)
if (quota && !quota.allowed) throw new Error('the upload limit for this minute is reached')
const owner = await noteRepository(db()).get(key)
if (!owner || owner.authorId !== actor) throw new Error('that note is not yours to attach to')
const stored = await blob().put(`notes/${key}/${limits.name}`, Buffer.from(await upload.arrayBuffer()), {
contentType: limits.contentType,
})
const row = await fileRepository(db()).create({
noteId: key,
authorId: actor,
name: limits.name,
contentType: limits.contentType,
size: limits.size,
url: stored.url,
})
await audit().log('file.uploaded', { actor: { kind: 'human', id: actor }, target: { id: row.id } })
// One upload, one usage event. The key is the attachment's id, so a resend stores nothing twice.
// A failed send is reported, not raised: the upload itself succeeded.
const env = getEnv()
await recordUsage({ baseUrl: env.CONUSAI_API_URL, token: env.CONUSAI_API_TOKEN }, [
{
idempotency_key: `upload:${row.id}`,
customer: { external_id: 'showcase' },
meter: 'uploads',
quantity: '1',
properties: { bytes: limits.size, content_type: limits.contentType },
},
]).catch((error: unknown) => serverErrors().captureException(error))
revalidatePath('/')
}
+2
View File
@@ -0,0 +1,2 @@
export { uploadFile } from './api/actions'
export { UploadFileForm } from './ui/UploadFileForm'
@@ -0,0 +1,13 @@
import { uploadFile } from '@/features/upload-file/api/actions'
export function UploadFileForm({ noteId }: { noteId: string }) {
return (
<form action={uploadFile.bind(null, noteId)}>
<label>
Attachment
<input name="file" type="file" required />
</label>
<button type="submit">Attach</button>
</form>
)
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+36
View File
@@ -0,0 +1,36 @@
import 'server-only'
import { ConusAIClient } from '@conusai-sdk/node-sdk'
import { createAuditClient, defineAuditEvents, type AuditClient } from '@conusai-sdk/node-sdk/audit'
import { getEnv } from '@/shared/config/env'
/** The actions this app writes to the audit log (plan §4.4): one definition, typed `log()`. */
export const events = defineAuditEvents({
'note.created': { target: 'note' },
'note.updated': { target: 'note' },
'note.deleted': { target: 'note' },
'note.announced': { target: 'note' },
'file.uploaded': { target: 'file' },
'file.deleted': { target: 'file' },
})
export type AppAuditClient = AuditClient<typeof events>
let client: AppAuditClient | undefined
/** The audit client. It reads `CONUSAI_API_URL` and `CONUSAI_API_TOKEN` itself (D8). */
export function audit(): AppAuditClient {
client ??= createAuditClient({ events })
return client
}
/**
* The project's own audit events, newest first, one page. The deployment token
* carries `audit:read` (showcase D9), so the platform answers for this project only.
*/
export async function readAuditEvents(options: { cursor?: string; limit?: number } = {}) {
const env = getEnv()
const platform = new ConusAIClient({ baseUrl: env.CONUSAI_API_URL, apiKey: env.CONUSAI_API_TOKEN })
const { data } = await platform.readEvents({ query: options, throwOnError: true })
if (!data) throw new Error('the platform returned no audit page')
return data
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+10
View File
@@ -0,0 +1,10 @@
import 'server-only'
import { createClient, type BlobClient } from '@conusai-sdk/blob'
let client: BlobClient | undefined
/** The Blob Storage client. It reads `CONUSAI_API_URL` and `CONUSAI_API_TOKEN` itself (D8). */
export function blob(): BlobClient {
client ??= createClient()
return client
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+14
View File
@@ -0,0 +1,14 @@
import 'server-only'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { getEnv } from '@/shared/config/env'
export type Db = ReturnType<typeof drizzle>
let connection: Db | undefined
/** The Postgres connection every server module shares, opened on first use. */
export function db(): Db {
connection ??= drizzle(postgres(getEnv().POSTGRES_URL))
return connection
}
+12
View File
@@ -0,0 +1,12 @@
// The public API of the shared server clients. Each one is `server-only`; a
// client component that imports this file fails the build, which is the guard.
export * from './audit'
export * from './blob'
export * from './db'
export * from './kv'
export * from './nats'
export * from './observe'
export * from './platform'
export * from './qdrant'
export * from './s3'
export * from './usage'
+1
View File
@@ -0,0 +1 @@
export * from './server'
+15
View File
@@ -0,0 +1,15 @@
import 'server-only'
import { createClient, type KV } from '@conusai-sdk/kv'
import { getEnv, modules } from '@/shared/config/env'
let client: KV | undefined
/**
* The KV client when the KV module is linked (D13: `REDIS_URL` present), else
* `null`. Callers render the enable card instead of calling a dead service.
*/
export function kv(): KV | null {
if (!modules(getEnv()).kv) return null
client ??= createClient()
return client
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+14
View File
@@ -0,0 +1,14 @@
import 'server-only'
import { connect, type NatsConnection } from 'nats'
import { getEnv, modules } from '@/shared/config/env'
let connection: Promise<NatsConnection> | undefined
/** The NATS connection, or `null` when the NATS module is not linked (D13). */
export function nats(): Promise<NatsConnection> | null {
const env = getEnv()
const servers = env.NATS_URL
if (!modules(env).nats || !servers) return null
if (!connection) connection = connect(env.NATS_TOKEN ? { servers, token: env.NATS_TOKEN } : { servers })
return connection
}
+1
View File
@@ -0,0 +1 @@
export { serverErrors, withRequest } from './server'
+19
View File
@@ -0,0 +1,19 @@
import 'server-only'
import { init, withRequest, type ErrorTrackingClient } from '@conusai-sdk/observe-node'
import { getEnv } from '@/shared/config/env'
let client: ErrorTrackingClient | undefined
/**
* The server error client, bound to this environment's DSN and release (D8). Server
* errors leave with the request id of the request that raised them.
*/
export function serverErrors(): ErrorTrackingClient {
if (!client) {
const env = getEnv()
client = init({ dsn: env.CONUSAI_DSN, release: env.CONUSAI_RELEASE })
}
return client
}
export { withRequest }
+1
View File
@@ -0,0 +1 @@
export * from './server'
+25
View File
@@ -0,0 +1,25 @@
import 'server-only'
import { getPlatformInfo, getServiceType, type ServiceType } from '@conusai-sdk/api'
import { getEnv } from '@/shared/config/env'
function auth() {
const env = getEnv()
return { baseUrl: env.CONUSAI_API_URL, headers: { Authorization: `Bearer ${env.CONUSAI_API_TOKEN}` } }
}
/** The platform's own description, read with this project's deployment token. */
export async function platformInfo() {
const { data } = await getPlatformInfo({ ...auth(), throwOnError: true })
if (!data) throw new Error('the platform returned no description')
return data
}
/**
* The catalog entry of a service type: the text an enable card shows, read from the
* platform rather than written here (plan D13).
*/
export async function serviceCatalogEntry(serviceType: ServiceType) {
const { data } = await getServiceType({ ...auth(), path: { service_type: serviceType }, throwOnError: true })
if (!data) throw new Error(`the platform returned no catalog entry for ${serviceType}`)
return data
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+47
View File
@@ -0,0 +1,47 @@
import 'server-only'
/** What the Qdrant REST API needs from this app: where it is and how to authenticate. */
export type QdrantConfig = { url: string; apiKey?: string | undefined; collection: string }
/** One note in the vector index: its id, its text's vector and its title. */
export type QdrantPoint = { id: string; vector: number[]; payload: { title: string } }
/** Creates the collection if it is missing, with cosine distance at the vector's size. */
export async function ensureCollection(config: QdrantConfig, size: number, fetcher: typeof fetch = fetch): Promise<void> {
const response = await fetcher(`${config.url}/collections/${config.collection}`, {
method: 'PUT',
headers: qdrantHeaders(config),
body: JSON.stringify({ vectors: { size, distance: 'Cosine' } }),
})
// 409 means the collection already exists: that is the state we want.
if (!response.ok && response.status !== 409) throw new Error(`qdrant collection answered ${response.status}`)
}
export async function upsertPoints(config: QdrantConfig, points: QdrantPoint[], fetcher: typeof fetch = fetch): Promise<void> {
const response = await fetcher(`${config.url}/collections/${config.collection}/points?wait=true`, {
method: 'PUT',
headers: qdrantHeaders(config),
body: JSON.stringify({ points }),
})
if (!response.ok) throw new Error(`qdrant upsert answered ${response.status}`)
}
export async function searchPoints(
config: QdrantConfig,
vector: number[],
limit: number,
fetcher: typeof fetch = fetch,
): Promise<Array<{ id: string; score: number; payload: { title: string } }>> {
const response = await fetcher(`${config.url}/collections/${config.collection}/points/search`, {
method: 'POST',
headers: qdrantHeaders(config),
body: JSON.stringify({ vector, limit, with_payload: true }),
})
if (!response.ok) throw new Error(`qdrant search answered ${response.status}`)
const body = (await response.json()) as { result: Array<{ id: string; score: number; payload: { title: string } }> }
return body.result
}
function qdrantHeaders(config: QdrantConfig): Record<string, string> {
return config.apiKey ? { 'content-type': 'application/json', 'api-key': config.apiKey } : { 'content-type': 'application/json' }
}
+1
View File
@@ -0,0 +1 @@
export * from './server'
+23
View File
@@ -0,0 +1,23 @@
import 'server-only'
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { getEnv, modules } from '@/shared/config/env'
let client: S3Client | undefined
/** The dedicated S3 client, or `null` when the S3 module is not linked (D13). */
export function s3(): { client: S3Client; bucket: string } | null {
const env = getEnv()
if (!modules(env).s3 || !env.S3_BUCKET || !env.AWS_ENDPOINT_URL) return null
const credentials =
env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY
? { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY }
: undefined
client ??= new S3Client(
credentials
? { endpoint: env.AWS_ENDPOINT_URL, forcePathStyle: true, region: 'us-east-1', credentials }
: { endpoint: env.AWS_ENDPOINT_URL, forcePathStyle: true, region: 'us-east-1' },
)
return { client, bucket: env.S3_BUCKET }
}
export { PutObjectCommand }
+1
View File
@@ -0,0 +1 @@
export { recordUsage, type UsageEvent, type UsageResult } from './server'
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { recordUsage, type UsageEvent } from '@/shared/api/usage/server'
const config = { baseUrl: 'https://console.example.com', token: 'dt_fixture' }
const event: UsageEvent = {
idempotency_key: 'upload:f1',
customer: { external_id: 'showcase' },
meter: 'uploads',
quantity: '1',
}
describe('recordUsage', () => {
it('posts one batch to the token route with the token as a bearer credential', async () => {
const calls: { url: string; init: RequestInit }[] = []
const fetcher = (async (url: string, init: RequestInit) => {
calls.push({ url, init })
return new Response(JSON.stringify({ accepted: 1, duplicates: 0, rejected: [] }), { status: 202 })
}) as typeof fetch
const result = await recordUsage(config, [event], fetcher)
expect(result).toEqual({ accepted: 1, duplicates: 0, rejected: [] })
expect(calls).toHaveLength(1)
expect(calls[0]?.url).toBe('https://console.example.com/_conusai/usage/events')
expect(calls[0]?.init.method).toBe('POST')
expect(new Headers(calls[0]?.init.headers).get('authorization')).toBe('Bearer dt_fixture')
expect(JSON.parse(String(calls[0]?.init.body))).toEqual({ events: [event] })
})
it('treats anything but 202 as a failure, so the caller can resend the same key', async () => {
const fetcher = (async () => new Response('{}', { status: 403 })) as typeof fetch
await expect(recordUsage(config, [event], fetcher)).rejects.toThrow(/403/)
})
})
+36
View File
@@ -0,0 +1,36 @@
import 'server-only'
/** One usage event, as the platform's metering ingest reads it (plan D9). */
export type UsageEvent = {
idempotency_key: string
customer: { external_id: string }
meter: string
quantity: string
properties?: Record<string, unknown>
}
/** The ingest's answer: what was stored, what was already there, and what was refused. */
export type UsageResult = {
accepted: number
duplicates: number
rejected: unknown[]
}
/**
* Sends usage events with the deployment token (showcase S4.6, D9). A retry with the
* same idempotency key stores nothing twice, so a failed send can be sent again.
* Anything but 202 is an error: the platform answers 422 only when every event was refused.
*/
export async function recordUsage(
config: { baseUrl: string; token: string },
events: UsageEvent[],
fetcher: typeof fetch = fetch,
): Promise<UsageResult> {
const response = await fetcher(`${config.baseUrl}/_conusai/usage/events`, {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${config.token}` },
body: JSON.stringify({ events }),
})
if (response.status !== 202) throw new Error(`usage ingest answered ${response.status}`)
return (await response.json()) as UsageResult
}
+34
View File
@@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { modules, parseEnv } from '@/shared/config/env'
const required = {
CONUSAI_API_URL: 'https://console.example.com',
CONUSAI_API_TOKEN: 'dt_fixture',
CONUSAI_PROJECT_ID: '42',
CONUSAI_DSN: 'https://key@errors.example.com/1',
CONUSAI_RELEASE: 'abc123',
POSTGRES_URL: 'postgres://app@db.example.com/app',
}
describe('parseEnv', () => {
it('accepts the required contract and leaves every optional module off', () => {
const env = parseEnv(required)
expect(modules(env)).toEqual({ kv: false, nats: false, qdrant: false, git: false, s3: false })
})
it('names the missing required variable', () => {
const rest: Record<string, string | undefined> = { ...required }
delete rest.CONUSAI_API_TOKEN
expect(() => parseEnv(rest)).toThrow(/CONUSAI_API_TOKEN/)
})
it('lights a module when its variable is present', () => {
const env = parseEnv({ ...required, REDIS_URL: 'redis://cache.example.com:6379' })
expect(modules(env).kv).toBe(true)
})
it('lights the S3 module only when both its endpoint and bucket are present', () => {
expect(modules(parseEnv({ ...required, AWS_ENDPOINT_URL: 'https://s3.example.com' })).s3).toBe(false)
expect(modules(parseEnv({ ...required, AWS_ENDPOINT_URL: 'https://s3.example.com', S3_BUCKET: 'exports' })).s3).toBe(true)
})
})
+59
View File
@@ -0,0 +1,59 @@
import 'server-only'
import { z } from 'zod'
// The SDK environment contract (showcase D8): the names every deployment receives.
const required = z.object({
CONUSAI_API_URL: z.string().url(),
CONUSAI_API_TOKEN: z.string().min(1),
CONUSAI_PROJECT_ID: z.string().min(1),
CONUSAI_DSN: z.string().url(),
CONUSAI_RELEASE: z.string().min(1),
POSTGRES_URL: z.string().url(),
})
// Optional services (D13): each one lights a widget when its variables are present.
const optional = z.object({
REDIS_URL: z.string().url().optional(),
NATS_URL: z.string().optional(),
NATS_TOKEN: z.string().optional(),
QDRANT_URL: z.string().url().optional(),
QDRANT_API_KEY: z.string().optional(),
GIT_URL: z.string().url().optional(),
AWS_ENDPOINT_URL: z.string().url().optional(),
S3_BUCKET: z.string().min(1).optional(),
AWS_ACCESS_KEY_ID: z.string().optional(),
AWS_SECRET_ACCESS_KEY: z.string().optional(),
})
const schema = required.merge(optional)
export type Env = z.infer<typeof schema>
/** Parse the environment. A missing required name throws with its name in the message. */
export function parseEnv(source: Record<string, string | undefined>): Env {
const result = schema.safeParse(source)
if (!result.success) {
const names = result.error.issues.map((issue) => issue.path.join('.')).join(', ')
throw new Error(`invalid environment: ${names}`)
}
return result.data
}
let parsed: Env | undefined
/** The environment, parsed once on first use. */
export function getEnv(): Env {
parsed ??= parseEnv(process.env)
return parsed
}
/** Which optional modules are linked, for the widgets that depend on them. */
export function modules(env: Env) {
return {
kv: Boolean(env.REDIS_URL),
nats: Boolean(env.NATS_URL),
qdrant: Boolean(env.QDRANT_URL),
git: Boolean(env.GIT_URL),
s3: Boolean(env.AWS_ENDPOINT_URL && env.S3_BUCKET),
} as const
}
+1
View File
@@ -0,0 +1 @@
export { getEnv, modules, parseEnv, type Env } from './env'
+64
View File
@@ -0,0 +1,64 @@
import { randomUUID } from 'node:crypto'
import { connect } from 'nats'
import { CreateBucketCommand, GetObjectCommand } from '@aws-sdk/client-s3'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
// The live check (plan S4.7 gap 4): NATS, Qdrant and S3 exercised against real
// services. It runs when their variables are present, the same names a deployment
// receives (D8, D13), and is skipped otherwise.
const live = Boolean(process.env.NATS_URL && process.env.QDRANT_URL && process.env.AWS_ENDPOINT_URL)
describe.runIf(live)('optional modules against live services', () => {
type Optional = typeof import('@/shared/lib/optional')
type Api = typeof import('@/shared/api')
let optional: Optional
let api: Api
beforeAll(async () => {
vi.stubEnv('S3_BUCKET', process.env.S3_BUCKET ?? 'showcase-live')
optional = await import('@/shared/lib/optional')
api = await import('@/shared/api')
const target = api.s3()
if (!target) throw new Error('S3 is not linked')
await target.client.send(new CreateBucketCommand({ Bucket: target.bucket })).catch((error: { name?: string }) => {
if (error.name !== 'BucketAlreadyOwnedByYou' && error.name !== 'BucketAlreadyExists') throw error
})
})
afterAll(() => vi.unstubAllEnvs())
it('publishes note.created on NATS and a subscriber receives it', async () => {
const subscriber = await connect({ servers: process.env.NATS_URL as string })
const sub = subscriber.subscribe('showcase.notes.created')
const id = randomUUID()
await optional.publishNoteCreated({ id, title: 'live note' })
await subscriber.flush()
const received = await Promise.race([
(async () => {
for await (const message of sub) return JSON.parse(new TextDecoder().decode(message.data)) as { id: string; title: string }
return undefined
})(),
new Promise((resolve) => setTimeout(() => resolve(undefined), 3000)),
])
await subscriber.close()
expect(received).toEqual({ id, title: 'live note' })
})
it('indexes a note in Qdrant and finds it as the top similarity hit', async () => {
const id = randomUUID()
const title = `budget ${id.slice(0, 8)}`
await optional.indexNote({ id, title, body: 'quarterly budget review with the finance team' })
const hits = await optional.similarNotes(`${title} quarterly budget review`, 1)
expect(hits?.[0]?.id).toBe(id)
})
it('writes an export object to the S3 bucket and reads it back', async () => {
const notes = [{ title: 'live export' }]
const key = await optional.exportToS3('live-visitor', notes)
expect(key).toMatch(/^exports\/live-visitor\/.+\.json$/)
const target = api.s3()
if (!target || !key) throw new Error('S3 is not linked')
const object = await target.client.send(new GetObjectCommand({ Bucket: target.bucket, Key: key }))
expect(JSON.parse(await object.Body!.transformToString())).toEqual(notes)
})
})
+81
View File
@@ -0,0 +1,81 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// The optional-module helpers run against a fake KV store: the modules' clients are
// replaced, so each test asserts what the helper asked for.
const mocks = vi.hoisted(() => {
const store = new Map<string, number>()
return {
store,
kvClient: null as null | {
incr: (key: string) => Promise<number>
expire: (key: string, seconds: number) => Promise<number>
get: <T>(key: string) => Promise<T | null>
},
expire: vi.fn(async () => 1),
}
})
vi.mock('@/shared/api', () => ({
kv: () => mocks.kvClient,
nats: () => null,
s3: () => null,
ensureCollection: vi.fn(async () => {}),
upsertPoints: vi.fn(async () => {}),
searchPoints: vi.fn(async () => []),
}))
vi.mock('@/shared/config', () => ({
getEnv: () => ({ QDRANT_URL: undefined }),
}))
import { consumeUploadQuota, embed, UPLOADS_PER_MINUTE, uploadsLeft } from '@/shared/lib/optional'
function fakeKv() {
mocks.kvClient = {
incr: async (key: string) => {
const next = (mocks.store.get(key) ?? 0) + 1
mocks.store.set(key, next)
return next
},
expire: mocks.expire,
get: async <T>(key: string) => (mocks.store.has(key) ? (mocks.store.get(key) as T) : null),
}
}
describe('consumeUploadQuota', () => {
beforeEach(() => {
mocks.store.clear()
mocks.expire.mockClear()
fakeKv()
})
it('allows the uploads of the minute up to the limit, refuses the next, and sets the window once', async () => {
let last: { allowed: boolean; remaining: number } | null = null
for (let i = 0; i < UPLOADS_PER_MINUTE; i += 1) last = await consumeUploadQuota('visitor-1')
expect(last).toEqual({ allowed: true, remaining: 0 })
expect(await consumeUploadQuota('visitor-1')).toEqual({ allowed: false, remaining: 0 })
expect(mocks.expire).toHaveBeenCalledTimes(1)
})
it('counts each visitor separately', async () => {
await consumeUploadQuota('visitor-1')
expect(await uploadsLeft('visitor-2')).toBe(UPLOADS_PER_MINUTE)
})
it('is null, and counts nothing, when KV is not linked', async () => {
mocks.kvClient = null
expect(await consumeUploadQuota('visitor-1')).toBeNull()
expect(await uploadsLeft('visitor-1')).toBeNull()
})
})
describe('embed', () => {
const dot = (x: number[], y: number[]) => x.reduce((sum, value, i) => sum + value * (y[i] ?? 0), 0)
it('is stable for the same text, and orthogonal for text with no word in common', () => {
const a = embed('plan the launch')
expect(embed('plan the launch')).toEqual(a)
expect(dot(a, a)).toBeCloseTo(1)
expect(dot(a, embed('zebra quartz'))).toBeCloseTo(0)
})
})

Some files were not shown because too many files have changed in this diff Show More