@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,101 @@
|
|
|
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, SchemaTransformation } from "effect";
|
|
30
|
+
/**
|
|
31
|
+
* Declare a many2one field: `company_id: Many2One(Company)`. Decodes the nested
|
|
32
|
+
* `{ id, ...Company fields }` wire dict to `Company | null` (Odoo's `false`
|
|
33
|
+
* empty → `null`). Note it does NOT accept the classic `[id, name]` pair — under
|
|
34
|
+
* a `specification` the server returns a dict, so a leaked pair is drift, not a
|
|
35
|
+
* silent decode.
|
|
36
|
+
*/
|
|
37
|
+
export const Many2One = (spec) => ({
|
|
38
|
+
_decl: "many2one",
|
|
39
|
+
spec,
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* Declare an x2many field: `child_ids: One2Many(Contact)` or
|
|
43
|
+
* `One2Many(Contact, { limit: 20 })`. Decodes the nested list to
|
|
44
|
+
* `ReadonlyArray<Contact>`; `limit`, when given, rides the compiled
|
|
45
|
+
* `specification` so the server bounds the co-recordset.
|
|
46
|
+
*/
|
|
47
|
+
export const One2Many = (spec, options) => ({ _decl: "one2many", spec, limit: options?.limit });
|
|
48
|
+
const isRelationDecl = (value) => typeof value === "object" && value !== null && "_decl" in value;
|
|
49
|
+
/**
|
|
50
|
+
* The nullable-many2one nested schema: the child dict, or Odoo's `false` empty
|
|
51
|
+
* → `null`. Strict: a classic `[id, name]` pair (or any non-dict) fails decode,
|
|
52
|
+
* surfacing as {@link SchemaDriftError} at the client boundary rather than being
|
|
53
|
+
* cast past.
|
|
54
|
+
*/
|
|
55
|
+
// oxlint-disable-next-line no-explicit-any -- Schema invariance; see FieldInput.
|
|
56
|
+
const many2oneFieldSchema = (child) => Schema.Union([child.schema, Schema.Literal(false)]).pipe(Schema.decodeTo(Schema.NullOr(Schema.toType(child.schema)), SchemaTransformation.transform({
|
|
57
|
+
decode: (wire) => (wire === false ? null : wire),
|
|
58
|
+
encode: (value) => (value === null ? false : value),
|
|
59
|
+
})));
|
|
60
|
+
/**
|
|
61
|
+
* Build a {@link RecordSpec} from a model name and a field declaration. `id`
|
|
62
|
+
* (`Schema.Number`) is added automatically and always compiled into the
|
|
63
|
+
* `specification`, so every declared record and every relation subtree carries
|
|
64
|
+
* its id — the key downstream joins and re-reads rely on.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* const Company = defineRecord("res.company", { name: Schema.String });
|
|
68
|
+
* const Partner = defineRecord("res.partner", {
|
|
69
|
+
* name: Schema.String,
|
|
70
|
+
* company_id: Many2One(Company),
|
|
71
|
+
* child_ids: One2Many(Partner_contact, { limit: 20 }),
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export const defineRecord = (model, fields) => {
|
|
76
|
+
const structFields = { id: Schema.Number };
|
|
77
|
+
const meta = { id: { kind: "scalar" } };
|
|
78
|
+
for (const [name, decl] of Object.entries(fields)) {
|
|
79
|
+
if (isRelationDecl(decl)) {
|
|
80
|
+
if (decl._decl === "many2one") {
|
|
81
|
+
structFields[name] = many2oneFieldSchema(decl.spec);
|
|
82
|
+
meta[name] = { kind: "many2one", child: decl.spec };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
structFields[name] = Schema.Array(decl.spec.schema);
|
|
86
|
+
meta[name] = { kind: "one2many", child: decl.spec, limit: decl.limit };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
structFields[name] = decl;
|
|
91
|
+
meta[name] = { kind: "scalar" };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// The dynamic Struct build cannot preserve the precise mapped type; we recover
|
|
95
|
+
// it via the RowType/RowEncoded projection, which is exactly what the loop
|
|
96
|
+
// constructs field-for-field.
|
|
97
|
+
const schema = Schema.Struct(structFields);
|
|
98
|
+
const hasRelations = Object.values(meta).some((m) => m.kind !== "scalar");
|
|
99
|
+
return { model, schema, fields: meta, hasRelations };
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=recordModel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordModel.js","sourceRoot":"","sources":["../../src/records/recordModel.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure join helpers for explicit relation traversal (the classic tier). No I/O: given
|
|
3
|
+
* already-decoded rows and a field accessor, collect the distinct related ids to
|
|
4
|
+
* fetch, and key decoded related rows by id for O(1) joins. The batched `read`
|
|
5
|
+
* itself lives in {@link ./typed.ts}; everything here is inert data shaping.
|
|
6
|
+
*/
|
|
7
|
+
import type { Many2OneRefValue } from "./relations.js";
|
|
8
|
+
/** A many2one field's decoded value: a ref, a bare id, or empty. */
|
|
9
|
+
export type RefOrId = Many2OneRefValue | number | null | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The related id a {@link RefOrId} points at, or `null` when empty. A ref value
|
|
12
|
+
* carries `.id`; a bare number is itself the id. Anything else is out of
|
|
13
|
+
* contract and yields `null` (the caller's runtime guard rejects true misuse
|
|
14
|
+
* before this is reached — see {@link ./typed.ts}).
|
|
15
|
+
*/
|
|
16
|
+
export declare const refId: (ref: RefOrId) => number | null;
|
|
17
|
+
/**
|
|
18
|
+
* The distinct, non-null related ids reachable through `accessor` across all
|
|
19
|
+
* `rows`, in first-seen order. This is the exact id set a single batched `read`
|
|
20
|
+
* must cover — an empty result means ZERO round trips are needed.
|
|
21
|
+
*/
|
|
22
|
+
export declare const collectRefIds: <Row>(rows: ReadonlyArray<Row>, accessor: (row: Row) => RefOrId) => ReadonlyArray<number>;
|
|
23
|
+
/**
|
|
24
|
+
* A snapshot of decoded related rows keyed by id. Inert: it holds data and never
|
|
25
|
+
* fetches. `get` accepts a many2one ref, a bare id, or `null`/`undefined` and
|
|
26
|
+
* returns the joined row or `null` — a pure lookup, never a round trip.
|
|
27
|
+
*/
|
|
28
|
+
export interface RelatedMap<A> {
|
|
29
|
+
/** Join a many2one value (ref/id/empty) to its decoded related row, or `null`. */
|
|
30
|
+
readonly get: (ref: RefOrId) => A | null;
|
|
31
|
+
/** The number of distinct related rows held. */
|
|
32
|
+
readonly size: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build a {@link RelatedMap} from decoded related rows, keyed by each row's
|
|
36
|
+
* `id`. The rows come straight from a single batched `read`, so a duplicate id
|
|
37
|
+
* cannot occur; last-write-wins is a harmless invariant, not a policy.
|
|
38
|
+
*/
|
|
39
|
+
export declare const makeRelatedMap: <A extends {
|
|
40
|
+
readonly id: number;
|
|
41
|
+
}>(rows: ReadonlyArray<A>) => RelatedMap<A>;
|
|
42
|
+
//# sourceMappingURL=related.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"related.d.ts","sourceRoot":"","sources":["../../src/records/related.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,oEAAoE;AACpE,MAAM,MAAM,OAAO,GAAG,gBAAgB,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,KAAK,QAAS,OAAO,KAAG,MAAM,GAAG,IAQ7C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,GAAG,QACzB,aAAa,CAAC,GAAG,CAAC,YACd,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,KAC9B,aAAa,CAAC,MAAM,CAWtB,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,CAAC,GAAG,IAAI,CAAC;IACzC,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,QACxD,aAAa,CAAC,CAAC,CAAC,KACrB,UAAU,CAAC,CAAC,CAYd,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The related id a {@link RefOrId} points at, or `null` when empty. A ref value
|
|
3
|
+
* carries `.id`; a bare number is itself the id. Anything else is out of
|
|
4
|
+
* contract and yields `null` (the caller's runtime guard rejects true misuse
|
|
5
|
+
* before this is reached — see {@link ./typed.ts}).
|
|
6
|
+
*/
|
|
7
|
+
export const refId = (ref) => {
|
|
8
|
+
if (ref === null || ref === undefined) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (typeof ref === "number") {
|
|
12
|
+
return ref;
|
|
13
|
+
}
|
|
14
|
+
return typeof ref === "object" && typeof ref.id === "number" ? ref.id : null;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The distinct, non-null related ids reachable through `accessor` across all
|
|
18
|
+
* `rows`, in first-seen order. This is the exact id set a single batched `read`
|
|
19
|
+
* must cover — an empty result means ZERO round trips are needed.
|
|
20
|
+
*/
|
|
21
|
+
export const collectRefIds = (rows, accessor) => {
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const id = refId(accessor(row));
|
|
26
|
+
if (id !== null && !seen.has(id)) {
|
|
27
|
+
seen.add(id);
|
|
28
|
+
out.push(id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Build a {@link RelatedMap} from decoded related rows, keyed by each row's
|
|
35
|
+
* `id`. The rows come straight from a single batched `read`, so a duplicate id
|
|
36
|
+
* cannot occur; last-write-wins is a harmless invariant, not a policy.
|
|
37
|
+
*/
|
|
38
|
+
export const makeRelatedMap = (rows) => {
|
|
39
|
+
const byId = new Map();
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
byId.set(row.id, row);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
get: (ref) => {
|
|
45
|
+
const id = refId(ref);
|
|
46
|
+
return id === null ? null : (byId.get(id) ?? null);
|
|
47
|
+
},
|
|
48
|
+
size: byId.size,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=related.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"related.js","sourceRoot":"","sources":["../../src/records/related.ts"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,GAAY,EAAiB,EAAE;IACnD,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/E,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,IAAwB,EACxB,QAA+B,EACR,EAAE;IACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;QAChC,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAcF;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,IAAsB,EACP,EAAE;IACjB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAa,CAAC;IAClC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IACxB,CAAC;IACD,OAAO;QACL,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE;YACX,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;YACtB,OAAO,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,EAAE,IAAI,CAAC,IAAI;KAChB,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure relation & temporal value schemas — the decode vocabulary for typed
|
|
3
|
+
* records. No I/O: these are `effect/Schema` definitions a caller composes into
|
|
4
|
+
* a row schema. They transform Odoo's on-the-wire shapes (classic `[id, name]`
|
|
5
|
+
* many2one pairs, `false` empties, naive datetime strings) into ergonomic,
|
|
6
|
+
* fully-typed domain values, failing loudly as schema drift on anything else.
|
|
7
|
+
*/
|
|
8
|
+
import { Schema } from "effect";
|
|
9
|
+
/**
|
|
10
|
+
* A decoded many2one reference: the related record's id and its display label,
|
|
11
|
+
* as Odoo carries them together in a classic `search_read`/`read` payload. The
|
|
12
|
+
* label is already present, so the common "show the name" case needs ZERO extra
|
|
13
|
+
* round trips — see {@link ../typed.ts | TypedRecordSet.fetchRelated} for when
|
|
14
|
+
* you need more than the label.
|
|
15
|
+
*/
|
|
16
|
+
export interface Many2OneRefValue {
|
|
17
|
+
readonly id: number;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
}
|
|
20
|
+
/** The decoded-value schema for a {@link Many2OneRefValue}. */
|
|
21
|
+
export declare const Many2OneRefValue: Schema.Codec<Many2OneRefValue>;
|
|
22
|
+
/**
|
|
23
|
+
* Decode a present many2one from its wire `[id, name]` pair to a
|
|
24
|
+
* {@link Many2OneRefValue}. Use {@link Many2OneRefOrNull} for a field that can
|
|
25
|
+
* be empty (Odoo sends `false`, not `null`, for an unset many2one).
|
|
26
|
+
*
|
|
27
|
+
* WIRE FACT: the `[id, name]` pair requires the classic `_classic_read` load
|
|
28
|
+
* (the `search_read`/`read` default). A context that changes the load (e.g. a
|
|
29
|
+
* future `load=None`) shifts the shape; strict decode then surfaces it as
|
|
30
|
+
* {@link SchemaDriftError} rather than casting past it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const Many2OneRefFromWire: Schema.Codec<Many2OneRefValue, readonly [number, string]>;
|
|
33
|
+
/** Ergonomic alias for {@link Many2OneRefFromWire} (the present-ref schema). */
|
|
34
|
+
export declare const Many2OneRef: Schema.Codec<Many2OneRefValue, readonly [number, string], never, never>;
|
|
35
|
+
/**
|
|
36
|
+
* Decode a nullable many2one: the wire `[id, name]` pair → {@link Many2OneRefValue},
|
|
37
|
+
* or Odoo's `false` empty → `null`. This is the schema a row declares for a
|
|
38
|
+
* many2one that can be unset, e.g. `company_id: Many2OneRefOrNull`.
|
|
39
|
+
*/
|
|
40
|
+
export declare const Many2OneRefOrNull: Schema.Codec<Many2OneRefValue | null, readonly [number, string] | false>;
|
|
41
|
+
/**
|
|
42
|
+
* Odoo `Date` field: `"YYYY-MM-DD"` → a `Date` at UTC midnight of that day.
|
|
43
|
+
*
|
|
44
|
+
* A `Date` is an instant, and Odoo's `Date` carries no time or zone. We anchor
|
|
45
|
+
* it at UTC midnight and NEVER localize — reading back `getUTCFullYear()`/
|
|
46
|
+
* `getUTCMonth()`/`getUTCDate()` reproduces the exact wire day on any host,
|
|
47
|
+
* whereas the local accessors could roll a day depending on the runner's tz.
|
|
48
|
+
* Decision (records design, 2026-07-10): decode as UTC, documented, never
|
|
49
|
+
* silently localized.
|
|
50
|
+
*/
|
|
51
|
+
export declare const OdooDate: Schema.Codec<Date, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Odoo `Datetime` field: `"YYYY-MM-DD HH:MM:SS"` → a `Date` parsed as UTC.
|
|
54
|
+
*
|
|
55
|
+
* Odoo stores and transmits datetimes as naive strings that are UTC by
|
|
56
|
+
* convention. A JS `Date` is a single instant, so parsing the wire string AS
|
|
57
|
+
* UTC is the faithful, non-localizing choice: it names the same instant Odoo
|
|
58
|
+
* meant, with no host-tz shift. We never apply the session tz here — that is a
|
|
59
|
+
* presentation concern the caller owns. Decision (records design, 2026-07-10).
|
|
60
|
+
*/
|
|
61
|
+
export declare const OdooDateTime: Schema.Codec<Date, string>;
|
|
62
|
+
/** {@link OdooDate} with Odoo's `false` empty decoding to `null`. */
|
|
63
|
+
export declare const OdooDateOrNull: Schema.Codec<Date | null, string | false>;
|
|
64
|
+
/** {@link OdooDateTime} with Odoo's `false` empty decoding to `null`. */
|
|
65
|
+
export declare const OdooDateTimeOrNull: Schema.Codec<Date | null, string | false>;
|
|
66
|
+
//# sourceMappingURL=relations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relations.d.ts","sourceRoot":"","sources":["../../src/records/relations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAkB,MAAM,EAAmD,MAAM,QAAQ,CAAC;AAIjG;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAG1D,CAAC;AAKH;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CASvF,CAAC;AAEJ,gFAAgF;AAChF,eAAO,MAAM,WAAW,yEAAsB,CAAC;AAK/C;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,KAAK,CAC1C,gBAAgB,GAAG,IAAI,EACvB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,CASlC,CAAC;AAuBF;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAmB/C,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAmBnD,CAAC;AA6BF,qEAAqE;AACrE,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,GAAG,KAAK,CAAyB,CAAC;AAE/F,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,GAAG,KAAK,CAC9C,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure relation & temporal value schemas — the decode vocabulary for typed
|
|
3
|
+
* records. No I/O: these are `effect/Schema` definitions a caller composes into
|
|
4
|
+
* a row schema. They transform Odoo's on-the-wire shapes (classic `[id, name]`
|
|
5
|
+
* many2one pairs, `false` empties, naive datetime strings) into ergonomic,
|
|
6
|
+
* fully-typed domain values, failing loudly as schema drift on anything else.
|
|
7
|
+
*/
|
|
8
|
+
import { Effect, Option, Schema, SchemaIssue, SchemaParser, SchemaTransformation } from "effect";
|
|
9
|
+
/** The decoded-value schema for a {@link Many2OneRefValue}. */
|
|
10
|
+
export const Many2OneRefValue = Schema.Struct({
|
|
11
|
+
id: Schema.Number,
|
|
12
|
+
name: Schema.String,
|
|
13
|
+
});
|
|
14
|
+
/** Odoo's on-the-wire many2one shape: the classic `[id, name]` pair. */
|
|
15
|
+
const Many2OneWirePair = Schema.Tuple([Schema.Number, Schema.String]);
|
|
16
|
+
/**
|
|
17
|
+
* Decode a present many2one from its wire `[id, name]` pair to a
|
|
18
|
+
* {@link Many2OneRefValue}. Use {@link Many2OneRefOrNull} for a field that can
|
|
19
|
+
* be empty (Odoo sends `false`, not `null`, for an unset many2one).
|
|
20
|
+
*
|
|
21
|
+
* WIRE FACT: the `[id, name]` pair requires the classic `_classic_read` load
|
|
22
|
+
* (the `search_read`/`read` default). A context that changes the load (e.g. a
|
|
23
|
+
* future `load=None`) shifts the shape; strict decode then surfaces it as
|
|
24
|
+
* {@link SchemaDriftError} rather than casting past it.
|
|
25
|
+
*/
|
|
26
|
+
export const Many2OneRefFromWire = Many2OneWirePair.pipe(Schema.decodeTo(Many2OneRefValue, SchemaTransformation.transform({
|
|
27
|
+
decode: ([id, name]) => ({ id, name }),
|
|
28
|
+
encode: ({ id, name }) => [id, name],
|
|
29
|
+
})));
|
|
30
|
+
/** Ergonomic alias for {@link Many2OneRefFromWire} (the present-ref schema). */
|
|
31
|
+
export const Many2OneRef = Many2OneRefFromWire;
|
|
32
|
+
/** The wire shape of a nullable many2one: the `[id, name]` pair or `false`. */
|
|
33
|
+
const Many2OneWireOrFalse = Schema.Union([Many2OneWirePair, Schema.Literal(false)]);
|
|
34
|
+
/**
|
|
35
|
+
* Decode a nullable many2one: the wire `[id, name]` pair → {@link Many2OneRefValue},
|
|
36
|
+
* or Odoo's `false` empty → `null`. This is the schema a row declares for a
|
|
37
|
+
* many2one that can be unset, e.g. `company_id: Many2OneRefOrNull`.
|
|
38
|
+
*/
|
|
39
|
+
export const Many2OneRefOrNull = Many2OneWireOrFalse.pipe(Schema.decodeTo(Schema.NullOr(Many2OneRefValue), SchemaTransformation.transform({
|
|
40
|
+
decode: (wire) => (wire === false ? null : { id: wire[0], name: wire[1] }),
|
|
41
|
+
encode: (value) => (value === null ? false : [value.id, value.name]),
|
|
42
|
+
})));
|
|
43
|
+
// --- dates & datetimes ------------------------------------------------------
|
|
44
|
+
// Odoo Date is `"YYYY-MM-DD"`; Datetime is `"YYYY-MM-DD HH:MM:SS"`, both naive.
|
|
45
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
46
|
+
const DATETIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
47
|
+
const pad = (n, width) => n.toString().padStart(width, "0");
|
|
48
|
+
const formatUtcDate = (date) => `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1, 2)}-${pad(date.getUTCDate(), 2)}`;
|
|
49
|
+
const formatUtcDateTime = (date) => `${formatUtcDate(date)} ${pad(date.getUTCHours(), 2)}:${pad(date.getUTCMinutes(), 2)}:${pad(date.getUTCSeconds(), 2)}`;
|
|
50
|
+
/** A decode/encode failure as a v4 schema issue on the offending value. */
|
|
51
|
+
const invalid = (value, message) => new SchemaIssue.InvalidValue(Option.some(value), { message });
|
|
52
|
+
/**
|
|
53
|
+
* Odoo `Date` field: `"YYYY-MM-DD"` → a `Date` at UTC midnight of that day.
|
|
54
|
+
*
|
|
55
|
+
* A `Date` is an instant, and Odoo's `Date` carries no time or zone. We anchor
|
|
56
|
+
* it at UTC midnight and NEVER localize — reading back `getUTCFullYear()`/
|
|
57
|
+
* `getUTCMonth()`/`getUTCDate()` reproduces the exact wire day on any host,
|
|
58
|
+
* whereas the local accessors could roll a day depending on the runner's tz.
|
|
59
|
+
* Decision (records design, 2026-07-10): decode as UTC, documented, never
|
|
60
|
+
* silently localized.
|
|
61
|
+
*/
|
|
62
|
+
export const OdooDate = Schema.String.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transformOrFail({
|
|
63
|
+
decode: (input) => {
|
|
64
|
+
if (!DATE_RE.test(input)) {
|
|
65
|
+
return Effect.fail(invalid(input, "expected YYYY-MM-DD"));
|
|
66
|
+
}
|
|
67
|
+
const ms = Date.parse(`${input}T00:00:00Z`);
|
|
68
|
+
return Number.isNaN(ms)
|
|
69
|
+
? Effect.fail(invalid(input, "not a valid calendar date"))
|
|
70
|
+
: Effect.succeed(new Date(ms));
|
|
71
|
+
},
|
|
72
|
+
encode: (date) => Number.isNaN(date.getTime())
|
|
73
|
+
? Effect.fail(invalid(date, "invalid Date"))
|
|
74
|
+
: Effect.succeed(formatUtcDate(date)),
|
|
75
|
+
})));
|
|
76
|
+
/**
|
|
77
|
+
* Odoo `Datetime` field: `"YYYY-MM-DD HH:MM:SS"` → a `Date` parsed as UTC.
|
|
78
|
+
*
|
|
79
|
+
* Odoo stores and transmits datetimes as naive strings that are UTC by
|
|
80
|
+
* convention. A JS `Date` is a single instant, so parsing the wire string AS
|
|
81
|
+
* UTC is the faithful, non-localizing choice: it names the same instant Odoo
|
|
82
|
+
* meant, with no host-tz shift. We never apply the session tz here — that is a
|
|
83
|
+
* presentation concern the caller owns. Decision (records design, 2026-07-10).
|
|
84
|
+
*/
|
|
85
|
+
export const OdooDateTime = Schema.String.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.transformOrFail({
|
|
86
|
+
decode: (input) => {
|
|
87
|
+
if (!DATETIME_RE.test(input)) {
|
|
88
|
+
return Effect.fail(invalid(input, "expected YYYY-MM-DD HH:MM:SS"));
|
|
89
|
+
}
|
|
90
|
+
const ms = Date.parse(`${input.replace(" ", "T")}Z`);
|
|
91
|
+
return Number.isNaN(ms)
|
|
92
|
+
? Effect.fail(invalid(input, "not a valid calendar datetime"))
|
|
93
|
+
: Effect.succeed(new Date(ms));
|
|
94
|
+
},
|
|
95
|
+
encode: (date) => Number.isNaN(date.getTime())
|
|
96
|
+
? Effect.fail(invalid(date, "invalid Date"))
|
|
97
|
+
: Effect.succeed(formatUtcDateTime(date)),
|
|
98
|
+
})));
|
|
99
|
+
/**
|
|
100
|
+
* Lift a `Codec<Date, string>` into one that also accepts Odoo's `false`
|
|
101
|
+
* empty, decoding it to `null`. Shared by the date and datetime nullable
|
|
102
|
+
* variants so the false↔null seam lives in exactly one place. Delegation to
|
|
103
|
+
* the base codec runs through `SchemaParser.*Effect`, which fails with a
|
|
104
|
+
* `SchemaIssue.Issue` directly — the v4 analogue of v3's
|
|
105
|
+
* `ParseResult.decodeUnknown` delegation.
|
|
106
|
+
*/
|
|
107
|
+
const orFalseNull = (base) => Schema.Union([Schema.String, Schema.Literal(false)]).pipe(Schema.decodeTo(Schema.NullOr(Schema.Date), SchemaTransformation.transformOrFail({
|
|
108
|
+
decode: (wire, options) => wire === false
|
|
109
|
+
? Effect.succeed(null)
|
|
110
|
+
: SchemaParser.decodeUnknownEffect(base)(wire, options),
|
|
111
|
+
encode: (value, options) => value === null
|
|
112
|
+
? Effect.succeed(false)
|
|
113
|
+
: SchemaParser.encodeUnknownEffect(base)(value, options),
|
|
114
|
+
})));
|
|
115
|
+
/** {@link OdooDate} with Odoo's `false` empty decoding to `null`. */
|
|
116
|
+
export const OdooDateOrNull = orFalseNull(OdooDate);
|
|
117
|
+
/** {@link OdooDateTime} with Odoo's `false` empty decoding to `null`. */
|
|
118
|
+
export const OdooDateTimeOrNull = orFalseNull(OdooDateTime);
|
|
119
|
+
//# sourceMappingURL=relations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relations.js","sourceRoot":"","sources":["../../src/records/relations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAgBjG,+DAA+D;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAmC,MAAM,CAAC,MAAM,CAAC;IAC5E,EAAE,EAAE,MAAM,CAAC,MAAM;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM;CACpB,CAAC,CAAC;AAEH,wEAAwE;AACxE,MAAM,gBAAgB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAEtE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,gBAAgB,CAAC,IAAI,CACnB,MAAM,CAAC,QAAQ,CACb,gBAAgB,EAChB,oBAAoB,CAAC,SAAS,CAAC;IAC7B,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,CAAU;CAC9C,CAAC,CACH,CACF,CAAC;AAEJ,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAE/C,+EAA+E;AAC/E,MAAM,mBAAmB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,gBAAgB,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEpF;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAG1B,mBAAmB,CAAC,IAAI,CAC1B,MAAM,CAAC,QAAQ,CACb,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAC/B,oBAAoB,CAAC,SAAS,CAAC;IAC7B,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAE,KAAe,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,CAAW,CAAC;CAC3F,CAAC,CACH,CACF,CAAC;AAEF,+EAA+E;AAE/E,gFAAgF;AAChF,MAAM,OAAO,GAAG,qBAAqB,CAAC;AACtC,MAAM,WAAW,GAAG,uCAAuC,CAAC;AAE5D,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,KAAa,EAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAEpF,MAAM,aAAa,GAAG,CAAC,IAAU,EAAU,EAAE,CAC3C,GAAG,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;AAEpG,MAAM,iBAAiB,GAAG,CAAC,IAAU,EAAU,EAAE,CAC/C,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,CACzF,IAAI,CAAC,aAAa,EAAE,EACpB,CAAC,CACF,EAAE,CAAC;AAEN,2EAA2E;AAC3E,MAAM,OAAO,GAAG,CAAC,KAAc,EAAE,OAAe,EAA4B,EAAE,CAC5E,IAAI,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAA+B,MAAM,CAAC,MAAM,CAAC,IAAI,CACpE,MAAM,CAAC,QAAQ,CACb,MAAM,CAAC,IAAI,EACX,oBAAoB,CAAC,eAAe,CAAC;IACnC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAChB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC;QAC5D,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,YAAY,CAAC,CAAC;QAC5C,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACrB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,2BAA2B,CAAC,CAAC;YAC1D,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACnC,CAAC;IACD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACf,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;QAC5C,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;CAC1C,CAAC,CACH,CACF,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAA+B,MAAM,CAAC,MAAM,CAAC,IAAI,CACxE,MAAM,CAAC,QAAQ,CACb,MAAM,CAAC,IAAI,EACX,oBAAoB,CAAC,eAAe,CAAC;IACnC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;QAChB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,8BAA8B,CAAC,CAAC,CAAC;QACrE,CAAC;QACD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACrB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,+BAA+B,CAAC,CAAC;YAC9D,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACnC,CAAC;IACD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACf,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;QAC5C,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;CAC9C,CAAC,CACH,CACF,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,GAAG,CAClB,IAAgC,EACW,EAAE,CAC7C,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CACvD,MAAM,CAAC,QAAQ,CACb,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAC1B,oBAAoB,CAAC,eAAe,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CACxB,IAAI,KAAK,KAAK;QACZ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;QACtB,CAAC,CAAC,YAAY,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC;IAC3D,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CACzB,KAAK,KAAK,IAAI;QACZ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAc,CAAC;QAChC,CAAC,CAAC,YAAY,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC;CAC7D,CAAC,CACH,CACF,CAAC;AAEJ,qEAAqE;AACrE,MAAM,CAAC,MAAM,cAAc,GAA8C,WAAW,CAAC,QAAQ,CAAC,CAAC;AAE/F,yEAAyE;AACzE,MAAM,CAAC,MAAM,kBAAkB,GAC7B,WAAW,CAAC,YAAY,CAAC,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure compilation of a declared {@link RecordSpec} into the Odoo `web_read`/
|
|
3
|
+
* `web_search_read` `specification` payload (17+). No I/O: given the field graph,
|
|
4
|
+
* emit `{ field: field_spec }` where a scalar is `{}`, a many2one is
|
|
5
|
+
* `{ fields: <nested> }`, and an x2many is `{ fields: <nested>, limit? }`. The
|
|
6
|
+
* server resolves each relation once over the whole co-recordset, so a compiled
|
|
7
|
+
* spec is exactly ONE batched round trip — no N+1.
|
|
8
|
+
*
|
|
9
|
+
* CYCLES. Eager child specs cannot form an object cycle in a strict language
|
|
10
|
+
* (you cannot reference a `RecordSpec` inside its own `defineRecord` call), so a
|
|
11
|
+
* cyclic declaration is inexpressible by construction and the normal path is
|
|
12
|
+
* total. We still guard defensively by RecordSpec IDENTITY along the ancestor
|
|
13
|
+
* chain — a spec forced into a cycle (via mutation / `any`) would otherwise
|
|
14
|
+
* recurse forever; instead it throws {@link RecordSpecCycleError}, which the
|
|
15
|
+
* client surfaces as a defect (a declaration bug, never a runtime condition). A
|
|
16
|
+
* finite DAG that references the same MODEL through two distinct specs is fine —
|
|
17
|
+
* only a repeated spec object is a cycle.
|
|
18
|
+
*/
|
|
19
|
+
import type { RecordSpec } from "./recordModel.js";
|
|
20
|
+
import type { HasId } from "./typed.js";
|
|
21
|
+
/** A declared record graph that references itself — a declaration bug, not a
|
|
22
|
+
* runtime fault. Thrown by {@link compileSpecification}; the client turns it into
|
|
23
|
+
* an `Effect` defect. */
|
|
24
|
+
export declare class RecordSpecCycleError extends Error {
|
|
25
|
+
readonly path: ReadonlyArray<string>;
|
|
26
|
+
constructor(path: ReadonlyArray<string>);
|
|
27
|
+
}
|
|
28
|
+
/** A declared record with no compilable fields — an empty `specification` would
|
|
29
|
+
* ask the server for nothing. Thrown by {@link compileSpecification}. */
|
|
30
|
+
export declare class EmptyRecordSpecError extends Error {
|
|
31
|
+
readonly model: string;
|
|
32
|
+
constructor(model: string);
|
|
33
|
+
}
|
|
34
|
+
/** `true` iff the spec declares at least one relation field (gate key). */
|
|
35
|
+
export declare const hasRelations: <A extends HasId, I>(spec: RecordSpec<A, I>) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Compile a declared {@link RecordSpec} into its nested `specification` payload.
|
|
38
|
+
* Pure and total for the by-construction-acyclic normal case; throws
|
|
39
|
+
* {@link RecordSpecCycleError} on a forced cycle and {@link EmptyRecordSpecError}
|
|
40
|
+
* on an empty declaration. The client runs it under `Effect.sync`, so either
|
|
41
|
+
* throw becomes a precise defect.
|
|
42
|
+
*/
|
|
43
|
+
export declare const compileSpecification: <A extends HasId, I>(spec: RecordSpec<A, I>) => Record<string, unknown>;
|
|
44
|
+
//# sourceMappingURL=spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../src/records/spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;yBAEyB;AACzB,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACrC,YAAY,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAQtC;CACF;AAED;yEACyE;AACzE,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,YAAY,KAAK,EAAE,MAAM,EAOxB;CACF;AAED,2EAA2E;AAC3E,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,KAAK,EAAE,CAAC,QAAQ,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,OACb,CAAC;AAiC9D;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,KAAK,EAAE,CAAC,QAC/C,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAsB,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** A declared record graph that references itself — a declaration bug, not a
|
|
2
|
+
* runtime fault. Thrown by {@link compileSpecification}; the client turns it into
|
|
3
|
+
* an `Effect` defect. */
|
|
4
|
+
export class RecordSpecCycleError extends Error {
|
|
5
|
+
path;
|
|
6
|
+
constructor(path) {
|
|
7
|
+
super(`RecordSpec declaration cycle: ${path.join(" -> ")} -> ${path[path.length - 1]}. ` +
|
|
8
|
+
`A relation subtree references a record spec already on its ancestor chain; ` +
|
|
9
|
+
`the specification would be infinite. Declared record graphs must be finite.`);
|
|
10
|
+
this.name = "RecordSpecCycleError";
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** A declared record with no compilable fields — an empty `specification` would
|
|
15
|
+
* ask the server for nothing. Thrown by {@link compileSpecification}. */
|
|
16
|
+
export class EmptyRecordSpecError extends Error {
|
|
17
|
+
model;
|
|
18
|
+
constructor(model) {
|
|
19
|
+
super(`RecordSpec for "${model}" compiles to an empty specification (no fields). ` +
|
|
20
|
+
`A declared record must request at least its id.`);
|
|
21
|
+
this.name = "EmptyRecordSpecError";
|
|
22
|
+
this.model = model;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** `true` iff the spec declares at least one relation field (gate key). */
|
|
26
|
+
export const hasRelations = (spec) => Object.values(spec.fields).some((m) => m.kind !== "scalar");
|
|
27
|
+
const compile = (
|
|
28
|
+
// oxlint-disable-next-line no-explicit-any -- Schema is invariant; the compiler
|
|
29
|
+
// walks the erased metadata graph structurally (see FieldMeta).
|
|
30
|
+
spec, ancestors) => {
|
|
31
|
+
if (ancestors.includes(spec)) {
|
|
32
|
+
throw new RecordSpecCycleError([...ancestors.map((a) => a.model), spec.model]);
|
|
33
|
+
}
|
|
34
|
+
const nextAncestors = [...ancestors, spec];
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [name, meta] of Object.entries(spec.fields)) {
|
|
37
|
+
if (meta.kind === "scalar") {
|
|
38
|
+
out[name] = {};
|
|
39
|
+
}
|
|
40
|
+
else if (meta.kind === "many2one") {
|
|
41
|
+
out[name] = { fields: compile(meta.child, nextAncestors) };
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
out[name] = {
|
|
45
|
+
fields: compile(meta.child, nextAncestors),
|
|
46
|
+
...(meta.limit !== undefined ? { limit: meta.limit } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(out).length === 0) {
|
|
51
|
+
throw new EmptyRecordSpecError(spec.model);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Compile a declared {@link RecordSpec} into its nested `specification` payload.
|
|
57
|
+
* Pure and total for the by-construction-acyclic normal case; throws
|
|
58
|
+
* {@link RecordSpecCycleError} on a forced cycle and {@link EmptyRecordSpecError}
|
|
59
|
+
* on an empty declaration. The client runs it under `Effect.sync`, so either
|
|
60
|
+
* throw becomes a precise defect.
|
|
61
|
+
*/
|
|
62
|
+
export const compileSpecification = (spec) => compile(spec, []);
|
|
63
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../../src/records/spec.ts"],"names":[],"mappings":"AAqBA;;yBAEyB;AACzB,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IACpC,IAAI,CAAwB;IACrC,YAAY,IAA2B;QACrC,KAAK,CACH,iCAAiC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI;YAChF,6EAA6E;YAC7E,6EAA6E,CAChF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED;yEACyE;AACzE,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IACpC,KAAK,CAAS;IACvB,YAAY,KAAa;QACvB,KAAK,CACH,mBAAmB,KAAK,oDAAoD;YAC1E,iDAAiD,CACpD,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAED,2EAA2E;AAC3E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAqB,IAAsB,EAAW,EAAE,CAClF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;AAE9D,MAAM,OAAO,GAAG;AACd,gFAAgF;AAChF,gEAAgE;AAChE,IAA0B,EAC1B,SAA8C,EACrB,EAAE;IAC3B,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,oBAAoB,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACjF,CAAC;IACD,MAAM,aAAa,GAAG,CAAC,GAAG,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3C,MAAM,GAAG,GAA4B,EAAE,CAAC;IAExC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACvD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QACjB,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACpC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC,EAAE,CAAC;QAC7D,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,GAAG;gBACV,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,aAAa,CAAC;gBAC1C,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3D,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,IAAsB,EACG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
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 type { Rpc } from "../rpc.js";
|
|
12
|
+
import type { TransportCallError } from "../transport.js";
|
|
13
|
+
import { type RelatedMap } from "./related.js";
|
|
14
|
+
import type { Many2OneRefValue } from "./relations.js";
|
|
15
|
+
/** A decoded row must carry its `id` — every traversal and join keys on it. */
|
|
16
|
+
export type HasId = {
|
|
17
|
+
readonly id: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The field names of `Row` whose decoded value is a many2one reference
|
|
21
|
+
* (`Many2OneRefValue | null`) — the only fields a relation can be traversed
|
|
22
|
+
* through. A compile-time constraint: passing any other key to
|
|
23
|
+
* {@link TypedRecordSet.fetchRelated}/`joinRelated` is a type error, not a
|
|
24
|
+
* runtime surprise.
|
|
25
|
+
*/
|
|
26
|
+
export type Many2OneRefField<Row> = {
|
|
27
|
+
readonly [K in keyof Row]: Row[K] extends Many2OneRefValue | null ? K : never;
|
|
28
|
+
}[keyof Row];
|
|
29
|
+
/**
|
|
30
|
+
* An immutable snapshot of typed rows with explicit relation traversal. Obtain
|
|
31
|
+
* one from `OdooClient.searchRecordsTyped`; it is bound to the `Rpc` seam and
|
|
32
|
+
* model it was read from so `fetchRelated` can batch a co-model `read`.
|
|
33
|
+
*/
|
|
34
|
+
export interface TypedRecordSet<Row extends HasId> extends Iterable<Row> {
|
|
35
|
+
/** The model these rows were read from. */
|
|
36
|
+
readonly model: string;
|
|
37
|
+
/** The decoded rows, in server order. Immutable. */
|
|
38
|
+
readonly rows: ReadonlyArray<Row>;
|
|
39
|
+
/** Each row's `id`, in row order. */
|
|
40
|
+
readonly ids: ReadonlyArray<number>;
|
|
41
|
+
/** The number of rows. */
|
|
42
|
+
readonly length: number;
|
|
43
|
+
/**
|
|
44
|
+
* Fetch the related records behind a many2one `field` in ONE batched `read`
|
|
45
|
+
* over the distinct, non-null ids across the whole snapshot, decoded through
|
|
46
|
+
* `schema`. Returns a {@link RelatedMap} keyed by related id — a fresh
|
|
47
|
+
* snapshot; the recordset is never back-filled.
|
|
48
|
+
*
|
|
49
|
+
* ZERO round trips when no row references a related record (all empty). The
|
|
50
|
+
* batched-single-read guarantee is the entire point of this API and is
|
|
51
|
+
* asserted in the tests, not merely documented.
|
|
52
|
+
*/
|
|
53
|
+
readonly fetchRelated: <K extends Many2OneRefField<Row>, A extends HasId, I = A>(field: K, relatedModel: string, schema: Schema.Codec<A, I>, fields?: ReadonlyArray<string>) => Effect.Effect<RelatedMap<A>, TransportCallError>;
|
|
54
|
+
/**
|
|
55
|
+
* Like {@link fetchRelated}, but returns each row paired with its joined
|
|
56
|
+
* related record (`null` when the many2one is empty or unresolved). One
|
|
57
|
+
* explicit, batched call — named to keep "this is a fetch" legible at the call
|
|
58
|
+
* site.
|
|
59
|
+
*/
|
|
60
|
+
readonly joinRelated: <K extends Many2OneRefField<Row>, A extends HasId, I = A>(field: K, relatedModel: string, schema: Schema.Codec<A, I>, fields?: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<readonly [Row, A | null]>, TransportCallError>;
|
|
61
|
+
}
|
|
62
|
+
/** The subset of the {@link Rpc} service {@link TypedRecordSet} depends on. */
|
|
63
|
+
type RpcSeam = Pick<typeof Rpc.Service, "callKw">;
|
|
64
|
+
/**
|
|
65
|
+
* Construct a {@link TypedRecordSet} over already-decoded rows. Internal: the
|
|
66
|
+
* public entry point is `OdooClient.searchRecordsTyped`, which owns the initial
|
|
67
|
+
* `search_read` decode and passes the resulting rows here.
|
|
68
|
+
*/
|
|
69
|
+
export declare const make: <Row extends HasId>(rpc: RpcSeam, model: string, rows: ReadonlyArray<Row>) => TypedRecordSet<Row>;
|
|
70
|
+
export {};
|
|
71
|
+
//# sourceMappingURL=typed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typed.d.ts","sourceRoot":"","sources":["../../src/records/typed.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAiC,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,+EAA+E;AAC/E,MAAM,MAAM,KAAK,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,CAAC,GAAG,IAAI;IAClC,QAAQ,EAAE,CAAC,IAAI,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,SAAS,gBAAgB,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK;CAC9E,CAAC,MAAM,GAAG,CAAC,CAAC;AAEb;;;;GAIG;AACH,MAAM,WAAW,cAAc,CAAC,GAAG,SAAS,KAAK,CAAE,SAAQ,QAAQ,CAAC,GAAG,CAAC;IACtE,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;IAClC,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;;;;;;;;OASG;IACH,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC,SAAS,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,KAAK,EAAE,CAAC,GAAG,CAAC,EAC7E,KAAK,EAAE,CAAC,EACR,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAEtD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,SAAS,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,KAAK,EAAE,CAAC,GAAG,CAAC,EAC5E,KAAK,EAAE,CAAC,EACR,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;CACjF;AAED,+EAA+E;AAC/E,KAAK,OAAO,GAAG,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAoClD;;;;GAIG;AACH,eAAO,MAAM,IAAI,GAAI,GAAG,SAAS,KAAK,OAC/B,OAAO,SACL,MAAM,QACP,aAAa,CAAC,GAAG,CAAC,KACvB,cAAc,CAAC,GAAG,CAyDpB,CAAC"}
|