Skip to content

Commit c0b5054

Browse files
authored
Merge pull request #56 from constructive-io/feat/fbp-graph-editor
feat: replace React Flow with @fbp/graph-editor + unified node palette
2 parents b73f70f + 0ccf757 commit c0b5054

42 files changed

Lines changed: 8315 additions & 271 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/spec/fbp-data-model.md

Lines changed: 458 additions & 0 deletions
Large diffs are not rendered by default.

docs/spec/fbp-deep-plan.md

Lines changed: 646 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "@fbp/evaluator",
3+
"version": "1.1.0",
4+
"private": true,
5+
"main": "src/index.ts",
6+
"types": "src/index.ts",
7+
"exports": {
8+
".": "./src/index.ts"
9+
},
10+
"dependencies": {
11+
"@fbp/types": "workspace:*"
12+
}
13+
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import type { NodeDefinitionWithImpl } from '../types';
2+
3+
/**
4+
* Core node definitions with implementations.
5+
* These nodes handle JSON manipulation, flow control, and string operations.
6+
*/
7+
8+
/**
9+
* core/json/select - Extract a value from JSON by path
10+
*
11+
* Props:
12+
* - path: string (dot-path like "a.b.c" or "data.user.email")
13+
*
14+
* Inputs:
15+
* - obj: json (the object to extract from)
16+
*
17+
* Outputs:
18+
* - value: any (the extracted value)
19+
*/
20+
export const jsonSelectDef: NodeDefinitionWithImpl = {
21+
context: 'js',
22+
name: 'select',
23+
category: 'json',
24+
icon: 'circle',
25+
inputs: [
26+
{ name: 'obj', type: 'json' }
27+
],
28+
outputs: [
29+
{ name: 'value', type: 'any' }
30+
],
31+
props: [
32+
{ name: 'path', type: 'string', default: '' }
33+
],
34+
description: 'Extract a value from JSON by dot-path',
35+
impl: (inputs, props) => {
36+
const { obj } = inputs;
37+
const { path } = props;
38+
39+
if (!path || obj === undefined || obj === null) {
40+
return { value: undefined };
41+
}
42+
43+
const parts = path.split('.');
44+
let current = obj;
45+
46+
for (const part of parts) {
47+
if (current === undefined || current === null) {
48+
return { value: undefined };
49+
}
50+
// Handle array index access like "items.0.name"
51+
const index = parseInt(part, 10);
52+
if (!isNaN(index) && Array.isArray(current)) {
53+
current = current[index];
54+
} else if (typeof current === 'object') {
55+
current = current[part];
56+
} else {
57+
return { value: undefined };
58+
}
59+
}
60+
61+
return { value: current };
62+
}
63+
};
64+
65+
/**
66+
* core/json/object - Build a JSON object from named inputs
67+
*
68+
* This node has dynamic inputs - any input wired to it becomes a key in the output object.
69+
* The implementation receives all inputs as a Record<string, any>.
70+
*
71+
* Inputs:
72+
* - (dynamic) arbitrary named inputs
73+
*
74+
* Outputs:
75+
* - value: json (the constructed object)
76+
*/
77+
export const jsonObjectDef: NodeDefinitionWithImpl = {
78+
context: 'js',
79+
name: 'object',
80+
category: 'json',
81+
icon: 'braces',
82+
inputs: [], // Dynamic inputs - any input name is valid
83+
outputs: [
84+
{ name: 'value', type: 'json' }
85+
],
86+
props: [],
87+
description: 'Build a JSON object from named inputs',
88+
impl: (inputs) => {
89+
// All inputs become keys in the output object
90+
return { value: { ...inputs } };
91+
}
92+
};
93+
94+
/**
95+
* core/flow/guard - Stop the flow if a condition fails
96+
*
97+
* Inputs:
98+
* - ok: boolean (the condition to check)
99+
* - error?: json (optional error info)
100+
*
101+
* Outputs:
102+
* - pass: signal (emitted if ok is true)
103+
* - fail: signal (emitted if ok is false)
104+
* - error: json (the error if failed)
105+
*/
106+
export const flowGuardDef: NodeDefinitionWithImpl = {
107+
context: 'js',
108+
name: 'guard',
109+
category: 'flow',
110+
icon: 'zap',
111+
inputs: [
112+
{ name: 'ok', type: 'boolean' },
113+
{ name: 'error', type: 'json' }
114+
],
115+
outputs: [
116+
{ name: 'pass', type: 'signal' },
117+
{ name: 'fail', type: 'signal' },
118+
{ name: 'error', type: 'json' }
119+
],
120+
props: [],
121+
description: 'Stop the flow if a condition fails',
122+
impl: (inputs) => {
123+
const { ok, error } = inputs;
124+
125+
if (ok) {
126+
return {
127+
pass: true,
128+
fail: false,
129+
error: null
130+
};
131+
} else {
132+
return {
133+
pass: false,
134+
fail: true,
135+
error: error || { message: 'Guard condition failed' }
136+
};
137+
}
138+
}
139+
};
140+
141+
/**
142+
* core/string/template - Build a string from a template with placeholders
143+
*
144+
* Props:
145+
* - template: string (template with {{placeholder}} syntax)
146+
*
147+
* Inputs:
148+
* - (dynamic) values to substitute into the template
149+
*
150+
* Outputs:
151+
* - value: string (the resulting string)
152+
*/
153+
export const stringTemplateDef: NodeDefinitionWithImpl = {
154+
context: 'js',
155+
name: 'template',
156+
category: 'string',
157+
icon: 'quote',
158+
inputs: [], // Dynamic inputs based on template placeholders
159+
outputs: [
160+
{ name: 'value', type: 'string' }
161+
],
162+
props: [
163+
{ name: 'template', type: 'string', default: '' }
164+
],
165+
description: 'Build a string from a template with {{placeholder}} syntax',
166+
impl: (inputs, props) => {
167+
const { template } = props;
168+
169+
if (!template) {
170+
return { value: '' };
171+
}
172+
173+
// Replace {{placeholder}} with input values
174+
const result = template.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => {
175+
const value = inputs[key];
176+
if (value === undefined || value === null) {
177+
return match; // Keep placeholder if no value
178+
}
179+
return String(value);
180+
});
181+
182+
return { value: result };
183+
}
184+
};
185+
186+
/**
187+
* core/string/concat - Concatenate strings with an optional separator
188+
*
189+
* Props:
190+
* - prefix: string (optional prefix)
191+
* - suffix: string (optional suffix)
192+
*
193+
* Inputs:
194+
* - value: string (the main value)
195+
*
196+
* Outputs:
197+
* - value: string (the resulting string)
198+
*/
199+
export const stringConcatDef: NodeDefinitionWithImpl = {
200+
context: 'js',
201+
name: 'concat',
202+
category: 'string',
203+
icon: 'link',
204+
inputs: [
205+
{ name: 'value', type: 'string' }
206+
],
207+
outputs: [
208+
{ name: 'value', type: 'string' }
209+
],
210+
props: [
211+
{ name: 'prefix', type: 'string', default: '' },
212+
{ name: 'suffix', type: 'string', default: '' }
213+
],
214+
description: 'Concatenate strings with optional prefix/suffix',
215+
impl: (inputs, props) => {
216+
const { value = '' } = inputs;
217+
const { prefix = '', suffix = '' } = props;
218+
219+
return { value: `${prefix}${value}${suffix}` };
220+
}
221+
};
222+
223+
export const coreDefinitions: NodeDefinitionWithImpl[] = [
224+
jsonSelectDef,
225+
jsonObjectDef,
226+
flowGuardDef,
227+
stringTemplateDef,
228+
stringConcatDef
229+
];
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { NodeDefinitionWithImpl } from '../types';
2+
3+
/**
4+
* Math node definitions with implementations for testing.
5+
*/
6+
7+
export const constNumberDef: NodeDefinitionWithImpl = {
8+
context: 'js',
9+
name: 'number',
10+
category: 'const',
11+
icon: 'hash',
12+
outputs: [{ name: 'value', type: 'number' }],
13+
props: [{ name: 'value', type: 'number', default: 0 }],
14+
description: 'Outputs a constant number value',
15+
impl: (_inputs, props) => ({
16+
value: props.value ?? 0
17+
})
18+
};
19+
20+
export const addDef: NodeDefinitionWithImpl = {
21+
context: 'js',
22+
name: 'add',
23+
category: 'math',
24+
icon: 'plus',
25+
inputs: [
26+
{ name: 'a', type: 'number' },
27+
{ name: 'b', type: 'number' }
28+
],
29+
outputs: [{ name: 'sum', type: 'number' }],
30+
description: 'Adds two numbers',
31+
impl: (inputs) => ({
32+
sum: (inputs.a ?? 0) + (inputs.b ?? 0)
33+
})
34+
};
35+
36+
export const multiplyDef: NodeDefinitionWithImpl = {
37+
context: 'js',
38+
name: 'multiply',
39+
category: 'math',
40+
icon: 'x',
41+
inputs: [
42+
{ name: 'a', type: 'number' },
43+
{ name: 'b', type: 'number' }
44+
],
45+
outputs: [{ name: 'product', type: 'number' }],
46+
description: 'Multiplies two numbers',
47+
impl: (inputs) => ({
48+
product: (inputs.a ?? 0) * (inputs.b ?? 0)
49+
})
50+
};
51+
52+
export const mathDefinitions: NodeDefinitionWithImpl[] = [
53+
constNumberDef,
54+
addDef,
55+
multiplyDef
56+
];

0 commit comments

Comments
 (0)