- 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).
276 lines
11 KiB
JavaScript
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;
|