/** * High-level API client. Each method maps to one REST endpoint of the * inkreach-teable backend, using the SELF-BUILT contract (not official teable). * * Base/table/field/record/view endpoints are compatible with official teable * open-api; workflow/automation endpoints use the project's own contract. */ import { request, ApiError } from './request'; import { IConfig } from './config'; export class Client { constructor(private cfg: IConfig) {} private get headers(): Record { return this.cfg.token ? { Authorization: `Bearer ${this.cfg.token}` } : {}; } private async call(method: string, p: string, body?: unknown): Promise { const res = await request(this.cfg.endpoint, p, { method, headers: this.headers, body }); return res.data; } // ---- auth / user ---- getMe() { // GET /api/auth/user — @TokenAccess() enabled, works with PAT. // (/api/auth/user/me is session-only and 403s for PAT.) return this.call('GET', '/api/auth/user'); } // ---- access tokens (PAT) ---- listAccessTokens() { return this.call('GET', '/api/access-token'); } createAccessToken(body: { name: string; description?: string; scopes: string[]; baseIds?: string[]; spaceIds?: string[]; hasFullAccess?: boolean; expiredTime: string; }) { return this.call<{ id: string; token: string }>('POST', '/api/access-token', body); } deleteAccessToken(id: string) { return this.call<{ id: string }>('DELETE', `/api/access-token/${id}`); } // ---- base ---- listBases() { // GET /api/base/access/all — lists bases the user can access. return this.call('GET', '/api/base/access/all'); } getBase(baseId: string) { return this.call('GET', `/api/base/${baseId}`); } // ---- table ---- listTables(baseId: string) { return this.call('GET', `/api/base/${baseId}/table`); } getTable(tableId: string) { return this.call('GET', `/api/table/${tableId}`); } createTable(baseId: string, body: unknown) { return this.call('POST', `/api/base/${baseId}/table`, body); } updateTable(tableId: string, body: unknown) { return this.call('PUT', `/api/table/${tableId}`, body); } deleteTable(tableId: string) { return this.call<{ id: string }>('DELETE', `/api/table/${tableId}`); } // ---- field ---- listFields(tableId: string) { return this.call('GET', `/api/table/${tableId}/field`); } getField(fieldId: string) { return this.call('GET', `/api/field/${fieldId}`); } createField(tableId: string, body: unknown) { return this.call('POST', `/api/table/${tableId}/field`, body); } updateField(fieldId: string, body: unknown) { return this.call('PUT', `/api/field/${fieldId}`, body); } deleteField(fieldId: string) { return this.call<{ id: string }>('DELETE', `/api/field/${fieldId}`); } // ---- record ---- getRecords(tableId: string, query?: Record) { const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : ''; return this.call('GET', `/api/table/${tableId}/record${qs}`); } /** Flatten search[] tuple arrays into repeated query keys (Teable expects search[]=...). */ private flattenQuery(query: Record): Record { const out: Record = {}; for (const [k, v] of Object.entries(query)) { if (v === undefined || v === null) continue; if (Array.isArray(v)) { v.forEach((item) => { out[`${k}[]`] = typeof item === 'object' ? JSON.stringify(item) : String(item); }); } else if (typeof v === 'object') { out[k] = JSON.stringify(v); } else { out[k] = String(v); } } return out; } getRecord(recordId: string, query?: Record) { const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : ''; return this.call('GET', `/api/record/${recordId}${qs}`); } createRecord(tableId: string, body: unknown) { return this.call('POST', `/api/table/${tableId}/record`, body); } updateRecord(tableId: string, body: unknown) { return this.call('PATCH', `/api/table/${tableId}/record`, body); } updateRecordOne(recordId: string, body: unknown) { return this.call('PATCH', `/api/record/${recordId}`, body); } deleteRecord(tableId: string, body: unknown) { return this.call('DELETE', `/api/table/${tableId}/record`, body); } // ---- view ---- listViews(tableId: string) { return this.call('GET', `/api/table/${tableId}/view`); } getView(viewId: string) { return this.call('GET', `/api/view/${viewId}`); } createView(tableId: string, body: unknown) { return this.call('POST', `/api/table/${tableId}/view`, body); } updateView(viewId: string, body: unknown) { return this.call('PATCH', `/api/view/${viewId}`, body); } deleteView(viewId: string) { return this.call<{ id: string }>('DELETE', `/api/view/${viewId}`); } // ---- workflow (self-built contract) ---- listWorkflows(baseId: string) { return this.call('GET', `/api/base/${baseId}/workflow`); } getWorkflow(workflowId: string) { return this.call('GET', `/api/base/any/workflow/${workflowId}`); } /** * Get workflow by explicit baseId + workflowId. getWorkflow() above hits a * route that resolves base from the workflow itself, but some auth/permission * paths need the base-scoped route. Prefer this when baseId is known. */ getWorkflowInBase(baseId: string, workflowId: string) { return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}`); } createWorkflow(baseId: string, body: { name: string; description?: string }) { return this.call('POST', `/api/base/${baseId}/workflow`, body); } updateWorkflow(baseId: string, workflowId: string, body: { name?: string; description?: string }) { return this.call('PATCH', `/api/base/${baseId}/workflow/${workflowId}`, body); } deleteWorkflow(baseId: string, workflowId: string) { return this.call('DELETE', `/api/base/${baseId}/workflow/${workflowId}`); } activateWorkflow(baseId: string, workflowId: string) { return this.call<{ snapshotId: string }>('POST', `/api/base/${baseId}/workflow/${workflowId}/activate`); } deactivateWorkflow(baseId: string, workflowId: string) { return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/deactivate`); } testWorkflow(baseId: string, workflowId: string) { return this.call<{ nodeResults: unknown[] }>('POST', `/api/base/${baseId}/workflow/${workflowId}/test`); } /** PUT /node — whole-graph replacement (delete-all + create-all in a txn). */ replaceNodes(baseId: string, workflowId: string, body: { nodes: IWorkflowNodeVo[]; edges: IWorkflowEdgeVo[] }) { return this.call('PUT', `/api/base/${baseId}/workflow/${workflowId}/node`, body); } // ---- secrets ---- listSecrets(baseId: string, workflowId: string) { return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/secret`); } createSecret(baseId: string, workflowId: string, body: { name: string; description?: string; value: string }) { return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/secret`, body); } updateSecret(baseId: string, workflowId: string, secretId: string, body: { name?: string; description?: string; value?: string }) { return this.call('PATCH', `/api/base/${baseId}/workflow/${workflowId}/secret/${secretId}`, body); } deleteSecret(baseId: string, workflowId: string, secretId: string) { return this.call('DELETE', `/api/base/${baseId}/workflow/${workflowId}/secret/${secretId}`); } // ---- execution history ---- listExecutions(baseId: string, workflowId: string) { return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/execution`); } getExecution(baseId: string, workflowId: string, executionId: string) { return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/execution/${executionId}`); } rerunExecution(baseId: string, workflowId: string, executionId: string) { return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/execution/${executionId}/rerun`); } // ---- trigger (manual fire for testing) ---- fireButtonClick(tableId: string, recordId: string, fieldId: string) { return this.call<{ runId: string; tableId: string; fieldId: string; record: unknown }>( 'POST', `/api/table/${tableId}/record/${recordId}/${fieldId}/button-click` ); } fireWebhook(baseId: string, workflowId: string, body?: unknown) { // NOTE: real backend route is /api/webhook/{baseId}/{workflowId} (no /base/ /workflow/ segments) return this.call('POST', `/api/webhook/${baseId}/${workflowId}`, body); } // ---- SQL (read-only) ---- sql(baseId: string, sql: string) { return this.call('POST', `/api/base/${baseId}/sql`, { sql }); } } // ---- shared types (mirror backend workflow_node / workflow_edge shapes) ---- export type WorkflowNodeType = 'trigger' | 'action' | 'control'; export type WorkflowNodeCategory = | 'recordCreated' | 'recordUpdated' | 'recordCondition' | 'recordCreatedOrUpdated' | 'formSubmitted' | 'scheduledTime' | 'buttonClick' | 'webhook' | 'script' | 'sendEmail' | 'httpRequest' | 'createRecord' | 'updateRecord' | 'getRecords' | 'triggerEnd'; export interface IWorkflowNodeVo { id: string; workflowId?: string; type: WorkflowNodeType; category: WorkflowNodeCategory | string; config: Record | null; name?: string | null; testResult?: Record | null; inputVariables?: unknown; outputVariables?: unknown; } export interface IWorkflowEdgeVo { id: string; workflowId?: string; sourceNodeId: string; targetNodeId: string; } export interface IWorkflowVo { id: string; baseId: string; name: string; description?: string | null; isActive: boolean; hasDraft: boolean; nodes: IWorkflowNodeVo[]; edges: IWorkflowEdgeVo[]; createdBy?: string; createdTime?: string; lastModifiedBy?: string | null; lastModifiedTime?: string | null; } export { ApiError };