Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copy to .env and fill in. Used only by the integration suite (__tests__/index.test.ts),
# which is skipped unless `url` and `odx_api_key` are set. .env is git-ignored.

# --- Odoo instance (carried inside each request body) ---
url=https://your-odoo.example.com # Base URL of your Odoo server
db=your-db # Odoo database name
uid=2 # Odoo user id (alias: user_id)
api_key=your-odoo-user-api-key # API key of that Odoo user

# --- ODXProxy gateway ---
odx_api_key=your-odxproxy-gateway-api-key # Proxy x-api-key

# Optional: only needed for a self-hosted proxy. Defaults to https://gateway.odxproxy.io.
# gateway_url=https://your-proxy.example.com
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ pnpm-debug.log*
*.swo

# junit
junit.xml
junit.xml

SYSTEM_ARCHITECTURE.md
54 changes: 54 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`@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.

`SYSTEM_ARCHITECTURE.md` is the **wire-protocol source of truth** for the gateway — consult it before changing request/response shapes, actions, or error handling.

## Commands

```bash
npm run build # tsup → dist/ (ESM index.mjs, CJS index.js, minified, + .d.ts)
npm test # jest in CI mode, writes junit.xml via jest-junit
npm run test:watch # jest --watch
npx jest unit.test.ts # hermetic unit tests only (no creds needed)
npx jest -t "error model" # run tests by name
```

`prepublishOnly` runs build + test, so a green `npm test` is required before publishing.

## Architecture

Two source files, with a deliberate split:

- **`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).

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.

- **`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.

### Error model (matches SYSTEM_ARCHITECTURE §6)

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`.

### Conventions that are easy to get wrong

- **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`.
- **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.
- **`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).
- **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 })`.
- **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.

## Testing

- **`__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.
- **`__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).

## Build / publishing notes

- 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`.
- **Zero runtime dependencies** and `"sideEffects": false` for clean browser tree-shaking. Runtime floor is **Node 18+** (`engines`), since global `fetch` is required.
- **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).
96 changes: 67 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Official JavaScript/TypeScript client for ODXProxy. This SDK provides a simple,

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

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

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

## Installation

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

Expand All @@ -105,30 +110,36 @@ Context (passed inside `keyword`) supports:
"allowed_company_ids": [1]
},
"fields": ["name", "email"],
"sort": "name asc",
"order": "name asc",
"limit": 10,
"offset": 0
}
```

Note: For actions that do not use fields/sort/limit/offset, the client will strip those keys before sending.
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).

Every helper also accepts an optional trailing `opts` argument for per-call control:

```ts
await search_read("res.partner", domain, keyword, /* id */ undefined, {
timeoutSecs: 60, // overrides default_timeout_secs for this call (x-request-timeout)
signal: ac.signal, // AbortController signal for cancellation
});
```

## API Reference
All functions return a promise resolving to:
On success, data functions resolve to the JSON-RPC envelope (failures are **thrown** as `OdxError` — see Error Handling):

```
{
jsonrpc: string; // typically "2.0"
id: string; // request id (ULID by default)
result?: any; // present on success
error?: { // present on error
code: number;
message: string;
data: any;
};
id: string; // request id (UUID by default)
result?: any; // the Odoo result
}
```

Each data function also accepts an optional trailing `opts?: OdxRequestOptions` ({ timeoutSecs?, signal? }) after `id`.

- init(options)
- Initializes the singleton client. Must be called before any other function.

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

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

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

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

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

## Error Handling
The client normalizes errors from Axios into a consistent structure. Timeouts (default 45s) surface as:
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`:

```
{ code: 408, message: "Request Timeout: exceeded client limit of 45000ms", data: null }
```ts
import { search_read, AuthError, OdooLogicError, OdxError } from "@terrakernel/odxproxy-client-js";

try {
const res = await search_read("res.partner", [[]], { context: { tz: "UTC" } });
console.log(res.result);
} catch (err) {
if (err instanceof AuthError) { /* bad x-api-key */ }
else if (err instanceof OdooLogicError) { /* Odoo validation/access error */ }
else if (err instanceof OdxError) { console.error(err.code, err.message, err.data, err.httpStatus); }
}
```

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

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

Wrap calls in try/catch if you prefer exceptions, or check for `error` on the returned object if you treat it as a value.
## Auxiliary endpoints

```ts
import { version, about, license, metrics } from "@terrakernel/odxproxy-client-js";

await version("https://your-odoo.example.com"); // Odoo version banner (no Odoo creds needed)
await about(); // { jsonrpc, id, result: { build, version } }
await license(); // { licensee, valid_until, is_valid } — flat object, not an envelope
await metrics(); // Prometheus metrics as a string
```

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

## Testing
This repo uses Jest. You can run tests locally (requires valid environment variables to hit a real ODXProxy/Odoo instance):
This repo uses Jest with two suites:

- **Unit tests** (`__tests__/unit.test.ts`) mock `fetch` and need no credentials — run them anytime.
- **Integration tests** (`__tests__/index.test.ts`) hit a real ODXProxy/Odoo instance and are **skipped unless `url` and `odx_api_key` are set**.

```
npm test
npm test # both suites (integration skipped without creds)
npx jest unit.test.ts # unit tests only
```

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

You can define them in a local .env and load with dotenv in your test runner setup if desired.

## Build
Build the package (generates ESM, CJS, and type definitions):

Expand Down
Loading
Loading