/** * 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, baseId?: string) { const base = baseId ?? this.cfg.baseId; return this.call<{ id: string }>('DELETE', `/api/base/${base}/table/${tableId}`); } // ---- field ---- listFields(tableId: string) { return this.call('GET', `/api/table/${tableId}/field`); } getField(tableId: string, fieldId: string) { return this.call('GET', `/api/table/${tableId}/field/${fieldId}`); } createField(tableId: string, body: unknown) { return this.call('POST', `/api/table/${tableId}/field`, body); } /** Rename/describe. NOTE: the update API accepts name/description only — * options (incl. select choice visibilityFilter) must go through convertField. */ updateField(tableId: string, fieldId: string, body: unknown) { return this.call('PATCH', `/api/table/${tableId}/field/${fieldId}`, body); } /** PUT convert: the ONLY API path that persists field options (choices, * visibilityFilter dependency config, etc.). Requires the field's current type. */ convertField(tableId: string, fieldId: string, body: unknown) { return this.call('PUT', `/api/table/${tableId}/field/${fieldId}/convert`, body); } deleteField(tableId: string, fieldId: string) { return this.call<{ id: string }>('DELETE', `/api/table/${tableId}/field/${fieldId}`); } /** Evaluate select-choice visibility dependency for one record+field. * GET /api/table/:tableId/select-visibility/:recordId/:fieldId */ getVisibleChoices(tableId: string, recordId: string, fieldId: string) { return this.call<{ visibleChoices: Array<{ id: string; name: string; color: string }> }>( 'GET', `/api/table/${tableId}/select-visibility/${recordId}/${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 if (k === 'search' && typeof v === 'string') { // The records list API expects `search[]=value` (tuple), not a plain // `search=value` string. https://github.com/colinhacks/zod out['search[]'] = 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); } deleteRecords(tableId: string, recordIds: string[]) { const qs = recordIds.map((id) => `recordIds=${encodeURIComponent(id)}`).join('&'); return this.call('DELETE', `/api/table/${tableId}/record?${qs}`); } // ---- 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}`); } // ---- view preset (self-built contract) ---- // Preset view configurations: VIEW-scoped named bundles of // filter/sort/group(/options/columnMeta) shared with all collaborators. listViewPresets(tableId: string, viewId?: string) { return this.call( 'GET', `/api/table/${tableId}/view-preset${viewId ? `?viewId=${encodeURIComponent(viewId)}` : ''}` ); } createViewPreset(tableId: string, body: unknown) { return this.call('POST', `/api/table/${tableId}/view-preset`, body); } updateViewPreset(tableId: string, presetId: string, body: unknown) { return this.call('PUT', `/api/table/${tableId}/view-preset/${presetId}`, body); } deleteViewPreset(tableId: string, presetId: string) { return this.call('DELETE', `/api/table/${tableId}/view-preset/${presetId}`); } // ---- 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 }); } // ---- plugin ---- getPlugins() { return this.call('GET', '/api/plugin'); } getPlugin(pluginId: string) { return this.call<{ id: string; name: string; positions: string[]; description?: string; detailDesc?: string; url?: string; logo?: string; helpUrl?: string; }>('GET', `/api/plugin/${encodeURIComponent(pluginId)}`); } updatePlugin(pluginId: string, body: Record) { return this.call('PUT', `/api/plugin/${encodeURIComponent(pluginId)}`, body); } getCspOrigins() { return this.call<{ origins: string[] }>('GET', '/api/plugin/csp-origins'); } registerPlugin(body: Record) { return this.call('POST', '/api/plugin', body); } listPluginConfigs(baseId: string) { return this.call('GET', `/api/base/${baseId}/plugin-config`); } upsertPluginConfig(baseId: string, pluginId: string, body: Record) { return this.call('PUT', `/api/base/${baseId}/plugin-config/${pluginId}`, body); } // ---- plugin installs ---- listInstalls(baseId: string) { return this.call('GET', `/api/base/${baseId}/plugin-installs`); } createInstall(baseId: string, body: Record) { return this.call('POST', `/api/base/${baseId}/plugin-installs`, body); } deleteInstall(baseId: string, installId: string) { return this.call('DELETE', `/api/base/${baseId}/plugin-installs/${installId}`); } // ---- plugin developers ---- listDevelopers() { return this.call('GET', '/api/plugin-developers'); } addDeveloper(userIdOrEmail: string) { // Backend reads @Body('userId') or @Body('email'). Send whichever looks right. const isEmail = userIdOrEmail.includes('@'); const body = isEmail ? { email: userIdOrEmail } : { userId: userIdOrEmail }; return this.call('POST', '/api/plugin-developers', body); } removeDeveloper(userId: string) { return this.call('DELETE', `/api/plugin-developers/${encodeURIComponent(userId)}`); } // ---- subscription center (record subscriptions) ---- listSubscriptions(baseId: string, tableId?: string) { const q = tableId ? `?tableId=${encodeURIComponent(tableId)}` : ''; return this.call('GET', `/api/base/${baseId}/subscription${q}`); } getSubscriptionEvents(baseId: string, subscriptionId: string) { return this.call( 'GET', `/api/base/${baseId}/subscription/${subscriptionId}/event` ); } createSubscription(baseId: string, body: IRecordSubscriptionRo) { return this.call('POST', `/api/base/${baseId}/subscription`, body); } updateSubscription(baseId: string, subscriptionId: string, body: Partial) { return this.call( 'PATCH', `/api/base/${baseId}/subscription/${subscriptionId}`, body ); } deleteSubscription(baseId: string, subscriptionId: string) { return this.call<{ id: string }>( 'DELETE', `/api/base/${baseId}/subscription/${subscriptionId}` ); } /** 立即发送(flush pending digest events) */ sendSubscriptionNow(baseId: string, subscriptionId: string) { return this.call('POST', `/api/base/${baseId}/subscription/${subscriptionId}/send`); } } // ---- 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 }; // ---- subscription types (mirror backend record_subscription contract) ---- export interface IRecordSubscriptionRo { name: string; tableId: string; watchCreated?: boolean; watchUpdated?: boolean; fieldIds?: string[]; recordIds?: string[]; displayFieldId?: string | null; attachCsv?: boolean; filter?: Record | null; triggerOnEnter?: boolean; sendMode?: 'digest' | 'instant'; recipients?: string[]; notifyInApp?: boolean; scheduleType?: 'daily' | 'weekly' | 'hours' | 'minutes'; scheduleTime?: string; scheduleWeekday?: number; scheduleInterval?: number; isActive?: boolean; } export interface IRecordSubscriptionVo extends IRecordSubscriptionRo { id: string; baseId: string; createdBy: string; createdTime: string; lastModifiedTime?: string | null; lastSentTime?: string | null; pendingCount?: number; } export interface IRecordSubscriptionEventVo { id: string; subscriptionId: string; recordId: string; changeType: 'created' | 'updated'; changedFieldIds?: string[] | null; createdTime: string; sentTime?: string | null; }