Skip to content

Commit 77f02e3

Browse files
authored
Merge pull request #1 from terrakernel/feat/fetch-migration-spec-coverage
Feat/fetch migration spec coverage
2 parents 7222db7 + d7f303a commit 77f02e3

11 files changed

Lines changed: 767 additions & 594 deletions

File tree

.env.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copy to .env and fill in. Used only by the integration suite (__tests__/index.test.ts),
2+
# which is skipped unless `url` and `odx_api_key` are set. .env is git-ignored.
3+
4+
# --- Odoo instance (carried inside each request body) ---
5+
url=https://your-odoo.example.com # Base URL of your Odoo server
6+
db=your-db # Odoo database name
7+
uid=2 # Odoo user id (alias: user_id)
8+
api_key=your-odoo-user-api-key # API key of that Odoo user
9+
10+
# --- ODXProxy gateway ---
11+
odx_api_key=your-odxproxy-gateway-api-key # Proxy x-api-key
12+
13+
# Optional: only needed for a self-hosted proxy. Defaults to https://gateway.odxproxy.io.
14+
# gateway_url=https://your-proxy.example.com

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ pnpm-debug.log*
2424
*.swo
2525

2626
# junit
27-
junit.xml
27+
junit.xml
28+
29+
SYSTEM_ARCHITECTURE.md

CLAUDE.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# CLAUDE.md
2+
3+
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.
10+
11+
## Commands
12+
13+
```bash
14+
npm run build # tsup → dist/ (ESM index.mjs, CJS index.js, minified, + .d.ts)
15+
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).

README.md

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Official JavaScript/TypeScript client for ODXProxy. This SDK provides a simple,
44

55
- Repository package name: `@terrakernel/odxproxy-client-js`
66
- License: MIT
7-
- Runtime: Node.js 18+
7+
- Runtime: Node.js 18+ (uses the global `fetch`) or any modern browser
8+
- Dependencies: none at runtime
89
- Language: TypeScript (bundled to ESM and CJS)
910

1011
## Table of Contents
@@ -36,10 +37,13 @@ ODXProxy is a gateway that securely exposes Odoo RPC functionality over HTTPS wi
3637

3738
## Features
3839
- Friendly, typed wrapper around ODXProxy endpoints
40+
- **Zero runtime dependencies** — built on the platform `fetch`, runs in Node 18+ and the browser
3941
- Supports both ESM and CommonJS
4042
- Works with TypeScript out of the box (bundled type definitions)
4143
- Covers common Odoo actions: search, search_read, read, fields_get, search_count, create, write, unlink (remove), and call_method
42-
- Request IDs are auto-generated with ULID (can be overridden)
44+
- Auxiliary endpoints: version, about, license, metrics
45+
- Typed errors (`OdxError` and subclasses) thrown for every failure
46+
- Request IDs are auto-generated (UUID via `crypto.randomUUID`, can be overridden)
4347

4448
## Installation
4549

@@ -92,6 +96,7 @@ init({
9296
},
9397
odx_api_key: "your-odxproxy-gateway-api-key", // ODXProxy Gateway API key
9498
gateway_url: "https://gateway.odxproxy.io", // Optional. Default shown (trailing slash trimmed)
99+
default_timeout_secs: 15, // Optional. Upstream Odoo timeout (sent as x-request-timeout)
95100
});
96101
```
97102

@@ -105,30 +110,36 @@ Context (passed inside `keyword`) supports:
105110
"allowed_company_ids": [1]
106111
},
107112
"fields": ["name", "email"],
108-
"sort": "name asc",
113+
"order": "name asc",
109114
"limit": 10,
110115
"offset": 0
111116
}
112117
```
113118

114-
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+
await search_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+
```
115129

116130
## 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):
118132

119133
```
120134
{
121135
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
129138
}
130139
```
131140

141+
Each data function also accepts an optional trailing `opts?: OdxRequestOptions` ({ timeoutSecs?, signal? }) after `id`.
142+
132143
- init(options)
133144
- Initializes the singleton client. Must be called before any other function.
134145

@@ -138,15 +149,15 @@ All functions return a promise resolving to:
138149

139150
- search_read<T = any>(model, params, keyword, id?)
140151
- params: domain array
141-
- keyword: can include fields, limit, offset, sort, and context
152+
- keyword: can include fields, limit, offset, order, and context
142153
- returns: result?: T[] (records)
143154

144155
- read<T = any>(model, params, keyword, id?)
145-
- params: array of record IDs wrapped (e.g., [[1,2,3]])
146-
- returns: result?: T
156+
- params: record IDs, with the field list passed positionally (e.g., [[1,2,3], ["name","email"]]); fields in `keyword` are ignored for read
157+
- returns: result?: T (array of records)
147158

148159
- fields_get<T = any>(model, keyword, id?)
149-
- returns: result?: T[] (field metadata)
160+
- returns: result?: T (object keyed by field name → field metadata)
150161

151162
- search_count<T = number>(model, params, keyword, id?)
152163
- returns: result?: T (count)
@@ -168,19 +179,44 @@ All functions return a promise resolving to:
168179
- returns: result?: T
169180

170181
## 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`:
172183

173-
```
174-
{ code: 408, message: "Request Timeout: exceeded client limit of 45000ms", data: null }
184+
```ts
185+
import { search_read, AuthError, OdooLogicError, OdxError } from "@terrakernel/odxproxy-client-js";
186+
187+
try {
188+
const res = await search_read("res.partner", [[]], { context: { tz: "UTC" } });
189+
console.log(res.result);
190+
} catch (err) {
191+
if (err instanceof AuthError) { /* bad x-api-key */ }
192+
else if (err instanceof OdooLogicError) { /* Odoo validation/access error */ }
193+
else if (err instanceof OdxError) { console.error(err.code, err.message, err.data, err.httpStatus); }
194+
}
175195
```
176196

177-
For other HTTP errors, you will receive:
197+
All errors extend `OdxError` and carry the JSON-RPC `code`, `message`, `data`, and the raw `httpStatus`. Subclasses map to the proxy's error catalog:
178198

179-
```
180-
{ code: number, message: string, data: any }
181-
```
199+
| Class | code | When |
200+
|---|---|---|
201+
| `AuthError` | -32000 | Missing/wrong `x-api-key` |
202+
| `InvalidActionError` | -32001 | Action not allowed |
203+
| `MissingFnNameError` | -32002 | `call_method` without `fn_name` (also raised client-side) |
204+
| `OdooTimeoutError` | -32003 | Upstream Odoo timeout (also on client-side abort) |
205+
| `OdooConnectError` | -32004 | Network failure reaching Odoo |
206+
| `InternalProxyError` | -32005 | Internal proxy error |
207+
| `LicenseError` | 0 | Proxy license expired/invalid (HTTP 403) |
208+
| `OdooLogicError` | *Odoo's code* | Odoo-side logic error returned on a 200 |
182209

183-
Wrap calls in try/catch if you prefer exceptions, or check for `error` on the returned object if you treat it as a value.
210+
## Auxiliary endpoints
211+
212+
```ts
213+
import { version, about, license, metrics } from "@terrakernel/odxproxy-client-js";
214+
215+
await version("https://your-odoo.example.com"); // Odoo version banner (no Odoo creds needed)
216+
await about(); // { jsonrpc, id, result: { build, version } }
217+
await license(); // { licensee, valid_until, is_valid } — flat object, not an envelope
218+
await metrics(); // Prometheus metrics as a string
219+
```
184220

185221
## Examples
186222
- Search partners (IDs only):
@@ -217,17 +253,19 @@ await call_method("account.move", [[5]], { context: { tz: "UTC" } }, "action_pos
217253
```
218254

219255
## Testing
220-
This repo uses Jest. You can run tests locally (requires valid environment variables to hit a real ODXProxy/Odoo instance):
256+
This repo uses Jest with two suites:
257+
258+
- **Unit tests** (`__tests__/unit.test.ts`) mock `fetch` and need no credentials — run them anytime.
259+
- **Integration tests** (`__tests__/index.test.ts`) hit a real ODXProxy/Odoo instance and are **skipped unless `url` and `odx_api_key` are set**.
221260

222261
```
223-
npm test
262+
npm test # both suites (integration skipped without creds)
263+
npx jest unit.test.ts # unit tests only
224264
```
225265

226-
Environment variables used in tests:
266+
Environment variables for the integration suite (loaded from `.env` by `jest.setup.ts`):
227267
- url, db, uid, api_key, odx_api_key
228268

229-
You can define them in a local .env and load with dotenv in your test runner setup if desired.
230-
231269
## Build
232270
Build the package (generates ESM, CJS, and type definitions):
233271

0 commit comments

Comments
 (0)