@ic-reactor/candid 3.0.2-beta.0 → 3.0.2

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.
Files changed (83) hide show
  1. package/README.md +33 -1
  2. package/dist/adapter.js +2 -1
  3. package/dist/adapter.js.map +1 -1
  4. package/dist/display-reactor.d.ts +4 -13
  5. package/dist/display-reactor.d.ts.map +1 -1
  6. package/dist/display-reactor.js +22 -8
  7. package/dist/display-reactor.js.map +1 -1
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +3 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/metadata-display-reactor.d.ts +108 -0
  13. package/dist/metadata-display-reactor.d.ts.map +1 -0
  14. package/dist/metadata-display-reactor.js +141 -0
  15. package/dist/metadata-display-reactor.js.map +1 -0
  16. package/dist/reactor.d.ts +1 -1
  17. package/dist/reactor.d.ts.map +1 -1
  18. package/dist/reactor.js +10 -6
  19. package/dist/reactor.js.map +1 -1
  20. package/dist/types.d.ts +38 -7
  21. package/dist/types.d.ts.map +1 -1
  22. package/dist/utils.d.ts +4 -4
  23. package/dist/utils.d.ts.map +1 -1
  24. package/dist/utils.js +33 -10
  25. package/dist/utils.js.map +1 -1
  26. package/dist/visitor/arguments/helpers.d.ts +55 -0
  27. package/dist/visitor/arguments/helpers.d.ts.map +1 -0
  28. package/dist/visitor/arguments/helpers.js +123 -0
  29. package/dist/visitor/arguments/helpers.js.map +1 -0
  30. package/dist/visitor/arguments/index.d.ts +101 -0
  31. package/dist/visitor/arguments/index.d.ts.map +1 -0
  32. package/dist/visitor/arguments/index.js +780 -0
  33. package/dist/visitor/arguments/index.js.map +1 -0
  34. package/dist/visitor/arguments/types.d.ts +270 -0
  35. package/dist/visitor/arguments/types.d.ts.map +1 -0
  36. package/dist/visitor/arguments/types.js +26 -0
  37. package/dist/visitor/arguments/types.js.map +1 -0
  38. package/dist/visitor/constants.d.ts +4 -0
  39. package/dist/visitor/constants.d.ts.map +1 -0
  40. package/dist/visitor/constants.js +73 -0
  41. package/dist/visitor/constants.js.map +1 -0
  42. package/dist/visitor/helpers.d.ts +30 -0
  43. package/dist/visitor/helpers.d.ts.map +1 -0
  44. package/dist/visitor/helpers.js +204 -0
  45. package/dist/visitor/helpers.js.map +1 -0
  46. package/dist/visitor/index.d.ts +5 -0
  47. package/dist/visitor/index.d.ts.map +1 -0
  48. package/dist/visitor/index.js +5 -0
  49. package/dist/visitor/index.js.map +1 -0
  50. package/dist/visitor/returns/index.d.ts +38 -0
  51. package/dist/visitor/returns/index.d.ts.map +1 -0
  52. package/dist/visitor/returns/index.js +460 -0
  53. package/dist/visitor/returns/index.js.map +1 -0
  54. package/dist/visitor/returns/types.d.ts +202 -0
  55. package/dist/visitor/returns/types.d.ts.map +1 -0
  56. package/dist/visitor/returns/types.js +2 -0
  57. package/dist/visitor/returns/types.js.map +1 -0
  58. package/dist/visitor/types.d.ts +19 -0
  59. package/dist/visitor/types.d.ts.map +1 -0
  60. package/dist/visitor/types.js +2 -0
  61. package/dist/visitor/types.js.map +1 -0
  62. package/package.json +16 -7
  63. package/src/adapter.ts +446 -0
  64. package/src/constants.ts +11 -0
  65. package/src/display-reactor.ts +337 -0
  66. package/src/index.ts +8 -0
  67. package/src/metadata-display-reactor.ts +230 -0
  68. package/src/reactor.ts +199 -0
  69. package/src/types.ts +127 -0
  70. package/src/utils.ts +60 -0
  71. package/src/visitor/arguments/helpers.ts +153 -0
  72. package/src/visitor/arguments/index.test.ts +1439 -0
  73. package/src/visitor/arguments/index.ts +981 -0
  74. package/src/visitor/arguments/schema.test.ts +324 -0
  75. package/src/visitor/arguments/types.ts +387 -0
  76. package/src/visitor/constants.ts +76 -0
  77. package/src/visitor/helpers.test.ts +274 -0
  78. package/src/visitor/helpers.ts +223 -0
  79. package/src/visitor/index.ts +4 -0
  80. package/src/visitor/returns/index.test.ts +2377 -0
  81. package/src/visitor/returns/index.ts +658 -0
  82. package/src/visitor/returns/types.ts +302 -0
  83. package/src/visitor/types.ts +75 -0
@@ -0,0 +1,387 @@
1
+ import type { BaseActor, FunctionName, FunctionType } from "@ic-reactor/core"
2
+ import * as z from "zod"
3
+ import type { VisitorDataType, TextFormat, NumberFormat } from "../types"
4
+
5
+ export type { VisitorDataType, TextFormat, NumberFormat }
6
+
7
+ // ════════════════════════════════════════════════════════════════════════════
8
+ // Custom Error Class
9
+ // ════════════════════════════════════════════════════════════════════════════
10
+
11
+ /**
12
+ * Custom error class for metadata-related errors.
13
+ * Provides additional context about the field path and Candid type.
14
+ */
15
+ export class MetadataError extends Error {
16
+ constructor(
17
+ message: string,
18
+ public readonly fieldPath?: string,
19
+ public readonly candidType?: string
20
+ ) {
21
+ super(message)
22
+ this.name = "MetadataError"
23
+ }
24
+ }
25
+
26
+ // ════════════════════════════════════════════════════════════════════════════
27
+ // Component & UI Types
28
+ // ════════════════════════════════════════════════════════════════════════════
29
+
30
+ /**
31
+ * Suggested component type for rendering the field.
32
+ * Use this to map field types to your UI components.
33
+ */
34
+ export type FieldComponentType =
35
+ | "record-container"
36
+ | "tuple-container"
37
+ | "variant-select"
38
+ | "optional-toggle"
39
+ | "vector-list"
40
+ | "blob-upload"
41
+ | "principal-input"
42
+ | "text-input"
43
+ | "number-input"
44
+ | "boolean-checkbox"
45
+ | "null-hidden"
46
+ | "recursive-lazy"
47
+ | "unknown-fallback"
48
+
49
+ /**
50
+ * Input type hints for HTML input elements.
51
+ */
52
+ export type InputType =
53
+ | "text"
54
+ | "number"
55
+ | "checkbox"
56
+ | "select"
57
+ | "file"
58
+ | "textarea"
59
+
60
+ /**
61
+ * Rendering hints for the UI.
62
+ */
63
+ export interface RenderHint {
64
+ isCompound: boolean
65
+ isPrimitive: boolean
66
+ inputType?: InputType
67
+ description?: string
68
+ }
69
+
70
+ /**
71
+ * Pre-computed HTML input props for primitive fields.
72
+ */
73
+ export interface PrimitiveInputProps {
74
+ type?: "text" | "number" | "checkbox" | "email" | "url" | "tel"
75
+ placeholder?: string
76
+ min?: string | number
77
+ max?: string | number
78
+ step?: string | number
79
+ pattern?: string
80
+ inputMode?: "text" | "numeric" | "decimal" | "email" | "tel" | "url"
81
+ autoComplete?: string
82
+ spellCheck?: boolean
83
+ minLength?: number
84
+ maxLength?: number
85
+ }
86
+
87
+ // ════════════════════════════════════════════════════════════════════════════
88
+ // Base Field Interface
89
+ // ════════════════════════════════════════════════════════════════════════════
90
+
91
+ /**
92
+ * Base properties shared by all field nodes.
93
+ */
94
+ interface FieldBase<T extends VisitorDataType = VisitorDataType> {
95
+ /** The Candid type category (record, variant, text, number, etc.) */
96
+ type: T
97
+ /** Raw label from Candid definition */
98
+ label: string
99
+ /** Human-readable formatted label for display */
100
+ displayLabel: string
101
+ /** Form path compatible with TanStack Form (e.g., "[0]", "[0].owner") */
102
+ name: string
103
+ /** Suggested component type for rendering */
104
+ component: FieldComponentType
105
+ /** UI rendering hints */
106
+ renderHint: RenderHint
107
+ /** Zod validation schema */
108
+ schema: z.ZodTypeAny
109
+ /** Original Candid type name */
110
+ candidType: string
111
+ /** Default value for form initialization */
112
+ defaultValue: unknown
113
+ }
114
+
115
+ // ════════════════════════════════════════════════════════════════════════════
116
+ // Field Extras - Type-Specific Properties
117
+ // ════════════════════════════════════════════════════════════════════════════
118
+
119
+ /** Blob upload limits configuration */
120
+ export interface BlobLimits {
121
+ maxHexBytes: number
122
+ maxFileBytes: number
123
+ maxHexDisplayLength: number
124
+ }
125
+
126
+ /** Blob validation result */
127
+ export interface BlobValidationResult {
128
+ valid: boolean
129
+ error?: string
130
+ }
131
+
132
+ interface RecordExtras {
133
+ /** Child fields of the record */
134
+ fields: FieldNode[]
135
+ /** Default value as object with all field defaults */
136
+ defaultValue: Record<string, unknown>
137
+ }
138
+
139
+ interface VariantExtras {
140
+ /** All variant options as fields */
141
+ options: FieldNode[]
142
+ /** The default selected option key */
143
+ defaultOption: string
144
+ /** Default value with the first option selected */
145
+ defaultValue: Record<string, unknown>
146
+ /** Get default value for a specific option */
147
+ getOptionDefault: (option: string) => Record<string, unknown>
148
+ /** Get field descriptor for a specific option */
149
+ getOption: (option: string) => FieldNode
150
+ /** Get currently selected option key from a value */
151
+ getSelectedKey: (value: Record<string, unknown>) => string
152
+ /** Get the field for the currently selected option */
153
+ getSelectedOption: (value: Record<string, unknown>) => FieldNode
154
+ }
155
+
156
+ interface TupleExtras {
157
+ /** Tuple element fields */
158
+ fields: FieldNode[]
159
+ /** Default value as array of element defaults */
160
+ defaultValue: unknown[]
161
+ }
162
+
163
+ interface OptionalExtras {
164
+ /** The inner type field */
165
+ innerField: FieldNode
166
+ /** Default value is always null */
167
+ defaultValue: null
168
+ /** Get default value of the inner type when enabling */
169
+ getInnerDefault: () => unknown
170
+ /** Check if a value represents an enabled optional */
171
+ isEnabled: (value: unknown) => boolean
172
+ }
173
+
174
+ interface VectorExtras {
175
+ /** Template field for vector items */
176
+ itemField: FieldNode
177
+ /** Default value is empty array */
178
+ defaultValue: unknown[]
179
+ /** Get default value for a new item */
180
+ getItemDefault: () => unknown
181
+ /** Create a field node for an item at a specific index */
182
+ createItemField: (index: number, overrides?: { label?: string }) => FieldNode
183
+ }
184
+
185
+ interface BlobExtras {
186
+ /** Template field for blob bytes */
187
+ itemField: FieldNode
188
+ /** Accepted input formats */
189
+ acceptedFormats: ("hex" | "base64" | "file")[]
190
+ /** Upload limits */
191
+ limits: BlobLimits
192
+ /** Normalize hex input string */
193
+ normalizeHex: (input: string) => string
194
+ /** Validate blob input */
195
+ validateInput: (value: string | Uint8Array) => BlobValidationResult
196
+ /** Default value is empty string */
197
+ defaultValue: string
198
+ }
199
+
200
+ interface RecursiveExtras {
201
+ /** The recursive type name */
202
+ typeName: string
203
+ /** Lazily extract the inner type */
204
+ extract: () => FieldNode
205
+ /** Get default value of the inner type */
206
+ getInnerDefault: () => unknown
207
+ /** Default value is undefined */
208
+ defaultValue: undefined
209
+ }
210
+
211
+ interface PrincipalExtras {
212
+ /** Maximum Principal string length */
213
+ maxLength: number
214
+ /** Minimum Principal string length */
215
+ minLength: number
216
+ /** Detected text format */
217
+ format: TextFormat
218
+ /** Pre-computed HTML input props */
219
+ inputProps: PrimitiveInputProps
220
+ /** Default value is empty string */
221
+ defaultValue: string
222
+ }
223
+
224
+ interface NumberExtras {
225
+ /** Whether the number is unsigned (nat vs int) */
226
+ unsigned: boolean
227
+ /** Whether the number is a float */
228
+ isFloat: boolean
229
+ /** Bit width (8, 16, 32, 64) */
230
+ bits?: number
231
+ /** Minimum value as string */
232
+ min?: string
233
+ /** Maximum value as string */
234
+ max?: string
235
+ /** Detected number format */
236
+ format: NumberFormat
237
+ /** Pre-computed HTML input props */
238
+ inputProps: PrimitiveInputProps
239
+ /** Default value is empty string */
240
+ defaultValue: string
241
+ }
242
+
243
+ interface TextExtras {
244
+ /** Minimum text length */
245
+ minLength?: number
246
+ /** Maximum text length */
247
+ maxLength?: number
248
+ /** Whether to use multiline input */
249
+ multiline?: boolean
250
+ /** Detected text format */
251
+ format: TextFormat
252
+ /** Pre-computed HTML input props */
253
+ inputProps: PrimitiveInputProps
254
+ /** Default value is empty string */
255
+ defaultValue: string
256
+ }
257
+
258
+ interface BooleanExtras {
259
+ /** Pre-computed HTML input props */
260
+ inputProps: PrimitiveInputProps
261
+ /** Default value is false */
262
+ defaultValue: boolean
263
+ }
264
+
265
+ interface NullExtras {
266
+ /** Default value is null */
267
+ defaultValue: null
268
+ }
269
+
270
+ interface UnknownExtras {
271
+ /** Default value is undefined */
272
+ defaultValue: undefined
273
+ }
274
+
275
+ type FieldExtras<T extends VisitorDataType> = T extends "record"
276
+ ? RecordExtras
277
+ : T extends "variant"
278
+ ? VariantExtras
279
+ : T extends "tuple"
280
+ ? TupleExtras
281
+ : T extends "optional"
282
+ ? OptionalExtras
283
+ : T extends "vector"
284
+ ? VectorExtras
285
+ : T extends "blob"
286
+ ? BlobExtras
287
+ : T extends "recursive"
288
+ ? RecursiveExtras
289
+ : T extends "principal"
290
+ ? PrincipalExtras
291
+ : T extends "number"
292
+ ? NumberExtras
293
+ : T extends "text"
294
+ ? TextExtras
295
+ : T extends "boolean"
296
+ ? BooleanExtras
297
+ : T extends "null"
298
+ ? NullExtras
299
+ : T extends "unknown"
300
+ ? UnknownExtras
301
+ : {}
302
+
303
+ /**
304
+ * A unified field node that contains all metadata needed for rendering.
305
+ */
306
+ export type FieldNode<T extends VisitorDataType = VisitorDataType> =
307
+ T extends any ? FieldBase<T> & FieldExtras<T> : never
308
+
309
+ export type RecordField = FieldNode<"record">
310
+ export type VariantField = FieldNode<"variant">
311
+ export type TupleField = FieldNode<"tuple">
312
+ export type OptionalField = FieldNode<"optional">
313
+ export type VectorField = FieldNode<"vector">
314
+ export type BlobField = FieldNode<"blob">
315
+ export type RecursiveField = FieldNode<"recursive">
316
+ export type PrincipalField = FieldNode<"principal">
317
+ export type NumberField = FieldNode<"number">
318
+ export type TextField = FieldNode<"text">
319
+ export type BooleanField = FieldNode<"boolean">
320
+ export type NullField = FieldNode<"null">
321
+ export type UnknownField = FieldNode<"unknown">
322
+
323
+ // ════════════════════════════════════════════════════════════════════════════
324
+ // Form Metadata
325
+ // ════════════════════════════════════════════════════════════════════════════
326
+
327
+ /**
328
+ * Metadata for a single method's input arguments.
329
+ * Use this to generate dynamic forms for calling canister methods.
330
+ */
331
+ export interface ArgumentsMeta<
332
+ A = BaseActor,
333
+ Name extends FunctionName<A> = FunctionName<A>,
334
+ > {
335
+ /** The original Candid type signature for the method's arguments */
336
+ candidType: string
337
+ /** Whether this is a "query" or "update" call */
338
+ functionType: FunctionType
339
+ /** The method name as defined in the Candid interface */
340
+ functionName: Name
341
+ /** Array of field descriptors, one per argument */
342
+ args: FieldNode[]
343
+ /** Default values suitable for form initialization */
344
+ defaults: unknown[]
345
+ /** Zod schema for validating all arguments together */
346
+ schema: z.ZodTuple<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
347
+ /** Number of arguments (0 for no-arg methods) */
348
+ argCount: number
349
+ /** True if this method takes no arguments */
350
+ isEmpty: boolean
351
+ }
352
+
353
+ /**
354
+ * Service-level metadata mapping method names to their argument metadata.
355
+ */
356
+ export type ArgumentsServiceMeta<A = BaseActor> = {
357
+ [K in FunctionName<A>]: ArgumentsMeta<A, K>
358
+ }
359
+
360
+ // ════════════════════════════════════════════════════════════════════════════
361
+ // Type Utilities
362
+ // ════════════════════════════════════════════════════════════════════════════
363
+
364
+ /**
365
+ * Extract field type by VisitorDataType.
366
+ */
367
+ export type FieldByType<T extends VisitorDataType> = Extract<
368
+ FieldNode,
369
+ { type: T }
370
+ >
371
+
372
+ /** Compound field types that contain other fields */
373
+ export type CompoundField =
374
+ | RecordField
375
+ | VariantField
376
+ | TupleField
377
+ | OptionalField
378
+ | VectorField
379
+ | RecursiveField
380
+
381
+ /** Primitive field types with direct values */
382
+ export type PrimitiveField =
383
+ | PrincipalField
384
+ | NumberField
385
+ | TextField
386
+ | BooleanField
387
+ | NullField
@@ -0,0 +1,76 @@
1
+ import type { TextFormat, NumberFormat } from "./returns/types"
2
+
3
+ const TAMESTAMP_KEYS = [
4
+ "time",
5
+ "date",
6
+ "deadline",
7
+ "timestamp",
8
+ "timestamp_nanos",
9
+ "statusAt",
10
+ "createdAt",
11
+ "updatedAt",
12
+ "deletedAt",
13
+ "validUntil",
14
+ "status_at",
15
+ "created_at",
16
+ "updated_at",
17
+ "deleted_at",
18
+ "valid_until",
19
+ ]
20
+
21
+ // Fixed ReDoS vulnerability by eliminating nested quantifiers
22
+ // Original pattern ^[\w-]*key[\w-]*$ had catastrophic backtracking
23
+ // New approach: use simple substring matching (case-insensitive)
24
+ const createKeyMatcher = (keys: string[]): ((str: string) => boolean) => {
25
+ const lowerKeys = keys.map((k) => k.toLowerCase())
26
+ return (str: string) => {
27
+ const lower = str.toLowerCase()
28
+ return lowerKeys.some((key) => lower.includes(key))
29
+ }
30
+ }
31
+
32
+ const isTimestampKey = createKeyMatcher(TAMESTAMP_KEYS)
33
+
34
+ const CYCLE_KEYS = ["cycle", "cycles"]
35
+
36
+ const isCycleKey = createKeyMatcher(CYCLE_KEYS)
37
+
38
+ const ACCOUNT_ID_KEYS_REGEX =
39
+ /account_identifier|ledger_account|block_hash|transaction_hash|tx_hash/i
40
+
41
+ const tokenize = (label: string): Set<string> => {
42
+ const parts = label
43
+ .replace(/_/g, " ")
44
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
45
+ .toLowerCase()
46
+ .split(/[\s-]+/)
47
+ return new Set(parts)
48
+ }
49
+
50
+ export const checkTextFormat = (label?: string): TextFormat => {
51
+ if (!label) return "plain"
52
+
53
+ if (isTimestampKey(label)) return "timestamp"
54
+ if (ACCOUNT_ID_KEYS_REGEX.test(label)) return "account-id"
55
+
56
+ const tokens = tokenize(label)
57
+
58
+ if (tokens.has("email") || tokens.has("mail")) return "email"
59
+ if (tokens.has("phone") || tokens.has("tel") || tokens.has("mobile"))
60
+ return "phone"
61
+ if (tokens.has("url") || tokens.has("link") || tokens.has("website"))
62
+ return "url"
63
+ if (tokens.has("uuid") || tokens.has("guid")) return "uuid"
64
+ if (tokens.has("btc") || tokens.has("bitcoin")) return "btc"
65
+ if (tokens.has("eth") || tokens.has("ethereum")) return "eth"
66
+ if (tokens.has("principal") || tokens.has("canister")) return "principal"
67
+
68
+ return "plain"
69
+ }
70
+
71
+ export const checkNumberFormat = (label?: string): NumberFormat => {
72
+ if (!label) return "normal"
73
+ if (isTimestampKey(label)) return "timestamp"
74
+ if (isCycleKey(label)) return "cycle"
75
+ return "normal"
76
+ }