@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
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Context, Data, Effect, Layer, Option, Ref } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* A profile's storage operation failed at the consumer-supplied backend (keyring
|
|
4
|
+
* denied, IndexedDB transaction aborted, file unwritable, …). Wraps the opaque
|
|
5
|
+
* platform `cause`; upper layers branch on `_tag`, never on the message. A store
|
|
6
|
+
* failure proves only that *that* operation failed — infer nothing about the
|
|
7
|
+
* other backend's state (see the removal ordering in {@link make}).
|
|
8
|
+
*/
|
|
9
|
+
export class ProfileStoreError extends Data.TaggedError("ProfileStoreError") {
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A profile's metadata was found but its credential secret is not reachable
|
|
13
|
+
* through {@link SecretStore} (the secret was never stored, was removed out of
|
|
14
|
+
* band, or the named profile does not exist at all). Distinct from
|
|
15
|
+
* {@link ProfileStoreError}: the backend answered fine — the secret is simply
|
|
16
|
+
* absent, so the {@link OdooConfig} cannot be reconstructed.
|
|
17
|
+
*/
|
|
18
|
+
export class ProfileSecretMissingError extends Data.TaggedError("ProfileSecretMissingError") {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Consumer-implemented secret vault, keyed by an opaque `account` string. The
|
|
22
|
+
* platform-portable seam: an OS keyring in Node, IndexedDB + WebCrypto in a
|
|
23
|
+
* browser, a `Ref`-backed map in tests. Secrets cross this boundary only as
|
|
24
|
+
* `Redacted` — never a bare string — so they cannot leak into logs or JSON.
|
|
25
|
+
*/
|
|
26
|
+
export class SecretStore extends Context.Service()("odoo-rpc-ts/SecretStore") {
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Consumer-implemented store for the secret-free {@link ProfileData} map, keyed
|
|
30
|
+
* by profile name. A JSON file on disk, `localStorage`, a config table — anything
|
|
31
|
+
* that round-trips a `Record<string, ProfileData>`. Never carries secrets.
|
|
32
|
+
*/
|
|
33
|
+
export class ProfileStorage extends Context.Service()("odoo-rpc-ts/ProfileStorage") {
|
|
34
|
+
}
|
|
35
|
+
/** The keyring account for a profile's secret. One secret per (name, kind). */
|
|
36
|
+
const accountOf = (name, credentialKind) => `${name}:${credentialKind}`;
|
|
37
|
+
const credentialKindOf = (c) => c._tag === "ApiKey" ? "api-key" : "password";
|
|
38
|
+
const secretOf = (c) => c._tag === "ApiKey" ? c.apiKey : c.password;
|
|
39
|
+
/** Rebuild the live {@link OdooConfig} from stored metadata + a resolved secret. */
|
|
40
|
+
const reconstructConfig = (data, secret) => ({
|
|
41
|
+
url: new URL(data.url),
|
|
42
|
+
db: data.db,
|
|
43
|
+
credentials: data.credentialKind === "api-key"
|
|
44
|
+
? { _tag: "ApiKey", username: data.username, apiKey: secret }
|
|
45
|
+
: { _tag: "Password", username: data.username, password: secret },
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Named connection profiles over a {@link SecretStore} + {@link ProfileStorage}
|
|
49
|
+
* seam. The metadata (URL, db, username, protocol) is serializable and travels
|
|
50
|
+
* through `ProfileStorage`; the credential secret travels *only* through
|
|
51
|
+
* `SecretStore` as `Redacted`. Serializing what this service persists can never
|
|
52
|
+
* expose secret material.
|
|
53
|
+
*/
|
|
54
|
+
export class Profiles extends Context.Service()("odoo-rpc-ts/Profiles") {
|
|
55
|
+
}
|
|
56
|
+
/** Hand-written {@link Profiles} layer over the two storage seams. */
|
|
57
|
+
export const layer = Layer.effect(Profiles, Effect.gen(function* () {
|
|
58
|
+
const secrets = yield* SecretStore;
|
|
59
|
+
const storage = yield* ProfileStorage;
|
|
60
|
+
/**
|
|
61
|
+
* Overwriting a profile with a DIFFERENT credentialKind must evict the
|
|
62
|
+
* superseded secret first, or it is orphaned in the keyring forever:
|
|
63
|
+
* removeProfile derives the account from the *current* metadata kind, so a
|
|
64
|
+
* password secret left behind by a password→session switch would be
|
|
65
|
+
* invisible and un-removable. Evict-before-write keeps the failure modes
|
|
66
|
+
* benign: if the subsequent new-secret write fails, the stored metadata
|
|
67
|
+
* still names the OLD kind whose secret is now gone — loadProfile then
|
|
68
|
+
* reports ProfileSecretMissingError rather than returning stale material.
|
|
69
|
+
*/
|
|
70
|
+
const evictSuperseded = (name, newKind, existing) => {
|
|
71
|
+
const previous = existing[name];
|
|
72
|
+
return previous !== undefined && previous.credentialKind !== newKind
|
|
73
|
+
? secrets.remove(accountOf(name, previous.credentialKind))
|
|
74
|
+
: Effect.void;
|
|
75
|
+
};
|
|
76
|
+
const saveProfile = (name, config, protocol) => Effect.gen(function* () {
|
|
77
|
+
const credentialKind = credentialKindOf(config.credentials);
|
|
78
|
+
const data = {
|
|
79
|
+
url: config.url.href,
|
|
80
|
+
db: config.db,
|
|
81
|
+
username: config.credentials.username,
|
|
82
|
+
credentialKind,
|
|
83
|
+
protocol,
|
|
84
|
+
};
|
|
85
|
+
const existing = yield* storage.load();
|
|
86
|
+
yield* evictSuperseded(name, credentialKind, existing);
|
|
87
|
+
// Secret before metadata: if metadata landed first and the secret write
|
|
88
|
+
// then failed, we'd persist a profile that can never be loaded.
|
|
89
|
+
yield* secrets.set(accountOf(name, credentialKind), secretOf(config.credentials));
|
|
90
|
+
yield* storage.save({ ...existing, [name]: data });
|
|
91
|
+
});
|
|
92
|
+
const saveSessionProfile = (name, url, sessionId) => Effect.gen(function* () {
|
|
93
|
+
const data = { url: url.href, credentialKind: "session", protocol: "web" };
|
|
94
|
+
const existing = yield* storage.load();
|
|
95
|
+
yield* evictSuperseded(name, "session", existing);
|
|
96
|
+
// Same ordering as saveProfile: secret (the cookie) before metadata.
|
|
97
|
+
yield* secrets.set(accountOf(name, "session"), sessionId);
|
|
98
|
+
yield* storage.save({ ...existing, [name]: data });
|
|
99
|
+
});
|
|
100
|
+
const loadProfile = (name) => Effect.gen(function* () {
|
|
101
|
+
const all = yield* storage.load();
|
|
102
|
+
const data = all[name];
|
|
103
|
+
if (data === undefined) {
|
|
104
|
+
// No metadata ⇒ no reachable secret either.
|
|
105
|
+
return yield* new ProfileSecretMissingError({ name, account: name });
|
|
106
|
+
}
|
|
107
|
+
// ProfileStorage is a consumer-supplied JSON round-trip — its output is
|
|
108
|
+
// NOT trusted. A corrupted/hand-edited record must fail in the typed
|
|
109
|
+
// channel (ProfileStoreError), never throw past it (new URL TypeError,
|
|
110
|
+
// or a credentials arm missing username/db sailing into OdooConfig).
|
|
111
|
+
const url = yield* Effect.try({
|
|
112
|
+
try: () => new URL(data.url),
|
|
113
|
+
catch: (cause) => new ProfileStoreError({ operation: "profile.decode", cause }),
|
|
114
|
+
});
|
|
115
|
+
if (data.credentialKind !== "session" &&
|
|
116
|
+
(typeof data.username !== "string" || typeof data.db !== "string")) {
|
|
117
|
+
return yield* new ProfileStoreError({
|
|
118
|
+
operation: "profile.decode",
|
|
119
|
+
cause: `stored profile "${name}" (${data.credentialKind}) lacks username/db`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const account = accountOf(name, data.credentialKind);
|
|
123
|
+
const maybeSecret = yield* secrets.get(account);
|
|
124
|
+
if (Option.isNone(maybeSecret)) {
|
|
125
|
+
return yield* new ProfileSecretMissingError({ name, account });
|
|
126
|
+
}
|
|
127
|
+
if (data.credentialKind === "session") {
|
|
128
|
+
// NEVER fabricate an OdooConfig around a session cookie — there is
|
|
129
|
+
// no credential; the consumer feeds this to CookieSessionLive.fromExisting.
|
|
130
|
+
return {
|
|
131
|
+
_tag: "session",
|
|
132
|
+
url,
|
|
133
|
+
sessionId: maybeSecret.value,
|
|
134
|
+
protocol: data.protocol,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
_tag: "credentials",
|
|
139
|
+
config: reconstructConfig(data, maybeSecret.value),
|
|
140
|
+
protocol: data.protocol,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
const listProfiles = () => storage.load();
|
|
144
|
+
const removeProfile = (name) => Effect.gen(function* () {
|
|
145
|
+
const all = yield* storage.load();
|
|
146
|
+
const data = all[name];
|
|
147
|
+
if (data === undefined) {
|
|
148
|
+
return; // Nothing to remove — idempotent.
|
|
149
|
+
}
|
|
150
|
+
// Ordering: remove the SECRET first, then the metadata. The dangerous
|
|
151
|
+
// half-state is an orphaned secret — a credential left in the keyring
|
|
152
|
+
// with no metadata pointing at it, invisible and un-removable. Deleting
|
|
153
|
+
// the secret first guarantees the credential is gone even if the
|
|
154
|
+
// subsequent metadata save fails. The reverse transient (metadata
|
|
155
|
+
// without a secret) is benign and self-heals: loadProfile reports
|
|
156
|
+
// ProfileSecretMissingError rather than handing back a broken config.
|
|
157
|
+
yield* secrets.remove(accountOf(name, data.credentialKind));
|
|
158
|
+
const { [name]: _removed, ...rest } = all;
|
|
159
|
+
yield* storage.save(rest);
|
|
160
|
+
});
|
|
161
|
+
return { saveProfile, saveSessionProfile, loadProfile, listProfiles, removeProfile };
|
|
162
|
+
}));
|
|
163
|
+
/**
|
|
164
|
+
* Ready-made, `Ref`-backed {@link SecretStore} for tests and single-process
|
|
165
|
+
* consumers. Each `.layer` build gets its own isolated map — no shared global
|
|
166
|
+
* state between tests.
|
|
167
|
+
*/
|
|
168
|
+
export const InMemorySecretStore = {
|
|
169
|
+
layer: Layer.effect(SecretStore, Effect.gen(function* () {
|
|
170
|
+
const store = yield* Ref.make({});
|
|
171
|
+
return {
|
|
172
|
+
get: (account) => Ref.get(store).pipe(Effect.map((s) => Option.fromNullishOr(s[account]))),
|
|
173
|
+
set: (account, secret) => Ref.update(store, (s) => ({ ...s, [account]: secret })),
|
|
174
|
+
remove: (account) => Ref.update(store, ({ [account]: _removed, ...rest }) => rest),
|
|
175
|
+
};
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Ready-made, `Ref`-backed {@link ProfileStorage} for tests and single-process
|
|
180
|
+
* consumers. Isolated per `.layer` build, like {@link InMemorySecretStore}.
|
|
181
|
+
*/
|
|
182
|
+
export const InMemoryProfileStorage = {
|
|
183
|
+
layer: Layer.effect(ProfileStorage, Effect.gen(function* () {
|
|
184
|
+
const store = yield* Ref.make({});
|
|
185
|
+
return {
|
|
186
|
+
load: () => Ref.get(store),
|
|
187
|
+
save: (profiles) => Ref.set(store, profiles),
|
|
188
|
+
};
|
|
189
|
+
})),
|
|
190
|
+
};
|
|
191
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.js","sourceRoot":"","sources":["../src/profiles.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { FaultCallSite } from "../errors/mapFault.js";
|
|
3
|
+
import { type OdooServerFault } from "../errors/server.js";
|
|
4
|
+
import type { OdooAuthenticationError } from "../errors/auth.js";
|
|
5
|
+
import { SessionExpiredError } from "../errors/session.js";
|
|
6
|
+
/**
|
|
7
|
+
* The JSON-RPC envelope shared by every route that speaks it (`/jsonrpc`,
|
|
8
|
+
* `/web/session/authenticate`, `/web/dataset/call_kw`). Deliberately
|
|
9
|
+
* transport-agnostic: `params` is opaque here so the same builder serves the
|
|
10
|
+
* `object`/`common` service calls and the web route's `{model, method, ...}`
|
|
11
|
+
* params alike.
|
|
12
|
+
*
|
|
13
|
+
* GOTCHA: Odoo ignores the JSON-RPC `method` member entirely — it dispatches on
|
|
14
|
+
* the URL and reads `params` by name. We still send the spec-mandated
|
|
15
|
+
* `"call"` so the envelope is valid JSON-RPC 2.0.
|
|
16
|
+
*/
|
|
17
|
+
export interface JsonRpcRequest<P> {
|
|
18
|
+
readonly jsonrpc: "2.0";
|
|
19
|
+
readonly method: "call";
|
|
20
|
+
readonly params: P;
|
|
21
|
+
readonly id: number;
|
|
22
|
+
}
|
|
23
|
+
/** Next distinct JSON-RPC request id (log-correlation only; see gotcha above). */
|
|
24
|
+
export declare const nextRequestId: () => number;
|
|
25
|
+
/** Build a JSON-RPC 2.0 request envelope around arbitrary by-name `params`. */
|
|
26
|
+
export declare const buildRequest: <P>(params: P, id: number) => JsonRpcRequest<P>;
|
|
27
|
+
/**
|
|
28
|
+
* The `error.data` payload. Odoo guarantees only `name` (the dotted Python
|
|
29
|
+
* exception class); every other field can be absent depending on the fault and
|
|
30
|
+
* the server's `dev_mode`. Decoded leniently, then normalized to
|
|
31
|
+
* ""/[]/{} before it reaches {@link mapServerFault}.
|
|
32
|
+
*/
|
|
33
|
+
export declare const JsonRpcErrorData: Schema.Struct<{
|
|
34
|
+
readonly name: Schema.String;
|
|
35
|
+
readonly debug: Schema.optional<Schema.String>;
|
|
36
|
+
readonly message: Schema.optional<Schema.String>;
|
|
37
|
+
readonly arguments: Schema.optional<Schema.$Array<Schema.Unknown>>;
|
|
38
|
+
readonly context: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
|
|
39
|
+
}>;
|
|
40
|
+
export type JsonRpcErrorData = typeof JsonRpcErrorData.Type;
|
|
41
|
+
/** The `error` member of a JSON-RPC error response. `data` may be absent. */
|
|
42
|
+
export declare const JsonRpcErrorPayload: Schema.Struct<{
|
|
43
|
+
readonly code: Schema.Number;
|
|
44
|
+
readonly message: Schema.String;
|
|
45
|
+
readonly data: Schema.optional<Schema.Struct<{
|
|
46
|
+
readonly name: Schema.String;
|
|
47
|
+
readonly debug: Schema.optional<Schema.String>;
|
|
48
|
+
readonly message: Schema.optional<Schema.String>;
|
|
49
|
+
readonly arguments: Schema.optional<Schema.$Array<Schema.Unknown>>;
|
|
50
|
+
readonly context: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
|
|
51
|
+
}>>;
|
|
52
|
+
}>;
|
|
53
|
+
export type JsonRpcErrorPayload = typeof JsonRpcErrorPayload.Type;
|
|
54
|
+
/** A JSON-RPC error response: `{jsonrpc, id?, error:{code, message, data?}}`. */
|
|
55
|
+
export declare const JsonRpcErrorResponse: Schema.Struct<{
|
|
56
|
+
readonly jsonrpc: Schema.Literal<"2.0">;
|
|
57
|
+
readonly id: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.String, Schema.Null]>>;
|
|
58
|
+
readonly error: Schema.Struct<{
|
|
59
|
+
readonly code: Schema.Number;
|
|
60
|
+
readonly message: Schema.String;
|
|
61
|
+
readonly data: Schema.optional<Schema.Struct<{
|
|
62
|
+
readonly name: Schema.String;
|
|
63
|
+
readonly debug: Schema.optional<Schema.String>;
|
|
64
|
+
readonly message: Schema.optional<Schema.String>;
|
|
65
|
+
readonly arguments: Schema.optional<Schema.$Array<Schema.Unknown>>;
|
|
66
|
+
readonly context: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
|
|
67
|
+
}>>;
|
|
68
|
+
}>;
|
|
69
|
+
}>;
|
|
70
|
+
export type JsonRpcErrorResponse = typeof JsonRpcErrorResponse.Type;
|
|
71
|
+
export declare const JsonRpcSuccessResponse: Schema.Union<readonly [Schema.Struct<{
|
|
72
|
+
readonly jsonrpc: Schema.Literal<"2.0">;
|
|
73
|
+
readonly id: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.String, Schema.Null]>>;
|
|
74
|
+
readonly result: Schema.Unknown;
|
|
75
|
+
readonly error: Schema.optionalKey<Schema.Never>;
|
|
76
|
+
}>, Schema.Struct<{
|
|
77
|
+
readonly jsonrpc: Schema.Literal<"2.0">;
|
|
78
|
+
readonly id: Schema.Union<readonly [Schema.Number, Schema.String, Schema.Null]>;
|
|
79
|
+
readonly result: Schema.withDecodingDefaultTypeKey<Schema.Unknown, never>;
|
|
80
|
+
readonly error: Schema.optionalKey<Schema.Never>;
|
|
81
|
+
}>]>;
|
|
82
|
+
export type JsonRpcSuccessResponse = typeof JsonRpcSuccessResponse.Type;
|
|
83
|
+
/**
|
|
84
|
+
* A whole JSON-RPC response. The error branch is tried first so a fault is
|
|
85
|
+
* never mistaken for a `result: undefined` success. A body that is neither
|
|
86
|
+
* shape (e.g. a proxy's HTML error page decoded as garbage) fails to decode →
|
|
87
|
+
* the caller raises `SchemaDriftError`.
|
|
88
|
+
*/
|
|
89
|
+
export declare const JsonRpcResponse: Schema.Union<readonly [Schema.Struct<{
|
|
90
|
+
readonly jsonrpc: Schema.Literal<"2.0">;
|
|
91
|
+
readonly id: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.String, Schema.Null]>>;
|
|
92
|
+
readonly error: Schema.Struct<{
|
|
93
|
+
readonly code: Schema.Number;
|
|
94
|
+
readonly message: Schema.String;
|
|
95
|
+
readonly data: Schema.optional<Schema.Struct<{
|
|
96
|
+
readonly name: Schema.String;
|
|
97
|
+
readonly debug: Schema.optional<Schema.String>;
|
|
98
|
+
readonly message: Schema.optional<Schema.String>;
|
|
99
|
+
readonly arguments: Schema.optional<Schema.$Array<Schema.Unknown>>;
|
|
100
|
+
readonly context: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
|
|
101
|
+
}>>;
|
|
102
|
+
}>;
|
|
103
|
+
}>, Schema.Union<readonly [Schema.Struct<{
|
|
104
|
+
readonly jsonrpc: Schema.Literal<"2.0">;
|
|
105
|
+
readonly id: Schema.optional<Schema.Union<readonly [Schema.Number, Schema.String, Schema.Null]>>;
|
|
106
|
+
readonly result: Schema.Unknown;
|
|
107
|
+
readonly error: Schema.optionalKey<Schema.Never>;
|
|
108
|
+
}>, Schema.Struct<{
|
|
109
|
+
readonly jsonrpc: Schema.Literal<"2.0">;
|
|
110
|
+
readonly id: Schema.Union<readonly [Schema.Number, Schema.String, Schema.Null]>;
|
|
111
|
+
readonly result: Schema.withDecodingDefaultTypeKey<Schema.Unknown, never>;
|
|
112
|
+
readonly error: Schema.optionalKey<Schema.Never>;
|
|
113
|
+
}>]>]>;
|
|
114
|
+
export type JsonRpcResponse = typeof JsonRpcResponse.Type;
|
|
115
|
+
/**
|
|
116
|
+
* The single JSON-RPC fault choke point. Both `/jsonrpc` and the web route feed
|
|
117
|
+
* their decoded `error` payload through here so every fault normalizes into the
|
|
118
|
+
* same tagged union.
|
|
119
|
+
*
|
|
120
|
+
* - code `100` → {@link SessionExpiredError} (Odoo's `SessionExpiredException`).
|
|
121
|
+
* - code `404` → {@link OdooServerError} (NotFound; the `data.name` is often a
|
|
122
|
+
* werkzeug class we do not map, so it lands on the base fault by design).
|
|
123
|
+
* - otherwise → {@link mapServerFault} on the normalized `data`; a missing
|
|
124
|
+
* `data` becomes name `"unknown"`, which falls back to {@link OdooServerError}.
|
|
125
|
+
*/
|
|
126
|
+
export declare const mapJsonRpcError: (error: JsonRpcErrorPayload, site?: FaultCallSite) => SessionExpiredError | OdooServerFault | OdooAuthenticationError;
|
|
127
|
+
//# sourceMappingURL=jsonrpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonrpc.d.ts","sourceRoot":"","sources":["../../src/protocol/jsonrpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAQD,kFAAkF;AAClF,eAAO,MAAM,aAAa,QAAO,MAAiC,CAAC;AAEnE,+EAA+E;AAC/E,eAAO,MAAM,YAAY,GAAI,CAAC,UAAU,CAAC,MAAM,MAAM,KAAG,cAAc,CAAC,CAAC,CAKtE,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;;;;;EAM3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,gBAAgB,CAAC,IAAI,CAAC;AAE5D,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB;;;;;;;;;;EAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,mBAAmB,CAAC,IAAI,CAAC;AAMlE,iFAAiF;AACjF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;EAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAC;AAwBpE,eAAO,MAAM,sBAAsB;;;;;;;;;;IAGjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,sBAAsB,CAAC,IAAI,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;MAA+D,CAAC;AAC5F,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,IAAI,CAAC;AAe1D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,UACnB,mBAAmB,SACpB,aAAa,KAClB,mBAAmB,GAAG,eAAe,GAAG,uBAoB1C,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { mapServerFault } from "../errors/mapFault.js";
|
|
3
|
+
import { OdooServerError } from "../errors/server.js";
|
|
4
|
+
import { SessionExpiredError } from "../errors/session.js";
|
|
5
|
+
// GOTCHA: Odoo ignores the JSON-RPC `id` entirely — it never round-trips into
|
|
6
|
+
// dispatch. This process-wide monotonic counter exists only to keep concurrent
|
|
7
|
+
// requests distinguishable in logs/traces; module-level mutable state is
|
|
8
|
+
// acceptable precisely because nothing correctness-bearing reads it back.
|
|
9
|
+
let requestIdCounter = 0;
|
|
10
|
+
/** Next distinct JSON-RPC request id (log-correlation only; see gotcha above). */
|
|
11
|
+
export const nextRequestId = () => (requestIdCounter += 1);
|
|
12
|
+
/** Build a JSON-RPC 2.0 request envelope around arbitrary by-name `params`. */
|
|
13
|
+
export const buildRequest = (params, id) => ({
|
|
14
|
+
jsonrpc: "2.0",
|
|
15
|
+
method: "call",
|
|
16
|
+
params,
|
|
17
|
+
id,
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* The `error.data` payload. Odoo guarantees only `name` (the dotted Python
|
|
21
|
+
* exception class); every other field can be absent depending on the fault and
|
|
22
|
+
* the server's `dev_mode`. Decoded leniently, then normalized to
|
|
23
|
+
* ""/[]/{} before it reaches {@link mapServerFault}.
|
|
24
|
+
*/
|
|
25
|
+
export const JsonRpcErrorData = Schema.Struct({
|
|
26
|
+
name: Schema.String,
|
|
27
|
+
debug: Schema.optional(Schema.String),
|
|
28
|
+
message: Schema.optional(Schema.String),
|
|
29
|
+
arguments: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
30
|
+
context: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
31
|
+
});
|
|
32
|
+
/** The `error` member of a JSON-RPC error response. `data` may be absent. */
|
|
33
|
+
export const JsonRpcErrorPayload = Schema.Struct({
|
|
34
|
+
code: Schema.Number,
|
|
35
|
+
message: Schema.String,
|
|
36
|
+
data: Schema.optional(JsonRpcErrorData),
|
|
37
|
+
});
|
|
38
|
+
// `id` echoes what we sent, but a few Odoo error paths return `null` — accept
|
|
39
|
+
// any of the JSON-RPC-legal id shapes rather than drift on the envelope.
|
|
40
|
+
const JsonRpcId = Schema.Union([Schema.Number, Schema.String, Schema.Null]);
|
|
41
|
+
/** A JSON-RPC error response: `{jsonrpc, id?, error:{code, message, data?}}`. */
|
|
42
|
+
export const JsonRpcErrorResponse = Schema.Struct({
|
|
43
|
+
jsonrpc: Schema.Literal("2.0"),
|
|
44
|
+
id: Schema.optional(JsonRpcId),
|
|
45
|
+
error: JsonRpcErrorPayload,
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* A JSON-RPC success response: `{jsonrpc, id?, result?}` (result is opaque).
|
|
49
|
+
*
|
|
50
|
+
* Odoo 16 and 17 omit the member entirely when a Python method returns
|
|
51
|
+
* `None`. Decode that wire quirk to an explicit `undefined`, so consumers have
|
|
52
|
+
* one stable successful-response shape.
|
|
53
|
+
*/
|
|
54
|
+
const JsonRpcResultSuccessResponse = Schema.Struct({
|
|
55
|
+
jsonrpc: Schema.Literal("2.0"),
|
|
56
|
+
id: Schema.optional(JsonRpcId),
|
|
57
|
+
result: Schema.Unknown,
|
|
58
|
+
error: Schema.optionalKey(Schema.Never),
|
|
59
|
+
});
|
|
60
|
+
const JsonRpcVoidSuccessResponse = Schema.Struct({
|
|
61
|
+
jsonrpc: Schema.Literal("2.0"),
|
|
62
|
+
id: JsonRpcId,
|
|
63
|
+
result: Schema.Unknown.pipe(Schema.withDecodingDefaultTypeKey(Effect.void)),
|
|
64
|
+
// Declare this forbidden member so malformed error envelopes cannot fall
|
|
65
|
+
// through to the missing-result success shape in the union below.
|
|
66
|
+
error: Schema.optionalKey(Schema.Never),
|
|
67
|
+
});
|
|
68
|
+
export const JsonRpcSuccessResponse = Schema.Union([
|
|
69
|
+
JsonRpcResultSuccessResponse,
|
|
70
|
+
JsonRpcVoidSuccessResponse,
|
|
71
|
+
]);
|
|
72
|
+
/**
|
|
73
|
+
* A whole JSON-RPC response. The error branch is tried first so a fault is
|
|
74
|
+
* never mistaken for a `result: undefined` success. A body that is neither
|
|
75
|
+
* shape (e.g. a proxy's HTML error page decoded as garbage) fails to decode →
|
|
76
|
+
* the caller raises `SchemaDriftError`.
|
|
77
|
+
*/
|
|
78
|
+
export const JsonRpcResponse = Schema.Union([JsonRpcErrorResponse, JsonRpcSuccessResponse]);
|
|
79
|
+
/** Normalize a lenient `error.data` (or its absence) into a {@link RawServerFault}. */
|
|
80
|
+
const normalizeFault = (data, fallbackMessage) => ({
|
|
81
|
+
name: data?.name ?? "unknown",
|
|
82
|
+
message: data?.message ?? fallbackMessage,
|
|
83
|
+
arguments: data?.arguments ?? [],
|
|
84
|
+
context: data?.context ?? {},
|
|
85
|
+
// exactOptionalPropertyTypes: attach `debug` only when actually present.
|
|
86
|
+
...(data?.debug !== undefined ? { debug: data.debug } : {}),
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* The single JSON-RPC fault choke point. Both `/jsonrpc` and the web route feed
|
|
90
|
+
* their decoded `error` payload through here so every fault normalizes into the
|
|
91
|
+
* same tagged union.
|
|
92
|
+
*
|
|
93
|
+
* - code `100` → {@link SessionExpiredError} (Odoo's `SessionExpiredException`).
|
|
94
|
+
* - code `404` → {@link OdooServerError} (NotFound; the `data.name` is often a
|
|
95
|
+
* werkzeug class we do not map, so it lands on the base fault by design).
|
|
96
|
+
* - otherwise → {@link mapServerFault} on the normalized `data`; a missing
|
|
97
|
+
* `data` becomes name `"unknown"`, which falls back to {@link OdooServerError}.
|
|
98
|
+
*/
|
|
99
|
+
export const mapJsonRpcError = (error, site = {}) => {
|
|
100
|
+
if (error.code === 100) {
|
|
101
|
+
return new SessionExpiredError({ message: error.message });
|
|
102
|
+
}
|
|
103
|
+
const raw = normalizeFault(error.data, error.message);
|
|
104
|
+
if (error.code === 404) {
|
|
105
|
+
return new OdooServerError({
|
|
106
|
+
name: raw.name,
|
|
107
|
+
message: raw.message,
|
|
108
|
+
arguments: raw.arguments,
|
|
109
|
+
context: raw.context,
|
|
110
|
+
...(raw.debug !== undefined ? { debug: raw.debug } : {}),
|
|
111
|
+
...(site.model !== undefined ? { model: site.model } : {}),
|
|
112
|
+
...(site.method !== undefined ? { method: site.method } : {}),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return mapServerFault(raw, site);
|
|
116
|
+
};
|
|
117
|
+
//# sourceMappingURL=jsonrpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonrpc.js","sourceRoot":"","sources":["../../src/protocol/jsonrpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed records — internal barrel for both per-protocol tiers of the records
|
|
3
|
+
* design: the CLASSIC tier (explicit traversal over the classic `[id, name]`
|
|
4
|
+
* pair protocol, 16+: relation/temporal decode schemas, pure join helpers,
|
|
5
|
+
* the `TypedRecordSet` snapshot) and the SPEC tier (declared prefetch over the
|
|
6
|
+
* 17+ `specification` protocol: `defineRecord` + the specification compiler).
|
|
7
|
+
* Re-exported to the public surface via `src/index.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export { Many2OneRef, Many2OneRefFromWire, Many2OneRefOrNull, Many2OneRefValue, OdooDate, OdooDateOrNull, OdooDateTime, OdooDateTimeOrNull, } from "./relations.js";
|
|
10
|
+
export { collectRefIds, makeRelatedMap, refId, type RefOrId, type RelatedMap, } from "./related.js";
|
|
11
|
+
export { make as makeTypedRecordSet, type HasId, type Many2OneRefField, type TypedRecordSet, } from "./typed.js";
|
|
12
|
+
export { defineRecord, Many2One, One2Many, type FieldInput, type FieldMeta, type Many2OneDecl, type One2ManyDecl, type RecordSpec, type RowEncoded, type RowType, } from "./recordModel.js";
|
|
13
|
+
export { compileSpecification, EmptyRecordSpecError, hasRelations, RecordSpecCycleError, } from "./spec.js";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/records/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,aAAa,EACb,cAAc,EACd,KAAK,EACL,KAAK,OAAO,EACZ,KAAK,UAAU,GAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,IAAI,IAAI,kBAAkB,EAC1B,KAAK,KAAK,EACV,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,OAAO,GACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed records — internal barrel for both per-protocol tiers of the records
|
|
3
|
+
* design: the CLASSIC tier (explicit traversal over the classic `[id, name]`
|
|
4
|
+
* pair protocol, 16+: relation/temporal decode schemas, pure join helpers,
|
|
5
|
+
* the `TypedRecordSet` snapshot) and the SPEC tier (declared prefetch over the
|
|
6
|
+
* 17+ `specification` protocol: `defineRecord` + the specification compiler).
|
|
7
|
+
* Re-exported to the public surface via `src/index.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export { Many2OneRef, Many2OneRefFromWire, Many2OneRefOrNull, Many2OneRefValue, OdooDate, OdooDateOrNull, OdooDateTime, OdooDateTimeOrNull, } from "./relations.js";
|
|
10
|
+
export { collectRefIds, makeRelatedMap, refId, } from "./related.js";
|
|
11
|
+
export { make as makeTypedRecordSet, } from "./typed.js";
|
|
12
|
+
export { defineRecord, Many2One, One2Many, } from "./recordModel.js";
|
|
13
|
+
export { compileSpecification, EmptyRecordSpecError, hasRelations, RecordSpecCycleError, } from "./spec.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/records/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,aAAa,EACb,cAAc,EACd,KAAK,GAGN,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,IAAI,IAAI,kBAAkB,GAI3B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,QAAQ,GAQT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,YAAY,EACZ,oBAAoB,GACrB,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared record models — the SPEC tier of the records design ("declared
|
|
3
|
+
* prefetch" over the 17+ `specification` protocol; the classic tier in
|
|
4
|
+
* `typed.ts`/`related.ts` speaks the 16+ `[id, name]`-pair protocol instead).
|
|
5
|
+
* A `RecordSpec`
|
|
6
|
+
* declares a model's field graph with `effect/Schema`: scalars decode directly,
|
|
7
|
+
* relations declared with {@link Many2One}/{@link One2Many} carry a nested child
|
|
8
|
+
* `RecordSpec`. From one declaration we derive BOTH:
|
|
9
|
+
*
|
|
10
|
+
* 1. a row {@link RecordSpec.schema} that strict-decodes the NESTED wire shape
|
|
11
|
+
* the 17+ `web_search_read`/`web_read` `specification` protocol returns
|
|
12
|
+
* (a many2one as a `{ id, ...declared }` dict or `false`; an x2many as a
|
|
13
|
+
* nested list), and
|
|
14
|
+
* 2. the field {@link RecordSpec.fields} metadata that {@link ./spec.ts} compiles
|
|
15
|
+
* into that `specification` payload.
|
|
16
|
+
*
|
|
17
|
+
* No I/O lives here — this is pure declaration. The single round trip that turns
|
|
18
|
+
* a spec into rows is in `OdooClient.searchTyped`/`readTyped`/`saveTyped`.
|
|
19
|
+
*
|
|
20
|
+
* MANY2ONE VALUE SHAPE (decision, records design 2026-07-10). A declared
|
|
21
|
+
* many2one decodes to a FLAT struct `{ id, ...declaredChildFields } | null`, not
|
|
22
|
+
* a `{ id, value }` wrapper. In TS, `| null` already carries "is it set?", and a
|
|
23
|
+
* flat struct exposes `.id` and every declared child field as directly-typed
|
|
24
|
+
* properties (`company.name`) with no `__getattr__`-style proxy and no extra
|
|
25
|
+
* `.value` hop — strictly better inference and ergonomics than a wrapper. This
|
|
26
|
+
* deliberately diverges from the Python design's `.value` recommendation, which
|
|
27
|
+
* existed to model presence separately from the record; TS does not need it.
|
|
28
|
+
*/
|
|
29
|
+
import { Schema } from "effect";
|
|
30
|
+
import type { HasId } from "./typed.js";
|
|
31
|
+
/**
|
|
32
|
+
* Per-field compilation metadata: how {@link ./spec.ts} renders each field. The
|
|
33
|
+
* relation `child` is typed loosely (`RecordSpec<any, any>`) because `Schema` is
|
|
34
|
+
* invariant in its type parameter — a precisely-typed child spec is not
|
|
35
|
+
* assignable to a widened one, so the metadata graph is deliberately erased. The
|
|
36
|
+
* precise types live on {@link RecordSpec.schema} for decode; this metadata is
|
|
37
|
+
* only ever walked structurally by the pure compiler.
|
|
38
|
+
*/
|
|
39
|
+
export type FieldMeta = {
|
|
40
|
+
readonly kind: "scalar";
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: "many2one";
|
|
43
|
+
readonly child: RecordSpec<any, any>;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: "one2many";
|
|
46
|
+
readonly child: RecordSpec<any, any>;
|
|
47
|
+
readonly limit: number | undefined;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* A declared record model: its Odoo model name, the row decode `schema` (the
|
|
51
|
+
* whole nested graph), the per-field compilation `fields`, and whether it
|
|
52
|
+
* declares any relation (the version gate keys on this). Build one with
|
|
53
|
+
* {@link defineRecord}.
|
|
54
|
+
*/
|
|
55
|
+
export interface RecordSpec<A extends HasId, I = A> {
|
|
56
|
+
readonly model: string;
|
|
57
|
+
readonly schema: Schema.Codec<A, I>;
|
|
58
|
+
readonly fields: Readonly<Record<string, FieldMeta>>;
|
|
59
|
+
/** `true` iff at least one declared field is a many2one/one2many relation. */
|
|
60
|
+
readonly hasRelations: boolean;
|
|
61
|
+
}
|
|
62
|
+
/** A declared many2one relation field — nests a child {@link RecordSpec}. */
|
|
63
|
+
export interface Many2OneDecl<A extends HasId, I> {
|
|
64
|
+
readonly _decl: "many2one";
|
|
65
|
+
readonly spec: RecordSpec<A, I>;
|
|
66
|
+
}
|
|
67
|
+
/** A declared one2many/many2many relation field — nests a child {@link RecordSpec}. */
|
|
68
|
+
export interface One2ManyDecl<A extends HasId, I> {
|
|
69
|
+
readonly _decl: "one2many";
|
|
70
|
+
readonly spec: RecordSpec<A, I>;
|
|
71
|
+
readonly limit: number | undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Declare a many2one field: `company_id: Many2One(Company)`. Decodes the nested
|
|
75
|
+
* `{ id, ...Company fields }` wire dict to `Company | null` (Odoo's `false`
|
|
76
|
+
* empty → `null`). Note it does NOT accept the classic `[id, name]` pair — under
|
|
77
|
+
* a `specification` the server returns a dict, so a leaked pair is drift, not a
|
|
78
|
+
* silent decode.
|
|
79
|
+
*/
|
|
80
|
+
export declare const Many2One: <A extends HasId, I>(spec: RecordSpec<A, I>) => Many2OneDecl<A, I>;
|
|
81
|
+
/**
|
|
82
|
+
* Declare an x2many field: `child_ids: One2Many(Contact)` or
|
|
83
|
+
* `One2Many(Contact, { limit: 20 })`. Decodes the nested list to
|
|
84
|
+
* `ReadonlyArray<Contact>`; `limit`, when given, rides the compiled
|
|
85
|
+
* `specification` so the server bounds the co-recordset.
|
|
86
|
+
*/
|
|
87
|
+
export declare const One2Many: <A extends HasId, I>(spec: RecordSpec<A, I>, options?: {
|
|
88
|
+
readonly limit?: number;
|
|
89
|
+
}) => One2ManyDecl<A, I>;
|
|
90
|
+
/**
|
|
91
|
+
* A field of a {@link defineRecord} declaration: a scalar schema or a relation.
|
|
92
|
+
* The relation members are `any`-parameterized because `Schema` (inside the
|
|
93
|
+
* nested child spec) is invariant — a precisely-typed `Many2One(Company)` is not
|
|
94
|
+
* assignable to a widened `Many2OneDecl<HasId, unknown>`. Field-level types are
|
|
95
|
+
* recovered structurally by {@link FieldType}/{@link FieldEncoded} via `infer`.
|
|
96
|
+
*/
|
|
97
|
+
export type FieldInput = Schema.Top | Many2OneDecl<any, any> | One2ManyDecl<any, any>;
|
|
98
|
+
/** The decoded (`Type`) value a declared field yields. */
|
|
99
|
+
type FieldType<T> = T extends Many2OneDecl<infer A, infer _I> ? A | null : T extends One2ManyDecl<infer A, infer _I> ? ReadonlyArray<A> : T extends Schema.Top ? T["Type"] : never;
|
|
100
|
+
/** The wire (`Encoded`) value a declared field maps from. */
|
|
101
|
+
type FieldEncoded<T> = T extends Many2OneDecl<infer _A, infer I> ? I | false : T extends One2ManyDecl<infer _A, infer I> ? ReadonlyArray<I> : T extends Schema.Top ? T["Encoded"] : never;
|
|
102
|
+
/** The decoded row type of a declaration `F` — always carries `id: number`. */
|
|
103
|
+
export type RowType<F extends Record<string, FieldInput>> = {
|
|
104
|
+
readonly id: number;
|
|
105
|
+
} & {
|
|
106
|
+
readonly [K in keyof F]: FieldType<F[K]>;
|
|
107
|
+
};
|
|
108
|
+
/** The wire row type of a declaration `F`. */
|
|
109
|
+
export type RowEncoded<F extends Record<string, FieldInput>> = {
|
|
110
|
+
readonly id: number;
|
|
111
|
+
} & {
|
|
112
|
+
readonly [K in keyof F]: FieldEncoded<F[K]>;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Build a {@link RecordSpec} from a model name and a field declaration. `id`
|
|
116
|
+
* (`Schema.Number`) is added automatically and always compiled into the
|
|
117
|
+
* `specification`, so every declared record and every relation subtree carries
|
|
118
|
+
* its id — the key downstream joins and re-reads rely on.
|
|
119
|
+
*
|
|
120
|
+
* ```ts
|
|
121
|
+
* const Company = defineRecord("res.company", { name: Schema.String });
|
|
122
|
+
* const Partner = defineRecord("res.partner", {
|
|
123
|
+
* name: Schema.String,
|
|
124
|
+
* company_id: Many2One(Company),
|
|
125
|
+
* child_ids: One2Many(Partner_contact, { limit: 20 }),
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare const defineRecord: <F extends Record<string, FieldInput>>(model: string, fields: F) => RecordSpec<RowType<F>, RowEncoded<F>>;
|
|
130
|
+
export {};
|
|
131
|
+
//# sourceMappingURL=recordModel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordModel.d.ts","sourceRoot":"","sources":["../../src/records/recordModel.ts"],"names":[],"mappings":"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"}
|