@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,180 @@
|
|
|
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.ts";
|
|
12
|
+
import type { Rpc } from "../rpc.ts";
|
|
13
|
+
import type { TransportCallError } from "../transport.ts";
|
|
14
|
+
import { collectRefIds, makeRelatedMap, type RelatedMap } from "./related.ts";
|
|
15
|
+
import type { Many2OneRefValue } from "./relations.ts";
|
|
16
|
+
|
|
17
|
+
/** A decoded row must carry its `id` — every traversal and join keys on it. */
|
|
18
|
+
export type HasId = { readonly id: number };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The field names of `Row` whose decoded value is a many2one reference
|
|
22
|
+
* (`Many2OneRefValue | null`) — the only fields a relation can be traversed
|
|
23
|
+
* through. A compile-time constraint: passing any other key to
|
|
24
|
+
* {@link TypedRecordSet.fetchRelated}/`joinRelated` is a type error, not a
|
|
25
|
+
* runtime surprise.
|
|
26
|
+
*/
|
|
27
|
+
export type Many2OneRefField<Row> = {
|
|
28
|
+
readonly [K in keyof Row]: Row[K] extends Many2OneRefValue | null ? K : never;
|
|
29
|
+
}[keyof Row];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An immutable snapshot of typed rows with explicit relation traversal. Obtain
|
|
33
|
+
* one from `OdooClient.searchRecordsTyped`; it is bound to the `Rpc` seam and
|
|
34
|
+
* model it was read from so `fetchRelated` can batch a co-model `read`.
|
|
35
|
+
*/
|
|
36
|
+
export interface TypedRecordSet<Row extends HasId> extends Iterable<Row> {
|
|
37
|
+
/** The model these rows were read from. */
|
|
38
|
+
readonly model: string;
|
|
39
|
+
/** The decoded rows, in server order. Immutable. */
|
|
40
|
+
readonly rows: ReadonlyArray<Row>;
|
|
41
|
+
/** Each row's `id`, in row order. */
|
|
42
|
+
readonly ids: ReadonlyArray<number>;
|
|
43
|
+
/** The number of rows. */
|
|
44
|
+
readonly length: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetch the related records behind a many2one `field` in ONE batched `read`
|
|
48
|
+
* over the distinct, non-null ids across the whole snapshot, decoded through
|
|
49
|
+
* `schema`. Returns a {@link RelatedMap} keyed by related id — a fresh
|
|
50
|
+
* snapshot; the recordset is never back-filled.
|
|
51
|
+
*
|
|
52
|
+
* ZERO round trips when no row references a related record (all empty). The
|
|
53
|
+
* batched-single-read guarantee is the entire point of this API and is
|
|
54
|
+
* asserted in the tests, not merely documented.
|
|
55
|
+
*/
|
|
56
|
+
readonly fetchRelated: <K extends Many2OneRefField<Row>, A extends HasId, I = A>(
|
|
57
|
+
field: K,
|
|
58
|
+
relatedModel: string,
|
|
59
|
+
schema: Schema.Codec<A, I>,
|
|
60
|
+
fields?: ReadonlyArray<string>,
|
|
61
|
+
) => Effect.Effect<RelatedMap<A>, TransportCallError>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Like {@link fetchRelated}, but returns each row paired with its joined
|
|
65
|
+
* related record (`null` when the many2one is empty or unresolved). One
|
|
66
|
+
* explicit, batched call — named to keep "this is a fetch" legible at the call
|
|
67
|
+
* site.
|
|
68
|
+
*/
|
|
69
|
+
readonly joinRelated: <K extends Many2OneRefField<Row>, A extends HasId, I = A>(
|
|
70
|
+
field: K,
|
|
71
|
+
relatedModel: string,
|
|
72
|
+
schema: Schema.Codec<A, I>,
|
|
73
|
+
fields?: ReadonlyArray<string>,
|
|
74
|
+
) => Effect.Effect<ReadonlyArray<readonly [Row, A | null]>, TransportCallError>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The subset of the {@link Rpc} service {@link TypedRecordSet} depends on. */
|
|
78
|
+
type RpcSeam = Pick<typeof Rpc.Service, "callKw">;
|
|
79
|
+
|
|
80
|
+
/** Boundary decode: a related-`read` payload that fails the caller schema is drift. */
|
|
81
|
+
const decodeRows =
|
|
82
|
+
<A, I>(schema: Schema.Codec<A, I>, context: string) =>
|
|
83
|
+
(raw: unknown): Effect.Effect<ReadonlyArray<A>, SchemaDriftError> =>
|
|
84
|
+
Schema.decodeUnknownEffect(Schema.Array(schema))(raw).pipe(
|
|
85
|
+
Effect.mapError((cause) => new SchemaDriftError({ context, payload: raw, cause })),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read a many2one field off a row as a traversable ref value, or fail the whole
|
|
90
|
+
* traversal as a defect. The type system already forbids non-ref fields; this
|
|
91
|
+
* guards the dynamic-misuse escape hatch (e.g. an `any`-typed field or a schema
|
|
92
|
+
* that decoded the wrong shape) — a `TypeError`-equivalent `Effect.die` with a
|
|
93
|
+
* precise locator, never a silent skip.
|
|
94
|
+
*/
|
|
95
|
+
const readRefField = <Row extends HasId>(
|
|
96
|
+
row: Row,
|
|
97
|
+
field: PropertyKey,
|
|
98
|
+
model: string,
|
|
99
|
+
): Effect.Effect<Many2OneRefValue | null> => {
|
|
100
|
+
const value = (row as Record<PropertyKey, unknown>)[field];
|
|
101
|
+
if (value === null || value === undefined) {
|
|
102
|
+
return Effect.succeed(null);
|
|
103
|
+
}
|
|
104
|
+
if (typeof value === "object" && typeof (value as { id?: unknown }).id === "number") {
|
|
105
|
+
return Effect.succeed(value as Many2OneRefValue);
|
|
106
|
+
}
|
|
107
|
+
return Effect.die(
|
|
108
|
+
`TypedRecordSet(${model}).fetchRelated: field ${String(field)} of record ${row.id} is not a ` +
|
|
109
|
+
`many2one reference ({ id, name } | null); got ${typeof value}. Declare it with ` +
|
|
110
|
+
`Many2OneRefOrNull so traversal is type-safe.`,
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Construct a {@link TypedRecordSet} over already-decoded rows. Internal: the
|
|
116
|
+
* public entry point is `OdooClient.searchRecordsTyped`, which owns the initial
|
|
117
|
+
* `search_read` decode and passes the resulting rows here.
|
|
118
|
+
*/
|
|
119
|
+
export const make = <Row extends HasId>(
|
|
120
|
+
rpc: RpcSeam,
|
|
121
|
+
model: string,
|
|
122
|
+
rows: ReadonlyArray<Row>,
|
|
123
|
+
): TypedRecordSet<Row> => {
|
|
124
|
+
const ids = rows.map((row) => row.id);
|
|
125
|
+
|
|
126
|
+
const fetchRelated = <K extends Many2OneRefField<Row>, A extends HasId, I = A>(
|
|
127
|
+
field: K,
|
|
128
|
+
relatedModel: string,
|
|
129
|
+
schema: Schema.Codec<A, I>,
|
|
130
|
+
fields?: ReadonlyArray<string>,
|
|
131
|
+
): Effect.Effect<RelatedMap<A>, TransportCallError> =>
|
|
132
|
+
// Validate every row's field up front (dynamic-misuse guard), then collect
|
|
133
|
+
// the distinct ids purely and issue AT MOST one read.
|
|
134
|
+
Effect.forEach(rows, (row) => readRefField(row, field, model)).pipe(
|
|
135
|
+
Effect.flatMap((refs) => {
|
|
136
|
+
const relatedIds = collectRefIds(refs, (ref) => ref);
|
|
137
|
+
if (relatedIds.length === 0) {
|
|
138
|
+
// No referenced records → no round trip. Asserted in the tests.
|
|
139
|
+
return Effect.succeed(makeRelatedMap<A>([]));
|
|
140
|
+
}
|
|
141
|
+
return rpc
|
|
142
|
+
.callKw(
|
|
143
|
+
relatedModel,
|
|
144
|
+
"read",
|
|
145
|
+
[],
|
|
146
|
+
fields === undefined ? {} : { fields },
|
|
147
|
+
{ ids: relatedIds },
|
|
148
|
+
)
|
|
149
|
+
.pipe(
|
|
150
|
+
Effect.flatMap(decodeRows(schema, `${relatedModel}.read`)),
|
|
151
|
+
Effect.map((relatedRows) => makeRelatedMap(relatedRows)),
|
|
152
|
+
);
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const joinRelated = <K extends Many2OneRefField<Row>, A extends HasId, I = A>(
|
|
157
|
+
field: K,
|
|
158
|
+
relatedModel: string,
|
|
159
|
+
schema: Schema.Codec<A, I>,
|
|
160
|
+
fields?: ReadonlyArray<string>,
|
|
161
|
+
): Effect.Effect<ReadonlyArray<readonly [Row, A | null]>, TransportCallError> =>
|
|
162
|
+
fetchRelated(field, relatedModel, schema, fields).pipe(
|
|
163
|
+
Effect.map((related) =>
|
|
164
|
+
rows.map(
|
|
165
|
+
(row) =>
|
|
166
|
+
[row, related.get((row as Record<PropertyKey, unknown>)[field] as never)] as const,
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
model,
|
|
173
|
+
rows,
|
|
174
|
+
ids,
|
|
175
|
+
length: rows.length,
|
|
176
|
+
[Symbol.iterator]: () => rows[Symbol.iterator](),
|
|
177
|
+
fetchRelated,
|
|
178
|
+
joinRelated,
|
|
179
|
+
};
|
|
180
|
+
};
|
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
2
|
+
import { SchemaDriftError } from "./errors/schema.ts";
|
|
3
|
+
import * as SingleFlight from "./internal/singleFlight.ts";
|
|
4
|
+
import {
|
|
5
|
+
type CallKwParams,
|
|
6
|
+
Transport,
|
|
7
|
+
type TransportCallError,
|
|
8
|
+
type TransportDialect,
|
|
9
|
+
} from "./transport.ts";
|
|
10
|
+
|
|
11
|
+
/** An Odoo `context` dict: opaque keys, values we do not model. */
|
|
12
|
+
export type OdooContext = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
/** A plain object (a mergeable context), excluding arrays and `null`. */
|
|
15
|
+
const isPlainObject = (value: unknown): value is OdooContext =>
|
|
16
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
+
|
|
18
|
+
/** Copy `kwargs` without its `context` key (never mutates the caller's object). */
|
|
19
|
+
const omitContext = (kwargs: OdooContext): OdooContext => {
|
|
20
|
+
const { context: _context, ...rest } = kwargs;
|
|
21
|
+
return rest;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The session/base Odoo `context` — the innermost, lowest-precedence layer of
|
|
26
|
+
* the context merge (e.g. `uid`, `lang`, `tz` established at authentication).
|
|
27
|
+
*
|
|
28
|
+
* Provided by the auth/transport layer that owns the session; absent here it
|
|
29
|
+
* defaults to `{}`. It is read once, when the {@link Rpc} layer is built.
|
|
30
|
+
*/
|
|
31
|
+
export class GlobalContext extends Context.Service<
|
|
32
|
+
GlobalContext,
|
|
33
|
+
OdooContext
|
|
34
|
+
>()("odoo-rpc-ts/GlobalContext") {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The `call_kw` choke point. Every high-level operation funnels through here so
|
|
38
|
+
* the Odoo `context` is merged in exactly one place, with a single, documented
|
|
39
|
+
* precedence (see {@link callKw}).
|
|
40
|
+
*/
|
|
41
|
+
export class Rpc extends Context.Service<
|
|
42
|
+
Rpc,
|
|
43
|
+
{
|
|
44
|
+
/** The underlying transport's wire dialect, for ops that must branch on it. */
|
|
45
|
+
readonly dialect: TransportDialect;
|
|
46
|
+
/**
|
|
47
|
+
* Execute one `call_kw`, merging the effective `context` and forwarding to
|
|
48
|
+
* the {@link Transport}. Caller inputs are never mutated — `args`, `kwargs`,
|
|
49
|
+
* and every context object are treated as read-only and copied.
|
|
50
|
+
*
|
|
51
|
+
* Context precedence (lowest → highest, later wins):
|
|
52
|
+
* session/base ({@link GlobalContext}) < layer overrides
|
|
53
|
+
* < `kwargs.context` < `options.context`.
|
|
54
|
+
*
|
|
55
|
+
* A `context` key inside `kwargs` is treated as a caller-level context (the
|
|
56
|
+
* ergonomic form `callKw(m, meth, [], { context })`), merged just below
|
|
57
|
+
* `options.context` and then stripped from `kwargs` so it never wires twice.
|
|
58
|
+
* If it is present but not a plain object it is a caller defect and fails
|
|
59
|
+
* with `Effect.die`.
|
|
60
|
+
*/
|
|
61
|
+
readonly callKw: (
|
|
62
|
+
model: string,
|
|
63
|
+
method: string,
|
|
64
|
+
args: ReadonlyArray<unknown>,
|
|
65
|
+
kwargs?: OdooContext,
|
|
66
|
+
options?: { readonly context?: OdooContext; readonly ids?: ReadonlyArray<number> },
|
|
67
|
+
) => Effect.Effect<unknown, TransportCallError>;
|
|
68
|
+
}
|
|
69
|
+
>()("odoo-rpc-ts/Rpc") {}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The provider seam for a lazily-seeded session/base context.
|
|
73
|
+
*
|
|
74
|
+
* A `Layer` resolves eagerly at build time, but the base tier must be lazy and
|
|
75
|
+
* retryable-on-failure (a transport error resolving it must surface as the
|
|
76
|
+
* failing call's error, never be baked in). So instead of a value provider we
|
|
77
|
+
* take an `Effect` that yields the base context; {@link buildRpc} runs it behind
|
|
78
|
+
* a success-only single-flight on the FIRST `callKw` (and retries on failure).
|
|
79
|
+
*
|
|
80
|
+
* Merge position: below the layer overrides, above the static {@link GlobalContext}
|
|
81
|
+
* tag (which, when both are present, seeds the innermost defaults).
|
|
82
|
+
*/
|
|
83
|
+
export type GlobalContextProvider = Effect.Effect<OdooContext, TransportCallError>;
|
|
84
|
+
|
|
85
|
+
const ContextRecord = Schema.Record(Schema.String, Schema.Unknown);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Decode a raw `res.users.context_get` result into an {@link OdooContext};
|
|
89
|
+
* drift (a non-object payload) fails as {@link SchemaDriftError}, which is part
|
|
90
|
+
* of {@link TransportCallError} and so propagates as the seeding call's error.
|
|
91
|
+
*/
|
|
92
|
+
const decodeContext = (raw: unknown): Effect.Effect<OdooContext, SchemaDriftError> =>
|
|
93
|
+
Schema.decodeUnknownEffect(ContextRecord)(raw).pipe(
|
|
94
|
+
Effect.mapError(
|
|
95
|
+
(cause) => new SchemaDriftError({ context: "res.users.context_get", payload: raw, cause }),
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The shared {@link Rpc} service constructor. `provider`, when given, seeds the
|
|
101
|
+
* base tier lazily (single-flight on first call). The static {@link GlobalContext}
|
|
102
|
+
* tag is always read (defaulting to `{}`) and sits at the very bottom.
|
|
103
|
+
*/
|
|
104
|
+
const buildRpc = (
|
|
105
|
+
transport: typeof Transport.Service,
|
|
106
|
+
config: { readonly globalContext?: OdooContext } | undefined,
|
|
107
|
+
provider: GlobalContextProvider | undefined,
|
|
108
|
+
): Effect.Effect<typeof Rpc.Service> =>
|
|
109
|
+
Effect.gen(function* () {
|
|
110
|
+
const staticBase = Option.getOrElse(
|
|
111
|
+
yield* Effect.serviceOption(GlobalContext),
|
|
112
|
+
(): OdooContext => ({}),
|
|
113
|
+
);
|
|
114
|
+
const layerOverrides = config?.globalContext ?? {};
|
|
115
|
+
|
|
116
|
+
// No provider: the base tier is the static tag value (today's behavior,
|
|
117
|
+
// resolved once at build). With a provider: resolve it lazily behind a
|
|
118
|
+
// success-only single-flight and layer it over the static base.
|
|
119
|
+
const resolveBase: Effect.Effect<OdooContext, TransportCallError> =
|
|
120
|
+
provider === undefined
|
|
121
|
+
? Effect.succeed(staticBase)
|
|
122
|
+
: yield* SingleFlight.make(provider).pipe(
|
|
123
|
+
Effect.map((sf) =>
|
|
124
|
+
sf.get.pipe(Effect.map((seeded): OdooContext => ({ ...staticBase, ...seeded }))),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const callKw = (
|
|
129
|
+
model: string,
|
|
130
|
+
method: string,
|
|
131
|
+
args: ReadonlyArray<unknown>,
|
|
132
|
+
kwargs?: OdooContext,
|
|
133
|
+
options?: { readonly context?: OdooContext; readonly ids?: ReadonlyArray<number> },
|
|
134
|
+
): Effect.Effect<unknown, TransportCallError> => {
|
|
135
|
+
// Pull any caller-supplied `kwargs.context` out of the spread so it is
|
|
136
|
+
// merged at the caller tier (not silently clobbered) and never wired twice.
|
|
137
|
+
const hasKwargsContext = kwargs !== undefined && "context" in kwargs;
|
|
138
|
+
const kwargsContext = hasKwargsContext ? kwargs["context"] : undefined;
|
|
139
|
+
if (hasKwargsContext && !isPlainObject(kwargsContext)) {
|
|
140
|
+
return Effect.die(
|
|
141
|
+
`Rpc.callKw: kwargs.context for "${model}.${method}" must be a plain object, ` +
|
|
142
|
+
`got ${typeof kwargsContext}.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const restKwargs = hasKwargsContext ? omitContext(kwargs) : (kwargs ?? {});
|
|
146
|
+
|
|
147
|
+
return resolveBase.pipe(
|
|
148
|
+
Effect.flatMap((base) => {
|
|
149
|
+
const context: OdooContext = {
|
|
150
|
+
...base,
|
|
151
|
+
...layerOverrides,
|
|
152
|
+
...(kwargsContext as OdooContext | undefined),
|
|
153
|
+
...options?.context,
|
|
154
|
+
};
|
|
155
|
+
const params: CallKwParams = {
|
|
156
|
+
model,
|
|
157
|
+
method,
|
|
158
|
+
args,
|
|
159
|
+
kwargs: { ...restKwargs, context },
|
|
160
|
+
...(options?.ids !== undefined ? { ids: options.ids } : {}),
|
|
161
|
+
};
|
|
162
|
+
return transport.callKw(params);
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return { dialect: transport.dialect, callKw };
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build the {@link Rpc} layer over a {@link Transport}, optionally baking in
|
|
172
|
+
* layer-level `globalContext` overrides that sit above the session/base context
|
|
173
|
+
* but below any per-call context, and/or a {@link GlobalContextProvider} that
|
|
174
|
+
* lazily seeds the base tier on the first call.
|
|
175
|
+
*/
|
|
176
|
+
export const layerWith = (config?: {
|
|
177
|
+
readonly globalContext?: OdooContext;
|
|
178
|
+
readonly globalContextProvider?: GlobalContextProvider;
|
|
179
|
+
}): Layer.Layer<Rpc, never, Transport> =>
|
|
180
|
+
Layer.effect(
|
|
181
|
+
Rpc,
|
|
182
|
+
Effect.gen(function* () {
|
|
183
|
+
const transport = yield* Transport;
|
|
184
|
+
return yield* buildRpc(transport, config, config?.globalContextProvider);
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The {@link Rpc} layer with the base tier seeded from the ambient
|
|
190
|
+
* {@link Transport}'s server-side user context.
|
|
191
|
+
*
|
|
192
|
+
* Resolves `res.users.context_get` (an `@api.model` method — kwargs-only, so it
|
|
193
|
+
* works identically over `execute_kw` and the JSON-2 by-name dialect) exactly
|
|
194
|
+
* once, lazily on the first `callKw`, behind a success-only single-flight. A
|
|
195
|
+
* transport/decoding failure while seeding is NOT swallowed: it surfaces as that
|
|
196
|
+
* first call's error and the seed is retried on the next call.
|
|
197
|
+
*/
|
|
198
|
+
export const layerSeeded = (config?: {
|
|
199
|
+
readonly globalContext?: OdooContext;
|
|
200
|
+
}): Layer.Layer<Rpc, never, Transport> =>
|
|
201
|
+
Layer.effect(
|
|
202
|
+
Rpc,
|
|
203
|
+
Effect.gen(function* () {
|
|
204
|
+
const transport = yield* Transport;
|
|
205
|
+
const provider: GlobalContextProvider = transport
|
|
206
|
+
.callKw({ model: "res.users", method: "context_get", args: [], kwargs: {} })
|
|
207
|
+
.pipe(Effect.flatMap(decodeContext));
|
|
208
|
+
return yield* buildRpc(transport, config, provider);
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The default {@link Rpc} layer: base tier seeded from the server-side user
|
|
214
|
+
* context ({@link layerSeeded}), so translated fields (stage names, selection
|
|
215
|
+
* labels, …) come back in the user's language instead of the source terms —
|
|
216
|
+
* matching odoo-rpc-py's `seed_context=True` default. Opt out with
|
|
217
|
+
* {@link layerWith} (no provider) when calls must carry no ambient context.
|
|
218
|
+
*/
|
|
219
|
+
export const layer: Layer.Layer<Rpc, never, Transport> = layerSeeded();
|
|
@@ -0,0 +1,282 @@
|
|
|
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 type { FaultCallSite } from "../errors/mapFault.ts";
|
|
33
|
+
import { SchemaDriftError } from "../errors/schema.ts";
|
|
34
|
+
import type { OdooAuthenticationError } from "../errors/auth.ts";
|
|
35
|
+
import type { OdooServerFault } from "../errors/server.ts";
|
|
36
|
+
import type { SessionExpiredError } from "../errors/session.ts";
|
|
37
|
+
import { OdooTransportError } from "../errors/transport.ts";
|
|
38
|
+
import { HttpClient, HttpClientRequest } from "../internal/platform.ts";
|
|
39
|
+
import {
|
|
40
|
+
buildRequest,
|
|
41
|
+
JsonRpcResponse,
|
|
42
|
+
mapJsonRpcError,
|
|
43
|
+
nextRequestId,
|
|
44
|
+
} from "../protocol/jsonrpc.ts";
|
|
45
|
+
|
|
46
|
+
/** The failure channel a single `db` round trip can produce. */
|
|
47
|
+
type DbCallError =
|
|
48
|
+
| OdooTransportError
|
|
49
|
+
| SchemaDriftError
|
|
50
|
+
| SessionExpiredError
|
|
51
|
+
| OdooServerFault
|
|
52
|
+
| OdooAuthenticationError;
|
|
53
|
+
|
|
54
|
+
/** Join the base URL with the `/jsonrpc` path, tolerating a trailing slash. */
|
|
55
|
+
const jsonRpcEndpoint = (url: URL): string => `${url.href.replace(/\/+$/, "")}/jsonrpc`;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One `db`-service round trip: build the `{service:"db", method, args}`
|
|
59
|
+
* envelope, POST it, decode the JSON-RPC response, and either return the raw
|
|
60
|
+
* `result` or map the fault through the shared choke point.
|
|
61
|
+
*
|
|
62
|
+
* GOTCHA: never record `args` on the span — the master password rides in
|
|
63
|
+
* position 0 for gated methods. Only the method name is safe.
|
|
64
|
+
*/
|
|
65
|
+
const dbCall = (
|
|
66
|
+
url: URL,
|
|
67
|
+
method: string,
|
|
68
|
+
args: ReadonlyArray<unknown>,
|
|
69
|
+
): Effect.Effect<unknown, DbCallError, HttpClient.HttpClient> =>
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const client = yield* HttpClient.HttpClient;
|
|
72
|
+
const endpoint = jsonRpcEndpoint(url);
|
|
73
|
+
const id = nextRequestId();
|
|
74
|
+
const site: FaultCallSite = { method: `db.${method}` };
|
|
75
|
+
|
|
76
|
+
const request = HttpClientRequest.post(endpoint).pipe(
|
|
77
|
+
HttpClientRequest.setHeader("Content-Type", "application/json"),
|
|
78
|
+
HttpClientRequest.bodyJsonUnsafe(buildRequest({ service: "db", method, args }, id)),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const payload: unknown = yield* client.execute(request).pipe(
|
|
82
|
+
Effect.flatMap((response) => response.json),
|
|
83
|
+
Effect.mapError((cause) =>
|
|
84
|
+
OdooTransportError.fromHttpClientError({ method: "POST", url: endpoint }, cause),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const decoded = yield* Schema.decodeUnknownEffect(JsonRpcResponse)(payload).pipe(
|
|
89
|
+
Effect.mapError(
|
|
90
|
+
(cause) => new SchemaDriftError({ context: "odoo.db response", payload, cause }),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if ("error" in decoded) {
|
|
95
|
+
return yield* Effect.fail(mapJsonRpcError(decoded.error, site));
|
|
96
|
+
}
|
|
97
|
+
return decoded.result;
|
|
98
|
+
}).pipe(Effect.withSpan("odoo.db", { attributes: { "odoo.db.method": method } }));
|
|
99
|
+
|
|
100
|
+
/** Decode a `db` result through `schema`, raising {@link SchemaDriftError} on drift. */
|
|
101
|
+
const decodeResult = <A, I>(
|
|
102
|
+
schema: Schema.Codec<A, I>,
|
|
103
|
+
context: string,
|
|
104
|
+
result: unknown,
|
|
105
|
+
): Effect.Effect<A, SchemaDriftError> =>
|
|
106
|
+
Schema.decodeUnknownEffect(schema)(result).pipe(
|
|
107
|
+
Effect.mapError((cause) => new SchemaDriftError({ context, payload: result, cause })),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* List the databases the server is willing to expose (honours `list_db` and any
|
|
112
|
+
* `dbfilter`). Unauthenticated — no master password required.
|
|
113
|
+
*/
|
|
114
|
+
export const listDatabases = (
|
|
115
|
+
url: URL,
|
|
116
|
+
): Effect.Effect<ReadonlyArray<string>, DbCallError, HttpClient.HttpClient> =>
|
|
117
|
+
dbCall(url, "list", []).pipe(
|
|
118
|
+
Effect.flatMap((result) =>
|
|
119
|
+
decodeResult(Schema.Array(Schema.String), "odoo.db exp_list", result),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
/** Whether a database of the given name exists. Unauthenticated. */
|
|
124
|
+
export const exists = (
|
|
125
|
+
url: URL,
|
|
126
|
+
name: string,
|
|
127
|
+
): Effect.Effect<boolean, DbCallError, HttpClient.HttpClient> =>
|
|
128
|
+
dbCall(url, "db_exist", [name]).pipe(
|
|
129
|
+
Effect.flatMap((result) => decodeResult(Schema.Boolean, "odoo.db exp_db_exist", result)),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
/** Options for {@link create}. `master` and `adminPassword` are the two secrets. */
|
|
133
|
+
export interface CreateOptions {
|
|
134
|
+
readonly master: Redacted.Redacted<string>;
|
|
135
|
+
readonly name: string;
|
|
136
|
+
/** Install demo data. Defaults to `false`. */
|
|
137
|
+
readonly demo?: boolean;
|
|
138
|
+
/** Load language / locale (e.g. `"en_US"`). Defaults to `"en_US"`. */
|
|
139
|
+
readonly lang?: string;
|
|
140
|
+
/** Login of the initial admin user. Defaults to `"admin"`. */
|
|
141
|
+
readonly adminLogin?: string;
|
|
142
|
+
/** Password of the initial admin user. */
|
|
143
|
+
readonly adminPassword: Redacted.Redacted<string>;
|
|
144
|
+
/** ISO country code to seed localization (e.g. `"US"`). */
|
|
145
|
+
readonly countryCode?: string;
|
|
146
|
+
/** Company phone to seed. */
|
|
147
|
+
readonly phone?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a fresh database and its initial admin user.
|
|
152
|
+
*
|
|
153
|
+
* Wire (after `dispatch` strips the master): `exp_create_database(db_name,
|
|
154
|
+
* demo, lang, user_password='admin', login='admin', country_code=None,
|
|
155
|
+
* phone=None)`.
|
|
156
|
+
*/
|
|
157
|
+
export const create = (
|
|
158
|
+
url: URL,
|
|
159
|
+
options: CreateOptions,
|
|
160
|
+
): Effect.Effect<void, DbCallError, HttpClient.HttpClient> =>
|
|
161
|
+
dbCall(url, "create_database", [
|
|
162
|
+
Redacted.value(options.master),
|
|
163
|
+
options.name,
|
|
164
|
+
options.demo ?? false,
|
|
165
|
+
options.lang ?? "en_US",
|
|
166
|
+
Redacted.value(options.adminPassword),
|
|
167
|
+
options.adminLogin ?? "admin",
|
|
168
|
+
options.countryCode ?? null,
|
|
169
|
+
options.phone ?? null,
|
|
170
|
+
]).pipe(Effect.asVoid);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Drop a database. Resolves to `true` when a database was actually dropped,
|
|
174
|
+
* `false` when none of that name existed.
|
|
175
|
+
*/
|
|
176
|
+
export const drop = (
|
|
177
|
+
url: URL,
|
|
178
|
+
master: Redacted.Redacted<string>,
|
|
179
|
+
name: string,
|
|
180
|
+
): Effect.Effect<boolean, DbCallError, HttpClient.HttpClient> =>
|
|
181
|
+
dbCall(url, "drop", [Redacted.value(master), name]).pipe(
|
|
182
|
+
Effect.flatMap((result) => decodeResult(Schema.Boolean, "odoo.db exp_drop", result)),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
/** Options for {@link duplicate}. */
|
|
186
|
+
export interface DuplicateOptions {
|
|
187
|
+
/**
|
|
188
|
+
* Neutralize the copy (disable outgoing mail/crons/payment providers, …) so it
|
|
189
|
+
* is safe as a staging database. Maps to `neutralize_database`. Defaults to
|
|
190
|
+
* `false`.
|
|
191
|
+
*/
|
|
192
|
+
readonly neutralize?: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Duplicate `source` into a new database `target`.
|
|
197
|
+
*
|
|
198
|
+
* Wire (after master strip): `exp_duplicate_database(db_original_name, db_name,
|
|
199
|
+
* neutralize_database=False)`.
|
|
200
|
+
*/
|
|
201
|
+
export const duplicate = (
|
|
202
|
+
url: URL,
|
|
203
|
+
master: Redacted.Redacted<string>,
|
|
204
|
+
source: string,
|
|
205
|
+
target: string,
|
|
206
|
+
options: DuplicateOptions = {},
|
|
207
|
+
): Effect.Effect<void, DbCallError, HttpClient.HttpClient> =>
|
|
208
|
+
dbCall(url, "duplicate_database", [
|
|
209
|
+
Redacted.value(master),
|
|
210
|
+
source,
|
|
211
|
+
target,
|
|
212
|
+
options.neutralize ?? false,
|
|
213
|
+
]).pipe(Effect.asVoid);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Dump a database into an in-memory archive.
|
|
217
|
+
*
|
|
218
|
+
* `format` is `"zip"` (default; includes the filestore) or `"dump"` (raw
|
|
219
|
+
* `pg_dump` custom format, database only). Odoo returns the archive
|
|
220
|
+
* base64-encoded inside the JSON-RPC `result`; we decode it to raw bytes.
|
|
221
|
+
*/
|
|
222
|
+
export const dump = (
|
|
223
|
+
url: URL,
|
|
224
|
+
master: Redacted.Redacted<string>,
|
|
225
|
+
name: string,
|
|
226
|
+
format: "zip" | "dump" = "zip",
|
|
227
|
+
): Effect.Effect<Uint8Array, DbCallError, HttpClient.HttpClient> =>
|
|
228
|
+
dbCall(url, "dump", [Redacted.value(master), name, format]).pipe(
|
|
229
|
+
// Odoo returns the archive base64-encoded in `result`; decode to raw bytes,
|
|
230
|
+
// any malformed base64 surfacing as SchemaDriftError like every other drift.
|
|
231
|
+
Effect.flatMap((result) =>
|
|
232
|
+
decodeResult(Schema.Uint8ArrayFromBase64, "odoo.db exp_dump", result),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
/** Options for {@link restore}. */
|
|
237
|
+
export interface RestoreOptions {
|
|
238
|
+
/**
|
|
239
|
+
* Restore as a *copy* — regenerate the database uuid and secret so it does not
|
|
240
|
+
* collide with the original in a mail/cron sense. Maps to `copy`. Defaults to
|
|
241
|
+
* `false`.
|
|
242
|
+
*
|
|
243
|
+
* NOTE: `exp_restore` has NO `neutralize` parameter on any supported version;
|
|
244
|
+
* there is deliberately no such option here.
|
|
245
|
+
*/
|
|
246
|
+
readonly copy?: boolean;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Restore a previously {@link dump}ed archive under a new database `name`.
|
|
251
|
+
*
|
|
252
|
+
* The bytes are re-encoded to the base64 string `exp_restore` expects.
|
|
253
|
+
* Wire (after master strip): `exp_restore(db_name, data, copy=False)`.
|
|
254
|
+
*/
|
|
255
|
+
export const restore = (
|
|
256
|
+
url: URL,
|
|
257
|
+
master: Redacted.Redacted<string>,
|
|
258
|
+
name: string,
|
|
259
|
+
archive: Uint8Array,
|
|
260
|
+
options: RestoreOptions = {},
|
|
261
|
+
): Effect.Effect<void, DbCallError, HttpClient.HttpClient> =>
|
|
262
|
+
dbCall(url, "restore", [
|
|
263
|
+
Redacted.value(master),
|
|
264
|
+
name,
|
|
265
|
+
Encoding.encodeBase64(archive),
|
|
266
|
+
options.copy ?? false,
|
|
267
|
+
]).pipe(Effect.asVoid);
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Change the server master password (`admin_passwd`). `master` is the current
|
|
271
|
+
* password; `next` becomes the new one.
|
|
272
|
+
*
|
|
273
|
+
* Wire (after master strip): `exp_change_admin_password(new_password)`.
|
|
274
|
+
*/
|
|
275
|
+
export const changeMasterPassword = (
|
|
276
|
+
url: URL,
|
|
277
|
+
master: Redacted.Redacted<string>,
|
|
278
|
+
next: Redacted.Redacted<string>,
|
|
279
|
+
): Effect.Effect<void, DbCallError, HttpClient.HttpClient> =>
|
|
280
|
+
dbCall(url, "change_admin_password", [Redacted.value(master), Redacted.value(next)]).pipe(
|
|
281
|
+
Effect.asVoid,
|
|
282
|
+
);
|