@hodeinavarro/odoo-rpc-ts 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/dist/client.d.ts +184 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +277 -0
- package/dist/client.js.map +1 -0
- package/dist/combinators/retryOnSessionExpired.d.ts +13 -0
- package/dist/combinators/retryOnSessionExpired.d.ts.map +1 -0
- package/dist/combinators/retryOnSessionExpired.js +15 -0
- package/dist/combinators/retryOnSessionExpired.js.map +1 -0
- package/dist/commands.d.ts +91 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +69 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +60 -0
- package/dist/config.js.map +1 -0
- package/dist/domain.d.ts +30 -0
- package/dist/domain.d.ts.map +1 -0
- package/dist/domain.js +70 -0
- package/dist/domain.js.map +1 -0
- package/dist/errors/auth.d.ts +27 -0
- package/dist/errors/auth.d.ts.map +1 -0
- package/dist/errors/auth.js +8 -0
- package/dist/errors/auth.js.map +1 -0
- package/dist/errors/index.d.ts +30 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +13 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/mapFault.d.ts +31 -0
- package/dist/errors/mapFault.d.ts.map +1 -0
- package/dist/errors/mapFault.js +48 -0
- package/dist/errors/mapFault.js.map +1 -0
- package/dist/errors/protocol.d.ts +22 -0
- package/dist/errors/protocol.d.ts.map +1 -0
- package/dist/errors/protocol.js +9 -0
- package/dist/errors/protocol.js.map +1 -0
- package/dist/errors/schema.d.ts +22 -0
- package/dist/errors/schema.d.ts.map +1 -0
- package/dist/errors/schema.js +14 -0
- package/dist/errors/schema.js.map +1 -0
- package/dist/errors/server.d.ts +109 -0
- package/dist/errors/server.d.ts.map +1 -0
- package/dist/errors/server.js +49 -0
- package/dist/errors/server.js.map +1 -0
- package/dist/errors/session.d.ts +16 -0
- package/dist/errors/session.d.ts.map +1 -0
- package/dist/errors/session.js +11 -0
- package/dist/errors/session.js.map +1 -0
- package/dist/errors/transport.d.ts +42 -0
- package/dist/errors/transport.d.ts.map +1 -0
- package/dist/errors/transport.js +49 -0
- package/dist/errors/transport.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/platform.d.ts +12 -0
- package/dist/internal/platform.d.ts.map +1 -0
- package/dist/internal/platform.js +11 -0
- package/dist/internal/platform.js.map +1 -0
- package/dist/internal/singleFlight.d.ts +29 -0
- package/dist/internal/singleFlight.d.ts.map +1 -0
- package/dist/internal/singleFlight.js +41 -0
- package/dist/internal/singleFlight.js.map +1 -0
- package/dist/profiles.d.ts +179 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +191 -0
- package/dist/profiles.js.map +1 -0
- package/dist/protocol/jsonrpc.d.ts +127 -0
- package/dist/protocol/jsonrpc.d.ts.map +1 -0
- package/dist/protocol/jsonrpc.js +117 -0
- package/dist/protocol/jsonrpc.js.map +1 -0
- package/dist/records/index.d.ts +14 -0
- package/dist/records/index.d.ts.map +1 -0
- package/dist/records/index.js +14 -0
- package/dist/records/index.js.map +1 -0
- package/dist/records/recordModel.d.ts +131 -0
- package/dist/records/recordModel.d.ts.map +1 -0
- package/dist/records/recordModel.js +101 -0
- package/dist/records/recordModel.js.map +1 -0
- package/dist/records/related.d.ts +42 -0
- package/dist/records/related.d.ts.map +1 -0
- package/dist/records/related.js +51 -0
- package/dist/records/related.js.map +1 -0
- package/dist/records/relations.d.ts +66 -0
- package/dist/records/relations.d.ts.map +1 -0
- package/dist/records/relations.js +119 -0
- package/dist/records/relations.js.map +1 -0
- package/dist/records/spec.d.ts +44 -0
- package/dist/records/spec.d.ts.map +1 -0
- package/dist/records/spec.js +63 -0
- package/dist/records/spec.js.map +1 -0
- package/dist/records/typed.d.ts +71 -0
- package/dist/records/typed.d.ts.map +1 -0
- package/dist/records/typed.js +65 -0
- package/dist/records/typed.js.map +1 -0
- package/dist/rpc.d.ts +90 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +113 -0
- package/dist/rpc.js.map +1 -0
- package/dist/services/db.d.ts +128 -0
- package/dist/services/db.d.ts.map +1 -0
- package/dist/services/db.js +132 -0
- package/dist/services/db.js.map +1 -0
- package/dist/services/report.d.ts +48 -0
- package/dist/services/report.d.ts.map +1 -0
- package/dist/services/report.js +103 -0
- package/dist/services/report.js.map +1 -0
- package/dist/session/cookie.d.ts +114 -0
- package/dist/session/cookie.d.ts.map +1 -0
- package/dist/session/cookie.js +231 -0
- package/dist/session/cookie.js.map +1 -0
- package/dist/testing/fakeTransport.d.ts +40 -0
- package/dist/testing/fakeTransport.d.ts.map +1 -0
- package/dist/testing/fakeTransport.js +36 -0
- package/dist/testing/fakeTransport.js.map +1 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/transport.d.ts +62 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +9 -0
- package/dist/transport.js.map +1 -0
- package/dist/transports/json2.d.ts +41 -0
- package/dist/transports/json2.d.ts.map +1 -0
- package/dist/transports/json2.js +196 -0
- package/dist/transports/json2.js.map +1 -0
- package/dist/transports/jsonrpc.d.ts +41 -0
- package/dist/transports/jsonrpc.d.ts.map +1 -0
- package/dist/transports/jsonrpc.js +119 -0
- package/dist/transports/jsonrpc.js.map +1 -0
- package/dist/transports/web.d.ts +15 -0
- package/dist/transports/web.d.ts.map +1 -0
- package/dist/transports/web.js +62 -0
- package/dist/transports/web.js.map +1 -0
- package/dist/version-live.d.ts +41 -0
- package/dist/version-live.d.ts.map +1 -0
- package/dist/version-live.js +26 -0
- package/dist/version-live.js.map +1 -0
- package/dist/version.d.ts +78 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +46 -0
- package/dist/version.js.map +1 -0
- package/docs/protocol-verification.md +43 -0
- package/package.json +71 -0
- package/src/client.ts +731 -0
- package/src/combinators/retryOnSessionExpired.ts +25 -0
- package/src/commands.ts +112 -0
- package/src/config.ts +110 -0
- package/src/domain.ts +91 -0
- package/src/errors/auth.ts +28 -0
- package/src/errors/index.ts +50 -0
- package/src/errors/mapFault.ts +92 -0
- package/src/errors/protocol.ts +19 -0
- package/src/errors/schema.ts +18 -0
- package/src/errors/server.ts +79 -0
- package/src/errors/session.ts +12 -0
- package/src/errors/transport.ts +74 -0
- package/src/index.ts +152 -0
- package/src/internal/platform.ts +22 -0
- package/src/internal/singleFlight.ts +80 -0
- package/src/profiles.ts +386 -0
- package/src/protocol/jsonrpc.ts +162 -0
- package/src/records/index.ts +49 -0
- package/src/records/recordModel.ts +211 -0
- package/src/records/related.ts +80 -0
- package/src/records/relations.ts +192 -0
- package/src/records/spec.ts +96 -0
- package/src/records/typed.ts +180 -0
- package/src/rpc.ts +219 -0
- package/src/services/db.ts +282 -0
- package/src/services/report.ts +165 -0
- package/src/session/cookie.ts +461 -0
- package/src/testing/fakeTransport.ts +84 -0
- package/src/testing/index.ts +9 -0
- package/src/transport.ts +72 -0
- package/src/transports/json2.ts +303 -0
- package/src/transports/jsonrpc.ts +218 -0
- package/src/transports/web.ts +96 -0
- package/src/version-live.ts +67 -0
- package/src/version.ts +102 -0
|
@@ -0,0 +1,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
|
package/dist/rpc.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"}
|