You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+
## What this is
6
+
7
+
`@terrakernel/odxproxy-client-js` is the official JS/TS client SDK for ODXProxy — a gateway that exposes Odoo JSON-RPC over HTTPS with API-key auth. The SDK is a thin, typed, **dependency-free** wrapper that turns helper calls into a single POST to the gateway's `/api/odoo/execute` endpoint (plus a few auxiliary endpoints). There is no UI and no server here; this package is published to npm and runs both in Node (18+) and the browser.
8
+
9
+
`SYSTEM_ARCHITECTURE.md` is the **wire-protocol source of truth** for the gateway — consult it before changing request/response shapes, actions, or error handling.
npm test# jest in CI mode, writes junit.xml via jest-junit
16
+
npm run test:watch # jest --watch
17
+
npx jest unit.test.ts # hermetic unit tests only (no creds needed)
18
+
npx jest -t "error model"# run tests by name
19
+
```
20
+
21
+
`prepublishOnly` runs build + test, so a green `npm test` is required before publishing.
22
+
23
+
## Architecture
24
+
25
+
Two source files, with a deliberate split:
26
+
27
+
-**`src/client.ts`** — the `OdxProxyClient`**singleton**, all interfaces, and the typed error hierarchy. `init(options)` constructs the one instance and **throws if called twice**; `getInstance()`**throws if `init` was never called**. The client holds the gateway URL (default `https://gateway.odxproxy.io`, trailing slash trimmed), the proxy `x-api-key`, and the single bound Odoo instance. **There is intentionally one Odoo instance per process — this is not multi-tenant** (see the project memory; do not add `for_instance`/per-call instance overrides).
28
+
29
+
All network I/O goes through one private `send()` method built on the **global `fetch`** (no axios) with an `AbortController` for timeouts. `send()` reads the body once, then `envelopeOrThrow()` applies the protocol's two-step error rule.
30
+
31
+
-**`src/index.ts`** — the public API: stateless module functions over the singleton. Data helpers (`search`, `search_read`, `read`, `fields_get`, `search_count`, `create`, `write`, `remove`, `call_method`) build an `OdxClientRequest` and call `postRequest`. Auxiliary helpers (`version`, `about`, `license`, `metrics`) map to the gateway's other endpoints. The file also re-exports all public types and error classes.
32
+
33
+
### Error model (matches SYSTEM_ARCHITECTURE §6)
34
+
35
+
Errors are **thrown**, not returned, and are typed. `OdxError` is the base (carries `code`, `message`, `data`, `httpStatus`); subclasses map to the JSON-RPC code catalog: `AuthError` (-32000), `InvalidActionError` (-32001), `MissingFnNameError` (-32002), `OdooTimeoutError` (-32003), `OdooConnectError` (-32004), `InternalProxyError` (-32005), `LicenseError` (0/403), `OdooLogicError` (Odoo's own code on a 200). The `code` is the **JSON-RPC code** (falling back to HTTP status when there's no body error) — *not* the HTTP status. Crucially, a **200 carrying an `error` body is thrown** as `OdooLogicError`, so callers use one `try/catch` for everything. When adding a code, update `makeError()` in `client.ts`.
36
+
37
+
### Conventions that are easy to get wrong
38
+
39
+
-**Action name vs. function name don't always match.**`remove` sends `action: "unlink"`; `call_method` sends `action: "call_method"` plus a separate `fn_name` (and rejects client-side with `MissingFnNameError` if it's empty). `update` is a deprecated alias of `write`.
40
+
-**Keyword stripping.** All helpers *except `search_read`* strip `order`, `limit`, `offset`, `fields` (the `NON_QUERY_KEYS` constant) from the keyword before sending — those only apply to a combined search+read. The sort key is **`order`** (Odoo's `execute_kw` kwarg), not `sort`; the older `sort` naming was a bug and is gone.
41
+
-**`params` shape is positional and Odoo-specific**, varying per action: search domains are triple-nested (`[[ ["field","=",val] ]]`), `read`/`remove` wrap ID lists (`[[1,2]]`), `write` is `[[ids], {fields}]`, `create` is `[{fields}]`. Match the existing JSDoc rather than guessing. `params` is passed through unchanged (not copied).
42
+
-**Per-call options.** Every helper takes an optional trailing `opts: OdxRequestOptions` (`timeoutSecs` → `x-request-timeout` header; `signal` → cancellation). Default upstream timeout can be set once via `init({ ..., default_timeout_secs })`.
43
+
-**Isomorphic gotchas.**`crypto.randomUUID()` is unavailable in non-secure browser contexts, so `newRequestId()` falls back to `getRandomValues`. `accept-encoding` is set but browsers ignore it (forbidden header); it only takes effect on Node/undici.
44
+
45
+
## Testing
46
+
47
+
-**`__tests__/unit.test.ts`** — hermetic; mocks global `fetch`, needs no credentials. This is the suite to run/extend when changing transport, error mapping, header injection, or the auxiliary endpoints.
48
+
-**`__tests__/index.test.ts`** — **live integration** against a real ODXProxy/Odoo instance. Guarded by `describeLive` and **skipped unless `url` and `odx_api_key` env vars are set** (loaded from `.env` by `jest.setup.ts`, using lowercase names: `url`, `db`, `uid`, `api_key`, `odx_api_key`). These assert only `res.jsonrpc === "2.0"` and chain create→write→remove through a shared id, so they are order-dependent and fail if the backend returns any error (the new client throws on those).
49
+
50
+
## Build / publishing notes
51
+
52
+
- TypeScript is `strict`; `tsconfig` uses `lib: ["ES2020", "DOM"]` so `fetch`/`AbortController`/`crypto`/`Response` type without `@types/node`. tsup bundles to ESM + CJS (minified) + `.d.ts`; `package.json``exports` maps `import`→`dist/index.mjs`, `require`→`dist/index.js`.
53
+
-**Zero runtime dependencies** and `"sideEffects": false` for clean browser tree-shaking. Runtime floor is **Node 18+** (`engines`), since global `fetch` is required.
54
+
-**Versioning tracks the proxy** — stay in `0.1.x`; breaking changes are acceptable within that range but don't jump to 0.2/1.0 ahead of the proxy (see project memory).
Note: For actions that do not use fields/sort/limit/offset, the client will strip those keys before sending.
119
+
Note: For actions other than `search_read`, the client strips `fields`/`order`/`limit`/`offset` before sending (they only apply to a combined search+read). The sort key is `order` (Odoo's `execute_kw` keyword).
120
+
121
+
Every helper also accepts an optional trailing `opts` argument for per-call control:
122
+
123
+
```ts
124
+
awaitsearch_read("res.partner", domain, keyword, /* id */undefined, {
125
+
timeoutSecs: 60, // overrides default_timeout_secs for this call (x-request-timeout)
126
+
signal: ac.signal, // AbortController signal for cancellation
127
+
});
128
+
```
115
129
116
130
## API Reference
117
-
All functions return a promise resolving to:
131
+
On success, data functions resolve to the JSON-RPC envelope (failures are **thrown** as `OdxError` — see Error Handling):
118
132
119
133
```
120
134
{
121
135
jsonrpc: string; // typically "2.0"
122
-
id: string; // request id (ULID by default)
123
-
result?: any; // present on success
124
-
error?: { // present on error
125
-
code: number;
126
-
message: string;
127
-
data: any;
128
-
};
136
+
id: string; // request id (UUID by default)
137
+
result?: any; // the Odoo result
129
138
}
130
139
```
131
140
141
+
Each data function also accepts an optional trailing `opts?: OdxRequestOptions` ({ timeoutSecs?, signal? }) after `id`.
142
+
132
143
- init(options)
133
144
- Initializes the singleton client. Must be called before any other function.
134
145
@@ -138,15 +149,15 @@ All functions return a promise resolving to:
@@ -168,19 +179,44 @@ All functions return a promise resolving to:
168
179
- returns: result?: T
169
180
170
181
## Error Handling
171
-
The client normalizes errors from Axios into a consistent structure. Timeouts (default 45s) surface as:
182
+
Every failure is **thrown** as a typed error — proxy-level failures (non-2xx) *and* Odoo logic errors (an `error` body on a `200`). On success the helper resolves to the envelope with `result` set. Use a single `try/catch`:
0 commit comments