Skip to content

Commit 57dafa3

Browse files
authored
release v3.3.2 (#1101)
1 parent d14b59c commit 57dafa3

File tree

11 files changed

+300
-18
lines changed

11 files changed

+300
-18
lines changed

components/openapi/api-playground/index.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useApiCredentials } from '@/providers/api-credentials-provider';
1414
import type { OpenAPIOperation } from '../types';
1515
import { RequestBuilder } from './request-builder';
1616
import { executeRequest } from './request-executor';
17+
import { coerceValueForSchema } from './schema-utils';
1718

1819
interface APIPlaygroundProps {
1920
operation: OpenAPIOperation;
@@ -230,6 +231,8 @@ export function APIPlayground({
230231
}
231232

232233
let finalFormData = { ...formData };
234+
let bodyFieldErrors: string[] = [];
235+
233236
if (operation.requestBody) {
234237
const bodySchema = operation.requestBody.content?.['application/json']?.schema;
235238
if (bodySchema?.type === 'object' && bodySchema.properties) {
@@ -294,18 +297,39 @@ export function APIPlayground({
294297
});
295298
} catch (error) {
296299
console.error('Failed to convert arguments:', error);
297-
bodyObject[propName] = fieldValue;
300+
bodyFieldErrors.push(
301+
error instanceof Error ? error.message : `Invalid value provided for ${propName}`,
302+
);
298303
}
299304
} else if (propName === 'sender') {
300305
// Sender stays as string
301306
bodyObject[propName] = fieldValue;
302307
} else {
303-
// Other fields - no conversion for now
304-
bodyObject[propName] = fieldValue;
308+
try {
309+
bodyObject[propName] = coerceValueForSchema(fieldValue, propSchema, {
310+
strict: true,
311+
fieldName: propName,
312+
});
313+
} catch (error) {
314+
bodyFieldErrors.push(
315+
error instanceof Error ? error.message : `Invalid value provided for ${propName}`,
316+
);
317+
}
305318
}
306319
}
307320
}
308321

322+
if (bodyFieldErrors.length > 0) {
323+
if (!openSections.includes('body')) {
324+
setOpenSections((prev) => [...prev, 'body']);
325+
}
326+
setResponse({
327+
status: 0,
328+
error: bodyFieldErrors[0],
329+
});
330+
return;
331+
}
332+
309333
finalFormData = {
310334
...finalFormData,
311335
body: JSON.stringify(bodyObject, null, 2),

components/openapi/api-playground/request-builder.tsx

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
1010
import { cn } from '@/lib/utils';
1111
import type { OpenAPIOperation, OpenAPIParameter } from '../types';
1212
import { ClarityConverter, type ClarityTypeHint } from './clarity-converter';
13+
import { coerceValueForSchema, resolveEffectiveSchema } from './schema-utils';
1314

1415
interface RequestBuilderProps {
1516
operation: OpenAPIOperation;
@@ -135,12 +136,14 @@ export const RequestBuilder = forwardRef<HTMLFormElement, RequestBuilderProps>(
135136
const bodySchema = operation.requestBody.content?.['application/json']?.schema;
136137
if (bodySchema?.type === 'object' && bodySchema.properties) {
137138
const bodyObject: Record<string, any> = {};
139+
const parseErrors: Record<string, string> = {};
138140

139141
for (const [propName, propSchema] of Object.entries(bodySchema.properties) as [
140142
string,
141143
any,
142144
][]) {
143145
const fieldValue = formData[`body.${propName}`];
146+
const fieldName = `body.${propName}`;
144147
if (fieldValue !== undefined && fieldValue !== '') {
145148
if (clarityConversion) {
146149
if (propName === 'arguments' && propSchema.type === 'array') {
@@ -152,7 +155,10 @@ export const RequestBuilder = forwardRef<HTMLFormElement, RequestBuilderProps>(
152155
});
153156
} catch (error) {
154157
console.error('Failed to convert arguments:', error);
155-
bodyObject[propName] = fieldValue;
158+
parseErrors[fieldName] =
159+
error instanceof Error
160+
? error.message
161+
: `Invalid value provided for ${propName}`;
156162
}
157163
} else {
158164
if (propName === 'sender') {
@@ -167,19 +173,54 @@ export const RequestBuilder = forwardRef<HTMLFormElement, RequestBuilderProps>(
167173
);
168174
bodyObject[propName] = cvToHex(clarityValue);
169175
} catch (error) {
170-
bodyObject[propName] = fieldValue;
176+
try {
177+
bodyObject[propName] = coerceValueForSchema(fieldValue, propSchema, {
178+
strict: true,
179+
fieldName: propName,
180+
});
181+
} catch (coerceError) {
182+
parseErrors[fieldName] =
183+
coerceError instanceof Error
184+
? coerceError.message
185+
: `Invalid value provided for ${propName}`;
186+
}
171187
}
172188
} else {
173-
bodyObject[propName] = fieldValue;
189+
try {
190+
bodyObject[propName] = coerceValueForSchema(fieldValue, propSchema, {
191+
strict: true,
192+
fieldName: propName,
193+
});
194+
} catch (error) {
195+
parseErrors[fieldName] =
196+
error instanceof Error
197+
? error.message
198+
: `Invalid value provided for ${propName}`;
199+
}
174200
}
175201
}
176202
}
177203
} else {
178-
bodyObject[propName] = fieldValue;
204+
try {
205+
bodyObject[propName] = coerceValueForSchema(fieldValue, propSchema, {
206+
strict: true,
207+
fieldName: propName,
208+
});
209+
} catch (error) {
210+
parseErrors[fieldName] =
211+
error instanceof Error
212+
? error.message
213+
: `Invalid value provided for ${propName}`;
214+
}
179215
}
180216
}
181217
}
182218

219+
if (Object.keys(parseErrors).length > 0) {
220+
setErrors((prev) => ({ ...prev, ...parseErrors }));
221+
return;
222+
}
223+
183224
finalFormData = {
184225
...finalFormData,
185226
body: JSON.stringify(bodyObject, null, 2),
@@ -422,9 +463,26 @@ export const RequestBuilder = forwardRef<HTMLFormElement, RequestBuilderProps>(
422463
const fieldName = `body.${propName}`;
423464
const isRequired = bodySchema.required?.includes(propName);
424465
const hasError = !!errors[fieldName];
466+
const resolvedPropSchema = resolveEffectiveSchema(propSchema) || propSchema;
467+
const schemaType = resolvedPropSchema?.type || propSchema.type;
425468
const clarityType = clarityConversion
426469
? detectClarityType(propName, propSchema, formData[fieldName] || '')
427470
: null;
471+
const exampleValue =
472+
typeof propSchema.example === 'string'
473+
? propSchema.example
474+
: propSchema.example
475+
? JSON.stringify(propSchema.example, null, 2)
476+
: undefined;
477+
const placeholder =
478+
exampleValue ||
479+
propSchema.description ||
480+
(schemaType === 'object'
481+
? 'Enter JSON object'
482+
: schemaType === 'array'
483+
? 'Enter array values as JSON array'
484+
: undefined);
485+
const shouldUseTextarea = schemaType === 'array' || schemaType === 'object';
428486

429487
return (
430488
<div key={propName} className="space-y-2">
@@ -451,19 +509,17 @@ export const RequestBuilder = forwardRef<HTMLFormElement, RequestBuilderProps>(
451509
)}
452510
</div>
453511

454-
{propSchema.type === 'array' ? (
512+
{shouldUseTextarea ? (
455513
<Textarea
456514
id={fieldName}
457515
value={formData[fieldName] || ''}
458516
onChange={(e) => handleInputChange(fieldName, e.target.value)}
459517
placeholder={
460-
propSchema.example?.toString() ||
461-
propSchema.description ||
462-
(propName === 'arguments'
518+
propName === 'arguments' && schemaType === 'array'
463519
? 'e.g. [SP123...] or [SP123..., 100] or [1, [2, 3, 4]]'
464-
: 'Enter array values as JSON array')
520+
: placeholder
465521
}
466-
rows={3}
522+
rows={schemaType === 'array' ? 3 : 5}
467523
className={cn(
468524
'font-mono text-sm bg-white dark:bg-neutral-950 border-border',
469525
hasError && 'border-red-500',

components/openapi/api-playground/request-executor.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ export async function executeRequest(
6666
// Build headers
6767
const headers: Record<string, string> = {
6868
Accept: 'application/json',
69-
'Content-Type': 'application/json',
7069
};
7170

7271
const headerParameters = operation.parameters?.filter((p) => p.in === 'header') || [];
@@ -90,7 +89,9 @@ export async function executeRequest(
9089
);
9190

9291
let requestBody: any;
93-
if (formData.body && (operation.requestBody || methodSupportsBody)) {
92+
const shouldAttachBody = formData.body && (operation.requestBody || methodSupportsBody);
93+
94+
if (shouldAttachBody) {
9495
try {
9596
// Parse and re-stringify to validate JSON
9697
const parsedBody = JSON.parse(formData.body);
@@ -104,6 +105,10 @@ export async function executeRequest(
104105
console.warn(`Request body is required for ${operation.method} ${operation.path}`);
105106
}
106107

108+
if (requestBody !== undefined) {
109+
headers['Content-Type'] = 'application/json';
110+
}
111+
107112
const startTime = performance.now();
108113

109114
try {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
type JSONSchema = {
2+
type?: string | string[];
3+
anyOf?: JSONSchema[];
4+
oneOf?: JSONSchema[];
5+
allOf?: JSONSchema[];
6+
[key: string]: any;
7+
};
8+
9+
interface CoerceOptions {
10+
strict?: boolean;
11+
fieldName?: string;
12+
}
13+
14+
const COMPOSITE_KEYS: Array<'anyOf' | 'oneOf' | 'allOf'> = ['anyOf', 'oneOf', 'allOf'];
15+
16+
const normalizeType = (schemaType?: string | string[]) => {
17+
if (!schemaType) return undefined;
18+
if (typeof schemaType === 'string') {
19+
return schemaType === 'null' ? undefined : schemaType;
20+
}
21+
if (Array.isArray(schemaType)) {
22+
return schemaType.find((type) => type !== 'null');
23+
}
24+
return undefined;
25+
};
26+
27+
export const resolveEffectiveSchema = (schema?: JSONSchema): JSONSchema | undefined => {
28+
if (!schema) return undefined;
29+
30+
const normalizedType = normalizeType(schema.type);
31+
if (normalizedType) {
32+
return { ...schema, type: normalizedType };
33+
}
34+
35+
for (const key of COMPOSITE_KEYS) {
36+
const candidates = schema[key];
37+
if (!Array.isArray(candidates)) continue;
38+
39+
for (const candidate of candidates) {
40+
const resolved = resolveEffectiveSchema(candidate);
41+
if (resolved?.type && resolved.type !== 'null') {
42+
return resolved;
43+
}
44+
}
45+
46+
if (candidates.length > 0) {
47+
return candidates[0];
48+
}
49+
}
50+
51+
return schema;
52+
};
53+
54+
const formatErrorMessage = (fieldName: string | undefined, message: string) =>
55+
fieldName ? `${fieldName} ${message}` : message;
56+
57+
export const coerceValueForSchema = (
58+
rawValue: string,
59+
schema?: JSONSchema,
60+
options?: CoerceOptions,
61+
) => {
62+
if (rawValue === undefined || rawValue === null) return rawValue;
63+
if (typeof rawValue !== 'string') return rawValue;
64+
65+
const trimmed = rawValue.trim();
66+
if (!trimmed) return rawValue;
67+
if (trimmed === 'null') return null;
68+
69+
const resolvedSchema = resolveEffectiveSchema(schema);
70+
const targetType = resolvedSchema?.type;
71+
72+
if (targetType === 'object' || targetType === 'array') {
73+
try {
74+
return JSON.parse(trimmed);
75+
} catch {
76+
if (options?.strict) {
77+
throw new Error(
78+
formatErrorMessage(
79+
options.fieldName,
80+
`must be valid JSON ${targetType === 'array' ? 'array' : 'object'}`,
81+
),
82+
);
83+
}
84+
console.warn('Failed to parse JSON body field value. Sending raw string instead.');
85+
return rawValue;
86+
}
87+
}
88+
89+
if (targetType === 'integer' || targetType === 'number') {
90+
const parsed = Number(trimmed);
91+
if (Number.isNaN(parsed)) {
92+
if (options?.strict) {
93+
throw new Error(
94+
formatErrorMessage(
95+
options.fieldName,
96+
`must be a valid ${targetType === 'integer' ? 'integer' : 'number'}`,
97+
),
98+
);
99+
}
100+
return rawValue;
101+
}
102+
103+
if (options?.strict && targetType === 'integer' && !Number.isInteger(parsed)) {
104+
throw new Error(formatErrorMessage(options.fieldName, 'must be an integer'));
105+
}
106+
107+
return parsed;
108+
}
109+
110+
if (targetType === 'boolean') {
111+
if (trimmed.toLowerCase() === 'true') return true;
112+
if (trimmed.toLowerCase() === 'false') return false;
113+
if (options?.strict) {
114+
throw new Error(formatErrorMessage(options.fieldName, 'must be a boolean (true or false)'));
115+
}
116+
return rawValue;
117+
}
118+
119+
return rawValue;
120+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: Get consumer secret
3+
sidebarTitle: Get consumer secret
4+
description: Retrieves current event payload consumer secret
5+
full: true
6+
---
7+
8+
<APIPage
9+
document="./openapi/chainhook-api.json"
10+
operations={[{ path: '/me/secret', method: 'get' }]}
11+
hasHead={false}
12+
/>

content/docs/es/apis/chainhooks-api/reference/chainhooks/meta.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

content/docs/es/apis/chainhooks-api/reference/info/meta.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

content/docs/es/apis/chainhooks-api/reference/secrets/get-secret.mdx

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)