From 4462475a6f2c76644ac2c50560856109811df041 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Wed, 14 Oct 2026 00:21:00 +0000 Subject: [PATCH] =?UTF-8?q?feat(cli):=20record=20=E5=86=99=E5=85=A5=20--ty?= =?UTF-8?q?pecast/--order/--field-key-type=20+=20=E9=99=84=E4=BB=B6?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=20URL=20=E5=AF=BC=E5=85=A5=20skill=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=20(0.2.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- scripts/smoke-commands.sh | 32 +++++ skills/inkreach-teable/SKILL.md | 2 + .../api-reference/field-types.md | 4 +- .../inkreach-teable/api-reference/rest-api.md | 10 ++ .../inkreach-teable/guides/cli-reference.md | 14 ++- .../inkreach-teable/guides/tables-fields.md | 31 ++++- src/commands/resources.ts | 114 +++++++++++++++++- src/lib/client.ts | 15 ++- 9 files changed, 206 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 72a27ed..ba2263a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@inkreach/inkreach-teable", - "version": "0.1.9", + "version": "0.2.0", "description": "CLI (command: inkreach-teable) for operating inkreach-teable: tables, fields, records, views, and the self-built automation engine (8 triggers + 7 actions).", "license": "MIT", "type": "commonjs", diff --git a/scripts/smoke-commands.sh b/scripts/smoke-commands.sh index a40343b..eb09bba 100755 --- a/scripts/smoke-commands.sh +++ b/scripts/smoke-commands.sh @@ -82,7 +82,32 @@ smoke "field update --options" $CLI field update "$TBL" Status --options '{" smoke "field delete" $CLI field delete "$TBL" Extra # ---------- record ---------- +smoke "field create (attachment)" $CLI field create --table-id "$TBL" --name Files --type attachment smoke "record create --fields" $CLI record create --table-id "$TBL" --fields '{"Title":"r1","Status":"todo","Num":1}' +# Attachment fields accept http(s) urls: string form needs --typecast, object +# form works without. Downloads a real file from the local gitea; a network +# failure degrades to a warning (feature check, not a route-shape check). +ATT_URL='https://gitea.inkreach.cc/inkreachhub/inkreach-teable-cli/raw/branch/main/README.md' +ATT_OUT=$($CLI record create --table-id "$TBL" --typecast --fields "{\"Title\":\"att1\",\"Files\":\"$ATT_URL\"}" --json 2>&1) +if echo "$ATT_OUT" | grep -qE "$FAILPAT"; then + FAIL=$((FAIL+1)); FAILED_CASES+=("record create attachment-url (typecast)") + echo "FAIL record create attachment-url (typecast)" + echo "$ATT_OUT" | grep -E "$FAILPAT" | head -2 | sed 's/^/ /' +elif echo "$ATT_OUT" | grep -q '"token"'; then + PASS=$((PASS+1)); [ "${SMOKE_VERBOSE:-0}" = 1 ] && echo "pass record create attachment-url (typecast)" +else + echo "warn record create attachment-url (typecast): no attachment token in response (download failed?)" +fi +ATT_OUT2=$($CLI record create --table-id "$TBL" --fields "{\"Title\":\"att2\",\"Files\":[{\"url\":\"$ATT_URL\",\"name\":\"readme.md\"}]}" --json 2>&1) +if echo "$ATT_OUT2" | grep -qE "$FAILPAT"; then + FAIL=$((FAIL+1)); FAILED_CASES+=("record create attachment-url (object)") + echo "FAIL record create attachment-url (object)" + echo "$ATT_OUT2" | grep -E "$FAILPAT" | head -2 | sed 's/^/ /' +elif echo "$ATT_OUT2" | grep -q '"token"'; then + PASS=$((PASS+1)); [ "${SMOKE_VERBOSE:-0}" = 1 ] && echo "pass record create attachment-url (object)" +else + echo "warn record create attachment-url (object): no attachment token in response (download failed?)" +fi smoke "record create --records" $CLI record create --table-id "$TBL" --records '[{"fields":{"Title":"r2"}},{"fields":{"Title":"r3"}}]' REC=$($CLI record list --table-id "$TBL" --json | j "d['records'][0]['id']") STATUS_FLD=$($CLI field list "$TBL" --json | j "[f['id'] for f in d if f['name']=='Status'][0]") @@ -96,6 +121,13 @@ smoke "record get --projection" $CLI record get --table-id "$TBL" --projectio smoke "record get-one" $CLI record get-one "$REC" --table-id "$TBL" smoke "record get --record-id" $CLI record get --record-id "$REC" --table-id "$TBL" smoke "record update" $CLI record update --table-id "$TBL" --records "[{\"id\":\"$REC\",\"fields\":{\"Num\":42}}]" +# --field-key-type / --order are body keys of create/updateRecordsRoSchema +VIEW=$($CLI view list "$TBL" --json | j "d[0]['id']") +TITLE_FLD=$($CLI field list "$TBL" --json | j "[f['id'] for f in d if f['name']=='Title'][0]") +REC2=$($CLI record list --table-id "$TBL" --json | j "d['records'][1]['id']") +smoke "record create --field-key-type id" $CLI record create --table-id "$TBL" --field-key-type id --fields "{\"$TITLE_FLD\":\"by-fld-id\"}" +smoke "record create --order" $CLI record create --table-id "$TBL" --order "{\"viewId\":\"$VIEW\",\"anchorId\":\"$REC2\",\"position\":\"before\"}" --fields '{"Title":"ordered-create"}' +smoke "record update --order" $CLI record update --table-id "$TBL" --order "{\"viewId\":\"$VIEW\",\"anchorId\":\"$REC2\",\"position\":\"after\"}" --records "[{\"id\":\"$REC\",\"fields\":{\"Num\":7}}]" smoke "record visible-choices" $CLI field visible-choices "$TBL" "$REC" Status smoke "record delete (positional)" $CLI record delete "$REC" --table-id "$TBL" smoke "record delete (--csv)" $CLI record delete --record-ids "$($CLI record list --table-id "$TBL" --json | j "','.join(r['id'] for r in d['records'])")" --table-id "$TBL" diff --git a/skills/inkreach-teable/SKILL.md b/skills/inkreach-teable/SKILL.md index fe98fb9..e087682 100644 --- a/skills/inkreach-teable/SKILL.md +++ b/skills/inkreach-teable/SKILL.md @@ -61,6 +61,7 @@ Full command syntax: [cli-reference.md](guides/cli-reference.md). Instance acces | **Inspect select-choice constraints** | `field list --with-options`, `field get `, `field visible-choices` | `--json` + manual filtering | | **Write a select-field value** | Verify the value exists in choices first (`field list --with-options`); need a new value → `field update --options` to add the choice, then write | Blind `record update` (400, or typecast silently resolves to null / mutationApplied=false — it never creates choices) | | Read records for later writes | `record get` / `record list` (returns record IDs) | Official-teable SQL APIs (none here) | +| **Attach a remote file to a record** | `record create/update --fields '{"Files":[{"url":"https://…"}]}'` (backend downloads & stores it) | The multipart `uploadAttachment` route or manual signature/notify upload (only needed for local files) | | Analytics / aggregation | `record get --filter` + compute, script action, or grouped grid view | `sql` — this deployment has NO SQL query API (CLI 0.1.5 removed the dead command) | | 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 | @@ -90,6 +91,7 @@ These differ from official teable and from the (stale) `docs/automation/triggers inkreach-teable auth login --endpoint https://table.inkreach.cc --token # once inkreach-teable table create --name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"singleSelect","options":{"choices":["Todo","Done"]}}]' inkreach-teable record create --table-id tblXXX --fields '{"Title":"a"}' # 单条;批量用 --records '[{"fields":{...}}]' +inkreach-teable record create --table-id tblXXX --fields '{"Files":[{"url":"https://…/f.png"}]}' # 附件字段直接吃 URL(服务端下载入库);字符串形式加 --typecast inkreach-teable record list --table-id tblXXX [--filter ''] # 查询/分页(= record get) inkreach-teable record delete --table-id tblXXX recXXX [recYYY ...] # 也可 --record-ids 'recA,recB' inkreach-teable workflow create --name "On Go click" # → wflXXX diff --git a/skills/inkreach-teable/api-reference/field-types.md b/skills/inkreach-teable/api-reference/field-types.md index fa6701a..699c841 100644 --- a/skills/inkreach-teable/api-reference/field-types.md +++ b/skills/inkreach-teable/api-reference/field-types.md @@ -21,8 +21,7 @@ Complete reference of all Teable field types, aliases, option shapes, and smart | `date` | Date | Date or datetime | No | | `checkbox` | Checkbox | Yes/no toggle | No | | `rating` | Rating | Star/numeric rating 1..10 | No (defaults: max=5, star, yellow) | -| `attachment` | Attachment | Files, images, documents | No | -| `user` | User | Person/collaborator reference | No | +| `attachment` | Attachment | Files, images, documents; cell values also accept `{"url":"https://…"}` items (server downloads the file) | No || `user` | User | Person/collaborator reference | No | | `link` | Link | Reference records in another table | Yes (`foreignTableId`/`foreignTableName`) | | `lookup` | Lookup | Mirror a field from linked record | Yes (`lookupOptions`) | | `rollup` | Rollup | Aggregate linked records (SUM, COUNT, etc.) | Yes (expression + `lookupOptions`) | @@ -410,6 +409,7 @@ When `--type` is omitted, the CLI infers the type from the field name: | `user` | `{"id":"usrXXX","title":"Alice"}` | `"Alice"` or `"alice@example.com"` | | `link` (single) | `{"id":"recXXX"}` or `[{"id":"recXXX"}]` | `"Record Display Name"` | | `link` (multi) | `[{"id":"recA"},{"id":"recB"}]` | `["Record A","Record B"]` | +| `attachment` | `[{"url":"https://…/f.png","name":"f.png"}]` (V1 legacy bases: token items only) | `"https://…/f.png"` or `"https://…/a.png,actXXX"` (comma-separated urls/ids) | **When to use**: always use `--typecast` when creating/updating records with user or link fields — it eliminates manual ID lookups. diff --git a/skills/inkreach-teable/api-reference/rest-api.md b/skills/inkreach-teable/api-reference/rest-api.md index 1fc413b..ac131a8 100644 --- a/skills/inkreach-teable/api-reference/rest-api.md +++ b/skills/inkreach-teable/api-reference/rest-api.md @@ -152,6 +152,16 @@ const res = await fetch(triggerInput._apiBaseUrl + "/api/table/tblB/record", { `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. +7. **Attachment fields accept URLs** (2026-10): pass `{"url":"https://…","name":"…"}` items + (or a URL string with `typecast:true` — note `typecast` is a BODY key) in + record create/update `fields`, and the backend downloads the file + (SSRF-guarded, size-capped) into attachment storage — response items come + back with real `token`/`presignedUrl`. A failed download rejects the whole + request (400); nothing is silently dropped. V1 (legacy) bases need + `typecast:true` for the string form, V2 bases accept both. The dedicated + `POST /api/table/{tid}/record/{rid}/{fid}/uploadAttachment` (multipart, + `file` or `fileUrl`) is the older single-record path — the plain record + routes now cover URL imports. ## 5. Full Route Inventory (this guide ≠ the whole API) diff --git a/skills/inkreach-teable/guides/cli-reference.md b/skills/inkreach-teable/guides/cli-reference.md index ea68053..bb3b88e 100644 --- a/skills/inkreach-teable/guides/cli-reference.md +++ b/skills/inkreach-teable/guides/cli-reference.md @@ -71,6 +71,8 @@ inkreach-teable record get-one --table-id inkreach-teable record create --table-id --fields '' # single inkreach-teable record create --table-id --records '' # batch inkreach-teable record update --table-id --records '' +# shared write flags: [--typecast] [--field-key-type ] +# [--order '{"viewId":"viwX","anchorId":"recY","position":"before|after"}'] — insert/move in a manual-sort view inkreach-teable record delete --table-id [recordId ...] # or --record-ids # view @@ -200,10 +202,14 @@ Manage plugins and developer whitelist. inkreach-teable plugin list # Register a URL-served plugin (runtimeType=url; status published immediately) -# positions: "base"|"panel"|"view"|"dashboard"|"contextMenu" +# positions: "base"|"panel"|"view"|"dashboard"|"contextMenu"|"route" inkreach-teable plugin register --url --name [--positions '["panel"]'] [--logo ] [--description ] -# Bundle (upload) plugins use a different flow — see skill guides/tables-fields.md -# or api docs: create kebab-id plugin (developer whitelist) → upload → admin publish. +# Bundle (IIFE upload) plugins use a different flow — use the +# `inkreach-teable-plugin` skill instead: teable-plugin create/dev/deploy +# (scaffold → watch+hot-reload dev loop → multi-asset upload with chunks, +# source maps and css). Since 2026-10: POST /api/plugin (custom kebab-id, +# plugin_developer whitelist) and upload-bundle both accept PATs, and +# position "route" renders full-page apps at /base/:baseId/p/:pluginId. # Developer whitelist (admin only) inkreach-teable plugin dev list @@ -215,6 +221,8 @@ inkreach-teable plugin install --plugin --name "My Plugin" --position # ⚠️ For view/dashboard-position plugins, run `plugin config enable` BEFORE # install, or the install API returns 403 RESTRICTED_RESOURCE # 'Plugin is not enabled for this base'. +# position "route" mounts a full-page app at /base/:baseId/p/:pluginId +# (no enable gate — the URL is the access point). inkreach-teable plugin installs # list installs in current base inkreach-teable plugin uninstall --install diff --git a/skills/inkreach-teable/guides/tables-fields.md b/skills/inkreach-teable/guides/tables-fields.md index 2271981..5701f3e 100644 --- a/skills/inkreach-teable/guides/tables-fields.md +++ b/skills/inkreach-teable/guides/tables-fields.md @@ -251,6 +251,25 @@ inkreach-teable record create --table-id tblXXX --fields '{"Assignee":{"id":"usr inkreach-teable record create --table-id tblXXX --fields '{"Assignee":"Alice"}' --typecast ``` +**Attachment URL import** (2026-10): attachment fields accept http(s) URLs — the +backend downloads the file (SSRF-guarded, size-capped) and stores it as a real +attachment with thumbnail. Two forms: + +```bash +# Object form — works without --typecast; name is optional (derived from the URL) +inkreach-teable record create --table-id tblXXX \ + --fields '{"Files":[{"url":"https://example.com/f.png","name":"report.png"}]}' + +# String form — needs --typecast; comma-separates URLs and existing actXXX ids +inkreach-teable record create --table-id tblXXX --typecast \ + --fields '{"Files":"https://example.com/a.png,https://example.com/b.png"}' +``` + +Notes: only `http:`/`https:` URLs; URLs containing commas cannot be expressed in +the string form (use the object form); a failed download rejects the whole +request with 400 (nothing is silently dropped); V1 (legacy) bases require +`--typecast` for the string form, V2 bases accept both forms directly. + **`--records` format** (batch): `{"header":["Field1","Field2"],"records":[["val1","val2"],["val3","val4"]]}` ### Update @@ -261,7 +280,7 @@ inkreach-teable record update --table-id --records '' **`--records` format**: `[{"id":"recXXX","fields":{"FieldName":"newValue"}}]` — array of `{id, fields}` objects. Batch up to 2000. -**Record reordering**: pass `--order` with `viewId`, `anchorId`, `position`: +**Record reordering**: pass `--order` with `viewId`, `anchorId`, `position` (works on both `record create` — insert at position — and `record update` — move existing records; only meaningful in a view without an explicit sort): ```bash inkreach-teable record update --table-id tblXXX \ @@ -269,6 +288,13 @@ inkreach-teable record update --table-id tblXXX \ --order '{"viewId":"viwXXX","anchorId":"recYYY","position":"before"}' ``` +**`--field-key-type `** (default `name`): read `--fields`/`--records` keys as field ids instead of names — useful when field names contain characters that are awkward in JSON keys, or when you already have `fldXXX` ids from `field list --json`: + +```bash +inkreach-teable record create --table-id tblXXX --field-key-type id \ + --fields '{"fldXXXX":"hello"}' +``` + ### Delete ```bash @@ -378,7 +404,7 @@ inkreach-teable preset delete --table-id tblXxx | `singleSelect` | `"Option Name"` | (string) | | `multipleSelect` | `["Tag A","Tag B"]` | (array of strings) | | `rating` | `3` | Integer 1..max | -| `attachment` | `[{"name":"f.png","token":"xxx"}]` | Always array of objects; update **replaces all** | +| `attachment` | `[{"name":"f.png","token":"xxx"}]` | Always array of objects; update **replaces all**. Also accepts `[{"url":"https://…","name":"f.png"}]` (server downloads the file) and, with `--typecast`, a plain URL string — see attachment URL import below | | `user` | `{"id":"usrXXX","title":"Alice"}` | With `--typecast`: just `"Alice"` or `"alice@example.com"` | | `link` | `[{"id":"recXXX"}]` | With `--typecast`: just `"Linked Record Name"` | | `formula` / `rollup` / `lookup` | Read-only | Cannot be set via record create/update | @@ -404,4 +430,5 @@ inkreach-teable preset delete --table-id tblXxx | View data in kanban board | `view create --type kanban` then `view update --group` | Creating special columns | | Set datetime in a date field | `"2025-06-15T14:30:00.000Z"` | `"2025-06-15 14:30"` (non-ISO; may be rejected) | | Update attachments | Always pass the full array (replaces all) | Trying to add/remove individual attachments | +| Attach a file by URL | `record create --fields '{"Files":[{"url":"https://…/f.png"}]}'` (object form, no typecast needed) or `--typecast --fields '{"Files":"https://…/f.png"}'` | Uploading via the attachments signature/notify endpoints first (only needed for local files) | | Batch insert 500 records | One `record create --records` call | 500 separate `record create` calls | diff --git a/src/commands/resources.ts b/src/commands/resources.ts index 15d177f..5c1588d 100644 --- a/src/commands/resources.ts +++ b/src/commands/resources.ts @@ -20,6 +20,55 @@ import { } from '../lib/field-normalize'; import { formatFieldDetail, summarizeFieldOptions, choiceRows } from '../lib/field-display'; +const FIELD_KEY_TYPES = ['name', 'id', 'dbFieldName'] as const; +type FieldKeyTypeCli = (typeof FIELD_KEY_TYPES)[number]; + +/** Validate --field-key-type (backend body key `fieldKeyType`, default "name"). */ +function parseFieldKeyType(raw: string | undefined): FieldKeyTypeCli | undefined { + if (raw === undefined) return undefined; + if (!(FIELD_KEY_TYPES as readonly string[]).includes(raw)) { + throw Object.assign( + new Error(`--field-key-type must be one of: ${FIELD_KEY_TYPES.join(', ')}`), + { code: 'BAD_FIELD_KEY_TYPE' } + ); + } + return raw as FieldKeyTypeCli; +} + +type RecordOrderRo = { viewId: string; anchorId: string; position: 'before' | 'after' }; + +/** + * Validate --order (backend body key `order` on record create/update): + * place the written records before/after an anchor record in a + * manually-sorted view. + */ +function parseRecordOrder(raw: string): RecordOrderRo { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw Object.assign( + new Error('--order must be JSON: {"viewId":"viwXXX","anchorId":"recYYY","position":"before"}'), + { code: 'BAD_JSON' } + ); + } + const obj = parsed as Record; + const { viewId, anchorId, position } = obj ?? {}; + if ( + typeof viewId !== 'string' || + typeof anchorId !== 'string' || + (position !== 'before' && position !== 'after') + ) { + throw Object.assign( + new Error( + '--order needs string viewId + anchorId and position "before"|"after", e.g. {"viewId":"viwXXX","anchorId":"recYYY","position":"before"}' + ), + { code: 'BAD_ORDER' } + ); + } + return { viewId, anchorId, position }; +} + /** * Fetch a field by ID or display name. A direct ID hit needs no extra call; * a 400/404 miss (the API rejects non-ID paths) falls back to resolving the @@ -677,10 +726,34 @@ function recordCmd(): Command { .requiredOption('--table-id ', 'Table id') .option('--fields ', 'Fields object JSON, e.g. \'{"Title":"hello"}\'') .option('--records ', 'Records array JSON for bulk, e.g. \'[{"fields":{"Title":"a"}}]\'') + .option( + '--typecast', + 'Send typecast=true (accepts human-readable values; attachment fields accept http(s) urls)' + ) + .option( + '--field-key-type ', + 'Keys in --fields/--records are field ids ("id"), names ("name", default) or dbFieldName' + ) + .option( + '--order ', + 'Insert position in a manually-sorted view: {"viewId":"viwXXX","anchorId":"recYYY","position":"before|after"}' + ) .action( run(async (ctx, _args, o) => { - const opts = o as { tableId: string; fields?: string; records?: string }; - let body: unknown; + const opts = o as { + tableId: string; + fields?: string; + records?: string; + typecast?: boolean; + fieldKeyType?: string; + order?: string; + }; + let body: { + typecast?: boolean; + fieldKeyType?: FieldKeyTypeCli; + order?: RecordOrderRo; + records: unknown; + }; if (opts.records) { body = { records: JSON.parse(opts.records) }; } else if (opts.fields) { @@ -692,6 +765,11 @@ function recordCmd(): Command { code: 'MISSING_FIELDS', }); } + // typecast/fieldKeyType/order are BODY keys of createRecordsRoSchema (not query params) + if (opts.typecast) body.typecast = true; + const fieldKeyType = parseFieldKeyType(opts.fieldKeyType); + if (fieldKeyType) body.fieldKeyType = fieldKeyType; + if (opts.order !== undefined) body.order = parseRecordOrder(opts.order); const result = await ctx.client.createRecord(opts.tableId, body); if (ctx.json) printJson(result); else process.stdout.write(`✓ Record(s) created\n`); @@ -706,10 +784,38 @@ function recordCmd(): Command { '--records ', 'Records JSON, e.g. \'[{"id":"recX","fields":{"Title":"b"}}]\'' ) + .option( + '--typecast', + 'Send typecast=true (accepts human-readable values; attachment fields accept http(s) urls)' + ) + .option( + '--field-key-type ', + 'Keys in --records fields are field ids ("id"), names ("name", default) or dbFieldName' + ) + .option( + '--order ', + 'Move the updated record(s) before/after an anchor in a manually-sorted view: {"viewId":"viwXXX","anchorId":"recYYY","position":"before|after"}' + ) .action( run(async (ctx, _args, o) => { - const opts = o as { tableId: string; records: string }; - const body = { records: JSON.parse(opts.records) }; + const opts = o as { + tableId: string; + records: string; + typecast?: boolean; + fieldKeyType?: string; + order?: string; + }; + const body: { + typecast?: boolean; + fieldKeyType?: FieldKeyTypeCli; + order?: RecordOrderRo; + records: unknown; + } = { records: JSON.parse(opts.records) }; + // typecast/fieldKeyType/order are BODY keys of updateRecordsRoSchema (not query params) + if (opts.typecast) body.typecast = true; + const fieldKeyType = parseFieldKeyType(opts.fieldKeyType); + if (fieldKeyType) body.fieldKeyType = fieldKeyType; + if (opts.order !== undefined) body.order = parseRecordOrder(opts.order); const result = await ctx.client.updateRecord(opts.tableId, body); if (ctx.json) printJson(result); else process.stdout.write(`✓ Records updated\n`); diff --git a/src/lib/client.ts b/src/lib/client.ts index 035df30..f389fe4 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -153,14 +153,17 @@ export class Client { const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : ''; return this.call('GET', `/api/table/${tableId}/record/${recordId}${qs}`); } - createRecord(tableId: string, body: unknown) { - return this.call('POST', `/api/table/${tableId}/record`, body); + createRecord(tableId: string, body: unknown, query?: Record) { + const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : ''; + return this.call('POST', `/api/table/${tableId}/record${qs}`, body); } - updateRecord(tableId: string, body: unknown) { - return this.call('PATCH', `/api/table/${tableId}/record`, body); + updateRecord(tableId: string, body: unknown, query?: Record) { + const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : ''; + return this.call('PATCH', `/api/table/${tableId}/record${qs}`, body); } - updateRecordOne(tableId: string, recordId: string, body: unknown) { - return this.call('PATCH', `/api/table/${tableId}/record/${recordId}`, body); + updateRecordOne(tableId: string, recordId: string, body: unknown, query?: Record) { + const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : ''; + return this.call('PATCH', `/api/table/${tableId}/record/${recordId}${qs}`, body); } deleteRecords(tableId: string, recordIds: string[]) { if (recordIds.length === 1) {