Files
inkreach-teable-cli/dist/lib/client.js
T
yeuimu 10b52db8b2 fix(field): enforce select-visibility guard on the V2 convert path + repair inkreach-cli field routes
- FieldOpenApiV2Service.convertField now calls the shared V1 validator
  (assertSelectVisibilityConfigValid) before executing the update command.
  Previously only the V1 path validated self-reference / dependency cycles,
  so invalid configs could be persisted through V2 (verified live: a
  self-referencing choice was accepted with 200).
- V1 assertSelectVisibilityConfigValid private -> public (shared guard).
- Unit tests: 3 new cases in field-open-api-v2.service.spec.ts (guard
  invoked with exact args; rejects before executing when validator throws;
  called with raw type/options for non-select conversions). 63/63 pass;
  tsc build config 0 errors.
- inkreach-cli: field get/update/delete targeted non-existent /api/field/:id
  routes; now use /api/table/:tableId/field/:fieldId. update --options goes
  through the convert API (the PATCH update API strips options by design).
  New: convertField + getVisibleChoices client methods, field visible-choices
  command (evaluates choice visibility for a record).
2026-08-16 05:31:05 +00:00

276 lines
11 KiB
JavaScript

"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}`);
}
// ---- 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)}`);
}
}
exports.Client = Client;