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
+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