@hodeinavarro/odoo-rpc-ts 0.2.0
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/LICENSE +21 -0
- package/README.md +401 -0
- package/dist/client.d.ts +184 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +277 -0
- package/dist/client.js.map +1 -0
- package/dist/combinators/retryOnSessionExpired.d.ts +13 -0
- package/dist/combinators/retryOnSessionExpired.d.ts.map +1 -0
- package/dist/combinators/retryOnSessionExpired.js +15 -0
- package/dist/combinators/retryOnSessionExpired.js.map +1 -0
- package/dist/commands.d.ts +91 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +69 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +60 -0
- package/dist/config.js.map +1 -0
- package/dist/domain.d.ts +30 -0
- package/dist/domain.d.ts.map +1 -0
- package/dist/domain.js +70 -0
- package/dist/domain.js.map +1 -0
- package/dist/errors/auth.d.ts +27 -0
- package/dist/errors/auth.d.ts.map +1 -0
- package/dist/errors/auth.js +8 -0
- package/dist/errors/auth.js.map +1 -0
- package/dist/errors/index.d.ts +30 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +13 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/mapFault.d.ts +31 -0
- package/dist/errors/mapFault.d.ts.map +1 -0
- package/dist/errors/mapFault.js +48 -0
- package/dist/errors/mapFault.js.map +1 -0
- package/dist/errors/protocol.d.ts +22 -0
- package/dist/errors/protocol.d.ts.map +1 -0
- package/dist/errors/protocol.js +9 -0
- package/dist/errors/protocol.js.map +1 -0
- package/dist/errors/schema.d.ts +22 -0
- package/dist/errors/schema.d.ts.map +1 -0
- package/dist/errors/schema.js +14 -0
- package/dist/errors/schema.js.map +1 -0
- package/dist/errors/server.d.ts +109 -0
- package/dist/errors/server.d.ts.map +1 -0
- package/dist/errors/server.js +49 -0
- package/dist/errors/server.js.map +1 -0
- package/dist/errors/session.d.ts +16 -0
- package/dist/errors/session.d.ts.map +1 -0
- package/dist/errors/session.js +11 -0
- package/dist/errors/session.js.map +1 -0
- package/dist/errors/transport.d.ts +42 -0
- package/dist/errors/transport.d.ts.map +1 -0
- package/dist/errors/transport.js +49 -0
- package/dist/errors/transport.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/platform.d.ts +12 -0
- package/dist/internal/platform.d.ts.map +1 -0
- package/dist/internal/platform.js +11 -0
- package/dist/internal/platform.js.map +1 -0
- package/dist/internal/singleFlight.d.ts +29 -0
- package/dist/internal/singleFlight.d.ts.map +1 -0
- package/dist/internal/singleFlight.js +41 -0
- package/dist/internal/singleFlight.js.map +1 -0
- package/dist/profiles.d.ts +179 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +191 -0
- package/dist/profiles.js.map +1 -0
- package/dist/protocol/jsonrpc.d.ts +127 -0
- package/dist/protocol/jsonrpc.d.ts.map +1 -0
- package/dist/protocol/jsonrpc.js +117 -0
- package/dist/protocol/jsonrpc.js.map +1 -0
- package/dist/records/index.d.ts +14 -0
- package/dist/records/index.d.ts.map +1 -0
- package/dist/records/index.js +14 -0
- package/dist/records/index.js.map +1 -0
- package/dist/records/recordModel.d.ts +131 -0
- package/dist/records/recordModel.d.ts.map +1 -0
- package/dist/records/recordModel.js +101 -0
- package/dist/records/recordModel.js.map +1 -0
- package/dist/records/related.d.ts +42 -0
- package/dist/records/related.d.ts.map +1 -0
- package/dist/records/related.js +51 -0
- package/dist/records/related.js.map +1 -0
- package/dist/records/relations.d.ts +66 -0
- package/dist/records/relations.d.ts.map +1 -0
- package/dist/records/relations.js +119 -0
- package/dist/records/relations.js.map +1 -0
- package/dist/records/spec.d.ts +44 -0
- package/dist/records/spec.d.ts.map +1 -0
- package/dist/records/spec.js +63 -0
- package/dist/records/spec.js.map +1 -0
- package/dist/records/typed.d.ts +71 -0
- package/dist/records/typed.d.ts.map +1 -0
- package/dist/records/typed.js +65 -0
- package/dist/records/typed.js.map +1 -0
- package/dist/rpc.d.ts +90 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +113 -0
- package/dist/rpc.js.map +1 -0
- package/dist/services/db.d.ts +128 -0
- package/dist/services/db.d.ts.map +1 -0
- package/dist/services/db.js +132 -0
- package/dist/services/db.js.map +1 -0
- package/dist/services/report.d.ts +48 -0
- package/dist/services/report.d.ts.map +1 -0
- package/dist/services/report.js +103 -0
- package/dist/services/report.js.map +1 -0
- package/dist/session/cookie.d.ts +114 -0
- package/dist/session/cookie.d.ts.map +1 -0
- package/dist/session/cookie.js +231 -0
- package/dist/session/cookie.js.map +1 -0
- package/dist/testing/fakeTransport.d.ts +40 -0
- package/dist/testing/fakeTransport.d.ts.map +1 -0
- package/dist/testing/fakeTransport.js +36 -0
- package/dist/testing/fakeTransport.js.map +1 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/transport.d.ts +62 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +9 -0
- package/dist/transport.js.map +1 -0
- package/dist/transports/json2.d.ts +41 -0
- package/dist/transports/json2.d.ts.map +1 -0
- package/dist/transports/json2.js +196 -0
- package/dist/transports/json2.js.map +1 -0
- package/dist/transports/jsonrpc.d.ts +41 -0
- package/dist/transports/jsonrpc.d.ts.map +1 -0
- package/dist/transports/jsonrpc.js +119 -0
- package/dist/transports/jsonrpc.js.map +1 -0
- package/dist/transports/web.d.ts +15 -0
- package/dist/transports/web.d.ts.map +1 -0
- package/dist/transports/web.js +62 -0
- package/dist/transports/web.js.map +1 -0
- package/dist/version-live.d.ts +41 -0
- package/dist/version-live.d.ts.map +1 -0
- package/dist/version-live.js +26 -0
- package/dist/version-live.js.map +1 -0
- package/dist/version.d.ts +78 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +46 -0
- package/dist/version.js.map +1 -0
- package/docs/protocol-verification.md +43 -0
- package/package.json +71 -0
- package/src/client.ts +731 -0
- package/src/combinators/retryOnSessionExpired.ts +25 -0
- package/src/commands.ts +112 -0
- package/src/config.ts +110 -0
- package/src/domain.ts +91 -0
- package/src/errors/auth.ts +28 -0
- package/src/errors/index.ts +50 -0
- package/src/errors/mapFault.ts +92 -0
- package/src/errors/protocol.ts +19 -0
- package/src/errors/schema.ts +18 -0
- package/src/errors/server.ts +79 -0
- package/src/errors/session.ts +12 -0
- package/src/errors/transport.ts +74 -0
- package/src/index.ts +152 -0
- package/src/internal/platform.ts +22 -0
- package/src/internal/singleFlight.ts +80 -0
- package/src/profiles.ts +386 -0
- package/src/protocol/jsonrpc.ts +162 -0
- package/src/records/index.ts +49 -0
- package/src/records/recordModel.ts +211 -0
- package/src/records/related.ts +80 -0
- package/src/records/relations.ts +192 -0
- package/src/records/spec.ts +96 -0
- package/src/records/typed.ts +180 -0
- package/src/rpc.ts +219 -0
- package/src/services/db.ts +282 -0
- package/src/services/report.ts +165 -0
- package/src/session/cookie.ts +461 -0
- package/src/testing/fakeTransport.ts +84 -0
- package/src/testing/index.ts +9 -0
- package/src/transport.ts +72 -0
- package/src/transports/json2.ts +303 -0
- package/src/transports/jsonrpc.ts +218 -0
- package/src/transports/web.ts +96 -0
- package/src/version-live.ts +67 -0
- package/src/version.ts +102 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Context, type Effect } from "effect";
|
|
2
|
+
import type { OdooAuthenticationError } from "./errors/auth.js";
|
|
3
|
+
import type { ProtocolUnsupportedError } from "./errors/protocol.js";
|
|
4
|
+
import type { SchemaDriftError } from "./errors/schema.js";
|
|
5
|
+
import type { OdooServerFault } from "./errors/server.js";
|
|
6
|
+
import type { SessionExpiredError } from "./errors/session.js";
|
|
7
|
+
import type { OdooTransportError } from "./errors/transport.js";
|
|
8
|
+
/**
|
|
9
|
+
* Parameters for a single `call_kw`-shaped round trip. Deliberately mirrors
|
|
10
|
+
* Odoo's `execute_kw(model, method, args, kwargs)`.
|
|
11
|
+
*/
|
|
12
|
+
export interface CallKwParams {
|
|
13
|
+
readonly model: string;
|
|
14
|
+
readonly method: string;
|
|
15
|
+
readonly args: ReadonlyArray<unknown>;
|
|
16
|
+
readonly kwargs: Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* The browse target — the record ids the method operates on.
|
|
19
|
+
*
|
|
20
|
+
* GOTCHA: this is protocol-agnostic on purpose. JSON-2 sends it as its special
|
|
21
|
+
* `ids` body key; the execute_kw-family transports (web, `/jsonrpc`) prepend it
|
|
22
|
+
* as the first positional argument. Callers set `ids` instead of hand-placing
|
|
23
|
+
* it in `args`, so the same call works over every seam.
|
|
24
|
+
*/
|
|
25
|
+
readonly ids?: ReadonlyArray<number>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The complete typed failure channel of a transport round trip. Every seam
|
|
29
|
+
* (web, JSON-RPC, JSON-2) maps its wire faults into exactly this union at its
|
|
30
|
+
* choke point, so application code sees one shape regardless of protocol.
|
|
31
|
+
*/
|
|
32
|
+
export type TransportCallError = OdooTransportError | SessionExpiredError | SchemaDriftError | OdooAuthenticationError | ProtocolUnsupportedError | OdooServerFault;
|
|
33
|
+
/**
|
|
34
|
+
* How a transport encodes calls on the wire. High-level ops that know a
|
|
35
|
+
* method's Python semantics branch on this — a transport itself never reorders
|
|
36
|
+
* args. The one live case: `create` vals MUST be positional over the
|
|
37
|
+
* execute_kw family (Odoo's `call_kw` reads `args[0]` unconditionally, 16–19)
|
|
38
|
+
* but keyword-only (`vals_list`) over JSON-2.
|
|
39
|
+
*/
|
|
40
|
+
export type TransportDialect = "execute-kw" | "json2";
|
|
41
|
+
declare const Transport_base: Context.ServiceClass<Transport, "odoo-rpc-ts/Transport", {
|
|
42
|
+
/** The wire dialect — see {@link TransportDialect}. */
|
|
43
|
+
readonly dialect: TransportDialect;
|
|
44
|
+
/**
|
|
45
|
+
* Execute one `call_kw`-shaped round trip and return the raw, still-undecoded
|
|
46
|
+
* result (callers decode through `effect/Schema` at their boundary).
|
|
47
|
+
*
|
|
48
|
+
* GOTCHA: the JSON-2 API (`/json/2/<model>/<method>`) is keyword-only — it
|
|
49
|
+
* cannot take positional `args`. A `Json2Transport` will reject any call
|
|
50
|
+
* where `args` is non-empty; put everything in `kwargs` when targeting it.
|
|
51
|
+
*/
|
|
52
|
+
readonly callKw: (params: CallKwParams) => Effect.Effect<unknown, TransportCallError>;
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* The one seam every protocol implements. `JsonRpcTransport`, `Json2Transport`,
|
|
56
|
+
* and the cookie-session `WebTransport` all provide this tag; application code
|
|
57
|
+
* depends only on it and stays protocol-agnostic.
|
|
58
|
+
*/
|
|
59
|
+
export declare class Transport extends Transport_base {
|
|
60
|
+
}
|
|
61
|
+
export {};
|
|
62
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC;;;;;;;OAOG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAC1B,kBAAkB,GAClB,mBAAmB,GACnB,gBAAgB,GAChB,uBAAuB,GACvB,wBAAwB,GACxB,eAAe,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,OAAO,CAAC;;IAUlD,uDAAuD;sBACrC,gBAAgB;IAElC;;;;;;;OAOG;qBACc,CAAC,MAAM,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,kBAAkB,CAAC;;AAnBzF;;;;GAIG;AACH,qBAAa,SAAU,SAAQ,cAgBH;CAAG"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Context } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* The one seam every protocol implements. `JsonRpcTransport`, `Json2Transport`,
|
|
4
|
+
* and the cookie-session `WebTransport` all provide this tag; application code
|
|
5
|
+
* depends only on it and stays protocol-agnostic.
|
|
6
|
+
*/
|
|
7
|
+
export class Transport extends Context.Service()("odoo-rpc-ts/Transport") {
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAe,MAAM,QAAQ,CAAC;AAkD9C;;;;GAIG;AACH,MAAM,OAAO,SAAU,SAAQ,OAAO,CAAC,OAAO,EAgB3C,CAAC,uBAAuB,CAAC;CAAG"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Config, Effect, Layer, Option, Schema } from "effect";
|
|
2
|
+
import type { OdooConfig as OdooConfigType } from "../config.js";
|
|
3
|
+
import { OdooAuthenticationError } from "../errors/auth.js";
|
|
4
|
+
import { SchemaDriftError } from "../errors/schema.js";
|
|
5
|
+
import { OdooTransportError } from "../errors/transport.js";
|
|
6
|
+
import { HttpClient } from "../internal/platform.js";
|
|
7
|
+
import { Transport } from "../transport.js";
|
|
8
|
+
/**
|
|
9
|
+
* Build a {@link Transport} bound to the JSON-2 route for a given config.
|
|
10
|
+
*
|
|
11
|
+
* GOTCHA: JSON-2 is bearer-only. If the config carries `Password` credentials
|
|
12
|
+
* there is no API key to send, so construction FAILS in the typed channel with
|
|
13
|
+
* {@link OdooAuthenticationError} rather than silently degrading — this is a
|
|
14
|
+
* `Layer` build failure, surfaced eagerly so a misconfigured client never issues
|
|
15
|
+
* an unauthenticated request.
|
|
16
|
+
*/
|
|
17
|
+
export declare const make: (config: OdooConfigType) => Effect.Effect<typeof Transport.Service, OdooAuthenticationError, HttpClient.HttpClient>;
|
|
18
|
+
/** Wire a JSON-2 {@link Transport} from an explicit config. */
|
|
19
|
+
export declare const layer: (config: OdooConfigType) => Layer.Layer<Transport, OdooAuthenticationError, HttpClient.HttpClient>;
|
|
20
|
+
/** Wire a JSON-2 {@link Transport}, reading {@link OdooConfig} from the environment. */
|
|
21
|
+
export declare const layerConfig: Layer.Layer<Transport, OdooAuthenticationError | Config.ConfigError, HttpClient.HttpClient>;
|
|
22
|
+
/**
|
|
23
|
+
* The `/json/version` probe result: the server's version string and its raw
|
|
24
|
+
* `version_info` tuple. Decoded at the boundary; drift raises
|
|
25
|
+
* {@link SchemaDriftError}.
|
|
26
|
+
*/
|
|
27
|
+
export declare const Json2Version: Schema.Struct<{
|
|
28
|
+
readonly version: Schema.String;
|
|
29
|
+
readonly version_info: Schema.$Array<Schema.Union<readonly [Schema.Number, Schema.String]>>;
|
|
30
|
+
}>;
|
|
31
|
+
export type Json2Version = typeof Json2Version.Type;
|
|
32
|
+
/**
|
|
33
|
+
* Probe `GET {url}/json/version` to detect JSON-2 support.
|
|
34
|
+
*
|
|
35
|
+
* Returns `Option.some(version)` on a 200, and `Option.none()` on a 404 — a 404
|
|
36
|
+
* is the documented "this server predates Odoo 19 (no JSON-2)" signal, not an
|
|
37
|
+
* error. Any other non-2xx is a real {@link OdooTransportError}; a 200 body that
|
|
38
|
+
* fails to decode is a {@link SchemaDriftError}.
|
|
39
|
+
*/
|
|
40
|
+
export declare const probeJson2Version: (config: OdooConfigType) => Effect.Effect<Option.Option<Json2Version>, OdooTransportError | SchemaDriftError, HttpClient.HttpClient>;
|
|
41
|
+
//# sourceMappingURL=json2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json2.d.ts","sourceRoot":"","sources":["../../src/transports/json2.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAY,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9E,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AASvD,OAAO,EAAE,kBAAkB,EAAoB,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EACL,UAAU,EAIX,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAqB,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAyH/D;;;;;;;;GAQG;AACH,eAAO,MAAM,IAAI,WACP,cAAc,KACrB,MAAM,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,OAAO,EAAE,uBAAuB,EAAE,UAAU,CAAC,UAAU,CAyErF,CAAC;AASL,+DAA+D;AAC/D,eAAO,MAAM,KAAK,WACR,cAAc,KACrB,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,uBAAuB,EAAE,UAAU,CAAC,UAAU,CACjC,CAAC;AAExC,wFAAwF;AACxF,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,KAAK,CACnC,SAAS,EACT,uBAAuB,GAAG,MAAM,CAAC,WAAW,EAC5C,UAAU,CAAC,UAAU,CACsC,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;EAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,WACpB,cAAc,KACrB,MAAM,CAAC,MAAM,CACd,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAC3B,kBAAkB,GAAG,gBAAgB,EACrC,UAAU,CAAC,UAAU,CA6BnB,CAAC"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Effect, Layer, Option, Redacted, Schema } from "effect";
|
|
2
|
+
import { OdooConfig } from "../config.js";
|
|
3
|
+
import { OdooAuthenticationError } from "../errors/auth.js";
|
|
4
|
+
import { mapServerFault } from "../errors/mapFault.js";
|
|
5
|
+
import { ProtocolUnsupportedError } from "../errors/protocol.js";
|
|
6
|
+
import { SchemaDriftError } from "../errors/schema.js";
|
|
7
|
+
import { OdooAccessError, OdooLockError, OdooMissingError, OdooServerError, OdooUserError, } from "../errors/server.js";
|
|
8
|
+
import { OdooTransportError } from "../errors/transport.js";
|
|
9
|
+
import { HttpClient, HttpClientRequest, HttpClientResponse, } from "../internal/platform.js";
|
|
10
|
+
import { Transport } from "../transport.js";
|
|
11
|
+
import { ServerVersionInfo } from "../version.js";
|
|
12
|
+
/**
|
|
13
|
+
* Odoo's `/json/2/<model>/<method>` API (Odoo 19+): a keyword-only, bearer-auth
|
|
14
|
+
* JSON route that returns the raw method result with no JSON-RPC envelope. This
|
|
15
|
+
* module is one of the three seams behind the {@link Transport} tag.
|
|
16
|
+
*/
|
|
17
|
+
/** Strip any trailing slashes so path segments join cleanly onto a base URL. */
|
|
18
|
+
const baseUrl = (url) => url.href.replace(/\/+$/u, "");
|
|
19
|
+
/** Sanitized request snapshot — never carries the bearer header or body. */
|
|
20
|
+
const requestInfo = (url, method) => ({ method, url });
|
|
21
|
+
const toTransportError = (request) => (cause) => OdooTransportError.fromHttpClientError(request, cause);
|
|
22
|
+
/**
|
|
23
|
+
* The `serialize_exception` payload Odoo returns in an error body. Everything is
|
|
24
|
+
* optional: a bare HTTP failure (proxy 502, gateway timeout) may have no JSON
|
|
25
|
+
* body at all, in which case we synthesize a fault from the status alone.
|
|
26
|
+
*/
|
|
27
|
+
const SerializeException = Schema.Struct({
|
|
28
|
+
name: Schema.optional(Schema.String),
|
|
29
|
+
message: Schema.optional(Schema.String),
|
|
30
|
+
arguments: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
31
|
+
context: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
32
|
+
debug: Schema.optional(Schema.String),
|
|
33
|
+
});
|
|
34
|
+
const emptyException = {};
|
|
35
|
+
/**
|
|
36
|
+
* Build a {@link RawServerFault} from a decoded exception body and the call
|
|
37
|
+
* site, honouring `exactOptionalPropertyTypes` (optional keys are attached only
|
|
38
|
+
* when present, never as explicit `undefined`).
|
|
39
|
+
*/
|
|
40
|
+
const rawFault = (exc, status, site) => ({
|
|
41
|
+
name: exc.name ?? `http.${status}`,
|
|
42
|
+
message: exc.message ?? "",
|
|
43
|
+
arguments: exc.arguments ?? [],
|
|
44
|
+
context: exc.context ?? {},
|
|
45
|
+
...(exc.debug !== undefined ? { debug: exc.debug } : {}),
|
|
46
|
+
...(site.model !== undefined ? { model: site.model } : {}),
|
|
47
|
+
...(site.method !== undefined ? { method: site.method } : {}),
|
|
48
|
+
});
|
|
49
|
+
/** Status → concrete server-fault constructor, for the no-`name` fallback path. */
|
|
50
|
+
const byStatus = {
|
|
51
|
+
403: OdooAccessError,
|
|
52
|
+
404: OdooMissingError,
|
|
53
|
+
409: OdooLockError,
|
|
54
|
+
422: OdooUserError,
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The single error-mapping choke point for JSON-2. A non-2xx response becomes
|
|
58
|
+
* exactly one tagged error:
|
|
59
|
+
*
|
|
60
|
+
* - `400` / `415` are transport-shaped (malformed request / wrong media type),
|
|
61
|
+
* so they map to {@link OdooTransportError} with sanitized response-error
|
|
62
|
+
* metadata.
|
|
63
|
+
* - Any body with a Python `name` goes through {@link mapServerFault} (shared
|
|
64
|
+
* with the other transports), so `AccessDenied` correctly becomes an auth
|
|
65
|
+
* error and known exception names get their subtype.
|
|
66
|
+
* - Otherwise we fall back on the HTTP status, synthesizing a fault named
|
|
67
|
+
* `http.<status>` so nothing is silently dropped.
|
|
68
|
+
*/
|
|
69
|
+
const mapJson2Fault = (response, request, site) => Effect.gen(function* () {
|
|
70
|
+
const status = response.status;
|
|
71
|
+
if (status === 400 || status === 415) {
|
|
72
|
+
// `filterStatusOk` manufactures a response error from the non-ok status
|
|
73
|
+
// without reading the body; the public error retains only safe metadata.
|
|
74
|
+
const cause = yield* Effect.flip(HttpClientResponse.filterStatusOk(response)).pipe(Effect.orDie);
|
|
75
|
+
return yield* Effect.fail(OdooTransportError.fromHttpClientError(request, cause));
|
|
76
|
+
}
|
|
77
|
+
// A non-JSON or shape-drifted error body falls back to an empty exception,
|
|
78
|
+
// which routes to status-based mapping below.
|
|
79
|
+
const exc = yield* response.json.pipe(Effect.flatMap(Schema.decodeUnknownEffect(SerializeException)), Effect.orElseSucceed(() => emptyException));
|
|
80
|
+
const raw = rawFault(exc, status, site);
|
|
81
|
+
// GOTCHA: 401 is checked BEFORE the body name. Odoo 19 serializes a bad
|
|
82
|
+
// bearer key as werkzeug.exceptions.Unauthorized — an unmapped name that
|
|
83
|
+
// would otherwise fall through to OdooServerError instead of auth.
|
|
84
|
+
if (status === 401) {
|
|
85
|
+
return yield* Effect.fail(new OdooAuthenticationError({
|
|
86
|
+
reason: "api-key-expired-or-invalid",
|
|
87
|
+
message: raw.message || "JSON-2 rejected the API key (HTTP 401).",
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
if (exc.name !== undefined) {
|
|
91
|
+
return yield* Effect.fail(mapServerFault(raw, site));
|
|
92
|
+
}
|
|
93
|
+
const Ctor = byStatus[status] ?? OdooServerError;
|
|
94
|
+
return yield* Effect.fail(new Ctor(raw));
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Build a {@link Transport} bound to the JSON-2 route for a given config.
|
|
98
|
+
*
|
|
99
|
+
* GOTCHA: JSON-2 is bearer-only. If the config carries `Password` credentials
|
|
100
|
+
* there is no API key to send, so construction FAILS in the typed channel with
|
|
101
|
+
* {@link OdooAuthenticationError} rather than silently degrading — this is a
|
|
102
|
+
* `Layer` build failure, surfaced eagerly so a misconfigured client never issues
|
|
103
|
+
* an unauthenticated request.
|
|
104
|
+
*/
|
|
105
|
+
export const make = (config) => Effect.gen(function* () {
|
|
106
|
+
if (config.credentials._tag !== "ApiKey") {
|
|
107
|
+
return yield* Effect.fail(new OdooAuthenticationError({
|
|
108
|
+
reason: "invalid-credentials",
|
|
109
|
+
message: "JSON-2 transport is bearer-only and requires ApiKey credentials; " +
|
|
110
|
+
"got Password. Provide ODOO_API_KEY.",
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
const apiKey = config.credentials.apiKey;
|
|
114
|
+
const client = yield* HttpClient.HttpClient;
|
|
115
|
+
const base = baseUrl(config.url);
|
|
116
|
+
const callKw = (params) => {
|
|
117
|
+
const site = { model: params.model, method: params.method };
|
|
118
|
+
const run = Effect.gen(function* () {
|
|
119
|
+
// JSON-2 cannot carry positional args; reordering silently would corrupt
|
|
120
|
+
// the call, so reject loudly and name the offending method.
|
|
121
|
+
if (params.args.length > 0) {
|
|
122
|
+
return yield* Effect.fail(new ProtocolUnsupportedError({
|
|
123
|
+
protocol: "json-2",
|
|
124
|
+
serverVersion: "19+",
|
|
125
|
+
message: `JSON-2 is keyword-only: "${params.model}.${params.method}" was called with ` +
|
|
126
|
+
`${params.args.length} positional argument(s); move them into kwargs.`,
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
const url = `${base}/json/2/${params.model}/${params.method}`;
|
|
130
|
+
const info = requestInfo(url, "POST");
|
|
131
|
+
// Keyword-only route: the browse target rides as the dedicated `ids` body
|
|
132
|
+
// key (see CallKwParams.ids), never as positional args.
|
|
133
|
+
const body = params.ids ? { ...params.kwargs, ids: params.ids } : params.kwargs;
|
|
134
|
+
// Redacted is unwrapped ONLY here, at the wire boundary.
|
|
135
|
+
const request = HttpClientRequest.post(url).pipe(HttpClientRequest.setHeaders({
|
|
136
|
+
Authorization: `Bearer ${Redacted.value(apiKey)}`,
|
|
137
|
+
"X-Odoo-Database": config.db,
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
}), HttpClientRequest.bodyJsonUnsafe(body));
|
|
140
|
+
const response = yield* client
|
|
141
|
+
.execute(request)
|
|
142
|
+
.pipe(Effect.mapError(toTransportError(info)));
|
|
143
|
+
if (response.status >= 200 && response.status < 300) {
|
|
144
|
+
// Raw method result, no envelope; callers decode at their own boundary.
|
|
145
|
+
return yield* response.json.pipe(Effect.mapError(toTransportError(info)));
|
|
146
|
+
}
|
|
147
|
+
return yield* mapJson2Fault(response, info, site);
|
|
148
|
+
});
|
|
149
|
+
// Span carries only the coordinates — never args, kwargs, or the API key.
|
|
150
|
+
return run.pipe(Effect.withSpan("odoo.json2", {
|
|
151
|
+
attributes: { model: params.model, method: params.method },
|
|
152
|
+
}));
|
|
153
|
+
};
|
|
154
|
+
return { dialect: "json2", callKw };
|
|
155
|
+
});
|
|
156
|
+
/** Wire a JSON-2 {@link Transport} from an explicit config. */
|
|
157
|
+
export const layer = (config) => Layer.effect(Transport, make(config));
|
|
158
|
+
/** Wire a JSON-2 {@link Transport}, reading {@link OdooConfig} from the environment. */
|
|
159
|
+
export const layerConfig = Layer.effect(Transport, Effect.flatMap(OdooConfig, make));
|
|
160
|
+
/**
|
|
161
|
+
* The `/json/version` probe result: the server's version string and its raw
|
|
162
|
+
* `version_info` tuple. Decoded at the boundary; drift raises
|
|
163
|
+
* {@link SchemaDriftError}.
|
|
164
|
+
*/
|
|
165
|
+
export const Json2Version = Schema.Struct({
|
|
166
|
+
version: Schema.String,
|
|
167
|
+
version_info: ServerVersionInfo,
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Probe `GET {url}/json/version` to detect JSON-2 support.
|
|
171
|
+
*
|
|
172
|
+
* Returns `Option.some(version)` on a 200, and `Option.none()` on a 404 — a 404
|
|
173
|
+
* is the documented "this server predates Odoo 19 (no JSON-2)" signal, not an
|
|
174
|
+
* error. Any other non-2xx is a real {@link OdooTransportError}; a 200 body that
|
|
175
|
+
* fails to decode is a {@link SchemaDriftError}.
|
|
176
|
+
*/
|
|
177
|
+
export const probeJson2Version = (config) => Effect.gen(function* () {
|
|
178
|
+
const client = yield* HttpClient.HttpClient;
|
|
179
|
+
const url = `${baseUrl(config.url)}/json/version`;
|
|
180
|
+
const info = requestInfo(url, "GET");
|
|
181
|
+
const response = yield* client
|
|
182
|
+
.execute(HttpClientRequest.get(url))
|
|
183
|
+
.pipe(Effect.mapError(toTransportError(info)));
|
|
184
|
+
const status = response.status;
|
|
185
|
+
if (status === 404) {
|
|
186
|
+
return Option.none();
|
|
187
|
+
}
|
|
188
|
+
if (status < 200 || status >= 300) {
|
|
189
|
+
const cause = yield* Effect.flip(HttpClientResponse.filterStatusOk(response)).pipe(Effect.orDie);
|
|
190
|
+
return yield* Effect.fail(OdooTransportError.fromHttpClientError(info, cause));
|
|
191
|
+
}
|
|
192
|
+
const payload = yield* response.json.pipe(Effect.mapError(toTransportError(info)));
|
|
193
|
+
const decoded = yield* Schema.decodeUnknownEffect(Json2Version)(payload).pipe(Effect.mapError((cause) => new SchemaDriftError({ context: "odoo.json2.version", payload, cause })));
|
|
194
|
+
return Option.some(decoded);
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=json2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json2.js","sourceRoot":"","sources":["../../src/transports/json2.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Config, Effect, Layer } from "effect";
|
|
2
|
+
import { OdooConfig } from "../config.js";
|
|
3
|
+
import { OdooAuthenticationError } from "../errors/auth.js";
|
|
4
|
+
import { SchemaDriftError } from "../errors/schema.js";
|
|
5
|
+
import type { OdooServerFault } from "../errors/server.js";
|
|
6
|
+
import type { SessionExpiredError } from "../errors/session.js";
|
|
7
|
+
import { OdooTransportError } from "../errors/transport.js";
|
|
8
|
+
import { HttpClient } from "../internal/platform.js";
|
|
9
|
+
import { Transport } from "../transport.js";
|
|
10
|
+
import { CommonVersionResponse } from "../version.js";
|
|
11
|
+
/** The failure channel a single round trip can produce (a subset of `TransportCallError`). */
|
|
12
|
+
type RoundTripError = OdooTransportError | SchemaDriftError | SessionExpiredError | OdooServerFault | OdooAuthenticationError;
|
|
13
|
+
/**
|
|
14
|
+
* Build a `JsonRpcTransport` service value against a fully-resolved config.
|
|
15
|
+
*
|
|
16
|
+
* `uid` is resolved lazily on the first `callKw` via `common.authenticate`, then
|
|
17
|
+
* success-only single-flight cached (a failed authenticate caches nothing, so
|
|
18
|
+
* the next call retries — never `Effect.cached`).
|
|
19
|
+
*
|
|
20
|
+
* Requires an `HttpClient` in context; the caller provides the platform layer.
|
|
21
|
+
*/
|
|
22
|
+
export declare const make: (config: OdooConfig) => Effect.Effect<typeof Transport.Service, never, HttpClient.HttpClient>;
|
|
23
|
+
/**
|
|
24
|
+
* `JsonRpcTransport` as a `Layer`, wired against an explicit config. Never
|
|
25
|
+
* fails to build (`E = never`); the config is already resolved. Requires an
|
|
26
|
+
* `HttpClient` from the platform layer the consumer provides.
|
|
27
|
+
*/
|
|
28
|
+
export declare const layer: (config: OdooConfig) => Layer.Layer<Transport, never, HttpClient.HttpClient>;
|
|
29
|
+
/**
|
|
30
|
+
* `JsonRpcTransport` as a `Layer` that reads {@link OdooConfig} from the ambient
|
|
31
|
+
* `ConfigProvider`. Fails with `ConfigError` if required values are missing.
|
|
32
|
+
*/
|
|
33
|
+
export declare const layerConfig: Layer.Layer<Transport, Config.ConfigError, HttpClient.HttpClient>;
|
|
34
|
+
/**
|
|
35
|
+
* The `common.version` probe, decoded into {@link CommonVersionResponse}. Shared
|
|
36
|
+
* with the `VersionResolver` implementation so version resolution and the
|
|
37
|
+
* transport agree on one wire path. Requires an `HttpClient` in context.
|
|
38
|
+
*/
|
|
39
|
+
export declare const makeVersion: (config: OdooConfig) => Effect.Effect<CommonVersionResponse, RoundTripError, HttpClient.HttpClient>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=jsonrpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonrpc.d.ts","sourceRoot":"","sources":["../../src/transports/jsonrpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAyB,MAAM,QAAQ,CAAC;AACtE,OAAO,EAAE,UAAU,EAAwB,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAqB,MAAM,yBAAyB,CAAC;AAGxE,OAAO,EAAqB,SAAS,EAA2B,MAAM,iBAAiB,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAStD,8FAA8F;AAC9F,KAAK,cAAc,GACf,kBAAkB,GAClB,gBAAgB,GAChB,mBAAmB,GACnB,eAAe,GACf,uBAAuB,CAAC;AA0D5B;;;;;;;;GAQG;AACH,eAAO,MAAM,IAAI,WACP,UAAU,KACjB,MAAM,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,CA+EnE,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,KAAK,WAAY,UAAU,KAAG,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,CACvD,CAAC;AAExC;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,UAAU,CAC/B,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,WAAW,WACd,UAAU,KACjB,MAAM,CAAC,MAAM,CAAC,qBAAqB,EAAE,cAAc,EAAE,UAAU,CAAC,UAAU,CAkBzE,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Config, Effect, Layer, Redacted, Ref, Schema } from "effect";
|
|
2
|
+
import { OdooConfig } from "../config.js";
|
|
3
|
+
import { OdooAuthenticationError } from "../errors/auth.js";
|
|
4
|
+
import { SchemaDriftError } from "../errors/schema.js";
|
|
5
|
+
import { OdooTransportError } from "../errors/transport.js";
|
|
6
|
+
import { HttpClient, HttpClientRequest } from "../internal/platform.js";
|
|
7
|
+
import * as SingleFlight from "../internal/singleFlight.js";
|
|
8
|
+
import { buildRequest, JsonRpcResponse, mapJsonRpcError } from "../protocol/jsonrpc.js";
|
|
9
|
+
import { Transport } from "../transport.js";
|
|
10
|
+
import { CommonVersionResponse } from "../version.js";
|
|
11
|
+
/** Join the configured base URL with the `/jsonrpc` path, tolerating a trailing slash. */
|
|
12
|
+
const jsonRpcEndpoint = (url) => `${url.href.replace(/\/+$/, "")}/jsonrpc`;
|
|
13
|
+
const extractCredentials = (credentials) => credentials._tag === "ApiKey"
|
|
14
|
+
? { username: credentials.username, secret: credentials.apiKey, isPassword: false }
|
|
15
|
+
: { username: credentials.username, secret: credentials.password, isPassword: true };
|
|
16
|
+
/**
|
|
17
|
+
* One JSON-RPC round trip: build the envelope, POST it, decode the response, and
|
|
18
|
+
* either return the raw `result` or map the fault into our union.
|
|
19
|
+
*
|
|
20
|
+
* GOTCHA: never put `args`/`kwargs`/the secret into the span attributes — only
|
|
21
|
+
* the model and method (or service/method) are safe to record.
|
|
22
|
+
*/
|
|
23
|
+
const roundTrip = (client, endpoint, id, params, attributes, site) => Effect.gen(function* () {
|
|
24
|
+
// v4 rename: bodyUnsafeJson → bodyJsonUnsafe.
|
|
25
|
+
const request = HttpClientRequest.post(endpoint).pipe(HttpClientRequest.setHeader("Content-Type", "application/json"), HttpClientRequest.bodyJsonUnsafe(buildRequest(params, id)));
|
|
26
|
+
// HttpClientError covers both connection failures and a non-JSON body.
|
|
27
|
+
const payload = yield* client.execute(request).pipe(Effect.flatMap((response) => response.json), Effect.mapError((cause) => OdooTransportError.fromHttpClientError({ method: "POST", url: endpoint }, cause)));
|
|
28
|
+
const decoded = yield* Schema.decodeUnknownEffect(JsonRpcResponse)(payload).pipe(Effect.mapError((cause) => new SchemaDriftError({ context: "odoo.jsonrpc response", payload, cause })));
|
|
29
|
+
if ("error" in decoded) {
|
|
30
|
+
return yield* Effect.fail(mapJsonRpcError(decoded.error, site));
|
|
31
|
+
}
|
|
32
|
+
return decoded.result;
|
|
33
|
+
}).pipe(Effect.withSpan("odoo.jsonrpc", { attributes }));
|
|
34
|
+
/**
|
|
35
|
+
* Build a `JsonRpcTransport` service value against a fully-resolved config.
|
|
36
|
+
*
|
|
37
|
+
* `uid` is resolved lazily on the first `callKw` via `common.authenticate`, then
|
|
38
|
+
* success-only single-flight cached (a failed authenticate caches nothing, so
|
|
39
|
+
* the next call retries — never `Effect.cached`).
|
|
40
|
+
*
|
|
41
|
+
* Requires an `HttpClient` in context; the caller provides the platform layer.
|
|
42
|
+
*/
|
|
43
|
+
export const make = (config) => Effect.gen(function* () {
|
|
44
|
+
const client = yield* HttpClient.HttpClient;
|
|
45
|
+
const idRef = yield* Ref.make(0);
|
|
46
|
+
const endpoint = jsonRpcEndpoint(config.url);
|
|
47
|
+
const { username, secret, isPassword } = extractCredentials(config.credentials);
|
|
48
|
+
const db = config.db;
|
|
49
|
+
const nextId = Ref.updateAndGet(idRef, (n) => n + 1);
|
|
50
|
+
const authenticate = Effect.gen(function* () {
|
|
51
|
+
const id = yield* nextId;
|
|
52
|
+
const result = yield* roundTrip(client, endpoint, id, {
|
|
53
|
+
service: "common",
|
|
54
|
+
method: "authenticate",
|
|
55
|
+
args: [db, username, Redacted.value(secret), {}],
|
|
56
|
+
}, {}, {});
|
|
57
|
+
if (typeof result === "number" && result > 0) {
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
// authenticate returned `false` (or a null uid): credentials rejected.
|
|
61
|
+
return yield* Effect.fail(new OdooAuthenticationError({
|
|
62
|
+
reason: "invalid-credentials",
|
|
63
|
+
message: isPassword
|
|
64
|
+
? "Odoo rejected the login. If this account has TOTP/2FA enabled, its password cannot be used over RPC — supply an API key as the secret instead."
|
|
65
|
+
: "Odoo rejected the provided credentials.",
|
|
66
|
+
}));
|
|
67
|
+
});
|
|
68
|
+
const uid = yield* SingleFlight.make(authenticate);
|
|
69
|
+
const callKw = (params) => Effect.gen(function* () {
|
|
70
|
+
const resolvedUid = yield* uid.get;
|
|
71
|
+
const id = yield* nextId;
|
|
72
|
+
// execute_kw-family route: an explicit `ids` browse target rides as the
|
|
73
|
+
// first positional argument (see CallKwParams.ids).
|
|
74
|
+
const args = params.ids ? [params.ids, ...params.args] : params.args;
|
|
75
|
+
return yield* roundTrip(client, endpoint, id, {
|
|
76
|
+
service: "object",
|
|
77
|
+
method: "execute_kw",
|
|
78
|
+
args: [
|
|
79
|
+
db,
|
|
80
|
+
resolvedUid,
|
|
81
|
+
Redacted.value(secret),
|
|
82
|
+
params.model,
|
|
83
|
+
params.method,
|
|
84
|
+
args,
|
|
85
|
+
params.kwargs,
|
|
86
|
+
],
|
|
87
|
+
}, { model: params.model, method: params.method }, { model: params.model, method: params.method });
|
|
88
|
+
}).pipe(
|
|
89
|
+
// Self-healing: an auth fault means the cached uid/credential pairing is
|
|
90
|
+
// no longer valid server-side (key revoked-then-restored, credentials
|
|
91
|
+
// out of sync, ...). Drop the cache so the next call re-authenticates
|
|
92
|
+
// instead of staying wedged until the layer is rebuilt — symmetric with
|
|
93
|
+
// CookieSession.invalidate.
|
|
94
|
+
Effect.tapError((error) => error._tag === "OdooAuthenticationError" ? uid.invalidate : Effect.void));
|
|
95
|
+
return { dialect: "execute-kw", callKw };
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* `JsonRpcTransport` as a `Layer`, wired against an explicit config. Never
|
|
99
|
+
* fails to build (`E = never`); the config is already resolved. Requires an
|
|
100
|
+
* `HttpClient` from the platform layer the consumer provides.
|
|
101
|
+
*/
|
|
102
|
+
export const layer = (config) => Layer.effect(Transport, make(config));
|
|
103
|
+
/**
|
|
104
|
+
* `JsonRpcTransport` as a `Layer` that reads {@link OdooConfig} from the ambient
|
|
105
|
+
* `ConfigProvider`. Fails with `ConfigError` if required values are missing.
|
|
106
|
+
*/
|
|
107
|
+
export const layerConfig = Layer.effect(Transport, Effect.flatMap(OdooConfig, make));
|
|
108
|
+
/**
|
|
109
|
+
* The `common.version` probe, decoded into {@link CommonVersionResponse}. Shared
|
|
110
|
+
* with the `VersionResolver` implementation so version resolution and the
|
|
111
|
+
* transport agree on one wire path. Requires an `HttpClient` in context.
|
|
112
|
+
*/
|
|
113
|
+
export const makeVersion = (config) => Effect.gen(function* () {
|
|
114
|
+
const client = yield* HttpClient.HttpClient;
|
|
115
|
+
const endpoint = jsonRpcEndpoint(config.url);
|
|
116
|
+
const raw = yield* roundTrip(client, endpoint, 1, { service: "common", method: "version", args: [] }, {}, {});
|
|
117
|
+
return yield* Schema.decodeUnknownEffect(CommonVersionResponse)(raw).pipe(Effect.mapError((cause) => new SchemaDriftError({ context: "odoo.jsonrpc common.version", payload: raw, cause })));
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=jsonrpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonrpc.js","sourceRoot":"","sources":["../../src/transports/jsonrpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Config, Effect, Layer } from "effect";
|
|
2
|
+
import { OdooConfig } from "../config.js";
|
|
3
|
+
import { CookieSession } from "../session/cookie.js";
|
|
4
|
+
import { Transport } from "../transport.js";
|
|
5
|
+
/**
|
|
6
|
+
* Build the `WebTransport` implementation. Every `call_kw` first ensures a
|
|
7
|
+
* cookie session exists (via {@link CookieSession}) and then rides the shared,
|
|
8
|
+
* cookie-bound HttpClient so `session_id` — and any rotation — is always sent.
|
|
9
|
+
*/
|
|
10
|
+
export declare const make: (config: Pick<OdooConfig, "url">) => Effect.Effect<typeof Transport.Service, never, CookieSession>;
|
|
11
|
+
/** Provide the web `Transport` from an already-resolved config. Requires `CookieSession`. */
|
|
12
|
+
export declare const layer: (config: Pick<OdooConfig, "url">) => Layer.Layer<Transport, never, CookieSession>;
|
|
13
|
+
/** Provide the web `Transport`, resolving `OdooConfig` from the environment. */
|
|
14
|
+
export declare const layerConfig: Layer.Layer<Transport, Config.ConfigError, CookieSession>;
|
|
15
|
+
//# sourceMappingURL=web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/transports/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAI1C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAQrD,OAAO,EAAqB,SAAS,EAA2B,MAAM,iBAAiB,CAAC;AAExF;;;;GAIG;AACH,eAAO,MAAM,IAAI,WAIP,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,KAC9B,MAAM,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,CA6D3D,CAAC;AAEL,6FAA6F;AAC7F,eAAO,MAAM,KAAK,WACR,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,KAC9B,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,EAAE,aAAa,CAA0C,CAAC;AAEzF,gFAAgF;AAChF,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,MAAM,CAAC,WAAW,EAAE,aAAa,CACvB,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Effect, Layer, Schema } from "effect";
|
|
2
|
+
import { OdooConfig } from "../config.js";
|
|
3
|
+
import { SchemaDriftError } from "../errors/schema.js";
|
|
4
|
+
import { OdooTransportError } from "../errors/transport.js";
|
|
5
|
+
import { HttpClientRequest } from "../internal/platform.js";
|
|
6
|
+
import { CookieSession } from "../session/cookie.js";
|
|
7
|
+
import { joinPath } from "../session/cookie.js";
|
|
8
|
+
import { buildRequest, JsonRpcResponse, mapJsonRpcError, nextRequestId, } from "../protocol/jsonrpc.js";
|
|
9
|
+
import { Transport } from "../transport.js";
|
|
10
|
+
/**
|
|
11
|
+
* Build the `WebTransport` implementation. Every `call_kw` first ensures a
|
|
12
|
+
* cookie session exists (via {@link CookieSession}) and then rides the shared,
|
|
13
|
+
* cookie-bound HttpClient so `session_id` — and any rotation — is always sent.
|
|
14
|
+
*/
|
|
15
|
+
export const make = (
|
|
16
|
+
// Only `url` is read: the transport never touches credentials, so an
|
|
17
|
+
// injected-cookie consumer (CookieSessionLive.fromExisting) can drive it
|
|
18
|
+
// without fabricating an OdooConfig. A full OdooConfig still satisfies this.
|
|
19
|
+
config) => Effect.gen(function* () {
|
|
20
|
+
const session = yield* CookieSession;
|
|
21
|
+
const callUrl = joinPath(config.url, "web/dataset/call_kw");
|
|
22
|
+
const request = { method: "POST", url: callUrl };
|
|
23
|
+
const callKw = (params) => Effect.gen(function* () {
|
|
24
|
+
// Choke point: ensure the session before the call. A SessionExpiredError
|
|
25
|
+
// from here does NOT auto-relogin (see retryOnSessionExpired).
|
|
26
|
+
yield* session.login;
|
|
27
|
+
// execute_kw-family route: an explicit `ids` browse target rides as the
|
|
28
|
+
// first positional argument (see CallKwParams.ids).
|
|
29
|
+
const args = params.ids ? [params.ids, ...params.args] : params.args;
|
|
30
|
+
const envelope = buildRequest({
|
|
31
|
+
model: params.model,
|
|
32
|
+
method: params.method,
|
|
33
|
+
args,
|
|
34
|
+
kwargs: params.kwargs,
|
|
35
|
+
}, nextRequestId());
|
|
36
|
+
const response = yield* session.client
|
|
37
|
+
.execute(HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(callUrl), envelope))
|
|
38
|
+
.pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
|
|
39
|
+
const body = yield* response.json.pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
|
|
40
|
+
const decoded = yield* Schema.decodeUnknownEffect(JsonRpcResponse)(body).pipe(Effect.mapError((cause) => new SchemaDriftError({
|
|
41
|
+
context: "web/dataset/call_kw envelope",
|
|
42
|
+
payload: body,
|
|
43
|
+
cause,
|
|
44
|
+
})));
|
|
45
|
+
if ("error" in decoded) {
|
|
46
|
+
// Single mapping choke point: code 100 → SessionExpiredError, else
|
|
47
|
+
// mapServerFault, with the call site threaded through.
|
|
48
|
+
return yield* Effect.fail(mapJsonRpcError(decoded.error, { model: params.model, method: params.method }));
|
|
49
|
+
}
|
|
50
|
+
return decoded.result;
|
|
51
|
+
}).pipe(
|
|
52
|
+
// No args/secrets in span attributes — model + method only.
|
|
53
|
+
Effect.withSpan("odoo.web.call_kw", {
|
|
54
|
+
attributes: { model: params.model, method: params.method },
|
|
55
|
+
}));
|
|
56
|
+
return { dialect: "execute-kw", callKw };
|
|
57
|
+
});
|
|
58
|
+
/** Provide the web `Transport` from an already-resolved config. Requires `CookieSession`. */
|
|
59
|
+
export const layer = (config) => Layer.effect(Transport, make(config));
|
|
60
|
+
/** Provide the web `Transport`, resolving `OdooConfig` from the environment. */
|
|
61
|
+
export const layerConfig = Layer.effect(Transport, Effect.flatMap(OdooConfig, make));
|
|
62
|
+
//# sourceMappingURL=web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/transports/web.ts"],"names":[],"mappings":"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"}
|