Files
inkreach-teable-cli/dist/lib/client.js
T
yeuimu 035384131c feat(view-preset): shared view presets with fork-on-edit semantics
Lands the view-preset / server-persisted personal-layer feature that has
been running in production, plus the hotfix for "the same preset shows
different filter configs to different users" (物流问题-已出轨迹 on
tblgsJTXhqwiDpsjeL5 / viwyszJmLQNOLhA9sh5).

Root cause of the divergence:
- usePersonalViewSync implicitly flushed any overlay edit by a
  view|update user INTO the shared preset row (last-writer-wins). Overlays
  are stale snapshots seeded at apply time, so one user's seed silently
  erased colleagues' filter changes (2026-09-17 02:55 the preset lost the
  流转角色=售后 condition), and active overlays never converged because
  preset updates do not propagate.

Fix (semantics: presets are shared definitions, edits are personal):
- fork-on-edit: the first user edit strips the seed markers from the
  overlay (toolbar switches to the personal config); edits only ever
  write the user's personal row
- shared presets change ONLY through the new explicit, confirmed
  "Update to current config" row action (canManage users)
- un-edited preset overlays auto-follow redefinitions: applyPreset stamps
  a presetVersion (preset.lastModifiedTime); when the preset list
  refetches with a newer row the overlay re-seeds. Edited overlays carry
  no markers and are never clobbered. Legacy rows without a version
  re-seed exactly once
- exits strip both seed markers from the persisted personal row

Also lands the pending WIP shipped with the deployed build: select-choice
visibility constraint, personal-view server sync + proxy, grid async
records scope/request tracking, CLI resource display, preset i18n (10
locales), view_preset migrations, docs/skill updates, .zcode gitignore.

Verified end-to-end on the live instance (headless chromium, real
session): apply -> edit flips toolbar to Personal with ZERO shared-row
PUTs; explicit update PUTs the shared row; API-side redefinition
auto-reseeds an un-edited overlay on the next list refetch.
2026-09-17 04:07:30 +00:00

312 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiError = exports.Client = void 0;
/**
* 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.
*/
const request_1 = require("./request");
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return request_1.ApiError; } });
class Client {
cfg;
constructor(cfg) {
this.cfg = cfg;
}
get headers() {
return this.cfg.token ? { Authorization: `Bearer ${this.cfg.token}` } : {};
}
async call(method, p, body) {
const res = await (0, request_1.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) {
return this.call('POST', '/api/access-token', body);
}
deleteAccessToken(id) {
return this.call('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) {
return this.call('GET', `/api/base/${baseId}`);
}
// ---- table ----
listTables(baseId) {
return this.call('GET', `/api/base/${baseId}/table`);
}
getTable(tableId) {
return this.call('GET', `/api/table/${tableId}`);
}
createTable(baseId, body) {
return this.call('POST', `/api/base/${baseId}/table`, body);
}
updateTable(tableId, body) {
return this.call('PUT', `/api/table/${tableId}`, body);
}
deleteTable(tableId, baseId) {
const base = baseId ?? this.cfg.baseId;
return this.call('DELETE', `/api/base/${base}/table/${tableId}`);
}
// ---- field ----
listFields(tableId) {
return this.call('GET', `/api/table/${tableId}/field`);
}
getField(tableId, fieldId) {
return this.call('GET', `/api/table/${tableId}/field/${fieldId}`);
}
createField(tableId, body) {
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, fieldId, body) {
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, fieldId, body) {
return this.call('PUT', `/api/table/${tableId}/field/${fieldId}/convert`, body);
}
deleteField(tableId, fieldId) {
return this.call('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, recordId, fieldId) {
return this.call('GET', `/api/table/${tableId}/select-visibility/${recordId}/${fieldId}`);
}
// ---- record ----
getRecords(tableId, query) {
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[]=...). */
flattenQuery(query) {
const out = {};
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, query) {
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
return this.call('GET', `/api/record/${recordId}${qs}`);
}
createRecord(tableId, body) {
return this.call('POST', `/api/table/${tableId}/record`, body);
}
updateRecord(tableId, body) {
return this.call('PATCH', `/api/table/${tableId}/record`, body);
}
updateRecordOne(recordId, body) {
return this.call('PATCH', `/api/record/${recordId}`, body);
}
deleteRecords(tableId, recordIds) {
const qs = recordIds.map((id) => `recordIds=${encodeURIComponent(id)}`).join('&');
return this.call('DELETE', `/api/table/${tableId}/record?${qs}`);
}
// ---- view ----
listViews(tableId) {
return this.call('GET', `/api/table/${tableId}/view`);
}
getView(viewId) {
return this.call('GET', `/api/view/${viewId}`);
}
createView(tableId, body) {
return this.call('POST', `/api/table/${tableId}/view`, body);
}
updateView(viewId, body) {
return this.call('PATCH', `/api/view/${viewId}`, body);
}
deleteView(viewId) {
return this.call('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, viewId) {
return this.call('GET', `/api/table/${tableId}/view-preset${viewId ? `?viewId=${encodeURIComponent(viewId)}` : ''}`);
}
createViewPreset(tableId, body) {
return this.call('POST', `/api/table/${tableId}/view-preset`, body);
}
updateViewPreset(tableId, presetId, body) {
return this.call('PUT', `/api/table/${tableId}/view-preset/${presetId}`, body);
}
deleteViewPreset(tableId, presetId) {
return this.call('DELETE', `/api/table/${tableId}/view-preset/${presetId}`);
}
// ---- workflow (self-built contract) ----
listWorkflows(baseId) {
return this.call('GET', `/api/base/${baseId}/workflow`);
}
getWorkflow(workflowId) {
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, workflowId) {
return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}`);
}
createWorkflow(baseId, body) {
return this.call('POST', `/api/base/${baseId}/workflow`, body);
}
updateWorkflow(baseId, workflowId, body) {
return this.call('PATCH', `/api/base/${baseId}/workflow/${workflowId}`, body);
}
deleteWorkflow(baseId, workflowId) {
return this.call('DELETE', `/api/base/${baseId}/workflow/${workflowId}`);
}
activateWorkflow(baseId, workflowId) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/activate`);
}
deactivateWorkflow(baseId, workflowId) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/deactivate`);
}
testWorkflow(baseId, workflowId) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/test`);
}
/** PUT /node — whole-graph replacement (delete-all + create-all in a txn). */
replaceNodes(baseId, workflowId, body) {
return this.call('PUT', `/api/base/${baseId}/workflow/${workflowId}/node`, body);
}
// ---- secrets ----
listSecrets(baseId, workflowId) {
return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/secret`);
}
createSecret(baseId, workflowId, body) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/secret`, body);
}
updateSecret(baseId, workflowId, secretId, body) {
return this.call('PATCH', `/api/base/${baseId}/workflow/${workflowId}/secret/${secretId}`, body);
}
deleteSecret(baseId, workflowId, secretId) {
return this.call('DELETE', `/api/base/${baseId}/workflow/${workflowId}/secret/${secretId}`);
}
// ---- execution history ----
listExecutions(baseId, workflowId) {
return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/execution`);
}
getExecution(baseId, workflowId, executionId) {
return this.call('GET', `/api/base/${baseId}/workflow/${workflowId}/execution/${executionId}`);
}
rerunExecution(baseId, workflowId, executionId) {
return this.call('POST', `/api/base/${baseId}/workflow/${workflowId}/execution/${executionId}/rerun`);
}
// ---- trigger (manual fire for testing) ----
fireButtonClick(tableId, recordId, fieldId) {
return this.call('POST', `/api/table/${tableId}/record/${recordId}/${fieldId}/button-click`);
}
fireWebhook(baseId, workflowId, body) {
// 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, sql) {
return this.call('POST', `/api/base/${baseId}/sql`, { sql });
}
// ---- plugin ----
getPlugins() {
return this.call('GET', '/api/plugin');
}
getPlugin(pluginId) {
return this.call('GET', `/api/plugin/${encodeURIComponent(pluginId)}`);
}
updatePlugin(pluginId, body) {
return this.call('PUT', `/api/plugin/${encodeURIComponent(pluginId)}`, body);
}
getCspOrigins() {
return this.call('GET', '/api/plugin/csp-origins');
}
registerPlugin(body) {
return this.call('POST', '/api/plugin', body);
}
listPluginConfigs(baseId) {
return this.call('GET', `/api/base/${baseId}/plugin-config`);
}
upsertPluginConfig(baseId, pluginId, body) {
return this.call('PUT', `/api/base/${baseId}/plugin-config/${pluginId}`, body);
}
// ---- plugin installs ----
listInstalls(baseId) {
return this.call('GET', `/api/base/${baseId}/plugin-installs`);
}
createInstall(baseId, body) {
return this.call('POST', `/api/base/${baseId}/plugin-installs`, body);
}
deleteInstall(baseId, installId) {
return this.call('DELETE', `/api/base/${baseId}/plugin-installs/${installId}`);
}
// ---- plugin developers ----
listDevelopers() {
return this.call('GET', '/api/plugin-developers');
}
addDeveloper(userIdOrEmail) {
// 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) {
return this.call('DELETE', `/api/plugin-developers/${encodeURIComponent(userId)}`);
}
// ---- subscription center (record subscriptions) ----
listSubscriptions(baseId, tableId) {
const q = tableId ? `?tableId=${encodeURIComponent(tableId)}` : '';
return this.call('GET', `/api/base/${baseId}/subscription${q}`);
}
getSubscriptionEvents(baseId, subscriptionId) {
return this.call('GET', `/api/base/${baseId}/subscription/${subscriptionId}/event`);
}
createSubscription(baseId, body) {
return this.call('POST', `/api/base/${baseId}/subscription`, body);
}
updateSubscription(baseId, subscriptionId, body) {
return this.call('PATCH', `/api/base/${baseId}/subscription/${subscriptionId}`, body);
}
deleteSubscription(baseId, subscriptionId) {
return this.call('DELETE', `/api/base/${baseId}/subscription/${subscriptionId}`);
}
/** 立即发送(flush pending digest events) */
sendSubscriptionNow(baseId, subscriptionId) {
return this.call('POST', `/api/base/${baseId}/subscription/${subscriptionId}/send`);
}
}
exports.Client = Client;