// REST 数据源:与 chusql-web 的 /api/* 一一对应。 // 请求都带 X-ChuSQL-Database 头;401 时广播登录事件。 export const AUTH_EVENT = 'chusql-auth-required'; let editing = null; let consoleTransaction = null; const pageOwner = [...crypto.getRandomValues(new Uint8Array(16))].map((byte) => byte.toString(16).padStart(2, '0')).join(''); // 是否是普通对象。 function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } // 校验结果列名及每行的列数。 function hasValidRows(body) { return isObject(body) && Array.isArray(body.columns) && body.columns.every((column) => typeof column === 'string') && Array.isArray(body.rows) && body.rows.every((row) => Array.isArray(row) && row.length === body.columns.length); } // 发一次请求并处理错误。 async function request(url, init, database) { const headers = new Headers(init && init.headers ? init.headers : undefined); if (database) headers.set('X-ChuSQL-Database', database); if (editing && editing.database === database && ((url.startsWith('/api/tables') && (!init || init.method === 'GET')) || url === '/api/normalization/analyze')) { headers.set('X-ChuSQL-Edit', editing.id); } let response; try { response = await fetch(url, { credentials: 'same-origin', ...(init ?? {}), headers }); } catch (error) { throw new Error(`无法连接服务器:${error && error.message ? error.message : String(error)}`); } let body; try { body = await response.json(); } catch { body = undefined; } if (!response.ok) { if (response.status === 401 || (isObject(body) && body.error === 'password_expired')) { window.dispatchEvent(new Event(AUTH_EVENT)); } const message = isObject(body) && typeof body.message === 'string' ? body.message : `请求失败(HTTP ${response.status})。`; const error = new Error(message); error.result = body; error.code = body?.error; throw error; } return body; } // 组装 JSON 请求参数。 function jsonInit(method, body) { return { method, headers: { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), }; } const identifier = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/; // 校验数据库名格式。 function checkDatabaseName(name) { if (!identifier.test(String(name))) throw new Error('数据库名只能是字母、数字与下划线,且以字母或下划线开头。'); } // ---------------------------------------------------------------- 会话 // 读取当前会话。 export async function currentSession() { const body = await request('/api/session'); if (!isObject(body) || typeof body.user !== 'string') throw new Error('服务器返回的会话信息无法识别。'); return { user: body.user, administrator: body.administrator === true, policy: isObject(body.policy) ? body.policy : { minLength: 0, classes: 0 }, }; } // 提交登录。 export async function login(user, password) { await request('/api/login', jsonInit('POST', { user, password })); } // 退出登录。 export async function logout() { await request('/api/logout', jsonInit('POST', {})); } // ---------------------------------------------------------------- 数据库 // 列出数据库。 export async function listDatabases() { const body = await request('/api/databases'); if (!Array.isArray(body) || !body.every((name) => typeof name === 'string')) { throw new Error('服务器返回的数据库列表无法识别。'); } return body; } // 新建数据库。 export async function createDatabase(name, database) { checkDatabaseName(name); await request('/api/databases', jsonInit('POST', { name }), database); } // 删除数据库。 export async function dropDatabase(name, database) { await request(`/api/databases/${encodeURIComponent(name)}`, jsonInit('DELETE'), database); } // 切换当前库(发 USE 语句)。 export async function useDatabase(name, database) { checkDatabaseName(name); return query(`USE ${name}`, database); } // ---------------------------------------------------------------- 表结构 // 校验并规整列描述。 function asColumns(value) { if (!Array.isArray(value)) throw new Error('服务器返回的列清单无法识别。'); return value.map((entry) => { if (!isObject(entry) || typeof entry.name !== 'string' || typeof entry.type !== 'string' || !entry.type.trim()) { throw new Error('服务器返回的列描述无法识别。'); } return { ...entry, name: entry.name, type: entry.type }; }); } // 校验并规整表描述。 function asTable(value) { if (!isObject(value) || typeof value.table !== 'string' || typeof value.rowCount !== 'number') { throw new Error('服务器返回的表描述无法识别。'); } return { name: value.table, rowCount: value.rowCount, columns: asColumns(value.columns), indexes: Array.isArray(value.indexes) ? value.indexes : [], kind: value.kind === 'system' ? 'system' : 'table', }; } // 列出表。 export async function listTables(database) { const body = await request('/api/tables', undefined, database); if (!Array.isArray(body)) throw new Error('服务器返回的表清单无法识别。'); return body.map(asTable); } // 取单表结构。 export async function getTable(database, table) { return asTable(await request(`/api/tables/${encodeURIComponent(table)}`, undefined, database)); } // 新建表。 export async function createTable(database, name, columns) { await request('/api/tables', jsonInit('POST', { name, columns }), database); } // 删除表。 export async function dropTable(database, table) { await request(`/api/tables/${encodeURIComponent(table)}`, jsonInit('DELETE'), database); } // 建索引。 export async function createIndex(database, table, column) { await request(`/api/tables/${encodeURIComponent(table)}/indexes`, jsonInit('POST', { column }), database); } // 删索引。 export async function dropIndex(database, table, column) { await request(`/api/tables/${encodeURIComponent(table)}/indexes/${encodeURIComponent(column)}`, jsonInit('DELETE'), database); } // 删列。 export async function dropColumn(database, table, column) { await request(`/api/tables/${encodeURIComponent(table)}/columns/${encodeURIComponent(column)}`, jsonInit('DELETE'), database); } // ---------------------------------------------------------------- 行 // 分页取表数据。 export async function fetchRows({ database, table, limit, offset, sort, dir, filters }) { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }); if (sort) { params.set('sort', sort); params.set('dir', dir === 'desc' ? 'desc' : 'asc'); } if (filters && filters.length) { params.set('filter', JSON.stringify(filters.map((f) => ({ column: f.column, value: f.expression })))); } const body = await request(`/api/tables/${encodeURIComponent(table)}/rows?${params}`, undefined, database); if (!hasValidRows(body) || !Number.isSafeInteger(body.total) || body.total < 0) { throw new Error('服务器返回的分页数据无法识别。'); } const columns = body.columns; const rows = body.rows.map((raw) => { return Object.fromEntries(columns.map((column, index) => [column, raw[index]])); }); return { columns, rows, total: body.total }; } // 插入一行。 export async function insertRow(database, table, values) { await request(`/api/tables/${encodeURIComponent(table)}/rows`, jsonInit('POST', { values }), database); } // 更新一行。 export async function updateRow(database, table, id, values) { await request(`/api/tables/${encodeURIComponent(table)}/rows/${encodeURIComponent(id)}`, jsonInit('PATCH', { values }), database); } // 删除一行。 export async function deleteRow(database, table, id) { await request(`/api/tables/${encodeURIComponent(table)}/rows/${encodeURIComponent(id)}`, jsonInit('DELETE'), database); } // 为页面开启固定数据库的编辑快照。 export async function beginEdit(id, database) { await request('/api/edit/begin', jsonInit('POST', { id, database })); editing = { id, database }; } // 在服务端事务中原子提交编辑集。 export async function commitChanges(id, database, changes) { try { const result = await request('/api/edit/commit', jsonInit('POST', { id, database, changes })); if (!isObject(result) || result.ok !== true || result.state !== 'committed') throw new Error('服务器未返回可确认的提交结果。'); editing = null; return result; } catch (error) { if (isObject(error.result) && typeof error.result.state === 'string') return error.result; return { ok: false, applied: 0, state: error.result ? 'lost' : 'unknown', reloadRequired: Boolean(error.result), message: error.message }; } } // 回滚编辑快照并清除本地事务标识。 export async function rollbackEdit(id, database) { const result = await request('/api/edit/rollback', jsonInit('POST', { id, database })); editing = null; return result; } // 忘记已失效的登录会话编辑标识。 export function forgetEdit() { editing = null; consoleTransaction = null; } // 标签关闭时回滚它持有的控制台事务。 export async function rollbackConsole(tabId) { if (!consoleTransaction || consoleTransaction.console !== `${pageOwner}:${tabId ?? 'default'}`) return; await request('/api/query/rollback', jsonInit('POST', { console: consoleTransaction.console })); consoleTransaction = null; } // 页面离开时请求清理控制台事务。 export function closeConsoleOnPagehide() { if (consoleTransaction) { navigator.sendBeacon('/api/query/rollback', new Blob([JSON.stringify({ console: consoleTransaction.console })], { type: 'application/json' })); } } // ---------------------------------------------------------------- 查询 // 执行 SQL 并规整结果。 export async function query(sql, database, tabId) { const started = performance.now(); try { const console = `${pageOwner}:${tabId ?? 'default'}`; const init = jsonInit('POST', { sql }); init.headers['X-ChuSQL-Console'] = console; const body = await request('/api/query', init, database); if (!hasValidRows(body)) { throw new Error('服务器返回的查询结果无法识别。'); } const durationMs = performance.now() - started; consoleTransaction = body.transaction === true ? { console, database } : null; return { type: body.columns.length ? 'rows' : 'affected', columns: body.columns, rows: body.rows, affected: typeof body.rowCount === 'number' ? body.rowCount : 0, truncated: body.truncated === true, database: typeof body.database === 'string' ? body.database : undefined, transactionActive: body.transaction === true, durationMs, }; } catch (error) { return { type: 'error', durationMs: performance.now() - started, message: error instanceof Error ? error.message : String(error) }; } } // ---------------------------------------------------------------- 设置 // 读取服务器设置。 export async function loadServerSettings() { const body = await request('/api/settings'); if (!isObject(body) || !Array.isArray(body.items)) throw new Error('服务器返回的设置清单无法识别。'); return body; } // 保存服务器设置。 export async function saveServerSettings(values) { const body = await request('/api/settings', jsonInit('PUT', { values })); return isObject(body) ? body : { ok: true }; } // 读取 IDE 设置。 export async function loadUiSettings() { return request('/api/ui-settings'); } // 保存 IDE 设置。 export async function saveUiSettings(settings) { await request('/api/ui-settings', jsonInit('PUT', settings)); } // 在页面已有视图上请求只读范式分析。 export async function analyzeNormalization(database, table, declaration, expected) { const payload = { api_version: 1, database, table }; if (expected) { payload.expected_object = expected.object; payload.expected_schema_fingerprint = expected.schema_fingerprint; } const init = jsonInit('POST', payload); if (!editing && consoleTransaction?.database === database) { init.headers['X-ChuSQL-Console'] = consoleTransaction.console; } // 原始声明直接嵌入,重复字段交给服务端明确拒绝。 init.body = `${JSON.stringify(payload).slice(0, -1)},"declaration":${typeof declaration === 'string' ? declaration : JSON.stringify(declaration)}}`; const result = await request('/api/normalization/analyze', init, database); if (!isObject(result) || result.api_version !== 1 || !Array.isArray(result.findings) || !Array.isArray(result.schema)) { throw new Error('服务器返回的范式报告无法识别。'); } return result; }