@deepdockai/cli 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/deepdock.js +12 -4
- package/package.json +1 -1
- package/skill/SKILL.md +4 -2
- package/skill/references/calling-actions.md +50 -21
- package/skill/references/cli.md +1 -1
- package/skill/references/clients/__pycache__/deepdock.cpython-312.pyc +0 -0
- package/skill/references/clients/__pycache__/deepdock.cpython-313.pyc +0 -0
- package/skill/references/clients/deepdock.py +25 -16
- package/skill/references/clients/deepdock.ts +32 -18
- package/skill/references/concepts.md +5 -3
- package/skill/references/discovery.md +11 -12
- package/skill/references/errors.md +1 -1
- package/skill/references/example-integration.md +12 -11
- package/skill/references/idempotency.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.27
|
|
4
|
+
|
|
5
|
+
- An update is called with `PATCH` again; a read and a create stay `POST`. All three take a flat JSON body and nothing in the URL. The method must be the action's own: any other answers `405 invalid_request` with `Allow` naming the one method (`POST` or `PATCH`), so a `POST` to an update is refused and does nothing. A query string on an action path is still `400 invalid_request`. A write still needs an `Idempotency-Key`; a read ignores it.
|
|
6
|
+
- Each entry of the action index has a `method` (`POST` or `PATCH`) beside its `operation` (`read`, `create` or `update`).
|
|
7
|
+
- The bundled clients' `write` takes the action's operation after the action name (`write(deviceId, system, action, 'create' | 'update', key, input)`) and sends `POST` for a create and `PATCH` for an update; `PendingWrite` carries it as `operation`. Reads are unchanged. Idempotency handling is unchanged.
|
|
8
|
+
- The skill's references describe the contract.
|
|
9
|
+
|
|
10
|
+
## 1.0.26
|
|
11
|
+
|
|
12
|
+
- Every action is called with `POST /v1/devices/{deviceId}/systems/{system}/actions/{action}` and its inputs in a flat JSON body, reads and updates included. There is no `GET` read and no `PATCH` update any more: any other method answers `405 invalid_request` with `Allow: POST`, and a query string on an action path answers `400 invalid_request` ("inputs go in the JSON body, not in the URL"), so no value reaches a URL, a request log or a proxy. A read takes `limit` (a JSON integer) and `cursor` (a string) in the same body; a write still needs an `Idempotency-Key`, a read ignores it. Values are typed as given: an integer input is a JSON number, and `"1001"` is refused where it is declared as one.
|
|
13
|
+
- The action index keeps its route, but each entry's `method` is replaced by `operation` (`read`, `create` or `update`). `actions list` derives its read/write column from it.
|
|
14
|
+
- The bundled TypeScript and Python clients send reads as `POST` with a JSON body, including `limit` and `cursor`. `write` always sends `POST`, so its `method` parameter (and `PendingWrite.method`) is gone; input values are typed (`string | number | boolean`) instead of text only. Idempotency handling is unchanged.
|
|
15
|
+
- The skill's references describe the new contract.
|
|
16
|
+
|
|
3
17
|
## 1.0.25
|
|
4
18
|
|
|
5
19
|
- Three new error codes for writes over a customer system's own HTTP interface: `applied_unverified` (409, exit 2: the write landed but its read-back did not finish; `error.created` names the entry as `{ chartId }`, or null), `credentials_rejected` and `certificate_changed` (503, exit 3: a person at the practice renews the login or confirms the certificate in the Deepdock client). An earlier CLI already exits with the same codes through its status fallback.
|
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ deepdock skill
|
|
|
101
101
|
| `devices pair <code> [--label <text>]` | claim a device by the code on its screen |
|
|
102
102
|
| `devices rename <id> <label>`, `devices release <id>` | rename a device; unpair it from the organisation (admin) |
|
|
103
103
|
| `devices systems <id>` | a device's live systems and action state, paused reasons included |
|
|
104
|
-
| `actions list <device> <system>` | every action a device offers, with its
|
|
104
|
+
| `actions list <device> <system>` | every action a device offers, with its operation and fields |
|
|
105
105
|
| `keys list`, `keys create <name>`, `keys revoke <id>` | the organisation's API keys (create and revoke: admin) |
|
|
106
106
|
| `incidents list` | open incidents, and those closed within the last 30 days |
|
|
107
107
|
| `skill [--agent claude\|codex\|cursor\|all]` | install the agent skill into the current directory |
|
package/dist/deepdock.js
CHANGED
|
@@ -11236,13 +11236,21 @@ var summary = (description) => {
|
|
|
11236
11236
|
return chars.length > 160 ? `${chars.slice(0, 159).join("").trimEnd()}…` : sentence;
|
|
11237
11237
|
};
|
|
11238
11238
|
var actionRow = (action) => {
|
|
11239
|
-
const
|
|
11239
|
+
const operation = typeof action.operation === "string" ? action.operation : undefined;
|
|
11240
11240
|
const fields = fieldNames(action.input);
|
|
11241
|
-
const {
|
|
11241
|
+
const {
|
|
11242
|
+
name,
|
|
11243
|
+
operation: _operation,
|
|
11244
|
+
method: _method,
|
|
11245
|
+
input: _input,
|
|
11246
|
+
output: _output,
|
|
11247
|
+
description,
|
|
11248
|
+
...rest
|
|
11249
|
+
} = action;
|
|
11242
11250
|
const brief = summary(description);
|
|
11243
11251
|
return {
|
|
11244
11252
|
...name === undefined ? {} : { name },
|
|
11245
|
-
...
|
|
11253
|
+
...operation ? { kind: operation === "read" ? "read" : "write" } : {},
|
|
11246
11254
|
...fields ? { fields } : {},
|
|
11247
11255
|
...brief ? { description: brief } : {},
|
|
11248
11256
|
...rest
|
|
@@ -11432,7 +11440,7 @@ import { spawn } from "node:child_process";
|
|
|
11432
11440
|
import { homedir, hostname } from "node:os";
|
|
11433
11441
|
|
|
11434
11442
|
// src/version.ts
|
|
11435
|
-
var VERSION = "1.0.
|
|
11443
|
+
var VERSION = "1.0.27";
|
|
11436
11444
|
|
|
11437
11445
|
// src/runtime.ts
|
|
11438
11446
|
var DEFAULT_BASE_URL = "https://api.deepdock.ai";
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -22,8 +22,10 @@ integration against a demo device. It is not for building or changing actions: D
|
|
|
22
22
|
screen, and from then on it belongs to your organisation until you release it.
|
|
23
23
|
3. A device reports the **systems** it runs; each system has **actions**, deterministic operations
|
|
24
24
|
that Deepdock builds and verifies before offering them.
|
|
25
|
-
4.
|
|
26
|
-
|
|
25
|
+
4. Every action takes a flat JSON body, never a value in the URL: `POST` for a read or create,
|
|
26
|
+
`PATCH` for an update. A **read** (the
|
|
27
|
+
action's `operation`) also takes `limit` and `cursor` in that body to page; a **write** (`create`
|
|
28
|
+
or `update`) needs an `Idempotency-Key` as well.
|
|
27
29
|
5. Every write needs an `Idempotency-Key`: a new key per intended write, the same key on every retry,
|
|
28
30
|
and a repeat answers the stored result with `Idempotent-Replayed: true`, writing nothing twice.
|
|
29
31
|
6. A write either fully applies or not: Deepdock checks its result before confirming, and a write
|
|
@@ -3,16 +3,36 @@
|
|
|
3
3
|
All paths are under `/v1/devices/{deviceId}/systems/{system}/actions/{action}`. `deviceId` is the
|
|
4
4
|
UUID from pairing; `system` and `action` are the slugs from the action index.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
composed and a decomposed spelling are the same value.
|
|
6
|
+
## Calling an action: POST, or PATCH for an update
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
Every action is called with a flat JSON object as the body. A read and a create are `POST`, an
|
|
9
|
+
update is `PATCH`. What the action does is its `operation` (`read`, `create` or `update`), and its
|
|
10
|
+
`method` (`POST` or `PATCH`), in the action index and the OpenAPI document. Any other method
|
|
11
|
+
answers `405 invalid_request` with `Allow` naming the action's one method, so a `POST` to an update
|
|
12
|
+
is refused, not run.
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
Inputs are typed as the action's schema declares them and are never sent as text when they are not:
|
|
15
|
+
an integer is a JSON number (`"patientId": 4711`, not `"4711"`; the text form is refused with
|
|
16
|
+
`400 invalid_request`). Text is normalised to Unicode NFC before anything else happens; a composed
|
|
17
|
+
and a decomposed spelling are the same value.
|
|
18
|
+
|
|
19
|
+
**Never put an input in the URL.** A value in a URL reaches request logs, proxies and browser
|
|
20
|
+
history, which is where a patient's name or id must not end up. Any query string on an action
|
|
21
|
+
path, whatever it holds, answers `400 invalid_request` ("inputs go in the JSON body, not in the
|
|
22
|
+
URL") without repeating a name or a value. Because the body is JSON, a `+` or a non-ASCII letter
|
|
23
|
+
needs no escaping.
|
|
24
|
+
|
|
25
|
+
### Reads
|
|
26
|
+
|
|
27
|
+
A read takes its inputs, and `limit` (a JSON integer from 1 to 250, default 25) and `cursor` (a
|
|
28
|
+
string) for paging, in the same body. No `Idempotency-Key`: a repeated read changes nothing, and the
|
|
29
|
+
header is ignored.
|
|
12
30
|
|
|
13
31
|
```bash
|
|
14
|
-
curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers
|
|
15
|
-
-H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
32
|
+
curl -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers" \
|
|
33
|
+
-H "Authorization: Bearer $DEEPDOCK_API_KEY" \
|
|
34
|
+
-H "Content-Type: application/json" \
|
|
35
|
+
-d '{"lastName":"Müller","limit":50}'
|
|
16
36
|
```
|
|
17
37
|
|
|
18
38
|
```json
|
|
@@ -30,11 +50,10 @@ curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchCustomers?lastN
|
|
|
30
50
|
punctuation, umlaut spelling). Design your search UI around that rule rather than filtering
|
|
31
51
|
client-side.
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
### Writes (create and update)
|
|
34
54
|
|
|
35
|
-
Body: a JSON object
|
|
36
|
-
|
|
37
|
-
`Idempotency-Key`, required.
|
|
55
|
+
Body: a flat JSON object with only the fields the schema lists (`additionalProperties: false`) and
|
|
56
|
+
all of its `required` ones, each typed as the schema declares. Header `Idempotency-Key`, required.
|
|
38
57
|
|
|
39
58
|
```bash
|
|
40
59
|
curl -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/createNote" \
|
|
@@ -46,11 +65,11 @@ curl -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/createNote" \
|
|
|
46
65
|
|
|
47
66
|
- A create answers `201` with `{ data: { <returned fields> } }`, typically the new record's id. A
|
|
48
67
|
replay of the same key answers `200` with the same data and `Idempotent-Replayed: true`.
|
|
49
|
-
- An update answers `200` whether it ran now or is a replay; the header tells
|
|
68
|
+
- An update is a `PATCH` to the same path, with the same body rules, and answers `200` whether it ran now or is a replay; the header tells
|
|
69
|
+
you which.
|
|
50
70
|
- The returned id is the system's own id (for example `noteId`). Store it as the system gave it;
|
|
51
|
-
do not predict or re-key it. Result field types come from each action's own output schema:
|
|
52
|
-
|
|
53
|
-
string. Do not reuse one action's result type for another.
|
|
71
|
+
do not predict or re-key it. Result field types come from each action's own output schema: do not
|
|
72
|
+
reuse one action's result type for another.
|
|
54
73
|
- Each update under a new key writes again and may add an entry to the system's own change log,
|
|
55
74
|
even when values are unchanged. Do not "update" to no-op.
|
|
56
75
|
|
|
@@ -63,7 +82,7 @@ of a customer, the note id of an update. The pattern is always the same:
|
|
|
63
82
|
|
|
64
83
|
1. Read the reference with the matching read action (for example a users read, a case read, a
|
|
65
84
|
customer search).
|
|
66
|
-
2. Pass its id back
|
|
85
|
+
2. Pass its id back in the type the write's schema declares, usually the integer the read returned.
|
|
67
86
|
3. Handle the action-specific refusal the description names: for example `422 unknown_author` when
|
|
68
87
|
the user is not one the system lists, or `422 no_open_case` when the customer has no open case and
|
|
69
88
|
the action does not open one. Both mean nothing was written; fix the reference, do not retry
|
|
@@ -73,13 +92,23 @@ Never invent an id, copy one from another device, or hard-code one from a test r
|
|
|
73
92
|
|
|
74
93
|
## Easy to get wrong
|
|
75
94
|
|
|
76
|
-
- **
|
|
77
|
-
|
|
78
|
-
`customerId: 1001
|
|
79
|
-
|
|
95
|
+
- **Send each input as the type its schema declares.** An id the schema declares as an integer goes
|
|
96
|
+
in as a JSON number (`"customerId": 1001`), one it declares as text goes in as text
|
|
97
|
+
(`"customerId": "1001"`: digits only, no leading zero, at most 10 characters). A value of the
|
|
98
|
+
wrong type is refused with `400 invalid_request`, never converted. Result field types also come
|
|
99
|
+
from the action's own output schema.
|
|
80
100
|
- **Mark every test text `TEST-DEEPDOCK-<id>`** on the demo device only, and list what you created
|
|
81
101
|
([testing.md](testing.md)).
|
|
82
|
-
- A
|
|
102
|
+
- A call with a query string, or with a method other than the action's own, is refused (`400` and `405`), so a
|
|
103
|
+
client library that puts parameters in the URL for you needs its body mode.
|
|
104
|
+
|
|
105
|
+
## Refusals specific to the call shape
|
|
106
|
+
|
|
107
|
+
| Answer | Cause | Fix |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `405 invalid_request`, `Allow: POST` or `Allow: PATCH` | a method other than the action's own (for example `POST` to an update, or `PATCH` to a read or create) | use the method `Allow` names: `POST` for a read or create, `PATCH` for an update |
|
|
110
|
+
| `400 invalid_request` | a query string on an action path | move every value into the JSON body |
|
|
111
|
+
| `400 invalid_request` | an input of the wrong type, such as `"4711"` where an integer is declared | send the type the schema declares |
|
|
83
112
|
|
|
84
113
|
## Timeouts and latency
|
|
85
114
|
|
|
@@ -110,7 +139,7 @@ attempts per key per rolling 60 seconds, whose `429` carries neither the header
|
|
|
110
139
|
| Header | When | Meaning |
|
|
111
140
|
|---|---|---|
|
|
112
141
|
| `Idempotent-Replayed: true` | a write's `200` | the stored result of an earlier run; nothing written again |
|
|
113
|
-
| `Allow` | `405 invalid_request` | the method
|
|
142
|
+
| `Allow` | `405 invalid_request` | the action's one method: `POST` for a read or create, `PATCH` for an update |
|
|
114
143
|
| `WWW-Authenticate: Bearer` | `401` | the key was refused |
|
|
115
144
|
| `Retry-After` | `429` | seconds to wait, when present |
|
|
116
145
|
|
package/skill/references/cli.md
CHANGED
|
@@ -24,7 +24,7 @@ the API key the CLI wrote to the app's env file.
|
|
|
24
24
|
| `devices rename <id> <label>` | rename a device | member |
|
|
25
25
|
| `devices release <id>` | release a device (asks on a terminal; `--yes` otherwise) | admin |
|
|
26
26
|
| `devices systems <id>` | the device's live systems and action state, paused reasons included; `device_offline` is a normal answer | member |
|
|
27
|
-
| `actions list <device> <system>` | the action index: every action with its
|
|
27
|
+
| `actions list <device> <system>` | the action index: every action with its operation and field schemas | member |
|
|
28
28
|
| `keys list` | the organisation's API keys: name, prefix, created, last used, expires, status | member |
|
|
29
29
|
| `keys create <name> --expires <7d\|30d\|90d> --write-env <file>` | create an API key: opens a link, a human approves "Create an API key in <Org>?", the CLI receives the key once and writes `DEEPDOCK_API_KEY=<key>` into the file. It keeps no copy and shows the value to nobody. Answers id, name, prefix, expiry and where it was written | admin |
|
|
30
30
|
| `keys revoke <id>` | revoke a key (asks on a terminal; `--yes` otherwise); removes the env entry if this CLI wrote it | admin |
|
|
Binary file
|
|
Binary file
|
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
It does the four things the OpenAPI document cannot tell you: the bearer header, the envelope
|
|
4
4
|
unwrap, the Idempotency-Key header with replay detection, and a typed error carrying the code.
|
|
5
|
-
Everything action-specific (
|
|
5
|
+
Everything action-specific (fields, types, patterns) comes from the live OpenAPI document.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
11
|
from dataclasses import dataclass, field, replace
|
|
12
|
-
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
|
12
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
|
13
13
|
from urllib.parse import quote
|
|
14
14
|
|
|
15
15
|
import httpx
|
|
16
16
|
|
|
17
|
+
# A flat JSON value as the action's schema declares it: a number is a number, never its text.
|
|
18
|
+
InputValue = Union[str, int, float, bool]
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class DeepdockError(Exception):
|
|
19
22
|
"""Deepdock answered `{ "error": { "code", ... } }`. Switch on `code`, never on `message`."""
|
|
@@ -79,18 +82,19 @@ class Deepdock:
|
|
|
79
82
|
def _action_path(device_id: str, system: str, action: str) -> str:
|
|
80
83
|
return f"/v1/devices/{quote(device_id, safe='')}/systems/{quote(system, safe='')}/actions/{quote(action, safe='')}"
|
|
81
84
|
|
|
82
|
-
def read(self, device_id: str, system: str, action: str, params: Dict[str,
|
|
85
|
+
def read(self, device_id: str, system: str, action: str, params: Dict[str, InputValue],
|
|
83
86
|
limit: Optional[int] = None, cursor: Optional[str] = None) -> Page:
|
|
84
|
-
"""Runs a read action
|
|
85
|
-
|
|
87
|
+
"""Runs a read action: a POST whose JSON body holds the inputs and the paging fields, no
|
|
88
|
+
Idempotency-Key. Pass `next_cursor` back as `cursor` for the next page."""
|
|
89
|
+
body: Dict[str, InputValue] = dict(params)
|
|
86
90
|
if limit is not None:
|
|
87
|
-
|
|
91
|
+
body["limit"] = limit
|
|
88
92
|
if cursor is not None:
|
|
89
|
-
|
|
90
|
-
data, _ = self._send("
|
|
93
|
+
body["cursor"] = cursor
|
|
94
|
+
data, _ = self._send("POST", self._action_path(device_id, system, action), json=body)
|
|
91
95
|
return Page(items=data["items"], next_cursor=data.get("nextCursor"))
|
|
92
96
|
|
|
93
|
-
def read_all(self, device_id: str, system: str, action: str, params: Dict[str,
|
|
97
|
+
def read_all(self, device_id: str, system: str, action: str, params: Dict[str, InputValue],
|
|
94
98
|
limit: Optional[int] = None) -> Iterator[Dict[str, Any]]:
|
|
95
99
|
"""Reads every page of a read action, in order."""
|
|
96
100
|
cursor: Optional[str] = None
|
|
@@ -101,10 +105,15 @@ class Deepdock:
|
|
|
101
105
|
return
|
|
102
106
|
cursor = page.next_cursor
|
|
103
107
|
|
|
104
|
-
def write(self, device_id: str, system: str, action: str,
|
|
105
|
-
body: Dict[str,
|
|
106
|
-
"""Runs a write action
|
|
107
|
-
|
|
108
|
+
def write(self, device_id: str, system: str, action: str, operation: str, idempotency_key: str,
|
|
109
|
+
body: Dict[str, InputValue]) -> WriteResult:
|
|
110
|
+
"""Runs a write action: POST for operation "create", PATCH for "update" (from the action index; any
|
|
111
|
+
other method answers 405 with `Allow` naming the right one). `body` values are typed as the
|
|
112
|
+
action's schema declares. Any other `operation` raises ValueError before anything is sent."""
|
|
113
|
+
if operation not in ("create", "update"):
|
|
114
|
+
raise ValueError(f"operation must be 'create' or 'update', got {operation!r}")
|
|
115
|
+
data, headers = self._send("PATCH" if operation == "update" else "POST",
|
|
116
|
+
self._action_path(device_id, system, action), json=body,
|
|
108
117
|
headers={"Idempotency-Key": idempotency_key})
|
|
109
118
|
return WriteResult(data=data, replayed=headers.get("Idempotent-Replayed") == "true")
|
|
110
119
|
|
|
@@ -146,8 +155,8 @@ class PendingWrite:
|
|
|
146
155
|
device_id: str
|
|
147
156
|
system: str
|
|
148
157
|
action: str
|
|
149
|
-
|
|
150
|
-
input: Dict[str,
|
|
158
|
+
operation: str # create | update, from the action index
|
|
159
|
+
input: Dict[str, InputValue]
|
|
151
160
|
status: str = "pending" # pending | sent | done | unknown | refused
|
|
152
161
|
conflict_retries: int = 0
|
|
153
162
|
result: Optional[Dict[str, Any]] = field(default=None)
|
|
@@ -158,7 +167,7 @@ class PendingWrite:
|
|
|
158
167
|
def attempt(dd: Deepdock, row: PendingWrite) -> PendingWrite:
|
|
159
168
|
"""One attempt; returns the row with its new status. The caller persists it."""
|
|
160
169
|
try:
|
|
161
|
-
res = dd.write(row.device_id, row.system, row.action, row.
|
|
170
|
+
res = dd.write(row.device_id, row.system, row.action, row.operation, row.key, row.input)
|
|
162
171
|
return replace(row, status="done", result=res.data)
|
|
163
172
|
except DeepdockTransportError:
|
|
164
173
|
return replace(row, status="sent")
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* It does the four things the OpenAPI document cannot tell you: the bearer header, the envelope
|
|
5
5
|
* unwrap, the Idempotency-Key header with replay detection, and a typed error carrying the code.
|
|
6
|
-
* Everything action-specific (
|
|
6
|
+
* Everything action-specific (fields, types, patterns) comes from the live OpenAPI document.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Plain fields, no constructor parameter properties: Node runs this file with its default type
|
|
@@ -55,7 +55,11 @@ type Envelope<T> = { data: T } | { error: ErrorBody }
|
|
|
55
55
|
|
|
56
56
|
export type Page<Item> = { items: Item[]; nextCursor: string | null }
|
|
57
57
|
export type WriteResult<T> = { data: T; replayed: boolean }
|
|
58
|
-
|
|
58
|
+
/** A flat JSON value as the action's schema declares it: a number is a number, never its text. */
|
|
59
|
+
export type InputValue = string | number | boolean
|
|
60
|
+
/** What a write action does, from `operation` in the action index: a create is POST, an update PATCH. */
|
|
61
|
+
export type WriteOperation = 'create' | 'update'
|
|
62
|
+
export type ActionInput = Record<string, InputValue>
|
|
59
63
|
|
|
60
64
|
export type SystemStatus = 'running' | 'stopped' | 'missing' | 'unknown'
|
|
61
65
|
/** The database access the device last reported; `null` while none is known. */
|
|
@@ -187,19 +191,27 @@ export class Deepdock {
|
|
|
187
191
|
return `/v1/devices/${encodeURIComponent(deviceId)}/systems/${encodeURIComponent(system)}/actions/${encodeURIComponent(action)}`
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* Runs a read action: a POST whose JSON body holds the inputs and the paging fields, no
|
|
196
|
+
* Idempotency-Key. Pass `nextCursor` back as `cursor` for the next page.
|
|
197
|
+
*/
|
|
191
198
|
async read<Item>(
|
|
192
199
|
deviceId: string,
|
|
193
200
|
system: string,
|
|
194
201
|
action: string,
|
|
195
|
-
input:
|
|
202
|
+
input: ActionInput,
|
|
196
203
|
page: { limit?: number; cursor?: string } = {},
|
|
197
204
|
): Promise<Page<Item>> {
|
|
198
|
-
const
|
|
199
|
-
if (page.limit !== undefined)
|
|
200
|
-
if (page.cursor !== undefined)
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
const body: ActionInput = { ...input }
|
|
206
|
+
if (page.limit !== undefined) body.limit = page.limit
|
|
207
|
+
if (page.cursor !== undefined) body.cursor = page.cursor
|
|
208
|
+
return (
|
|
209
|
+
await this.send<Page<Item>>(this.actionPath(deviceId, system, action), {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
body: JSON.stringify(body),
|
|
213
|
+
})
|
|
214
|
+
).data
|
|
203
215
|
}
|
|
204
216
|
|
|
205
217
|
/** Reads every page of a read action, in order. */
|
|
@@ -207,7 +219,7 @@ export class Deepdock {
|
|
|
207
219
|
deviceId: string,
|
|
208
220
|
system: string,
|
|
209
221
|
action: string,
|
|
210
|
-
input:
|
|
222
|
+
input: ActionInput,
|
|
211
223
|
limit?: number,
|
|
212
224
|
): AsyncGenerator<Item> {
|
|
213
225
|
let cursor: string | undefined
|
|
@@ -219,19 +231,21 @@ export class Deepdock {
|
|
|
219
231
|
}
|
|
220
232
|
|
|
221
233
|
/**
|
|
222
|
-
* Runs a write action
|
|
223
|
-
*
|
|
234
|
+
* Runs a write action: POST for a `create`, PATCH for an `update`. Take `operation` from the
|
|
235
|
+
* action index; any other method answers 405 with `Allow` naming the right one. `input` values
|
|
236
|
+
* are typed as the action's schema declares. `idempotencyKey` is new per intended write and the
|
|
237
|
+
* same on every retry.
|
|
224
238
|
*/
|
|
225
239
|
async write<Result>(
|
|
226
240
|
deviceId: string,
|
|
227
241
|
system: string,
|
|
228
242
|
action: string,
|
|
229
|
-
|
|
243
|
+
operation: WriteOperation,
|
|
230
244
|
idempotencyKey: string,
|
|
231
|
-
input:
|
|
245
|
+
input: ActionInput,
|
|
232
246
|
): Promise<WriteResult<Result>> {
|
|
233
247
|
const { data, headers } = await this.send<Result>(this.actionPath(deviceId, system, action), {
|
|
234
|
-
method,
|
|
248
|
+
method: operation === 'update' ? 'PATCH' : 'POST',
|
|
235
249
|
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
|
|
236
250
|
body: JSON.stringify(input),
|
|
237
251
|
})
|
|
@@ -276,8 +290,8 @@ export type PendingWrite = {
|
|
|
276
290
|
deviceId: string
|
|
277
291
|
system: string
|
|
278
292
|
action: string
|
|
279
|
-
|
|
280
|
-
input:
|
|
293
|
+
operation: WriteOperation
|
|
294
|
+
input: ActionInput
|
|
281
295
|
status: PendingStatus
|
|
282
296
|
conflictRetries: number
|
|
283
297
|
result?: unknown
|
|
@@ -303,7 +317,7 @@ const RETRY_SAME_KEY = new Set([
|
|
|
303
317
|
/** One attempt; returns the row with its new status. The caller persists it. */
|
|
304
318
|
export const attempt = async (dd: Deepdock, row: PendingWrite): Promise<PendingWrite> => {
|
|
305
319
|
try {
|
|
306
|
-
const { data } = await dd.write(row.deviceId, row.system, row.action, row.
|
|
320
|
+
const { data } = await dd.write(row.deviceId, row.system, row.action, row.operation, row.key, row.input)
|
|
307
321
|
return { ...row, status: 'done', result: data }
|
|
308
322
|
} catch (err) {
|
|
309
323
|
if (err instanceof DeepdockTransportError) return { ...row, status: 'sent' }
|
|
@@ -72,9 +72,11 @@ offering it; it then runs the same way every time. Actions are
|
|
|
72
72
|
**assigned** to your organisation by Deepdock, so the set you can call is yours, not the whole
|
|
73
73
|
catalogue.
|
|
74
74
|
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
- Every action takes a flat JSON body; its `operation` says what it does: `POST` for a read or a
|
|
76
|
+
create, `PATCH` for an update.
|
|
77
|
+
- A **read** also pages with `limit` and `cursor` in the body.
|
|
78
|
+
- A **write** is a `create` (answers `201`) or an `update` (answers `200`), and needs an
|
|
79
|
+
`Idempotency-Key` header.
|
|
78
80
|
- A write **either fully applies or not**: Deepdock checks the result before confirming, and if the check fails it saves nothing
|
|
79
81
|
and pauses the action (`409 readback_failed`).
|
|
80
82
|
- An action is **active** or **paused**. `GET /v1/devices/{deviceId}/systems` shows each action's
|
|
@@ -7,7 +7,7 @@ Never hard-code an action's fields. Three sources, each authoritative for one th
|
|
|
7
7
|
| Which systems and actions does my organisation have? | `deepdock systems list --json` | CLI sign-in |
|
|
8
8
|
| What does this device run, and is each action active or paused? | `GET /v1/devices/{deviceId}/systems` | organisation |
|
|
9
9
|
| Which actions can I call on this device's system, with which schemas? | `GET /v1/devices/{deviceId}/systems/{system}/actions` | organisation |
|
|
10
|
-
| The exact paths,
|
|
10
|
+
| The exact paths, operations, fields, patterns, enums, lengths, responses | `deepdock systems openapi <system>` (CLI sign-in) or `GET /v1/systems/{system}/openapi.json` (API key); per device `deepdock actions list <device> <system>` (CLI sign-in) | CLI sign-in or API key |
|
|
11
11
|
|
|
12
12
|
## The OpenAPI document
|
|
13
13
|
|
|
@@ -25,20 +25,19 @@ document into the repository and refresh it with the same command when you touch
|
|
|
25
25
|
What to read off it:
|
|
26
26
|
|
|
27
27
|
- **`paths`**: one operation per action at
|
|
28
|
-
`/v1/devices/{deviceId}/systems/{system}/actions/{action}`;
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
`/v1/devices/{deviceId}/systems/{system}/actions/{action}`; each is a `post` (read, create) or a `patch` (update);
|
|
29
|
+
its `operation` says which. Any other method answers `405 invalid_request` with `Allow` naming
|
|
30
|
+
the right one.
|
|
31
31
|
- **`summary` and `description`**: what the action does and what it changes in the system, what
|
|
32
32
|
an omitted optional input becomes, action-specific refusals (`code` and when), and what the
|
|
33
33
|
system only warns about and what the action does then. Read the description before mapping your
|
|
34
34
|
fields.
|
|
35
|
-
- **
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
property `type: string`, `required` lists the mandatory ones. The `Idempotency-Key` header
|
|
35
|
+
- **Inputs** are the `requestBody` object schema of every action, read or write:
|
|
36
|
+
`additionalProperties: false`, each property with its declared `type`, and for text `maxLength`
|
|
37
|
+
and possibly `minLength`, `pattern`, `enum` and a `description` such as "prefix match: case, punctuation and
|
|
38
|
+
umlaut spelling are ignored". A read's body also takes `limit` (integer, 1 to 250, default 25) and
|
|
39
|
+
`cursor` (opaque). An optional input is not in `required` and its description says what an
|
|
40
|
+
omitted value becomes. A write's `Idempotency-Key` header
|
|
42
41
|
parameter carries its own pattern (8 to 128 characters of `A-Z a-z 0-9 _ -`).
|
|
43
42
|
- **Responses**: a read's `200` is `{ data: { items: [...], nextCursor } }`; a create's `201` and a
|
|
44
43
|
replayed `200`; an update's `200`. Item and result fields are typed `string`, `integer` or `number`,
|
|
@@ -57,7 +56,7 @@ curl "$BASE/v1/devices/$DEVICE/systems/example-erp/actions" \
|
|
|
57
56
|
-H "Authorization: Bearer $DEEPDOCK_API_KEY"
|
|
58
57
|
```
|
|
59
58
|
|
|
60
|
-
Answers `{ data: [ { name, method, description, input, output } ] }`, filtered to your assigned
|
|
59
|
+
Answers `{ data: [ { name, operation, method, description, input, output } ] }`, filtered to your assigned
|
|
61
60
|
actions, with the same JSON Schemas the OpenAPI document carries. Use it at runtime to confirm an
|
|
62
61
|
action exists before offering it in your UI, or to validate input client-side before sending.
|
|
63
62
|
|
|
@@ -17,7 +17,7 @@ with the **same** `Idempotency-Key`); **Show** says what the end user should see
|
|
|
17
17
|
|
|
18
18
|
| Code | Status | Meaning | Retry | Show |
|
|
19
19
|
|---|---|---|---|---|
|
|
20
|
-
| `invalid_request` | 400 (405 for a
|
|
20
|
+
| `invalid_request` | 400 (405 for a method other than the action's own) | malformed request: bad `limit` or `cursor` in a read's body; missing or malformed `Idempotency-Key`; a body that is not an object; an action input that is unknown, missing, too short or long, out of bounds or not an allowed value (`fields` names each); a malformed pairing code or device id; a query string on an action path (inputs go in the JSON body), an input of the wrong type, such as `"4711"` where an integer is declared, an action called with a method other than its own (`405`, `Allow` names the one: `POST` for a read or create, `PATCH` for an update) | no; fix the code | nothing; a developer bug |
|
|
21
21
|
| `payload_too_large` | 413 | the request body is over 1 MiB | no; send less | nothing; a developer bug, or shorten the text |
|
|
22
22
|
| `unauthenticated` | 401 | the key is missing, malformed, for the other environment, unknown, revoked or expired | no | "Deepdock API key refused; check it in the dashboard" to an operator |
|
|
23
23
|
| `partner_not_approved` | 403 | your organisation is waiting for approval by Deepdock; no key of it works yet. Keys made before the approval start working once it is given | no, until Deepdock approves; then yes | "our Deepdock account is waiting for approval; contact Deepdock" |
|
|
@@ -23,7 +23,7 @@ import { Deepdock, DeepdockError, DeepdockTransportError, attempt, type PendingW
|
|
|
23
23
|
const dd = new Deepdock(process.env.DEEPDOCK_API_KEY!) // base from DEEPDOCK_BASE_URL, else production; 30 s timeout
|
|
24
24
|
const page = await dd.read<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' }, { limit: 50 })
|
|
25
25
|
for await (const p of dd.readAll<Note>(deviceId, 'example-erp', 'searchNotes', { text: 'TEST-DEEPDOCK-' })) { /* every page */ }
|
|
26
|
-
const { data, replayed } = await dd.write<{ noteId: number }>(deviceId, 'example-erp', 'createNote', '
|
|
26
|
+
const { data, replayed } = await dd.write<{ noteId: number }>(deviceId, 'example-erp', 'createNote', 'create', key, input)
|
|
27
27
|
const systems = await dd.systems(deviceId) // live; can throw device_offline
|
|
28
28
|
const device = await dd.pair('K7Q-4MX', 'Demo') // keep device.id
|
|
29
29
|
```
|
|
@@ -33,7 +33,7 @@ from deepdock import Deepdock, DeepdockError, DeepdockTransportError, PendingWri
|
|
|
33
33
|
|
|
34
34
|
dd = Deepdock(os.environ["DEEPDOCK_API_KEY"])
|
|
35
35
|
page = dd.read(device_id, "example-erp", "searchNotes", {"text": "TEST-DEEPDOCK-"}, limit=50)
|
|
36
|
-
res = dd.write(device_id, "example-erp", "createNote", "
|
|
36
|
+
res = dd.write(device_id, "example-erp", "createNote", "create", key, body) # res.data, res.replayed
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Errors: `DeepdockError` has `status`, `code`, `requestId`, `systems`; switch on `code`
|
|
@@ -42,7 +42,7 @@ Errors: `DeepdockError` has `status`, `code`, `requestId`, `systems`; switch on
|
|
|
42
42
|
|
|
43
43
|
## Sending a write: persist first, then one attempt at a time
|
|
44
44
|
|
|
45
|
-
Persist the pending write (key, device, action,
|
|
45
|
+
Persist the pending write (key, device, action, operation, input, status) **before** the first send,
|
|
46
46
|
so a crash between send and answer cannot produce a second record under a new key. `attempt()`
|
|
47
47
|
moves the row to `done`, `sent` (retry later, same key), `unknown` (read back before anything
|
|
48
48
|
else) or `refused` (nothing was written; a changed input gets a new key). A job retries `sent`
|
|
@@ -51,7 +51,7 @@ rows with backoff: seconds, then minutes, for hours if the device stays offline.
|
|
|
51
51
|
```ts
|
|
52
52
|
const row: PendingWrite = {
|
|
53
53
|
key: `order-${order.id}-create-note`, // 8 to 128 chars of A-Z a-z 0-9 _ -, stable per business event
|
|
54
|
-
deviceId, system: 'example-erp', action: 'createNote',
|
|
54
|
+
deviceId, system: 'example-erp', action: 'createNote', operation: 'create',
|
|
55
55
|
input, status: 'pending', conflictRetries: 0,
|
|
56
56
|
}
|
|
57
57
|
await store.save(row) // before the first send
|
|
@@ -74,7 +74,7 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
|
74
74
|
export const run = async (store: { save: (row: PendingWrite) => Promise<void> }) => {
|
|
75
75
|
const marker = `TEST-DEEPDOCK-${randomUUID().replaceAll('-', '').slice(0, 6).toUpperCase()}`
|
|
76
76
|
let row: PendingWrite = {
|
|
77
|
-
key: `demo-${marker}-create`, deviceId, system, action: 'createNote',
|
|
77
|
+
key: `demo-${marker}-create`, deviceId, system, action: 'createNote', operation: 'create',
|
|
78
78
|
input: { text: marker, orderNumber: '1001' },
|
|
79
79
|
status: 'pending', conflictRetries: 0,
|
|
80
80
|
}
|
|
@@ -122,8 +122,9 @@ curl -sS "$BASE/v1/devices/$DEVICE/systems" -H "$AUTH"
|
|
|
122
122
|
# The action index (your assigned actions, with schemas)
|
|
123
123
|
curl -sS "$BASE/v1/devices/$DEVICE/systems/example-erp/actions" -H "$AUTH"
|
|
124
124
|
|
|
125
|
-
# A read, paged: pass data.nextCursor back as cursor
|
|
126
|
-
curl -sS "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchNotes
|
|
125
|
+
# A read is a POST too, with its inputs and limit/cursor in the body; paged: pass data.nextCursor back as cursor
|
|
126
|
+
curl -sS -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/searchNotes" \
|
|
127
|
+
-H "$AUTH" -H 'Content-Type: application/json' -d '{"text":"TEST-DEEPDOCK-","limit":50}'
|
|
127
128
|
|
|
128
129
|
# A create: new Idempotency-Key per intended write, the same on every retry; 201, or 200 with
|
|
129
130
|
# Idempotent-Replayed: true on a replay (-i shows the headers)
|
|
@@ -134,7 +135,7 @@ curl -sS -i -X POST "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/create
|
|
|
134
135
|
# An update: PATCH, same rules
|
|
135
136
|
curl -sS -i -X PATCH "$BASE/v1/devices/$DEVICE/systems/example-erp/actions/updateNote" \
|
|
136
137
|
-H "$AUTH" -H "Idempotency-Key: demo-$(date +%s)-update" -H 'Content-Type: application/json' \
|
|
137
|
-
-d '{"noteId":
|
|
138
|
+
-d '{"noteId":1001,"text":"TEST-DEEPDOCK-K3M9QA edited"}'
|
|
138
139
|
|
|
139
140
|
# Release a device
|
|
140
141
|
curl -sS -X DELETE "$BASE/v1/devices/$DEVICE" -H "$AUTH"
|
|
@@ -142,7 +143,7 @@ curl -sS -X DELETE "$BASE/v1/devices/$DEVICE" -H "$AUTH"
|
|
|
142
143
|
|
|
143
144
|
A port in another language keeps the clients' shape: one `send` that sets the bearer header,
|
|
144
145
|
parses the envelope, throws a typed error with `code` on `{ error }` and a transport error on no
|
|
145
|
-
answer or non-JSON; `read` with `limit` and `cursor
|
|
146
|
+
answer or non-JSON; `read` as a POST with `limit` and `cursor` in the body; `write` (POST for a create, PATCH for an update) with `Idempotency-Key` and the
|
|
146
147
|
`Idempotent-Replayed` header; `attempt` over a persisted pending write.
|
|
147
148
|
|
|
148
149
|
## Mapping the user's intent to a call (checklist)
|
|
@@ -150,9 +151,9 @@ answer or non-JSON; `read` with `limit` and `cursor`; `write` with `Idempotency-
|
|
|
150
151
|
1. Which device? From your product's configuration for that customer site; never from the
|
|
151
152
|
user's free text.
|
|
152
153
|
2. Is the action active? `systems()` once per session; show paused features as paused.
|
|
153
|
-
3. Which action and
|
|
154
|
+
3. Which action and operation (create is POST, update is PATCH)? From the stored index for that system.
|
|
154
155
|
4. Does the write need a reference (author user, open case, record id)? Read it first.
|
|
155
|
-
5. Build the input
|
|
156
|
+
5. Build the input in the types the schema declares; validate against the schema (`pattern`, `minLength`, `maxLength`,
|
|
156
157
|
`enum`) before sending so the user sees the rule where they typed.
|
|
157
158
|
6. Choose the idempotency key from your business event and persist it with the pending write.
|
|
158
159
|
7. Send; map the code with the reaction table; show the user the right sentence.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Idempotency for writes
|
|
2
2
|
|
|
3
|
-
Every `
|
|
4
|
-
refused with `400 invalid_request`.
|
|
3
|
+
Every write (a `create` action, called with `POST`, or an `update`, called with `PATCH`) carries an `Idempotency-Key` header. Without it the request is
|
|
4
|
+
refused with `400 invalid_request`. A read ignores the header.
|
|
5
5
|
|
|
6
6
|
## The key
|
|
7
7
|
|