@firstlovecenter/ai-chat 0.6.0 → 0.7.0

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 CHANGED
@@ -5,6 +5,37 @@ 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.7.0] — 2026-05-08
9
+
10
+ Admin-configurable GCP project. A third runtime knob on `ai_settings` joins `max_output_tokens` (0.6.0) and `role_prompt` (0.6.0): admins can flip the active GCP project from the settings UI without redeploying — useful for staging/prod migrations and running multiple FLC tenants from one runtime.
11
+
12
+ ### Added
13
+
14
+ - **`ai_settings.gcp_project_id`** (`VARCHAR(64) NULL`) — admin-editable GCP project override on `AiSettings`. When non-null, takes precedence over the host's static `VertexPort.projectId` (passed in via `configureAiChat({ vertex: { projectId } })`); when null, the static fallback is used. Mirrors the resolution semantics of `role_prompt` introduced in 0.6.0.
15
+
16
+ ### Changed
17
+
18
+ - **`AiSettings` and `AiSettingsPatch` types** gain `gcpProjectId: string | null` (patch type optional; passing `null` clears the override). All three adapters (drizzle, prisma, memory) thread the field through identically.
19
+ - **`agent-custom` and `agent-vercel` routes** now consult `aiSettings.gcpProjectId` per request and feed the resolved value into both the tool-provider construction and (custom path) the narrator stream — Vertex calls now target the admin-set project when one is configured.
20
+ - **`configureAiChat({...}).runAgent`** picks up the override on the same per-request basis as the routes — non-HTTP entry points (jobs, eval harnesses) honour the admin-set project too.
21
+ - **`admin-settings` PATCH** validates `gcp_project_id` (string or null; whitespace-only canonicalised to null; non-null values must match GCP's project-id format `[a-z][-a-z0-9]{4,28}[a-z0-9]` and stay within 64 chars). New error code: `invalid_gcp_project_id`. The GET response now includes the field.
22
+
23
+ ### Drizzle migration
24
+
25
+ Hosts using the drizzle adapter run `pnpm drizzle:generate` after upgrading to pick up the new column:
26
+
27
+ ```sql
28
+ ALTER TABLE `ai_settings` ADD `gcp_project_id` varchar(64);
29
+ ```
30
+
31
+ Prisma hosts re-paste the updated `ai_settings` model fragment from `node_modules/@firstlovecenter/ai-chat/prisma/chat-models.prisma` and run `pnpm prisma migrate dev`.
32
+
33
+ ## [0.6.1] — 2026-05-08
34
+
35
+ ### Fixed
36
+
37
+ - **`isBlockEmpty()` crash on malformed historical rows** — `AnswerBlocks`'s render-time filter called `f.trim()` on every entry of `key_facts` / `items`, which threw `Cannot read properties of undefined (reading 'trim')` when a stored chat had a `null` or non-string entry in either array. Stored chats from truncated reasoning-model responses (pre-0.6.0, before `max_output_tokens` was admin-configurable) commonly contained such entries. The new `isBlockEmpty` treats null / undefined / non-string entries as blank via a small `isBlankString()` helper, and guards `.length === 0` checks behind `Array.isArray(...)` so a row missing the array entirely also no-ops cleanly. Auto-resuming an older session now renders without throwing.
38
+
8
39
  ## [0.6.0] — 2026-05-08
9
40
 
10
41
  Admin-configurable runtime knobs and chat continuity. Two new fields on `ai_settings` give admins control over the token budget and the assistant's persona without a redeploy, and both chat UIs now auto-resume the most recent session on return.
@@ -146,6 +177,8 @@ Initial public release on npm under `@firstlovecenter/ai-chat`.
146
177
  - Dual ESM + CJS output via `tsup` with full `.d.ts` (and `.d.cts`) generation.
147
178
  - UI bundle gets a post-build `'use client';` directive injection (tsup otherwise strips module-level directives during bundling, breaking RSC consumers that import the UI from a server file).
148
179
 
180
+ [0.7.0]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.6.1...v0.7.0
181
+ [0.6.1]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.6.0...v0.6.1
149
182
  [0.6.0]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.5.0...v0.6.0
150
183
  [0.5.0]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.2.3...v0.5.0
151
184
  [0.2.3]: https://github.com/firstlovecenter/flc-ai-chat/compare/v0.2.2...v0.2.3
@@ -13,6 +13,13 @@ var aiSettings = mysqlCore.mysqlTable("ai_settings", {
13
13
  toolProvider: mysqlCore.varchar("tool_provider", { length: 32 }).notNull().default("claude"),
14
14
  gcpLocation: mysqlCore.varchar("gcp_location", { length: 32 }).notNull().default("us-east5"),
15
15
  chatInterface: mysqlCore.varchar("chat_interface", { length: 32 }).notNull().default("custom"),
16
+ /**
17
+ * Admin-editable GCP project override. NULL means the host's static
18
+ * `VertexPort.projectId` is used. Lets admins flip the active GCP
19
+ * project without redeploying — useful for staging/prod migrations
20
+ * or running multiple FLC tenants from one runtime.
21
+ */
22
+ gcpProjectId: mysqlCore.varchar("gcp_project_id", { length: 64 }),
16
23
  // Caps the per-turn output budget for both the agent loop AND the prose
17
24
  // narrator. Reasoning models charge internal thinking against this; bump
18
25
  // it well past 4096 when one is in use.
@@ -86,6 +93,7 @@ function mapSettings(row) {
86
93
  chatInterface: row.chatInterface,
87
94
  maxOutputTokens: row.maxOutputTokens,
88
95
  rolePrompt: row.rolePrompt,
96
+ gcpProjectId: row.gcpProjectId,
89
97
  updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,
90
98
  updatedByUserId: row.updatedByUserId
91
99
  };
@@ -171,6 +179,7 @@ function createDrizzlePersistence(db) {
171
179
  chatInterface: "custom",
172
180
  maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,
173
181
  rolePrompt: null,
182
+ gcpProjectId: null,
174
183
  updatedAt: null,
175
184
  updatedByUserId: null
176
185
  };
@@ -185,6 +194,7 @@ function createDrizzlePersistence(db) {
185
194
  chatInterface: patch.chatInterface ?? "custom",
186
195
  maxOutputTokens: patch.maxOutputTokens ?? DEFAULT_AI_MAX_OUTPUT_TOKENS,
187
196
  rolePrompt: patch.rolePrompt ?? null,
197
+ gcpProjectId: patch.gcpProjectId ?? null,
188
198
  updatedByUserId: byUserId
189
199
  };
190
200
  const updateSet = {
@@ -205,6 +215,9 @@ function createDrizzlePersistence(db) {
205
215
  if (patch.rolePrompt !== void 0) {
206
216
  updateSet.rolePrompt = patch.rolePrompt;
207
217
  }
218
+ if (patch.gcpProjectId !== void 0) {
219
+ updateSet.gcpProjectId = patch.gcpProjectId;
220
+ }
208
221
  await db.insert(aiSettings).values(insertValues).onDuplicateKeyUpdate({ set: updateSet });
209
222
  const [row] = await db.select().from(aiSettings).where(drizzleOrm.eq(aiSettings.id, 1)).limit(1);
210
223
  if (!row) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/drizzle/tables.ts","../../src/server/ports/types.ts","../../src/adapters/drizzle/adapter.ts"],"names":["bigint","datetime","sql","mysqlTable","tinyint","varchar","int","text","index","mysqlEnum","json","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/** 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 // 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: bigintPk(),\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: bigintPk(),\n sessionId: bigintFk('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\nexport type ChatSession = {\n id: number;\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: number;\n sessionId: number;\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 */\nexport type AiSettings = {\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: 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: number;\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};\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: number, 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: number, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: number, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: number, 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 * 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 } 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';\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 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 const inserted = await db\n .insert(chatSessions)\n .values({\n userId: input.userId,\n title: input.title\n })\n .$returningId();\n\n const id = inserted[0]?.id;\n if (id == null) {\n throw new Error('createSession: insert returned no id');\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: number, 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: number,\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: number, 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 inserted = await db\n .insert(chatMessages)\n .values({\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 .$returningId();\n\n const id = inserted[0]?.id;\n if (id == null) {\n throw new Error('appendMessage: insert returned no id');\n }\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: number,\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 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 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\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/adapters/drizzle/adapter.ts"],"names":["bigint","datetime","sql","mysqlTable","tinyint","varchar","int","text","index","mysqlEnum","json","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/** 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: bigintPk(),\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: bigintPk(),\n sessionId: bigintFk('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\nexport type ChatSession = {\n id: number;\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: number;\n sessionId: number;\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: number;\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: number, 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: number, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: number, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: number, 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 * 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 } 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';\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 const inserted = await db\n .insert(chatSessions)\n .values({\n userId: input.userId,\n title: input.title\n })\n .$returningId();\n\n const id = inserted[0]?.id;\n if (id == null) {\n throw new Error('createSession: insert returned no id');\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: number, 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: number,\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: number, 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 inserted = await db\n .insert(chatMessages)\n .values({\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 .$returningId();\n\n const id = inserted[0]?.id;\n if (id == null) {\n throw new Error('appendMessage: insert returned no id');\n }\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: number,\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,6 +1,6 @@
1
1
  import * as drizzle_orm_mysql_core from 'drizzle-orm/mysql-core';
2
2
  import { MySql2Database } from 'drizzle-orm/mysql2';
3
- import { c as PersistencePort } from '../types-CQntnyDJ.cjs';
3
+ import { c as PersistencePort } from '../types-BnwUkqKb.cjs';
4
4
  import 'google-auth-library';
5
5
  import 'zod';
6
6
 
@@ -81,6 +81,23 @@ declare const aiSettings: drizzle_orm_mysql_core.MySqlTableWithColumns<{
81
81
  identity: undefined;
82
82
  generated: undefined;
83
83
  }, object>;
84
+ gcpProjectId: drizzle_orm_mysql_core.MySqlColumn<{
85
+ name: "gcp_project_id";
86
+ tableName: "ai_settings";
87
+ dataType: "string";
88
+ columnType: "MySqlVarChar";
89
+ data: string;
90
+ driverParam: string | number;
91
+ notNull: false;
92
+ hasDefault: false;
93
+ isPrimaryKey: false;
94
+ isAutoincrement: false;
95
+ hasRuntimeDefault: false;
96
+ enumValues: [string, ...string[]];
97
+ baseColumn: never;
98
+ identity: undefined;
99
+ generated: undefined;
100
+ }, object>;
84
101
  maxOutputTokens: drizzle_orm_mysql_core.MySqlColumn<{
85
102
  name: "max_output_tokens";
86
103
  tableName: "ai_settings";
@@ -1,6 +1,6 @@
1
1
  import * as drizzle_orm_mysql_core from 'drizzle-orm/mysql-core';
2
2
  import { MySql2Database } from 'drizzle-orm/mysql2';
3
- import { c as PersistencePort } from '../types-CQntnyDJ.js';
3
+ import { c as PersistencePort } from '../types-BnwUkqKb.js';
4
4
  import 'google-auth-library';
5
5
  import 'zod';
6
6
 
@@ -81,6 +81,23 @@ declare const aiSettings: drizzle_orm_mysql_core.MySqlTableWithColumns<{
81
81
  identity: undefined;
82
82
  generated: undefined;
83
83
  }, object>;
84
+ gcpProjectId: drizzle_orm_mysql_core.MySqlColumn<{
85
+ name: "gcp_project_id";
86
+ tableName: "ai_settings";
87
+ dataType: "string";
88
+ columnType: "MySqlVarChar";
89
+ data: string;
90
+ driverParam: string | number;
91
+ notNull: false;
92
+ hasDefault: false;
93
+ isPrimaryKey: false;
94
+ isAutoincrement: false;
95
+ hasRuntimeDefault: false;
96
+ enumValues: [string, ...string[]];
97
+ baseColumn: never;
98
+ identity: undefined;
99
+ generated: undefined;
100
+ }, object>;
84
101
  maxOutputTokens: drizzle_orm_mysql_core.MySqlColumn<{
85
102
  name: "max_output_tokens";
86
103
  tableName: "ai_settings";
@@ -11,6 +11,13 @@ var aiSettings = mysqlTable("ai_settings", {
11
11
  toolProvider: varchar("tool_provider", { length: 32 }).notNull().default("claude"),
12
12
  gcpLocation: varchar("gcp_location", { length: 32 }).notNull().default("us-east5"),
13
13
  chatInterface: varchar("chat_interface", { length: 32 }).notNull().default("custom"),
14
+ /**
15
+ * Admin-editable GCP project override. NULL means the host's static
16
+ * `VertexPort.projectId` is used. Lets admins flip the active GCP
17
+ * project without redeploying — useful for staging/prod migrations
18
+ * or running multiple FLC tenants from one runtime.
19
+ */
20
+ gcpProjectId: varchar("gcp_project_id", { length: 64 }),
14
21
  // Caps the per-turn output budget for both the agent loop AND the prose
15
22
  // narrator. Reasoning models charge internal thinking against this; bump
16
23
  // it well past 4096 when one is in use.
@@ -84,6 +91,7 @@ function mapSettings(row) {
84
91
  chatInterface: row.chatInterface,
85
92
  maxOutputTokens: row.maxOutputTokens,
86
93
  rolePrompt: row.rolePrompt,
94
+ gcpProjectId: row.gcpProjectId,
87
95
  updatedAt: row.updatedAt ? new Date(row.updatedAt) : null,
88
96
  updatedByUserId: row.updatedByUserId
89
97
  };
@@ -169,6 +177,7 @@ function createDrizzlePersistence(db) {
169
177
  chatInterface: "custom",
170
178
  maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,
171
179
  rolePrompt: null,
180
+ gcpProjectId: null,
172
181
  updatedAt: null,
173
182
  updatedByUserId: null
174
183
  };
@@ -183,6 +192,7 @@ function createDrizzlePersistence(db) {
183
192
  chatInterface: patch.chatInterface ?? "custom",
184
193
  maxOutputTokens: patch.maxOutputTokens ?? DEFAULT_AI_MAX_OUTPUT_TOKENS,
185
194
  rolePrompt: patch.rolePrompt ?? null,
195
+ gcpProjectId: patch.gcpProjectId ?? null,
186
196
  updatedByUserId: byUserId
187
197
  };
188
198
  const updateSet = {
@@ -203,6 +213,9 @@ function createDrizzlePersistence(db) {
203
213
  if (patch.rolePrompt !== void 0) {
204
214
  updateSet.rolePrompt = patch.rolePrompt;
205
215
  }
216
+ if (patch.gcpProjectId !== void 0) {
217
+ updateSet.gcpProjectId = patch.gcpProjectId;
218
+ }
206
219
  await db.insert(aiSettings).values(insertValues).onDuplicateKeyUpdate({ set: updateSet });
207
220
  const [row] = await db.select().from(aiSettings).where(eq(aiSettings.id, 1)).limit(1);
208
221
  if (!row) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/drizzle/tables.ts","../../src/server/ports/types.ts","../../src/adapters/drizzle/adapter.ts"],"names":[],"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/** 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 // 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: bigintPk(),\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: bigintPk(),\n sessionId: bigintFk('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\nexport type ChatSession = {\n id: number;\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: number;\n sessionId: number;\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 */\nexport type AiSettings = {\n toolProvider: string;\n gcpLocation: string;\n chatInterface: string;\n maxOutputTokens: number;\n rolePrompt: 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: number;\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};\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: number, 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: number, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: number, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: number, 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 * 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 } 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';\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 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 const inserted = await db\n .insert(chatSessions)\n .values({\n userId: input.userId,\n title: input.title\n })\n .$returningId();\n\n const id = inserted[0]?.id;\n if (id == null) {\n throw new Error('createSession: insert returned no id');\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: number, 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: number,\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: number, 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 inserted = await db\n .insert(chatMessages)\n .values({\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 .$returningId();\n\n const id = inserted[0]?.id;\n if (id == null) {\n throw new Error('appendMessage: insert returned no id');\n }\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: number,\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 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 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\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/adapters/drizzle/adapter.ts"],"names":[],"mappings":";;;;AAwCA,IAAM,QAAA,GAAW,MACf,MAAA,CAAO,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,QAAA,EAAU,IAAA,EAAM,CAAA,CAC5C,OAAA,EAAQ,CACR,UAAA,GACA,aAAA,EAAc;AAGnB,IAAM,QAAA,GAAW,CAAC,IAAA,KAChB,MAAA,CAAO,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,QAAA,EAAU,IAAA,EAAM,CAAA,CAAE,OAAA,EAAQ;AAG3D,IAAM,gBAAA,GAAmB,CAAC,IAAA,KACxB,MAAA,CAAO,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,QAAA,EAAU,IAAA,EAAM,CAAA;AAGjD,IAAM,SAAA,GAAY,MAChB,QAAA,CAAS,YAAA,EAAc,EAAE,IAAA,EAAM,QAAA,EAAU,CAAA,CACtC,OAAA,EAAQ,CACR,OAAA,CAAQ,GAAA,CAAA,iBAAA,CAAsB,CAAA;AAW5B,IAAM,UAAA,GAAa,WAAW,aAAA,EAAe;AAAA,EAClD,EAAA,EAAI,QAAQ,IAAI,CAAA,CAAE,SAAQ,CAAE,UAAA,EAAW,CAAE,OAAA,CAAQ,CAAC,CAAA;AAAA,EAClD,YAAA,EAAc,OAAA,CAAQ,eAAA,EAAiB,EAAE,MAAA,EAAQ,EAAA,EAAI,CAAA,CAClD,OAAA,EAAQ,CACR,OAAA,CAAQ,QAAQ,CAAA;AAAA,EACnB,WAAA,EAAa,OAAA,CAAQ,cAAA,EAAgB,EAAE,MAAA,EAAQ,EAAA,EAAI,CAAA,CAChD,OAAA,EAAQ,CACR,OAAA,CAAQ,UAAU,CAAA;AAAA,EACrB,aAAA,EAAe,OAAA,CAAQ,gBAAA,EAAkB,EAAE,MAAA,EAAQ,EAAA,EAAI,CAAA,CACpD,OAAA,EAAQ,CACR,OAAA,CAAQ,QAAQ,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOnB,cAAc,OAAA,CAAQ,gBAAA,EAAkB,EAAE,MAAA,EAAQ,IAAI,CAAA;AAAA;AAAA;AAAA;AAAA,EAItD,iBAAiB,GAAA,CAAI,mBAAmB,EAAE,OAAA,EAAQ,CAAE,QAAQ,IAAI,CAAA;AAAA;AAAA;AAAA,EAGhE,UAAA,EAAY,KAAK,aAAa,CAAA;AAAA,EAC9B,SAAA,EAAW,QAAA,CAAS,YAAA,EAAc,EAAE,IAAA,EAAM,QAAA,EAAU,CAAA,CACjD,OAAA,EAAQ,CACR,OAAA,CAAQ,GAAA,CAAA,iBAAA,CAAsB,CAAA;AAAA,EACjC,eAAA,EAAiB,iBAAiB,oBAAoB;AACxD,CAAC;AAMM,IAAM,YAAA,GAAe,UAAA;AAAA,EAC1B,eAAA;AAAA,EACA;AAAA,IACE,IAAI,QAAA,EAAS;AAAA,IACb,MAAA,EAAQ,SAAS,SAAS,CAAA;AAAA,IAC1B,KAAA,EAAO,QAAQ,OAAA,EAAS,EAAE,QAAQ,GAAA,EAAK,EAAE,OAAA,EAAQ;AAAA,IACjD,WAAW,SAAA,EAAU;AAAA,IACrB,SAAA,EAAW,QAAA,CAAS,YAAA,EAAc,EAAE,IAAA,EAAM,QAAA,EAAU,CAAA,CACjD,OAAA,EAAQ,CACR,OAAA,CAAQ,GAAA,CAAA,iBAAA,CAAsB;AAAA,GACnC;AAAA,EACA,CAAC,CAAA,MAAO;AAAA,IACN,cAAA,EAAgB,MAAM,+BAA+B,CAAA,CAAE,GAAG,CAAA,CAAE,MAAA,EAAQ,EAAE,SAAS;AAAA,GACjF;AACF;AAMO,IAAM,mBAAA,GAAsB,CAAC,MAAA,EAAQ,WAAW,CAAA;AAEhD,IAAM,YAAA,GAAe,UAAA;AAAA,EAC1B,eAAA;AAAA,EACA;AAAA,IACE,IAAI,QAAA,EAAS;AAAA,IACb,SAAA,EAAW,SAAS,YAAY,CAAA;AAAA,IAChC,IAAA,EAAM,SAAA,CAAU,MAAA,EAAQ,mBAAmB,EAAE,OAAA,EAAQ;AAAA,IACrD,QAAA,EAAU,KAAK,UAAU,CAAA;AAAA,IACzB,MAAA,EAAQ,KAAK,QAAQ,CAAA;AAAA,IACrB,KAAA,EAAO,KAAK,OAAO,CAAA;AAAA,IACnB,SAAA,EAAW,KAAK,YAAY,CAAA;AAAA,IAC5B,WAAW,SAAA;AAAU,GACvB;AAAA,EACA,CAAC,CAAA,MAAO;AAAA,IACN,iBAAA,EAAmB,MAAM,8BAA8B,CAAA,CAAE,GAAG,CAAA,CAAE,SAAA,EAAW,EAAE,SAAS;AAAA,GACtF;AACF;;;AC1DO,IAAM,4BAAA,GAA+B,IAAA;;;ACrC5C,SAAS,WAAW,GAAA,EAAkC;AACpD,EAAA,OAAO;AAAA,IACL,IAAI,GAAA,CAAI,EAAA;AAAA,IACR,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,OAAO,GAAA,CAAI,KAAA;AAAA,IACX,SAAA,EAAW,IAAI,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA;AAAA,IACjC,SAAA,EAAW,IAAI,IAAA,CAAK,GAAA,CAAI,SAAS;AAAA,GACnC;AACF;AAEA,SAAS,WAAW,GAAA,EAAkC;AACpD,EAAA,OAAO;AAAA,IACL,IAAI,GAAA,CAAI,EAAA;AAAA,IACR,WAAW,GAAA,CAAI,SAAA;AAAA,IACf,MAAM,GAAA,CAAI,IAAA;AAAA,IACV,UAAU,GAAA,CAAI,QAAA;AAAA,IACd,QAAQ,GAAA,CAAI,MAAA;AAAA,IACZ,OAAO,GAAA,CAAI,KAAA;AAAA,IACX,WAAW,GAAA,CAAI,SAAA;AAAA,IACf,SAAA,EAAW,IAAI,IAAA,CAAK,GAAA,CAAI,SAAS;AAAA,GACnC;AACF;AAEA,SAAS,YAAY,GAAA,EAAgC;AACnD,EAAA,OAAO;AAAA,IACL,cAAc,GAAA,CAAI,YAAA;AAAA,IAClB,aAAa,GAAA,CAAI,WAAA;AAAA,IACjB,eAAe,GAAA,CAAI,aAAA;AAAA,IACnB,iBAAiB,GAAA,CAAI,eAAA;AAAA,IACrB,YAAY,GAAA,CAAI,UAAA;AAAA,IAChB,cAAc,GAAA,CAAI,YAAA;AAAA,IAClB,WAAW,GAAA,CAAI,SAAA,GAAY,IAAI,IAAA,CAAK,GAAA,CAAI,SAAS,CAAA,GAAI,IAAA;AAAA,IACrD,iBAAiB,GAAA,CAAI;AAAA,GACvB;AACF;AAMO,SAAS,yBACd,EAAA,EACiB;AACjB,EAAA,OAAO;AAAA;AAAA;AAAA;AAAA,IAKL,MAAM,cAAc,KAAA,EAAiD;AACnE,MAAA,MAAM,WAAW,MAAM,EAAA,CACpB,MAAA,CAAO,YAAY,EACnB,MAAA,CAAO;AAAA,QACN,QAAQ,KAAA,CAAM,MAAA;AAAA,QACd,OAAO,KAAA,CAAM;AAAA,OACd,EACA,YAAA,EAAa;AAEhB,MAAA,MAAM,EAAA,GAAK,QAAA,CAAS,CAAC,CAAA,EAAG,EAAA;AACxB,MAAA,IAAI,MAAM,IAAA,EAAM;AACd,QAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,MACxD;AAEA,MAAA,MAAM,CAAC,GAAG,CAAA,GAAI,MAAM,EAAA,CACjB,MAAA,GACA,IAAA,CAAK,YAAY,CAAA,CACjB,KAAA,CAAM,GAAG,YAAA,CAAa,EAAA,EAAI,EAAE,CAAC,CAAA,CAC7B,MAAM,CAAC,CAAA;AAEV,MAAA,IAAI,CAAC,GAAA,EAAK;AACR,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,EAAE,CAAA,uBAAA,CAAyB,CAAA;AAAA,MACnE;AACA,MAAA,OAAO,WAAW,GAAG,CAAA;AAAA,IACvB,CAAA;AAAA,IAEA,MAAM,UAAA,CAAW,EAAA,EAAY,MAAA,EAA6C;AACxE,MAAA,MAAM,CAAC,GAAG,CAAA,GAAI,MAAM,EAAA,CACjB,QAAO,CACP,IAAA,CAAK,YAAY,CAAA,CACjB,KAAA,CAAM,GAAA,CAAI,GAAG,YAAA,CAAa,EAAA,EAAI,EAAE,CAAA,EAAG,EAAA,CAAG,YAAA,CAAa,MAAA,EAAQ,MAAM,CAAC,CAAC,CAAA,CACnE,KAAA,CAAM,CAAC,CAAA;AAEV,MAAA,OAAO,GAAA,GAAM,UAAA,CAAW,GAAG,CAAA,GAAI,IAAA;AAAA,IACjC,CAAA;AAAA,IAEA,MAAM,mBAAA,CACJ,MAAA,EACA,IAAA,EACwB;AACxB,MAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,IAAS,EAAA;AAC7B,MAAA,MAAM,IAAA,GAAO,MAAM,EAAA,CAChB,MAAA,GACA,IAAA,CAAK,YAAY,CAAA,CACjB,KAAA,CAAM,EAAA,CAAG,YAAA,CAAa,QAAQ,MAAM,CAAC,EACrC,OAAA,CAAQ,IAAA,CAAK,aAAa,SAAS,CAAC,CAAA,CACpC,KAAA,CAAM,KAAK,CAAA;AAEd,MAAA,OAAO,IAAA,CAAK,IAAI,UAAU,CAAA;AAAA,IAC5B,CAAA;AAAA,IAEA,MAAM,aAAA,CACJ,EAAA,EACA,MAAA,EACA,KAAA,EACe;AACf,MAAA,IAAI,KAAA,CAAM,UAAU,MAAA,EAAW;AAE/B,MAAA,MAAM,EAAA,CACH,OAAO,YAAY,CAAA,CACnB,IAAI,EAAE,KAAA,EAAO,KAAA,CAAM,KAAA,EAAO,CAAA,CAC1B,MAAM,GAAA,CAAI,EAAA,CAAG,YAAA,CAAa,EAAA,EAAI,EAAE,CAAA,EAAG,GAAG,YAAA,CAAa,MAAA,EAAQ,MAAM,CAAC,CAAC,CAAA;AAAA,IACxE,CAAA;AAAA,IAEA,MAAM,aAAA,CAAc,EAAA,EAAY,MAAA,EAA+B;AAE7D,MAAA,MAAM,KAAA,GAAQ,MAAM,EAAA,CACjB,MAAA,CAAO,EAAE,EAAA,EAAI,YAAA,CAAa,EAAA,EAAI,CAAA,CAC9B,IAAA,CAAK,YAAY,CAAA,CACjB,KAAA,CAAM,GAAA,CAAI,EAAA,CAAG,YAAA,CAAa,EAAA,EAAI,EAAE,CAAA,EAAG,EAAA,CAAG,YAAA,CAAa,MAAA,EAAQ,MAAM,CAAC,CAAC,CAAA,CACnE,KAAA,CAAM,CAAC,CAAA;AAEV,MAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AAExB,MAAA,MAAM,EAAA,CAAG,OAAO,YAAY,CAAA,CAAE,MAAM,EAAA,CAAG,YAAA,CAAa,SAAA,EAAW,EAAE,CAAC,CAAA;AAClE,MAAA,MAAM,GACH,MAAA,CAAO,YAAY,CAAA,CACnB,KAAA,CAAM,IAAI,EAAA,CAAG,YAAA,CAAa,EAAA,EAAI,EAAE,GAAG,EAAA,CAAG,YAAA,CAAa,MAAA,EAAQ,MAAM,CAAC,CAAC,CAAA;AAAA,IACxE,CAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,cAAc,KAAA,EAAiD;AACnE,MAAA,MAAM,WAAW,MAAM,EAAA,CACpB,MAAA,CAAO,YAAY,EACnB,MAAA,CAAO;AAAA,QACN,WAAW,KAAA,CAAM,SAAA;AAAA,QACjB,MAAM,KAAA,CAAM,IAAA;AAAA,QACZ,QAAA,EAAU,MAAM,QAAA,IAAY,IAAA;AAAA,QAC5B,MAAA,EAAQ,MAAM,MAAA,IAAU,IAAA;AAAA,QACxB,KAAA,EAAO,MAAM,KAAA,IAAS,IAAA;AAAA,QACtB,SAAA,EAAW,MAAM,SAAA,IAAa;AAAA,OAC/B,EACA,YAAA,EAAa;AAEhB,MAAA,MAAM,EAAA,GAAK,QAAA,CAAS,CAAC,CAAA,EAAG,EAAA;AACxB,MAAA,IAAI,MAAM,IAAA,EAAM;AACd,QAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,MACxD;AAEA,MAAA,MAAM,CAAC,GAAG,CAAA,GAAI,MAAM,EAAA,CACjB,MAAA,GACA,IAAA,CAAK,YAAY,CAAA,CACjB,KAAA,CAAM,GAAG,YAAA,CAAa,EAAA,EAAI,EAAE,CAAC,CAAA,CAC7B,MAAM,CAAC,CAAA;AAEV,MAAA,IAAI,CAAC,GAAA,EAAK;AACR,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,EAAE,CAAA,uBAAA,CAAyB,CAAA;AAAA,MACnE;AACA,MAAA,OAAO,WAAW,GAAG,CAAA;AAAA,IACvB,CAAA;AAAA,IAEA,MAAM,sBAAA,CACJ,SAAA,EACA,MAAA,EACwB;AAGxB,MAAA,MAAM,KAAA,GAAQ,MAAM,EAAA,CACjB,MAAA,CAAO,EAAE,EAAA,EAAI,YAAA,CAAa,EAAA,EAAI,CAAA,CAC9B,IAAA,CAAK,YAAY,CAAA,CACjB,KAAA;AAAA,QACC,GAAA,CAAI,EAAA,CAAG,YAAA,CAAa,EAAA,EAAI,SAAS,GAAG,EAAA,CAAG,YAAA,CAAa,MAAA,EAAQ,MAAM,CAAC;AAAA,OACrE,CACC,MAAM,CAAC,CAAA;AAEV,MAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,EAAC;AAEhC,MAAA,MAAM,OAAO,MAAM,EAAA,CAChB,QAAO,CACP,IAAA,CAAK,YAAY,CAAA,CACjB,KAAA,CAAM,GAAG,YAAA,CAAa,SAAA,EAAW,SAAS,CAAC,CAAA,CAC3C,QAAQ,YAAA,CAAa,SAAA,EAAW,aAAa,EAAE,CAAA;AAElD,MAAA,OAAO,IAAA,CAAK,IAAI,UAAU,CAAA;AAAA,IAC5B,CAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,aAAA,GAAqC;AACzC,MAAA,MAAM,CAAC,GAAG,CAAA,GAAI,MAAM,EAAA,CACjB,MAAA,GACA,IAAA,CAAK,UAAU,CAAA,CACf,KAAA,CAAM,GAAG,UAAA,CAAW,EAAA,EAAI,CAAC,CAAC,CAAA,CAC1B,MAAM,CAAC,CAAA;AAEV,MAAA,IAAI,CAAC,GAAA,EAAK;AAGR,QAAA,OAAO;AAAA,UACL,YAAA,EAAc,QAAA;AAAA,UACd,WAAA,EAAa,UAAA;AAAA,UACb,aAAA,EAAe,QAAA;AAAA,UACf,eAAA,EAAiB,4BAAA;AAAA,UACjB,UAAA,EAAY,IAAA;AAAA,UACZ,YAAA,EAAc,IAAA;AAAA,UACd,SAAA,EAAW,IAAA;AAAA,UACX,eAAA,EAAiB;AAAA,SACnB;AAAA,MACF;AAEA,MAAA,OAAO,YAAY,GAAG,CAAA;AAAA,IACxB,CAAA;AAAA,IAEA,MAAM,gBAAA,CACJ,KAAA,EACA,QAAA,EACqB;AAIrB,MAAA,MAAM,YAAA,GAAe;AAAA,QACnB,EAAA,EAAI,CAAA;AAAA,QACJ,YAAA,EAAc,MAAM,YAAA,IAAgB,QAAA;AAAA,QACpC,WAAA,EAAa,MAAM,WAAA,IAAe,UAAA;AAAA,QAClC,aAAA,EAAe,MAAM,aAAA,IAAiB,QAAA;AAAA,QACtC,eAAA,EACE,MAAM,eAAA,IAAmB,4BAAA;AAAA,QAC3B,UAAA,EAAY,MAAM,UAAA,IAAc,IAAA;AAAA,QAChC,YAAA,EAAc,MAAM,YAAA,IAAgB,IAAA;AAAA,QACpC,eAAA,EAAiB;AAAA,OACnB;AAEA,MAAA,MAAM,SAAA,GAAqC;AAAA,QACzC,eAAA,EAAiB;AAAA,OACnB;AACA,MAAA,IAAI,KAAA,CAAM,iBAAiB,MAAA,EAAW;AACpC,QAAA,SAAA,CAAU,eAAe,KAAA,CAAM,YAAA;AAAA,MACjC;AACA,MAAA,IAAI,KAAA,CAAM,gBAAgB,MAAA,EAAW;AACnC,QAAA,SAAA,CAAU,cAAc,KAAA,CAAM,WAAA;AAAA,MAChC;AACA,MAAA,IAAI,KAAA,CAAM,kBAAkB,MAAA,EAAW;AACrC,QAAA,SAAA,CAAU,gBAAgB,KAAA,CAAM,aAAA;AAAA,MAClC;AACA,MAAA,IAAI,KAAA,CAAM,oBAAoB,MAAA,EAAW;AACvC,QAAA,SAAA,CAAU,kBAAkB,KAAA,CAAM,eAAA;AAAA,MACpC;AACA,MAAA,IAAI,KAAA,CAAM,eAAe,MAAA,EAAW;AAClC,QAAA,SAAA,CAAU,aAAa,KAAA,CAAM,UAAA;AAAA,MAC/B;AACA,MAAA,IAAI,KAAA,CAAM,iBAAiB,MAAA,EAAW;AACpC,QAAA,SAAA,CAAU,eAAe,KAAA,CAAM,YAAA;AAAA,MACjC;AAEA,MAAA,MAAM,EAAA,CACH,MAAA,CAAO,UAAU,CAAA,CACjB,MAAA,CAAO,YAAY,CAAA,CACnB,oBAAA,CAAqB,EAAE,GAAA,EAAK,SAAA,EAAW,CAAA;AAE1C,MAAA,MAAM,CAAC,GAAG,CAAA,GAAI,MAAM,EAAA,CACjB,MAAA,GACA,IAAA,CAAK,UAAU,CAAA,CACf,KAAA,CAAM,GAAG,UAAA,CAAW,EAAA,EAAI,CAAC,CAAC,CAAA,CAC1B,MAAM,CAAC,CAAA;AAEV,MAAA,IAAI,CAAC,GAAA,EAAK;AACR,QAAA,MAAM,IAAI,MAAM,sDAAsD,CAAA;AAAA,MACxE;AACA,MAAA,OAAO,YAAY,GAAG,CAAA;AAAA,IACxB;AAAA,GACF;AACF","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/** 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: bigintPk(),\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: bigintPk(),\n sessionId: bigintFk('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\nexport type ChatSession = {\n id: number;\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: number;\n sessionId: number;\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: number;\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: number, 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: number, userId: number, patch: { title?: string }): Promise<void>;\n deleteSession(id: number, userId: number): Promise<void>;\n\n // Messages ---------------------------------------------------------------\n appendMessage(input: AppendMessageInput): Promise<ChatMessage>;\n listMessagesForSession(sessionId: number, 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 * 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 } 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';\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 const inserted = await db\n .insert(chatSessions)\n .values({\n userId: input.userId,\n title: input.title\n })\n .$returningId();\n\n const id = inserted[0]?.id;\n if (id == null) {\n throw new Error('createSession: insert returned no id');\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: number, 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: number,\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: number, 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 inserted = await db\n .insert(chatMessages)\n .values({\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 .$returningId();\n\n const id = inserted[0]?.id;\n if (id == null) {\n throw new Error('appendMessage: insert returned no id');\n }\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: number,\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"]}
@@ -28,6 +28,7 @@ var toAiSettings = (row) => ({
28
28
  chatInterface: row.chatInterface,
29
29
  maxOutputTokens: row.maxOutputTokens,
30
30
  rolePrompt: row.rolePrompt,
31
+ gcpProjectId: row.gcpProjectId,
31
32
  updatedAt: row.updatedAt,
32
33
  updatedByUserId: row.updatedByUserId === null ? null : Number(row.updatedByUserId)
33
34
  });
@@ -37,6 +38,7 @@ var DEFAULT_AI_SETTINGS = {
37
38
  chatInterface: "custom",
38
39
  maxOutputTokens: DEFAULT_AI_MAX_OUTPUT_TOKENS,
39
40
  rolePrompt: null,
41
+ gcpProjectId: null,
40
42
  updatedAt: null,
41
43
  updatedByUserId: null
42
44
  };