Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/cli/cmd/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ var yamlflowRunCmd = &cobra.Command{
&services.NodeAI,
&services.NodeAiProvider,
&services.NodeMemory,
&services.NodeGraphQL,
&services.GraphQL,
&services.GraphQLHeader,
&services.Workspace,
&services.Variable,
&services.FlowVariable,
Expand Down
11 changes: 11 additions & 0 deletions apps/cli/internal/common/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/the-dev-tools/dev-tools/packages/server/pkg/service/scredential"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/service/senv"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/service/sflow"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/service/sgraphql"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/service/shttp"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/service/sworkspace"
)
Expand Down Expand Up @@ -40,6 +41,11 @@ type Services struct {
NodeAI sflow.NodeAIService
NodeAiProvider sflow.NodeAiProviderService
NodeMemory sflow.NodeMemoryService
NodeGraphQL sflow.NodeGraphQLService

// GraphQL
GraphQL sgraphql.GraphQLService
GraphQLHeader sgraphql.GraphQLHeaderService

// Credentials
Credential scredential.CredentialService
Expand Down Expand Up @@ -87,6 +93,11 @@ func CreateServices(ctx context.Context, db *sql.DB, logger *slog.Logger) (*Serv
NodeAI: sflow.NewNodeAIService(queries),
NodeAiProvider: sflow.NewNodeAiProviderService(queries),
NodeMemory: sflow.NewNodeMemoryService(queries),
NodeGraphQL: sflow.NewNodeGraphQLService(queries),

// GraphQL
GraphQL: sgraphql.New(queries, logger),
GraphQLHeader: sgraphql.NewGraphQLHeaderService(queries),

// Credentials
Credential: scredential.NewCredentialService(queries),
Expand Down
13 changes: 13 additions & 0 deletions apps/cli/internal/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/the-dev-tools/dev-tools/apps/cli/internal/reporter"

"github.com/the-dev-tools/dev-tools/packages/server/pkg/flow/node"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/flow/node/ngraphql"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/flow/node/nrequest"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/flow/runner"
"github.com/the-dev-tools/dev-tools/packages/server/pkg/flow/runner/flowlocalrunner"
Expand Down Expand Up @@ -251,6 +252,17 @@ func RunFlow(ctx context.Context, flowPtr *mflow.Flow, services RunnerServices,
}()
defer close(requestRespChan)

// Initialize GraphQL response channel
gqlRespChan := make(chan ngraphql.NodeGraphQLSideResp, requestBufferSize)
go func() {
for resp := range gqlRespChan {
if resp.Done != nil {
close(resp.Done)
}
}
}()
defer close(gqlRespChan)

// Build flow node map using flowbuilder
flowNodeMap, startNodeID, err := services.Builder.BuildNodes(
ctx,
Expand All @@ -259,6 +271,7 @@ func RunFlow(ctx context.Context, flowPtr *mflow.Flow, services RunnerServices,
nodeTimeout,
httpClient,
requestRespChan,
gqlRespChan,
services.JSClient,
)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions apps/cli/internal/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ func newFlowTestFixture(t *testing.T) *flowTestFixture {
nil, // NodeAIService - not needed for CLI tests
nil, // NodeAiProviderService - not needed for CLI tests
nil, // NodeMemoryService - not needed for CLI tests
nil, // NodeGraphQLService - not needed for CLI tests
nil, // GraphQLService - not needed for CLI tests
nil, // GraphQLHeaderService - not needed for CLI tests
&workspaceService,
&varService,
&flowVariableService,
Expand Down
62 changes: 62 additions & 0 deletions packages/client/src/app/router/route-tree.gen.ts

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions packages/client/src/features/file-system/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
FolderSchema,
} from '@the-dev-tools/spec/buf/api/file_system/v1/file_system_pb';
import { FlowSchema, FlowService } from '@the-dev-tools/spec/buf/api/flow/v1/flow_pb';
import { GraphQLSchema as GraphQLItemSchema } from '@the-dev-tools/spec/buf/api/graph_q_l/v1/graph_q_l_pb';
import { HttpDeltaSchema, HttpMethod, HttpSchema, HttpService } from '@the-dev-tools/spec/buf/api/http/v1/http_pb';
import {
CredentialAnthropicCollectionSchema,
Expand All @@ -38,6 +39,7 @@ import {
} from '@the-dev-tools/spec/tanstack-db/v1/api/credential';
import { FileCollectionSchema, FolderCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/file_system';
import { FlowCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/flow';
import { GraphQLCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/graph_q_l';
import { HttpCollectionSchema, HttpDeltaCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/http';
import { Button } from '@the-dev-tools/ui/button';
import { FlowsIcon, FolderOpenedIcon } from '@the-dev-tools/ui/icons';
Expand Down Expand Up @@ -84,6 +86,7 @@ export const FileCreateMenu = ({ parentFolderId, ...props }: FileCreateMenuProps
const { workspaceId } = routes.dashboard.workspace.route.useLoaderData();

const folderCollection = useApiCollection(FolderCollectionSchema);
const graphqlCollection = useApiCollection(GraphQLCollectionSchema);
const httpCollection = useApiCollection(HttpCollectionSchema);
const flowCollection = useApiCollection(FlowCollectionSchema);

Expand Down Expand Up @@ -116,6 +119,22 @@ export const FileCreateMenu = ({ parentFolderId, ...props }: FileCreateMenuProps
HTTP request
</MenuItem>

<MenuItem
onAction={async () => {
const graphqlUlid = Ulid.generate();
graphqlCollection.utils.insert({ graphqlId: graphqlUlid.bytes, name: 'New GraphQL request' });
await insertFile({ fileId: graphqlUlid.bytes, kind: FileKind.GRAPH_Q_L });
if (toNavigate)
await navigate({
from: router.routesById[routes.dashboard.workspace.route.id].fullPath,
params: { graphqlIdCan: graphqlUlid.toCanonical() },
to: router.routesById[routes.dashboard.workspace.graphql.route.id].fullPath,
});
}}
>
GraphQL request
</MenuItem>

<MenuItem
onAction={async () => {
const flowUlid = Ulid.generate();
Expand Down Expand Up @@ -332,6 +351,7 @@ const FileItem = ({ id }: FileItemProps) => {
Match.when(FileKind.HTTP, () => <HttpFile id={id} />),
Match.when(FileKind.HTTP_DELTA, () => <HttpDeltaFile id={id} />),
Match.when(FileKind.FLOW, () => <FlowFile id={id} />),
Match.when(FileKind.GRAPH_Q_L, () => <GraphQLFile id={id} />),
Match.when(FileKind.CREDENTIAL, () => <CredentialFile id={id} />),
Match.orElse(() => null),
);
Expand Down Expand Up @@ -884,6 +904,93 @@ const FlowFile = ({ id }: FileItemProps) => {
return toNavigate ? <TreeItemRouteLink {...props} {...route} /> : <TreeItem {...props} />;
};

const GraphQLFile = ({ id }: FileItemProps) => {
const router = useRouter();
const matchRoute = useMatchRoute();

const fileCollection = useApiCollection(FileCollectionSchema);

const { fileId: graphqlId } = useMemo(() => fileCollection.utils.parseKeyUnsafe(id), [fileCollection.utils, id]);

const graphqlCollection = useApiCollection(GraphQLCollectionSchema);

const { name } =
useLiveQuery(
(_) =>
_.from({ item: graphqlCollection })
.where((_) => eq(_.item.graphqlId, graphqlId))
.select((_) => pick(_.item, 'name'))
.findOne(),
[graphqlCollection, graphqlId],
).data ?? create(GraphQLItemSchema);

const { containerRef, navigate: toNavigate = false, showControls } = useContext(FileTreeContext);

const { escapeRef, escapeRender } = useEscapePortal(containerRef);

const { edit, isEditing, textFieldProps } = useEditableTextState({
onSuccess: (_) => graphqlCollection.utils.update({ graphqlId, name: _ }),
value: name,
});

const { menuProps, menuTriggerProps, onContextMenu } = useContextMenuState();

const route = {
from: router.routesById[routes.dashboard.workspace.route.id].fullPath,
params: { graphqlIdCan: Ulid.construct(graphqlId).toCanonical() },
to: router.routesById[routes.dashboard.workspace.graphql.route.id].fullPath,
} satisfies ToOptions;

const content = (
<>
<span className={tw`rounded bg-pink-100 px-1.5 py-0.5 text-[10px] font-semibold text-pink-700`}>GQL</span>

<Text className={twJoin(tw`flex-1 truncate`, isEditing && tw`opacity-0`)} ref={escapeRef}>
{name}
</Text>

{isEditing &&
escapeRender(
<TextInputField
aria-label='GraphQL request name'
className={tw`w-full`}
inputClassName={tw`-my-1 py-1`}
{...textFieldProps}
/>,
)}

{showControls && (
<MenuTrigger {...menuTriggerProps}>
<Button className={tw`p-0.5`} variant='ghost'>
<FiMoreHorizontal className={tw`size-4 text-on-neutral-low`} />
</Button>

<Menu {...menuProps}>
<MenuItem onAction={() => void edit()}>Rename</MenuItem>

<MenuItem
onAction={() => pipe(fileCollection.utils.parseKeyUnsafe(id), (_) => fileCollection.utils.delete(_))}
variant='danger'
>
Delete
</MenuItem>
</Menu>
</MenuTrigger>
)}
</>
);

const props = {
children: content,
className: toNavigate && matchRoute(route) !== false ? tw`bg-neutral` : '',
id,
onContextMenu,
textValue: name,
} satisfies TreeItemProps<object>;

return toNavigate ? <TreeItemRouteLink {...props} {...route} /> : <TreeItem {...props} />;
};

const CredentialFile = ({ id }: FileItemProps) => {
const router = useRouter();
const matchRoute = useMatchRoute();
Expand Down
74 changes: 73 additions & 1 deletion packages/client/src/pages/flow/add-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as RAC from 'react-aria-components';
import { FiArrowLeft, FiBriefcase, FiChevronRight, FiTerminal, FiX } from 'react-icons/fi';
import { TbRobotFace } from 'react-icons/tb';
import { FileKind } from '@the-dev-tools/spec/buf/api/file_system/v1/file_system_pb';
import { HandleKind, NodeHttpInsertSchema, NodeKind } from '@the-dev-tools/spec/buf/api/flow/v1/flow_pb';
import { HandleKind, NodeGraphQLInsertSchema, NodeHttpInsertSchema, NodeKind } from '@the-dev-tools/spec/buf/api/flow/v1/flow_pb';
import { HttpMethod } from '@the-dev-tools/spec/buf/api/http/v1/http_pb';
import { FileCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/file_system';
import {
Expand All @@ -16,9 +16,11 @@ import {
NodeConditionCollectionSchema,
NodeForCollectionSchema,
NodeForEachCollectionSchema,
NodeGraphQLCollectionSchema,
NodeHttpCollectionSchema,
NodeJsCollectionSchema,
} from '@the-dev-tools/spec/tanstack-db/v1/api/flow';
import { GraphQLCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/graph_q_l';
import { HttpCollectionSchema } from '@the-dev-tools/spec/tanstack-db/v1/api/http';
import { Button } from '@the-dev-tools/ui/button';
import { FlowsIcon, ForIcon, IfIcon, SendRequestIcon } from '@the-dev-tools/ui/icons';
Expand Down Expand Up @@ -247,6 +249,13 @@ const AddCoreNodeSidebar = (props: AddNodeSidebarProps) => {
onAction={() => void setSidebar?.((_) => <AddHttpRequestNodeSidebar {...props} previous={_} />)}
title='HTTP Request'
/>

<SidebarItem
description='Makes a GraphQL request and returns the response data'
icon={<SendRequestIcon />}
onAction={() => void setSidebar?.((_) => <AddGraphQLRequestNodeSidebar {...props} previous={_} />)}
title='GraphQL Request'
/>
</RAC.ListBox>
</>
);
Expand Down Expand Up @@ -318,6 +327,69 @@ const AddHttpRequestNodeSidebar = ({ handleKind, position, previous, sourceId, t
);
};

const AddGraphQLRequestNodeSidebar = ({ handleKind, position, previous, sourceId, targetId }: AddNodeSidebarProps) => {
const { workspaceId } = routes.dashboard.workspace.route.useLoaderData();

const insertNode = useInsertNode();

const fileCollection = useApiCollection(FileCollectionSchema);
const graphqlCollection = useApiCollection(GraphQLCollectionSchema);
const nodeGraphQLCollection = useApiCollection(NodeGraphQLCollectionSchema);

return (
<>
<SidebarHeader previous={previous} title='GraphQL request' />

<div className={tw`mx-4 my-3`}>
<Button
className={tw`w-full`}
onPress={async () => {
const graphqlId = Ulid.generate().bytes;

graphqlCollection.utils.insert({
graphqlId,
name: 'New GraphQL request',
url: '',
});

fileCollection.utils.insert({
fileId: graphqlId,
kind: FileKind.GRAPH_Q_L,
order: await getNextOrder(fileCollection),
workspaceId,
});

const nodeId = Ulid.generate().bytes;
nodeGraphQLCollection.utils.insert({ graphqlId, nodeId });
insertNode({ handleKind, kind: NodeKind.GRAPH_Q_L, name: 'graphql', nodeId, position, sourceId, targetId });
}}
>
New GraphQL request
</Button>
</div>

<FileTree
onAction={(key) => {
const nodeId = Ulid.generate().bytes;
const data: MessageInitShape<typeof NodeGraphQLInsertSchema> = { nodeId };

const file = fileCollection.get(key.toString())!;

if (file.kind === FileKind.GRAPH_Q_L) {
data.graphqlId = file.fileId;
} else {
return;
}

nodeGraphQLCollection.utils.insert(data);
insertNode({ handleKind, kind: NodeKind.GRAPH_Q_L, name: 'graphql', nodeId, position, sourceId, targetId });
}}
showControls
/>
</>
);
};

const AddAiNode = ({ handleKind, position, sourceId, targetId }: AddNodeSidebarProps) => {
const insertNode = useInsertNode();

Expand Down
Loading
Loading