diff --git a/skills/inkreach-teable/SKILL.md b/skills/inkreach-teable/SKILL.md index 462e5fd..243afa4 100644 --- a/skills/inkreach-teable/SKILL.md +++ b/skills/inkreach-teable/SKILL.md @@ -42,6 +42,7 @@ Respond in the user's language. Keep answers concise and action-oriented. | Execution History | Run results / rerun failures | `runs list/get/rerun` | [automation-guide.md § Testing](guides/automation-guide.md) | | Manual Trigger | Test button-click/webhook | `fire button-click`, `fire webhook` | [automation-guide.md § Testing](guides/automation-guide.md) | | SQL | Read-only analytics, JOINs | `sql ''` | [cli-reference.md § sql](guides/cli-reference.md) | +| Direct REST API | Integrate external scripts/services; endpoints the CLI lacks; script actions calling back | `curl` / `fetch` + `Authorization: Bearer ` | [rest-api.md](api-reference/rest-api.md) | | Plugins | Dynamic plugins, dev whitelist, CSP | `plugin *` | [cli-reference.md § plugin](guides/cli-reference.md) | | Subscriptions | Record digest/instant email | `subscription *` | [subscription.md](guides/subscription.md) | | Dev/Deploy | Build, restart, gotchas | systemd / pnpm | [development.md](guides/development.md) | @@ -65,6 +66,7 @@ Full command syntax: [cli-reference.md](guides/cli-reference.md). Instance acces | Analytics / JOIN | `sql` with GROUP BY | Fetch all records + compute | | Store an API key for scripts | `secret set` → `process.env.NAME` | Hardcoding | | Workflow didn't run | `runs list --status failed` → `runs get` | Blind re-activate | +| Call the instance from external code / a route the CLI lacks | Direct REST: `Authorization: Bearer ` + routes in [rest-api.md](api-reference/rest-api.md); unknown route → run the nearest CLI command with `--debug` and copy its request | Official teable API docs (different automation contract) | ## 3. Key Constraints diff --git a/skills/inkreach-teable/api-reference/rest-api.md b/skills/inkreach-teable/api-reference/rest-api.md new file mode 100644 index 0000000..e1e7fc2 --- /dev/null +++ b/skills/inkreach-teable/api-reference/rest-api.md @@ -0,0 +1,153 @@ +# REST API Reference (direct HTTP) + +How to call the inkreach-teable backend API directly with plain HTTP (curl / +node:http / axios / any language) — no CLI involved. The CLI wraps exactly +these routes, so everything it does is reproducible raw. + +**Use direct API when**: integrating external scripts/services, the CLI lacks +an endpoint (auth signup, share views…), writing automation script actions +that call back into the instance, or working from a language/runtime without +the CLI. For one-off table/automation operations, prefer the CLI. + +## 1. Base URL & Authentication + +```bash +BASE=http://localhost:3003 # backend API origin (see instance-access.md) +TOKEN=teable_xxx_yyy # PAT; web UI Settings → Personal Access Tokens +curl -s "$BASE/api/auth/user" -H "Authorization: Bearer $TOKEN" # verify → 200 + user JSON +``` + +- Every authenticated call: `Authorization: Bearer ` (+ `Content-Type: + application/json` on bodies). PAT scopes ∩ base role = effective permission. +- Discover the configured base/token the CLI already uses: + `inkreach-teable auth status`, or read `~/.inkreach-teable/config.json`. +- ⚠️ **Node v24 global `fetch()` (undici) can silently drop the Authorization + header** — use `curl`, `node:http`, or `axios` instead (details in + [development.md](../guides/development.md)). + +## 2. Endpoint Map + +Verified route-for-route against `src/lib/client.ts` (the CLI's HTTP layer). + +### Records + +| Method & route | Purpose | +|---|---| +| `GET /api/table/{tid}/record` | List/query records (params below) | +| `POST /api/table/{tid}/record` | Create one (`{fields}`) or many (`{records:[{fields}]}`) | +| `PATCH /api/table/{tid}/record` | Batch update — `{records:[{id, fields}]}` | +| `GET /api/record/{rid}` | Fetch one record | +| `PATCH /api/record/{rid}` | Update one — `{fields}` | +| `DELETE /api/table/{tid}/record?recordIds=recA&recordIds=recB` | Delete by ids | + +List-query params (objects are passed **JSON-stringified**; repeated keys use +`key[]`): `filter` (JSON), `projection` (JSON array), `search[]` (keyword — +MUST be the `search[]` tuple form, plain `search=` is ignored), `skip` / `take` +(pagination; no `orderBy` on this fork — sort via view or `sql`). Response: +`{ records: [{id, fields}], total? }`. + +### Fields + +| Method & route | Purpose | +|---|---| +| `GET`/`POST` `/api/table/{tid}/field` | List / create | +| `GET`/`PATCH`/`DELETE` `/api/table/{tid}/field/{fid}` | Read / update / delete | +| `PUT /api/table/{tid}/field/{fid}/convert` | Change type **or options** (PATCH strips options!) | +| `GET /api/table/{tid}/select-visibility/{rid}/{fid}` | Choices visible to a given row (visibilityFilter) | + +### Tables / Views / Presets + +| Method & route | Purpose | +|---|---| +| `GET`/`POST` `/api/base/{bid}/table` | List / create (body `{name, fields:[…]}`) | +| `GET`/`PUT` `/api/table/{tid}`, `DELETE /api/base/{bid}/table/{tid}` | Read-update / delete table | +| `GET`/`POST` `/api/table/{tid}/view`, `GET`/`PATCH`/`DELETE` `/api/view/{vid}` | View CRUD | +| `GET`/`POST` `/api/table/{tid}/view-preset`, `PUT`/`DELETE` `…/view-preset/{pid}` | Shared view presets | + +### Automation (self-built engine — `bid` = base id) + +| Method & route | Purpose | +|---|---| +| `GET`/`POST` `/api/base/{bid}/workflow` | List / create (`{name, description?}`) | +| `GET` `/api/base/{bid}/workflow/{wid}` or `/api/base/any/workflow/{wid}` | Read one (any-variant works cross-base) | +| `PATCH`/`DELETE` `/api/base/{bid}/workflow/{wid}` | Update metadata / soft delete | +| `POST` `…/workflow/{wid}/activate` \| `…/deactivate` | Activate (creates snapshot) / deactivate | +| `PUT` `…/workflow/{wid}/node` | **Whole-graph** node replacement (`{nodes:[…]}`) | +| `POST` `…/workflow/{wid}/test` | Dry-run → `{nodeResults}` | +| `GET`/`POST` `…/workflow/{wid}/secret`, `PUT`/`DELETE` `…/secret/{sid}` | Secrets (write-only values) | +| `GET` `…/workflow/{wid}/execution`, `GET` `…/execution/{eid}`, `POST` `…/execution/{eid}/rerun` | Run history / rerun | +| `POST` `/api/table/{tid}/record/{rid}/{fid}/button-click` | Fire a button field | +| `POST` `/api/webhook/{bid}/{wid}` | Fire a webhook (⚠️ real route — the UI shows `/api/webhook/base/{bid}/workflow/{wid}` which is WRONG; `authorization` is never validated) | + +### Misc + +| Method & route | Purpose | +|---|---| +| `POST /api/base/{bid}/sql` | Read-only SQL (`{sql}`; JOIN/GROUP BY; no record ids) | +| `GET`/`POST` `/api/access-token`, `DELETE /api/access-token/{id}` | PAT management | +| `POST /api/auth/signup` | Create account (open on this instance) | + +## 3. Canonical Examples + +```bash +BASE=http://localhost:3003; AUTH="Authorization: Bearer $TOKEN" + +# create records (single + bulk). typecast: accepts strings for non-text +# fields; fieldKeyType 'id' = keys are fld ids ('name' = field names) +curl -s -X POST "$BASE/api/table/tblXXX/record" -H "$AUTH" -H 'Content-Type: application/json' \ + -d '{"fieldKeyType":"id","typecast":true,"records":[{"fields":{"fldTitle":"hello","fldStatus":"Todo"}}]}' + +# batch update — each record needs its id +curl -s -X PATCH "$BASE/api/table/tblXXX/record" -H "$AUTH" -H 'Content-Type: application/json' \ + -d '{"fieldKeyType":"id","typecast":true,"records":[{"id":"recXXX","fields":{"fldStatus":"Done"}}]}' + +# query with filter (JSON must be url-encoded), pagination +FILTER='{"conjunction":"and","filterSet":[{"fieldId":"fldStatus","operator":"is","value":"Todo"}]}' +curl -s -G "$BASE/api/table/tblXXX/record" -H "$AUTH" \ + --data-urlencode "filter=$FILTER" --data-urlencode "take=50" --data-urlencode "skip=0" + +# change field options — MUST go through convert (PATCH strips options) +curl -s -X PUT "$BASE/api/table/tblXXX/field/fldXXX/convert" -H "$AUTH" -H 'Content-Type: application/json' \ + -d '{"type":"singleSelect","options":{"choices":[{"name":"Todo"},{"name":"Done"}]}}' + +# automation: create → wire nodes → activate. Node JSON uses the SELF-BUILT +# contract: trigger "conditions" (not filter), "fieldIds" (not watchFieldIds), +# category "recordCondition" (not recordMatchesConditions) — full shapes in +# trigger-types.md / action-types.md +curl -s -X POST "$BASE/api/base/bseXXX/workflow" -H "$AUTH" -H 'Content-Type: application/json' \ + -d '{"name":"On Go click"}' +curl -s -X PUT "$BASE/api/base/bseXXX/workflow/wflXXX/node" -H "$AUTH" -H 'Content-Type: application/json' \ + -d '{"nodes":[{"type":"trigger","data":{"triggerType":"buttonClick","tableId":"tblXXX","fieldIds":["fldXXX"]}},{"type":"action","data":{"actionType":"script","code":"return triggerInput.record.fields"}}]}' +curl -s -X POST "$BASE/api/base/bseXXX/workflow/wflXXX/activate" -H "$AUTH" +``` + +Inside an automation **script action**, don't hardcode the token — call back +with the injected values: + +```js +const res = await fetch(triggerInput._apiBaseUrl + "/api/table/tblB/record", { + method: "POST", + headers: { Authorization: "Bearer " + triggerInput._accessToken, "Content-Type": "application/json" }, + body: JSON.stringify({ records: [{ fields: { fldLog: "done" } }] }), +}); +``` + +## 4. Gotchas (read before debugging a silent no-op) + +1. **`typecast:true` does NOT create select choices.** An invalid + singleSelect string resolves to null and the update becomes a **silent + no-op** (`mutationApplied=false`): HTTP 200, response echoes the OLD value, + `__last_modified_time` frozen. Always write existing choice values — check + `field get` / select-visibility first. +2. **400 "does not meet the current row conditions"** — value rejected by the + choice's `visibilityFilter`. Query + `GET /api/table/{tid}/select-visibility/{rid}/{fid}` for the allowed set. +3. **Field `options` only via the convert route** — plain `PATCH field` strips + them. +4. **`PUT …/node` is whole-graph replacement** — GET current nodes, modify, + PUT all of them back. Node edits take effect only after re-`activate` + (`hasDraft: true` = unactivated changes). +5. **Response-error shape**: non-2xx returns `{status, code, message}`; + `401` = bad/expired token, `403` = scope or base-role too low. +6. To see the exact request the CLI sends for any operation, run it with + `--debug` (prints method/url/body) — the fastest way to learn a route.