@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hodei Navarro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,401 @@
1
+ # @hodeinavarro/odoo-rpc-ts
2
+
3
+ Effect-native, strongly-typed Odoo RPC client for TypeScript. **Odoo 16–19**,
4
+ three wire protocols behind one seam — JSON-RPC, the new JSON-2 API (19+),
5
+ and the cookie-session web route — with a tagged-error taxonomy you can
6
+ `catchTag` instead of parsing messages.
7
+
8
+ > **Status:** public and pre-1.0. Releases are published to the public npm
9
+ > registry with provenance.
10
+
11
+ ## Scope
12
+
13
+ This package owns generic Odoo transport, authentication/session, schema
14
+ decoding, typed records, connection profiles, and a small set of reusable
15
+ services. It does not own application-specific models, UI, persistence,
16
+ workflow policy, or automatic protocol selection. XML-RPC and generated
17
+ per-model clients are deliberately out of scope.
18
+
19
+ Install it from the public npm registry with:
20
+
21
+ ```
22
+ pnpm add @hodeinavarro/odoo-rpc-ts effect@4.0.0-beta.93
23
+ pnpm add @effect/platform-node@4.0.0-beta.93 # or run in the browser with FetchHttpClient
24
+ ```
25
+
26
+ Effect 4 is in beta and moves between betas — pin it **exact** (no caret) and
27
+ upgrade deliberately.
28
+
29
+ ## Quick start
30
+
31
+ Set `ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, and `ODOO_API_KEY` (or
32
+ `ODOO_PASSWORD`), then:
33
+
34
+ ```ts
35
+ import { Effect, Layer, Schema } from "effect";
36
+ import { NodeHttpClient } from "@effect/platform-node";
37
+ import { JsonRpcTransport, OdooClient, OdooClientLive, RpcLive } from "@hodeinavarro/odoo-rpc-ts";
38
+
39
+ // One layer graph: client → rpc → transport → your HTTP runtime.
40
+ const OdooLive = OdooClientLive.layer.pipe(
41
+ Layer.provideMerge(RpcLive.layer),
42
+ Layer.provide(JsonRpcTransport.layerConfig),
43
+ Layer.provide(NodeHttpClient.layerUndici),
44
+ );
45
+
46
+ // Rows decode through a schema — drift fails loudly, never a silent cast.
47
+ const Partner = Schema.Struct({
48
+ id: Schema.Number,
49
+ name: Schema.String,
50
+ email: Schema.Union([Schema.String, Schema.Literal(false)]), // Odoo sends false, not null
51
+ });
52
+
53
+ const companies = Effect.gen(function* () {
54
+ const odoo = yield* OdooClient;
55
+ return yield* odoo.searchRead(
56
+ "res.partner",
57
+ { domain: [["is_company", "=", true]], fields: ["id", "name", "email"], limit: 10 },
58
+ Partner,
59
+ );
60
+ });
61
+
62
+ await companies.pipe(Effect.provide(OdooLive), Effect.runPromise);
63
+ ```
64
+
65
+ `OdooClient` gives you `searchRead`, `search`, `read`, `create`, `write`,
66
+ `unlink`, `fieldsGet`, `searchCount`, `readGroup`, `nameSearch`, `ref`
67
+ (resolve an XML id to `[model, id]`), and a generic `call` escape hatch —
68
+ every op works over every transport. `Rpc.callKw` sits underneath for
69
+ anything else. `RpcLive.layer` seeds the user's server-side context
70
+ (`lang`, `tz` — via `res.users.context_get`, lazily on the first call) as
71
+ the base tier of every call, so translated fields come back in the user's
72
+ language; use `RpcLive.layerWith()` to opt out of seeding.
73
+
74
+ ## Everyday workflows
75
+
76
+ **Find or create:**
77
+
78
+ ```ts
79
+ const findOrCreatePartner = (email: string, name: string) =>
80
+ Effect.gen(function* () {
81
+ const odoo = yield* OdooClient;
82
+ const existing = yield* odoo.search("res.partner", {
83
+ domain: [["email", "=", email]],
84
+ limit: 1,
85
+ });
86
+ if (existing.length > 0) return existing[0]!;
87
+ const [id] = yield* odoo.create("res.partner", { name, email });
88
+ return id!;
89
+ });
90
+ ```
91
+
92
+ **Bulk update with domain combinators** (`AND` / `OR` / `NOT`):
93
+
94
+ ```ts
95
+ import { AND, OR } from "@hodeinavarro/odoo-rpc-ts";
96
+
97
+ const archiveStaleLeads = Effect.gen(function* () {
98
+ const odoo = yield* OdooClient;
99
+ const stale = yield* odoo.search("crm.lead", {
100
+ domain: AND(
101
+ [["active", "=", true]],
102
+ OR([["probability", "=", 0]], [["write_date", "<", "2025-01-01"]]),
103
+ ),
104
+ });
105
+ if (stale.length === 0) return 0;
106
+ yield* odoo.write("crm.lead", stale, { active: false });
107
+ return stale.length;
108
+ });
109
+ ```
110
+
111
+ **Paginate a big model:**
112
+
113
+ ```ts
114
+ const allPartnerIds = Effect.gen(function* () {
115
+ const odoo = yield* OdooClient;
116
+ const out: number[] = [];
117
+ for (let offset = 0; ; offset += 500) {
118
+ const page = yield* odoo.search("res.partner", { limit: 500, offset });
119
+ out.push(...page);
120
+ if (page.length < 500) return out;
121
+ }
122
+ });
123
+ ```
124
+
125
+ ## Typed records and relations
126
+
127
+ Two per-protocol tiers — dialects of the same design, not competitors. Both
128
+ make N+1 storms structurally impossible, and every RPC stays visible. The
129
+ **spec tier** speaks the `specification` protocol Odoo grew in 17; the
130
+ **classic tier** speaks the classic `[id, name]`-pair protocol every supported
131
+ version (16+) understands. Pick by the oldest server you must reach.
132
+
133
+ **Spec tier — declared prefetch** (Odoo 17+ `specification` protocol) —
134
+ declare the graph, get ONE `web_search_read`:
135
+
136
+ ```ts
137
+ import { defineRecord, Many2One, OdooClient, OdooDateTime } from "@hodeinavarro/odoo-rpc-ts"
138
+ import { Schema } from "effect"
139
+
140
+ const Company = defineRecord("res.company", { name: Schema.String })
141
+ const Partner = defineRecord("res.partner", {
142
+ name: Schema.String,
143
+ create_date: OdooDateTime, // parsed as UTC, never localized
144
+ company_id: Many2One(Company), // row.company_id?.name — already fetched
145
+ })
146
+
147
+ const rows = Effect.gen(function* () {
148
+ const odoo = yield* OdooClient
149
+ return yield* odoo.searchTyped(Partner, { domain: [["is_company", "=", true]], limit: 10 })
150
+ })
151
+ ```
152
+
153
+ The version gate is explicit: the spec tier's relations REQUIRE Odoo 17+.
154
+ Provide a `VersionResolver` layer and the gate holds automatically — a
155
+ declared relation on Odoo 16 fails with `ProtocolUnsupportedError` before any
156
+ round trip, while relation-free records degrade to plain `search_read`.
157
+ `saveTyped` (17+) writes and returns the fresh nested snapshot in one call.
158
+ For anything that must also run on 16, use the classic tier below.
159
+
160
+ **Classic tier — explicit traversal** (all versions, 16+) — rows keep the
161
+ classic `[id, name]` refs; batch the hop when you need it, exactly one deduped
162
+ `read`:
163
+
164
+ ```ts
165
+ import { Many2OneRefOrNull, OdooClient } from "@hodeinavarro/odoo-rpc-ts"
166
+
167
+ const PartnerRow = Schema.Struct({
168
+ id: Schema.Number,
169
+ name: Schema.String,
170
+ company_id: Many2OneRefOrNull, // Odoo's [id, name] pair; false -> null
171
+ })
172
+ const CompanyRow = Schema.Struct({ id: Schema.Number, name: Schema.String })
173
+
174
+ const pairs = Effect.gen(function* () {
175
+ const odoo = yield* OdooClient
176
+ const rows = yield* odoo.searchRecordsTyped("res.partner", {}, PartnerRow)
177
+ return yield* rows.joinRelated("company_id", "res.company", CompanyRow) // ONE read
178
+ })
179
+ ```
180
+
181
+ ## x2many writes without magic tuples
182
+
183
+ ```ts
184
+ import { Command } from "@hodeinavarro/odoo-rpc-ts"
185
+
186
+ yield* odoo.write("res.partner", [companyId], {
187
+ child_ids: [Command.create({ name: "New contact" }), Command.link(existingId)],
188
+ })
189
+ ```
190
+
191
+ ## Services: databases, reports, profiles
192
+
193
+ ```ts
194
+ import { DbService, ReportService } from "@hodeinavarro/odoo-rpc-ts"
195
+
196
+ // database administration (master-password gated; Node-oriented)
197
+ const names = yield* DbService.listDatabases(url)
198
+ yield* DbService.duplicate(url, master, "prod", "staging")
199
+
200
+ // report downloads over the cookie session — works on ALL of 16-19
201
+ const pdf = yield* ReportService.download(session, {
202
+ reportName: "base.report_irmodeloverview",
203
+ ids: [modelId],
204
+ })
205
+ ```
206
+
207
+ Connection profiles never serialize secrets: `ProfileData` holds the metadata,
208
+ secrets cross only as `Redacted` through a `SecretStore` you implement (OS
209
+ keyring in Node, IndexedDB in the browser); in-memory layers ship for tests.
210
+ Besides api-key/password presets, `saveSessionProfile(name, url, sessionId)`
211
+ stores a harvested `session_id` cookie (the TOTP/SSO flow — no credentials
212
+ exist); `loadProfile` returns a tagged union, and its `"session"` arm feeds
213
+ `CookieSessionLive.fromExisting` directly instead of fabricating a config.
214
+
215
+ ## Bring your own HTTP layer
216
+
217
+ The library depends only on the abstract `HttpClient` tag — timeouts, proxies,
218
+ custom CAs, and retry policy are yours:
219
+
220
+ ```ts
221
+ import { FetchHttpClient, HttpClient } from "effect/unstable/http"
222
+ import { Layer } from "effect"
223
+
224
+ const TunedHttp = Layer.effect(
225
+ HttpClient.HttpClient,
226
+ Effect.map(HttpClient.HttpClient, (client) => client.pipe(HttpClient.retryTransient({ times: 3 }))),
227
+ ).pipe(Layer.provide(FetchHttpClient.layer))
228
+ ```
229
+
230
+ ## Errors are a typed API
231
+
232
+ Every failure is a tagged error carrying the raw fault, the Python exception
233
+ name, and the call site — normalized identically across all three protocols:
234
+
235
+ ```ts
236
+ const created = Effect.gen(function* () {
237
+ const odoo = yield* OdooClient;
238
+ return yield* odoo.create("res.partner", { name: "Ada" });
239
+ }).pipe(
240
+ Effect.catchTags({
241
+ OdooValidationError: (e) => Effect.die(`rejected by a constraint: ${e.message}`),
242
+ OdooAccessError: () => Effect.succeed([]), // degrade gracefully
243
+ OdooAuthenticationError: (e) => Effect.die(e.reason), // "invalid-credentials" | "mfa-pending" | …
244
+ }),
245
+ );
246
+ ```
247
+
248
+ The full union: `OdooTransportError` · `OdooAuthenticationError` ·
249
+ `SessionExpiredError` · `SchemaDriftError` · `ProtocolUnsupportedError` ·
250
+ `OdooServerError` (+ `OdooAccessError`, `OdooValidationError`,
251
+ `OdooMissingError`, `OdooUserError`, `OdooLockError`). Unknown server faults
252
+ are preserved on `OdooServerError` with the raw name — never swallowed.
253
+ `OdooTransportError` retains only a sanitized `{ method, url }` request,
254
+ failure `kind`, safe message, and optional HTTP status; it never exposes the
255
+ platform request, response, headers, body, or cause.
256
+
257
+ ## Pick your transport
258
+
259
+ Same application code, different layer — always your explicit choice:
260
+
261
+ ```ts
262
+ // Server-to-server, stateless: /jsonrpc + execute_kw (Odoo 16–19)
263
+ Layer.provide(JsonRpcTransport.layerConfig);
264
+
265
+ // Odoo 19+: /json/2, bearer API key, real HTTP status codes
266
+ Layer.provide(Json2Transport.layerConfig);
267
+
268
+ // Browser/long-lived: cookie session over /web/dataset/call_kw
269
+ Layer.provide(WebTransport.layerConfig).pipe(Layer.provideMerge(CookieSessionLive.layerConfig));
270
+ ```
271
+
272
+ Cookie sessions expire; recovery is opt-in, never magic:
273
+
274
+ ```ts
275
+ import { CookieSession, retryOnSessionExpired } from "@hodeinavarro/odoo-rpc-ts";
276
+
277
+ const resilientCount = Effect.gen(function* () {
278
+ const session = yield* CookieSession;
279
+ const odoo = yield* OdooClient;
280
+ return yield* retryOnSessionExpired(odoo.searchCount("res.partner", []), session);
281
+ });
282
+ ```
283
+
284
+ Already hold a `session_id` minted elsewhere — e.g. harvested from an
285
+ embedded browser window after the user completed the real `/web/login` page
286
+ (the only stock flow for TOTP/SSO accounts)? Adopt it directly; the client
287
+ never holds credentials:
288
+
289
+ ```ts
290
+ import { Redacted } from "effect";
291
+ import { CookieSessionLive, WebTransport } from "@hodeinavarro/odoo-rpc-ts";
292
+
293
+ const url = new URL("https://erp.example.com");
294
+
295
+ const HarvestedOdoo = OdooClientLive.layer.pipe(
296
+ Layer.provideMerge(RpcLive.layer),
297
+ Layer.provide(WebTransport.layer({ url })), // no credentials needed
298
+ Layer.provide(
299
+ CookieSessionLive.layerFromExisting({
300
+ url,
301
+ sessionId: Redacted.make(harvestedCookieValue),
302
+ // Optional renew hook: mint a fresh cookie after an invalidate (e.g.
303
+ // reopen the login window). Without it the session cannot recover —
304
+ // once the server kills it, calls (and the one relogin attempted by
305
+ // retryOnSessionExpired) fail with SessionExpiredError, and your shell
306
+ // constructs a new session from a fresh cookie.
307
+ // renew: driveLoginWindow,
308
+ }),
309
+ ),
310
+ Layer.provide(NodeHttpClient.layerUndici),
311
+ );
312
+ ```
313
+
314
+ The injected session hydrates honestly on first use via
315
+ `POST /web/session/get_session_info`, so `login` still yields the real
316
+ `uid`/context/version — and a dead cookie surfaces immediately as
317
+ `SessionExpiredError`.
318
+
319
+ Notes: API keys work as the password on every route (and are _required_ for
320
+ accounts with 2FA). JSON-2 is bearer-only and keyword-only — the client
321
+ handles the encoding differences for you via the transport's `dialect`.
322
+
323
+ ## Testing without a server
324
+
325
+ `@hodeinavarro/odoo-rpc-ts/testing` ships a deterministic `FakeTransport` with
326
+ a call log:
327
+
328
+ ```ts
329
+ import * as FakeTransport from "@hodeinavarro/odoo-rpc-ts/testing";
330
+
331
+ const fake = FakeTransport.make({
332
+ "res.partner": {
333
+ search_read: () => [{ id: 1, name: "Alice", email: "alice@example.com" }],
334
+ create: (params) => (params.args[0] as unknown[]).map((_, i) => 100 + i),
335
+ },
336
+ });
337
+
338
+ const TestLayer = OdooClientLive.layer.pipe(
339
+ Layer.provideMerge(RpcLive.layer),
340
+ Layer.provide(fake.layer),
341
+ );
342
+ ```
343
+
344
+ Config is `effect/Config`, so tests can inject values without touching env:
345
+
346
+ ```ts
347
+ import { ConfigProvider } from "effect";
348
+
349
+ const TestConfig = ConfigProvider.layer(
350
+ ConfigProvider.fromEnv({
351
+ env: {
352
+ ODOO_URL: "https://mycompany.odoo.com",
353
+ ODOO_DB: "mycompany",
354
+ ODOO_USERNAME: "integration@mycompany.com",
355
+ ODOO_API_KEY: "…",
356
+ },
357
+ }),
358
+ );
359
+ ```
360
+
361
+ Want the real thing? `pnpm harness up 16.0` (or 17/18/19) boots a disposable
362
+ seeded Odoo in Docker and `pnpm test:integration` runs the live suite against
363
+ it — see [harness/README.md](harness/README.md).
364
+
365
+ ## Support matrix
366
+
367
+ | | 16.0 | 17.0 | 18.0 | 19.0 |
368
+ | -------------------------- | ---- | ---- | ---- | ----------------------- |
369
+ | JSON-RPC `/jsonrpc` | ✓ | ✓ | ✓ | ✓ (deprecated upstream) |
370
+ | Web session + `call_kw` | ✓ | ✓ | ✓ | ✓ |
371
+ | JSON-2 `/json/2` (bearer) | – | – | – | ✓ |
372
+ | API key auth (as password) | ✓ | ✓ | ✓ | ✓ |
373
+
374
+ XML-RPC is deliberately out of scope — JSON-RPC reaches the same services on
375
+ every supported version with strictly richer errors.
376
+
377
+ ## Design, in one paragraph
378
+
379
+ Everything returns `Effect<A, OdooError, R>` — nothing throws. Services are
380
+ `Context.Service`s wired with `Layer`s; the HTTP runtime is yours (`effect` —
381
+ v4, incl. `effect/unstable/http` — is the only peer dependency, so it runs
382
+ wherever Effect does, browser included). Responses are schema-decoded at the boundary
383
+ (`SchemaDriftError` on drift), secrets stay `Redacted`, TLS is never
384
+ disabled, and no URL/db/credential is ever assumed. The wire behavior was
385
+ mapped from pinned Odoo 16–19 source snapshots and exercised against disposable
386
+ instances. See the reproducible
387
+ [protocol verification record](docs/protocol-verification.md) for the exact
388
+ upstream revisions and source paths; architectural decisions live in
389
+ [AGENTS.md](AGENTS.md).
390
+
391
+ Every example in this README is compile-checked in CI
392
+ ([test/readme-snippets.test-d.ts](test/readme-snippets.test-d.ts)).
393
+
394
+ ## Status
395
+
396
+ 🚧 Pre-1.0. Core is implemented, the unit suite covers the shared API, and CI
397
+ runs the integration suite against Odoo 16, 17, 18, and 19. The API may still
398
+ move while Effect 4 remains in beta.
399
+
400
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the local gates and
401
+ [SECURITY.md](SECURITY.md) for private vulnerability reporting.
@@ -0,0 +1,184 @@
1
+ import { Context, Effect, Layer, Schema } from "effect";
2
+ import type { Domain } from "./domain.js";
3
+ import { type HasId, type RecordSpec, type TypedRecordSet } from "./records/index.js";
4
+ import { type OdooContext, Rpc } from "./rpc.js";
5
+ import type { TransportCallError } from "./transport.js";
6
+ /** A decoded Odoo record with unmodeled field values. */
7
+ export type OdooRecord = {
8
+ readonly [field: string]: unknown;
9
+ };
10
+ /** `fields_get` metadata: field name → its attribute dict. */
11
+ export type FieldsMetadata = {
12
+ readonly [field: string]: OdooRecord;
13
+ };
14
+ /** Options for {@link OdooClient.call} — the undecoded model-method escape hatch. */
15
+ export interface CallOptions {
16
+ readonly ids?: ReadonlyArray<number>;
17
+ readonly context?: OdooContext;
18
+ readonly kwargs?: Record<string, unknown>;
19
+ }
20
+ /** Arguments for `read_group` (all Python-stable parameter names, 16–19). */
21
+ export interface ReadGroupOptions {
22
+ readonly domain: Domain;
23
+ readonly fields: ReadonlyArray<string>;
24
+ readonly groupby: ReadonlyArray<string>;
25
+ readonly limit?: number;
26
+ readonly offset?: number;
27
+ readonly orderby?: string;
28
+ readonly lazy?: boolean;
29
+ readonly context?: OdooContext;
30
+ }
31
+ /** Arguments for `name_search`. `args` is the optional search domain. */
32
+ export interface NameSearchOptions {
33
+ readonly name?: string;
34
+ readonly args?: Domain;
35
+ readonly operator?: string;
36
+ readonly limit?: number;
37
+ readonly context?: OdooContext;
38
+ }
39
+ /** Options shared by the record-listing ops. `context` merges via {@link Rpc}. */
40
+ export interface SearchOptions {
41
+ readonly domain?: Domain;
42
+ readonly limit?: number;
43
+ readonly offset?: number;
44
+ readonly order?: string;
45
+ readonly context?: OdooContext;
46
+ }
47
+ export interface SearchReadOptions extends SearchOptions {
48
+ readonly fields?: ReadonlyArray<string>;
49
+ }
50
+ /**
51
+ * Options for the declared-prefetch typed reads ({@link OdooClient.searchTyped}).
52
+ * `serverMajor` is the explicit version-gate override used when NO
53
+ * {@link VersionResolver} is in scope: it decides whether the `specification`
54
+ * path is taken (>= 17) or, for a relation-free spec, degrades to `search_read`
55
+ * (< 17). When a `VersionResolver` IS provided it always wins and `serverMajor`
56
+ * is ignored.
57
+ */
58
+ export interface TypedOptions extends SearchOptions {
59
+ readonly serverMajor?: number;
60
+ }
61
+ /** Options for {@link OdooClient.readTyped}/`saveTyped` — context + the same
62
+ * explicit `serverMajor` gate override as {@link TypedOptions}. */
63
+ export interface TypedReadOptions {
64
+ readonly context?: OdooContext;
65
+ readonly serverMajor?: number;
66
+ }
67
+ declare const OdooClient_base: Context.ServiceClass<OdooClient, "odoo-rpc-ts/OdooClient", {
68
+ /**
69
+ * `search_read(domain, fields, offset, limit, order)` — search and read in
70
+ * one round trip. Rows decode through `schema` (default: unknown record).
71
+ */
72
+ readonly searchRead: <A = OdooRecord, I = A>(model: string, options?: SearchReadOptions, schema?: Schema.Codec<A, I>) => Effect.Effect<ReadonlyArray<A>, TransportCallError>;
73
+ /**
74
+ * `search_read`, decoding rows through `schema`, into a {@link TypedRecordSet}
75
+ * — an immutable snapshot bound to `(rpc, model)` with explicit, batched
76
+ * relation traversal (`fetchRelated`/`joinRelated`). The schema MUST decode
77
+ * an `id: number` on every row (`A extends { id: number }`); that id keys all
78
+ * downstream joins. This is the classic-tier ("explicit traversal",
79
+ * `[id, name]`-pair protocol, works on 16+) entry
80
+ * point — one `search_read` here, then one `read` per relation you traverse.
81
+ */
82
+ readonly searchRecordsTyped: <A extends HasId, I = A>(model: string, options: SearchReadOptions | undefined, schema: Schema.Codec<A, I>) => Effect.Effect<TypedRecordSet<A>, TransportCallError>;
83
+ /**
84
+ * Spec-tier typed search (declared prefetch, 17+ `specification`
85
+ * protocol). Compiles `record`'s field
86
+ * graph into ONE `web_search_read` `specification` call and strict-decodes
87
+ * the nested payload — a declared many2one comes back as a `{ id, ... }` dict
88
+ * (or `null`), an x2many as a nested list — with NO per-relation round trip.
89
+ *
90
+ * VERSION GATE (before any round trip). If a {@link VersionResolver} is in
91
+ * scope it decides; else `options.serverMajor` does; else the spec path is
92
+ * assumed (17+). A declared relation on a < 17 server fails with
93
+ * {@link ProtocolUnsupportedError}; a relation-free declared model degrades to
94
+ * a plain `search_read` (one trip, identical decode).
95
+ */
96
+ readonly searchTyped: <A extends HasId, I = A>(record: RecordSpec<A, I>, options?: TypedOptions) => Effect.Effect<ReadonlyArray<A>, TransportCallError>;
97
+ /**
98
+ * Declared-prefetch typed read by ids via `web_read` (17+). Same nested
99
+ * decode and version gate as {@link searchTyped}; degrades to plain `read`
100
+ * for a relation-free spec on < 17.
101
+ */
102
+ readonly readTyped: <A extends HasId, I = A>(record: RecordSpec<A, I>, ids: ReadonlyArray<number>, options?: TypedReadOptions) => Effect.Effect<ReadonlyArray<A>, TransportCallError>;
103
+ /**
104
+ * Write `values` to `ids` and return the FRESH nested snapshot via
105
+ * `web_save` (17+ ONLY — no degrade, even for a relation-free spec). One
106
+ * explicit call: it writes, then re-reads through `record`'s compiled
107
+ * `specification`. On < 17 fails with {@link ProtocolUnsupportedError}.
108
+ */
109
+ readonly saveTyped: <A extends HasId, I = A>(record: RecordSpec<A, I>, ids: ReadonlyArray<number>, values: OdooRecord, options?: TypedReadOptions) => Effect.Effect<ReadonlyArray<A>, TransportCallError>;
110
+ /** `search(domain, offset, limit, order)` — matching record ids. */
111
+ readonly search: (model: string, options?: SearchOptions) => Effect.Effect<ReadonlyArray<number>, TransportCallError>;
112
+ /** `read(ids, fields)` — read fields of the given records. */
113
+ readonly read: (model: string, ids: ReadonlyArray<number>, fields?: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<OdooRecord>, TransportCallError>;
114
+ /**
115
+ * `create(vals_list)` — create one or many records, always returning the new
116
+ * ids as an array (even for a single dict, which is normalized to a
117
+ * one-element list). Odoo 16–19 `model_create_multi` returns a list of ids
118
+ * for a list input, so the return contract is uniformly {@link ReadonlyArray}.
119
+ */
120
+ readonly create: (model: string, values: OdooRecord | ReadonlyArray<OdooRecord>) => Effect.Effect<ReadonlyArray<number>, TransportCallError>;
121
+ /** `write(ids, values)` — update records; Odoo returns `true`. */
122
+ readonly write: (model: string, ids: ReadonlyArray<number>, values: OdooRecord) => Effect.Effect<true, TransportCallError>;
123
+ /** `unlink(ids)` — delete records; Odoo returns `true`. */
124
+ readonly unlink: (model: string, ids: ReadonlyArray<number>) => Effect.Effect<true, TransportCallError>;
125
+ /** `fields_get(attributes=...)` — field metadata keyed by field name. */
126
+ readonly fieldsGet: (model: string, options?: {
127
+ readonly attributes?: ReadonlyArray<string>;
128
+ }) => Effect.Effect<FieldsMetadata, TransportCallError>;
129
+ /** `search_count(domain)` — number of records matching the domain. */
130
+ readonly searchCount: (model: string, domain: Domain) => Effect.Effect<number, TransportCallError>;
131
+ /**
132
+ * Resolve an external identifier (`module.name`) to its `(model, id)` pair,
133
+ * via `ir.model.data.check_object_reference`.
134
+ *
135
+ * We always use `check_object_reference` (present since Odoo 15) rather than
136
+ * branching to the legacy `xmlid_to_res_model_res_id` — this library
137
+ * supports 16–19 only, so the legacy branch is dead weight. A missing xml_id
138
+ * (the server raises a `ValueError`, mapped to `OdooServerError`) or an
139
+ * xml_id that resolves but is not visible to the caller (`[model, false]`)
140
+ * both fail with {@link OdooMissingError} naming the xml_id.
141
+ */
142
+ readonly ref: (xmlId: string) => Effect.Effect<readonly [model: string, id: number], TransportCallError>;
143
+ /**
144
+ * Escape hatch: call any `model.method` and return its raw, UNDECODED result.
145
+ * The browse `ids`, `context`, and `kwargs` ride the {@link Rpc} seam; no
146
+ * schema is applied, so the caller owns validating the shape. Prefer the
147
+ * typed ops above where one exists.
148
+ */
149
+ readonly call: (model: string, method: string, options?: CallOptions) => Effect.Effect<unknown, TransportCallError>;
150
+ /**
151
+ * `read_group(domain, fields, groupby, offset, limit, orderby, lazy)` —
152
+ * aggregate records into groups. Rows decode as unknown records (group keys,
153
+ * aggregates, and `__count`/`__domain` are model-dependent).
154
+ */
155
+ readonly readGroup: (model: string, options: ReadGroupOptions) => Effect.Effect<ReadonlyArray<OdooRecord>, TransportCallError>;
156
+ /**
157
+ * `name_search(name, domain, operator, limit)` → `(id, display_name)` pairs.
158
+ *
159
+ * WIRE FACT: the second parameter was renamed `args` → `domain` in Odoo 19.
160
+ * Over the execute_kw family we pass all four POSITIONALLY (position is
161
+ * stable across 16–19); over JSON-2 (19+) we pass them by MODERN name
162
+ * (`name`, `domain`, `operator`, `limit`).
163
+ */
164
+ readonly nameSearch: (model: string, options?: NameSearchOptions) => Effect.Effect<ReadonlyArray<readonly [number, string]>, TransportCallError>;
165
+ /**
166
+ * `name_get()` → `(id, display_name)` pairs for the given ids.
167
+ *
168
+ * GOTCHA: `name_get` was REMOVED server-side in Odoo 17+. We call it as-is
169
+ * with no polyfill; on 17+ it fails as a server fault. Use {@link nameSearch}
170
+ * or read `display_name` instead on modern servers.
171
+ */
172
+ readonly nameGet: (model: string, ids: ReadonlyArray<number>) => Effect.Effect<ReadonlyArray<readonly [number, string]>, TransportCallError>;
173
+ }>;
174
+ /**
175
+ * Hand-typed, boundary-decoded high-level Odoo operations over {@link Rpc}.
176
+ * Every result is decoded through `effect/Schema`; a wire-shape mismatch fails
177
+ * with {@link SchemaDriftError} carrying the raw payload, never a silent cast.
178
+ */
179
+ export declare class OdooClient extends OdooClient_base {
180
+ }
181
+ /** The {@link OdooClient} layer over an {@link Rpc} service. */
182
+ export declare const layer: Layer.Layer<OdooClient, never, Rpc>;
183
+ export {};
184
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}