Skip to content

Commit 8b9de31

Browse files
feat(core): Implement Test workflow MCP tool (n8n-io#27348)
1 parent 6de4662 commit 8b9de31

21 files changed

Lines changed: 3039 additions & 90 deletions

packages/@n8n/workflow-sdk/src/generate-types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,8 @@ export {
7878
planSplitVersionSchemaFiles,
7979
} from './generate-zod-schemas';
8080

81+
// JSON Schema inference from data
82+
export { generateJsonSchemaFromData } from './json-schema-from-data';
83+
8184
// Zod helpers (for use in generated files)
8285
export * from './zod-helpers';
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { generateJsonSchemaFromData } from './json-schema-from-data';
2+
3+
describe('generateJsonSchemaFromData', () => {
4+
it('infers string type', () => {
5+
expect(generateJsonSchemaFromData('hello')).toEqual({ type: 'string' });
6+
});
7+
8+
it('infers number type', () => {
9+
expect(generateJsonSchemaFromData(42)).toEqual({ type: 'number' });
10+
});
11+
12+
it('infers boolean type', () => {
13+
expect(generateJsonSchemaFromData(true)).toEqual({ type: 'boolean' });
14+
});
15+
16+
it('infers null type', () => {
17+
expect(generateJsonSchemaFromData(null)).toEqual({ type: 'null' });
18+
});
19+
20+
it('infers undefined as null', () => {
21+
expect(generateJsonSchemaFromData(undefined)).toEqual({ type: 'null' });
22+
});
23+
24+
it('infers flat object', () => {
25+
expect(generateJsonSchemaFromData({ name: 'test', count: 5 })).toEqual({
26+
type: 'object',
27+
properties: {
28+
name: { type: 'string' },
29+
count: { type: 'number' },
30+
},
31+
required: ['name', 'count'],
32+
});
33+
});
34+
35+
it('infers nested object', () => {
36+
expect(generateJsonSchemaFromData({ user: { email: 'a@b.com', active: true } })).toEqual({
37+
type: 'object',
38+
properties: {
39+
user: {
40+
type: 'object',
41+
properties: {
42+
email: { type: 'string' },
43+
active: { type: 'boolean' },
44+
},
45+
required: ['email', 'active'],
46+
},
47+
},
48+
required: ['user'],
49+
});
50+
});
51+
52+
it('infers array with items', () => {
53+
expect(generateJsonSchemaFromData([{ id: 1 }])).toEqual({
54+
type: 'array',
55+
items: {
56+
type: 'object',
57+
properties: {
58+
id: { type: 'number' },
59+
},
60+
required: ['id'],
61+
},
62+
});
63+
});
64+
65+
it('infers empty array', () => {
66+
expect(generateJsonSchemaFromData([])).toEqual({
67+
type: 'array',
68+
items: {},
69+
});
70+
});
71+
72+
it('does not retain actual values', () => {
73+
const data = { secret: 'my-api-key', email: 'user@private.com', count: 999 };
74+
const schema = generateJsonSchemaFromData(data);
75+
76+
const json = JSON.stringify(schema);
77+
expect(json).not.toContain('my-api-key');
78+
expect(json).not.toContain('user@private.com');
79+
expect(json).not.toContain('999');
80+
});
81+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* JSON Schema from Data Inferrer
3+
*
4+
* Infers a JSON Schema from a concrete data value.
5+
* Used to derive output shapes from execution history without
6+
* exposing the actual data values.
7+
*/
8+
9+
import type { JsonSchema } from './generate-types';
10+
11+
/**
12+
* Infer a JSON Schema from a concrete data value.
13+
*
14+
* Recursively inspects the value to produce a schema describing
15+
* its shape (types, property names, array item types) without
16+
* retaining any of the original values.
17+
*/
18+
export function generateJsonSchemaFromData(value: unknown): JsonSchema {
19+
if (value === null || value === undefined) {
20+
return { type: 'null' };
21+
}
22+
23+
const type = typeof value;
24+
25+
if (type === 'string') return { type: 'string' };
26+
if (type === 'number') return { type: 'number' };
27+
if (type === 'boolean') return { type: 'boolean' };
28+
29+
if (Array.isArray(value)) {
30+
return {
31+
type: 'array',
32+
items: value.length > 0 ? generateJsonSchemaFromData(value[0]) : {},
33+
};
34+
}
35+
36+
if (type === 'object') {
37+
const properties: Record<string, JsonSchema> = {};
38+
for (const [key, propValue] of Object.entries(value as Record<string, unknown>)) {
39+
properties[key] = generateJsonSchemaFromData(propValue);
40+
}
41+
const keys = Object.keys(properties);
42+
return {
43+
type: 'object',
44+
properties,
45+
...(keys.length > 0 && { required: keys }),
46+
};
47+
}
48+
49+
return { type: 'string' };
50+
}

packages/@n8n/workflow-sdk/src/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,15 @@ export {
179179
type SerializerPlugin,
180180
} from './workflow-builder/plugins';
181181

182+
// Pin data utilities
183+
export {
184+
needsPinData,
185+
discoverOutputSchemaForNode,
186+
inferSchemasFromRunData,
187+
normalizePinData,
188+
type IsTriggerNodeFn,
189+
} from './pin-data-utils';
190+
182191
// Node type constants
183192
export {
184193
NODE_TYPES,

0 commit comments

Comments
 (0)