@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,65 @@
1
+ /**
2
+ * `TypedRecordSet` — an immutable, decoded snapshot of rows bound to its
3
+ * `(rpc, model)` origin, with explicit, batched relation traversal — the
4
+ * CLASSIC tier of the records design (the classic `[id, name]`-pair protocol,
5
+ * works on 16+; the spec tier in `recordModel.ts` needs 17+). Traversal is always ONE `read` per relation per
6
+ * recordset — never per row — so the N+1 storm is structurally impossible: there
7
+ * is no per-record fetch API to misuse. The snapshot is never mutated; a fetch
8
+ * produces a fresh {@link RelatedMap}, and the caller joins explicitly.
9
+ */
10
+ import { Effect, Schema } from "effect";
11
+ import { SchemaDriftError } from "../errors/schema.js";
12
+ import { collectRefIds, makeRelatedMap } from "./related.js";
13
+ /** Boundary decode: a related-`read` payload that fails the caller schema is drift. */
14
+ const decodeRows = (schema, context) => (raw) => Schema.decodeUnknownEffect(Schema.Array(schema))(raw).pipe(Effect.mapError((cause) => new SchemaDriftError({ context, payload: raw, cause })));
15
+ /**
16
+ * Read a many2one field off a row as a traversable ref value, or fail the whole
17
+ * traversal as a defect. The type system already forbids non-ref fields; this
18
+ * guards the dynamic-misuse escape hatch (e.g. an `any`-typed field or a schema
19
+ * that decoded the wrong shape) — a `TypeError`-equivalent `Effect.die` with a
20
+ * precise locator, never a silent skip.
21
+ */
22
+ const readRefField = (row, field, model) => {
23
+ const value = row[field];
24
+ if (value === null || value === undefined) {
25
+ return Effect.succeed(null);
26
+ }
27
+ if (typeof value === "object" && typeof value.id === "number") {
28
+ return Effect.succeed(value);
29
+ }
30
+ return Effect.die(`TypedRecordSet(${model}).fetchRelated: field ${String(field)} of record ${row.id} is not a ` +
31
+ `many2one reference ({ id, name } | null); got ${typeof value}. Declare it with ` +
32
+ `Many2OneRefOrNull so traversal is type-safe.`);
33
+ };
34
+ /**
35
+ * Construct a {@link TypedRecordSet} over already-decoded rows. Internal: the
36
+ * public entry point is `OdooClient.searchRecordsTyped`, which owns the initial
37
+ * `search_read` decode and passes the resulting rows here.
38
+ */
39
+ export const make = (rpc, model, rows) => {
40
+ const ids = rows.map((row) => row.id);
41
+ const fetchRelated = (field, relatedModel, schema, fields) =>
42
+ // Validate every row's field up front (dynamic-misuse guard), then collect
43
+ // the distinct ids purely and issue AT MOST one read.
44
+ Effect.forEach(rows, (row) => readRefField(row, field, model)).pipe(Effect.flatMap((refs) => {
45
+ const relatedIds = collectRefIds(refs, (ref) => ref);
46
+ if (relatedIds.length === 0) {
47
+ // No referenced records → no round trip. Asserted in the tests.
48
+ return Effect.succeed(makeRelatedMap([]));
49
+ }
50
+ return rpc
51
+ .callKw(relatedModel, "read", [], fields === undefined ? {} : { fields }, { ids: relatedIds })
52
+ .pipe(Effect.flatMap(decodeRows(schema, `${relatedModel}.read`)), Effect.map((relatedRows) => makeRelatedMap(relatedRows)));
53
+ }));
54
+ const joinRelated = (field, relatedModel, schema, fields) => fetchRelated(field, relatedModel, schema, fields).pipe(Effect.map((related) => rows.map((row) => [row, related.get(row[field])])));
55
+ return {
56
+ model,
57
+ rows,
58
+ ids,
59
+ length: rows.length,
60
+ [Symbol.iterator]: () => rows[Symbol.iterator](),
61
+ fetchRelated,
62
+ joinRelated,
63
+ };
64
+ };
65
+ //# sourceMappingURL=typed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed.js","sourceRoot":"","sources":["../../src/records/typed.ts"],"names":[],"mappings":"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"}
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { Transport, type TransportCallError, type TransportDialect } from "./transport.js";
3
+ /** An Odoo `context` dict: opaque keys, values we do not model. */
4
+ export type OdooContext = Record<string, unknown>;
5
+ declare const GlobalContext_base: Context.ServiceClass<GlobalContext, "odoo-rpc-ts/GlobalContext", OdooContext>;
6
+ /**
7
+ * The session/base Odoo `context` — the innermost, lowest-precedence layer of
8
+ * the context merge (e.g. `uid`, `lang`, `tz` established at authentication).
9
+ *
10
+ * Provided by the auth/transport layer that owns the session; absent here it
11
+ * defaults to `{}`. It is read once, when the {@link Rpc} layer is built.
12
+ */
13
+ export declare class GlobalContext extends GlobalContext_base {
14
+ }
15
+ declare const Rpc_base: Context.ServiceClass<Rpc, "odoo-rpc-ts/Rpc", {
16
+ /** The underlying transport's wire dialect, for ops that must branch on it. */
17
+ readonly dialect: TransportDialect;
18
+ /**
19
+ * Execute one `call_kw`, merging the effective `context` and forwarding to
20
+ * the {@link Transport}. Caller inputs are never mutated — `args`, `kwargs`,
21
+ * and every context object are treated as read-only and copied.
22
+ *
23
+ * Context precedence (lowest → highest, later wins):
24
+ * session/base ({@link GlobalContext}) < layer overrides
25
+ * < `kwargs.context` < `options.context`.
26
+ *
27
+ * A `context` key inside `kwargs` is treated as a caller-level context (the
28
+ * ergonomic form `callKw(m, meth, [], { context })`), merged just below
29
+ * `options.context` and then stripped from `kwargs` so it never wires twice.
30
+ * If it is present but not a plain object it is a caller defect and fails
31
+ * with `Effect.die`.
32
+ */
33
+ readonly callKw: (model: string, method: string, args: ReadonlyArray<unknown>, kwargs?: OdooContext, options?: {
34
+ readonly context?: OdooContext;
35
+ readonly ids?: ReadonlyArray<number>;
36
+ }) => Effect.Effect<unknown, TransportCallError>;
37
+ }>;
38
+ /**
39
+ * The `call_kw` choke point. Every high-level operation funnels through here so
40
+ * the Odoo `context` is merged in exactly one place, with a single, documented
41
+ * precedence (see {@link callKw}).
42
+ */
43
+ export declare class Rpc extends Rpc_base {
44
+ }
45
+ /**
46
+ * The provider seam for a lazily-seeded session/base context.
47
+ *
48
+ * A `Layer` resolves eagerly at build time, but the base tier must be lazy and
49
+ * retryable-on-failure (a transport error resolving it must surface as the
50
+ * failing call's error, never be baked in). So instead of a value provider we
51
+ * take an `Effect` that yields the base context; {@link buildRpc} runs it behind
52
+ * a success-only single-flight on the FIRST `callKw` (and retries on failure).
53
+ *
54
+ * Merge position: below the layer overrides, above the static {@link GlobalContext}
55
+ * tag (which, when both are present, seeds the innermost defaults).
56
+ */
57
+ export type GlobalContextProvider = Effect.Effect<OdooContext, TransportCallError>;
58
+ /**
59
+ * Build the {@link Rpc} layer over a {@link Transport}, optionally baking in
60
+ * layer-level `globalContext` overrides that sit above the session/base context
61
+ * but below any per-call context, and/or a {@link GlobalContextProvider} that
62
+ * lazily seeds the base tier on the first call.
63
+ */
64
+ export declare const layerWith: (config?: {
65
+ readonly globalContext?: OdooContext;
66
+ readonly globalContextProvider?: GlobalContextProvider;
67
+ }) => Layer.Layer<Rpc, never, Transport>;
68
+ /**
69
+ * The {@link Rpc} layer with the base tier seeded from the ambient
70
+ * {@link Transport}'s server-side user context.
71
+ *
72
+ * Resolves `res.users.context_get` (an `@api.model` method — kwargs-only, so it
73
+ * works identically over `execute_kw` and the JSON-2 by-name dialect) exactly
74
+ * once, lazily on the first `callKw`, behind a success-only single-flight. A
75
+ * transport/decoding failure while seeding is NOT swallowed: it surfaces as that
76
+ * first call's error and the seed is retried on the next call.
77
+ */
78
+ export declare const layerSeeded: (config?: {
79
+ readonly globalContext?: OdooContext;
80
+ }) => Layer.Layer<Rpc, never, Transport>;
81
+ /**
82
+ * The default {@link Rpc} layer: base tier seeded from the server-side user
83
+ * context ({@link layerSeeded}), so translated fields (stage names, selection
84
+ * labels, …) come back in the user's language instead of the source terms —
85
+ * matching odoo-rpc-py's `seed_context=True` default. Opt out with
86
+ * {@link layerWith} (no provider) when calls must carry no ambient context.
87
+ */
88
+ export declare const layer: Layer.Layer<Rpc, never, Transport>;
89
+ export {};
90
+ //# sourceMappingURL=rpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAkB,MAAM,QAAQ,CAAC;AAGhE,OAAO,EAEL,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,gBAAgB,CAAC;AAExB,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;;AAYlD;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,kBAGH;CAAG;;IAU/B,+EAA+E;sBAC7D,gBAAgB;IAClC;;;;;;;;;;;;;;OAcG;qBACc,CACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,EAC5B,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;QAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;KAAE,KAC/E,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,kBAAkB,CAAC;;AA/BnD;;;;GAIG;AACH,qBAAa,GAAI,SAAQ,QA4BH;CAAG;AAEzB;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAC;AAuFnF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,YAAa;IACjC,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC;IACrC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;CACxD,KAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAOlC,CAAC;AAEJ;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,YAAa;IACnC,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC;CACtC,KAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAUlC,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAAiB,CAAC"}
package/dist/rpc.js ADDED
@@ -0,0 +1,113 @@
1
+ import { Context, Effect, Layer, Option, Schema } from "effect";
2
+ import { SchemaDriftError } from "./errors/schema.js";
3
+ import * as SingleFlight from "./internal/singleFlight.js";
4
+ import { Transport, } from "./transport.js";
5
+ /** A plain object (a mergeable context), excluding arrays and `null`. */
6
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7
+ /** Copy `kwargs` without its `context` key (never mutates the caller's object). */
8
+ const omitContext = (kwargs) => {
9
+ const { context: _context, ...rest } = kwargs;
10
+ return rest;
11
+ };
12
+ /**
13
+ * The session/base Odoo `context` — the innermost, lowest-precedence layer of
14
+ * the context merge (e.g. `uid`, `lang`, `tz` established at authentication).
15
+ *
16
+ * Provided by the auth/transport layer that owns the session; absent here it
17
+ * defaults to `{}`. It is read once, when the {@link Rpc} layer is built.
18
+ */
19
+ export class GlobalContext extends Context.Service()("odoo-rpc-ts/GlobalContext") {
20
+ }
21
+ /**
22
+ * The `call_kw` choke point. Every high-level operation funnels through here so
23
+ * the Odoo `context` is merged in exactly one place, with a single, documented
24
+ * precedence (see {@link callKw}).
25
+ */
26
+ export class Rpc extends Context.Service()("odoo-rpc-ts/Rpc") {
27
+ }
28
+ const ContextRecord = Schema.Record(Schema.String, Schema.Unknown);
29
+ /**
30
+ * Decode a raw `res.users.context_get` result into an {@link OdooContext};
31
+ * drift (a non-object payload) fails as {@link SchemaDriftError}, which is part
32
+ * of {@link TransportCallError} and so propagates as the seeding call's error.
33
+ */
34
+ const decodeContext = (raw) => Schema.decodeUnknownEffect(ContextRecord)(raw).pipe(Effect.mapError((cause) => new SchemaDriftError({ context: "res.users.context_get", payload: raw, cause })));
35
+ /**
36
+ * The shared {@link Rpc} service constructor. `provider`, when given, seeds the
37
+ * base tier lazily (single-flight on first call). The static {@link GlobalContext}
38
+ * tag is always read (defaulting to `{}`) and sits at the very bottom.
39
+ */
40
+ const buildRpc = (transport, config, provider) => Effect.gen(function* () {
41
+ const staticBase = Option.getOrElse(yield* Effect.serviceOption(GlobalContext), () => ({}));
42
+ const layerOverrides = config?.globalContext ?? {};
43
+ // No provider: the base tier is the static tag value (today's behavior,
44
+ // resolved once at build). With a provider: resolve it lazily behind a
45
+ // success-only single-flight and layer it over the static base.
46
+ const resolveBase = provider === undefined
47
+ ? Effect.succeed(staticBase)
48
+ : yield* SingleFlight.make(provider).pipe(Effect.map((sf) => sf.get.pipe(Effect.map((seeded) => ({ ...staticBase, ...seeded })))));
49
+ const callKw = (model, method, args, kwargs, options) => {
50
+ // Pull any caller-supplied `kwargs.context` out of the spread so it is
51
+ // merged at the caller tier (not silently clobbered) and never wired twice.
52
+ const hasKwargsContext = kwargs !== undefined && "context" in kwargs;
53
+ const kwargsContext = hasKwargsContext ? kwargs["context"] : undefined;
54
+ if (hasKwargsContext && !isPlainObject(kwargsContext)) {
55
+ return Effect.die(`Rpc.callKw: kwargs.context for "${model}.${method}" must be a plain object, ` +
56
+ `got ${typeof kwargsContext}.`);
57
+ }
58
+ const restKwargs = hasKwargsContext ? omitContext(kwargs) : (kwargs ?? {});
59
+ return resolveBase.pipe(Effect.flatMap((base) => {
60
+ const context = {
61
+ ...base,
62
+ ...layerOverrides,
63
+ ...kwargsContext,
64
+ ...options?.context,
65
+ };
66
+ const params = {
67
+ model,
68
+ method,
69
+ args,
70
+ kwargs: { ...restKwargs, context },
71
+ ...(options?.ids !== undefined ? { ids: options.ids } : {}),
72
+ };
73
+ return transport.callKw(params);
74
+ }));
75
+ };
76
+ return { dialect: transport.dialect, callKw };
77
+ });
78
+ /**
79
+ * Build the {@link Rpc} layer over a {@link Transport}, optionally baking in
80
+ * layer-level `globalContext` overrides that sit above the session/base context
81
+ * but below any per-call context, and/or a {@link GlobalContextProvider} that
82
+ * lazily seeds the base tier on the first call.
83
+ */
84
+ export const layerWith = (config) => Layer.effect(Rpc, Effect.gen(function* () {
85
+ const transport = yield* Transport;
86
+ return yield* buildRpc(transport, config, config?.globalContextProvider);
87
+ }));
88
+ /**
89
+ * The {@link Rpc} layer with the base tier seeded from the ambient
90
+ * {@link Transport}'s server-side user context.
91
+ *
92
+ * Resolves `res.users.context_get` (an `@api.model` method — kwargs-only, so it
93
+ * works identically over `execute_kw` and the JSON-2 by-name dialect) exactly
94
+ * once, lazily on the first `callKw`, behind a success-only single-flight. A
95
+ * transport/decoding failure while seeding is NOT swallowed: it surfaces as that
96
+ * first call's error and the seed is retried on the next call.
97
+ */
98
+ export const layerSeeded = (config) => Layer.effect(Rpc, Effect.gen(function* () {
99
+ const transport = yield* Transport;
100
+ const provider = transport
101
+ .callKw({ model: "res.users", method: "context_get", args: [], kwargs: {} })
102
+ .pipe(Effect.flatMap(decodeContext));
103
+ return yield* buildRpc(transport, config, provider);
104
+ }));
105
+ /**
106
+ * The default {@link Rpc} layer: base tier seeded from the server-side user
107
+ * context ({@link layerSeeded}), so translated fields (stage names, selection
108
+ * labels, …) come back in the user's language instead of the source terms —
109
+ * matching odoo-rpc-py's `seed_context=True` default. Opt out with
110
+ * {@link layerWith} (no provider) when calls must carry no ambient context.
111
+ */
112
+ export const layer = layerSeeded();
113
+ //# sourceMappingURL=rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,YAAY,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAEL,SAAS,GAGV,MAAM,gBAAgB,CAAC;AAKxB,yEAAyE;AACzE,MAAM,aAAa,GAAG,CAAC,KAAc,EAAwB,EAAE,CAC7D,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAEvE,mFAAmF;AACnF,MAAM,WAAW,GAAG,CAAC,MAAmB,EAAe,EAAE;IACvD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC;IAC9C,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,OAAO,aAAc,SAAQ,OAAO,CAAC,OAAO,EAG/C,CAAC,2BAA2B,CAAC;CAAG;AAEnC;;;;GAIG;AACH,MAAM,OAAO,GAAI,SAAQ,OAAO,CAAC,OAAO,EA4BrC,CAAC,iBAAiB,CAAC;CAAG;AAgBzB,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;AAEnE;;;;GAIG;AACH,MAAM,aAAa,GAAG,CAAC,GAAY,EAAgD,EAAE,CACnF,MAAM,CAAC,mBAAmB,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CACjD,MAAM,CAAC,QAAQ,CACb,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,uBAAuB,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAC3F,CACF,CAAC;AAEJ;;;;GAIG;AACH,MAAM,QAAQ,GAAG,CACf,SAAmC,EACnC,MAA4D,EAC5D,QAA2C,EACR,EAAE,CACrC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,UAAU,GAAG,MAAM,CAAC,SAAS,CACjC,KAAK,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,EAC1C,GAAgB,EAAE,CAAC,CAAC,EAAE,CAAC,CACxB,CAAC;IACF,MAAM,cAAc,GAAG,MAAM,EAAE,aAAa,IAAI,EAAE,CAAC;IAEnD,wEAAwE;IACxE,uEAAuE;IACvE,gEAAgE;IAChE,MAAM,WAAW,GACf,QAAQ,KAAK,SAAS;QACpB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;QAC5B,CAAC,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CACrC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAChB,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAe,EAAE,CAAC,CAAC,EAAE,GAAG,UAAU,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC,CAAC,CACjF,CACF,CAAC;IAER,MAAM,MAAM,GAAG,CACb,KAAa,EACb,MAAc,EACd,IAA4B,EAC5B,MAAoB,EACpB,OAAkF,EACtC,EAAE;QAC9C,uEAAuE;QACvE,4EAA4E;QAC5E,MAAM,gBAAgB,GAAG,MAAM,KAAK,SAAS,IAAI,SAAS,IAAI,MAAM,CAAC;QACrE,MAAM,aAAa,GAAG,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvE,IAAI,gBAAgB,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC;YACtD,OAAO,MAAM,CAAC,GAAG,CACf,mCAAmC,KAAK,IAAI,MAAM,4BAA4B;gBAC5E,OAAO,OAAO,aAAa,GAAG,CACjC,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,gBAAgB,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;QAE3E,OAAO,WAAW,CAAC,IAAI,CACrB,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACtB,MAAM,OAAO,GAAgB;gBAC3B,GAAG,IAAI;gBACP,GAAG,cAAc;gBACjB,GAAI,aAAyC;gBAC7C,GAAG,OAAO,EAAE,OAAO;aACpB,CAAC;YACF,MAAM,MAAM,GAAiB;gBAC3B,KAAK;gBACL,MAAM;gBACN,IAAI;gBACJ,MAAM,EAAE,EAAE,GAAG,UAAU,EAAE,OAAO,EAAE;gBAClC,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D,CAAC;YACF,OAAO,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC,CAAC,CACH,CAAC;IACJ,CAAC,CAAC;IAEF,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;AAChD,CAAC,CAAC,CAAC;AAEL;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAGzB,EAAsC,EAAE,CACvC,KAAK,CAAC,MAAM,CACV,GAAG,EACH,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC;IACnC,OAAO,KAAK,CAAC,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,CAAC,CAAC;AAC3E,CAAC,CAAC,CACH,CAAC;AAEJ;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAE3B,EAAsC,EAAE,CACvC,KAAK,CAAC,MAAM,CACV,GAAG,EACH,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC;IACnC,MAAM,QAAQ,GAA0B,SAAS;SAC9C,MAAM,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;SAC3E,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IACvC,OAAO,KAAK,CAAC,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;AACtD,CAAC,CAAC,CACH,CAAC;AAEJ;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAuC,WAAW,EAAE,CAAC"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `DbService` — the master-password-gated database-management endpoints exposed
3
+ * by Odoo's `db` service over `POST /jsonrpc` (`exp_list`, `exp_db_exist`,
4
+ * `exp_create_database`, `exp_drop`, `exp_duplicate_database`, `exp_dump`,
5
+ * `exp_restore`, `exp_change_admin_password`).
6
+ *
7
+ * Unlike the model-level `Transport`, this service is **database-less and
8
+ * credential-less**: it authenticates nothing and speaks to no database. The
9
+ * only secret is the per-call master password (Odoo's `admin_passwd`), taken as
10
+ * a `Redacted<string>` and unwrapped solely at the point the request body is
11
+ * built. There is therefore no `Transport` tag, no uid, and no session — every
12
+ * function takes the server `url: URL` explicitly and requires only an abstract
13
+ * `HttpClient`.
14
+ *
15
+ * A wrong master password surfaces as `odoo.exceptions.AccessDenied`, which the
16
+ * shared JSON-RPC choke point ({@link mapJsonRpcError}) maps to
17
+ * {@link OdooAuthenticationError} — same as any other auth rejection.
18
+ *
19
+ * NODE ORIENTATION: {@link dump} and {@link restore} carry the whole backup in
20
+ * memory as a `Uint8Array`. This is inherent to the wire contract — Odoo's
21
+ * `exp_dump` returns the archive base64-encoded inside the JSON-RPC `result`,
22
+ * and `exp_restore` takes it back the same way — so there is no streaming path
23
+ * to preserve. Fine for the CI/ops-sized databases this is meant for; not a
24
+ * mechanism for multi-gigabyte production dumps.
25
+ *
26
+ * Wire arities verified against `odoo/service/db.py` on 16.0 and 19.0 (stable
27
+ * 16–19). `dispatch` strips the leading master password for every `exp_*`
28
+ * method except the four unauthenticated ones (`list`, `db_exist`, `list_lang`,
29
+ * `server_version`); `exp_restore` has NO `neutralize` parameter on any version.
30
+ */
31
+ import { Effect, Redacted } from "effect";
32
+ import { SchemaDriftError } from "../errors/schema.js";
33
+ import type { OdooAuthenticationError } from "../errors/auth.js";
34
+ import type { OdooServerFault } from "../errors/server.js";
35
+ import type { SessionExpiredError } from "../errors/session.js";
36
+ import { OdooTransportError } from "../errors/transport.js";
37
+ import { HttpClient } from "../internal/platform.js";
38
+ /** The failure channel a single `db` round trip can produce. */
39
+ type DbCallError = OdooTransportError | SchemaDriftError | SessionExpiredError | OdooServerFault | OdooAuthenticationError;
40
+ /**
41
+ * List the databases the server is willing to expose (honours `list_db` and any
42
+ * `dbfilter`). Unauthenticated — no master password required.
43
+ */
44
+ export declare const listDatabases: (url: URL) => Effect.Effect<ReadonlyArray<string>, DbCallError, HttpClient.HttpClient>;
45
+ /** Whether a database of the given name exists. Unauthenticated. */
46
+ export declare const exists: (url: URL, name: string) => Effect.Effect<boolean, DbCallError, HttpClient.HttpClient>;
47
+ /** Options for {@link create}. `master` and `adminPassword` are the two secrets. */
48
+ export interface CreateOptions {
49
+ readonly master: Redacted.Redacted<string>;
50
+ readonly name: string;
51
+ /** Install demo data. Defaults to `false`. */
52
+ readonly demo?: boolean;
53
+ /** Load language / locale (e.g. `"en_US"`). Defaults to `"en_US"`. */
54
+ readonly lang?: string;
55
+ /** Login of the initial admin user. Defaults to `"admin"`. */
56
+ readonly adminLogin?: string;
57
+ /** Password of the initial admin user. */
58
+ readonly adminPassword: Redacted.Redacted<string>;
59
+ /** ISO country code to seed localization (e.g. `"US"`). */
60
+ readonly countryCode?: string;
61
+ /** Company phone to seed. */
62
+ readonly phone?: string;
63
+ }
64
+ /**
65
+ * Create a fresh database and its initial admin user.
66
+ *
67
+ * Wire (after `dispatch` strips the master): `exp_create_database(db_name,
68
+ * demo, lang, user_password='admin', login='admin', country_code=None,
69
+ * phone=None)`.
70
+ */
71
+ export declare const create: (url: URL, options: CreateOptions) => Effect.Effect<void, DbCallError, HttpClient.HttpClient>;
72
+ /**
73
+ * Drop a database. Resolves to `true` when a database was actually dropped,
74
+ * `false` when none of that name existed.
75
+ */
76
+ export declare const drop: (url: URL, master: Redacted.Redacted<string>, name: string) => Effect.Effect<boolean, DbCallError, HttpClient.HttpClient>;
77
+ /** Options for {@link duplicate}. */
78
+ export interface DuplicateOptions {
79
+ /**
80
+ * Neutralize the copy (disable outgoing mail/crons/payment providers, …) so it
81
+ * is safe as a staging database. Maps to `neutralize_database`. Defaults to
82
+ * `false`.
83
+ */
84
+ readonly neutralize?: boolean;
85
+ }
86
+ /**
87
+ * Duplicate `source` into a new database `target`.
88
+ *
89
+ * Wire (after master strip): `exp_duplicate_database(db_original_name, db_name,
90
+ * neutralize_database=False)`.
91
+ */
92
+ export declare const duplicate: (url: URL, master: Redacted.Redacted<string>, source: string, target: string, options?: DuplicateOptions) => Effect.Effect<void, DbCallError, HttpClient.HttpClient>;
93
+ /**
94
+ * Dump a database into an in-memory archive.
95
+ *
96
+ * `format` is `"zip"` (default; includes the filestore) or `"dump"` (raw
97
+ * `pg_dump` custom format, database only). Odoo returns the archive
98
+ * base64-encoded inside the JSON-RPC `result`; we decode it to raw bytes.
99
+ */
100
+ export declare const dump: (url: URL, master: Redacted.Redacted<string>, name: string, format?: "zip" | "dump") => Effect.Effect<Uint8Array, DbCallError, HttpClient.HttpClient>;
101
+ /** Options for {@link restore}. */
102
+ export interface RestoreOptions {
103
+ /**
104
+ * Restore as a *copy* — regenerate the database uuid and secret so it does not
105
+ * collide with the original in a mail/cron sense. Maps to `copy`. Defaults to
106
+ * `false`.
107
+ *
108
+ * NOTE: `exp_restore` has NO `neutralize` parameter on any supported version;
109
+ * there is deliberately no such option here.
110
+ */
111
+ readonly copy?: boolean;
112
+ }
113
+ /**
114
+ * Restore a previously {@link dump}ed archive under a new database `name`.
115
+ *
116
+ * The bytes are re-encoded to the base64 string `exp_restore` expects.
117
+ * Wire (after master strip): `exp_restore(db_name, data, copy=False)`.
118
+ */
119
+ export declare const restore: (url: URL, master: Redacted.Redacted<string>, name: string, archive: Uint8Array, options?: RestoreOptions) => Effect.Effect<void, DbCallError, HttpClient.HttpClient>;
120
+ /**
121
+ * Change the server master password (`admin_passwd`). `master` is the current
122
+ * password; `next` becomes the new one.
123
+ *
124
+ * Wire (after master strip): `exp_change_admin_password(new_password)`.
125
+ */
126
+ export declare const changeMasterPassword: (url: URL, master: Redacted.Redacted<string>, next: Redacted.Redacted<string>) => Effect.Effect<void, DbCallError, HttpClient.HttpClient>;
127
+ export {};
128
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `DbService` — the master-password-gated database-management endpoints exposed
3
+ * by Odoo's `db` service over `POST /jsonrpc` (`exp_list`, `exp_db_exist`,
4
+ * `exp_create_database`, `exp_drop`, `exp_duplicate_database`, `exp_dump`,
5
+ * `exp_restore`, `exp_change_admin_password`).
6
+ *
7
+ * Unlike the model-level `Transport`, this service is **database-less and
8
+ * credential-less**: it authenticates nothing and speaks to no database. The
9
+ * only secret is the per-call master password (Odoo's `admin_passwd`), taken as
10
+ * a `Redacted<string>` and unwrapped solely at the point the request body is
11
+ * built. There is therefore no `Transport` tag, no uid, and no session — every
12
+ * function takes the server `url: URL` explicitly and requires only an abstract
13
+ * `HttpClient`.
14
+ *
15
+ * A wrong master password surfaces as `odoo.exceptions.AccessDenied`, which the
16
+ * shared JSON-RPC choke point ({@link mapJsonRpcError}) maps to
17
+ * {@link OdooAuthenticationError} — same as any other auth rejection.
18
+ *
19
+ * NODE ORIENTATION: {@link dump} and {@link restore} carry the whole backup in
20
+ * memory as a `Uint8Array`. This is inherent to the wire contract — Odoo's
21
+ * `exp_dump` returns the archive base64-encoded inside the JSON-RPC `result`,
22
+ * and `exp_restore` takes it back the same way — so there is no streaming path
23
+ * to preserve. Fine for the CI/ops-sized databases this is meant for; not a
24
+ * mechanism for multi-gigabyte production dumps.
25
+ *
26
+ * Wire arities verified against `odoo/service/db.py` on 16.0 and 19.0 (stable
27
+ * 16–19). `dispatch` strips the leading master password for every `exp_*`
28
+ * method except the four unauthenticated ones (`list`, `db_exist`, `list_lang`,
29
+ * `server_version`); `exp_restore` has NO `neutralize` parameter on any version.
30
+ */
31
+ import { Effect, Encoding, Redacted, Schema } from "effect";
32
+ import { SchemaDriftError } from "../errors/schema.js";
33
+ import { OdooTransportError } from "../errors/transport.js";
34
+ import { HttpClient, HttpClientRequest } from "../internal/platform.js";
35
+ import { buildRequest, JsonRpcResponse, mapJsonRpcError, nextRequestId, } from "../protocol/jsonrpc.js";
36
+ /** Join the base URL with the `/jsonrpc` path, tolerating a trailing slash. */
37
+ const jsonRpcEndpoint = (url) => `${url.href.replace(/\/+$/, "")}/jsonrpc`;
38
+ /**
39
+ * One `db`-service round trip: build the `{service:"db", method, args}`
40
+ * envelope, POST it, decode the JSON-RPC response, and either return the raw
41
+ * `result` or map the fault through the shared choke point.
42
+ *
43
+ * GOTCHA: never record `args` on the span — the master password rides in
44
+ * position 0 for gated methods. Only the method name is safe.
45
+ */
46
+ const dbCall = (url, method, args) => Effect.gen(function* () {
47
+ const client = yield* HttpClient.HttpClient;
48
+ const endpoint = jsonRpcEndpoint(url);
49
+ const id = nextRequestId();
50
+ const site = { method: `db.${method}` };
51
+ const request = HttpClientRequest.post(endpoint).pipe(HttpClientRequest.setHeader("Content-Type", "application/json"), HttpClientRequest.bodyJsonUnsafe(buildRequest({ service: "db", method, args }, id)));
52
+ const payload = yield* client.execute(request).pipe(Effect.flatMap((response) => response.json), Effect.mapError((cause) => OdooTransportError.fromHttpClientError({ method: "POST", url: endpoint }, cause)));
53
+ const decoded = yield* Schema.decodeUnknownEffect(JsonRpcResponse)(payload).pipe(Effect.mapError((cause) => new SchemaDriftError({ context: "odoo.db response", payload, cause })));
54
+ if ("error" in decoded) {
55
+ return yield* Effect.fail(mapJsonRpcError(decoded.error, site));
56
+ }
57
+ return decoded.result;
58
+ }).pipe(Effect.withSpan("odoo.db", { attributes: { "odoo.db.method": method } }));
59
+ /** Decode a `db` result through `schema`, raising {@link SchemaDriftError} on drift. */
60
+ const decodeResult = (schema, context, result) => Schema.decodeUnknownEffect(schema)(result).pipe(Effect.mapError((cause) => new SchemaDriftError({ context, payload: result, cause })));
61
+ /**
62
+ * List the databases the server is willing to expose (honours `list_db` and any
63
+ * `dbfilter`). Unauthenticated — no master password required.
64
+ */
65
+ export const listDatabases = (url) => dbCall(url, "list", []).pipe(Effect.flatMap((result) => decodeResult(Schema.Array(Schema.String), "odoo.db exp_list", result)));
66
+ /** Whether a database of the given name exists. Unauthenticated. */
67
+ export const exists = (url, name) => dbCall(url, "db_exist", [name]).pipe(Effect.flatMap((result) => decodeResult(Schema.Boolean, "odoo.db exp_db_exist", result)));
68
+ /**
69
+ * Create a fresh database and its initial admin user.
70
+ *
71
+ * Wire (after `dispatch` strips the master): `exp_create_database(db_name,
72
+ * demo, lang, user_password='admin', login='admin', country_code=None,
73
+ * phone=None)`.
74
+ */
75
+ export const create = (url, options) => dbCall(url, "create_database", [
76
+ Redacted.value(options.master),
77
+ options.name,
78
+ options.demo ?? false,
79
+ options.lang ?? "en_US",
80
+ Redacted.value(options.adminPassword),
81
+ options.adminLogin ?? "admin",
82
+ options.countryCode ?? null,
83
+ options.phone ?? null,
84
+ ]).pipe(Effect.asVoid);
85
+ /**
86
+ * Drop a database. Resolves to `true` when a database was actually dropped,
87
+ * `false` when none of that name existed.
88
+ */
89
+ export const drop = (url, master, name) => dbCall(url, "drop", [Redacted.value(master), name]).pipe(Effect.flatMap((result) => decodeResult(Schema.Boolean, "odoo.db exp_drop", result)));
90
+ /**
91
+ * Duplicate `source` into a new database `target`.
92
+ *
93
+ * Wire (after master strip): `exp_duplicate_database(db_original_name, db_name,
94
+ * neutralize_database=False)`.
95
+ */
96
+ export const duplicate = (url, master, source, target, options = {}) => dbCall(url, "duplicate_database", [
97
+ Redacted.value(master),
98
+ source,
99
+ target,
100
+ options.neutralize ?? false,
101
+ ]).pipe(Effect.asVoid);
102
+ /**
103
+ * Dump a database into an in-memory archive.
104
+ *
105
+ * `format` is `"zip"` (default; includes the filestore) or `"dump"` (raw
106
+ * `pg_dump` custom format, database only). Odoo returns the archive
107
+ * base64-encoded inside the JSON-RPC `result`; we decode it to raw bytes.
108
+ */
109
+ export const dump = (url, master, name, format = "zip") => dbCall(url, "dump", [Redacted.value(master), name, format]).pipe(
110
+ // Odoo returns the archive base64-encoded in `result`; decode to raw bytes,
111
+ // any malformed base64 surfacing as SchemaDriftError like every other drift.
112
+ Effect.flatMap((result) => decodeResult(Schema.Uint8ArrayFromBase64, "odoo.db exp_dump", result)));
113
+ /**
114
+ * Restore a previously {@link dump}ed archive under a new database `name`.
115
+ *
116
+ * The bytes are re-encoded to the base64 string `exp_restore` expects.
117
+ * Wire (after master strip): `exp_restore(db_name, data, copy=False)`.
118
+ */
119
+ export const restore = (url, master, name, archive, options = {}) => dbCall(url, "restore", [
120
+ Redacted.value(master),
121
+ name,
122
+ Encoding.encodeBase64(archive),
123
+ options.copy ?? false,
124
+ ]).pipe(Effect.asVoid);
125
+ /**
126
+ * Change the server master password (`admin_passwd`). `master` is the current
127
+ * password; `next` becomes the new one.
128
+ *
129
+ * Wire (after master strip): `exp_change_admin_password(new_password)`.
130
+ */
131
+ export const changeMasterPassword = (url, master, next) => dbCall(url, "change_admin_password", [Redacted.value(master), Redacted.value(next)]).pipe(Effect.asVoid);
132
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { Effect } from "effect";
2
+ import type { CookieLoginError, CookieSessionService } from "../session/cookie.js";
3
+ /**
4
+ * A decoded `ir.actions.report` row. Field names are normalized to camelCase at
5
+ * the schema boundary so callers never touch Odoo's snake_case wire shape.
6
+ */
7
+ export interface ReportAction {
8
+ readonly id: number;
9
+ readonly name: string;
10
+ readonly reportName: string;
11
+ readonly model: string;
12
+ readonly reportType: string;
13
+ }
14
+ /**
15
+ * List `ir.actions.report` definitions over a cookie session, optionally scoped
16
+ * to a single `model`. Self-contained: goes through the session's `json` hatch
17
+ * (`web/dataset/call_kw` → `search_read`) so it needs no `Transport`/`Rpc`
18
+ * wiring. Fails with the session's `CookieLoginError` union (login faults,
19
+ * server faults, or `SchemaDriftError` on a drifted row shape).
20
+ */
21
+ export declare const list: (session: CookieSessionService, model?: string) => Effect.Effect<ReadonlyArray<ReportAction>, CookieLoginError>;
22
+ /**
23
+ * Options for {@link download}. `converter` is the report renderer segment of
24
+ * the download URL (`pdf` by default; `html`, `text`, … are also valid).
25
+ */
26
+ export interface DownloadOptions {
27
+ readonly reportName: string;
28
+ readonly ids: ReadonlyArray<number>;
29
+ readonly converter?: string;
30
+ }
31
+ /**
32
+ * Download a rendered report as raw bytes via `GET
33
+ * /report/<converter>/<reportName>/<ids.join(",")>` on the cookie-bound client
34
+ * (the route is identical and CSRF-free across Odoo 16–19).
35
+ *
36
+ * Status mapping: `404 → OdooMissingError`, `403 → OdooAccessError`, any other
37
+ * non-2xx (including the `500` Odoo raises for an unknown report name) →
38
+ * `OdooTransportError`. For `converter === "pdf"` the 2xx body is checked for
39
+ * the `%PDF` magic and fails as `SchemaDriftError` if the server handed back
40
+ * something else (e.g. an HTML error page with a 200).
41
+ */
42
+ export declare const download: (session: CookieSessionService, options: DownloadOptions) => Effect.Effect<Uint8Array, CookieLoginError>;
43
+ /** The report service, grouped for `ReportService.list` / `ReportService.download`. */
44
+ export declare const ReportService: {
45
+ readonly list: typeof list;
46
+ readonly download: typeof download;
47
+ };
48
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/services/report.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAkB,MAAM,QAAQ,CAAC;AAIhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEnF;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAuBD;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,YACN,oBAAoB,UACrB,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,EAAE,gBAAgB,CAkB1D,CAAC;AAEL;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAQD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,YACV,oBAAoB,WACpB,eAAe,KACvB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,gBAAgB,CA8DzC,CAAC;AAEL,uFAAuF;AACvF,eAAO,MAAM,aAAa;;;CAA8B,CAAC"}