@ic-reactor/candid 3.0.11-beta.1 → 3.0.12-beta.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/dist/display-reactor.d.ts +1 -2
- package/dist/display-reactor.d.ts.map +1 -1
- package/dist/display-reactor.js +1 -1
- package/dist/display-reactor.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata-display-reactor.d.ts +3 -3
- package/dist/metadata-display-reactor.d.ts.map +1 -1
- package/dist/visitor/arguments/index.d.ts +58 -39
- package/dist/visitor/arguments/index.d.ts.map +1 -1
- package/dist/visitor/arguments/index.js +273 -81
- package/dist/visitor/arguments/index.js.map +1 -1
- package/dist/visitor/arguments/types.d.ts +228 -45
- package/dist/visitor/arguments/types.d.ts.map +1 -1
- package/dist/visitor/arguments/types.js +40 -1
- package/dist/visitor/arguments/types.js.map +1 -1
- package/dist/visitor/helpers.d.ts +1 -1
- package/dist/visitor/helpers.d.ts.map +1 -1
- package/dist/visitor/returns/index.d.ts +3 -3
- package/dist/visitor/returns/index.d.ts.map +1 -1
- package/dist/visitor/returns/index.js +54 -15
- package/dist/visitor/returns/index.js.map +1 -1
- package/dist/visitor/types.d.ts +2 -3
- package/dist/visitor/types.d.ts.map +1 -1
- package/dist/visitor/types.js +1 -2
- package/dist/visitor/types.js.map +1 -1
- package/package.json +6 -3
- package/src/display-reactor.ts +4 -6
- package/src/index.ts +0 -1
- package/src/metadata-display-reactor.ts +6 -6
- package/src/visitor/arguments/README.md +230 -0
- package/src/visitor/arguments/index.test.ts +144 -51
- package/src/visitor/arguments/index.ts +351 -146
- package/src/visitor/arguments/schema.test.ts +215 -0
- package/src/visitor/arguments/types.ts +287 -61
- package/src/visitor/helpers.ts +1 -1
- package/src/visitor/returns/index.test.ts +163 -1
- package/src/visitor/returns/index.ts +62 -16
- package/src/visitor/types.ts +2 -3
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
|
-
import { IDL } from "../types"
|
|
3
2
|
import { ResultFieldVisitor } from "./index"
|
|
4
3
|
import type {
|
|
5
4
|
ResultNode,
|
|
@@ -19,6 +18,7 @@ import type {
|
|
|
19
18
|
MethodMeta,
|
|
20
19
|
ServiceMeta,
|
|
21
20
|
} from "./types"
|
|
21
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
22
22
|
|
|
23
23
|
describe("ResultFieldVisitor", () => {
|
|
24
24
|
const visitor = new ResultFieldVisitor()
|
|
@@ -1974,3 +1974,165 @@ describe("ResultFieldVisitor", () => {
|
|
|
1974
1974
|
})
|
|
1975
1975
|
})
|
|
1976
1976
|
})
|
|
1977
|
+
|
|
1978
|
+
describe("ResultFieldVisitor Reproduction - User reported issue", () => {
|
|
1979
|
+
const visitor = new ResultFieldVisitor()
|
|
1980
|
+
|
|
1981
|
+
it("should handle record data provided as an array (tuple-like) even if IDL has names", () => {
|
|
1982
|
+
const Rule = IDL.Variant({
|
|
1983
|
+
Quorum: IDL.Record({
|
|
1984
|
+
min_approved: IDL.Nat,
|
|
1985
|
+
approvers: IDL.Variant({ Group: IDL.Vec(IDL.Principal) }),
|
|
1986
|
+
}),
|
|
1987
|
+
})
|
|
1988
|
+
|
|
1989
|
+
const NamedRule = IDL.Record({
|
|
1990
|
+
description: IDL.Opt(IDL.Text),
|
|
1991
|
+
id: IDL.Text,
|
|
1992
|
+
name: IDL.Text,
|
|
1993
|
+
rule: Rule,
|
|
1994
|
+
})
|
|
1995
|
+
|
|
1996
|
+
const field = visitor.visitRecord(
|
|
1997
|
+
NamedRule,
|
|
1998
|
+
[
|
|
1999
|
+
["description", IDL.Opt(IDL.Text)],
|
|
2000
|
+
["id", IDL.Text],
|
|
2001
|
+
["name", IDL.Text],
|
|
2002
|
+
["rule", Rule],
|
|
2003
|
+
],
|
|
2004
|
+
"NamedRule"
|
|
2005
|
+
)
|
|
2006
|
+
|
|
2007
|
+
// Simulate lib-agent returning an array for a named record
|
|
2008
|
+
const arrayData = [
|
|
2009
|
+
[], // description (empty opt)
|
|
2010
|
+
"1253ec41-ef1d-4317-bb82-2366fc34f37c", // id
|
|
2011
|
+
"Admin approval", // name
|
|
2012
|
+
{ Quorum: { min_approved: BigInt(1), approvers: { Group: [] } } }, // rule
|
|
2013
|
+
]
|
|
2014
|
+
|
|
2015
|
+
// Before the fix, this would throw because arrayData["rule"] is undefined
|
|
2016
|
+
const resolved = field.resolve(arrayData)
|
|
2017
|
+
|
|
2018
|
+
expect(resolved.fields.id.value).toBe(
|
|
2019
|
+
"1253ec41-ef1d-4317-bb82-2366fc34f37c"
|
|
2020
|
+
)
|
|
2021
|
+
expect(resolved.fields.name.value).toBe("Admin approval")
|
|
2022
|
+
|
|
2023
|
+
const ruleNode = resolved.fields.rule as VariantNode
|
|
2024
|
+
expect(ruleNode.selected).toBe("Quorum")
|
|
2025
|
+
})
|
|
2026
|
+
|
|
2027
|
+
it("should handle already transformed variant data with _type", () => {
|
|
2028
|
+
const Rule = IDL.Variant({
|
|
2029
|
+
Quorum: IDL.Text,
|
|
2030
|
+
})
|
|
2031
|
+
|
|
2032
|
+
const field = visitor.visitVariant(Rule, [["Quorum", IDL.Text]], "Rule")
|
|
2033
|
+
|
|
2034
|
+
// Data transformed by DisplayCodecVisitor
|
|
2035
|
+
const transformedData = {
|
|
2036
|
+
_type: "Quorum",
|
|
2037
|
+
Quorum: "some text",
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Before the fix, this would throw because transformedData["Quorum"] is there but
|
|
2041
|
+
// Object.keys(transformedData)[0] is "_type"
|
|
2042
|
+
const resolved = field.resolve(transformedData)
|
|
2043
|
+
|
|
2044
|
+
expect(resolved.selected).toBe("Quorum")
|
|
2045
|
+
expect(resolved.selectedOption.value).toBe("some text")
|
|
2046
|
+
})
|
|
2047
|
+
|
|
2048
|
+
it("should handle already transformed optional data (unwrapped)", () => {
|
|
2049
|
+
const OptText = IDL.Opt(IDL.Text)
|
|
2050
|
+
const field = visitor.visitOpt(OptText, IDL.Text, "maybeText")
|
|
2051
|
+
|
|
2052
|
+
// Raw format is [value]
|
|
2053
|
+
expect(field.resolve(["hello"]).value?.value).toBe("hello")
|
|
2054
|
+
expect(field.resolve([]).value).toBeNull()
|
|
2055
|
+
|
|
2056
|
+
// Transformed format is just the value
|
|
2057
|
+
expect(field.resolve("hello").value?.value).toBe("hello")
|
|
2058
|
+
expect(field.resolve(null).value).toBeNull()
|
|
2059
|
+
expect(field.resolve(undefined).value).toBeNull()
|
|
2060
|
+
})
|
|
2061
|
+
|
|
2062
|
+
it("should provide a better error message when an option is missing", () => {
|
|
2063
|
+
const MyVariant = IDL.Variant({ A: IDL.Null, B: IDL.Null })
|
|
2064
|
+
const field = visitor.visitVariant(
|
|
2065
|
+
MyVariant,
|
|
2066
|
+
[
|
|
2067
|
+
["A", IDL.Null],
|
|
2068
|
+
["B", IDL.Null],
|
|
2069
|
+
],
|
|
2070
|
+
"MyVariant"
|
|
2071
|
+
)
|
|
2072
|
+
|
|
2073
|
+
expect(() => field.resolve({ C: null })).toThrow(
|
|
2074
|
+
/Option C not found in variant MyVariant. Available options: A, B/
|
|
2075
|
+
)
|
|
2076
|
+
})
|
|
2077
|
+
|
|
2078
|
+
describe("Recursive types", () => {
|
|
2079
|
+
it("should handle recursive variant with transformed data", () => {
|
|
2080
|
+
const Rule = IDL.Rec()
|
|
2081
|
+
const RuleType = IDL.Variant({
|
|
2082
|
+
Quorum: IDL.Record({
|
|
2083
|
+
min_approved: IDL.Nat,
|
|
2084
|
+
}),
|
|
2085
|
+
Nested: Rule,
|
|
2086
|
+
})
|
|
2087
|
+
Rule.fill(RuleType)
|
|
2088
|
+
|
|
2089
|
+
const field = visitor.visitRec(Rule, RuleType, "rule")
|
|
2090
|
+
|
|
2091
|
+
// Transformed data (using _type)
|
|
2092
|
+
const transformedData = {
|
|
2093
|
+
_type: "Quorum",
|
|
2094
|
+
Quorum: {
|
|
2095
|
+
min_approved: "1",
|
|
2096
|
+
},
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// This should work because Rule.resolve calls Variant.resolve
|
|
2100
|
+
const resolved = field.resolve(transformedData) as RecursiveNode
|
|
2101
|
+
const variantNode = resolved.inner as VariantNode
|
|
2102
|
+
|
|
2103
|
+
expect(variantNode.selected).toBe("Quorum")
|
|
2104
|
+
expect(variantNode.selectedOption.type).toBe("record")
|
|
2105
|
+
})
|
|
2106
|
+
|
|
2107
|
+
it("should handle deeply nested recursive variant with transformed data", () => {
|
|
2108
|
+
const Rule = IDL.Rec()
|
|
2109
|
+
const RuleType = IDL.Variant({
|
|
2110
|
+
Quorum: IDL.Record({
|
|
2111
|
+
min_approved: IDL.Nat,
|
|
2112
|
+
}),
|
|
2113
|
+
Nested: Rule,
|
|
2114
|
+
})
|
|
2115
|
+
Rule.fill(RuleType)
|
|
2116
|
+
|
|
2117
|
+
const field = visitor.visitRec(Rule, RuleType, "rule")
|
|
2118
|
+
|
|
2119
|
+
const transformedData = {
|
|
2120
|
+
_type: "Nested",
|
|
2121
|
+
Nested: {
|
|
2122
|
+
_type: "Quorum",
|
|
2123
|
+
Quorum: {
|
|
2124
|
+
min_approved: "2",
|
|
2125
|
+
},
|
|
2126
|
+
},
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
const resolved = field.resolve(transformedData) as RecursiveNode
|
|
2130
|
+
const variantNode = resolved.inner as VariantNode
|
|
2131
|
+
expect(variantNode.selected).toBe("Nested")
|
|
2132
|
+
|
|
2133
|
+
const nestedResolved = variantNode.selectedOption as RecursiveNode
|
|
2134
|
+
const innerVariant = nestedResolved.inner as VariantNode
|
|
2135
|
+
expect(innerVariant.selected).toBe("Quorum")
|
|
2136
|
+
})
|
|
2137
|
+
})
|
|
2138
|
+
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { isQuery } from "../helpers"
|
|
2
2
|
import { checkTextFormat, checkNumberFormat } from "../constants"
|
|
3
|
-
import { IDL } from "../types"
|
|
4
3
|
import type {
|
|
5
4
|
ResultNode,
|
|
6
5
|
ResolvedNode,
|
|
@@ -12,8 +11,8 @@ import type {
|
|
|
12
11
|
TextFormat,
|
|
13
12
|
} from "./types"
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
import {
|
|
14
|
+
import { sha256 } from "@noble/hashes/sha2.js"
|
|
15
|
+
import { IDL } from "@icp-sdk/core/candid"
|
|
17
16
|
import { DisplayCodecVisitor, uint8ArrayToHex } from "@ic-reactor/core"
|
|
18
17
|
import type {
|
|
19
18
|
ActorMethodReturnType,
|
|
@@ -48,11 +47,17 @@ function primitiveNode<T extends NodeType>(
|
|
|
48
47
|
displayType,
|
|
49
48
|
...extras,
|
|
50
49
|
resolve(data: unknown): ResolvedNode<T> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
try {
|
|
51
|
+
return {
|
|
52
|
+
...node,
|
|
53
|
+
value: codec.decode(data),
|
|
54
|
+
raw: data,
|
|
55
|
+
} as unknown as ResolvedNode<T>
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Failed to decode field "${label}" of type ${type} (${candidType}) with data: ${JSON.stringify(data)}. Error: ${e}`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
56
61
|
},
|
|
57
62
|
} as unknown as ResultNode<T>
|
|
58
63
|
return node
|
|
@@ -68,8 +73,19 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
68
73
|
> {
|
|
69
74
|
private codec = new DisplayCodecVisitor()
|
|
70
75
|
|
|
76
|
+
private recCache = new Map<IDL.RecClass<any>, ResultNode<"recursive">>()
|
|
77
|
+
|
|
71
78
|
private getCodec(t: IDL.Type): Codec {
|
|
72
|
-
|
|
79
|
+
const codec = t.accept(this.codec, null) as any
|
|
80
|
+
return {
|
|
81
|
+
decode: (v: unknown) => {
|
|
82
|
+
try {
|
|
83
|
+
return typeof codec?.decode === "function" ? codec.decode(v) : v
|
|
84
|
+
} catch {
|
|
85
|
+
return v
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -138,8 +154,20 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
138
154
|
}
|
|
139
155
|
const recordData = data as Record<string, unknown>
|
|
140
156
|
const resolvedFields: Record<string, ResolvedNode> = {}
|
|
157
|
+
let index = 0
|
|
141
158
|
for (const [key, field] of Object.entries(fields)) {
|
|
142
|
-
|
|
159
|
+
// Try named key first, then try numeric index (for tuples/indexed records)
|
|
160
|
+
const value =
|
|
161
|
+
recordData[key] !== undefined ? recordData[key] : recordData[index]
|
|
162
|
+
|
|
163
|
+
if (!field || typeof field.resolve !== "function") {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Field "${key}" in record "${label}" is not a valid ResultNode. Got: ${JSON.stringify(field)}`
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
resolvedFields[key] = field.resolve(value)
|
|
170
|
+
index++
|
|
143
171
|
}
|
|
144
172
|
return { ...node, fields: resolvedFields, raw: data }
|
|
145
173
|
},
|
|
@@ -170,10 +198,15 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
170
198
|
)
|
|
171
199
|
}
|
|
172
200
|
const variantData = data as Record<string, unknown>
|
|
173
|
-
|
|
201
|
+
// Support both raw { Selected: value } and transformed { _type: 'Selected', Selected: value }
|
|
202
|
+
const selected =
|
|
203
|
+
(variantData._type as string) || Object.keys(variantData)[0]
|
|
174
204
|
const optionNode = selectedOption[selected]
|
|
205
|
+
|
|
175
206
|
if (!optionNode) {
|
|
176
|
-
throw new Error(
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Option ${selected} not found in variant ${label}. Available options: ${Object.keys(selectedOption).join(", ")}`
|
|
209
|
+
)
|
|
177
210
|
}
|
|
178
211
|
return {
|
|
179
212
|
...node,
|
|
@@ -230,9 +263,16 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
230
263
|
displayType: "nullable",
|
|
231
264
|
value: null, // null until resolved
|
|
232
265
|
resolve(data: unknown): ResolvedNode<"optional"> {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
266
|
+
// If data is an array (raw format [T] or []), unwrap it.
|
|
267
|
+
// Otherwise, use data directly (already transformed or null/undefined).
|
|
268
|
+
const resolved = Array.isArray(data)
|
|
269
|
+
? data.length > 0
|
|
270
|
+
? inner.resolve(data[0])
|
|
271
|
+
: null
|
|
272
|
+
: data !== null && data !== undefined
|
|
273
|
+
? inner.resolve(data)
|
|
274
|
+
: null
|
|
275
|
+
|
|
236
276
|
return { ...node, value: resolved, raw: data }
|
|
237
277
|
},
|
|
238
278
|
}
|
|
@@ -300,10 +340,14 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
300
340
|
}
|
|
301
341
|
|
|
302
342
|
public visitRec<T>(
|
|
303
|
-
|
|
343
|
+
t: IDL.RecClass<T>,
|
|
304
344
|
ty: IDL.ConstructType<T>,
|
|
305
345
|
label: string
|
|
306
346
|
): ResultNode<"recursive"> {
|
|
347
|
+
if (this.recCache.has(t)) {
|
|
348
|
+
return this.recCache.get(t)! as ResultNode<"recursive">
|
|
349
|
+
}
|
|
350
|
+
|
|
307
351
|
const self = this
|
|
308
352
|
// Lazy extraction to prevent infinite loops
|
|
309
353
|
let innerSchema: ResultNode | null = null
|
|
@@ -320,6 +364,8 @@ export class ResultFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
320
364
|
return { ...node, inner: getInner().resolve(data), raw: data }
|
|
321
365
|
},
|
|
322
366
|
}
|
|
367
|
+
|
|
368
|
+
this.recCache.set(t, node)
|
|
323
369
|
return node
|
|
324
370
|
}
|
|
325
371
|
|
package/src/visitor/types.ts
CHANGED
|
@@ -17,10 +17,9 @@ export type FieldType =
|
|
|
17
17
|
| "boolean"
|
|
18
18
|
| "null"
|
|
19
19
|
|
|
20
|
-
export {
|
|
21
|
-
export { Principal } from "@icp-sdk/core/principal"
|
|
20
|
+
export type { Principal } from "@icp-sdk/core/principal"
|
|
22
21
|
|
|
23
|
-
import { IDL } from "@icp-sdk/core/candid"
|
|
22
|
+
import type { IDL } from "@icp-sdk/core/candid"
|
|
24
23
|
export type AllNumberTypes =
|
|
25
24
|
| IDL.NatClass
|
|
26
25
|
| IDL.IntClass
|