feat(cli): record 写入 --typecast/--order/--field-key-type + 附件字段 URL 导入 skill 文档 (0.2.0)
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@inkreach/inkreach-teable",
|
"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).",
|
"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",
|
"license": "MIT",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
|
|||||||
@@ -82,7 +82,32 @@ smoke "field update --options" $CLI field update "$TBL" Status --options '{"
|
|||||||
smoke "field delete" $CLI field delete "$TBL" Extra
|
smoke "field delete" $CLI field delete "$TBL" Extra
|
||||||
|
|
||||||
# ---------- record ----------
|
# ---------- 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}'
|
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"}}]'
|
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']")
|
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]")
|
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-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 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}}]"
|
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 visible-choices" $CLI field visible-choices "$TBL" "$REC" Status
|
||||||
smoke "record delete (positional)" $CLI record delete "$REC" --table-id "$TBL"
|
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"
|
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"
|
||||||
|
|||||||
@@ -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 <tbl> <name>`, `field visible-choices` | `--json` + manual filtering |
|
| **Inspect select-choice constraints** | `field list --with-options`, `field get <tbl> <name>`, `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) |
|
| **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) |
|
| 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) |
|
| 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 |
|
| 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 |
|
| 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 <PAT> # once
|
inkreach-teable auth login --endpoint https://table.inkreach.cc --token <PAT> # once
|
||||||
inkreach-teable table create --name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"singleSelect","options":{"choices":["Todo","Done"]}}]'
|
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 '{"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 '<json>'] # 查询/分页(= record get)
|
inkreach-teable record list --table-id tblXXX [--filter '<json>'] # 查询/分页(= record get)
|
||||||
inkreach-teable record delete --table-id tblXXX recXXX [recYYY ...] # 也可 --record-ids 'recA,recB'
|
inkreach-teable record delete --table-id tblXXX recXXX [recYYY ...] # 也可 --record-ids 'recA,recB'
|
||||||
inkreach-teable workflow create --name "On Go click" # → wflXXX
|
inkreach-teable workflow create --name "On Go click" # → wflXXX
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ Complete reference of all Teable field types, aliases, option shapes, and smart
|
|||||||
| `date` | Date | Date or datetime | No |
|
| `date` | Date | Date or datetime | No |
|
||||||
| `checkbox` | Checkbox | Yes/no toggle | No |
|
| `checkbox` | Checkbox | Yes/no toggle | No |
|
||||||
| `rating` | Rating | Star/numeric rating 1..10 | No (defaults: max=5, star, yellow) |
|
| `rating` | Rating | Star/numeric rating 1..10 | No (defaults: max=5, star, yellow) |
|
||||||
| `attachment` | Attachment | Files, images, documents | No |
|
| `attachment` | Attachment | Files, images, documents; cell values also accept `{"url":"https://…"}` items (server downloads the file) | No || `user` | User | Person/collaborator reference | No |
|
||||||
| `user` | User | Person/collaborator reference | No |
|
|
||||||
| `link` | Link | Reference records in another table | Yes (`foreignTableId`/`foreignTableName`) |
|
| `link` | Link | Reference records in another table | Yes (`foreignTableId`/`foreignTableName`) |
|
||||||
| `lookup` | Lookup | Mirror a field from linked record | Yes (`lookupOptions`) |
|
| `lookup` | Lookup | Mirror a field from linked record | Yes (`lookupOptions`) |
|
||||||
| `rollup` | Rollup | Aggregate linked records (SUM, COUNT, etc.) | Yes (expression + `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"` |
|
| `user` | `{"id":"usrXXX","title":"Alice"}` | `"Alice"` or `"alice@example.com"` |
|
||||||
| `link` (single) | `{"id":"recXXX"}` or `[{"id":"recXXX"}]` | `"Record Display Name"` |
|
| `link` (single) | `{"id":"recXXX"}` or `[{"id":"recXXX"}]` | `"Record Display Name"` |
|
||||||
| `link` (multi) | `[{"id":"recA"},{"id":"recB"}]` | `["Record A","Record B"]` |
|
| `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.
|
**When to use**: always use `--typecast` when creating/updating records with user or link fields — it eliminates manual ID lookups.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
`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
|
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.
|
`--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)
|
## 5. Full Route Inventory (this guide ≠ the whole API)
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ inkreach-teable record get-one <recordId> --table-id <tid>
|
|||||||
inkreach-teable record create --table-id <tid> --fields '<json>' # single
|
inkreach-teable record create --table-id <tid> --fields '<json>' # single
|
||||||
inkreach-teable record create --table-id <tid> --records '<json>' # batch
|
inkreach-teable record create --table-id <tid> --records '<json>' # batch
|
||||||
inkreach-teable record update --table-id <tid> --records '<json>'
|
inkreach-teable record update --table-id <tid> --records '<json>'
|
||||||
|
# shared write flags: [--typecast] [--field-key-type <name|id|dbFieldName>]
|
||||||
|
# [--order '{"viewId":"viwX","anchorId":"recY","position":"before|after"}'] — insert/move in a manual-sort view
|
||||||
inkreach-teable record delete --table-id <tid> <recordId> [recordId ...] # or --record-ids <csv>
|
inkreach-teable record delete --table-id <tid> <recordId> [recordId ...] # or --record-ids <csv>
|
||||||
|
|
||||||
# view
|
# view
|
||||||
@@ -200,10 +202,14 @@ Manage plugins and developer whitelist.
|
|||||||
inkreach-teable plugin list
|
inkreach-teable plugin list
|
||||||
|
|
||||||
# Register a URL-served plugin (runtimeType=url; status published immediately)
|
# 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 <url> --name <name> [--positions '["panel"]'] [--logo <url>] [--description <d>]
|
inkreach-teable plugin register --url <url> --name <name> [--positions '["panel"]'] [--logo <url>] [--description <d>]
|
||||||
# Bundle (upload) plugins use a different flow — see skill guides/tables-fields.md
|
# Bundle (IIFE upload) plugins use a different flow — use the
|
||||||
# or api docs: create kebab-id plugin (developer whitelist) → upload → admin publish.
|
# `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)
|
# Developer whitelist (admin only)
|
||||||
inkreach-teable plugin dev list
|
inkreach-teable plugin dev list
|
||||||
@@ -215,6 +221,8 @@ inkreach-teable plugin install --plugin <pluginId> --name "My Plugin" --position
|
|||||||
# ⚠️ For view/dashboard-position plugins, run `plugin config enable` BEFORE
|
# ⚠️ For view/dashboard-position plugins, run `plugin config enable` BEFORE
|
||||||
# install, or the install API returns 403 RESTRICTED_RESOURCE
|
# install, or the install API returns 403 RESTRICTED_RESOURCE
|
||||||
# 'Plugin is not enabled for this base'.
|
# '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 installs # list installs in current base
|
||||||
inkreach-teable plugin uninstall --install <installId>
|
inkreach-teable plugin uninstall --install <installId>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
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"]]}`
|
**`--records` format** (batch): `{"header":["Field1","Field2"],"records":[["val1","val2"],["val3","val4"]]}`
|
||||||
|
|
||||||
### Update
|
### Update
|
||||||
@@ -261,7 +280,7 @@ inkreach-teable record update --table-id <tid> --records '<json>'
|
|||||||
|
|
||||||
**`--records` format**: `[{"id":"recXXX","fields":{"FieldName":"newValue"}}]` — array of `{id, fields}` objects. Batch up to 2000.
|
**`--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
|
```bash
|
||||||
inkreach-teable record update --table-id tblXXX \
|
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"}'
|
--order '{"viewId":"viwXXX","anchorId":"recYYY","position":"before"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**`--field-key-type <name|id|dbFieldName>`** (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
|
### Delete
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -378,7 +404,7 @@ inkreach-teable preset delete <presetId> --table-id tblXxx
|
|||||||
| `singleSelect` | `"Option Name"` | (string) |
|
| `singleSelect` | `"Option Name"` | (string) |
|
||||||
| `multipleSelect` | `["Tag A","Tag B"]` | (array of strings) |
|
| `multipleSelect` | `["Tag A","Tag B"]` | (array of strings) |
|
||||||
| `rating` | `3` | Integer 1..max |
|
| `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"` |
|
| `user` | `{"id":"usrXXX","title":"Alice"}` | With `--typecast`: just `"Alice"` or `"alice@example.com"` |
|
||||||
| `link` | `[{"id":"recXXX"}]` | With `--typecast`: just `"Linked Record Name"` |
|
| `link` | `[{"id":"recXXX"}]` | With `--typecast`: just `"Linked Record Name"` |
|
||||||
| `formula` / `rollup` / `lookup` | Read-only | Cannot be set via record create/update |
|
| `formula` / `rollup` / `lookup` | Read-only | Cannot be set via record create/update |
|
||||||
@@ -404,4 +430,5 @@ inkreach-teable preset delete <presetId> --table-id tblXxx
|
|||||||
| View data in kanban board | `view create --type kanban` then `view update --group` | Creating special columns |
|
| 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) |
|
| 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 |
|
| 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 |
|
| Batch insert 500 records | One `record create --records` call | 500 separate `record create` calls |
|
||||||
|
|||||||
+110
-4
@@ -20,6 +20,55 @@ import {
|
|||||||
} from '../lib/field-normalize';
|
} from '../lib/field-normalize';
|
||||||
import { formatFieldDetail, summarizeFieldOptions, choiceRows } from '../lib/field-display';
|
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<string, unknown>;
|
||||||
|
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;
|
* 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
|
* 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 <id>', 'Table id')
|
.requiredOption('--table-id <id>', 'Table id')
|
||||||
.option('--fields <json>', 'Fields object JSON, e.g. \'{"Title":"hello"}\'')
|
.option('--fields <json>', 'Fields object JSON, e.g. \'{"Title":"hello"}\'')
|
||||||
.option('--records <json>', 'Records array JSON for bulk, e.g. \'[{"fields":{"Title":"a"}}]\'')
|
.option('--records <json>', '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 <type>',
|
||||||
|
'Keys in --fields/--records are field ids ("id"), names ("name", default) or dbFieldName'
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
'--order <json>',
|
||||||
|
'Insert position in a manually-sorted view: {"viewId":"viwXXX","anchorId":"recYYY","position":"before|after"}'
|
||||||
|
)
|
||||||
.action(
|
.action(
|
||||||
run(async (ctx, _args, o) => {
|
run(async (ctx, _args, o) => {
|
||||||
const opts = o as { tableId: string; fields?: string; records?: string };
|
const opts = o as {
|
||||||
let body: unknown;
|
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) {
|
if (opts.records) {
|
||||||
body = { records: JSON.parse(opts.records) };
|
body = { records: JSON.parse(opts.records) };
|
||||||
} else if (opts.fields) {
|
} else if (opts.fields) {
|
||||||
@@ -692,6 +765,11 @@ function recordCmd(): Command {
|
|||||||
code: 'MISSING_FIELDS',
|
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);
|
const result = await ctx.client.createRecord(opts.tableId, body);
|
||||||
if (ctx.json) printJson(result);
|
if (ctx.json) printJson(result);
|
||||||
else process.stdout.write(`✓ Record(s) created\n`);
|
else process.stdout.write(`✓ Record(s) created\n`);
|
||||||
@@ -706,10 +784,38 @@ function recordCmd(): Command {
|
|||||||
'--records <json>',
|
'--records <json>',
|
||||||
'Records JSON, e.g. \'[{"id":"recX","fields":{"Title":"b"}}]\''
|
'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 <type>',
|
||||||
|
'Keys in --records fields are field ids ("id"), names ("name", default) or dbFieldName'
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
'--order <json>',
|
||||||
|
'Move the updated record(s) before/after an anchor in a manually-sorted view: {"viewId":"viwXXX","anchorId":"recYYY","position":"before|after"}'
|
||||||
|
)
|
||||||
.action(
|
.action(
|
||||||
run(async (ctx, _args, o) => {
|
run(async (ctx, _args, o) => {
|
||||||
const opts = o as { tableId: string; records: string };
|
const opts = o as {
|
||||||
const body = { records: JSON.parse(opts.records) };
|
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);
|
const result = await ctx.client.updateRecord(opts.tableId, body);
|
||||||
if (ctx.json) printJson(result);
|
if (ctx.json) printJson(result);
|
||||||
else process.stdout.write(`✓ Records updated\n`);
|
else process.stdout.write(`✓ Records updated\n`);
|
||||||
|
|||||||
+9
-6
@@ -153,14 +153,17 @@ export class Client {
|
|||||||
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
|
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
|
||||||
return this.call<unknown>('GET', `/api/table/${tableId}/record/${recordId}${qs}`);
|
return this.call<unknown>('GET', `/api/table/${tableId}/record/${recordId}${qs}`);
|
||||||
}
|
}
|
||||||
createRecord(tableId: string, body: unknown) {
|
createRecord(tableId: string, body: unknown, query?: Record<string, unknown>) {
|
||||||
return this.call<unknown>('POST', `/api/table/${tableId}/record`, body);
|
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
|
||||||
|
return this.call<unknown>('POST', `/api/table/${tableId}/record${qs}`, body);
|
||||||
}
|
}
|
||||||
updateRecord(tableId: string, body: unknown) {
|
updateRecord(tableId: string, body: unknown, query?: Record<string, unknown>) {
|
||||||
return this.call<unknown>('PATCH', `/api/table/${tableId}/record`, body);
|
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
|
||||||
|
return this.call<unknown>('PATCH', `/api/table/${tableId}/record${qs}`, body);
|
||||||
}
|
}
|
||||||
updateRecordOne(tableId: string, recordId: string, body: unknown) {
|
updateRecordOne(tableId: string, recordId: string, body: unknown, query?: Record<string, unknown>) {
|
||||||
return this.call<unknown>('PATCH', `/api/table/${tableId}/record/${recordId}`, body);
|
const qs = query ? '?' + new URLSearchParams(this.flattenQuery(query)).toString() : '';
|
||||||
|
return this.call<unknown>('PATCH', `/api/table/${tableId}/record/${recordId}${qs}`, body);
|
||||||
}
|
}
|
||||||
deleteRecords(tableId: string, recordIds: string[]) {
|
deleteRecords(tableId: string, recordIds: string[]) {
|
||||||
if (recordIds.length === 1) {
|
if (recordIds.length === 1) {
|
||||||
|
|||||||
Reference in New Issue
Block a user