Add update control

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-27 15:37:10 +01:00
parent 52de0387fa
commit 40ee608005
13 changed files with 1456 additions and 202 deletions

View File

@@ -24,7 +24,7 @@ posthog.init(process.env.POSTHOG_KEY!, {
}); });
const OrganizationSelectionPage = lazy( const OrganizationSelectionPage = lazy(
() => import("./pages/OrganizationSelectionPage"), () => import("./pages/OrganizationSelectionPage")
); );
const HomePage = lazy(() => import("./pages/HomePage")); const HomePage = lazy(() => import("./pages/HomePage"));
const NotFoundPage = lazy(() => import("./pages/NotFoundPage")); const NotFoundPage = lazy(() => import("./pages/NotFoundPage"));
@@ -35,18 +35,19 @@ const VendorOverviewPage = lazy(() => import("./pages/VendorOverviewPage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage")); const SettingsPage = lazy(() => import("./pages/SettingsPage"));
const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage")); const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage"));
const FrameworkOverviewPage = lazy( const FrameworkOverviewPage = lazy(
() => import("./pages/FrameworkOverviewPage"), () => import("./pages/FrameworkOverviewPage")
); );
const ControlOverviewPage = lazy(() => import("./pages/ControlOverviewPage")); const ControlOverviewPage = lazy(() => import("./pages/ControlOverviewPage"));
const PeopleOverviewPage = lazy(() => import("./pages/PeopleOverviewPage")); const PeopleOverviewPage = lazy(() => import("./pages/PeopleOverviewPage"));
const LoginPage = lazy(() => import("./pages/LoginPage")); const LoginPage = lazy(() => import("./pages/LoginPage"));
const RegisterPage = lazy(() => import("./pages/RegisterPage")); const RegisterPage = lazy(() => import("./pages/RegisterPage"));
const CreateOrganizationPage = lazy( const CreateOrganizationPage = lazy(
() => import("./pages/CreateOrganizationPage"), () => import("./pages/CreateOrganizationPage")
); );
const CreateFrameworkPage = lazy(() => import("./pages/CreateFrameworkPage")); const CreateFrameworkPage = lazy(() => import("./pages/CreateFrameworkPage"));
const CreateControlPage = lazy(() => import("./pages/CreateControlPage")); const CreateControlPage = lazy(() => import("./pages/CreateControlPage"));
const UpdateFrameworkPage = lazy(() => import("./pages/UpdateFrameworkPage")); const UpdateFrameworkPage = lazy(() => import("./pages/UpdateFrameworkPage"));
const UpdateControlPage = lazy(() => import("./pages/UpdateControlPage"));
function App() { function App() {
return ( return (
@@ -261,6 +262,16 @@ function App() {
</Suspense> </Suspense>
} }
/> />
<Route
path="frameworks/:frameworkId/controls/:controlId/update"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<UpdateControlPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route <Route
path="vendors/:vendorId" path="vendors/:vendorId"
element={ element={

View File

@@ -1,5 +1,5 @@
import { Suspense, useEffect, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import { useParams } from "react-router"; import { useParams, useNavigate } from "react-router";
import { import {
graphql, graphql,
PreloadedQuery, PreloadedQuery,
@@ -102,12 +102,14 @@ function ControlOverviewPageContent({
}) { }) {
const data = usePreloadedQuery<ControlOverviewPageQueryType>( const data = usePreloadedQuery<ControlOverviewPageQueryType>(
controlOverviewPageQuery, controlOverviewPageQuery,
queryRef, queryRef
); );
const { toast } = useToast(); const { toast } = useToast();
const { organizationId, frameworkId, controlId } = useParams();
const navigate = useNavigate();
const [updateTaskState] = const [updateTaskState] =
useMutation<ControlOverviewPageUpdateTaskStateMutationType>( useMutation<ControlOverviewPageUpdateTaskStateMutationType>(
updateTaskStateMutation, updateTaskStateMutation
); );
const [createTask] = const [createTask] =
useMutation<ControlOverviewPageCreateTaskMutationType>(createTaskMutation); useMutation<ControlOverviewPageCreateTaskMutationType>(createTaskMutation);
@@ -244,12 +246,25 @@ function ControlOverviewPageContent({
}); });
}; };
const handleEditControl = () => {
navigate(
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}/update`
);
};
return ( return (
<>
<Helmet>
<title>{control?.name || "Control"} - Probo</title>
</Helmet>
<div className="min-h-screen bg-white p-6 space-y-6"> <div className="min-h-screen bg-white p-6 space-y-6">
<div className="space-y-4"> <div className="space-y-4 mb-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">{control?.name}</h1> <h1 className="text-2xl font-semibold">{control?.name}</h1>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handleEditControl}>
Edit Control
</Button>
<div className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm"> <div className="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm">
30 min 30 min
</div> </div>
@@ -267,7 +282,9 @@ function ControlOverviewPageContent({
<Card className="bg-gray-50 border border-gray-200"> <Card className="bg-gray-50 border border-gray-200">
<CardContent className="p-6"> <CardContent className="p-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-4 h-4 rounded-full bg-white flex items-center justify-center border border-gray-200"> <div
className={`w-4 h-4 rounded-full bg-white flex items-center justify-center border border-gray-200`}
>
<div <div
className={`w-2 h-2 rounded-full ${ className={`w-2 h-2 rounded-full ${
control?.state === "IMPLEMENTED" control?.state === "IMPLEMENTED"
@@ -277,7 +294,9 @@ function ControlOverviewPageContent({
/> />
</div> </div>
<span className="text-sm text-gray-700"> <span className="text-sm text-gray-700">
{control?.state === "IMPLEMENTED" ? "Validated" : "Not validated"} {control?.state === "IMPLEMENTED"
? "Validated"
: "Not validated"}
</span> </span>
</div> </div>
</CardContent> </CardContent>
@@ -314,7 +333,10 @@ function ControlOverviewPageContent({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="description" className="text-sm font-medium"> <label
htmlFor="description"
className="text-sm font-medium"
>
Description (optional) Description (optional)
</label> </label>
<Input <Input
@@ -439,6 +461,7 @@ function ControlOverviewPageContent({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
</>
); );
} }
@@ -466,21 +489,22 @@ function ControlOverviewPageFallback() {
export default function ControlOverviewPage() { export default function ControlOverviewPage() {
const { controlId } = useParams(); const { controlId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<ControlOverviewPageQueryType>( const [queryRef, loadQuery] = useQueryLoader<ControlOverviewPageQueryType>(
controlOverviewPageQuery, controlOverviewPageQuery
); );
useEffect(() => { useEffect(() => {
loadQuery({ controlId: controlId! }); if (controlId) {
}, [loadQuery, controlId]); loadQuery({ controlId });
}
}, [controlId, loadQuery]);
if (!queryRef) {
return <ControlOverviewPageFallback />;
}
return ( return (
<>
<Helmet>
<title>Control Overview - Probo Console</title>
</Helmet>
<Suspense fallback={<ControlOverviewPageFallback />}> <Suspense fallback={<ControlOverviewPageFallback />}>
{queryRef && <ControlOverviewPageContent queryRef={queryRef} />} <ControlOverviewPageContent queryRef={queryRef} />
</Suspense> </Suspense>
</>
); );
} }

View File

@@ -0,0 +1,335 @@
import { Suspense, useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router";
import {
graphql,
useMutation,
usePreloadedQuery,
PreloadedQuery,
useQueryLoader,
} from "react-relay";
import { Helmet } from "react-helmet-async";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import { HelpCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// Type imports will be available after Relay compiler runs
// import type { UpdateControlPageQuery as UpdateControlPageQueryType } from "./__generated__/UpdateControlPageQuery.graphql";
// import type { UpdateControlPageUpdateControlMutation as UpdateControlPageUpdateControlMutationType } from "./__generated__/UpdateControlPageUpdateControlMutation.graphql";
const updateControlMutation = graphql`
mutation UpdateControlPageUpdateControlMutation($input: UpdateControlInput!) {
updateControl(input: $input) {
control {
id
name
description
category
state
version
}
}
}
`;
const updateControlQuery = graphql`
query UpdateControlPageQuery($controlId: ID!) {
node(id: $controlId) {
... on Control {
id
name
description
category
state
version
}
}
}
`;
function EditableField({
label,
value,
onChange,
type = "text",
helpText,
required,
multiline = false,
}: {
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
helpText?: string;
required?: boolean;
multiline?: boolean;
}) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor={label} className="text-sm font-medium">
{label}
{required && <span className="text-red-500">*</span>}
</Label>
{helpText && (
<div className="relative flex items-center">
<HelpCircle className="h-4 w-4 text-muted-foreground" />
<span className="sr-only">{helpText}</span>
</div>
)}
</div>
{multiline ? (
<Textarea
id={label}
value={value}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
onChange(e.target.value)
}
className={cn(
"w-full resize-none",
required && !value && "border-red-500"
)}
placeholder={`Enter ${label.toLowerCase()}`}
rows={4}
/>
) : (
<Input
id={label}
type={type}
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(e.target.value)
}
className={cn("w-full", required && !value && "border-red-500")}
placeholder={`Enter ${label.toLowerCase()}`}
/>
)}
</div>
);
}
function UpdateControlPageContent({
queryRef,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryRef: PreloadedQuery<any>;
}) {
const { organizationId, frameworkId, controlId } = useParams();
const navigate = useNavigate();
const { toast } = useToast();
const data = usePreloadedQuery(updateControlQuery, queryRef);
const [editedFields, setEditedFields] = useState<Set<string>>(new Set());
const [formData, setFormData] = useState({
name: "",
description: "",
category: "",
state: "",
});
useEffect(() => {
if (data.node) {
setFormData({
name: data.node.name || "",
description: data.node.description || "",
category: data.node.category || "",
state: data.node.state || "",
});
}
}, [data.node]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [commit, isInFlight] = useMutation<any>(updateControlMutation);
const handleFieldChange = (field: keyof typeof formData, value: string) => {
setFormData((prev) => ({
...prev,
[field]: value,
}));
setEditedFields((prev) => new Set(prev).add(field));
};
const handleCancel = () => {
navigate(
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`
);
};
const hasChanges = editedFields.size > 0;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name || !formData.description || !formData.category) {
toast({
title: "Validation Error",
description: "Please fill in all required fields.",
variant: "destructive",
});
return;
}
const input: {
id: string;
expectedVersion: number;
name?: string;
description?: string;
category?: string;
state?: string;
} = {
id: controlId!,
expectedVersion: data.node.version,
};
if (editedFields.has("name")) {
input.name = formData.name;
}
if (editedFields.has("description")) {
input.description = formData.description;
}
if (editedFields.has("category")) {
input.category = formData.category;
}
if (editedFields.has("state")) {
input.state = formData.state;
}
commit({
variables: {
input,
},
onCompleted(data, errors) {
if (errors) {
toast({
title: "Error",
description: errors[0]?.message || "Failed to update control",
variant: "destructive",
});
return;
}
toast({
title: "Success",
description: "Control updated successfully",
});
navigate(
`/organizations/${organizationId}/frameworks/${frameworkId}/controls/${controlId}`
);
},
onError(error) {
toast({
title: "Error",
description: error.message || "Failed to update control",
variant: "destructive",
});
},
});
};
return (
<>
<Helmet>
<title>Update Control - Probo</title>
</Helmet>
<div className="container mx-auto py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold">Update Control</h1>
<p className="text-muted-foreground">Update the control details</p>
</div>
<Card className="max-w-2xl">
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<EditableField
label="Name"
value={formData.name}
onChange={(value) => handleFieldChange("name", value)}
required
/>
<EditableField
label="Description"
value={formData.description}
onChange={(value) => handleFieldChange("description", value)}
required
multiline
helpText="Provide a detailed description of the control"
/>
<EditableField
label="Category"
value={formData.category}
onChange={(value) => handleFieldChange("category", value)}
required
/>
<div className="space-y-2">
<Label htmlFor="state" className="text-sm font-medium">
State
</Label>
<Select
value={formData.state}
onValueChange={(value) => handleFieldChange("state", value)}
>
<SelectTrigger>
<SelectValue placeholder="Select state" />
</SelectTrigger>
<SelectContent>
<SelectItem value="NOT_STARTED">Not Started</SelectItem>
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
<SelectItem value="NOT_APPLICABLE">Not Applicable</SelectItem>
<SelectItem value="IMPLEMENTED">Implemented</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end gap-3">
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" disabled={isInFlight || !hasChanges}>
{isInFlight ? "Updating..." : "Update Control"}
</Button>
</div>
</form>
</Card>
</div>
</>
);
}
function UpdateControlPageFallback() {
return <div>Loading...</div>;
}
export default function UpdateControlPage() {
const { controlId } = useParams();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [queryRef, loadQuery] = useQueryLoader<any>(updateControlQuery);
useEffect(() => {
if (controlId) {
loadQuery({ controlId });
}
}, [controlId, loadQuery]);
if (!queryRef) {
return <UpdateControlPageFallback />;
}
return (
<Suspense fallback={<UpdateControlPageFallback />}>
<UpdateControlPageContent queryRef={queryRef} />
</Suspense>
);
}

View File

@@ -0,0 +1,175 @@
/**
* @generated SignedSource<<daddf46b2039f01b80967778f24dd7f4>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type UpdateControlPageQuery$variables = {
controlId: string;
};
export type UpdateControlPageQuery$data = {
readonly node: {
readonly category?: string;
readonly description?: string;
readonly id?: string;
readonly name?: string;
readonly state?: ControlState;
readonly version?: number;
};
};
export type UpdateControlPageQuery = {
response: UpdateControlPageQuery$data;
variables: UpdateControlPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "controlId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "controlId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "UpdateControlPageQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/)
],
"type": "Control",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "UpdateControlPageQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/)
],
"type": "Control",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "c44526d8079e0c539d634841a1f14923",
"id": null,
"metadata": {},
"name": "UpdateControlPageQuery",
"operationKind": "query",
"text": "query UpdateControlPageQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n ... on Control {\n id\n name\n description\n category\n state\n version\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "d0a9cc6ebcb5b97ea57567edea1f7d5f";
export default node;

View File

@@ -0,0 +1,151 @@
/**
* @generated SignedSource<<81cac01f1ba6637b3367363835258e4c>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type UpdateControlInput = {
category?: string | null | undefined;
description?: string | null | undefined;
expectedVersion: number;
id: string;
name?: string | null | undefined;
state?: ControlState | null | undefined;
};
export type UpdateControlPageUpdateControlMutation$variables = {
input: UpdateControlInput;
};
export type UpdateControlPageUpdateControlMutation$data = {
readonly updateControl: {
readonly control: {
readonly category: string;
readonly description: string;
readonly id: string;
readonly name: string;
readonly state: ControlState;
readonly version: number;
};
};
};
export type UpdateControlPageUpdateControlMutation = {
response: UpdateControlPageUpdateControlMutation$data;
variables: UpdateControlPageUpdateControlMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateControlPayload",
"kind": "LinkedField",
"name": "updateControl",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Control",
"kind": "LinkedField",
"name": "control",
"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": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "UpdateControlPageUpdateControlMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "UpdateControlPageUpdateControlMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "1e9e2b8000a1b3061fcfda07260413ed",
"id": null,
"metadata": {},
"name": "UpdateControlPageUpdateControlMutation",
"operationKind": "mutation",
"text": "mutation UpdateControlPageUpdateControlMutation(\n $input: UpdateControlInput!\n) {\n updateControl(input: $input) {\n control {\n id\n name\n description\n category\n state\n version\n }\n }\n}\n"
}
};
})();
(node as any).hash = "861813b150cef2977f8d9455a9bcf4fa";
export default node;

View File

@@ -212,6 +212,7 @@ type ControlEdge {
type Control implements Node { type Control implements Node {
id: ID! id: ID!
version: Int!
category: String! category: String!
name: String! name: String!
description: String! description: String!
@@ -399,6 +400,7 @@ type Mutation {
createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload! createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload!
createControl(input: CreateControlInput!): CreateControlPayload! createControl(input: CreateControlInput!): CreateControlPayload!
updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload! updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload!
updateControl(input: UpdateControlInput!): UpdateControlPayload!
} }
input CreateVendorInput { input CreateVendorInput {
@@ -585,3 +587,16 @@ type UpdateVendorPayload {
type UpdatePeoplePayload { type UpdatePeoplePayload {
people: People! people: People!
} }
input UpdateControlInput {
id: ID!
expectedVersion: Int!
name: String
description: String
category: String
state: ControlState
}
type UpdateControlPayload {
control: Control!
}

View File

@@ -66,6 +66,7 @@ type ComplexityRoot struct {
StateTransisions func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int StateTransisions func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
UpdatedAt func(childComplexity int) int UpdatedAt func(childComplexity int) int
Version func(childComplexity int) int
} }
ControlConnection struct { ControlConnection struct {
@@ -208,6 +209,7 @@ type ComplexityRoot struct {
DeletePeople func(childComplexity int, input types.DeletePeopleInput) int DeletePeople func(childComplexity int, input types.DeletePeopleInput) int
DeleteTask func(childComplexity int, input types.DeleteTaskInput) int DeleteTask func(childComplexity int, input types.DeleteTaskInput) int
DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int DeleteVendor func(childComplexity int, input types.DeleteVendorInput) int
UpdateControl func(childComplexity int, input types.UpdateControlInput) int
UpdateFramework func(childComplexity int, input types.UpdateFrameworkInput) int UpdateFramework func(childComplexity int, input types.UpdateFrameworkInput) int
UpdatePeople func(childComplexity int, input types.UpdatePeopleInput) int UpdatePeople func(childComplexity int, input types.UpdatePeopleInput) int
UpdateTaskState func(childComplexity int, input types.UpdateTaskStateInput) int UpdateTaskState func(childComplexity int, input types.UpdateTaskStateInput) int
@@ -313,6 +315,10 @@ type ComplexityRoot struct {
Node func(childComplexity int) int Node func(childComplexity int) int
} }
UpdateControlPayload struct {
Control func(childComplexity int) int
}
UpdateFrameworkPayload struct { UpdateFrameworkPayload struct {
Framework func(childComplexity int) int Framework func(childComplexity int) int
} }
@@ -390,6 +396,7 @@ type MutationResolver interface {
CreateFramework(ctx context.Context, input types.CreateFrameworkInput) (*types.CreateFrameworkPayload, error) CreateFramework(ctx context.Context, input types.CreateFrameworkInput) (*types.CreateFrameworkPayload, error)
CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error) CreateControl(ctx context.Context, input types.CreateControlInput) (*types.CreateControlPayload, error)
UpdateFramework(ctx context.Context, input types.UpdateFrameworkInput) (*types.UpdateFrameworkPayload, error) UpdateFramework(ctx context.Context, input types.UpdateFrameworkInput) (*types.UpdateFrameworkPayload, error)
UpdateControl(ctx context.Context, input types.UpdateControlInput) (*types.UpdateControlPayload, error)
} }
type OrganizationResolver interface { type OrganizationResolver interface {
Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error)
@@ -500,6 +507,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Control.UpdatedAt(childComplexity), true return e.complexity.Control.UpdatedAt(childComplexity), true
case "Control.version":
if e.complexity.Control.Version == nil {
break
}
return e.complexity.Control.Version(childComplexity), true
case "ControlConnection.edges": case "ControlConnection.edges":
if e.complexity.ControlConnection.Edges == nil { if e.complexity.ControlConnection.Edges == nil {
break break
@@ -1029,6 +1043,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.DeleteVendor(childComplexity, args["input"].(types.DeleteVendorInput)), true return e.complexity.Mutation.DeleteVendor(childComplexity, args["input"].(types.DeleteVendorInput)), true
case "Mutation.updateControl":
if e.complexity.Mutation.UpdateControl == nil {
break
}
args, err := ec.field_Mutation_updateControl_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.UpdateControl(childComplexity, args["input"].(types.UpdateControlInput)), true
case "Mutation.updateFramework": case "Mutation.updateFramework":
if e.complexity.Mutation.UpdateFramework == nil { if e.complexity.Mutation.UpdateFramework == nil {
break break
@@ -1485,6 +1511,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.TaskStateTransitionEdge.Node(childComplexity), true return e.complexity.TaskStateTransitionEdge.Node(childComplexity), true
case "UpdateControlPayload.control":
if e.complexity.UpdateControlPayload.Control == nil {
break
}
return e.complexity.UpdateControlPayload.Control(childComplexity), true
case "UpdateFrameworkPayload.framework": case "UpdateFrameworkPayload.framework":
if e.complexity.UpdateFrameworkPayload.Framework == nil { if e.complexity.UpdateFrameworkPayload.Framework == nil {
break break
@@ -1697,6 +1730,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputDeletePeopleInput, ec.unmarshalInputDeletePeopleInput,
ec.unmarshalInputDeleteTaskInput, ec.unmarshalInputDeleteTaskInput,
ec.unmarshalInputDeleteVendorInput, ec.unmarshalInputDeleteVendorInput,
ec.unmarshalInputUpdateControlInput,
ec.unmarshalInputUpdateFrameworkInput, ec.unmarshalInputUpdateFrameworkInput,
ec.unmarshalInputUpdatePeopleInput, ec.unmarshalInputUpdatePeopleInput,
ec.unmarshalInputUpdateTaskStateInput, ec.unmarshalInputUpdateTaskStateInput,
@@ -2012,6 +2046,7 @@ type ControlEdge {
type Control implements Node { type Control implements Node {
id: ID! id: ID!
version: Int!
category: String! category: String!
name: String! name: String!
description: String! description: String!
@@ -2199,6 +2234,7 @@ type Mutation {
createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload! createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload!
createControl(input: CreateControlInput!): CreateControlPayload! createControl(input: CreateControlInput!): CreateControlPayload!
updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload! updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload!
updateControl(input: UpdateControlInput!): UpdateControlPayload!
} }
input CreateVendorInput { input CreateVendorInput {
@@ -2385,6 +2421,19 @@ type UpdateVendorPayload {
type UpdatePeoplePayload { type UpdatePeoplePayload {
people: People! people: People!
} }
input UpdateControlInput {
id: ID!
expectedVersion: Int!
name: String
description: String
category: String
state: ControlState
}
type UpdateControlPayload {
control: Control!
}
`, BuiltIn: false}, `, BuiltIn: false},
} }
var parsedSchema = gqlparser.MustLoadSchema(sources...) var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -2931,6 +2980,29 @@ func (ec *executionContext) field_Mutation_deleteVendor_argsInput(
return zeroVal, nil return zeroVal, nil
} }
func (ec *executionContext) field_Mutation_updateControl_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_updateControl_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_updateControl_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.UpdateControlInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNUpdateControlInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateControlInput(ctx, tmp)
}
var zeroVal types.UpdateControlInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_updateFramework_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Mutation_updateFramework_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -3623,6 +3695,44 @@ func (ec *executionContext) fieldContext_Control_id(_ context.Context, field gra
return fc, nil return fc, nil
} }
func (ec *executionContext) _Control_version(ctx context.Context, field graphql.CollectedField, obj *types.Control) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Control_version(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 obj.Version, nil
})
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.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Control_version(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Control",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Control_category(ctx context.Context, field graphql.CollectedField, obj *types.Control) (ret graphql.Marshaler) { func (ec *executionContext) _Control_category(ctx context.Context, field graphql.CollectedField, obj *types.Control) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Control_category(ctx, field) fc, err := ec.fieldContext_Control_category(ctx, field)
if err != nil { if err != nil {
@@ -4114,6 +4224,8 @@ func (ec *executionContext) fieldContext_ControlEdge_node(_ context.Context, fie
switch field.Name { switch field.Name {
case "id": case "id":
return ec.fieldContext_Control_id(ctx, field) return ec.fieldContext_Control_id(ctx, field)
case "version":
return ec.fieldContext_Control_version(ctx, field)
case "category": case "category":
return ec.fieldContext_Control_category(ctx, field) return ec.fieldContext_Control_category(ctx, field)
case "name": case "name":
@@ -6981,6 +7093,53 @@ func (ec *executionContext) fieldContext_Mutation_updateFramework(ctx context.Co
return fc, nil return fc, nil
} }
func (ec *executionContext) _Mutation_updateControl(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_updateControl(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().UpdateControl(rctx, fc.Args["input"].(types.UpdateControlInput))
})
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.UpdateControlPayload)
fc.Result = res
return ec.marshalNUpdateControlPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateControlPayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_updateControl(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 "control":
return ec.fieldContext_UpdateControlPayload_control(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type UpdateControlPayload", field.Name)
},
}
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_updateControl_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) { 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) fc, err := ec.fieldContext_Organization_id(ctx, field)
if err != nil { if err != nil {
@@ -9338,6 +9497,66 @@ func (ec *executionContext) fieldContext_TaskStateTransitionEdge_node(_ context.
return fc, nil return fc, nil
} }
func (ec *executionContext) _UpdateControlPayload_control(ctx context.Context, field graphql.CollectedField, obj *types.UpdateControlPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UpdateControlPayload_control(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 obj.Control, nil
})
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.Control)
fc.Result = res
return ec.marshalNControl2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐControl(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_UpdateControlPayload_control(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "UpdateControlPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_Control_id(ctx, field)
case "version":
return ec.fieldContext_Control_version(ctx, field)
case "category":
return ec.fieldContext_Control_category(ctx, field)
case "name":
return ec.fieldContext_Control_name(ctx, field)
case "description":
return ec.fieldContext_Control_description(ctx, field)
case "state":
return ec.fieldContext_Control_state(ctx, field)
case "stateTransisions":
return ec.fieldContext_Control_stateTransisions(ctx, field)
case "tasks":
return ec.fieldContext_Control_tasks(ctx, field)
case "createdAt":
return ec.fieldContext_Control_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Control_updatedAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Control", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _UpdateFrameworkPayload_framework(ctx context.Context, field graphql.CollectedField, obj *types.UpdateFrameworkPayload) (ret graphql.Marshaler) { func (ec *executionContext) _UpdateFrameworkPayload_framework(ctx context.Context, field graphql.CollectedField, obj *types.UpdateFrameworkPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UpdateFrameworkPayload_framework(ctx, field) fc, err := ec.fieldContext_UpdateFrameworkPayload_framework(ctx, field)
if err != nil { if err != nil {
@@ -12448,6 +12667,68 @@ func (ec *executionContext) unmarshalInputDeleteVendorInput(ctx context.Context,
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputUpdateControlInput(ctx context.Context, obj any) (types.UpdateControlInput, error) {
var it types.UpdateControlInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"id", "expectedVersion", "name", "description", "category", "state"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "id":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.ID = data
case "expectedVersion":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("expectedVersion"))
data, err := ec.unmarshalNInt2int(ctx, v)
if err != nil {
return it, err
}
it.ExpectedVersion = data
case "name":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.Name = data
case "description":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.Description = data
case "category":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("category"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.Category = data
case "state":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
data, err := ec.unmarshalOControlState2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋproboᚋcoredataᚐControlState(ctx, v)
if err != nil {
return it, err
}
it.State = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputUpdateFrameworkInput(ctx context.Context, obj any) (types.UpdateFrameworkInput, error) { func (ec *executionContext) unmarshalInputUpdateFrameworkInput(ctx context.Context, obj any) (types.UpdateFrameworkInput, error) {
var it types.UpdateFrameworkInput var it types.UpdateFrameworkInput
asMap := map[string]any{} asMap := map[string]any{}
@@ -12778,6 +13059,11 @@ func (ec *executionContext) _Control(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1) atomic.AddUint32(&out.Invalids, 1)
} }
case "version":
out.Values[i] = ec._Control_version(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "category": case "category":
out.Values[i] = ec._Control_category(ctx, field, obj) out.Values[i] = ec._Control_category(ctx, field, obj)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
@@ -14151,6 +14437,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "updateControl":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_updateControl(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -15112,6 +15405,45 @@ func (ec *executionContext) _TaskStateTransitionEdge(ctx context.Context, sel as
return out return out
} }
var updateControlPayloadImplementors = []string{"UpdateControlPayload"}
func (ec *executionContext) _UpdateControlPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateControlPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, updateControlPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("UpdateControlPayload")
case "control":
out.Values[i] = ec._UpdateControlPayload_control(ctx, field, obj)
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 updateFrameworkPayloadImplementors = []string{"UpdateFrameworkPayload"} var updateFrameworkPayloadImplementors = []string{"UpdateFrameworkPayload"}
func (ec *executionContext) _UpdateFrameworkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateFrameworkPayload) graphql.Marshaler { func (ec *executionContext) _UpdateFrameworkPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateFrameworkPayload) graphql.Marshaler {
@@ -17011,6 +17343,25 @@ func (ec *executionContext) marshalNTaskStateTransitionEdge2ᚖgithubᚗcomᚋge
return ec._TaskStateTransitionEdge(ctx, sel, v) return ec._TaskStateTransitionEdge(ctx, sel, v)
} }
func (ec *executionContext) unmarshalNUpdateControlInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateControlInput(ctx context.Context, v any) (types.UpdateControlInput, error) {
res, err := ec.unmarshalInputUpdateControlInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNUpdateControlPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateControlPayload(ctx context.Context, sel ast.SelectionSet, v types.UpdateControlPayload) graphql.Marshaler {
return ec._UpdateControlPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNUpdateControlPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateControlPayload(ctx context.Context, sel ast.SelectionSet, v *types.UpdateControlPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._UpdateControlPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNUpdateFrameworkInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateFrameworkInput(ctx context.Context, v any) (types.UpdateFrameworkInput, error) { func (ec *executionContext) unmarshalNUpdateFrameworkInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateFrameworkInput(ctx context.Context, v any) (types.UpdateFrameworkInput, error) {
res, err := ec.unmarshalInputUpdateFrameworkInput(ctx, v) res, err := ec.unmarshalInputUpdateFrameworkInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -42,6 +42,7 @@ func NewControlEdge(c *coredata.Control) *ControlEdge {
func NewControl(c *coredata.Control) *Control { func NewControl(c *coredata.Control) *Control {
return &Control{ return &Control{
ID: c.ID, ID: c.ID,
Version: c.Version,
Category: c.Category, Category: c.Category,
Name: c.Name, Name: c.Name,
Description: c.Description, Description: c.Description,

View File

@@ -17,6 +17,7 @@ type Node interface {
type Control struct { type Control struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
Version int `json:"version"`
Category string `json:"category"` Category string `json:"category"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
@@ -334,6 +335,19 @@ type TaskStateTransitionEdge struct {
Node *TaskStateTransition `json:"node"` Node *TaskStateTransition `json:"node"`
} }
type UpdateControlInput struct {
ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty"`
State *coredata.ControlState `json:"state,omitempty"`
}
type UpdateControlPayload struct {
Control *Control `json:"control"`
}
type UpdateFrameworkInput struct { type UpdateFrameworkInput struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"` ExpectedVersion int `json:"expectedVersion"`

View File

@@ -276,11 +276,19 @@ func (r *mutationResolver) CreateControl(ctx context.Context, input types.Create
// UpdateFramework is the resolver for the updateFramework field. // UpdateFramework is the resolver for the updateFramework field.
func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.UpdateFrameworkInput) (*types.UpdateFrameworkPayload, error) { func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.UpdateFrameworkInput) (*types.UpdateFrameworkPayload, error) {
var name, description *string
if input.Name != nil {
name = input.Name
}
if input.Description != nil {
description = input.Description
}
framework, err := r.proboSvc.UpdateFramework(ctx, probo.UpdateFrameworkRequest{ framework, err := r.proboSvc.UpdateFramework(ctx, probo.UpdateFrameworkRequest{
ID: input.ID, ID: input.ID,
ExpectedVersion: input.ExpectedVersion, ExpectedVersion: input.ExpectedVersion,
Name: input.Name, Name: name,
Description: input.Description, Description: description,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot update framework: %w", err) return nil, fmt.Errorf("cannot update framework: %w", err)
@@ -291,6 +299,41 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda
}, nil }, nil
} }
// UpdateControl is the resolver for the updateControl field.
func (r *mutationResolver) UpdateControl(ctx context.Context, input types.UpdateControlInput) (*types.UpdateControlPayload, error) {
var name, description, category *string
var state *coredata.ControlState
if input.Name != nil {
name = input.Name
}
if input.Description != nil {
description = input.Description
}
if input.Category != nil {
category = input.Category
}
if input.State != nil {
state = input.State
}
control, err := r.proboSvc.UpdateControl(ctx, probo.UpdateControlRequest{
ID: input.ID,
ExpectedVersion: input.ExpectedVersion,
Name: name,
Description: description,
Category: category,
State: state,
})
if err != nil {
return nil, fmt.Errorf("cannot update control: %w", err)
}
return &types.UpdateControlPayload{
Control: types.NewControl(control),
}, nil
}
// Frameworks is the resolver for the frameworks field. // 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) { 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) cursor := types.NewCursor(first, after, last, before)

View File

@@ -16,6 +16,7 @@ package coredata
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"maps" "maps"
"time" "time"
@@ -38,9 +39,18 @@ type (
ContentRef string ContentRef string
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Version int
} }
Controls []*Control Controls []*Control
UpdateControlParams struct {
ExpectedVersion int
Name *string
Description *string
Category *string
State *ControlState
}
) )
func (c Control) CursorKey() page.CursorKey { func (c Control) CursorKey() page.CursorKey {
@@ -58,6 +68,7 @@ func (c *Control) scan(r pgx.Row) error {
&c.ContentRef, &c.ContentRef,
&c.CreatedAt, &c.CreatedAt,
&c.UpdatedAt, &c.UpdatedAt,
&c.Version,
) )
} }
@@ -90,7 +101,8 @@ SELECT
cs.to_state AS state, cs.to_state AS state,
content_ref, content_ref,
created_at, created_at,
updated_at updated_at,
version
FROM FROM
controls controls
INNER JOIN INNER JOIN
@@ -133,7 +145,8 @@ INSERT INTO
description, description,
content_ref, content_ref,
created_at, created_at,
updated_at updated_at,
version
) )
VALUES ( VALUES (
@control_id, @control_id,
@@ -152,6 +165,7 @@ VALUES (
"framework_id": c.FrameworkID, "framework_id": c.FrameworkID,
"category": c.Category, "category": c.Category,
"name": c.Name, "name": c.Name,
"version": 0,
"description": c.Description, "description": c.Description,
"content_ref": c.ContentRef, "content_ref": c.ContentRef,
"created_at": c.CreatedAt, "created_at": c.CreatedAt,
@@ -189,7 +203,8 @@ SELECT
cs.to_state AS state, cs.to_state AS state,
content_ref, content_ref,
created_at, created_at,
updated_at updated_at,
version
FROM FROM
controls controls
INNER JOIN INNER JOIN
@@ -230,3 +245,76 @@ WHERE
return nil return nil
} }
func (c *Control) Update(
ctx context.Context,
conn pg.Conn,
scope *Scope,
params UpdateControlParams,
) error {
q := `
WITH control_states AS (
SELECT
control_id,
to_state,
reason,
RANK() OVER w
FROM
control_state_transitions
WINDOW
w AS (PARTITION BY control_id ORDER BY created_at DESC)
)
UPDATE controls SET
name = COALESCE(@name, name),
description = COALESCE(@description, description),
category = COALESCE(@category, category),
updated_at = @updated_at,
version = version + 1
WHERE %s
AND id = @control_id
AND version = @expected_version
RETURNING
id,
framework_id,
category,
name,
description,
(SELECT to_state FROM control_states WHERE control_id = controls.id AND rank = 1) AS state,
content_ref,
created_at,
updated_at,
version
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.NamedArgs{
"control_id": c.ID,
"expected_version": params.ExpectedVersion,
"updated_at": time.Now(),
}
if params.Name != nil {
args["name"] = *params.Name
}
if params.Description != nil {
args["description"] = *params.Description
}
if params.Category != nil {
args["category"] = *params.Category
}
maps.Copy(args, scope.SQLArguments())
r := conn.QueryRow(ctx, q, args)
c2 := Control{}
if err := c2.scan(r); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrConcurrentModification
}
return err
}
*c = c2
return nil
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE controls ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
ALTER TABLE controls ALTER COLUMN version DROP DEFAULT;

View File

@@ -0,0 +1,44 @@
package probo
import (
"context"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/probo/coredata"
"go.gearno.de/kit/pg"
)
type UpdateControlRequest struct {
ID gid.GID
ExpectedVersion int
Name *string
Description *string
Category *string
State *coredata.ControlState
}
func (s Service) UpdateControl(
ctx context.Context,
req UpdateControlRequest,
) (*coredata.Control, error) {
params := coredata.UpdateControlParams{
ExpectedVersion: req.ExpectedVersion,
Name: req.Name,
Description: req.Description,
Category: req.Category,
State: req.State,
}
control := &coredata.Control{ID: req.ID}
err := s.pg.WithTx(
ctx,
func(conn pg.Conn) error {
return control.Update(ctx, conn, s.scope, params)
})
if err != nil {
return nil, err
}
return control, nil
}