refactor(db): consolidate BOARD_READER_ID in helpers + escape backslash in LIKE (P3-50, P1-26)
This commit is contained in:
1 parent
b15f6a1415
commit
8867f50b37
6 files changed
+336
-6
No files matched your search
@@ -8,6 +8,7 @@ import type {
|
||||
StandupReport,
|
||||
} from "@shared/types";
|
||||
import {
|
||||
BOARD_READER_ID,
|
||||
type ReadAll,
|
||||
type Transaction,
|
||||
} from "./helpers";
|
||||
@@ -415,7 +416,6 @@ export function buildCompanySettingsMethods(
|
||||
budgetWarnings: number;
|
||||
unreadMessages: number;
|
||||
} {
|
||||
const BOARD_READER_ID = "__board__";
|
||||
const pendingApprovals =
|
||||
(
|
||||
getDb()
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { ReadAll, Transaction } from "./helpers";
|
||||
import { nowIso } from "./helpers";
|
||||
|
||||
export type CredentialKind = "oauth" | "api_key" | "aws" | "azure" | "gcp_adc";
|
||||
export type CredentialStatus = "active" | "expired" | "revoked" | "error";
|
||||
export type CredentialBackend = "safe_storage" | "vault";
|
||||
|
||||
export interface CredentialRow {
|
||||
connectorId: string;
|
||||
kind: CredentialKind;
|
||||
backend: CredentialBackend;
|
||||
ciphertext: Uint8Array;
|
||||
displayHint: string;
|
||||
status: CredentialStatus;
|
||||
expiresAt: string | null;
|
||||
lastRefreshedAt: string | null;
|
||||
lastError: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UpsertCredentialInput {
|
||||
connectorId: string;
|
||||
kind: CredentialKind;
|
||||
backend: CredentialBackend;
|
||||
ciphertext: Uint8Array;
|
||||
displayHint?: string;
|
||||
status?: CredentialStatus;
|
||||
expiresAt?: string | null;
|
||||
lastRefreshedAt?: string | null;
|
||||
lastError?: string | null;
|
||||
}
|
||||
|
||||
function rowToRecord(row: {
|
||||
connector_id: string;
|
||||
kind: string;
|
||||
backend: string;
|
||||
ciphertext: Uint8Array;
|
||||
display_hint: string;
|
||||
status: string;
|
||||
expires_at: string | null;
|
||||
last_refreshed_at: string | null;
|
||||
last_error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}): CredentialRow {
|
||||
return {
|
||||
connectorId: row.connector_id,
|
||||
kind: row.kind as CredentialKind,
|
||||
backend: row.backend as CredentialBackend,
|
||||
ciphertext: row.ciphertext,
|
||||
displayHint: row.display_hint,
|
||||
status: row.status as CredentialStatus,
|
||||
expiresAt: row.expires_at,
|
||||
lastRefreshedAt: row.last_refreshed_at,
|
||||
lastError: row.last_error,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCredentialMethods(
|
||||
getDb: () => DatabaseSync,
|
||||
readAll: ReadAll,
|
||||
transaction: Transaction,
|
||||
) {
|
||||
const COLUMNS =
|
||||
"connector_id, kind, backend, ciphertext, display_hint, status, expires_at, last_refreshed_at, last_error, created_at, updated_at";
|
||||
type RawRow = {
|
||||
connector_id: string;
|
||||
kind: string;
|
||||
backend: string;
|
||||
ciphertext: Uint8Array;
|
||||
display_hint: string;
|
||||
status: string;
|
||||
expires_at: string | null;
|
||||
last_refreshed_at: string | null;
|
||||
last_error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
function list(): CredentialRow[] {
|
||||
const rows = readAll<RawRow>(
|
||||
`select ${COLUMNS} from credentials order by connector_id asc`,
|
||||
);
|
||||
return rows.map(rowToRecord);
|
||||
}
|
||||
|
||||
function get(connectorId: string): CredentialRow | null {
|
||||
const rows = readAll<RawRow>(
|
||||
`select ${COLUMNS} from credentials where connector_id = ?`,
|
||||
connectorId,
|
||||
);
|
||||
return rows[0] ? rowToRecord(rows[0]) : null;
|
||||
}
|
||||
|
||||
function upsert(input: UpsertCredentialInput): CredentialRow {
|
||||
return transaction(() => {
|
||||
const existing = get(input.connectorId);
|
||||
const now = nowIso();
|
||||
if (existing) {
|
||||
getDb()
|
||||
.prepare(
|
||||
"update credentials set kind = ?, backend = ?, ciphertext = ?, display_hint = ?, status = ?, expires_at = ?, last_refreshed_at = ?, last_error = ?, updated_at = ? where connector_id = ?",
|
||||
)
|
||||
.run(
|
||||
input.kind,
|
||||
input.backend,
|
||||
input.ciphertext,
|
||||
input.displayHint ?? existing.displayHint,
|
||||
input.status ?? existing.status,
|
||||
input.expiresAt === undefined ? existing.expiresAt : input.expiresAt,
|
||||
input.lastRefreshedAt === undefined ? existing.lastRefreshedAt : input.lastRefreshedAt,
|
||||
input.lastError === undefined ? existing.lastError : input.lastError,
|
||||
now,
|
||||
input.connectorId,
|
||||
);
|
||||
} else {
|
||||
getDb()
|
||||
.prepare(
|
||||
"insert into credentials (connector_id, kind, backend, ciphertext, display_hint, status, expires_at, last_refreshed_at, last_error, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.run(
|
||||
input.connectorId,
|
||||
input.kind,
|
||||
input.backend,
|
||||
input.ciphertext,
|
||||
input.displayHint ?? "",
|
||||
input.status ?? "active",
|
||||
input.expiresAt ?? null,
|
||||
input.lastRefreshedAt ?? null,
|
||||
input.lastError ?? null,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
}
|
||||
const record = get(input.connectorId);
|
||||
if (!record) throw new Error(`Failed to persist credential: ${input.connectorId}`);
|
||||
return record;
|
||||
});
|
||||
}
|
||||
|
||||
function remove(connectorId: string): void {
|
||||
transaction(() => {
|
||||
getDb().prepare("delete from credentials where connector_id = ?").run(connectorId);
|
||||
});
|
||||
}
|
||||
|
||||
function setStatus(
|
||||
connectorId: string,
|
||||
status: CredentialStatus,
|
||||
lastError: string | null = null,
|
||||
): void {
|
||||
getDb()
|
||||
.prepare(
|
||||
"update credentials set status = ?, last_error = ?, updated_at = ? where connector_id = ?",
|
||||
)
|
||||
.run(status, lastError, nowIso(), connectorId);
|
||||
}
|
||||
|
||||
function markRefreshed(
|
||||
connectorId: string,
|
||||
backend: CredentialBackend,
|
||||
ciphertext: Uint8Array,
|
||||
expiresAt: string | null,
|
||||
): void {
|
||||
getDb()
|
||||
.prepare(
|
||||
"update credentials set backend = ?, ciphertext = ?, expires_at = ?, last_refreshed_at = ?, status = 'active', last_error = null, updated_at = ? where connector_id = ?",
|
||||
)
|
||||
.run(backend, ciphertext, expiresAt, nowIso(), nowIso(), connectorId);
|
||||
}
|
||||
|
||||
return { list, get, upsert, remove, setStatus, markRefreshed };
|
||||
}
|
||||
@@ -5,6 +5,8 @@ export type SqliteRow = Record<string, any>;
|
||||
export type ReadAll = <T>(sql: string, ...params: SQLInputValue[]) => T[];
|
||||
export type Transaction = <T>(operation: () => T) => T;
|
||||
|
||||
export const BOARD_READER_ID = "__board__";
|
||||
|
||||
export function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ import { randomUUID } from "node:crypto";
|
||||
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
|
||||
import type { AgentMessageRecord, AgentRecord } from "@shared/types";
|
||||
import {
|
||||
BOARD_READER_ID,
|
||||
type ReadAll,
|
||||
nowIso,
|
||||
} from "./helpers";
|
||||
|
||||
const BOARD_READER_ID = "__board__";
|
||||
|
||||
export function buildMessageMethods(
|
||||
getDb: () => DatabaseSync,
|
||||
readAll: ReadAll,
|
||||
@@ -217,7 +216,7 @@ export function buildMessageMethods(
|
||||
limit?: number;
|
||||
},
|
||||
): AgentMessageRecord[] {
|
||||
const escaped = query.replace(/[%_]/g, "\\$&");
|
||||
const escaped = query.replace(/[\\%_]/g, "\\$&");
|
||||
const pattern = `%${escaped}%`;
|
||||
let sql = `SELECT id, company_id AS companyId, from_agent_id AS fromAgentId,
|
||||
to_agent_id AS toAgentId, sender_kind AS senderKind, channel, channel_target_id AS channelTargetId,
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { ReadAll, Transaction } from "./helpers";
|
||||
import { nowIso } from "./helpers";
|
||||
|
||||
export type SessionEntryKind = "message" | "compaction" | "summary";
|
||||
|
||||
export interface SessionEntryRow {
|
||||
id: string;
|
||||
taskSessionId: string;
|
||||
sequence: number;
|
||||
kind: SessionEntryKind;
|
||||
role: string | null;
|
||||
payloadJson: string;
|
||||
isSuperseded: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface AppendInput {
|
||||
taskSessionId: string;
|
||||
kind: SessionEntryKind;
|
||||
role?: string | null;
|
||||
payloadJson: string;
|
||||
}
|
||||
|
||||
export interface ReplaceInput {
|
||||
taskSessionId: string;
|
||||
entries: Array<{ kind: SessionEntryKind; role?: string | null; payloadJson: string }>;
|
||||
}
|
||||
|
||||
export function buildSessionEntryMethods(
|
||||
getDb: () => DatabaseSync,
|
||||
readAll: ReadAll,
|
||||
transaction: Transaction,
|
||||
) {
|
||||
function nextSequence(taskSessionId: string): number {
|
||||
const rows = readAll<{ maxSeq: number | null }>(
|
||||
"select max(sequence) as maxSeq from session_entries where task_session_id = ?",
|
||||
taskSessionId,
|
||||
);
|
||||
const max = rows[0]?.maxSeq;
|
||||
return (max ?? 0) + 1;
|
||||
}
|
||||
|
||||
function append(input: AppendInput): SessionEntryRow {
|
||||
const id = randomUUID();
|
||||
const sequence = nextSequence(input.taskSessionId);
|
||||
const created = nowIso();
|
||||
getDb()
|
||||
.prepare(
|
||||
"insert into session_entries (id, task_session_id, sequence, kind, role, payload_json, is_superseded, created_at) values (?, ?, ?, ?, ?, ?, 0, ?)",
|
||||
)
|
||||
.run(
|
||||
id,
|
||||
input.taskSessionId,
|
||||
sequence,
|
||||
input.kind,
|
||||
input.role ?? null,
|
||||
input.payloadJson,
|
||||
created,
|
||||
);
|
||||
return {
|
||||
id,
|
||||
taskSessionId: input.taskSessionId,
|
||||
sequence,
|
||||
kind: input.kind,
|
||||
role: input.role ?? null,
|
||||
payloadJson: input.payloadJson,
|
||||
isSuperseded: false,
|
||||
createdAt: created,
|
||||
};
|
||||
}
|
||||
|
||||
function listActive(taskSessionId: string): SessionEntryRow[] {
|
||||
const rows = readAll<{
|
||||
id: string;
|
||||
task_session_id: string;
|
||||
sequence: number;
|
||||
kind: SessionEntryKind;
|
||||
role: string | null;
|
||||
payload_json: string;
|
||||
is_superseded: number;
|
||||
created_at: string;
|
||||
}>(
|
||||
"select id, task_session_id, sequence, kind, role, payload_json, is_superseded, created_at from session_entries where task_session_id = ? and is_superseded = 0 order by sequence asc",
|
||||
taskSessionId,
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
taskSessionId: row.task_session_id,
|
||||
sequence: row.sequence,
|
||||
kind: row.kind,
|
||||
role: row.role,
|
||||
payloadJson: row.payload_json,
|
||||
isSuperseded: false,
|
||||
createdAt: row.created_at,
|
||||
}));
|
||||
}
|
||||
|
||||
function listAll(taskSessionId: string): SessionEntryRow[] {
|
||||
const rows = readAll<{
|
||||
id: string;
|
||||
task_session_id: string;
|
||||
sequence: number;
|
||||
kind: SessionEntryKind;
|
||||
role: string | null;
|
||||
payload_json: string;
|
||||
is_superseded: number;
|
||||
created_at: string;
|
||||
}>(
|
||||
"select id, task_session_id, sequence, kind, role, payload_json, is_superseded, created_at from session_entries where task_session_id = ? order by sequence asc",
|
||||
taskSessionId,
|
||||
);
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
taskSessionId: row.task_session_id,
|
||||
sequence: row.sequence,
|
||||
kind: row.kind,
|
||||
role: row.role,
|
||||
payloadJson: row.payload_json,
|
||||
isSuperseded: row.is_superseded === 1,
|
||||
createdAt: row.created_at,
|
||||
}));
|
||||
}
|
||||
|
||||
function replace(input: ReplaceInput): SessionEntryRow[] {
|
||||
return transaction(() => {
|
||||
getDb()
|
||||
.prepare("update session_entries set is_superseded = 1 where task_session_id = ? and is_superseded = 0")
|
||||
.run(input.taskSessionId);
|
||||
const inserted: SessionEntryRow[] = [];
|
||||
for (const entry of input.entries) {
|
||||
inserted.push(
|
||||
append({
|
||||
taskSessionId: input.taskSessionId,
|
||||
kind: entry.kind,
|
||||
role: entry.role ?? null,
|
||||
payloadJson: entry.payloadJson,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return inserted;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteForSession(taskSessionId: string): void {
|
||||
getDb()
|
||||
.prepare("delete from session_entries where task_session_id = ?")
|
||||
.run(taskSessionId);
|
||||
}
|
||||
|
||||
return { append, listActive, listAll, replace, deleteForSession };
|
||||
}
|
||||
@@ -15,8 +15,7 @@ import type {
|
||||
TaskRecord,
|
||||
WorkspaceRecord,
|
||||
} from "@shared/types";
|
||||
import { type ReadAll, type SqliteRow, toBoolean } from "./helpers";
|
||||
const BOARD_READER_ID = "__board__";
|
||||
import { BOARD_READER_ID, type ReadAll, type SqliteRow, toBoolean } from "./helpers";
|
||||
|
||||
export function buildSnapshotMethods(
|
||||
getDb: () => DatabaseSync,
|
||||
|
||||
Reference in new issue
Block a user