@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.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +401 -0
  3. package/dist/client.d.ts +184 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +277 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/combinators/retryOnSessionExpired.d.ts +13 -0
  8. package/dist/combinators/retryOnSessionExpired.d.ts.map +1 -0
  9. package/dist/combinators/retryOnSessionExpired.js +15 -0
  10. package/dist/combinators/retryOnSessionExpired.js.map +1 -0
  11. package/dist/commands.d.ts +91 -0
  12. package/dist/commands.d.ts.map +1 -0
  13. package/dist/commands.js +69 -0
  14. package/dist/commands.js.map +1 -0
  15. package/dist/config.d.ts +32 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +60 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/domain.d.ts +30 -0
  20. package/dist/domain.d.ts.map +1 -0
  21. package/dist/domain.js +70 -0
  22. package/dist/domain.js.map +1 -0
  23. package/dist/errors/auth.d.ts +27 -0
  24. package/dist/errors/auth.d.ts.map +1 -0
  25. package/dist/errors/auth.js +8 -0
  26. package/dist/errors/auth.js.map +1 -0
  27. package/dist/errors/index.d.ts +30 -0
  28. package/dist/errors/index.d.ts.map +1 -0
  29. package/dist/errors/index.js +13 -0
  30. package/dist/errors/index.js.map +1 -0
  31. package/dist/errors/mapFault.d.ts +31 -0
  32. package/dist/errors/mapFault.d.ts.map +1 -0
  33. package/dist/errors/mapFault.js +48 -0
  34. package/dist/errors/mapFault.js.map +1 -0
  35. package/dist/errors/protocol.d.ts +22 -0
  36. package/dist/errors/protocol.d.ts.map +1 -0
  37. package/dist/errors/protocol.js +9 -0
  38. package/dist/errors/protocol.js.map +1 -0
  39. package/dist/errors/schema.d.ts +22 -0
  40. package/dist/errors/schema.d.ts.map +1 -0
  41. package/dist/errors/schema.js +14 -0
  42. package/dist/errors/schema.js.map +1 -0
  43. package/dist/errors/server.d.ts +109 -0
  44. package/dist/errors/server.d.ts.map +1 -0
  45. package/dist/errors/server.js +49 -0
  46. package/dist/errors/server.js.map +1 -0
  47. package/dist/errors/session.d.ts +16 -0
  48. package/dist/errors/session.d.ts.map +1 -0
  49. package/dist/errors/session.js +11 -0
  50. package/dist/errors/session.js.map +1 -0
  51. package/dist/errors/transport.d.ts +42 -0
  52. package/dist/errors/transport.d.ts.map +1 -0
  53. package/dist/errors/transport.js +49 -0
  54. package/dist/errors/transport.js.map +1 -0
  55. package/dist/index.d.ts +33 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +57 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/internal/platform.d.ts +12 -0
  60. package/dist/internal/platform.d.ts.map +1 -0
  61. package/dist/internal/platform.js +11 -0
  62. package/dist/internal/platform.js.map +1 -0
  63. package/dist/internal/singleFlight.d.ts +29 -0
  64. package/dist/internal/singleFlight.d.ts.map +1 -0
  65. package/dist/internal/singleFlight.js +41 -0
  66. package/dist/internal/singleFlight.js.map +1 -0
  67. package/dist/profiles.d.ts +179 -0
  68. package/dist/profiles.d.ts.map +1 -0
  69. package/dist/profiles.js +191 -0
  70. package/dist/profiles.js.map +1 -0
  71. package/dist/protocol/jsonrpc.d.ts +127 -0
  72. package/dist/protocol/jsonrpc.d.ts.map +1 -0
  73. package/dist/protocol/jsonrpc.js +117 -0
  74. package/dist/protocol/jsonrpc.js.map +1 -0
  75. package/dist/records/index.d.ts +14 -0
  76. package/dist/records/index.d.ts.map +1 -0
  77. package/dist/records/index.js +14 -0
  78. package/dist/records/index.js.map +1 -0
  79. package/dist/records/recordModel.d.ts +131 -0
  80. package/dist/records/recordModel.d.ts.map +1 -0
  81. package/dist/records/recordModel.js +101 -0
  82. package/dist/records/recordModel.js.map +1 -0
  83. package/dist/records/related.d.ts +42 -0
  84. package/dist/records/related.d.ts.map +1 -0
  85. package/dist/records/related.js +51 -0
  86. package/dist/records/related.js.map +1 -0
  87. package/dist/records/relations.d.ts +66 -0
  88. package/dist/records/relations.d.ts.map +1 -0
  89. package/dist/records/relations.js +119 -0
  90. package/dist/records/relations.js.map +1 -0
  91. package/dist/records/spec.d.ts +44 -0
  92. package/dist/records/spec.d.ts.map +1 -0
  93. package/dist/records/spec.js +63 -0
  94. package/dist/records/spec.js.map +1 -0
  95. package/dist/records/typed.d.ts +71 -0
  96. package/dist/records/typed.d.ts.map +1 -0
  97. package/dist/records/typed.js +65 -0
  98. package/dist/records/typed.js.map +1 -0
  99. package/dist/rpc.d.ts +90 -0
  100. package/dist/rpc.d.ts.map +1 -0
  101. package/dist/rpc.js +113 -0
  102. package/dist/rpc.js.map +1 -0
  103. package/dist/services/db.d.ts +128 -0
  104. package/dist/services/db.d.ts.map +1 -0
  105. package/dist/services/db.js +132 -0
  106. package/dist/services/db.js.map +1 -0
  107. package/dist/services/report.d.ts +48 -0
  108. package/dist/services/report.d.ts.map +1 -0
  109. package/dist/services/report.js +103 -0
  110. package/dist/services/report.js.map +1 -0
  111. package/dist/session/cookie.d.ts +114 -0
  112. package/dist/session/cookie.d.ts.map +1 -0
  113. package/dist/session/cookie.js +231 -0
  114. package/dist/session/cookie.js.map +1 -0
  115. package/dist/testing/fakeTransport.d.ts +40 -0
  116. package/dist/testing/fakeTransport.d.ts.map +1 -0
  117. package/dist/testing/fakeTransport.js +36 -0
  118. package/dist/testing/fakeTransport.js.map +1 -0
  119. package/dist/testing/index.d.ts +9 -0
  120. package/dist/testing/index.d.ts.map +1 -0
  121. package/dist/testing/index.js +9 -0
  122. package/dist/testing/index.js.map +1 -0
  123. package/dist/transport.d.ts +62 -0
  124. package/dist/transport.d.ts.map +1 -0
  125. package/dist/transport.js +9 -0
  126. package/dist/transport.js.map +1 -0
  127. package/dist/transports/json2.d.ts +41 -0
  128. package/dist/transports/json2.d.ts.map +1 -0
  129. package/dist/transports/json2.js +196 -0
  130. package/dist/transports/json2.js.map +1 -0
  131. package/dist/transports/jsonrpc.d.ts +41 -0
  132. package/dist/transports/jsonrpc.d.ts.map +1 -0
  133. package/dist/transports/jsonrpc.js +119 -0
  134. package/dist/transports/jsonrpc.js.map +1 -0
  135. package/dist/transports/web.d.ts +15 -0
  136. package/dist/transports/web.d.ts.map +1 -0
  137. package/dist/transports/web.js +62 -0
  138. package/dist/transports/web.js.map +1 -0
  139. package/dist/version-live.d.ts +41 -0
  140. package/dist/version-live.d.ts.map +1 -0
  141. package/dist/version-live.js +26 -0
  142. package/dist/version-live.js.map +1 -0
  143. package/dist/version.d.ts +78 -0
  144. package/dist/version.d.ts.map +1 -0
  145. package/dist/version.js +46 -0
  146. package/dist/version.js.map +1 -0
  147. package/docs/protocol-verification.md +43 -0
  148. package/package.json +71 -0
  149. package/src/client.ts +731 -0
  150. package/src/combinators/retryOnSessionExpired.ts +25 -0
  151. package/src/commands.ts +112 -0
  152. package/src/config.ts +110 -0
  153. package/src/domain.ts +91 -0
  154. package/src/errors/auth.ts +28 -0
  155. package/src/errors/index.ts +50 -0
  156. package/src/errors/mapFault.ts +92 -0
  157. package/src/errors/protocol.ts +19 -0
  158. package/src/errors/schema.ts +18 -0
  159. package/src/errors/server.ts +79 -0
  160. package/src/errors/session.ts +12 -0
  161. package/src/errors/transport.ts +74 -0
  162. package/src/index.ts +152 -0
  163. package/src/internal/platform.ts +22 -0
  164. package/src/internal/singleFlight.ts +80 -0
  165. package/src/profiles.ts +386 -0
  166. package/src/protocol/jsonrpc.ts +162 -0
  167. package/src/records/index.ts +49 -0
  168. package/src/records/recordModel.ts +211 -0
  169. package/src/records/related.ts +80 -0
  170. package/src/records/relations.ts +192 -0
  171. package/src/records/spec.ts +96 -0
  172. package/src/records/typed.ts +180 -0
  173. package/src/rpc.ts +219 -0
  174. package/src/services/db.ts +282 -0
  175. package/src/services/report.ts +165 -0
  176. package/src/session/cookie.ts +461 -0
  177. package/src/testing/fakeTransport.ts +84 -0
  178. package/src/testing/index.ts +9 -0
  179. package/src/transport.ts +72 -0
  180. package/src/transports/json2.ts +303 -0
  181. package/src/transports/jsonrpc.ts +218 -0
  182. package/src/transports/web.ts +96 -0
  183. package/src/version-live.ts +67 -0
  184. package/src/version.ts +102 -0
@@ -0,0 +1,96 @@
1
+ import { type Config, Effect, Layer, Schema } from "effect";
2
+ import { OdooConfig } from "../config.ts";
3
+ import { SchemaDriftError } from "../errors/schema.ts";
4
+ import { OdooTransportError, type RequestInfo } from "../errors/transport.ts";
5
+ import { HttpClientRequest } from "../internal/platform.ts";
6
+ import { CookieSession } from "../session/cookie.ts";
7
+ import { joinPath } from "../session/cookie.ts";
8
+ import {
9
+ buildRequest,
10
+ JsonRpcResponse,
11
+ mapJsonRpcError,
12
+ nextRequestId,
13
+ } from "../protocol/jsonrpc.ts";
14
+ import { type CallKwParams, Transport, type TransportCallError } from "../transport.ts";
15
+
16
+ /**
17
+ * Build the `WebTransport` implementation. Every `call_kw` first ensures a
18
+ * cookie session exists (via {@link CookieSession}) and then rides the shared,
19
+ * cookie-bound HttpClient so `session_id` — and any rotation — is always sent.
20
+ */
21
+ export const make = (
22
+ // Only `url` is read: the transport never touches credentials, so an
23
+ // injected-cookie consumer (CookieSessionLive.fromExisting) can drive it
24
+ // without fabricating an OdooConfig. A full OdooConfig still satisfies this.
25
+ config: Pick<OdooConfig, "url">,
26
+ ): Effect.Effect<typeof Transport.Service, never, CookieSession> =>
27
+ Effect.gen(function* () {
28
+ const session = yield* CookieSession;
29
+ const callUrl = joinPath(config.url, "web/dataset/call_kw");
30
+ const request: RequestInfo = { method: "POST", url: callUrl };
31
+
32
+ const callKw = (params: CallKwParams): Effect.Effect<unknown, TransportCallError> =>
33
+ Effect.gen(function* () {
34
+ // Choke point: ensure the session before the call. A SessionExpiredError
35
+ // from here does NOT auto-relogin (see retryOnSessionExpired).
36
+ yield* session.login;
37
+
38
+ // execute_kw-family route: an explicit `ids` browse target rides as the
39
+ // first positional argument (see CallKwParams.ids).
40
+ const args = params.ids ? [params.ids, ...params.args] : params.args;
41
+ const envelope = buildRequest(
42
+ {
43
+ model: params.model,
44
+ method: params.method,
45
+ args,
46
+ kwargs: params.kwargs,
47
+ },
48
+ nextRequestId(),
49
+ );
50
+
51
+ const response = yield* session.client
52
+ .execute(HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(callUrl), envelope))
53
+ .pipe(Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)));
54
+
55
+ const body = yield* response.json.pipe(
56
+ Effect.mapError((cause) => OdooTransportError.fromHttpClientError(request, cause)),
57
+ );
58
+
59
+ const decoded = yield* Schema.decodeUnknownEffect(JsonRpcResponse)(body).pipe(
60
+ Effect.mapError(
61
+ (cause) =>
62
+ new SchemaDriftError({
63
+ context: "web/dataset/call_kw envelope",
64
+ payload: body,
65
+ cause,
66
+ }),
67
+ ),
68
+ );
69
+
70
+ if ("error" in decoded) {
71
+ // Single mapping choke point: code 100 → SessionExpiredError, else
72
+ // mapServerFault, with the call site threaded through.
73
+ return yield* Effect.fail(
74
+ mapJsonRpcError(decoded.error, { model: params.model, method: params.method }),
75
+ );
76
+ }
77
+
78
+ return decoded.result;
79
+ }).pipe(
80
+ // No args/secrets in span attributes — model + method only.
81
+ Effect.withSpan("odoo.web.call_kw", {
82
+ attributes: { model: params.model, method: params.method },
83
+ }),
84
+ );
85
+
86
+ return { dialect: "execute-kw" as const, callKw };
87
+ });
88
+
89
+ /** Provide the web `Transport` from an already-resolved config. Requires `CookieSession`. */
90
+ export const layer = (
91
+ config: Pick<OdooConfig, "url">,
92
+ ): Layer.Layer<Transport, never, CookieSession> => Layer.effect(Transport, make(config));
93
+
94
+ /** Provide the web `Transport`, resolving `OdooConfig` from the environment. */
95
+ export const layerConfig: Layer.Layer<Transport, Config.ConfigError, CookieSession> =
96
+ Layer.effect(Transport, Effect.flatMap(OdooConfig, make));
@@ -0,0 +1,67 @@
1
+ import { Effect, Layer } from "effect";
2
+ import type { SchemaDriftError } from "./errors/schema.ts";
3
+ import type { ProtocolUnsupportedError } from "./errors/protocol.ts";
4
+ import type { OdooTransportError } from "./errors/transport.ts";
5
+ import * as SingleFlight from "./internal/singleFlight.ts";
6
+ import {
7
+ type CommonVersionResponse,
8
+ deriveWireCapabilities,
9
+ parseVersionInfo,
10
+ type ResolvedVersion,
11
+ type ServerVersionInfo,
12
+ VersionResolver,
13
+ } from "./version.ts";
14
+
15
+ /**
16
+ * The two version-probe shapes we accept, already boundary-decoded by whichever
17
+ * transport family issued the probe:
18
+ *
19
+ * - {@link CommonVersionResponse} — the `/jsonrpc` `common.version` response.
20
+ * - `{ version, version_info }` — the web `/web/webclient/version_info` shape.
21
+ *
22
+ * Both carry the `server_version_info` tuple under different keys; that tuple is
23
+ * all this resolver needs.
24
+ */
25
+ export type VersionProbeResponse =
26
+ | CommonVersionResponse
27
+ | { readonly version: string; readonly version_info: ServerVersionInfo };
28
+
29
+ /**
30
+ * The typed failures a transport-provided VERSION probe may surface. The
31
+ * `VersionProbe` prefix is deliberate: the bare word "probe" belongs to the
32
+ * application layer's product-capability probing (`fields_get` reachability);
33
+ * this package only ever probes the server version.
34
+ */
35
+ export type VersionProbeError = OdooTransportError | SchemaDriftError | ProtocolUnsupportedError;
36
+
37
+ const extractInfo = (response: VersionProbeResponse): ServerVersionInfo =>
38
+ "server_version_info" in response ? response.server_version_info : response.version_info;
39
+
40
+ const resolveVersion = (response: VersionProbeResponse): ResolvedVersion => {
41
+ const version = parseVersionInfo(extractInfo(response));
42
+ return { version, capabilities: deriveWireCapabilities(version) };
43
+ };
44
+
45
+ /**
46
+ * Build a {@link VersionResolver} service from a transport-agnostic `probe`.
47
+ *
48
+ * The `Transport` seam only exposes `call_kw`, so version discovery lives with
49
+ * each transport family (which owns the right endpoint and decode). This
50
+ * resolver stays generic: hand it the decoded probe effect and it success-only
51
+ * single-flight caches the mapped {@link ResolvedVersion}.
52
+ */
53
+ export const make = (
54
+ probe: Effect.Effect<VersionProbeResponse, VersionProbeError>,
55
+ ): Effect.Effect<typeof VersionResolver.Service, never> =>
56
+ Effect.gen(function* () {
57
+ const cache = yield* SingleFlight.make(Effect.map(probe, resolveVersion));
58
+ return { resolve: cache.get };
59
+ });
60
+
61
+ /**
62
+ * The {@link VersionResolver} layer over a transport-provided `probe`. Transport
63
+ * packages construct the probe (endpoint + decode) and pass it here.
64
+ */
65
+ export const layer = (
66
+ probe: Effect.Effect<VersionProbeResponse, VersionProbeError>,
67
+ ): Layer.Layer<VersionResolver> => Layer.effect(VersionResolver, make(probe));
package/src/version.ts ADDED
@@ -0,0 +1,102 @@
1
+ import { Context, type Effect, Schema } from "effect";
2
+ import type { ProtocolUnsupportedError } from "./errors/protocol.ts";
3
+ import type { SchemaDriftError } from "./errors/schema.ts";
4
+ import type { OdooTransportError } from "./errors/transport.ts";
5
+
6
+ /**
7
+ * Odoo's `server_version_info`: `[major, minor, micro, releaselevel, serial]`,
8
+ * a heterogeneous tuple (numbers plus a string release level). Modeled
9
+ * leniently as a mixed array so a shape change on a future version surfaces as
10
+ * a decode drift rather than a silent truncation.
11
+ */
12
+ export const ServerVersionInfo = Schema.Array(Schema.Union([Schema.Number, Schema.String]));
13
+ export type ServerVersionInfo = typeof ServerVersionInfo.Type;
14
+
15
+ /**
16
+ * The response of the `common.version` call (`/jsonrpc` service `common`,
17
+ * method `version`). Decoded at the boundary; drift raises `SchemaDriftError`.
18
+ */
19
+ export const CommonVersionResponse = Schema.Struct({
20
+ server_version: Schema.String,
21
+ server_version_info: ServerVersionInfo,
22
+ server_serie: Schema.String,
23
+ protocol_version: Schema.Number,
24
+ });
25
+ export type CommonVersionResponse = typeof CommonVersionResponse.Type;
26
+
27
+ /** A parsed, structured server version. */
28
+ export interface OdooVersion {
29
+ readonly major: number;
30
+ readonly minor: number;
31
+ readonly micro: number;
32
+ readonly releaseLevel: string;
33
+ readonly serial: number;
34
+ readonly raw: ServerVersionInfo;
35
+ }
36
+
37
+ /**
38
+ * What a given server version can and cannot do, ON THE WIRE. This package
39
+ * owns WIRE capabilities derived from the server version; the application
40
+ * layer owns PRODUCT capabilities (derived from `fields_get` reachability,
41
+ * installed modules, …) — the bare words "capabilities"/"probe" belong to the
42
+ * app layer, hence the `Wire`/`VersionProbe` prefixes here.
43
+ */
44
+ export interface WireCapabilities {
45
+ /** The JSON-2 `/json/2` bearer API exists (Odoo 19+). */
46
+ readonly supportsJson2: boolean;
47
+ /** `/jsonrpc` and `/xmlrpc` are deprecated upstream (Odoo 19+). */
48
+ readonly jsonRpcDeprecated: boolean;
49
+ /**
50
+ * `web_read`/`web_search_read`/`web_save` accept a nested `specification`
51
+ * kwarg (Odoo 17+). This gates the declared-prefetch typed record path
52
+ * (`searchTyped`/`readTyped`/`saveTyped`); on 16 the kwarg is rejected, so a
53
+ * relation-free declared model degrades to `search_read`/`read` and a declared
54
+ * relation fails with `ProtocolUnsupportedError` before any round trip.
55
+ */
56
+ readonly supportsWebReadSpec: boolean;
57
+ }
58
+
59
+ /** A resolved version together with its derived wire capabilities. */
60
+ export interface ResolvedVersion {
61
+ readonly version: OdooVersion;
62
+ readonly capabilities: WireCapabilities;
63
+ }
64
+
65
+ const asNumber = (value: number | string | undefined): number =>
66
+ typeof value === "number" ? value : 0;
67
+
68
+ /**
69
+ * Turn a raw `server_version_info` tuple into a structured {@link OdooVersion}.
70
+ * Missing trailing elements default to `0` / empty; the raw tuple is retained.
71
+ */
72
+ export const parseVersionInfo = (raw: ServerVersionInfo): OdooVersion => ({
73
+ major: asNumber(raw[0]),
74
+ minor: asNumber(raw[1]),
75
+ micro: asNumber(raw[2]),
76
+ releaseLevel: typeof raw[3] === "string" ? raw[3] : "",
77
+ serial: asNumber(raw[4]),
78
+ raw,
79
+ });
80
+
81
+ /** Derive wire capabilities from a version. JSON-2 lands, and the legacy RPC
82
+ * routes are deprecated, at major 19. */
83
+ export const deriveWireCapabilities = (version: OdooVersion): WireCapabilities => ({
84
+ supportsJson2: version.major >= 19,
85
+ jsonRpcDeprecated: version.major >= 19,
86
+ supportsWebReadSpec: version.major >= 17,
87
+ });
88
+
89
+ /**
90
+ * Resolves (and success-only caches) the server version behind whichever
91
+ * transport is provided. Implementation ships later; this is the seam so the
92
+ * client can gate protocol choices on capabilities.
93
+ */
94
+ export class VersionResolver extends Context.Service<
95
+ VersionResolver,
96
+ {
97
+ readonly resolve: Effect.Effect<
98
+ ResolvedVersion,
99
+ OdooTransportError | SchemaDriftError | ProtocolUnsupportedError
100
+ >;
101
+ }
102
+ >()("odoo-rpc-ts/VersionResolver") {}