@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,74 @@
1
+ import { Data } from "effect";
2
+ import type { HttpClientErrorType } from "../internal/platform.ts";
3
+
4
+ /** The stable platform failure distinction, without the unsafe platform error. */
5
+ export type TransportErrorKind = HttpClientErrorType["reason"]["_tag"];
6
+
7
+ /**
8
+ * Sanitized snapshot of the outbound request, safe to log and to attach to
9
+ * errors. The URL has no userinfo, query, or fragment. Never carries auth
10
+ * headers, cookies, or the request body — those may hold `session_id`, API
11
+ * keys, or passwords.
12
+ */
13
+ export interface RequestInfo {
14
+ readonly method: string;
15
+ readonly url: string;
16
+ }
17
+
18
+ const sanitizeUrl = (raw: string): string => {
19
+ try {
20
+ const url = new URL(raw);
21
+ url.username = "";
22
+ url.password = "";
23
+ url.search = "";
24
+ url.hash = "";
25
+ return url.href;
26
+ } catch {
27
+ return "<invalid-url>";
28
+ }
29
+ };
30
+
31
+ /**
32
+ * The network / HTTP layer failed: DNS, TLS, connection reset, non-decodable
33
+ * response, timeout. Carries only primitive metadata copied from the platform
34
+ * error. It never retains the `HttpClientError`, its request, response, cause,
35
+ * headers, or body.
36
+ *
37
+ * A transport failure proves only that *something* failed at the wire — it
38
+ * says nothing about Odoo's application state. Never infer success/idempotency
39
+ * from it; probe actual state before any resume.
40
+ */
41
+ export class OdooTransportError extends Data.TaggedError("OdooTransportError")<{
42
+ readonly request: RequestInfo;
43
+ readonly kind: TransportErrorKind;
44
+ readonly message: string;
45
+ readonly status?: number;
46
+ }> {
47
+ constructor(fields: {
48
+ readonly request: RequestInfo;
49
+ readonly kind: TransportErrorKind;
50
+ readonly status?: number;
51
+ }) {
52
+ const request = {
53
+ method: fields.request.method,
54
+ url: sanitizeUrl(fields.request.url),
55
+ };
56
+ const suffix = fields.status === undefined ? "" : ` (HTTP ${fields.status})`;
57
+ super({
58
+ request,
59
+ kind: fields.kind,
60
+ message: `${fields.kind} during ${request.method} ${request.url}${suffix}`,
61
+ ...(fields.status !== undefined ? { status: fields.status } : {}),
62
+ });
63
+ }
64
+
65
+ /** Copy safe diagnostics out of an Effect HTTP error, then discard it. */
66
+ static fromHttpClientError(request: RequestInfo, error: HttpClientErrorType): OdooTransportError {
67
+ const status = error.response?.status;
68
+ return new OdooTransportError({
69
+ request,
70
+ kind: error.reason._tag,
71
+ ...(status !== undefined ? { status } : {}),
72
+ });
73
+ }
74
+ }
package/src/index.ts ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @hodeinavarro/odoo-rpc-ts — public surface.
3
+ *
4
+ * A modern, Effect-native, strongly-typed Odoo RPC client: the shared
5
+ * contracts (errors, the `Transport` seam, config, domains, version
6
+ * resolution), the three transports, the session/auth services, and the
7
+ * high-level `OdooClient`. The `FakeTransport` testing double ships from the
8
+ * separate `@hodeinavarro/odoo-rpc-ts/testing` entry point.
9
+ */
10
+
11
+ // Error taxonomy + the fault mapper.
12
+ export * from "./errors/index.ts";
13
+
14
+ // The one transport seam and its call shape.
15
+ export { Transport, type CallKwParams, type TransportCallError } from "./transport.ts";
16
+
17
+ // Configuration.
18
+ export { OdooConfig, type OdooCredentials } from "./config.ts";
19
+
20
+ // Search-domain types and combinators.
21
+ export {
22
+ AND,
23
+ OR,
24
+ NOT,
25
+ normalizeDomain,
26
+ type Domain,
27
+ type DomainItem,
28
+ type DomainLeaf,
29
+ type DomainOperator,
30
+ } from "./domain.ts";
31
+
32
+ // Version resolution and WIRE-capability derivation (the app layer owns
33
+ // product capabilities — see WireCapabilities).
34
+ export {
35
+ VersionResolver,
36
+ CommonVersionResponse,
37
+ ServerVersionInfo,
38
+ parseVersionInfo,
39
+ deriveWireCapabilities,
40
+ type OdooVersion,
41
+ type WireCapabilities,
42
+ type ResolvedVersion,
43
+ } from "./version.ts";
44
+ export * as VersionResolverLive from "./version-live.ts";
45
+
46
+ // The call_kw choke point (context merge) and the high-level client.
47
+ export { Rpc, GlobalContext, type OdooContext, type GlobalContextProvider } from "./rpc.ts";
48
+ export * as RpcLive from "./rpc.ts";
49
+ export {
50
+ OdooClient,
51
+ type OdooRecord,
52
+ type FieldsMetadata,
53
+ type SearchOptions,
54
+ type SearchReadOptions,
55
+ type CallOptions,
56
+ type ReadGroupOptions,
57
+ type NameSearchOptions,
58
+ type TypedOptions,
59
+ type TypedReadOptions,
60
+ } from "./client.ts";
61
+ export * as OdooClientLive from "./client.ts";
62
+
63
+ // Typed records — two per-protocol tiers, not competitors. The CLASSIC tier
64
+ // (explicit traversal: Many2OneRef*, TypedRecordSet) speaks the classic
65
+ // `[id, name]`-pair protocol and works on 16+; the SPEC tier (declared
66
+ // prefetch: defineRecord/Many2One/One2Many + the pure specification compiler)
67
+ // speaks the 17+ `specification` protocol. Relation/temporal decode
68
+ // vocabulary is shared.
69
+ export {
70
+ // Relation & temporal decode schemas (classic-tier row declarations).
71
+ Many2OneRef,
72
+ Many2OneRefFromWire,
73
+ Many2OneRefOrNull,
74
+ Many2OneRefValue,
75
+ OdooDate,
76
+ OdooDateOrNull,
77
+ OdooDateTime,
78
+ OdooDateTimeOrNull,
79
+ // Pure join helpers + the classic-tier explicit-traversal snapshot.
80
+ collectRefIds,
81
+ makeRelatedMap,
82
+ makeTypedRecordSet,
83
+ refId,
84
+ type HasId,
85
+ type Many2OneRefField,
86
+ type RefOrId,
87
+ type RelatedMap,
88
+ type TypedRecordSet,
89
+ // Spec-tier model declaration + compilation (17+ `specification` protocol).
90
+ defineRecord,
91
+ Many2One,
92
+ One2Many,
93
+ compileSpecification,
94
+ hasRelations,
95
+ EmptyRecordSpecError,
96
+ RecordSpecCycleError,
97
+ type FieldInput,
98
+ type FieldMeta,
99
+ type Many2OneDecl,
100
+ type One2ManyDecl,
101
+ type RecordSpec,
102
+ type RowEncoded,
103
+ type RowType,
104
+ } from "./records/index.ts";
105
+
106
+ // Transports — one Layer per protocol, all implementing the Transport tag.
107
+ export * as JsonRpcTransport from "./transports/jsonrpc.ts";
108
+ export * as Json2Transport from "./transports/json2.ts";
109
+ export * as WebTransport from "./transports/web.ts";
110
+
111
+ // Cookie session + opt-in session-expiry recovery.
112
+ export {
113
+ CookieSession,
114
+ type CookieSessionService,
115
+ type CookieLoginError,
116
+ type ExistingSessionOptions,
117
+ type OdooSessionInfo,
118
+ type RawHttpOptions,
119
+ } from "./session/cookie.ts";
120
+ export * as CookieSessionLive from "./session/cookie.ts";
121
+ export { retryOnSessionExpired } from "./combinators/retryOnSessionExpired.ts";
122
+
123
+ // Typed x2many write commands.
124
+ export {
125
+ Command,
126
+ type CommandTuple,
127
+ type X2ManyCommands,
128
+ } from "./commands.ts";
129
+
130
+ // Database administration (master-password gated; Node-oriented buffers).
131
+ export * as DbService from "./services/db.ts";
132
+ export type { CreateOptions, DuplicateOptions, RestoreOptions } from "./services/db.ts";
133
+
134
+ // Report downloads over a cookie session.
135
+ export { ReportService, type ReportAction, type DownloadOptions } from "./services/report.ts";
136
+
137
+ // Named connection profiles — secrets never serialize; storage is yours.
138
+ export {
139
+ Profiles,
140
+ SecretStore,
141
+ ProfileStorage,
142
+ InMemorySecretStore,
143
+ InMemoryProfileStorage,
144
+ ProfileStoreError,
145
+ ProfileSecretMissingError,
146
+ type ProfileData,
147
+ type CredentialProfileData,
148
+ type SessionProfileData,
149
+ type LoadedProfile,
150
+ type ProfileProtocol,
151
+ } from "./profiles.ts";
152
+ export * as ProfilesLive from "./profiles.ts";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The single funnel for everything we consume from the platform HTTP layer.
3
+ *
4
+ * Every other module in this package imports platform values and types
5
+ * from HERE — never directly from `effect/unstable/http`. This kept the
6
+ * blast radius of the platform v4 move (v3 `@effect/platform` →
7
+ * `effect/unstable/http`) to exactly this file, and documents the exact
8
+ * platform surface the client depends on.
9
+ */
10
+
11
+ export {
12
+ Cookies,
13
+ FetchHttpClient,
14
+ HttpClient,
15
+ HttpClientError,
16
+ HttpClientRequest,
17
+ HttpClientResponse,
18
+ } from "effect/unstable/http";
19
+
20
+ // Type-only alias for the transport error channel of `HttpClient`, kept
21
+ // here so error modules can name it without reaching into the namespace.
22
+ export type { HttpClientError as HttpClientErrorType } from "effect/unstable/http/HttpClientError";
@@ -0,0 +1,80 @@
1
+ import { Effect, Option, Ref, Semaphore } from "effect";
2
+
3
+ /** A memoized `get` whose underlying `acquire` runs at most once on success. */
4
+ export interface SingleFlight<A, E, R> {
5
+ /**
6
+ * Return the cached value, or acquire it. Concurrent callers race a single
7
+ * permit so `acquire` runs once; a failure caches nothing, so the next call
8
+ * retries. This is the success-only single-flight AGENTS.md mandates in place
9
+ * of `Effect.cached` (which would also memoize failures).
10
+ */
11
+ readonly get: Effect.Effect<A, E, R>;
12
+ /**
13
+ * Drop the cached value so the next `get` re-acquires.
14
+ *
15
+ * GOTCHA: an acquire already in flight when `invalidate` runs will complete
16
+ * and be returned to ITS caller, but is NOT cached — a generation counter
17
+ * guards the write, so a stale result can never resurrect the cache.
18
+ */
19
+ readonly invalidate: Effect.Effect<void>;
20
+ /** The current cached value, without triggering an acquire. */
21
+ readonly peek: Effect.Effect<Option.Option<A>>;
22
+ }
23
+
24
+ interface State<A> {
25
+ readonly generation: number;
26
+ readonly value: Option.Option<A>;
27
+ }
28
+
29
+ /**
30
+ * Build a success-only single-flight cache around `acquire`.
31
+ *
32
+ * Invariant (AGENTS.md): only successful results are cached. A failed `acquire`
33
+ * leaves the cache empty so callers can retry — never `Effect.cached`.
34
+ */
35
+ export const make = <A, E, R>(
36
+ acquire: Effect.Effect<A, E, R>,
37
+ ): Effect.Effect<SingleFlight<A, E, R>, never, never> =>
38
+ Effect.gen(function* () {
39
+ const ref = yield* Ref.make<State<A>>({ generation: 0, value: Option.none() });
40
+ const semaphore = yield* Semaphore.make(1);
41
+
42
+ const get: Effect.Effect<A, E, R> = Effect.gen(function* () {
43
+ // Fast path: already cached, no lock needed.
44
+ const cached = yield* Ref.get(ref);
45
+ if (Option.isSome(cached.value)) {
46
+ return cached.value.value;
47
+ }
48
+
49
+ // Slow path: serialize acquisition behind the single permit.
50
+ return yield* semaphore.withPermits(1)(
51
+ Effect.gen(function* () {
52
+ // Double-check: a racing caller may have filled the cache while we
53
+ // waited for the permit.
54
+ const state = yield* Ref.get(ref);
55
+ if (Option.isSome(state.value)) {
56
+ return state.value.value;
57
+ }
58
+
59
+ const startGeneration = state.generation;
60
+ const value = yield* acquire;
61
+ // Cache only if no invalidate happened while we were acquiring.
62
+ yield* Ref.update(ref, (current) =>
63
+ current.generation === startGeneration
64
+ ? { generation: current.generation, value: Option.some(value) }
65
+ : current,
66
+ );
67
+ return value;
68
+ }),
69
+ );
70
+ });
71
+
72
+ const invalidate = Ref.update(ref, (current) => ({
73
+ generation: current.generation + 1,
74
+ value: Option.none<A>(),
75
+ }));
76
+
77
+ const peek = Ref.get(ref).pipe(Effect.map((state) => state.value));
78
+
79
+ return { get, invalidate, peek };
80
+ });