@firstlovecenter/ai-chat 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/drizzle/index.cjs +11 -3
- package/dist/drizzle/index.cjs.map +1 -1
- package/dist/drizzle/index.js +11 -3
- package/dist/drizzle/index.js.map +1 -1
- package/dist/ui/index.cjs +100 -26
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.js +98 -26
- package/dist/ui/index.js.map +1 -1
- package/package.json +12 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to `@firstlovecenter/ai-chat` are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.9.4] — 2026-05-09
|
|
9
|
+
|
|
10
|
+
Markdown in assistant answers now actually renders as markdown.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Literal `**bold**` / `*italic*` showing in chat output.** `paragraph_brief` prose, `list` items, and `callout` text were rendered as plain text inside `<p>{block.prose}</p>`, so markdown emitted by the model leaked through verbatim. The renderer now pipes those three block types through `react-markdown` (with `remark-gfm`) and a small components map that keeps the existing `text-sm leading-6` look. Errors and the user's own messages are still rendered as plain text — only model-authored content is parsed.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`react-markdown` + `remark-gfm` dependencies.** Required by the change above. The `ui` bundle grows by ~30 KB gzipped — small enough that no lazy-load gymnastics are warranted.
|
|
19
|
+
|
|
20
|
+
## [0.9.3] — 2026-05-09
|
|
21
|
+
|
|
22
|
+
Fixes the actual root cause of the "messages persisted but reload shows empty thread" bug — turns out it was at the data layer, not the navigation layer.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **MySQL `JSON` columns returned as strings.** `mysql2/promise` (the driver underneath `drizzle-orm/mysql2`) does **not** auto-parse `JSON` columns by default — it hands back the raw JSON-encoded string. Drizzle's `json()` column type didn't paper over that for hosts on this driver, so `chat_messages.blocks`/`prose`/`errorJson` came out as strings. The hydration path in the UI then called `.forEach` on a string, threw a `TypeError`, was swallowed by the effect's `catch`, and silently emptied the answer thread on reload. The drizzle adapter now parses JSON columns at the boundary in `mapMessage`. Both `AiChat.messagesToAnswers` and `VercelChat.storedToUseChat` also coerce strings → JSON as a second line of defence so even hosts wiring a different persistence path are robust.
|
|
27
|
+
- **`activeSessionId` falling out of sync with `initialSessionId`** — added a defensive `useEffect` in both shells that re-syncs from the prop. `useState(initialSessionId)` only honours the prop on the first render, so anything that resets state could land us in the "URL has a UID but the component thinks it's a fresh chat" limbo we saw.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Hydration error logging.** The hydration-effect's `catch` block now logs the error before clearing answers, instead of silently swallowing. Future regressions in this path will be visible in DevTools console rather than masquerading as a UI bug.
|
|
32
|
+
|
|
8
33
|
## [0.9.2] — 2026-05-09
|
|
9
34
|
|
|
10
35
|
Fixes a streaming-vs-navigation race that caused the first turn of a brand-new conversation to vanish on reload.
|
package/dist/drizzle/index.cjs
CHANGED
|
@@ -86,15 +86,23 @@ function mapSession(row) {
|
|
|
86
86
|
updatedAt: new Date(row.updatedAt)
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
+
function parseJsonColumn(v) {
|
|
90
|
+
if (typeof v !== "string" || v.length === 0) return v;
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(v);
|
|
93
|
+
} catch {
|
|
94
|
+
return v;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
89
97
|
function mapMessage(row) {
|
|
90
98
|
return {
|
|
91
99
|
id: row.id,
|
|
92
100
|
sessionId: row.sessionId,
|
|
93
101
|
role: row.role,
|
|
94
102
|
question: row.question,
|
|
95
|
-
blocks: row.blocks,
|
|
96
|
-
prose: row.prose,
|
|
97
|
-
errorJson: row.errorJson,
|
|
103
|
+
blocks: parseJsonColumn(row.blocks),
|
|
104
|
+
prose: parseJsonColumn(row.prose),
|
|
105
|
+
errorJson: parseJsonColumn(row.errorJson),
|
|
98
106
|
createdAt: new Date(row.createdAt)
|
|
99
107
|
};
|
|
100
108
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/drizzle/tables.ts","../../src/server/ports/types.ts","../../src/server/uid.ts","../../src/adapters/drizzle/adapter.ts"],"names":["bigint","varchar","datetime","sql","mysqlTable","tinyint","int","text","index","mysqlEnum","json","randomBytes","eq","and","desc"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Canonical drizzle MySQL table definitions for `@firstlovecenter/ai-chat`.\n *\n * These three tables (`chat_sessions`, `chat_messages`, `ai_settings`) are\n * the data shape the package needs to persist its conversations and global\n * config. The host re-exports them from its own schema so that an existing\n * deployment keeps the same column names, lengths, FKs, and indexes — making\n * the host's data fully portable into and out of the package.\n *\n * Deviations from the host's prior shape are deliberate:\n * - `tool_provider` and `gcp_location` are plain VARCHAR (not enums) so a\n * consumer can register additional providers / regions without a schema\n * migration. Validation against the runtime registries happens at a\n * higher layer.\n * - `chat_interface` is a new VARCHAR column controlling which chat UI\n * (custom vs. vercel-ai) renders globally; default 'custom'.\n *\n * The small column helpers from the host (`bigintPk`, `bigintFk`,\n * `bigintFkNullable`, `createdAt`) are re-defined inline here — this package\n * never imports from the host repo.\n */\nimport { sql } from 'drizzle-orm';\nimport {\n bigint,\n datetime,\n index,\n int,\n json,\n mysqlEnum,\n mysqlTable,\n text,\n tinyint,\n varchar\n} from 'drizzle-orm/mysql-core';\n\n// ---------------------------------------------------------------------------\n// Inline column helpers (mirrors host `src/db/columns.ts` shapes)\n// ---------------------------------------------------------------------------\n\n/** BIGINT UNSIGNED PK with auto-increment, returned as `number`. */\nconst bigintPk = () =>\n bigint('id', { mode: 'number', unsigned: true })\n .notNull()\n .primaryKey()\n .autoincrement();\n\n/** NOT NULL BIGINT UNSIGNED FK column, returned as `number`. */\nconst bigintFk = (name: string) =>\n bigint(name, { mode: 'number', unsigned: true }).notNull();\n\n/**\n * VARCHAR(12) PK for chat session/message rows. The package generates\n * UIDs via `generateChatUid()` (12 chars, ~72 bits, URL-safe alphabet)\n * and writes them on insert — the column has no DB-side default.\n */\nconst uidPk = () =>\n varchar('id', { length: 12 }).notNull().primaryKey();\n\n/** NOT NULL VARCHAR(12) FK referencing a chat-session UID. */\nconst uidFk = (name: string) => varchar(name, { length: 12 }).notNull();\n\n/** Nullable BIGINT UNSIGNED FK column, returned as `number | null`. */\nconst bigintFkNullable = (name: string) =>\n bigint(name, { mode: 'number', unsigned: true });\n\n/** `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP`, mode 'string'. */\nconst createdAt = () =>\n datetime('created_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`);\n\n// ---------------------------------------------------------------------------\n// ai_settings (singleton — always one row at id=1)\n// ---------------------------------------------------------------------------\n\n/**\n * Global AI configuration. Singleton row enforced by `id=1` default + PK.\n * `tool_provider`, `gcp_location`, and `chat_interface` are open VARCHARs;\n * the runtime layer validates against the host's registries before write.\n */\nexport const aiSettings = mysqlTable('ai_settings', {\n id: tinyint('id').notNull().primaryKey().default(1),\n toolProvider: varchar('tool_provider', { length: 32 })\n .notNull()\n .default('claude'),\n gcpLocation: varchar('gcp_location', { length: 32 })\n .notNull()\n .default('us-east5'),\n chatInterface: varchar('chat_interface', { length: 32 })\n .notNull()\n .default('custom'),\n /**\n * Admin-editable GCP project override. NULL means the host's static\n * `VertexPort.projectId` is used. Lets admins flip the active GCP\n * project without redeploying — useful for staging/prod migrations\n * or running multiple FLC tenants from one runtime.\n */\n gcpProjectId: varchar('gcp_project_id', { length: 64 }),\n // Caps the per-turn output budget for both the agent loop AND the prose\n // narrator. Reasoning models charge internal thinking against this; bump\n // it well past 4096 when one is in use.\n maxOutputTokens: int('max_output_tokens').notNull().default(4096),\n // Optional admin-editable role/persona. NULL means the host's static\n // `rolePrompt` (passed to configureAiChat) is used as the fallback.\n rolePrompt: text('role_prompt'),\n updatedAt: datetime('updated_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`),\n updatedByUserId: bigintFkNullable('updated_by_user_id')\n});\n\n// ---------------------------------------------------------------------------\n// chat_sessions (one per conversation)\n// ---------------------------------------------------------------------------\n\nexport const chatSessions = mysqlTable(\n 'chat_sessions',\n {\n id: uidPk(),\n userId: bigintFk('user_id'),\n title: varchar('title', { length: 200 }).notNull(),\n createdAt: createdAt(),\n updatedAt: datetime('updated_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`)\n },\n (t) => ({\n idxUserUpdated: index('idx_chat_session_user_updated').on(t.userId, t.updatedAt)\n })\n);\n\n// ---------------------------------------------------------------------------\n// chat_messages (one row per turn in a chat_session)\n// ---------------------------------------------------------------------------\n\nexport const chatMessageRoleEnum = ['user', 'assistant'] as const;\n\nexport const chatMessages = mysqlTable(\n 'chat_messages',\n {\n id: uidPk(),\n sessionId: uidFk('session_id'),\n role: mysqlEnum('role', chatMessageRoleEnum).notNull(),\n question: text('question'),\n blocks: json('blocks'),\n prose: json('prose'),\n errorJson: json('error_json'),\n createdAt: createdAt()\n },\n (t) => ({\n idxSessionCreated: index('idx_chat_msg_session_created').on(t.sessionId, t.createdAt)\n })\n);\n\n// ---------------------------------------------------------------------------\n// Inferred row types — distinct from the domain types in ports/types.ts.\n// Adapters map these row shapes (datetime as string) to the domain shapes\n// (Date) at the boundary.\n// ---------------------------------------------------------------------------\n\nexport type ChatSessionRow = typeof chatSessions.$inferSelect;\nexport type ChatMessageRow = typeof chatMessages.$inferSelect;\nexport type AiSettingsRow = typeof aiSettings.$inferSelect;\n","/**\n * Ports — the contract between this package and any host that consumes it.\n *\n * Every project-specific concern (auth, scope, persistence, tools, credentials)\n * crosses the boundary as a typed port the host implements and passes into\n * `configureAiChat({...})`. The package itself has zero knowledge of the\n * host's database, RBAC model, or environment.\n *\n * Domain types (ChatSession, ChatMessage, AiSettings, PresentPayload, Block)\n * are pure TS — no ORM coupling. The Drizzle and Prisma adapters each map\n * their respective row types into these shapes so callers see one contract\n * regardless of the chosen ORM.\n */\nimport type { GoogleAuth } from 'google-auth-library';\nimport type { ToolDefinition, ToolContext, SystemBlock } from '../tools/types';\n\n// ---------------------------------------------------------------------------\n// Domain types — used by every port\n// ---------------------------------------------------------------------------\n\n/**\n * Chat session and message identifiers are short URL-safe UIDs (12 chars,\n * ~72 bits of entropy, alphabet matches nanoid's URL-safe set). The\n * package generates them in `createSession`/`appendMessage` so adapters\n * never need to know about auto-increment vs UID.\n */\nexport type ChatSession = {\n id: string;\n userId: number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type ChatMessageRole = 'user' | 'assistant';\n\n/**\n * One message turn. For 'user' rows, `question` carries the raw question text.\n * For 'assistant' rows, `blocks` holds the structured PresentPayload and\n * `prose` holds any paragraph_brief prose collected from the streamed\n * narrator. `errorJson` is set when an assistant turn failed mid-stream.\n */\nexport type ChatMessage = {\n id: string;\n sessionId: string;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\n/**\n * Singleton row controlling global AI runtime settings. All open-string\n * fields are validated at runtime against the registries the host configures\n * — they are not enums in the package's types so consumers can register\n * additional providers / interfaces without a schema change.\n *\n * `maxOutputTokens` caps both the agent loop's per-turn output AND each\n * narrator's prose pass. Reasoning models (e.g. `xai/grok-4.1-fast-reasoning`)\n * charge internal thinking against this budget — set it generously when\n * those are in use.\n *\n * `rolePrompt` is the persona the assistant adopts. When non-null, it\n * takes precedence over the host's static `rolePrompt` configureAiChat\n * option, so admins can edit live in the settings UI.\n *\n * `gcpProjectId` is an admin-editable override for the GCP project that\n * every Vertex API call targets. When non-null, it takes precedence over\n * the host's static `VertexPort.projectId` so admins can flip projects\n * (staging ↔ prod, multi-tenant) without redeploying.\n */\nexport type AiSettings = {\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: string | null;\n gcpProjectId: string | null;\n updatedAt: Date | null;\n updatedByUserId: number | null;\n};\n\n/**\n * Default cap when nothing is persisted. Conservative so non-reasoning\n * models don't pay for headroom they don't need; admins bump it for\n * reasoning models via the settings UI.\n */\nexport const DEFAULT_AI_MAX_OUTPUT_TOKENS = 4096;\n\n// ---------------------------------------------------------------------------\n// AuthPort — the host tells us who's calling\n// ---------------------------------------------------------------------------\n\n/**\n * The host's resolved Scope (org/tenant/RBAC shape) is opaque to the package.\n * `S` is whatever the host wants — typically a discriminated union with at\n * least `userId: number` plus role/tenancy fields.\n */\nexport type AuthOk<S> = { ok: true; scope: S; userId: number };\nexport type AuthFail = { ok: false; response: Response };\nexport type AuthResult<S> = AuthOk<S> | AuthFail;\n\nexport type AuthPort<S = unknown> = {\n /**\n * Resolve the calling user from the request. Returning `{ ok: false }`\n * lets the route hand back the prepared 401/403 response untouched.\n */\n requireAuth(req: Request): Promise<AuthResult<S>>;\n /**\n * Predicate the admin-settings route uses to gate writes. Hosts that\n * don't model admins can return `() => true` if they trust their own\n * routing layer.\n */\n isSuperAdmin(scope: S): boolean;\n};\n\n// ---------------------------------------------------------------------------\n// ScopePort — human-readable scope description fed into the system prompt\n// ---------------------------------------------------------------------------\n\nexport type ScopePort<S = unknown> = {\n /** Short label rendered in the chat UI (e.g. \"Ghana\", \"All countries\"). */\n resolveScopeLabel(scope: S): Promise<string>;\n /** Longer narrative the system prompt uses to describe the data slice. */\n buildScopeSummary(scope: S): Promise<string>;\n};\n\n// ---------------------------------------------------------------------------\n// ToolsPort — host supplies its own tool registry + system prompts\n// ---------------------------------------------------------------------------\n\nexport type ToolsPort = {\n /**\n * Map of tool name → definition. Keys must match `definition.schema.name`.\n * Must include the terminal `present` tool — the agent loop refuses to\n * end a turn without it.\n */\n tools: Record<string, ToolDefinition>;\n /**\n * Build the ordered list of system blocks for one agent run. The host\n * decides how much of the prompt is project-specific (schema doc,\n * semantic-layer doc, scope summary, etc.). Blocks marked `cached: true`\n * become Anthropic ephemeral cache markers and Vertex prefix-cache hints.\n */\n buildSystemBlocks(ctx: ToolContext): Promise<SystemBlock[]>;\n};\n\n// ---------------------------------------------------------------------------\n// VertexPort — credentials are host-supplied; the package never reads env\n// ---------------------------------------------------------------------------\n\nexport type VertexPort = {\n projectId: string;\n /** e.g. 'us-east5' or 'global' — used unless `aiSettings.gcpLocation` overrides per-request. */\n defaultLocation: string;\n /**\n * A constructed `GoogleAuth` instance. Any auth scheme that yields one\n * works (ADC, Workload Identity Federation, split-key env vars,\n * Secret Manager, etc.). The package never reads `process.env.GCP_*`.\n */\n auth: GoogleAuth;\n /** Vertex model IDs pinned by the host (orgs pin their own versions). */\n modelIds: { claude: string; gemini: string };\n};\n\n// ---------------------------------------------------------------------------\n// LoggerPort — optional structured logging\n// ---------------------------------------------------------------------------\n\nexport type LoggerPort = {\n debug(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n error(...args: unknown[]): void;\n};\n\n// ---------------------------------------------------------------------------\n// PersistencePort — domain-shaped queries the package needs\n// ---------------------------------------------------------------------------\n\nexport type CreateSessionInput = {\n userId: number;\n title: string;\n};\n\nexport type AppendMessageInput = {\n sessionId: string;\n role: ChatMessageRole;\n question?: string | null;\n blocks?: unknown | null;\n prose?: unknown | null;\n errorJson?: unknown | null;\n};\n\nexport type ListSessionsOpts = {\n limit?: number;\n};\n\nexport type AiSettingsPatch = {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n /** Pass `null` to clear back to the host's static fallback. */\n rolePrompt?: string | null;\n /** Pass `null` to clear back to the host's static VertexPort.projectId. */\n gcpProjectId?: string | null;\n};\n\n/**\n * The whole reason this package is ORM-agnostic. Implemented by:\n * - createDrizzlePersistence (src/adapters/drizzle/)\n * - createPrismaPersistence (src/adapters/prisma/)\n * - createMemoryPersistence (src/adapters/memory/, internal test use)\n *\n * Routes, agent loop, and UI never touch ORMs directly — they only call\n * methods on this port. A parameterized contract test runs the same\n * assertions against all three adapters to keep them in sync.\n */\nexport type PersistencePort = {\n // Sessions ---------------------------------------------------------------\n createSession(input: CreateSessionInput): Promise<ChatSession>;\n /** Returns null when the session doesn't exist OR doesn't belong to this user. */\n getSession(id: string, userId: number): Promise<ChatSession | null>;\n listSessionsForUser(userId: number, opts?: ListSessionsOpts): Promise<ChatSession[]>;\n /** No-op when session doesn't exist or doesn't belong to user — caller asserted authorisation. */\n updateSession(id: string, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: string, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: string, userId: number): Promise<ChatMessage[]>;\n\n // AI settings (singleton row) -------------------------------------------\n /** Returns the singleton row, applying defaults if it's never been written. */\n getAiSettings(): Promise<AiSettings>;\n /**\n * Upsert. Validates `toolProvider` and `chatInterface` against the runtime\n * registries before writing — invalid values throw before SQL is issued.\n */\n updateAiSettings(patch: AiSettingsPatch, byUserId: number): Promise<AiSettings>;\n};\n","/**\n * Short URL-safe identifier generator for chat session and message rows.\n *\n * 12 chars from a 64-char URL-safe alphabet ≈ 72 bits of entropy. That's\n * collision-safe well past any per-user chat count and short enough to\n * appear unobtrusively in URLs (`/chat/V1StGXR8_Z5j`). The alphabet\n * matches nanoid's URL-safe set so the IDs round-trip cleanly through\n * URL paths, query strings, JSON, and SQL VARCHAR columns.\n */\nimport { randomBytes } from 'crypto';\n\nconst ALPHABET =\n 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';\n\nexport const CHAT_UID_LENGTH = 12;\n\nexport function generateChatUid(size: number = CHAT_UID_LENGTH): string {\n const bytes = randomBytes(size);\n let id = '';\n for (let i = 0; i < size; i += 1) {\n // Mask to 6 bits → uniform index into the 64-char alphabet.\n id += ALPHABET[bytes[i] & 63];\n }\n return id;\n}\n","/**\n * Drizzle MySQL implementation of `PersistencePort`.\n *\n * The package never imports a concrete drizzle client — the host passes a\n * `MySql2Database` (or any compatible drizzle MySQL handle) and we use it\n * to issue the queries this port describes.\n *\n * Boundary mapping: the drizzle MySQL `datetime` columns are declared with\n * `mode: 'string'` to match the host's existing schema, so row reads return\n * ISO strings. The PersistencePort domain types use `Date`, so every row\n * crossing the boundary is converted via `new Date(row.createdAt)`.\n *\n * Per-user safety: `getSession`, `updateSession`, `deleteSession`, and\n * `listMessagesForSession` all join `userId` into the WHERE clause — the\n * port's contract is that no caller should be able to read or mutate\n * another user's data even if they've forged a session id.\n */\nimport { and, desc, eq, sql } from 'drizzle-orm';\nimport type { MySql2Database } from 'drizzle-orm/mysql2';\n\nimport {\n DEFAULT_AI_MAX_OUTPUT_TOKENS,\n type AiSettings,\n type AiSettingsPatch,\n type AppendMessageInput,\n type ChatMessage,\n type ChatMessageRole,\n type ChatSession,\n type CreateSessionInput,\n type ListSessionsOpts,\n type PersistencePort\n} from '../../server/ports/types';\nimport { generateChatUid } from '../../server/uid';\n\nimport {\n aiSettings,\n chatMessages,\n chatSessions,\n type AiSettingsRow,\n type ChatMessageRow,\n type ChatSessionRow\n} from './tables';\n\n// ---------------------------------------------------------------------------\n// Row -> domain mappers\n// ---------------------------------------------------------------------------\n\nfunction mapSession(row: ChatSessionRow): ChatSession {\n return {\n id: row.id,\n userId: row.userId,\n title: row.title,\n createdAt: new Date(row.createdAt),\n updatedAt: new Date(row.updatedAt)\n };\n}\n\nfunction mapMessage(row: ChatMessageRow): ChatMessage {\n return {\n id: row.id,\n sessionId: row.sessionId,\n role: row.role as ChatMessageRole,\n question: row.question,\n blocks: row.blocks,\n prose: row.prose,\n errorJson: row.errorJson,\n createdAt: new Date(row.createdAt)\n };\n}\n\nfunction mapSettings(row: AiSettingsRow): AiSettings {\n return {\n toolProvider: row.toolProvider,\n gcpLocation: row.gcpLocation,\n chatInterface: row.chatInterface,\n maxOutputTokens: row.maxOutputTokens,\n rolePrompt: row.rolePrompt,\n gcpProjectId: row.gcpProjectId,\n updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,\n updatedByUserId: row.updatedByUserId\n };\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createDrizzlePersistence(\n db: MySql2Database<any>\n): PersistencePort {\n return {\n // ---------------------------------------------------------------------\n // Sessions\n // ---------------------------------------------------------------------\n\n async createSession(input: CreateSessionInput): Promise<ChatSession> {\n // We generate the UID client-side rather than relying on a DB\n // default — keeps the schema portable (Postgres, SQLite ports\n // would otherwise need their own gen_random_uuid()/etc. shapes).\n const id = generateChatUid();\n await db.insert(chatSessions).values({\n id,\n userId: input.userId,\n title: input.title\n });\n\n const [row] = await db\n .select()\n .from(chatSessions)\n .where(eq(chatSessions.id, id))\n .limit(1);\n\n if (!row) {\n throw new Error(`createSession: row ${id} not found after insert`);\n }\n return mapSession(row);\n },\n\n async getSession(id: string, userId: number): Promise<ChatSession | null> {\n const [row] = await db\n .select()\n .from(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)))\n .limit(1);\n\n return row ? mapSession(row) : null;\n },\n\n async listSessionsForUser(\n userId: number,\n opts?: ListSessionsOpts\n ): Promise<ChatSession[]> {\n const limit = opts?.limit ?? 50;\n const rows = await db\n .select()\n .from(chatSessions)\n .where(eq(chatSessions.userId, userId))\n .orderBy(desc(chatSessions.updatedAt))\n .limit(limit);\n\n return rows.map(mapSession);\n },\n\n async updateSession(\n id: string,\n userId: number,\n patch: { title?: string }\n ): Promise<void> {\n if (patch.title === undefined) return;\n\n await db\n .update(chatSessions)\n .set({ title: patch.title })\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)));\n },\n\n async deleteSession(id: string, userId: number): Promise<void> {\n // Delete messages first (no ON DELETE CASCADE assumed at table level).\n const owned = await db\n .select({ id: chatSessions.id })\n .from(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)))\n .limit(1);\n\n if (owned.length === 0) return;\n\n await db.delete(chatMessages).where(eq(chatMessages.sessionId, id));\n await db\n .delete(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)));\n },\n\n // ---------------------------------------------------------------------\n // Messages\n // ---------------------------------------------------------------------\n\n async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {\n const id = generateChatUid();\n await db.insert(chatMessages).values({\n id,\n sessionId: input.sessionId,\n role: input.role,\n question: input.question ?? null,\n blocks: input.blocks ?? null,\n prose: input.prose ?? null,\n errorJson: input.errorJson ?? null\n });\n\n // Bump the parent session's `updated_at` so `listSessionsForUser`\n // (ordered by updatedAt DESC) reflects \"most recent activity\"\n // rather than creation time. The column has no MySQL ON UPDATE\n // clause, so we set it explicitly here.\n await db\n .update(chatSessions)\n .set({ updatedAt: sql`CURRENT_TIMESTAMP` })\n .where(eq(chatSessions.id, input.sessionId));\n\n const [row] = await db\n .select()\n .from(chatMessages)\n .where(eq(chatMessages.id, id))\n .limit(1);\n\n if (!row) {\n throw new Error(`appendMessage: row ${id} not found after insert`);\n }\n return mapMessage(row);\n },\n\n async listMessagesForSession(\n sessionId: string,\n userId: number\n ): Promise<ChatMessage[]> {\n // Verify ownership before returning rows — never trust the caller to\n // have filtered already.\n const owned = await db\n .select({ id: chatSessions.id })\n .from(chatSessions)\n .where(\n and(eq(chatSessions.id, sessionId), eq(chatSessions.userId, userId))\n )\n .limit(1);\n\n if (owned.length === 0) return [];\n\n const rows = await db\n .select()\n .from(chatMessages)\n .where(eq(chatMessages.sessionId, sessionId))\n .orderBy(chatMessages.createdAt, chatMessages.id);\n\n return rows.map(mapMessage);\n },\n\n // ---------------------------------------------------------------------\n // AI settings (singleton row)\n // ---------------------------------------------------------------------\n\n async getAiSettings(): Promise<AiSettings> {\n const [row] = await db\n .select()\n .from(aiSettings)\n .where(eq(aiSettings.id, 1))\n .limit(1);\n\n if (!row) {\n // Synthesize defaults rather than failing — the row is created on\n // first write via INSERT … ON DUPLICATE KEY UPDATE.\n return {\n toolProvider: 'claude',\n gcpLocation: 'us-east5',\n chatInterface: 'custom',\n maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: null,\n gcpProjectId: null,\n updatedAt: null,\n updatedByUserId: null\n };\n }\n\n return mapSettings(row);\n },\n\n async updateAiSettings(\n patch: AiSettingsPatch,\n byUserId: number\n ): Promise<AiSettings> {\n // Build the values object with the singleton id and any defaults the\n // INSERT branch needs. ON DUPLICATE KEY UPDATE only touches keys in\n // the patch (plus updated_at / updated_by_user_id audit columns).\n const insertValues = {\n id: 1 as const,\n toolProvider: patch.toolProvider ?? 'claude',\n gcpLocation: patch.gcpLocation ?? 'us-east5',\n chatInterface: patch.chatInterface ?? 'custom',\n maxOutputTokens:\n patch.maxOutputTokens ?? DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: patch.rolePrompt ?? null,\n gcpProjectId: patch.gcpProjectId ?? null,\n updatedByUserId: byUserId\n };\n\n const updateSet: Record<string, unknown> = {\n updatedByUserId: byUserId\n };\n if (patch.toolProvider !== undefined) {\n updateSet.toolProvider = patch.toolProvider;\n }\n if (patch.gcpLocation !== undefined) {\n updateSet.gcpLocation = patch.gcpLocation;\n }\n if (patch.chatInterface !== undefined) {\n updateSet.chatInterface = patch.chatInterface;\n }\n if (patch.maxOutputTokens !== undefined) {\n updateSet.maxOutputTokens = patch.maxOutputTokens;\n }\n if (patch.rolePrompt !== undefined) {\n updateSet.rolePrompt = patch.rolePrompt;\n }\n if (patch.gcpProjectId !== undefined) {\n updateSet.gcpProjectId = patch.gcpProjectId;\n }\n\n await db\n .insert(aiSettings)\n .values(insertValues)\n .onDuplicateKeyUpdate({ set: updateSet });\n\n const [row] = await db\n .select()\n .from(aiSettings)\n .where(eq(aiSettings.id, 1))\n .limit(1);\n\n if (!row) {\n throw new Error('updateAiSettings: singleton row missing after upsert');\n }\n return mapSettings(row);\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/drizzle/tables.ts","../../src/server/ports/types.ts","../../src/server/uid.ts","../../src/adapters/drizzle/adapter.ts"],"names":["bigint","varchar","datetime","sql","mysqlTable","tinyint","int","text","index","mysqlEnum","json","randomBytes","eq","and","desc"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Canonical drizzle MySQL table definitions for `@firstlovecenter/ai-chat`.\n *\n * These three tables (`chat_sessions`, `chat_messages`, `ai_settings`) are\n * the data shape the package needs to persist its conversations and global\n * config. The host re-exports them from its own schema so that an existing\n * deployment keeps the same column names, lengths, FKs, and indexes — making\n * the host's data fully portable into and out of the package.\n *\n * Deviations from the host's prior shape are deliberate:\n * - `tool_provider` and `gcp_location` are plain VARCHAR (not enums) so a\n * consumer can register additional providers / regions without a schema\n * migration. Validation against the runtime registries happens at a\n * higher layer.\n * - `chat_interface` is a new VARCHAR column controlling which chat UI\n * (custom vs. vercel-ai) renders globally; default 'custom'.\n *\n * The small column helpers from the host (`bigintPk`, `bigintFk`,\n * `bigintFkNullable`, `createdAt`) are re-defined inline here — this package\n * never imports from the host repo.\n */\nimport { sql } from 'drizzle-orm';\nimport {\n bigint,\n datetime,\n index,\n int,\n json,\n mysqlEnum,\n mysqlTable,\n text,\n tinyint,\n varchar\n} from 'drizzle-orm/mysql-core';\n\n// ---------------------------------------------------------------------------\n// Inline column helpers (mirrors host `src/db/columns.ts` shapes)\n// ---------------------------------------------------------------------------\n\n/** BIGINT UNSIGNED PK with auto-increment, returned as `number`. */\nconst bigintPk = () =>\n bigint('id', { mode: 'number', unsigned: true })\n .notNull()\n .primaryKey()\n .autoincrement();\n\n/** NOT NULL BIGINT UNSIGNED FK column, returned as `number`. */\nconst bigintFk = (name: string) =>\n bigint(name, { mode: 'number', unsigned: true }).notNull();\n\n/**\n * VARCHAR(12) PK for chat session/message rows. The package generates\n * UIDs via `generateChatUid()` (12 chars, ~72 bits, URL-safe alphabet)\n * and writes them on insert — the column has no DB-side default.\n */\nconst uidPk = () =>\n varchar('id', { length: 12 }).notNull().primaryKey();\n\n/** NOT NULL VARCHAR(12) FK referencing a chat-session UID. */\nconst uidFk = (name: string) => varchar(name, { length: 12 }).notNull();\n\n/** Nullable BIGINT UNSIGNED FK column, returned as `number | null`. */\nconst bigintFkNullable = (name: string) =>\n bigint(name, { mode: 'number', unsigned: true });\n\n/** `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP`, mode 'string'. */\nconst createdAt = () =>\n datetime('created_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`);\n\n// ---------------------------------------------------------------------------\n// ai_settings (singleton — always one row at id=1)\n// ---------------------------------------------------------------------------\n\n/**\n * Global AI configuration. Singleton row enforced by `id=1` default + PK.\n * `tool_provider`, `gcp_location`, and `chat_interface` are open VARCHARs;\n * the runtime layer validates against the host's registries before write.\n */\nexport const aiSettings = mysqlTable('ai_settings', {\n id: tinyint('id').notNull().primaryKey().default(1),\n toolProvider: varchar('tool_provider', { length: 32 })\n .notNull()\n .default('claude'),\n gcpLocation: varchar('gcp_location', { length: 32 })\n .notNull()\n .default('us-east5'),\n chatInterface: varchar('chat_interface', { length: 32 })\n .notNull()\n .default('custom'),\n /**\n * Admin-editable GCP project override. NULL means the host's static\n * `VertexPort.projectId` is used. Lets admins flip the active GCP\n * project without redeploying — useful for staging/prod migrations\n * or running multiple FLC tenants from one runtime.\n */\n gcpProjectId: varchar('gcp_project_id', { length: 64 }),\n // Caps the per-turn output budget for both the agent loop AND the prose\n // narrator. Reasoning models charge internal thinking against this; bump\n // it well past 4096 when one is in use.\n maxOutputTokens: int('max_output_tokens').notNull().default(4096),\n // Optional admin-editable role/persona. NULL means the host's static\n // `rolePrompt` (passed to configureAiChat) is used as the fallback.\n rolePrompt: text('role_prompt'),\n updatedAt: datetime('updated_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`),\n updatedByUserId: bigintFkNullable('updated_by_user_id')\n});\n\n// ---------------------------------------------------------------------------\n// chat_sessions (one per conversation)\n// ---------------------------------------------------------------------------\n\nexport const chatSessions = mysqlTable(\n 'chat_sessions',\n {\n id: uidPk(),\n userId: bigintFk('user_id'),\n title: varchar('title', { length: 200 }).notNull(),\n createdAt: createdAt(),\n updatedAt: datetime('updated_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`)\n },\n (t) => ({\n idxUserUpdated: index('idx_chat_session_user_updated').on(t.userId, t.updatedAt)\n })\n);\n\n// ---------------------------------------------------------------------------\n// chat_messages (one row per turn in a chat_session)\n// ---------------------------------------------------------------------------\n\nexport const chatMessageRoleEnum = ['user', 'assistant'] as const;\n\nexport const chatMessages = mysqlTable(\n 'chat_messages',\n {\n id: uidPk(),\n sessionId: uidFk('session_id'),\n role: mysqlEnum('role', chatMessageRoleEnum).notNull(),\n question: text('question'),\n blocks: json('blocks'),\n prose: json('prose'),\n errorJson: json('error_json'),\n createdAt: createdAt()\n },\n (t) => ({\n idxSessionCreated: index('idx_chat_msg_session_created').on(t.sessionId, t.createdAt)\n })\n);\n\n// ---------------------------------------------------------------------------\n// Inferred row types — distinct from the domain types in ports/types.ts.\n// Adapters map these row shapes (datetime as string) to the domain shapes\n// (Date) at the boundary.\n// ---------------------------------------------------------------------------\n\nexport type ChatSessionRow = typeof chatSessions.$inferSelect;\nexport type ChatMessageRow = typeof chatMessages.$inferSelect;\nexport type AiSettingsRow = typeof aiSettings.$inferSelect;\n","/**\n * Ports — the contract between this package and any host that consumes it.\n *\n * Every project-specific concern (auth, scope, persistence, tools, credentials)\n * crosses the boundary as a typed port the host implements and passes into\n * `configureAiChat({...})`. The package itself has zero knowledge of the\n * host's database, RBAC model, or environment.\n *\n * Domain types (ChatSession, ChatMessage, AiSettings, PresentPayload, Block)\n * are pure TS — no ORM coupling. The Drizzle and Prisma adapters each map\n * their respective row types into these shapes so callers see one contract\n * regardless of the chosen ORM.\n */\nimport type { GoogleAuth } from 'google-auth-library';\nimport type { ToolDefinition, ToolContext, SystemBlock } from '../tools/types';\n\n// ---------------------------------------------------------------------------\n// Domain types — used by every port\n// ---------------------------------------------------------------------------\n\n/**\n * Chat session and message identifiers are short URL-safe UIDs (12 chars,\n * ~72 bits of entropy, alphabet matches nanoid's URL-safe set). The\n * package generates them in `createSession`/`appendMessage` so adapters\n * never need to know about auto-increment vs UID.\n */\nexport type ChatSession = {\n id: string;\n userId: number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type ChatMessageRole = 'user' | 'assistant';\n\n/**\n * One message turn. For 'user' rows, `question` carries the raw question text.\n * For 'assistant' rows, `blocks` holds the structured PresentPayload and\n * `prose` holds any paragraph_brief prose collected from the streamed\n * narrator. `errorJson` is set when an assistant turn failed mid-stream.\n */\nexport type ChatMessage = {\n id: string;\n sessionId: string;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\n/**\n * Singleton row controlling global AI runtime settings. All open-string\n * fields are validated at runtime against the registries the host configures\n * — they are not enums in the package's types so consumers can register\n * additional providers / interfaces without a schema change.\n *\n * `maxOutputTokens` caps both the agent loop's per-turn output AND each\n * narrator's prose pass. Reasoning models (e.g. `xai/grok-4.1-fast-reasoning`)\n * charge internal thinking against this budget — set it generously when\n * those are in use.\n *\n * `rolePrompt` is the persona the assistant adopts. When non-null, it\n * takes precedence over the host's static `rolePrompt` configureAiChat\n * option, so admins can edit live in the settings UI.\n *\n * `gcpProjectId` is an admin-editable override for the GCP project that\n * every Vertex API call targets. When non-null, it takes precedence over\n * the host's static `VertexPort.projectId` so admins can flip projects\n * (staging ↔ prod, multi-tenant) without redeploying.\n */\nexport type AiSettings = {\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: string | null;\n gcpProjectId: string | null;\n updatedAt: Date | null;\n updatedByUserId: number | null;\n};\n\n/**\n * Default cap when nothing is persisted. Conservative so non-reasoning\n * models don't pay for headroom they don't need; admins bump it for\n * reasoning models via the settings UI.\n */\nexport const DEFAULT_AI_MAX_OUTPUT_TOKENS = 4096;\n\n// ---------------------------------------------------------------------------\n// AuthPort — the host tells us who's calling\n// ---------------------------------------------------------------------------\n\n/**\n * The host's resolved Scope (org/tenant/RBAC shape) is opaque to the package.\n * `S` is whatever the host wants — typically a discriminated union with at\n * least `userId: number` plus role/tenancy fields.\n */\nexport type AuthOk<S> = { ok: true; scope: S; userId: number };\nexport type AuthFail = { ok: false; response: Response };\nexport type AuthResult<S> = AuthOk<S> | AuthFail;\n\nexport type AuthPort<S = unknown> = {\n /**\n * Resolve the calling user from the request. Returning `{ ok: false }`\n * lets the route hand back the prepared 401/403 response untouched.\n */\n requireAuth(req: Request): Promise<AuthResult<S>>;\n /**\n * Predicate the admin-settings route uses to gate writes. Hosts that\n * don't model admins can return `() => true` if they trust their own\n * routing layer.\n */\n isSuperAdmin(scope: S): boolean;\n};\n\n// ---------------------------------------------------------------------------\n// ScopePort — human-readable scope description fed into the system prompt\n// ---------------------------------------------------------------------------\n\nexport type ScopePort<S = unknown> = {\n /** Short label rendered in the chat UI (e.g. \"Ghana\", \"All countries\"). */\n resolveScopeLabel(scope: S): Promise<string>;\n /** Longer narrative the system prompt uses to describe the data slice. */\n buildScopeSummary(scope: S): Promise<string>;\n};\n\n// ---------------------------------------------------------------------------\n// ToolsPort — host supplies its own tool registry + system prompts\n// ---------------------------------------------------------------------------\n\nexport type ToolsPort = {\n /**\n * Map of tool name → definition. Keys must match `definition.schema.name`.\n * Must include the terminal `present` tool — the agent loop refuses to\n * end a turn without it.\n */\n tools: Record<string, ToolDefinition>;\n /**\n * Build the ordered list of system blocks for one agent run. The host\n * decides how much of the prompt is project-specific (schema doc,\n * semantic-layer doc, scope summary, etc.). Blocks marked `cached: true`\n * become Anthropic ephemeral cache markers and Vertex prefix-cache hints.\n */\n buildSystemBlocks(ctx: ToolContext): Promise<SystemBlock[]>;\n};\n\n// ---------------------------------------------------------------------------\n// VertexPort — credentials are host-supplied; the package never reads env\n// ---------------------------------------------------------------------------\n\nexport type VertexPort = {\n projectId: string;\n /** e.g. 'us-east5' or 'global' — used unless `aiSettings.gcpLocation` overrides per-request. */\n defaultLocation: string;\n /**\n * A constructed `GoogleAuth` instance. Any auth scheme that yields one\n * works (ADC, Workload Identity Federation, split-key env vars,\n * Secret Manager, etc.). The package never reads `process.env.GCP_*`.\n */\n auth: GoogleAuth;\n /** Vertex model IDs pinned by the host (orgs pin their own versions). */\n modelIds: { claude: string; gemini: string };\n};\n\n// ---------------------------------------------------------------------------\n// LoggerPort — optional structured logging\n// ---------------------------------------------------------------------------\n\nexport type LoggerPort = {\n debug(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n error(...args: unknown[]): void;\n};\n\n// ---------------------------------------------------------------------------\n// PersistencePort — domain-shaped queries the package needs\n// ---------------------------------------------------------------------------\n\nexport type CreateSessionInput = {\n userId: number;\n title: string;\n};\n\nexport type AppendMessageInput = {\n sessionId: string;\n role: ChatMessageRole;\n question?: string | null;\n blocks?: unknown | null;\n prose?: unknown | null;\n errorJson?: unknown | null;\n};\n\nexport type ListSessionsOpts = {\n limit?: number;\n};\n\nexport type AiSettingsPatch = {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n /** Pass `null` to clear back to the host's static fallback. */\n rolePrompt?: string | null;\n /** Pass `null` to clear back to the host's static VertexPort.projectId. */\n gcpProjectId?: string | null;\n};\n\n/**\n * The whole reason this package is ORM-agnostic. Implemented by:\n * - createDrizzlePersistence (src/adapters/drizzle/)\n * - createPrismaPersistence (src/adapters/prisma/)\n * - createMemoryPersistence (src/adapters/memory/, internal test use)\n *\n * Routes, agent loop, and UI never touch ORMs directly — they only call\n * methods on this port. A parameterized contract test runs the same\n * assertions against all three adapters to keep them in sync.\n */\nexport type PersistencePort = {\n // Sessions ---------------------------------------------------------------\n createSession(input: CreateSessionInput): Promise<ChatSession>;\n /** Returns null when the session doesn't exist OR doesn't belong to this user. */\n getSession(id: string, userId: number): Promise<ChatSession | null>;\n listSessionsForUser(userId: number, opts?: ListSessionsOpts): Promise<ChatSession[]>;\n /** No-op when session doesn't exist or doesn't belong to user — caller asserted authorisation. */\n updateSession(id: string, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: string, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: string, userId: number): Promise<ChatMessage[]>;\n\n // AI settings (singleton row) -------------------------------------------\n /** Returns the singleton row, applying defaults if it's never been written. */\n getAiSettings(): Promise<AiSettings>;\n /**\n * Upsert. Validates `toolProvider` and `chatInterface` against the runtime\n * registries before writing — invalid values throw before SQL is issued.\n */\n updateAiSettings(patch: AiSettingsPatch, byUserId: number): Promise<AiSettings>;\n};\n","/**\n * Short URL-safe identifier generator for chat session and message rows.\n *\n * 12 chars from a 64-char URL-safe alphabet ≈ 72 bits of entropy. That's\n * collision-safe well past any per-user chat count and short enough to\n * appear unobtrusively in URLs (`/chat/V1StGXR8_Z5j`). The alphabet\n * matches nanoid's URL-safe set so the IDs round-trip cleanly through\n * URL paths, query strings, JSON, and SQL VARCHAR columns.\n */\nimport { randomBytes } from 'crypto';\n\nconst ALPHABET =\n 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';\n\nexport const CHAT_UID_LENGTH = 12;\n\nexport function generateChatUid(size: number = CHAT_UID_LENGTH): string {\n const bytes = randomBytes(size);\n let id = '';\n for (let i = 0; i < size; i += 1) {\n // Mask to 6 bits → uniform index into the 64-char alphabet.\n id += ALPHABET[bytes[i] & 63];\n }\n return id;\n}\n","/**\n * Drizzle MySQL implementation of `PersistencePort`.\n *\n * The package never imports a concrete drizzle client — the host passes a\n * `MySql2Database` (or any compatible drizzle MySQL handle) and we use it\n * to issue the queries this port describes.\n *\n * Boundary mapping: the drizzle MySQL `datetime` columns are declared with\n * `mode: 'string'` to match the host's existing schema, so row reads return\n * ISO strings. The PersistencePort domain types use `Date`, so every row\n * crossing the boundary is converted via `new Date(row.createdAt)`.\n *\n * Per-user safety: `getSession`, `updateSession`, `deleteSession`, and\n * `listMessagesForSession` all join `userId` into the WHERE clause — the\n * port's contract is that no caller should be able to read or mutate\n * another user's data even if they've forged a session id.\n */\nimport { and, desc, eq, sql } from 'drizzle-orm';\nimport type { MySql2Database } from 'drizzle-orm/mysql2';\n\nimport {\n DEFAULT_AI_MAX_OUTPUT_TOKENS,\n type AiSettings,\n type AiSettingsPatch,\n type AppendMessageInput,\n type ChatMessage,\n type ChatMessageRole,\n type ChatSession,\n type CreateSessionInput,\n type ListSessionsOpts,\n type PersistencePort\n} from '../../server/ports/types';\nimport { generateChatUid } from '../../server/uid';\n\nimport {\n aiSettings,\n chatMessages,\n chatSessions,\n type AiSettingsRow,\n type ChatMessageRow,\n type ChatSessionRow\n} from './tables';\n\n// ---------------------------------------------------------------------------\n// Row -> domain mappers\n// ---------------------------------------------------------------------------\n\nfunction mapSession(row: ChatSessionRow): ChatSession {\n return {\n id: row.id,\n userId: row.userId,\n title: row.title,\n createdAt: new Date(row.createdAt),\n updatedAt: new Date(row.updatedAt)\n };\n}\n\n/**\n * Defensive JSON parse for MySQL `JSON` columns. mysql2 (the driver\n * drizzle-orm/mysql2 sits on top of) does NOT auto-parse JSON columns\n * by default — it returns the raw string. Drizzle's `json()` column\n * doesn't always paper over that, so reading `blocks` / `prose` /\n * `errorJson` can land us with a string instead of the structured value\n * the rest of the package expects.\n *\n * If the driver already parsed (recent mysql2 versions, or hosts with\n * `typeCast` configured), the value is already an object/array and we\n * pass it through. If we get a string, we try `JSON.parse`; on failure\n * (the column shouldn't ever contain non-JSON, but defensive) we hand\n * back the original so callers get something instead of throwing.\n */\nfunction parseJsonColumn(v: unknown): unknown {\n if (typeof v !== 'string' || v.length === 0) return v;\n try {\n return JSON.parse(v);\n } catch {\n return v;\n }\n}\n\nfunction mapMessage(row: ChatMessageRow): ChatMessage {\n return {\n id: row.id,\n sessionId: row.sessionId,\n role: row.role as ChatMessageRole,\n question: row.question,\n blocks: parseJsonColumn(row.blocks),\n prose: parseJsonColumn(row.prose),\n errorJson: parseJsonColumn(row.errorJson),\n createdAt: new Date(row.createdAt)\n };\n}\n\nfunction mapSettings(row: AiSettingsRow): AiSettings {\n return {\n toolProvider: row.toolProvider,\n gcpLocation: row.gcpLocation,\n chatInterface: row.chatInterface,\n maxOutputTokens: row.maxOutputTokens,\n rolePrompt: row.rolePrompt,\n gcpProjectId: row.gcpProjectId,\n updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,\n updatedByUserId: row.updatedByUserId\n };\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createDrizzlePersistence(\n db: MySql2Database<any>\n): PersistencePort {\n return {\n // ---------------------------------------------------------------------\n // Sessions\n // ---------------------------------------------------------------------\n\n async createSession(input: CreateSessionInput): Promise<ChatSession> {\n // We generate the UID client-side rather than relying on a DB\n // default — keeps the schema portable (Postgres, SQLite ports\n // would otherwise need their own gen_random_uuid()/etc. shapes).\n const id = generateChatUid();\n await db.insert(chatSessions).values({\n id,\n userId: input.userId,\n title: input.title\n });\n\n const [row] = await db\n .select()\n .from(chatSessions)\n .where(eq(chatSessions.id, id))\n .limit(1);\n\n if (!row) {\n throw new Error(`createSession: row ${id} not found after insert`);\n }\n return mapSession(row);\n },\n\n async getSession(id: string, userId: number): Promise<ChatSession | null> {\n const [row] = await db\n .select()\n .from(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)))\n .limit(1);\n\n return row ? mapSession(row) : null;\n },\n\n async listSessionsForUser(\n userId: number,\n opts?: ListSessionsOpts\n ): Promise<ChatSession[]> {\n const limit = opts?.limit ?? 50;\n const rows = await db\n .select()\n .from(chatSessions)\n .where(eq(chatSessions.userId, userId))\n .orderBy(desc(chatSessions.updatedAt))\n .limit(limit);\n\n return rows.map(mapSession);\n },\n\n async updateSession(\n id: string,\n userId: number,\n patch: { title?: string }\n ): Promise<void> {\n if (patch.title === undefined) return;\n\n await db\n .update(chatSessions)\n .set({ title: patch.title })\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)));\n },\n\n async deleteSession(id: string, userId: number): Promise<void> {\n // Delete messages first (no ON DELETE CASCADE assumed at table level).\n const owned = await db\n .select({ id: chatSessions.id })\n .from(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)))\n .limit(1);\n\n if (owned.length === 0) return;\n\n await db.delete(chatMessages).where(eq(chatMessages.sessionId, id));\n await db\n .delete(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)));\n },\n\n // ---------------------------------------------------------------------\n // Messages\n // ---------------------------------------------------------------------\n\n async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {\n const id = generateChatUid();\n await db.insert(chatMessages).values({\n id,\n sessionId: input.sessionId,\n role: input.role,\n question: input.question ?? null,\n blocks: input.blocks ?? null,\n prose: input.prose ?? null,\n errorJson: input.errorJson ?? null\n });\n\n // Bump the parent session's `updated_at` so `listSessionsForUser`\n // (ordered by updatedAt DESC) reflects \"most recent activity\"\n // rather than creation time. The column has no MySQL ON UPDATE\n // clause, so we set it explicitly here.\n await db\n .update(chatSessions)\n .set({ updatedAt: sql`CURRENT_TIMESTAMP` })\n .where(eq(chatSessions.id, input.sessionId));\n\n const [row] = await db\n .select()\n .from(chatMessages)\n .where(eq(chatMessages.id, id))\n .limit(1);\n\n if (!row) {\n throw new Error(`appendMessage: row ${id} not found after insert`);\n }\n return mapMessage(row);\n },\n\n async listMessagesForSession(\n sessionId: string,\n userId: number\n ): Promise<ChatMessage[]> {\n // Verify ownership before returning rows — never trust the caller to\n // have filtered already.\n const owned = await db\n .select({ id: chatSessions.id })\n .from(chatSessions)\n .where(\n and(eq(chatSessions.id, sessionId), eq(chatSessions.userId, userId))\n )\n .limit(1);\n\n if (owned.length === 0) return [];\n\n const rows = await db\n .select()\n .from(chatMessages)\n .where(eq(chatMessages.sessionId, sessionId))\n .orderBy(chatMessages.createdAt, chatMessages.id);\n\n return rows.map(mapMessage);\n },\n\n // ---------------------------------------------------------------------\n // AI settings (singleton row)\n // ---------------------------------------------------------------------\n\n async getAiSettings(): Promise<AiSettings> {\n const [row] = await db\n .select()\n .from(aiSettings)\n .where(eq(aiSettings.id, 1))\n .limit(1);\n\n if (!row) {\n // Synthesize defaults rather than failing — the row is created on\n // first write via INSERT … ON DUPLICATE KEY UPDATE.\n return {\n toolProvider: 'claude',\n gcpLocation: 'us-east5',\n chatInterface: 'custom',\n maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: null,\n gcpProjectId: null,\n updatedAt: null,\n updatedByUserId: null\n };\n }\n\n return mapSettings(row);\n },\n\n async updateAiSettings(\n patch: AiSettingsPatch,\n byUserId: number\n ): Promise<AiSettings> {\n // Build the values object with the singleton id and any defaults the\n // INSERT branch needs. ON DUPLICATE KEY UPDATE only touches keys in\n // the patch (plus updated_at / updated_by_user_id audit columns).\n const insertValues = {\n id: 1 as const,\n toolProvider: patch.toolProvider ?? 'claude',\n gcpLocation: patch.gcpLocation ?? 'us-east5',\n chatInterface: patch.chatInterface ?? 'custom',\n maxOutputTokens:\n patch.maxOutputTokens ?? DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: patch.rolePrompt ?? null,\n gcpProjectId: patch.gcpProjectId ?? null,\n updatedByUserId: byUserId\n };\n\n const updateSet: Record<string, unknown> = {\n updatedByUserId: byUserId\n };\n if (patch.toolProvider !== undefined) {\n updateSet.toolProvider = patch.toolProvider;\n }\n if (patch.gcpLocation !== undefined) {\n updateSet.gcpLocation = patch.gcpLocation;\n }\n if (patch.chatInterface !== undefined) {\n updateSet.chatInterface = patch.chatInterface;\n }\n if (patch.maxOutputTokens !== undefined) {\n updateSet.maxOutputTokens = patch.maxOutputTokens;\n }\n if (patch.rolePrompt !== undefined) {\n updateSet.rolePrompt = patch.rolePrompt;\n }\n if (patch.gcpProjectId !== undefined) {\n updateSet.gcpProjectId = patch.gcpProjectId;\n }\n\n await db\n .insert(aiSettings)\n .values(insertValues)\n .onDuplicateKeyUpdate({ set: updateSet });\n\n const [row] = await db\n .select()\n .from(aiSettings)\n .where(eq(aiSettings.id, 1))\n .limit(1);\n\n if (!row) {\n throw new Error('updateAiSettings: singleton row missing after upsert');\n }\n return mapSettings(row);\n }\n };\n}\n"]}
|
package/dist/drizzle/index.js
CHANGED
|
@@ -84,15 +84,23 @@ function mapSession(row) {
|
|
|
84
84
|
updatedAt: new Date(row.updatedAt)
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
function parseJsonColumn(v) {
|
|
88
|
+
if (typeof v !== "string" || v.length === 0) return v;
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(v);
|
|
91
|
+
} catch {
|
|
92
|
+
return v;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
87
95
|
function mapMessage(row) {
|
|
88
96
|
return {
|
|
89
97
|
id: row.id,
|
|
90
98
|
sessionId: row.sessionId,
|
|
91
99
|
role: row.role,
|
|
92
100
|
question: row.question,
|
|
93
|
-
blocks: row.blocks,
|
|
94
|
-
prose: row.prose,
|
|
95
|
-
errorJson: row.errorJson,
|
|
101
|
+
blocks: parseJsonColumn(row.blocks),
|
|
102
|
+
prose: parseJsonColumn(row.prose),
|
|
103
|
+
errorJson: parseJsonColumn(row.errorJson),
|
|
96
104
|
createdAt: new Date(row.createdAt)
|
|
97
105
|
};
|
|
98
106
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/drizzle/tables.ts","../../src/server/ports/types.ts","../../src/server/uid.ts","../../src/adapters/drizzle/adapter.ts"],"names":["sql"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Canonical drizzle MySQL table definitions for `@firstlovecenter/ai-chat`.\n *\n * These three tables (`chat_sessions`, `chat_messages`, `ai_settings`) are\n * the data shape the package needs to persist its conversations and global\n * config. The host re-exports them from its own schema so that an existing\n * deployment keeps the same column names, lengths, FKs, and indexes — making\n * the host's data fully portable into and out of the package.\n *\n * Deviations from the host's prior shape are deliberate:\n * - `tool_provider` and `gcp_location` are plain VARCHAR (not enums) so a\n * consumer can register additional providers / regions without a schema\n * migration. Validation against the runtime registries happens at a\n * higher layer.\n * - `chat_interface` is a new VARCHAR column controlling which chat UI\n * (custom vs. vercel-ai) renders globally; default 'custom'.\n *\n * The small column helpers from the host (`bigintPk`, `bigintFk`,\n * `bigintFkNullable`, `createdAt`) are re-defined inline here — this package\n * never imports from the host repo.\n */\nimport { sql } from 'drizzle-orm';\nimport {\n bigint,\n datetime,\n index,\n int,\n json,\n mysqlEnum,\n mysqlTable,\n text,\n tinyint,\n varchar\n} from 'drizzle-orm/mysql-core';\n\n// ---------------------------------------------------------------------------\n// Inline column helpers (mirrors host `src/db/columns.ts` shapes)\n// ---------------------------------------------------------------------------\n\n/** BIGINT UNSIGNED PK with auto-increment, returned as `number`. */\nconst bigintPk = () =>\n bigint('id', { mode: 'number', unsigned: true })\n .notNull()\n .primaryKey()\n .autoincrement();\n\n/** NOT NULL BIGINT UNSIGNED FK column, returned as `number`. */\nconst bigintFk = (name: string) =>\n bigint(name, { mode: 'number', unsigned: true }).notNull();\n\n/**\n * VARCHAR(12) PK for chat session/message rows. The package generates\n * UIDs via `generateChatUid()` (12 chars, ~72 bits, URL-safe alphabet)\n * and writes them on insert — the column has no DB-side default.\n */\nconst uidPk = () =>\n varchar('id', { length: 12 }).notNull().primaryKey();\n\n/** NOT NULL VARCHAR(12) FK referencing a chat-session UID. */\nconst uidFk = (name: string) => varchar(name, { length: 12 }).notNull();\n\n/** Nullable BIGINT UNSIGNED FK column, returned as `number | null`. */\nconst bigintFkNullable = (name: string) =>\n bigint(name, { mode: 'number', unsigned: true });\n\n/** `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP`, mode 'string'. */\nconst createdAt = () =>\n datetime('created_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`);\n\n// ---------------------------------------------------------------------------\n// ai_settings (singleton — always one row at id=1)\n// ---------------------------------------------------------------------------\n\n/**\n * Global AI configuration. Singleton row enforced by `id=1` default + PK.\n * `tool_provider`, `gcp_location`, and `chat_interface` are open VARCHARs;\n * the runtime layer validates against the host's registries before write.\n */\nexport const aiSettings = mysqlTable('ai_settings', {\n id: tinyint('id').notNull().primaryKey().default(1),\n toolProvider: varchar('tool_provider', { length: 32 })\n .notNull()\n .default('claude'),\n gcpLocation: varchar('gcp_location', { length: 32 })\n .notNull()\n .default('us-east5'),\n chatInterface: varchar('chat_interface', { length: 32 })\n .notNull()\n .default('custom'),\n /**\n * Admin-editable GCP project override. NULL means the host's static\n * `VertexPort.projectId` is used. Lets admins flip the active GCP\n * project without redeploying — useful for staging/prod migrations\n * or running multiple FLC tenants from one runtime.\n */\n gcpProjectId: varchar('gcp_project_id', { length: 64 }),\n // Caps the per-turn output budget for both the agent loop AND the prose\n // narrator. Reasoning models charge internal thinking against this; bump\n // it well past 4096 when one is in use.\n maxOutputTokens: int('max_output_tokens').notNull().default(4096),\n // Optional admin-editable role/persona. NULL means the host's static\n // `rolePrompt` (passed to configureAiChat) is used as the fallback.\n rolePrompt: text('role_prompt'),\n updatedAt: datetime('updated_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`),\n updatedByUserId: bigintFkNullable('updated_by_user_id')\n});\n\n// ---------------------------------------------------------------------------\n// chat_sessions (one per conversation)\n// ---------------------------------------------------------------------------\n\nexport const chatSessions = mysqlTable(\n 'chat_sessions',\n {\n id: uidPk(),\n userId: bigintFk('user_id'),\n title: varchar('title', { length: 200 }).notNull(),\n createdAt: createdAt(),\n updatedAt: datetime('updated_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`)\n },\n (t) => ({\n idxUserUpdated: index('idx_chat_session_user_updated').on(t.userId, t.updatedAt)\n })\n);\n\n// ---------------------------------------------------------------------------\n// chat_messages (one row per turn in a chat_session)\n// ---------------------------------------------------------------------------\n\nexport const chatMessageRoleEnum = ['user', 'assistant'] as const;\n\nexport const chatMessages = mysqlTable(\n 'chat_messages',\n {\n id: uidPk(),\n sessionId: uidFk('session_id'),\n role: mysqlEnum('role', chatMessageRoleEnum).notNull(),\n question: text('question'),\n blocks: json('blocks'),\n prose: json('prose'),\n errorJson: json('error_json'),\n createdAt: createdAt()\n },\n (t) => ({\n idxSessionCreated: index('idx_chat_msg_session_created').on(t.sessionId, t.createdAt)\n })\n);\n\n// ---------------------------------------------------------------------------\n// Inferred row types — distinct from the domain types in ports/types.ts.\n// Adapters map these row shapes (datetime as string) to the domain shapes\n// (Date) at the boundary.\n// ---------------------------------------------------------------------------\n\nexport type ChatSessionRow = typeof chatSessions.$inferSelect;\nexport type ChatMessageRow = typeof chatMessages.$inferSelect;\nexport type AiSettingsRow = typeof aiSettings.$inferSelect;\n","/**\n * Ports — the contract between this package and any host that consumes it.\n *\n * Every project-specific concern (auth, scope, persistence, tools, credentials)\n * crosses the boundary as a typed port the host implements and passes into\n * `configureAiChat({...})`. The package itself has zero knowledge of the\n * host's database, RBAC model, or environment.\n *\n * Domain types (ChatSession, ChatMessage, AiSettings, PresentPayload, Block)\n * are pure TS — no ORM coupling. The Drizzle and Prisma adapters each map\n * their respective row types into these shapes so callers see one contract\n * regardless of the chosen ORM.\n */\nimport type { GoogleAuth } from 'google-auth-library';\nimport type { ToolDefinition, ToolContext, SystemBlock } from '../tools/types';\n\n// ---------------------------------------------------------------------------\n// Domain types — used by every port\n// ---------------------------------------------------------------------------\n\n/**\n * Chat session and message identifiers are short URL-safe UIDs (12 chars,\n * ~72 bits of entropy, alphabet matches nanoid's URL-safe set). The\n * package generates them in `createSession`/`appendMessage` so adapters\n * never need to know about auto-increment vs UID.\n */\nexport type ChatSession = {\n id: string;\n userId: number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type ChatMessageRole = 'user' | 'assistant';\n\n/**\n * One message turn. For 'user' rows, `question` carries the raw question text.\n * For 'assistant' rows, `blocks` holds the structured PresentPayload and\n * `prose` holds any paragraph_brief prose collected from the streamed\n * narrator. `errorJson` is set when an assistant turn failed mid-stream.\n */\nexport type ChatMessage = {\n id: string;\n sessionId: string;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\n/**\n * Singleton row controlling global AI runtime settings. All open-string\n * fields are validated at runtime against the registries the host configures\n * — they are not enums in the package's types so consumers can register\n * additional providers / interfaces without a schema change.\n *\n * `maxOutputTokens` caps both the agent loop's per-turn output AND each\n * narrator's prose pass. Reasoning models (e.g. `xai/grok-4.1-fast-reasoning`)\n * charge internal thinking against this budget — set it generously when\n * those are in use.\n *\n * `rolePrompt` is the persona the assistant adopts. When non-null, it\n * takes precedence over the host's static `rolePrompt` configureAiChat\n * option, so admins can edit live in the settings UI.\n *\n * `gcpProjectId` is an admin-editable override for the GCP project that\n * every Vertex API call targets. When non-null, it takes precedence over\n * the host's static `VertexPort.projectId` so admins can flip projects\n * (staging ↔ prod, multi-tenant) without redeploying.\n */\nexport type AiSettings = {\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: string | null;\n gcpProjectId: string | null;\n updatedAt: Date | null;\n updatedByUserId: number | null;\n};\n\n/**\n * Default cap when nothing is persisted. Conservative so non-reasoning\n * models don't pay for headroom they don't need; admins bump it for\n * reasoning models via the settings UI.\n */\nexport const DEFAULT_AI_MAX_OUTPUT_TOKENS = 4096;\n\n// ---------------------------------------------------------------------------\n// AuthPort — the host tells us who's calling\n// ---------------------------------------------------------------------------\n\n/**\n * The host's resolved Scope (org/tenant/RBAC shape) is opaque to the package.\n * `S` is whatever the host wants — typically a discriminated union with at\n * least `userId: number` plus role/tenancy fields.\n */\nexport type AuthOk<S> = { ok: true; scope: S; userId: number };\nexport type AuthFail = { ok: false; response: Response };\nexport type AuthResult<S> = AuthOk<S> | AuthFail;\n\nexport type AuthPort<S = unknown> = {\n /**\n * Resolve the calling user from the request. Returning `{ ok: false }`\n * lets the route hand back the prepared 401/403 response untouched.\n */\n requireAuth(req: Request): Promise<AuthResult<S>>;\n /**\n * Predicate the admin-settings route uses to gate writes. Hosts that\n * don't model admins can return `() => true` if they trust their own\n * routing layer.\n */\n isSuperAdmin(scope: S): boolean;\n};\n\n// ---------------------------------------------------------------------------\n// ScopePort — human-readable scope description fed into the system prompt\n// ---------------------------------------------------------------------------\n\nexport type ScopePort<S = unknown> = {\n /** Short label rendered in the chat UI (e.g. \"Ghana\", \"All countries\"). */\n resolveScopeLabel(scope: S): Promise<string>;\n /** Longer narrative the system prompt uses to describe the data slice. */\n buildScopeSummary(scope: S): Promise<string>;\n};\n\n// ---------------------------------------------------------------------------\n// ToolsPort — host supplies its own tool registry + system prompts\n// ---------------------------------------------------------------------------\n\nexport type ToolsPort = {\n /**\n * Map of tool name → definition. Keys must match `definition.schema.name`.\n * Must include the terminal `present` tool — the agent loop refuses to\n * end a turn without it.\n */\n tools: Record<string, ToolDefinition>;\n /**\n * Build the ordered list of system blocks for one agent run. The host\n * decides how much of the prompt is project-specific (schema doc,\n * semantic-layer doc, scope summary, etc.). Blocks marked `cached: true`\n * become Anthropic ephemeral cache markers and Vertex prefix-cache hints.\n */\n buildSystemBlocks(ctx: ToolContext): Promise<SystemBlock[]>;\n};\n\n// ---------------------------------------------------------------------------\n// VertexPort — credentials are host-supplied; the package never reads env\n// ---------------------------------------------------------------------------\n\nexport type VertexPort = {\n projectId: string;\n /** e.g. 'us-east5' or 'global' — used unless `aiSettings.gcpLocation` overrides per-request. */\n defaultLocation: string;\n /**\n * A constructed `GoogleAuth` instance. Any auth scheme that yields one\n * works (ADC, Workload Identity Federation, split-key env vars,\n * Secret Manager, etc.). The package never reads `process.env.GCP_*`.\n */\n auth: GoogleAuth;\n /** Vertex model IDs pinned by the host (orgs pin their own versions). */\n modelIds: { claude: string; gemini: string };\n};\n\n// ---------------------------------------------------------------------------\n// LoggerPort — optional structured logging\n// ---------------------------------------------------------------------------\n\nexport type LoggerPort = {\n debug(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n error(...args: unknown[]): void;\n};\n\n// ---------------------------------------------------------------------------\n// PersistencePort — domain-shaped queries the package needs\n// ---------------------------------------------------------------------------\n\nexport type CreateSessionInput = {\n userId: number;\n title: string;\n};\n\nexport type AppendMessageInput = {\n sessionId: string;\n role: ChatMessageRole;\n question?: string | null;\n blocks?: unknown | null;\n prose?: unknown | null;\n errorJson?: unknown | null;\n};\n\nexport type ListSessionsOpts = {\n limit?: number;\n};\n\nexport type AiSettingsPatch = {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n /** Pass `null` to clear back to the host's static fallback. */\n rolePrompt?: string | null;\n /** Pass `null` to clear back to the host's static VertexPort.projectId. */\n gcpProjectId?: string | null;\n};\n\n/**\n * The whole reason this package is ORM-agnostic. Implemented by:\n * - createDrizzlePersistence (src/adapters/drizzle/)\n * - createPrismaPersistence (src/adapters/prisma/)\n * - createMemoryPersistence (src/adapters/memory/, internal test use)\n *\n * Routes, agent loop, and UI never touch ORMs directly — they only call\n * methods on this port. A parameterized contract test runs the same\n * assertions against all three adapters to keep them in sync.\n */\nexport type PersistencePort = {\n // Sessions ---------------------------------------------------------------\n createSession(input: CreateSessionInput): Promise<ChatSession>;\n /** Returns null when the session doesn't exist OR doesn't belong to this user. */\n getSession(id: string, userId: number): Promise<ChatSession | null>;\n listSessionsForUser(userId: number, opts?: ListSessionsOpts): Promise<ChatSession[]>;\n /** No-op when session doesn't exist or doesn't belong to user — caller asserted authorisation. */\n updateSession(id: string, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: string, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: string, userId: number): Promise<ChatMessage[]>;\n\n // AI settings (singleton row) -------------------------------------------\n /** Returns the singleton row, applying defaults if it's never been written. */\n getAiSettings(): Promise<AiSettings>;\n /**\n * Upsert. Validates `toolProvider` and `chatInterface` against the runtime\n * registries before writing — invalid values throw before SQL is issued.\n */\n updateAiSettings(patch: AiSettingsPatch, byUserId: number): Promise<AiSettings>;\n};\n","/**\n * Short URL-safe identifier generator for chat session and message rows.\n *\n * 12 chars from a 64-char URL-safe alphabet ≈ 72 bits of entropy. That's\n * collision-safe well past any per-user chat count and short enough to\n * appear unobtrusively in URLs (`/chat/V1StGXR8_Z5j`). The alphabet\n * matches nanoid's URL-safe set so the IDs round-trip cleanly through\n * URL paths, query strings, JSON, and SQL VARCHAR columns.\n */\nimport { randomBytes } from 'crypto';\n\nconst ALPHABET =\n 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';\n\nexport const CHAT_UID_LENGTH = 12;\n\nexport function generateChatUid(size: number = CHAT_UID_LENGTH): string {\n const bytes = randomBytes(size);\n let id = '';\n for (let i = 0; i < size; i += 1) {\n // Mask to 6 bits → uniform index into the 64-char alphabet.\n id += ALPHABET[bytes[i] & 63];\n }\n return id;\n}\n","/**\n * Drizzle MySQL implementation of `PersistencePort`.\n *\n * The package never imports a concrete drizzle client — the host passes a\n * `MySql2Database` (or any compatible drizzle MySQL handle) and we use it\n * to issue the queries this port describes.\n *\n * Boundary mapping: the drizzle MySQL `datetime` columns are declared with\n * `mode: 'string'` to match the host's existing schema, so row reads return\n * ISO strings. The PersistencePort domain types use `Date`, so every row\n * crossing the boundary is converted via `new Date(row.createdAt)`.\n *\n * Per-user safety: `getSession`, `updateSession`, `deleteSession`, and\n * `listMessagesForSession` all join `userId` into the WHERE clause — the\n * port's contract is that no caller should be able to read or mutate\n * another user's data even if they've forged a session id.\n */\nimport { and, desc, eq, sql } from 'drizzle-orm';\nimport type { MySql2Database } from 'drizzle-orm/mysql2';\n\nimport {\n DEFAULT_AI_MAX_OUTPUT_TOKENS,\n type AiSettings,\n type AiSettingsPatch,\n type AppendMessageInput,\n type ChatMessage,\n type ChatMessageRole,\n type ChatSession,\n type CreateSessionInput,\n type ListSessionsOpts,\n type PersistencePort\n} from '../../server/ports/types';\nimport { generateChatUid } from '../../server/uid';\n\nimport {\n aiSettings,\n chatMessages,\n chatSessions,\n type AiSettingsRow,\n type ChatMessageRow,\n type ChatSessionRow\n} from './tables';\n\n// ---------------------------------------------------------------------------\n// Row -> domain mappers\n// ---------------------------------------------------------------------------\n\nfunction mapSession(row: ChatSessionRow): ChatSession {\n return {\n id: row.id,\n userId: row.userId,\n title: row.title,\n createdAt: new Date(row.createdAt),\n updatedAt: new Date(row.updatedAt)\n };\n}\n\nfunction mapMessage(row: ChatMessageRow): ChatMessage {\n return {\n id: row.id,\n sessionId: row.sessionId,\n role: row.role as ChatMessageRole,\n question: row.question,\n blocks: row.blocks,\n prose: row.prose,\n errorJson: row.errorJson,\n createdAt: new Date(row.createdAt)\n };\n}\n\nfunction mapSettings(row: AiSettingsRow): AiSettings {\n return {\n toolProvider: row.toolProvider,\n gcpLocation: row.gcpLocation,\n chatInterface: row.chatInterface,\n maxOutputTokens: row.maxOutputTokens,\n rolePrompt: row.rolePrompt,\n gcpProjectId: row.gcpProjectId,\n updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,\n updatedByUserId: row.updatedByUserId\n };\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createDrizzlePersistence(\n db: MySql2Database<any>\n): PersistencePort {\n return {\n // ---------------------------------------------------------------------\n // Sessions\n // ---------------------------------------------------------------------\n\n async createSession(input: CreateSessionInput): Promise<ChatSession> {\n // We generate the UID client-side rather than relying on a DB\n // default — keeps the schema portable (Postgres, SQLite ports\n // would otherwise need their own gen_random_uuid()/etc. shapes).\n const id = generateChatUid();\n await db.insert(chatSessions).values({\n id,\n userId: input.userId,\n title: input.title\n });\n\n const [row] = await db\n .select()\n .from(chatSessions)\n .where(eq(chatSessions.id, id))\n .limit(1);\n\n if (!row) {\n throw new Error(`createSession: row ${id} not found after insert`);\n }\n return mapSession(row);\n },\n\n async getSession(id: string, userId: number): Promise<ChatSession | null> {\n const [row] = await db\n .select()\n .from(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)))\n .limit(1);\n\n return row ? mapSession(row) : null;\n },\n\n async listSessionsForUser(\n userId: number,\n opts?: ListSessionsOpts\n ): Promise<ChatSession[]> {\n const limit = opts?.limit ?? 50;\n const rows = await db\n .select()\n .from(chatSessions)\n .where(eq(chatSessions.userId, userId))\n .orderBy(desc(chatSessions.updatedAt))\n .limit(limit);\n\n return rows.map(mapSession);\n },\n\n async updateSession(\n id: string,\n userId: number,\n patch: { title?: string }\n ): Promise<void> {\n if (patch.title === undefined) return;\n\n await db\n .update(chatSessions)\n .set({ title: patch.title })\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)));\n },\n\n async deleteSession(id: string, userId: number): Promise<void> {\n // Delete messages first (no ON DELETE CASCADE assumed at table level).\n const owned = await db\n .select({ id: chatSessions.id })\n .from(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)))\n .limit(1);\n\n if (owned.length === 0) return;\n\n await db.delete(chatMessages).where(eq(chatMessages.sessionId, id));\n await db\n .delete(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)));\n },\n\n // ---------------------------------------------------------------------\n // Messages\n // ---------------------------------------------------------------------\n\n async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {\n const id = generateChatUid();\n await db.insert(chatMessages).values({\n id,\n sessionId: input.sessionId,\n role: input.role,\n question: input.question ?? null,\n blocks: input.blocks ?? null,\n prose: input.prose ?? null,\n errorJson: input.errorJson ?? null\n });\n\n // Bump the parent session's `updated_at` so `listSessionsForUser`\n // (ordered by updatedAt DESC) reflects \"most recent activity\"\n // rather than creation time. The column has no MySQL ON UPDATE\n // clause, so we set it explicitly here.\n await db\n .update(chatSessions)\n .set({ updatedAt: sql`CURRENT_TIMESTAMP` })\n .where(eq(chatSessions.id, input.sessionId));\n\n const [row] = await db\n .select()\n .from(chatMessages)\n .where(eq(chatMessages.id, id))\n .limit(1);\n\n if (!row) {\n throw new Error(`appendMessage: row ${id} not found after insert`);\n }\n return mapMessage(row);\n },\n\n async listMessagesForSession(\n sessionId: string,\n userId: number\n ): Promise<ChatMessage[]> {\n // Verify ownership before returning rows — never trust the caller to\n // have filtered already.\n const owned = await db\n .select({ id: chatSessions.id })\n .from(chatSessions)\n .where(\n and(eq(chatSessions.id, sessionId), eq(chatSessions.userId, userId))\n )\n .limit(1);\n\n if (owned.length === 0) return [];\n\n const rows = await db\n .select()\n .from(chatMessages)\n .where(eq(chatMessages.sessionId, sessionId))\n .orderBy(chatMessages.createdAt, chatMessages.id);\n\n return rows.map(mapMessage);\n },\n\n // ---------------------------------------------------------------------\n // AI settings (singleton row)\n // ---------------------------------------------------------------------\n\n async getAiSettings(): Promise<AiSettings> {\n const [row] = await db\n .select()\n .from(aiSettings)\n .where(eq(aiSettings.id, 1))\n .limit(1);\n\n if (!row) {\n // Synthesize defaults rather than failing — the row is created on\n // first write via INSERT … ON DUPLICATE KEY UPDATE.\n return {\n toolProvider: 'claude',\n gcpLocation: 'us-east5',\n chatInterface: 'custom',\n maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: null,\n gcpProjectId: null,\n updatedAt: null,\n updatedByUserId: null\n };\n }\n\n return mapSettings(row);\n },\n\n async updateAiSettings(\n patch: AiSettingsPatch,\n byUserId: number\n ): Promise<AiSettings> {\n // Build the values object with the singleton id and any defaults the\n // INSERT branch needs. ON DUPLICATE KEY UPDATE only touches keys in\n // the patch (plus updated_at / updated_by_user_id audit columns).\n const insertValues = {\n id: 1 as const,\n toolProvider: patch.toolProvider ?? 'claude',\n gcpLocation: patch.gcpLocation ?? 'us-east5',\n chatInterface: patch.chatInterface ?? 'custom',\n maxOutputTokens:\n patch.maxOutputTokens ?? DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: patch.rolePrompt ?? null,\n gcpProjectId: patch.gcpProjectId ?? null,\n updatedByUserId: byUserId\n };\n\n const updateSet: Record<string, unknown> = {\n updatedByUserId: byUserId\n };\n if (patch.toolProvider !== undefined) {\n updateSet.toolProvider = patch.toolProvider;\n }\n if (patch.gcpLocation !== undefined) {\n updateSet.gcpLocation = patch.gcpLocation;\n }\n if (patch.chatInterface !== undefined) {\n updateSet.chatInterface = patch.chatInterface;\n }\n if (patch.maxOutputTokens !== undefined) {\n updateSet.maxOutputTokens = patch.maxOutputTokens;\n }\n if (patch.rolePrompt !== undefined) {\n updateSet.rolePrompt = patch.rolePrompt;\n }\n if (patch.gcpProjectId !== undefined) {\n updateSet.gcpProjectId = patch.gcpProjectId;\n }\n\n await db\n .insert(aiSettings)\n .values(insertValues)\n .onDuplicateKeyUpdate({ set: updateSet });\n\n const [row] = await db\n .select()\n .from(aiSettings)\n .where(eq(aiSettings.id, 1))\n .limit(1);\n\n if (!row) {\n throw new Error('updateAiSettings: singleton row missing after upsert');\n }\n return mapSettings(row);\n }\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/drizzle/tables.ts","../../src/server/ports/types.ts","../../src/server/uid.ts","../../src/adapters/drizzle/adapter.ts"],"names":["sql"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Canonical drizzle MySQL table definitions for `@firstlovecenter/ai-chat`.\n *\n * These three tables (`chat_sessions`, `chat_messages`, `ai_settings`) are\n * the data shape the package needs to persist its conversations and global\n * config. The host re-exports them from its own schema so that an existing\n * deployment keeps the same column names, lengths, FKs, and indexes — making\n * the host's data fully portable into and out of the package.\n *\n * Deviations from the host's prior shape are deliberate:\n * - `tool_provider` and `gcp_location` are plain VARCHAR (not enums) so a\n * consumer can register additional providers / regions without a schema\n * migration. Validation against the runtime registries happens at a\n * higher layer.\n * - `chat_interface` is a new VARCHAR column controlling which chat UI\n * (custom vs. vercel-ai) renders globally; default 'custom'.\n *\n * The small column helpers from the host (`bigintPk`, `bigintFk`,\n * `bigintFkNullable`, `createdAt`) are re-defined inline here — this package\n * never imports from the host repo.\n */\nimport { sql } from 'drizzle-orm';\nimport {\n bigint,\n datetime,\n index,\n int,\n json,\n mysqlEnum,\n mysqlTable,\n text,\n tinyint,\n varchar\n} from 'drizzle-orm/mysql-core';\n\n// ---------------------------------------------------------------------------\n// Inline column helpers (mirrors host `src/db/columns.ts` shapes)\n// ---------------------------------------------------------------------------\n\n/** BIGINT UNSIGNED PK with auto-increment, returned as `number`. */\nconst bigintPk = () =>\n bigint('id', { mode: 'number', unsigned: true })\n .notNull()\n .primaryKey()\n .autoincrement();\n\n/** NOT NULL BIGINT UNSIGNED FK column, returned as `number`. */\nconst bigintFk = (name: string) =>\n bigint(name, { mode: 'number', unsigned: true }).notNull();\n\n/**\n * VARCHAR(12) PK for chat session/message rows. The package generates\n * UIDs via `generateChatUid()` (12 chars, ~72 bits, URL-safe alphabet)\n * and writes them on insert — the column has no DB-side default.\n */\nconst uidPk = () =>\n varchar('id', { length: 12 }).notNull().primaryKey();\n\n/** NOT NULL VARCHAR(12) FK referencing a chat-session UID. */\nconst uidFk = (name: string) => varchar(name, { length: 12 }).notNull();\n\n/** Nullable BIGINT UNSIGNED FK column, returned as `number | null`. */\nconst bigintFkNullable = (name: string) =>\n bigint(name, { mode: 'number', unsigned: true });\n\n/** `created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP`, mode 'string'. */\nconst createdAt = () =>\n datetime('created_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`);\n\n// ---------------------------------------------------------------------------\n// ai_settings (singleton — always one row at id=1)\n// ---------------------------------------------------------------------------\n\n/**\n * Global AI configuration. Singleton row enforced by `id=1` default + PK.\n * `tool_provider`, `gcp_location`, and `chat_interface` are open VARCHARs;\n * the runtime layer validates against the host's registries before write.\n */\nexport const aiSettings = mysqlTable('ai_settings', {\n id: tinyint('id').notNull().primaryKey().default(1),\n toolProvider: varchar('tool_provider', { length: 32 })\n .notNull()\n .default('claude'),\n gcpLocation: varchar('gcp_location', { length: 32 })\n .notNull()\n .default('us-east5'),\n chatInterface: varchar('chat_interface', { length: 32 })\n .notNull()\n .default('custom'),\n /**\n * Admin-editable GCP project override. NULL means the host's static\n * `VertexPort.projectId` is used. Lets admins flip the active GCP\n * project without redeploying — useful for staging/prod migrations\n * or running multiple FLC tenants from one runtime.\n */\n gcpProjectId: varchar('gcp_project_id', { length: 64 }),\n // Caps the per-turn output budget for both the agent loop AND the prose\n // narrator. Reasoning models charge internal thinking against this; bump\n // it well past 4096 when one is in use.\n maxOutputTokens: int('max_output_tokens').notNull().default(4096),\n // Optional admin-editable role/persona. NULL means the host's static\n // `rolePrompt` (passed to configureAiChat) is used as the fallback.\n rolePrompt: text('role_prompt'),\n updatedAt: datetime('updated_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`),\n updatedByUserId: bigintFkNullable('updated_by_user_id')\n});\n\n// ---------------------------------------------------------------------------\n// chat_sessions (one per conversation)\n// ---------------------------------------------------------------------------\n\nexport const chatSessions = mysqlTable(\n 'chat_sessions',\n {\n id: uidPk(),\n userId: bigintFk('user_id'),\n title: varchar('title', { length: 200 }).notNull(),\n createdAt: createdAt(),\n updatedAt: datetime('updated_at', { mode: 'string' })\n .notNull()\n .default(sql`CURRENT_TIMESTAMP`)\n },\n (t) => ({\n idxUserUpdated: index('idx_chat_session_user_updated').on(t.userId, t.updatedAt)\n })\n);\n\n// ---------------------------------------------------------------------------\n// chat_messages (one row per turn in a chat_session)\n// ---------------------------------------------------------------------------\n\nexport const chatMessageRoleEnum = ['user', 'assistant'] as const;\n\nexport const chatMessages = mysqlTable(\n 'chat_messages',\n {\n id: uidPk(),\n sessionId: uidFk('session_id'),\n role: mysqlEnum('role', chatMessageRoleEnum).notNull(),\n question: text('question'),\n blocks: json('blocks'),\n prose: json('prose'),\n errorJson: json('error_json'),\n createdAt: createdAt()\n },\n (t) => ({\n idxSessionCreated: index('idx_chat_msg_session_created').on(t.sessionId, t.createdAt)\n })\n);\n\n// ---------------------------------------------------------------------------\n// Inferred row types — distinct from the domain types in ports/types.ts.\n// Adapters map these row shapes (datetime as string) to the domain shapes\n// (Date) at the boundary.\n// ---------------------------------------------------------------------------\n\nexport type ChatSessionRow = typeof chatSessions.$inferSelect;\nexport type ChatMessageRow = typeof chatMessages.$inferSelect;\nexport type AiSettingsRow = typeof aiSettings.$inferSelect;\n","/**\n * Ports — the contract between this package and any host that consumes it.\n *\n * Every project-specific concern (auth, scope, persistence, tools, credentials)\n * crosses the boundary as a typed port the host implements and passes into\n * `configureAiChat({...})`. The package itself has zero knowledge of the\n * host's database, RBAC model, or environment.\n *\n * Domain types (ChatSession, ChatMessage, AiSettings, PresentPayload, Block)\n * are pure TS — no ORM coupling. The Drizzle and Prisma adapters each map\n * their respective row types into these shapes so callers see one contract\n * regardless of the chosen ORM.\n */\nimport type { GoogleAuth } from 'google-auth-library';\nimport type { ToolDefinition, ToolContext, SystemBlock } from '../tools/types';\n\n// ---------------------------------------------------------------------------\n// Domain types — used by every port\n// ---------------------------------------------------------------------------\n\n/**\n * Chat session and message identifiers are short URL-safe UIDs (12 chars,\n * ~72 bits of entropy, alphabet matches nanoid's URL-safe set). The\n * package generates them in `createSession`/`appendMessage` so adapters\n * never need to know about auto-increment vs UID.\n */\nexport type ChatSession = {\n id: string;\n userId: number;\n title: string;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type ChatMessageRole = 'user' | 'assistant';\n\n/**\n * One message turn. For 'user' rows, `question` carries the raw question text.\n * For 'assistant' rows, `blocks` holds the structured PresentPayload and\n * `prose` holds any paragraph_brief prose collected from the streamed\n * narrator. `errorJson` is set when an assistant turn failed mid-stream.\n */\nexport type ChatMessage = {\n id: string;\n sessionId: string;\n role: ChatMessageRole;\n question: string | null;\n blocks: unknown | null;\n prose: unknown | null;\n errorJson: unknown | null;\n createdAt: Date;\n};\n\n/**\n * Singleton row controlling global AI runtime settings. All open-string\n * fields are validated at runtime against the registries the host configures\n * — they are not enums in the package's types so consumers can register\n * additional providers / interfaces without a schema change.\n *\n * `maxOutputTokens` caps both the agent loop's per-turn output AND each\n * narrator's prose pass. Reasoning models (e.g. `xai/grok-4.1-fast-reasoning`)\n * charge internal thinking against this budget — set it generously when\n * those are in use.\n *\n * `rolePrompt` is the persona the assistant adopts. When non-null, it\n * takes precedence over the host's static `rolePrompt` configureAiChat\n * option, so admins can edit live in the settings UI.\n *\n * `gcpProjectId` is an admin-editable override for the GCP project that\n * every Vertex API call targets. When non-null, it takes precedence over\n * the host's static `VertexPort.projectId` so admins can flip projects\n * (staging ↔ prod, multi-tenant) without redeploying.\n */\nexport type AiSettings = {\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: string | null;\n gcpProjectId: string | null;\n updatedAt: Date | null;\n updatedByUserId: number | null;\n};\n\n/**\n * Default cap when nothing is persisted. Conservative so non-reasoning\n * models don't pay for headroom they don't need; admins bump it for\n * reasoning models via the settings UI.\n */\nexport const DEFAULT_AI_MAX_OUTPUT_TOKENS = 4096;\n\n// ---------------------------------------------------------------------------\n// AuthPort — the host tells us who's calling\n// ---------------------------------------------------------------------------\n\n/**\n * The host's resolved Scope (org/tenant/RBAC shape) is opaque to the package.\n * `S` is whatever the host wants — typically a discriminated union with at\n * least `userId: number` plus role/tenancy fields.\n */\nexport type AuthOk<S> = { ok: true; scope: S; userId: number };\nexport type AuthFail = { ok: false; response: Response };\nexport type AuthResult<S> = AuthOk<S> | AuthFail;\n\nexport type AuthPort<S = unknown> = {\n /**\n * Resolve the calling user from the request. Returning `{ ok: false }`\n * lets the route hand back the prepared 401/403 response untouched.\n */\n requireAuth(req: Request): Promise<AuthResult<S>>;\n /**\n * Predicate the admin-settings route uses to gate writes. Hosts that\n * don't model admins can return `() => true` if they trust their own\n * routing layer.\n */\n isSuperAdmin(scope: S): boolean;\n};\n\n// ---------------------------------------------------------------------------\n// ScopePort — human-readable scope description fed into the system prompt\n// ---------------------------------------------------------------------------\n\nexport type ScopePort<S = unknown> = {\n /** Short label rendered in the chat UI (e.g. \"Ghana\", \"All countries\"). */\n resolveScopeLabel(scope: S): Promise<string>;\n /** Longer narrative the system prompt uses to describe the data slice. */\n buildScopeSummary(scope: S): Promise<string>;\n};\n\n// ---------------------------------------------------------------------------\n// ToolsPort — host supplies its own tool registry + system prompts\n// ---------------------------------------------------------------------------\n\nexport type ToolsPort = {\n /**\n * Map of tool name → definition. Keys must match `definition.schema.name`.\n * Must include the terminal `present` tool — the agent loop refuses to\n * end a turn without it.\n */\n tools: Record<string, ToolDefinition>;\n /**\n * Build the ordered list of system blocks for one agent run. The host\n * decides how much of the prompt is project-specific (schema doc,\n * semantic-layer doc, scope summary, etc.). Blocks marked `cached: true`\n * become Anthropic ephemeral cache markers and Vertex prefix-cache hints.\n */\n buildSystemBlocks(ctx: ToolContext): Promise<SystemBlock[]>;\n};\n\n// ---------------------------------------------------------------------------\n// VertexPort — credentials are host-supplied; the package never reads env\n// ---------------------------------------------------------------------------\n\nexport type VertexPort = {\n projectId: string;\n /** e.g. 'us-east5' or 'global' — used unless `aiSettings.gcpLocation` overrides per-request. */\n defaultLocation: string;\n /**\n * A constructed `GoogleAuth` instance. Any auth scheme that yields one\n * works (ADC, Workload Identity Federation, split-key env vars,\n * Secret Manager, etc.). The package never reads `process.env.GCP_*`.\n */\n auth: GoogleAuth;\n /** Vertex model IDs pinned by the host (orgs pin their own versions). */\n modelIds: { claude: string; gemini: string };\n};\n\n// ---------------------------------------------------------------------------\n// LoggerPort — optional structured logging\n// ---------------------------------------------------------------------------\n\nexport type LoggerPort = {\n debug(...args: unknown[]): void;\n info(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n error(...args: unknown[]): void;\n};\n\n// ---------------------------------------------------------------------------\n// PersistencePort — domain-shaped queries the package needs\n// ---------------------------------------------------------------------------\n\nexport type CreateSessionInput = {\n userId: number;\n title: string;\n};\n\nexport type AppendMessageInput = {\n sessionId: string;\n role: ChatMessageRole;\n question?: string | null;\n blocks?: unknown | null;\n prose?: unknown | null;\n errorJson?: unknown | null;\n};\n\nexport type ListSessionsOpts = {\n limit?: number;\n};\n\nexport type AiSettingsPatch = {\n toolProvider?: string;\n gcpLocation?: string;\n chatInterface?: string;\n maxOutputTokens?: number;\n /** Pass `null` to clear back to the host's static fallback. */\n rolePrompt?: string | null;\n /** Pass `null` to clear back to the host's static VertexPort.projectId. */\n gcpProjectId?: string | null;\n};\n\n/**\n * The whole reason this package is ORM-agnostic. Implemented by:\n * - createDrizzlePersistence (src/adapters/drizzle/)\n * - createPrismaPersistence (src/adapters/prisma/)\n * - createMemoryPersistence (src/adapters/memory/, internal test use)\n *\n * Routes, agent loop, and UI never touch ORMs directly — they only call\n * methods on this port. A parameterized contract test runs the same\n * assertions against all three adapters to keep them in sync.\n */\nexport type PersistencePort = {\n // Sessions ---------------------------------------------------------------\n createSession(input: CreateSessionInput): Promise<ChatSession>;\n /** Returns null when the session doesn't exist OR doesn't belong to this user. */\n getSession(id: string, userId: number): Promise<ChatSession | null>;\n listSessionsForUser(userId: number, opts?: ListSessionsOpts): Promise<ChatSession[]>;\n /** No-op when session doesn't exist or doesn't belong to user — caller asserted authorisation. */\n updateSession(id: string, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: string, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: string, userId: number): Promise<ChatMessage[]>;\n\n // AI settings (singleton row) -------------------------------------------\n /** Returns the singleton row, applying defaults if it's never been written. */\n getAiSettings(): Promise<AiSettings>;\n /**\n * Upsert. Validates `toolProvider` and `chatInterface` against the runtime\n * registries before writing — invalid values throw before SQL is issued.\n */\n updateAiSettings(patch: AiSettingsPatch, byUserId: number): Promise<AiSettings>;\n};\n","/**\n * Short URL-safe identifier generator for chat session and message rows.\n *\n * 12 chars from a 64-char URL-safe alphabet ≈ 72 bits of entropy. That's\n * collision-safe well past any per-user chat count and short enough to\n * appear unobtrusively in URLs (`/chat/V1StGXR8_Z5j`). The alphabet\n * matches nanoid's URL-safe set so the IDs round-trip cleanly through\n * URL paths, query strings, JSON, and SQL VARCHAR columns.\n */\nimport { randomBytes } from 'crypto';\n\nconst ALPHABET =\n 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';\n\nexport const CHAT_UID_LENGTH = 12;\n\nexport function generateChatUid(size: number = CHAT_UID_LENGTH): string {\n const bytes = randomBytes(size);\n let id = '';\n for (let i = 0; i < size; i += 1) {\n // Mask to 6 bits → uniform index into the 64-char alphabet.\n id += ALPHABET[bytes[i] & 63];\n }\n return id;\n}\n","/**\n * Drizzle MySQL implementation of `PersistencePort`.\n *\n * The package never imports a concrete drizzle client — the host passes a\n * `MySql2Database` (or any compatible drizzle MySQL handle) and we use it\n * to issue the queries this port describes.\n *\n * Boundary mapping: the drizzle MySQL `datetime` columns are declared with\n * `mode: 'string'` to match the host's existing schema, so row reads return\n * ISO strings. The PersistencePort domain types use `Date`, so every row\n * crossing the boundary is converted via `new Date(row.createdAt)`.\n *\n * Per-user safety: `getSession`, `updateSession`, `deleteSession`, and\n * `listMessagesForSession` all join `userId` into the WHERE clause — the\n * port's contract is that no caller should be able to read or mutate\n * another user's data even if they've forged a session id.\n */\nimport { and, desc, eq, sql } from 'drizzle-orm';\nimport type { MySql2Database } from 'drizzle-orm/mysql2';\n\nimport {\n DEFAULT_AI_MAX_OUTPUT_TOKENS,\n type AiSettings,\n type AiSettingsPatch,\n type AppendMessageInput,\n type ChatMessage,\n type ChatMessageRole,\n type ChatSession,\n type CreateSessionInput,\n type ListSessionsOpts,\n type PersistencePort\n} from '../../server/ports/types';\nimport { generateChatUid } from '../../server/uid';\n\nimport {\n aiSettings,\n chatMessages,\n chatSessions,\n type AiSettingsRow,\n type ChatMessageRow,\n type ChatSessionRow\n} from './tables';\n\n// ---------------------------------------------------------------------------\n// Row -> domain mappers\n// ---------------------------------------------------------------------------\n\nfunction mapSession(row: ChatSessionRow): ChatSession {\n return {\n id: row.id,\n userId: row.userId,\n title: row.title,\n createdAt: new Date(row.createdAt),\n updatedAt: new Date(row.updatedAt)\n };\n}\n\n/**\n * Defensive JSON parse for MySQL `JSON` columns. mysql2 (the driver\n * drizzle-orm/mysql2 sits on top of) does NOT auto-parse JSON columns\n * by default — it returns the raw string. Drizzle's `json()` column\n * doesn't always paper over that, so reading `blocks` / `prose` /\n * `errorJson` can land us with a string instead of the structured value\n * the rest of the package expects.\n *\n * If the driver already parsed (recent mysql2 versions, or hosts with\n * `typeCast` configured), the value is already an object/array and we\n * pass it through. If we get a string, we try `JSON.parse`; on failure\n * (the column shouldn't ever contain non-JSON, but defensive) we hand\n * back the original so callers get something instead of throwing.\n */\nfunction parseJsonColumn(v: unknown): unknown {\n if (typeof v !== 'string' || v.length === 0) return v;\n try {\n return JSON.parse(v);\n } catch {\n return v;\n }\n}\n\nfunction mapMessage(row: ChatMessageRow): ChatMessage {\n return {\n id: row.id,\n sessionId: row.sessionId,\n role: row.role as ChatMessageRole,\n question: row.question,\n blocks: parseJsonColumn(row.blocks),\n prose: parseJsonColumn(row.prose),\n errorJson: parseJsonColumn(row.errorJson),\n createdAt: new Date(row.createdAt)\n };\n}\n\nfunction mapSettings(row: AiSettingsRow): AiSettings {\n return {\n toolProvider: row.toolProvider,\n gcpLocation: row.gcpLocation,\n chatInterface: row.chatInterface,\n maxOutputTokens: row.maxOutputTokens,\n rolePrompt: row.rolePrompt,\n gcpProjectId: row.gcpProjectId,\n updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,\n updatedByUserId: row.updatedByUserId\n };\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\nexport function createDrizzlePersistence(\n db: MySql2Database<any>\n): PersistencePort {\n return {\n // ---------------------------------------------------------------------\n // Sessions\n // ---------------------------------------------------------------------\n\n async createSession(input: CreateSessionInput): Promise<ChatSession> {\n // We generate the UID client-side rather than relying on a DB\n // default — keeps the schema portable (Postgres, SQLite ports\n // would otherwise need their own gen_random_uuid()/etc. shapes).\n const id = generateChatUid();\n await db.insert(chatSessions).values({\n id,\n userId: input.userId,\n title: input.title\n });\n\n const [row] = await db\n .select()\n .from(chatSessions)\n .where(eq(chatSessions.id, id))\n .limit(1);\n\n if (!row) {\n throw new Error(`createSession: row ${id} not found after insert`);\n }\n return mapSession(row);\n },\n\n async getSession(id: string, userId: number): Promise<ChatSession | null> {\n const [row] = await db\n .select()\n .from(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)))\n .limit(1);\n\n return row ? mapSession(row) : null;\n },\n\n async listSessionsForUser(\n userId: number,\n opts?: ListSessionsOpts\n ): Promise<ChatSession[]> {\n const limit = opts?.limit ?? 50;\n const rows = await db\n .select()\n .from(chatSessions)\n .where(eq(chatSessions.userId, userId))\n .orderBy(desc(chatSessions.updatedAt))\n .limit(limit);\n\n return rows.map(mapSession);\n },\n\n async updateSession(\n id: string,\n userId: number,\n patch: { title?: string }\n ): Promise<void> {\n if (patch.title === undefined) return;\n\n await db\n .update(chatSessions)\n .set({ title: patch.title })\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)));\n },\n\n async deleteSession(id: string, userId: number): Promise<void> {\n // Delete messages first (no ON DELETE CASCADE assumed at table level).\n const owned = await db\n .select({ id: chatSessions.id })\n .from(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)))\n .limit(1);\n\n if (owned.length === 0) return;\n\n await db.delete(chatMessages).where(eq(chatMessages.sessionId, id));\n await db\n .delete(chatSessions)\n .where(and(eq(chatSessions.id, id), eq(chatSessions.userId, userId)));\n },\n\n // ---------------------------------------------------------------------\n // Messages\n // ---------------------------------------------------------------------\n\n async appendMessage(input: AppendMessageInput): Promise<ChatMessage> {\n const id = generateChatUid();\n await db.insert(chatMessages).values({\n id,\n sessionId: input.sessionId,\n role: input.role,\n question: input.question ?? null,\n blocks: input.blocks ?? null,\n prose: input.prose ?? null,\n errorJson: input.errorJson ?? null\n });\n\n // Bump the parent session's `updated_at` so `listSessionsForUser`\n // (ordered by updatedAt DESC) reflects \"most recent activity\"\n // rather than creation time. The column has no MySQL ON UPDATE\n // clause, so we set it explicitly here.\n await db\n .update(chatSessions)\n .set({ updatedAt: sql`CURRENT_TIMESTAMP` })\n .where(eq(chatSessions.id, input.sessionId));\n\n const [row] = await db\n .select()\n .from(chatMessages)\n .where(eq(chatMessages.id, id))\n .limit(1);\n\n if (!row) {\n throw new Error(`appendMessage: row ${id} not found after insert`);\n }\n return mapMessage(row);\n },\n\n async listMessagesForSession(\n sessionId: string,\n userId: number\n ): Promise<ChatMessage[]> {\n // Verify ownership before returning rows — never trust the caller to\n // have filtered already.\n const owned = await db\n .select({ id: chatSessions.id })\n .from(chatSessions)\n .where(\n and(eq(chatSessions.id, sessionId), eq(chatSessions.userId, userId))\n )\n .limit(1);\n\n if (owned.length === 0) return [];\n\n const rows = await db\n .select()\n .from(chatMessages)\n .where(eq(chatMessages.sessionId, sessionId))\n .orderBy(chatMessages.createdAt, chatMessages.id);\n\n return rows.map(mapMessage);\n },\n\n // ---------------------------------------------------------------------\n // AI settings (singleton row)\n // ---------------------------------------------------------------------\n\n async getAiSettings(): Promise<AiSettings> {\n const [row] = await db\n .select()\n .from(aiSettings)\n .where(eq(aiSettings.id, 1))\n .limit(1);\n\n if (!row) {\n // Synthesize defaults rather than failing — the row is created on\n // first write via INSERT … ON DUPLICATE KEY UPDATE.\n return {\n toolProvider: 'claude',\n gcpLocation: 'us-east5',\n chatInterface: 'custom',\n maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: null,\n gcpProjectId: null,\n updatedAt: null,\n updatedByUserId: null\n };\n }\n\n return mapSettings(row);\n },\n\n async updateAiSettings(\n patch: AiSettingsPatch,\n byUserId: number\n ): Promise<AiSettings> {\n // Build the values object with the singleton id and any defaults the\n // INSERT branch needs. ON DUPLICATE KEY UPDATE only touches keys in\n // the patch (plus updated_at / updated_by_user_id audit columns).\n const insertValues = {\n id: 1 as const,\n toolProvider: patch.toolProvider ?? 'claude',\n gcpLocation: patch.gcpLocation ?? 'us-east5',\n chatInterface: patch.chatInterface ?? 'custom',\n maxOutputTokens:\n patch.maxOutputTokens ?? DEFAULT_AI_MAX_OUTPUT_TOKENS,\n rolePrompt: patch.rolePrompt ?? null,\n gcpProjectId: patch.gcpProjectId ?? null,\n updatedByUserId: byUserId\n };\n\n const updateSet: Record<string, unknown> = {\n updatedByUserId: byUserId\n };\n if (patch.toolProvider !== undefined) {\n updateSet.toolProvider = patch.toolProvider;\n }\n if (patch.gcpLocation !== undefined) {\n updateSet.gcpLocation = patch.gcpLocation;\n }\n if (patch.chatInterface !== undefined) {\n updateSet.chatInterface = patch.chatInterface;\n }\n if (patch.maxOutputTokens !== undefined) {\n updateSet.maxOutputTokens = patch.maxOutputTokens;\n }\n if (patch.rolePrompt !== undefined) {\n updateSet.rolePrompt = patch.rolePrompt;\n }\n if (patch.gcpProjectId !== undefined) {\n updateSet.gcpProjectId = patch.gcpProjectId;\n }\n\n await db\n .insert(aiSettings)\n .values(insertValues)\n .onDuplicateKeyUpdate({ set: updateSet });\n\n const [row] = await db\n .select()\n .from(aiSettings)\n .where(eq(aiSettings.id, 1))\n .limit(1);\n\n if (!row) {\n throw new Error('updateAiSettings: singleton row missing after upsert');\n }\n return mapSettings(row);\n }\n };\n}\n"]}
|