From 84736a21b4a3e7a6d88f2f2f977335771eb54c87 Mon Sep 17 00:00:00 2001 From: gearnode Date: Wed, 12 Feb 2025 11:29:00 -0800 Subject: [PATCH] Add create vendor action Signed-off-by: gearnode --- apps/console/src/pages/VendorListPage.tsx | 43 +++- ...dorListPageCreateVendorMutation.graphql.ts | 117 ++++++++++ pkg/api/console/v1/schema.graphql | 9 + pkg/api/console/v1/schema/schema.go | 217 +++++++++++++++++- pkg/api/console/v1/types/types.go | 8 + pkg/api/console/v1/v1_resolver.go | 18 ++ 6 files changed, 401 insertions(+), 11 deletions(-) create mode 100644 apps/console/src/pages/__generated__/VendorListPageCreateVendorMutation.graphql.ts diff --git a/apps/console/src/pages/VendorListPage.tsx b/apps/console/src/pages/VendorListPage.tsx index 9d997278a..ae19eac83 100644 --- a/apps/console/src/pages/VendorListPage.tsx +++ b/apps/console/src/pages/VendorListPage.tsx @@ -4,6 +4,9 @@ import { PreloadedQuery, usePreloadedQuery, useQueryLoader, + useMutation, + loadQuery, + useLazyLoadQuery, } from "react-relay"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; @@ -14,7 +17,7 @@ import { Link } from "react-router"; import Fuse from "fuse.js"; import type { VendorListPageQuery as VendorListPageQueryType } from "./__generated__/VendorListPageQuery.graphql"; -const VendorListPageQuery = graphql` +const vendorListPageQuery = graphql` query VendorListPageQuery { node(id: "AZSfP_xAcAC5IAAAAAAltA") { id @@ -34,6 +37,17 @@ const VendorListPageQuery = graphql` } `; +const createVendorMutation = graphql` + mutation VendorListPageCreateVendorMutation($input: CreateVendorInput!) { + createVendor(input: $input) { + id + name + createdAt + updatedAt + } + } +`; + // TODO: Remove this once we have a real list of vendors const vendorsList = [ { id: '1', name: 'Amazon Web Services', createdAt: new Date().toISOString() }, @@ -43,21 +57,23 @@ const vendorsList = [ { id: '5', name: 'Slack', createdAt: new Date().toISOString() }, ]; - function VendorListContent({ queryRef, }: { queryRef: PreloadedQuery; }) { - const data = usePreloadedQuery(VendorListPageQuery, queryRef); - const vendors = data.node?.vendors?.edges?.map(edge => edge?.node) ?? []; + const data = usePreloadedQuery(vendorListPageQuery, queryRef); const [searchTerm, setSearchTerm] = useState(""); const [filteredVendors, setFilteredVendors] = useState>([]); + const [createVendor] = useMutation(createVendorMutation); + const [_, loadQuery] = useQueryLoader(vendorListPageQuery); const fuse = new Fuse(vendorsList, { keys: ['name'], threshold: 0.3, }); + const vendors = data.node.vendors?.edges.map(edge => edge.node) ?? []; + return (
@@ -103,10 +119,19 @@ function VendorListContent({ key={vendor?.id} className="px-3 py-2 hover:bg-gray-100 cursor-pointer" onClick={() => { - // Handle vendor selection - console.log('Selected vendor:', vendor); - setSearchTerm(""); - setFilteredVendors([]); + createVendor({ + variables: { + input: { + organizationId: data.node.id, + name: vendor.name + } + }, + onCompleted(response: any) { + setSearchTerm(""); + setFilteredVendors([]); + loadQuery({}, {fetchPolicy: 'network-only'}); + }, + }); }} >
@@ -179,7 +204,7 @@ function VendorListFallback() { } export default function VendorListPage() { - const [queryRef, loadQuery] = useQueryLoader(VendorListPageQuery); + const [queryRef, loadQuery] = useQueryLoader(vendorListPageQuery); useEffect(() => { loadQuery({}); diff --git a/apps/console/src/pages/__generated__/VendorListPageCreateVendorMutation.graphql.ts b/apps/console/src/pages/__generated__/VendorListPageCreateVendorMutation.graphql.ts new file mode 100644 index 000000000..bb967181b --- /dev/null +++ b/apps/console/src/pages/__generated__/VendorListPageCreateVendorMutation.graphql.ts @@ -0,0 +1,117 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type CreateVendorInput = { + name: string; + organizationId: string; +}; +export type VendorListPageCreateVendorMutation$variables = { + input: CreateVendorInput; +}; +export type VendorListPageCreateVendorMutation$data = { + readonly createVendor: { + readonly createdAt: any; + readonly id: string; + readonly name: string; + readonly updatedAt: any; + }; +}; +export type VendorListPageCreateVendorMutation = { + response: VendorListPageCreateVendorMutation$data; + variables: VendorListPageCreateVendorMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "Vendor", + "kind": "LinkedField", + "name": "createVendor", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "VendorListPageCreateVendorMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "VendorListPageCreateVendorMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "11eabcf8c7c1fef7b2cb37bcadf52666", + "id": null, + "metadata": {}, + "name": "VendorListPageCreateVendorMutation", + "operationKind": "mutation", + "text": "mutation VendorListPageCreateVendorMutation(\n $input: CreateVendorInput!\n) {\n createVendor(input: $input) {\n id\n name\n createdAt\n updatedAt\n }\n}\n" + } +}; +})(); + +(node as any).hash = "aeb5751c0d301a9433e706d237a025cd"; + +export default node; diff --git a/pkg/api/console/v1/schema.graphql b/pkg/api/console/v1/schema.graphql index 913c1296c..eb1c2998c 100644 --- a/pkg/api/console/v1/schema.graphql +++ b/pkg/api/console/v1/schema.graphql @@ -286,3 +286,12 @@ type EvidenceStateTransition { type Query { node(id: ID!): Node! } + +type Mutation { + createVendor(input: CreateVendorInput!): Vendor! +} + +input CreateVendorInput { + organizationId: ID! + name: String! +} \ No newline at end of file diff --git a/pkg/api/console/v1/schema/schema.go b/pkg/api/console/v1/schema/schema.go index 0c0769685..f610b2305 100644 --- a/pkg/api/console/v1/schema/schema.go +++ b/pkg/api/console/v1/schema/schema.go @@ -44,6 +44,7 @@ type ResolverRoot interface { Control() ControlResolver Evidence() EvidenceResolver Framework() FrameworkResolver + Mutation() MutationResolver Organization() OrganizationResolver Query() QueryResolver Task() TaskResolver @@ -152,6 +153,10 @@ type ComplexityRoot struct { Node func(childComplexity int) int } + Mutation struct { + CreateVendor func(childComplexity int, input types.CreateVendorInput) int + } + Organization struct { CreatedAt func(childComplexity int) int Frameworks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int @@ -261,6 +266,9 @@ type EvidenceResolver interface { type FrameworkResolver interface { Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlConnection, error) } +type MutationResolver interface { + CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.Vendor, error) +} type OrganizationResolver interface { Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.VendorConnection, error) @@ -691,6 +699,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.FrameworkEdge.Node(childComplexity), true + case "Mutation.createVendor": + if e.complexity.Mutation.CreateVendor == nil { + break + } + + args, err := ec.field_Mutation_createVendor_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateVendor(childComplexity, args["input"].(types.CreateVendorInput)), true + case "Organization.createdAt": if e.complexity.Organization.CreatedAt == nil { break @@ -1099,7 +1119,9 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { opCtx := graphql.GetOperationContext(ctx) ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} - inputUnmarshalMap := graphql.BuildUnmarshalerMap() + inputUnmarshalMap := graphql.BuildUnmarshalerMap( + ec.unmarshalInputCreateVendorInput, + ) first := true switch opCtx.Operation.Operation { @@ -1133,6 +1155,21 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { return &response } + case ast.Mutation: + return func(ctx context.Context) *graphql.Response { + if !first { + return nil + } + first = false + ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) + data := ec._Mutation(ctx, opCtx.Operation.SelectionSet) + var buf bytes.Buffer + data.MarshalGQL(&buf) + + return &graphql.Response{ + Data: buf.Bytes(), + } + } default: return graphql.OneShot(graphql.ErrorResponse(ctx, "unsupported GraphQL operation")) @@ -1469,7 +1506,15 @@ type EvidenceStateTransition { type Query { node(id: ID!): Node! } -`, BuiltIn: false}, + +type Mutation { + createVendor(input: CreateVendorInput!): Vendor! +} + +input CreateVendorInput { + organizationId: ID! + name: String! +}`, BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) @@ -1785,6 +1830,29 @@ func (ec *executionContext) field_Framework_controls_argsBefore( return zeroVal, nil } +func (ec *executionContext) field_Mutation_createVendor_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_createVendor_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_createVendor_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (types.CreateVendorInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNCreateVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateVendorInput(ctx, tmp) + } + + var zeroVal types.CreateVendorInput + return zeroVal, nil +} + func (ec *executionContext) field_Organization_frameworks_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -4512,6 +4580,59 @@ func (ec *executionContext) fieldContext_FrameworkEdge_node(_ context.Context, f return fc, nil } +func (ec *executionContext) _Mutation_createVendor(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createVendor(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateVendor(rctx, fc.Args["input"].(types.CreateVendorInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*types.Vendor) + fc.Result = res + return ec.marshalNVendor2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createVendor(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Vendor_id(ctx, field) + case "name": + return ec.fieldContext_Vendor_name(ctx, field) + case "createdAt": + return ec.fieldContext_Vendor_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Vendor_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Vendor", field.Name) + }, + } + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createVendor_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Organization_id(ctx, field) if err != nil { @@ -8356,6 +8477,40 @@ func (ec *executionContext) fieldContext___Type_specifiedByURL(_ context.Context // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputCreateVendorInput(ctx context.Context, obj any) (types.CreateVendorInput, error) { + var it types.CreateVendorInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"organizationId", "name"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "organizationId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.OrganizationID = data + case "name": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Name = data + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -9294,6 +9449,55 @@ func (ec *executionContext) _FrameworkEdge(ctx context.Context, sel ast.Selectio return out } +var mutationImplementors = []string{"Mutation"} + +func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, mutationImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Mutation", + }) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Mutation") + case "createVendor": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createVendor(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var organizationImplementors = []string{"Organization", "Node"} func (ec *executionContext) _Organization(ctx context.Context, sel ast.SelectionSet, obj *types.Organization) graphql.Marshaler { @@ -10710,6 +10914,11 @@ func (ec *executionContext) marshalNControlStateTransitionEdge2ᚖgithubᚗcom return ec._ControlStateTransitionEdge(ctx, sel, v) } +func (ec *executionContext) unmarshalNCreateVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateVendorInput(ctx context.Context, v any) (types.CreateVendorInput, error) { + res, err := ec.unmarshalInputCreateVendorInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNCursorKey2githubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx context.Context, v any) (page.CursorKey, error) { res, err := types.UnmarshalCursorKeyScalar(v) return res, graphql.ErrorOnPath(ctx, err) @@ -11289,6 +11498,10 @@ func (ec *executionContext) marshalNTaskStateTransitionEdge2ᚖgithubᚗcomᚋge return ec._TaskStateTransitionEdge(ctx, sel, v) } +func (ec *executionContext) marshalNVendor2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx context.Context, sel ast.SelectionSet, v types.Vendor) graphql.Marshaler { + return ec._Vendor(ctx, sel, &v) +} + func (ec *executionContext) marshalNVendor2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx context.Context, sel ast.SelectionSet, v *types.Vendor) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { diff --git a/pkg/api/console/v1/types/types.go b/pkg/api/console/v1/types/types.go index 4f5eec1f1..90ccc34e2 100644 --- a/pkg/api/console/v1/types/types.go +++ b/pkg/api/console/v1/types/types.go @@ -60,6 +60,11 @@ type ControlStateTransitionEdge struct { Node *ControlStateTransition `json:"node"` } +type CreateVendorInput struct { + OrganizationID gid.GID `json:"organizationId"` + Name string `json:"name"` +} + type Evidence struct { ID gid.GID `json:"id"` FileURL string `json:"fileUrl"` @@ -125,6 +130,9 @@ type FrameworkEdge struct { Node *Framework `json:"node"` } +type Mutation struct { +} + type Organization struct { ID gid.GID `json:"id"` Name string `json:"name"` diff --git a/pkg/api/console/v1/v1_resolver.go b/pkg/api/console/v1/v1_resolver.go index 4d4a3d9a9..b0f732f5e 100644 --- a/pkg/api/console/v1/v1_resolver.go +++ b/pkg/api/console/v1/v1_resolver.go @@ -12,6 +12,7 @@ import ( "github.com/getprobo/probo/pkg/api/console/v1/types" "github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/page" + "github.com/getprobo/probo/pkg/probo" "github.com/getprobo/probo/pkg/probo/coredata" "github.com/vektah/gqlparser/v2/gqlerror" ) @@ -64,6 +65,19 @@ func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, return types.NewControlConnection(page), nil } +// CreateVendor is the resolver for the createVendor field. +func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.Vendor, error) { + vendor, err := r.svc.CreateVendor(ctx, probo.CreateVendorRequest{ + OrganizationID: input.OrganizationID, + Name: input.Name, + }) + if err != nil { + return nil, fmt.Errorf("cannot create vendor: %w", err) + } + + return types.NewVendor(vendor), nil +} + // Frameworks is the resolver for the frameworks field. func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) { cursor := types.NewCursor(first, after, last, before) @@ -191,6 +205,9 @@ func (r *Resolver) Evidence() schema.EvidenceResolver { return &evidenceResolver // Framework returns schema.FrameworkResolver implementation. func (r *Resolver) Framework() schema.FrameworkResolver { return &frameworkResolver{r} } +// Mutation returns schema.MutationResolver implementation. +func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} } + // Organization returns schema.OrganizationResolver implementation. func (r *Resolver) Organization() schema.OrganizationResolver { return &organizationResolver{r} } @@ -203,6 +220,7 @@ func (r *Resolver) Task() schema.TaskResolver { return &taskResolver{r} } type controlResolver struct{ *Resolver } type evidenceResolver struct{ *Resolver } type frameworkResolver struct{ *Resolver } +type mutationResolver struct{ *Resolver } type organizationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } type taskResolver struct{ *Resolver }