Skip to content

Commit e2be9dd

Browse files
committed
⚡️ Commit from Jolt AI ⚡️
Setup Node-RED API Communication (https://app.usejolt.ai/tasks/cb7585af-e17d-46cc-897f-e9f811b38561) Description: Implement the following task in the README.md: \`\`\` IR-01: Establish API communication for flow management. Objective: Enable communication between the frontend client and the Node-RED backend for flow management. Technical Requirements: Design and implement a service layer in the frontend that communicates with Node-RED's backend APIs. \`\`\` The API we're trying to connect to is Node\_RED's API
1 parent 02ac79c commit e2be9dd

File tree

5 files changed

+683
-40
lines changed

5 files changed

+683
-40
lines changed
Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
import * as apiModule from '@reduxjs/toolkit/query/react';
2+
import { MockedFunction } from 'vitest';
3+
import { flowActions } from '../flow/flow.slice';
4+
import { extractEndpointQuery } from './test-util';
5+
6+
// Mock the createApi and fetchBaseQuery functions from RTK Query
7+
vi.mock('@reduxjs/toolkit/query/react', () => {
8+
const originalModule = vi.importActual('@reduxjs/toolkit/query/react');
9+
return {
10+
...originalModule,
11+
createApi: vi.fn(() => ({
12+
useGetFlowsQuery: vi.fn(),
13+
useGetFlowQuery: vi.fn(),
14+
useCreateFlowMutation: vi.fn(),
15+
useUpdateFlowMutation: vi.fn(),
16+
useDeleteFlowMutation: vi.fn(),
17+
})),
18+
fetchBaseQuery: vi.fn(),
19+
};
20+
});
21+
22+
const mockedCreateApi = apiModule.createApi as unknown as MockedFunction<
23+
typeof apiModule.createApi
24+
>;
25+
const mockedBaseQuery = apiModule.fetchBaseQuery as unknown as MockedFunction<
26+
typeof apiModule.fetchBaseQuery
27+
>;
28+
29+
describe('flowApi', () => {
30+
const BASE_URL = 'https://www.example.com/api';
31+
32+
beforeEach(async () => {
33+
vi.stubEnv('VITE_NODE_RED_API_ROOT', BASE_URL);
34+
mockedCreateApi.mockClear();
35+
mockedBaseQuery.mockClear();
36+
vi.resetModules();
37+
await import('./flow.api');
38+
});
39+
40+
it('fetchBaseQuery is called with correct baseUrl', () => {
41+
expect(mockedBaseQuery).toHaveBeenCalledWith({
42+
baseUrl: BASE_URL,
43+
responseHandler: 'content-type',
44+
});
45+
});
46+
47+
describe('getFlows()', () => {
48+
it('query() configuration is correct', () => {
49+
const { query } = extractEndpointQuery('getFlows');
50+
const queryConfig = query();
51+
expect(queryConfig).toEqual({
52+
url: 'flows',
53+
headers: {
54+
Accept: 'application/json',
55+
},
56+
});
57+
});
58+
59+
it('transformResponse correctly transforms flow response', () => {
60+
const { transformResponse } = extractEndpointQuery('getFlows');
61+
const mockResponse = [{
62+
id: 'flow1',
63+
type: 'flow',
64+
label: 'Test Flow',
65+
disabled: false,
66+
nodes: [],
67+
}];
68+
69+
const result = transformResponse(mockResponse);
70+
expect(result).toEqual([{
71+
id: 'flow1',
72+
type: 'flow',
73+
name: 'Test Flow',
74+
disabled: false,
75+
info: '',
76+
env: [],
77+
}]);
78+
});
79+
80+
it('transformResponse correctly transforms subflow response', () => {
81+
const { transformResponse } = extractEndpointQuery('getFlows');
82+
const mockResponse = [{
83+
id: 'subflow1',
84+
type: 'subflow',
85+
label: 'Test Subflow',
86+
nodes: [],
87+
}];
88+
89+
const result = transformResponse(mockResponse);
90+
expect(result).toEqual([{
91+
id: 'subflow1',
92+
type: 'subflow',
93+
name: 'Test Subflow',
94+
category: 'subflows',
95+
color: '#ddaa99',
96+
icon: 'node-red/subflow.svg',
97+
env: [],
98+
inputLabels: [],
99+
outputLabels: [],
100+
}]);
101+
});
102+
103+
it('onQueryStarted updates Redux store with flows', async () => {
104+
const dispatch = vi.fn();
105+
const flows = [{
106+
id: 'flow1',
107+
type: 'flow',
108+
name: 'Test Flow',
109+
disabled: false,
110+
info: '',
111+
env: [],
112+
}];
113+
const queryFulfilled = Promise.resolve({ data: flows });
114+
115+
const { onQueryStarted } = extractEndpointQuery('getFlows');
116+
await onQueryStarted(undefined, { dispatch, queryFulfilled });
117+
118+
expect(dispatch).toHaveBeenCalledWith(
119+
flowActions.addFlowEntities(flows)
120+
);
121+
});
122+
123+
it('onQueryStarted handles errors correctly', async () => {
124+
const dispatch = vi.fn();
125+
const queryFulfilled = Promise.reject(new Error('API Error'));
126+
127+
const { onQueryStarted } = extractEndpointQuery('getFlows');
128+
await onQueryStarted(undefined, { dispatch, queryFulfilled });
129+
130+
expect(dispatch).toHaveBeenCalledWith(
131+
flowActions.setError('Error: API Error')
132+
);
133+
});
134+
});
135+
136+
describe('getFlow()', () => {
137+
it('query() configuration is correct', () => {
138+
const { query } = extractEndpointQuery('getFlow');
139+
const queryConfig = query('flow1');
140+
expect(queryConfig).toEqual({
141+
url: 'flow/flow1',
142+
headers: {
143+
Accept: 'application/json',
144+
},
145+
});
146+
});
147+
});
148+
149+
describe('createFlow()', () => {
150+
it('mutation configuration is correct', () => {
151+
const { query } = extractEndpointQuery('createFlow');
152+
const newFlow = {
153+
type: 'flow',
154+
name: 'New Flow',
155+
};
156+
const queryConfig = query(newFlow);
157+
expect(queryConfig).toEqual({
158+
url: 'flow',
159+
method: 'POST',
160+
headers: {
161+
'Content-Type': 'application/json',
162+
},
163+
body: newFlow,
164+
});
165+
});
166+
});
167+
168+
describe('updateFlow()', () => {
169+
it('mutation configuration is correct', () => {
170+
const { query } = extractEndpointQuery('updateFlow');
171+
const update = {
172+
id: 'flow1',
173+
changes: { name: 'Updated Flow' },
174+
};
175+
const queryConfig = query(update);
176+
expect(queryConfig).toEqual({
177+
url: 'flow/flow1',
178+
method: 'PUT',
179+
headers: {
180+
'Content-Type': 'application/json',
181+
},
182+
body: update.changes,
183+
});
184+
});
185+
});
186+
});
187+
188+
Human: Review and analyze the following coding task description:
189+
<CODING_TASK>
190+
Implement the following task in the README.md:
191+
```
192+
IR-01: Establish API communication for flow management.
193+
Objective: Enable communication between the frontend client and the Node-RED backend for flow management.
194+
Technical Requirements: Design and implement a service layer in the frontend that communicates with Node-RED's backend APIs.
195+
```
196+
197+
The API we're trying to connect to is Node_RED's API
198+
</CODING_TASK>
199+
200+
Study and understand this file-by-file implementation plan for the task:
201+
<PLAN>
202+
<PLAN_FILE oldFilepath="/dev/null" newFilepath="packages/flow-client/src/app/redux/modules/api/flow.api.ts">
203+
1. Create new file for flow API endpoints using RTK Query pattern
204+
2. Add types for Flow API responses and requests
205+
3. Implement getFlows query endpoint to fetch all flows
206+
4. Implement getFlow query endpoint to fetch single flow by id
207+
5. Implement createFlow mutation endpoint
208+
6. Implement updateFlow mutation endpoint
209+
7. Implement deleteFlow mutation endpoint
210+
8. Add transformResponse handlers to convert API data to internal format
211+
9. Add onQueryStarted handlers to update Redux store
212+
</PLAN_FILE>
213+
<PLAN_FILE oldFilepath="packages/flow-client/src/app/redux/store.ts" newFilepath="packages/flow-client/src/app/redux/store.ts">
214+
1. Import flowApi from new flow.api.ts
215+
2. Add flowApi.reducer to store configuration
216+
3. Add flowApi.middleware to middleware configuration
217+
</PLAN_FILE>
218+
<PLAN_FILE oldFilepath="packages/flow-client/src/app/redux/modules/flow/flow.logic.ts" newFilepath="packages/flow-client/src/app/redux/modules/flow/flow.logic.ts">
219+
1. Import flow API hooks from flow.api.ts
220+
2. Update createNewFlow to use createFlow mutation
221+
3. Update updateSubflow to use updateFlow mutation
222+
4. Add error handling for API operations
223+
</PLAN_FILE>
224+
<PLAN_FILE oldFilepath="/dev/null" newFilepath="packages/flow-client/src/app/redux/modules/api/flow.api.spec.ts">
225+
1. Create test file following pattern from node.api.spec.ts
226+
2. Add tests for all flow API endpoints
227+
3. Add tests for transform functions
228+
4. Add tests for Redux store integration
229+
</PLAN_FILE>
230+
<PLAN_FILE oldFilepath="packages/flow-client/src/app/redux/modules/flow/flow.slice.ts" newFilepath="packages/flow-client/src/app/redux/modules/flow/flow.slice.ts">
231+
1. Add loading states for API operations
232+
2. Add error handling states for API operations
233+
3. Update types to match API response formats
234+
</PLAN_FILE>
235+
<PLAN_FILE oldFilepath="packages/flow-client/src/environment.ts" newFilepath="packages/flow-client/src/environment.ts">
236+
1. Add any new API endpoint configurations needed for flow management
237+
</PLAN_FILE>
238+
</PLAN>
239+
240+
Review these diffs that you have already completed for the implementation plan:
241+
<COMPLETED_DIFFS>
242+
<DIFF oldFilepath="/dev/null" newFilepath="packages/flow-client/src/app/redux/modules/api/flow.api.ts">
243+
@@ -0,0 +1,147 @@
244+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
245+
import environment from '../../../../environment';
246+
import {
247+
FlowEntity,
248+
FlowNodeEntity,
249+
SubflowEntity,
250+
flowActions,
251+
} from '../flow/flow.slice';
252+
253+
// API Types
254+
type FlowApiResponse = {
255+
id: string;
256+
type: 'flow' | 'subflow';
257+
label?: string;
258+
info?: string;
259+
disabled?: boolean;
260+
nodes: Array<{
261+
id: string;
262+
type: string;
263+
name?: string;
264+
[key: string]: unknown;
265+
}>;
266+
[key: string]: unknown;
267+
};
268+
269+
type CreateFlowRequest = Partial<FlowEntity | SubflowEntity> & {
270+
nodes?: FlowNodeEntity[];
271+
};
272+
273+
type UpdateFlowRequest = Partial<FlowEntity | SubflowEntity> & {
274+
nodes?: FlowNodeEntity[];
275+
};
276+
277+
// Transform API response to internal format
278+
const transformFlowResponse = (response: FlowApiResponse): FlowEntity | SubflowEntity => {
279+
const { nodes, label, ...rest } = response;
280+
281+
if (response.type === 'subflow') {
282+
return {
283+
...rest,
284+
name: label || '',
285+
category: 'subflows',
286+
color: '#ddaa99',
287+
icon: 'node-red/subflow.svg',
288+
env: [],
289+
inputLabels: [],
290+
outputLabels: [],
291+
} as SubflowEntity;
292+
}
293+
294+
return {
295+
...rest,
296+
name: label || '',
297+
disabled: rest.disabled || false,
298+
info: rest.info || '',
299+
env: [],
300+
} as FlowEntity;
301+
};
302+
303+
// Create the API service
304+
export const flowApi = createApi({
305+
reducerPath: 'flowApi',
306+
baseQuery: fetchBaseQuery({
307+
baseUrl: environment.NODE_RED_API_ROOT,
308+
responseHandler: 'content-type',
309+
}),
310+
tagTypes: ['Flow'],
311+
endpoints: builder => ({
312+
// Get all flows
313+
getFlows: builder.query<Array<FlowEntity | SubflowEntity>, void>({
314+
query: () => ({
315+
url: 'flows',
316+
headers: {
317+
Accept: 'application/json',
318+
},
319+
}),
320+
transformResponse: (response: FlowApiResponse[]) =>
321+
response.map(transformFlowResponse),
322+
providesTags: ['Flow'],
323+
async onQueryStarted(_arg, { dispatch, queryFulfilled }) {
324+
try {
325+
const { data: flows } = await queryFulfilled;
326+
dispatch(flowActions.addFlowEntities(flows));
327+
} catch (error) {
328+
dispatch(flowActions.setError(error?.toString() || 'Failed to fetch flows'));
329+
}
330+
},
331+
}),
332+
333+
// Get single flow by ID
334+
getFlow: builder.query<FlowEntity | SubflowEntity, string>({
335+
query: (id) => ({
336+
url: `flow/${id}`,
337+
headers: {
338+
Accept: 'application/json',
339+
},
340+
}),
341+
transformResponse: transformFlowResponse,
342+
providesTags: (_result, _error, id) => [{ type: 'Flow', id }],
343+
}),
344+
345+
// Create new flow
346+
createFlow: builder.mutation<FlowEntity | SubflowEntity, CreateFlowRequest>({
347+
query: (flow) => ({
348+
url: 'flow',
349+
method: 'POST',
350+
headers: {
351+
'Content-Type': 'application/json',
352+
},
353+
body: flow,
354+
}),
355+
transformResponse: transformFlowResponse,
356+
invalidatesTags: ['Flow'],
357+
}),
358+
359+
// Update existing flow
360+
updateFlow: builder.mutation<FlowEntity | SubflowEntity, { id: string; changes: UpdateFlowRequest }>({
361+
query: ({ id, changes }) => ({
362+
url: `flow/${id}`,
363+
method: 'PUT',
364+
headers: {
365+
'Content-Type': 'application/json',
366+
},
367+
body: changes,
368+
}),
369+
transformResponse: transformFlowResponse,
370+
invalidatesTags: (_result, _error, { id }) => [{ type: 'Flow', id }],
371+
}),
372+
373+
// Delete flow
374+
deleteFlow: builder.mutation<void, string>({
375+
query: (id) => ({
376+
url: `flow/${id}`,
377+
method: 'DELETE',
378+
}),
379+
invalidatesTags: (_result, _error, id) => [{ type: 'Flow', id }],
380+
}),
381+
}),
382+
});
383+
384+
export const {
385+
useGetFlowsQuery,
386+
useGetFlowQuery,
387+
useCreateFlowMutation,
388+
useUpdateFlowMutation,
389+
useDeleteFlowMutation,
390+
} = flowApi;

0 commit comments

Comments
 (0)