Skip to content

Commit 6ec0ba5

Browse files
moosebayclaude
authored andcommitted
feat: add GraphQL delta support with assertions and response history
This commit implements comprehensive GraphQL delta functionality, matching the HTTP delta system, and fixes critical bugs in delta field updates. - Add `rgraphql_crud_delta.go` - GraphQL delta CRUD operations - Add `rgraphql_crud_header_delta.go` - GraphQL header delta operations - Add `rgraphql_delta_converter.go` - Delta field conversions - Add `rgraphql_crud_version.go` - Version collection and sync endpoints - Add delta schema `09_graphql_delta.sql` with proper foreign keys - Add `rgraphql_crud_assert.go` - GraphQL assertion CRUD - Add `rgraphql_exec_assert.go` - Assertion execution during requests - Add `rgraphql_exec_assert_test.go` - Assertion execution tests - Add `rgraphql_crud_response_assert.go` - Response assertion tracking - Add frontend assertion UI components - Add GraphQL response tracking and history UI - Add response header tracking - Add version sync for real-time updates - Fix GraphQL Writer.Update() to check IsDelta flag - Add UpdateGraphQLDelta SQL query for delta field updates - Prevents deltas from incorrectly overwriting parent base fields - Follows HTTP delta pattern exactly - Add `history.tsx` - GraphQL response history view - Add `request/assert.tsx` - Request assertion editor - Add `request/url.tsx` - URL editor component - Add `response/assert.tsx` - Response assertion viewer - Add delta route handler - Update flow nodes to support GraphQL deltas - Add graphql_assert table with delta support - Add graphql_response_assert table - Add UpdateGraphQLDelta query - Add migration 01KHEX5H_add_graphql_delta.go - Add .ralph/ and .ralphrc to .gitignore Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6699d36 commit 6ec0ba5

69 files changed

Lines changed: 8137 additions & 417 deletions

Some content is hidden

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

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ storybook-static
2828
tmp/
2929
tsconfig.tsbuildinfo
3030
tsp-output
31+
.bench/
32+
.ralph/
33+
.ralphrc
3134

3235
# Coverage files
3336
*.out

packages/client/src/app/router/route-tree.gen.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Route as dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDot
2323
import { Route as dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotCredentialRoutesCredentialCredentialIdCanIndexRouteImport } from './../../pages/credential/routes/credential/$credentialIdCan/index'
2424
import { Route as dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotFlowRoutesFlowFlowIdCanHistoryRouteImport } from './../../pages/flow/routes/flow/$flowIdCan/history'
2525
import { Route as dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanDeltaDotdeltaHttpIdCanRouteImport } from './../../pages/http/routes/http/$httpIdCan/delta.$deltaHttpIdCan'
26+
import { Route as dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRouteImport } from './../../pages/graphql/routes/graphql/$graphqlIdCan/delta.$deltaGraphqlIdCan'
2627

2728
const dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesIndexRoute =
2829
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesIndexRouteImport.update({
@@ -144,6 +145,15 @@ const dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoute
144145
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanRouteRoute,
145146
} as any,
146147
)
148+
const dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRoute =
149+
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRouteImport.update(
150+
{
151+
id: '/delta/$deltaGraphqlIdCan',
152+
path: '/delta/$deltaGraphqlIdCan',
153+
getParentRoute: () =>
154+
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanRouteRoute,
155+
} as any,
156+
)
147157

148158
export interface FileRoutesByFullPath {
149159
'/': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesIndexRoute
@@ -159,6 +169,7 @@ export interface FileRoutesByFullPath {
159169
'/workspace/$workspaceIdCan/flow/$flowIdCan/': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotFlowRoutesFlowFlowIdCanIndexRoute
160170
'/workspace/$workspaceIdCan/graphql/$graphqlIdCan/': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanIndexRoute
161171
'/workspace/$workspaceIdCan/http/$httpIdCan/': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanIndexRoute
172+
'/workspace/$workspaceIdCan/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRoute
162173
'/workspace/$workspaceIdCan/http/$httpIdCan/delta/$deltaHttpIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanDeltaDotdeltaHttpIdCanRoute
163174
}
164175
export interface FileRoutesByTo {
@@ -171,6 +182,7 @@ export interface FileRoutesByTo {
171182
'/workspace/$workspaceIdCan/flow/$flowIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotFlowRoutesFlowFlowIdCanIndexRoute
172183
'/workspace/$workspaceIdCan/graphql/$graphqlIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanIndexRoute
173184
'/workspace/$workspaceIdCan/http/$httpIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanIndexRoute
185+
'/workspace/$workspaceIdCan/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRoute
174186
'/workspace/$workspaceIdCan/http/$httpIdCan/delta/$deltaHttpIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanDeltaDotdeltaHttpIdCanRoute
175187
}
176188
export interface FileRoutesById {
@@ -188,6 +200,7 @@ export interface FileRoutesById {
188200
'/(dashboard)/(workspace)/workspace/$workspaceIdCan/(flow)/flow/$flowIdCan/': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotFlowRoutesFlowFlowIdCanIndexRoute
189201
'/(dashboard)/(workspace)/workspace/$workspaceIdCan/(graphql)/graphql/$graphqlIdCan/': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanIndexRoute
190202
'/(dashboard)/(workspace)/workspace/$workspaceIdCan/(http)/http/$httpIdCan/': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanIndexRoute
203+
'/(dashboard)/(workspace)/workspace/$workspaceIdCan/(graphql)/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRoute
191204
'/(dashboard)/(workspace)/workspace/$workspaceIdCan/(http)/http/$httpIdCan/delta/$deltaHttpIdCan': typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanDeltaDotdeltaHttpIdCanRoute
192205
}
193206
export interface FileRouteTypes {
@@ -206,6 +219,7 @@ export interface FileRouteTypes {
206219
| '/workspace/$workspaceIdCan/flow/$flowIdCan/'
207220
| '/workspace/$workspaceIdCan/graphql/$graphqlIdCan/'
208221
| '/workspace/$workspaceIdCan/http/$httpIdCan/'
222+
| '/workspace/$workspaceIdCan/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan'
209223
| '/workspace/$workspaceIdCan/http/$httpIdCan/delta/$deltaHttpIdCan'
210224
fileRoutesByTo: FileRoutesByTo
211225
to:
@@ -218,6 +232,7 @@ export interface FileRouteTypes {
218232
| '/workspace/$workspaceIdCan/flow/$flowIdCan'
219233
| '/workspace/$workspaceIdCan/graphql/$graphqlIdCan'
220234
| '/workspace/$workspaceIdCan/http/$httpIdCan'
235+
| '/workspace/$workspaceIdCan/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan'
221236
| '/workspace/$workspaceIdCan/http/$httpIdCan/delta/$deltaHttpIdCan'
222237
id:
223238
| '__root__'
@@ -234,6 +249,7 @@ export interface FileRouteTypes {
234249
| '/(dashboard)/(workspace)/workspace/$workspaceIdCan/(flow)/flow/$flowIdCan/'
235250
| '/(dashboard)/(workspace)/workspace/$workspaceIdCan/(graphql)/graphql/$graphqlIdCan/'
236251
| '/(dashboard)/(workspace)/workspace/$workspaceIdCan/(http)/http/$httpIdCan/'
252+
| '/(dashboard)/(workspace)/workspace/$workspaceIdCan/(graphql)/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan'
237253
| '/(dashboard)/(workspace)/workspace/$workspaceIdCan/(http)/http/$httpIdCan/delta/$deltaHttpIdCan'
238254
fileRoutesById: FileRoutesById
239255
}
@@ -344,6 +360,13 @@ declare module '@tanstack/react-router' {
344360
preLoaderRoute: typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanDeltaDotdeltaHttpIdCanRouteImport
345361
parentRoute: typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotHttpRoutesHttpHttpIdCanRouteRoute
346362
}
363+
'/(dashboard)/(workspace)/workspace/$workspaceIdCan/(graphql)/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan': {
364+
id: '/(dashboard)/(workspace)/workspace/$workspaceIdCan/(graphql)/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan'
365+
path: '/delta/$deltaGraphqlIdCan'
366+
fullPath: '/workspace/$workspaceIdCan/graphql/$graphqlIdCan/delta/$deltaGraphqlIdCan'
367+
preLoaderRoute: typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRouteImport
368+
parentRoute: typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanRouteRoute
369+
}
347370
}
348371
}
349372

@@ -367,12 +390,15 @@ const dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotFlowRoute
367390

368391
interface dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanRouteRouteChildren {
369392
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanIndexRoute: typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanIndexRoute
393+
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRoute: typeof dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRoute
370394
}
371395

372396
const dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanRouteRouteChildren: dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanRouteRouteChildren =
373397
{
374398
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanIndexRoute:
375399
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanIndexRoute,
400+
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRoute:
401+
dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanDeltaDotdeltaGraphqlIdCanRoute,
376402
}
377403

378404
const dashboardDotDotDotDotDotDotDotDotPagesDashboardRoutesDotDotDotDotGraphqlRoutesGraphqlGraphqlIdCanRouteRouteWithChildren =

packages/client/src/pages/flow/nodes/graphql.tsx

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ import { eq, useLiveQuery } from '@tanstack/react-db';
33
import { useRouter } from '@tanstack/react-router';
44
import * as XF from '@xyflow/react';
55
import { Ulid } from 'id128';
6+
import { use } from 'react';
67
import { FiExternalLink } from 'react-icons/fi';
78
import { NodeGraphQLSchema } from '@the-dev-tools/spec/buf/api/flow/v1/flow_pb';
89
import {
910
NodeExecutionCollectionSchema,
1011
NodeGraphQLCollectionSchema,
1112
} from '@the-dev-tools/spec/tanstack-db/v1/api/flow';
12-
import { GraphQLCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/graph_q_l';
13+
import { GraphQLCollectionSchema, GraphQLDeltaCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/graph_q_l';
1314
import { ButtonAsLink } from '@the-dev-tools/ui/button';
1415
import { SendRequestIcon } from '@the-dev-tools/ui/icons';
1516
import { tw } from '@the-dev-tools/ui/tailwind-literal';
16-
import { GraphQLRequestPanel, GraphQLResponsePanel } from '~/pages/graphql/@x/flow';
17+
import { useDeltaState } from '~/features/delta';
18+
import { ReferenceContext } from '~/features/expression';
19+
import { GraphQLRequestPanel, GraphQLResponseInfo, GraphQLResponsePanel } from '~/pages/graphql/@x/flow';
1720
import { useApiCollection } from '~/shared/api';
1821
import { pick } from '~/shared/lib';
1922
import { routes } from '~/shared/routes';
23+
import { FlowContext } from '../context';
2024
import { Handle } from '../handle';
2125
import { NodeSettingsBody, NodeSettingsOutputProps, NodeSettingsProps, SimpleNode } from '../node';
2226

@@ -25,27 +29,25 @@ export const GraphQLNode = ({ id, selected }: XF.NodeProps) => {
2529

2630
const nodeGraphQLCollection = useApiCollection(NodeGraphQLCollectionSchema);
2731

28-
const { graphqlId } =
32+
const { deltaGraphqlId, graphqlId } =
2933
useLiveQuery(
3034
(_) =>
3135
_.from({ item: nodeGraphQLCollection })
3236
.where((_) => eq(_.item.nodeId, nodeId))
33-
.select((_) => pick(_.item, 'graphqlId'))
37+
.select((_) => pick(_.item, 'graphqlId', 'deltaGraphqlId'))
3438
.findOne(),
3539
[nodeGraphQLCollection, nodeId],
3640
).data ?? create(NodeGraphQLSchema);
3741

38-
const graphqlCollection = useApiCollection(GraphQLCollectionSchema);
42+
const deltaOptions = {
43+
deltaId: deltaGraphqlId,
44+
deltaSchema: GraphQLDeltaCollectionSchema,
45+
isDelta: deltaGraphqlId !== undefined,
46+
originId: graphqlId,
47+
originSchema: GraphQLCollectionSchema,
48+
};
3949

40-
const { name, url } =
41-
useLiveQuery(
42-
(_) =>
43-
_.from({ item: graphqlCollection })
44-
.where((_) => eq(_.item.graphqlId, graphqlId))
45-
.select((_) => pick(_.item, 'name', 'url'))
46-
.findOne(),
47-
[graphqlCollection, graphqlId],
48-
).data ?? {};
50+
const [name] = useDeltaState({ ...deltaOptions, valueKey: 'name' });
4951

5052
return (
5153
<SimpleNode
@@ -63,7 +65,7 @@ export const GraphQLNode = ({ id, selected }: XF.NodeProps) => {
6365
>
6466
<div className={tw`min-w-0 flex-1`}>
6567
<div className={tw`truncate text-xs font-medium tracking-tight text-teal-600`}>GQL</div>
66-
<div className={tw`truncate text-xs tracking-tight text-on-neutral-low`}>{name ?? url}</div>
68+
<div className={tw`truncate text-xs tracking-tight text-on-neutral-low`}>{name}</div>
6769
</div>
6870
</SimpleNode>
6971
);
@@ -72,16 +74,19 @@ export const GraphQLNode = ({ id, selected }: XF.NodeProps) => {
7274
export const GraphQLSettings = ({ nodeId }: NodeSettingsProps) => {
7375
const router = useRouter();
7476

77+
const { isReadOnly = false } = use(FlowContext);
78+
79+
const { workspaceId } = routes.dashboard.workspace.route.useLoaderData();
7580
const { workspaceIdCan } = routes.dashboard.workspace.route.useParams();
7681

7782
const nodeGraphQLCollection = useApiCollection(NodeGraphQLCollectionSchema);
7883

79-
const { graphqlId } =
84+
const { deltaGraphqlId, graphqlId } =
8085
useLiveQuery(
8186
(_) =>
8287
_.from({ item: nodeGraphQLCollection })
8388
.where((_) => eq(_.item.nodeId, nodeId))
84-
.select((_) => pick(_.item, 'graphqlId'))
89+
.select((_) => pick(_.item, 'graphqlId', 'deltaGraphqlId'))
8590
.findOne(),
8691
[nodeGraphQLCollection, nodeId],
8792
).data ?? create(NodeGraphQLSchema);
@@ -93,20 +98,39 @@ export const GraphQLSettings = ({ nodeId }: NodeSettingsProps) => {
9398
settingsHeader={
9499
<ButtonAsLink
95100
className={tw`-my-4 shrink-0 px-2`}
96-
params={{
97-
graphqlIdCan: Ulid.construct(graphqlId).toCanonical(),
98-
workspaceIdCan,
99-
}}
100-
to={router.routesById[routes.dashboard.workspace.graphql.route.id].fullPath}
101101
variant='ghost'
102+
{...(deltaGraphqlId
103+
? {
104+
params: {
105+
deltaGraphqlIdCan: Ulid.construct(deltaGraphqlId).toCanonical(),
106+
graphqlIdCan: Ulid.construct(graphqlId).toCanonical(),
107+
workspaceIdCan,
108+
},
109+
to: router.routesById[routes.dashboard.workspace.graphql.delta.id].fullPath,
110+
}
111+
: {
112+
params: {
113+
graphqlIdCan: Ulid.construct(graphqlId).toCanonical(),
114+
workspaceIdCan,
115+
},
116+
to: router.routesById[routes.dashboard.workspace.graphql.route.id].fullPath,
117+
})}
102118
>
103119
<FiExternalLink className={tw`size-4 text-on-neutral-low`} />
104120
Open GraphQL
105121
</ButtonAsLink>
106122
}
107123
title='GraphQL request'
108124
>
109-
<GraphQLRequestPanel graphqlId={graphqlId} />
125+
<ReferenceContext
126+
value={{ flowNodeId: nodeId, graphqlId, workspaceId, ...(deltaGraphqlId && { deltaGraphqlId }) }}
127+
>
128+
<GraphQLRequestPanel
129+
deltaGraphqlId={deltaGraphqlId}
130+
graphqlId={graphqlId}
131+
isReadOnly={isReadOnly}
132+
/>
133+
</ReferenceContext>
110134
</NodeSettingsBody>
111135
);
112136
};
@@ -128,7 +152,8 @@ const Output = ({ nodeExecutionId }: NodeSettingsOutputProps) => {
128152

129153
return (
130154
<div className={tw`flex h-full flex-col`}>
131-
<GraphQLResponsePanel graphqlResponseId={graphqlResponseId} />
155+
<GraphQLResponseInfo className={tw`-m-2`} graphqlResponseId={graphqlResponseId} />
156+
<GraphQLResponsePanel className={tw`flex-1`} graphqlResponseId={graphqlResponseId} />
132157
</div>
133158
);
134159
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export { GraphQLRequestPanel } from '../request/panel';
2-
export { GraphQLResponsePanel } from '../response';
2+
export { GraphQLResponseInfo, GraphQLResponsePanel } from '../response';

0 commit comments

Comments
 (0)