diff --git a/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx b/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx new file mode 100644 index 000000000..7efdb3c78 --- /dev/null +++ b/apps/console/src/components/snapshots/LinkedSnapshotsCard.tsx @@ -0,0 +1,200 @@ +import { graphql } from "relay-runtime"; +import { + Card, + IconPlusLarge, + Button, + Tr, + Td, + Table, + Thead, + Tbody, + Th, + IconChevronDown, + IconTrashCan, + Badge, + TrButton, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import type { LinkedSnapshotsCardFragment$key } from "./__generated__/LinkedSnapshotsCardFragment.graphql"; +import { useFragment } from "react-relay"; +import { useMemo, useState } from "react"; +import { sprintf, getSnapshotTypeLabel, getSnapshotTypeUrlPath } from "@probo/helpers"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { LinkedSnapshotsDialog } from "./LinkedSnapshotsDialog"; +import clsx from "clsx"; + +const linkedSnapshotFragment = graphql` + fragment LinkedSnapshotsCardFragment on Snapshot { + id + name + description + type + createdAt + } +`; + +type Mutation = (p: { + variables: { + input: { + snapshotId: string; + } & Params; + connections: string[]; + }; +}) => void; + +type Props = { + snapshots: (LinkedSnapshotsCardFragment$key & { id: string })[]; + params: Params; + disabled?: boolean; + connectionId: string; + onAttach: Mutation; + onDetach: Mutation; + variant?: "card" | "table"; +}; + +export function LinkedSnapshotsCard(props: Props) { + const { __ } = useTranslate(); + const [limit, setLimit] = useState(4); + const snapshots = useMemo(() => { + return limit ? props.snapshots.slice(0, limit) : props.snapshots; + }, [props.snapshots, limit]); + const showMoreButton = limit !== null && props.snapshots.length > limit; + const variant = props.variant ?? "table"; + + const onAttach = (snapshotId: string) => { + props.onAttach({ + variables: { + input: { + snapshotId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + const onDetach = (snapshotId: string) => { + props.onDetach({ + variables: { + input: { + snapshotId, + ...props.params, + }, + connections: [props.connectionId], + }, + }); + }; + + const Wrapper = variant === "card" ? Card : "div"; + + return ( + + {variant === "card" && ( +
+
{__("Snapshots")}
+ + + +
+ )} + + + + + + {variant === "table" && } + + + + + + {snapshots.length === 0 && ( + + + + )} + {snapshots.map((snapshot) => ( + + ))} + {variant === "table" && ( + + + {__("Link snapshot")} + + + )} + +
{__("Name")}{__("Type")}{__("Description")}{__("Created")}
+ {__("No snapshots linked")} +
+ {showMoreButton && ( + + )} +
+ ); +} + +function SnapshotRow(props: { + snapshot: LinkedSnapshotsCardFragment$key & { id: string }; + onClick: (snapshotId: string) => void; + variant: "card" | "table"; +}) { + const snapshot = useFragment(linkedSnapshotFragment, props.snapshot); + const organizationId = useOrganizationId(); + const { __, dateFormat } = useTranslate(); + + const urlPath = getSnapshotTypeUrlPath(snapshot.type); + const snapshotUrl = `/organizations/${organizationId}/snapshots/${snapshot.id}${urlPath}`; + + return ( + + {snapshot.name} + + + {getSnapshotTypeLabel(__, snapshot.type)} + + + {props.variant === "table" && ( + + {snapshot.description || __("No description")} + + )} + + {dateFormat(snapshot.createdAt, { year: "numeric", month: "short", day: "numeric" })} + + + + + + ); +} diff --git a/apps/console/src/components/snapshots/LinkedSnapshotsDialog.tsx b/apps/console/src/components/snapshots/LinkedSnapshotsDialog.tsx new file mode 100644 index 000000000..df0d42a8a --- /dev/null +++ b/apps/console/src/components/snapshots/LinkedSnapshotsDialog.tsx @@ -0,0 +1,194 @@ +import { + Button, + Dialog, + DialogContent, + DialogFooter, + Badge, + InfiniteScrollTrigger, + Input, + Spinner, + IconMagnifyingGlass, + IconPlusLarge, + IconTrashCan, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { getSnapshotTypeLabel } from "@probo/helpers"; +import { Suspense, useMemo, useState, type ReactNode } from "react"; +import { graphql } from "relay-runtime"; +import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; +import type { LinkedSnapshotsDialogQuery } from "./__generated__/LinkedSnapshotsDialogQuery.graphql"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import type { NodeOf } from "/types"; +import type { + LinkedSnapshotsDialogFragment$data, + LinkedSnapshotsDialogFragment$key, +} from "./__generated__/LinkedSnapshotsDialogFragment.graphql"; + +const snapshotsQuery = graphql` + query LinkedSnapshotsDialogQuery($organizationId: ID!) { + organization: node(id: $organizationId) { + id + ... on Organization { + ...LinkedSnapshotsDialogFragment + } + } + } +`; + +const snapshotsFragment = graphql` + fragment LinkedSnapshotsDialogFragment on Organization + @refetchable(queryName: "LinkedSnapshotsDialogQuery_fragment") + @argumentDefinitions( + first: { type: "Int", defaultValue: 20 } + order: { type: "SnapshotOrder", defaultValue: null } + after: { type: "CursorKey", defaultValue: null } + before: { type: "CursorKey", defaultValue: null } + last: { type: "Int", defaultValue: null } + ) { + snapshots( + first: $first + after: $after + last: $last + before: $before + orderBy: $order + ) @connection(key: "LinkedSnapshotsDialogQuery_snapshots") { + edges { + node { + id + name + description + type + createdAt + } + } + } + } +`; + +type Props = { + children: ReactNode; + disabled?: boolean; + linkedSnapshots?: { id: string }[]; + onLink: (snapshotId: string) => void; + onUnlink: (snapshotId: string) => void; +}; + +export function LinkedSnapshotsDialog({ children, ...props }: Props) { + const { __ } = useTranslate(); + + return ( + + + }> + + + + + + ); +} + +function LinkedSnapshotsDialogContent(props: Omit) { + const organizationId = useOrganizationId(); + const query = useLazyLoadQuery(snapshotsQuery, { + organizationId, + }); + const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment< + LinkedSnapshotsDialogQuery, + LinkedSnapshotsDialogFragment$key>( + snapshotsFragment, + query.organization + ); + + const { __ } = useTranslate(); + const [search, setSearch] = useState(""); + const snapshots = data.snapshots?.edges?.map((edge) => edge.node) ?? []; + const linkedIds = useMemo(() => { + return new Set(props.linkedSnapshots?.map((s) => s.id) ?? []); + }, [props.linkedSnapshots]); + + const filteredSnapshots = useMemo(() => { + return snapshots.filter((snapshot) => + snapshot.name.toLowerCase().includes(search.toLowerCase()) + ); + }, [snapshots, search]); + + return ( + <> +
+ +
+
+ {filteredSnapshots.map((snapshot) => ( + + ))} + {hasNext && ( + loadNext(20)} + /> + )} +
+ + ); +} + +type Snapshot = NodeOf; + +type RowProps = { + snapshot: Snapshot; + linkedSnapshots: Set; + disabled?: boolean; + onLink: (snapshotId: string) => void; + onUnlink: (snapshotId: string) => void; +}; + +function SnapshotRow(props: RowProps) { + const { __, dateFormat } = useTranslate(); + + const isLinked = props.linkedSnapshots.has(props.snapshot.id); + const onClick = isLinked ? props.onUnlink : props.onLink; + const IconComponent = isLinked ? IconTrashCan : IconPlusLarge; + + return ( + + + ); +} diff --git a/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsCardFragment.graphql.ts b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsCardFragment.graphql.ts new file mode 100644 index 000000000..b6cbc3b60 --- /dev/null +++ b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsCardFragment.graphql.ts @@ -0,0 +1,75 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NON_CONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; +import { FragmentRefs } from "relay-runtime"; +export type LinkedSnapshotsCardFragment$data = { + readonly createdAt: any; + readonly description: string | null | undefined; + readonly id: string; + readonly name: string; + readonly type: SnapshotsType; + readonly " $fragmentType": "LinkedSnapshotsCardFragment"; +}; +export type LinkedSnapshotsCardFragment$key = { + readonly " $data"?: LinkedSnapshotsCardFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"LinkedSnapshotsCardFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LinkedSnapshotsCardFragment", + "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": "type", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "type": "Snapshot", + "abstractKey": null +}; + +(node as any).hash = "b9b682f8e57082c98d8b0460155757fb"; + +export default node; diff --git a/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogFragment.graphql.ts b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogFragment.graphql.ts new file mode 100644 index 000000000..05c07a1e3 --- /dev/null +++ b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogFragment.graphql.ts @@ -0,0 +1,239 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NON_CONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; +import { FragmentRefs } from "relay-runtime"; +export type LinkedSnapshotsDialogFragment$data = { + readonly id: string; + readonly snapshots: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly createdAt: any; + readonly description: string | null | undefined; + readonly id: string; + readonly name: string; + readonly type: SnapshotsType; + }; + }>; + }; + readonly " $fragmentType": "LinkedSnapshotsDialogFragment"; +}; +export type LinkedSnapshotsDialogFragment$key = { + readonly " $data"?: LinkedSnapshotsDialogFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"LinkedSnapshotsDialogFragment">; +}; + +import LinkedSnapshotsDialogQuery_fragment_graphql from './LinkedSnapshotsDialogQuery_fragment.graphql'; + +const node: ReaderFragment = (function(){ +var v0 = [ + "snapshots" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" + }, + { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "bidirectional", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "first", + "cursor": "after" + }, + "backward": { + "count": "last", + "cursor": "before" + }, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": LinkedSnapshotsDialogQuery_fragment_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "LinkedSnapshotsDialogFragment", + "selections": [ + { + "alias": "snapshots", + "args": [ + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } + ], + "concreteType": "SnapshotConnection", + "kind": "LinkedField", + "name": "__LinkedSnapshotsDialogQuery_snapshots_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SnapshotEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Snapshot", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "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": "type", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + (v1/*: any*/) + ], + "type": "Organization", + "abstractKey": null +}; +})(); + +(node as any).hash = "bdc77ee756ac59c099caa8765403cd22"; + +export default node; diff --git a/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogQuery.graphql.ts b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogQuery.graphql.ts new file mode 100644 index 000000000..2898d4fcc --- /dev/null +++ b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogQuery.graphql.ts @@ -0,0 +1,259 @@ +/** + * @generated SignedSource<<78d7f0f48f20187e9eef7744a19d8e1e>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type LinkedSnapshotsDialogQuery$variables = { + organizationId: string; +}; +export type LinkedSnapshotsDialogQuery$data = { + readonly organization: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedSnapshotsDialogFragment">; + }; +}; +export type LinkedSnapshotsDialogQuery = { + response: LinkedSnapshotsDialogQuery$data; + variables: LinkedSnapshotsDialogQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v4 = [ + { + "kind": "Literal", + "name": "first", + "value": 20 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "LinkedSnapshotsDialogQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedSnapshotsDialogFragment" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "LinkedSnapshotsDialogQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v4/*: any*/), + "concreteType": "SnapshotConnection", + "kind": "LinkedField", + "name": "snapshots", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SnapshotEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Snapshot", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "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": "type", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "snapshots(first:20)" + }, + { + "alias": null, + "args": (v4/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "LinkedSnapshotsDialogQuery_snapshots", + "kind": "LinkedHandle", + "name": "snapshots" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "200a82b795d348f0d8a63a3b2b779d7c", + "id": null, + "metadata": {}, + "name": "LinkedSnapshotsDialogQuery", + "operationKind": "query", + "text": "query LinkedSnapshotsDialogQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n ...LinkedSnapshotsDialogFragment\n }\n }\n}\n\nfragment LinkedSnapshotsDialogFragment on Organization {\n snapshots(first: 20) {\n edges {\n node {\n id\n name\n description\n type\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "b91ff32883e1f6e83d9b43251fc83001"; + +export default node; diff --git a/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogQuery_fragment.graphql.ts b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogQuery_fragment.graphql.ts new file mode 100644 index 000000000..d3e6b71ef --- /dev/null +++ b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogQuery_fragment.graphql.ts @@ -0,0 +1,332 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrderDirection = "ASC" | "DESC"; +export type SnapshotOrderField = "CREATED_AT" | "NAME" | "TYPE"; +export type SnapshotOrder = { + direction: OrderDirection; + field: SnapshotOrderField; +}; +export type LinkedSnapshotsDialogQuery_fragment$variables = { + after?: any | null | undefined; + before?: any | null | undefined; + first?: number | null | undefined; + id: string; + last?: number | null | undefined; + order?: SnapshotOrder | null | undefined; +}; +export type LinkedSnapshotsDialogQuery_fragment$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"LinkedSnapshotsDialogFragment">; + }; +}; +export type LinkedSnapshotsDialogQuery_fragment = { + response: LinkedSnapshotsDialogQuery_fragment$data; + variables: LinkedSnapshotsDialogQuery_fragment$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "before" +}, +v2 = { + "defaultValue": 20, + "kind": "LocalArgument", + "name": "first" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v4 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "last" +}, +v5 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "order" +}, +v6 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v7 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v8 = { + "kind": "Variable", + "name": "before", + "variableName": "before" +}, +v9 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v10 = { + "kind": "Variable", + "name": "last", + "variableName": "last" +}, +v11 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v12 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v13 = [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "orderBy", + "variableName": "order" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/), + (v4/*: any*/), + (v5/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "LinkedSnapshotsDialogQuery_fragment", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + (v7/*: any*/), + (v8/*: any*/), + (v9/*: any*/), + (v10/*: any*/), + { + "kind": "Variable", + "name": "order", + "variableName": "order" + } + ], + "kind": "FragmentSpread", + "name": "LinkedSnapshotsDialogFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v4/*: any*/), + (v5/*: any*/), + (v3/*: any*/) + ], + "kind": "Operation", + "name": "LinkedSnapshotsDialogQuery_fragment", + "selections": [ + { + "alias": null, + "args": (v6/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v11/*: any*/), + (v12/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v13/*: any*/), + "concreteType": "SnapshotConnection", + "kind": "LinkedField", + "name": "snapshots", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SnapshotEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Snapshot", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v12/*: any*/), + { + "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": "type", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + (v11/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasPreviousPage", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "startCursor", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v13/*: any*/), + "filters": [ + "orderBy" + ], + "handle": "connection", + "key": "LinkedSnapshotsDialogQuery_snapshots", + "kind": "LinkedHandle", + "name": "snapshots" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "e4c40c765a5b6ef4ba63a6343c76e6e0", + "id": null, + "metadata": {}, + "name": "LinkedSnapshotsDialogQuery_fragment", + "operationKind": "query", + "text": "query LinkedSnapshotsDialogQuery_fragment(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: SnapshotOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...LinkedSnapshotsDialogFragment_16fISc\n id\n }\n}\n\nfragment LinkedSnapshotsDialogFragment_16fISc on Organization {\n snapshots(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n name\n description\n type\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n" + } +}; +})(); + +(node as any).hash = "bdc77ee756ac59c099caa8765403cd22"; + +export default node; diff --git a/apps/console/src/hooks/graph/FrameworkGraph.ts b/apps/console/src/hooks/graph/FrameworkGraph.ts index 9ae18d9ec..21433368a 100644 --- a/apps/console/src/hooks/graph/FrameworkGraph.ts +++ b/apps/console/src/hooks/graph/FrameworkGraph.ts @@ -130,6 +130,16 @@ export const frameworkControlNodeQuery = graphql` } } } + snapshots(first: 100) + @connection(key: "FrameworkGraphControl_snapshots") { + __id + edges { + node { + id + ...LinkedSnapshotsCardFragment + } + } + } } } } diff --git a/apps/console/src/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql.ts index 367d668f9..395aa3036 100644 --- a/apps/console/src/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<0e6281a892cbd711d75d22153d4710ea>> * @lightSyntaxTransform * @nogrep */ @@ -48,6 +48,15 @@ export type FrameworkGraphControlNodeQuery$data = { }; readonly name?: string; readonly sectionTitle?: string; + readonly snapshots?: { + readonly __id: string; + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedSnapshotsCardFragment">; + }; + }>; + }; readonly status?: ControlStatus; readonly " $fragmentSpreads": FragmentRefs<"FrameworkControlDialogFragment">; }; @@ -343,6 +352,49 @@ return { (v11/*: any*/) ], "storageKey": null + }, + { + "alias": "snapshots", + "args": null, + "concreteType": "SnapshotConnection", + "kind": "LinkedField", + "name": "__FrameworkGraphControl_snapshots_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SnapshotEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Snapshot", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedSnapshotsCardFragment" + }, + (v8/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/) + ], + "storageKey": null + }, + (v10/*: any*/), + (v11/*: any*/) + ], + "storageKey": null } ], "type": "Control", @@ -607,6 +659,63 @@ return { "key": "FrameworkGraphControl_audits", "kind": "LinkedHandle", "name": "audits" + }, + { + "alias": null, + "args": (v12/*: any*/), + "concreteType": "SnapshotConnection", + "kind": "LinkedField", + "name": "snapshots", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SnapshotEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Snapshot", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + (v5/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "type", + "storageKey": null + }, + (v14/*: any*/), + (v8/*: any*/) + ], + "storageKey": null + }, + (v9/*: any*/) + ], + "storageKey": null + }, + (v10/*: any*/), + (v11/*: any*/) + ], + "storageKey": "snapshots(first:100)" + }, + { + "alias": null, + "args": (v12/*: any*/), + "filters": null, + "handle": "connection", + "key": "FrameworkGraphControl_snapshots", + "kind": "LinkedHandle", + "name": "snapshots" } ], "type": "Control", @@ -618,7 +727,7 @@ return { ] }, "params": { - "cacheID": "20a0b80a6b0d796951c7f8c6eca41d3c", + "cacheID": "2ce9eb2cbe052019e86b2d0baecfb6f0", "id": null, "metadata": { "connection": [ @@ -648,16 +757,25 @@ return { "node", "audits" ] + }, + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "node", + "snapshots" + ] } ] }, "name": "FrameworkGraphControlNodeQuery", "operationKind": "query", - "text": "query FrameworkGraphControlNodeQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n ... on Control {\n id\n name\n sectionTitle\n description\n status\n exclusionJustification\n ...FrameworkControlDialogFragment\n measures(first: 100) {\n edges {\n node {\n id\n ...LinkedMeasuresCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...LinkedDocumentsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n ...LinkedAuditsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment FrameworkControlDialogFragment on Control {\n id\n name\n description\n sectionTitle\n status\n exclusionJustification\n}\n\nfragment LinkedAuditsCardFragment on Audit {\n id\n name\n createdAt\n state\n validFrom\n validUntil\n framework {\n id\n name\n }\n}\n\nfragment LinkedDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment LinkedMeasuresCardFragment on Measure {\n id\n name\n state\n}\n" + "text": "query FrameworkGraphControlNodeQuery(\n $controlId: ID!\n) {\n node(id: $controlId) {\n __typename\n ... on Control {\n id\n name\n sectionTitle\n description\n status\n exclusionJustification\n ...FrameworkControlDialogFragment\n measures(first: 100) {\n edges {\n node {\n id\n ...LinkedMeasuresCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n documents(first: 100) {\n edges {\n node {\n id\n ...LinkedDocumentsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n audits(first: 100) {\n edges {\n node {\n id\n ...LinkedAuditsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n snapshots(first: 100) {\n edges {\n node {\n id\n ...LinkedSnapshotsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment FrameworkControlDialogFragment on Control {\n id\n name\n description\n sectionTitle\n status\n exclusionJustification\n}\n\nfragment LinkedAuditsCardFragment on Audit {\n id\n name\n createdAt\n state\n validFrom\n validUntil\n framework {\n id\n name\n }\n}\n\nfragment LinkedDocumentsCardFragment on Document {\n id\n title\n createdAt\n documentType\n versions(first: 1) {\n edges {\n node {\n id\n status\n }\n }\n }\n}\n\nfragment LinkedMeasuresCardFragment on Measure {\n id\n name\n state\n}\n\nfragment LinkedSnapshotsCardFragment on Snapshot {\n id\n name\n description\n type\n createdAt\n}\n" } }; })(); -(node as any).hash = "ac5240126dfbf4882ba2b111e865045d"; +(node as any).hash = "afc4bbbce8d8b3cd57ac2bf77db58e55"; export default node; diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx index 369ebcf98..94d7badaa 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx @@ -18,6 +18,7 @@ import { useNavigate, useOutletContext } from "react-router"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { LinkedDocumentsCard } from "/components/documents/LinkedDocumentsCard"; import { LinkedAuditsCard } from "/components/audits/LinkedAuditsCard"; +import { LinkedSnapshotsCard } from "/components/snapshots/LinkedSnapshotsCard"; import { FrameworkControlDialog } from "./dialogs/FrameworkControlDialog"; import { promisifyMutation } from "@probo/helpers"; import type { FrameworkGraphControlNodeQuery } from "/hooks/graph/__generated__/FrameworkGraphControlNodeQuery.graphql"; @@ -105,6 +106,33 @@ const detachAuditMutation = graphql` } `; +const attachSnapshotMutation = graphql` + mutation FrameworkControlPageAttachSnapshotMutation( + $input: CreateControlSnapshotMappingInput! + $connections: [ID!]! + ) { + createControlSnapshotMapping(input: $input) { + snapshotEdge @prependEdge(connections: $connections) { + node { + id + ...LinkedSnapshotsCardFragment + } + } + } + } +`; + +const detachSnapshotMutation = graphql` + mutation FrameworkControlPageDetachSnapshotMutation( + $input: DeleteControlSnapshotMappingInput! + $connections: [ID!]! + ) { + deleteControlSnapshotMapping(input: $input) { + deletedSnapshotId @deleteEdge(connections: $connections) + } + } +`; + const deleteControlMutation = graphql` mutation FrameworkControlPageDeleteControlMutation( $input: DeleteControlInput! @@ -148,6 +176,8 @@ export default function FrameworkControlPage({ queryRef }: Props) { ); const [detachAudit, isDetachingAudit] = useMutation(detachAuditMutation); const [attachAudit, isAttachingAudit] = useMutation(attachAuditMutation); + const [detachSnapshot, isDetachingSnapshot] = useMutation(detachSnapshotMutation); + const [attachSnapshot, isAttachingSnapshot] = useMutation(attachSnapshotMutation); const [deleteControl] = useMutation(deleteControlMutation); const onDelete = () => { @@ -216,34 +246,51 @@ export default function FrameworkControlPage({ queryRef }: Props) { )}
-
{control.name}
- edge.node) ?? []} - params={{ controlId: control.id }} - connectionId={control.measures?.__id!} - onAttach={attachMeasure} - onDetach={detachMeasure} - disabled={isAttachingMeasure || isDetachingMeasure} - /> - edge.node) ?? []} - params={{ controlId: control.id }} - connectionId={control.documents?.__id!} - onAttach={attachDocument} - onDetach={detachDocument} - disabled={isAttachingDocument || isDetachingDocument} - /> - edge.node) ?? []} - params={{ controlId: control.id }} - connectionId={control.audits?.__id!} - onAttach={attachAudit} - onDetach={detachAudit} - disabled={isAttachingAudit || isDetachingAudit} - /> +
{control.name}
+
+ edge.node) ?? []} + params={{ controlId: control.id }} + connectionId={control.measures?.__id!} + onAttach={attachMeasure} + onDetach={detachMeasure} + disabled={isAttachingMeasure || isDetachingMeasure} + /> +
+
+ edge.node) ?? []} + params={{ controlId: control.id }} + connectionId={control.documents?.__id!} + onAttach={attachDocument} + onDetach={detachDocument} + disabled={isAttachingDocument || isDetachingDocument} + /> +
+
+ edge.node) ?? []} + params={{ controlId: control.id }} + connectionId={control.audits?.__id!} + onAttach={attachAudit} + onDetach={detachAudit} + disabled={isAttachingAudit || isDetachingAudit} + /> +
+
+ edge.node) ?? []} + params={{ controlId: control.id }} + connectionId={control.snapshots?.__id!} + onAttach={attachSnapshot} + onDetach={detachSnapshot} + disabled={isAttachingSnapshot || isDetachingSnapshot} + /> +
); diff --git a/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkControlPageAttachSnapshotMutation.graphql.ts b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkControlPageAttachSnapshotMutation.graphql.ts new file mode 100644 index 000000000..2cb72b588 --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkControlPageAttachSnapshotMutation.graphql.ts @@ -0,0 +1,216 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type CreateControlSnapshotMappingInput = { + controlId: string; + snapshotId: string; +}; +export type FrameworkControlPageAttachSnapshotMutation$variables = { + connections: ReadonlyArray; + input: CreateControlSnapshotMappingInput; +}; +export type FrameworkControlPageAttachSnapshotMutation$data = { + readonly createControlSnapshotMapping: { + readonly snapshotEdge: { + readonly node: { + readonly id: string; + readonly " $fragmentSpreads": FragmentRefs<"LinkedSnapshotsCardFragment">; + }; + }; + }; +}; +export type FrameworkControlPageAttachSnapshotMutation = { + response: FrameworkControlPageAttachSnapshotMutation$data; + variables: FrameworkControlPageAttachSnapshotMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "FrameworkControlPageAttachSnapshotMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlSnapshotMappingPayload", + "kind": "LinkedField", + "name": "createControlSnapshotMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SnapshotEdge", + "kind": "LinkedField", + "name": "snapshotEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Snapshot", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "LinkedSnapshotsCardFragment" + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "FrameworkControlPageAttachSnapshotMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "CreateControlSnapshotMappingPayload", + "kind": "LinkedField", + "name": "createControlSnapshotMapping", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "SnapshotEdge", + "kind": "LinkedField", + "name": "snapshotEdge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Snapshot", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "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": "type", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "filters": null, + "handle": "prependEdge", + "key": "", + "kind": "LinkedHandle", + "name": "snapshotEdge", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "52c76e336bb7f38dc2d2f6620817100a", + "id": null, + "metadata": {}, + "name": "FrameworkControlPageAttachSnapshotMutation", + "operationKind": "mutation", + "text": "mutation FrameworkControlPageAttachSnapshotMutation(\n $input: CreateControlSnapshotMappingInput!\n) {\n createControlSnapshotMapping(input: $input) {\n snapshotEdge {\n node {\n id\n ...LinkedSnapshotsCardFragment\n }\n }\n }\n}\n\nfragment LinkedSnapshotsCardFragment on Snapshot {\n id\n name\n description\n type\n createdAt\n}\n" + } +}; +})(); + +(node as any).hash = "9e8ad62cc8d9f3672e1f785c31a91624"; + +export default node; diff --git a/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkControlPageDetachSnapshotMutation.graphql.ts b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkControlPageDetachSnapshotMutation.graphql.ts new file mode 100644 index 000000000..5ea859a02 --- /dev/null +++ b/apps/console/src/pages/organizations/frameworks/__generated__/FrameworkControlPageDetachSnapshotMutation.graphql.ts @@ -0,0 +1,133 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type DeleteControlSnapshotMappingInput = { + controlId: string; + snapshotId: string; +}; +export type FrameworkControlPageDetachSnapshotMutation$variables = { + connections: ReadonlyArray; + input: DeleteControlSnapshotMappingInput; +}; +export type FrameworkControlPageDetachSnapshotMutation$data = { + readonly deleteControlSnapshotMapping: { + readonly deletedSnapshotId: string; + }; +}; +export type FrameworkControlPageDetachSnapshotMutation = { + response: FrameworkControlPageDetachSnapshotMutation$data; + variables: FrameworkControlPageDetachSnapshotMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "connections" +}, +v1 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" +}, +v2 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedSnapshotId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "FrameworkControlPageDetachSnapshotMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteControlSnapshotMappingPayload", + "kind": "LinkedField", + "name": "deleteControlSnapshotMapping", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "FrameworkControlPageDetachSnapshotMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "DeleteControlSnapshotMappingPayload", + "kind": "LinkedField", + "name": "deleteControlSnapshotMapping", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "deletedSnapshotId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "610636745f2c0441c54bd0028fc52cf0", + "id": null, + "metadata": {}, + "name": "FrameworkControlPageDetachSnapshotMutation", + "operationKind": "mutation", + "text": "mutation FrameworkControlPageDetachSnapshotMutation(\n $input: DeleteControlSnapshotMappingInput!\n) {\n deleteControlSnapshotMapping(input: $input) {\n deletedSnapshotId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "be8d40beadabe60d8c9bee4d99b885b7"; + +export default node; diff --git a/apps/console/src/pages/organizations/snapshots/SnapshotDetailPage.tsx b/apps/console/src/pages/organizations/snapshots/SnapshotDetailPage.tsx new file mode 100644 index 000000000..5faf7705d --- /dev/null +++ b/apps/console/src/pages/organizations/snapshots/SnapshotDetailPage.tsx @@ -0,0 +1,39 @@ +import { useEffect } from "react"; +import { useNavigate, useParams } from "react-router"; +import { usePreloadedQuery, type PreloadedQuery } from "react-relay"; +import { snapshotNodeQuery } from "/hooks/graph/SnapshotGraph"; +import type { SnapshotGraphNodeQuery } from "/hooks/graph/__generated__/SnapshotGraphNodeQuery.graphql"; +import { useOrganizationId } from "/hooks/useOrganizationId"; +import { PageError } from "/components/PageError"; +import { getSnapshotTypeUrlPath } from "@probo/helpers"; + +type Props = { + queryRef: PreloadedQuery; +}; + +export default function SnapshotDetailPage({ queryRef }: Props) { + const navigate = useNavigate(); + const organizationId = useOrganizationId(); + const { snapshotId } = useParams(); + const data = usePreloadedQuery(snapshotNodeQuery, queryRef); + + useEffect(() => { + if (!data.node || !data.node.type) { + return; + } + + const snapshot = data.node; + const snapshotType = snapshot.type; + const urlPath = getSnapshotTypeUrlPath(snapshotType); + + navigate(`/organizations/${organizationId}/snapshots/${snapshotId}${urlPath}`, { + replace: true, + }); + }, [data.node, navigate, organizationId, snapshotId]); + + if (!data.node || !data.node.type) { + return ; + } + + return null; +} diff --git a/apps/console/src/pages/organizations/snapshots/SnapshotsPage.tsx b/apps/console/src/pages/organizations/snapshots/SnapshotsPage.tsx index 0f7c88c4d..93b3c1d88 100644 --- a/apps/console/src/pages/organizations/snapshots/SnapshotsPage.tsx +++ b/apps/console/src/pages/organizations/snapshots/SnapshotsPage.tsx @@ -5,9 +5,10 @@ import { type PreloadedQuery, } from "react-relay"; import { useTranslate } from "@probo/i18n"; -import { getSnapshotTypeLabel } from "@probo/helpers"; +import { getSnapshotTypeLabel, getSnapshotTypeUrlPath } from "@probo/helpers"; import { ActionDropdown, + Badge, Button, DropdownItem, IconPlusLarge, @@ -131,28 +132,15 @@ function SnapshotRow(props: SnapshotRowProps) { const { __, dateFormat } = useTranslate(); const deleteSnapshot = useDeleteSnapshot(props.snapshot, props.connectionId); - const getSnapshotUrl = (snapshot: SnapshotRowProps["snapshot"]) => { - const baseUrl = `/organizations/${props.organizationId}/snapshots/${snapshot.id}`; - - switch (snapshot.type) { - case "DATA": - return `${baseUrl}/data`; - case "VENDORS": - return `${baseUrl}/vendors`; - case "RISKS": - return `${baseUrl}/risks`; - case "ASSETS": - return `${baseUrl}/assets`; - default: - return baseUrl; - } - }; + const typePath = getSnapshotTypeUrlPath(props.snapshot.type); return ( - + {props.snapshot.name} - - {getSnapshotTypeLabel(__, props.snapshot.type)} + + + {getSnapshotTypeLabel(__, props.snapshot.type)} + {props.snapshot.description || __("No description")} diff --git a/apps/console/src/routes/snapshotsRoutes.ts b/apps/console/src/routes/snapshotsRoutes.ts index f48256d55..3efed2fc8 100644 --- a/apps/console/src/routes/snapshotsRoutes.ts +++ b/apps/console/src/routes/snapshotsRoutes.ts @@ -1,7 +1,7 @@ import { loadQuery } from "react-relay"; import { PageSkeleton } from "/components/skeletons/PageSkeleton"; import { relayEnvironment } from "/providers/RelayProviders"; -import { snapshotsQuery } from "/hooks/graph/SnapshotGraph"; +import { snapshotsQuery, snapshotNodeQuery } from "/hooks/graph/SnapshotGraph"; import type { AppRoute } from "/routes"; import { lazy } from "@probo/react-lazy"; @@ -15,4 +15,13 @@ export const snapshotsRoutes = [ () => import("/pages/organizations/snapshots/SnapshotsPage") ), }, + { + path: "snapshots/:snapshotId", + fallback: PageSkeleton, + queryLoader: ({ snapshotId }) => + loadQuery(relayEnvironment, snapshotNodeQuery, { snapshotId }), + Component: lazy( + () => import("/pages/organizations/snapshots/SnapshotDetailPage") + ), + }, ] satisfies AppRoute[]; diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index cabfc7d27..a6ee4ebf8 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -15,7 +15,7 @@ export { certificationCategoryLabel, certifications } from "./certifications"; export { availableFrameworks } from "./frameworks"; export { getDocumentTypeLabel, documentTypes } from "./documents"; export { getAssetTypeVariant, getCriticityVariant } from "./assets"; -export { getSnapshotTypeLabel, snapshotTypes } from "./snapshots"; +export { getSnapshotTypeLabel, getSnapshotTypeUrlPath, snapshotTypes } from "./snapshots"; export { getAuditStateLabel, getAuditStateVariant, auditStates } from "./audits"; export { getStatusVariant, getStatusLabel, getNonconformityRegistryStatusOptions, getComplianceRegistryStatusOptions, registryStatuses } from "./registryStatus"; export { promisifyMutation } from "./relay"; diff --git a/packages/helpers/src/snapshots.ts b/packages/helpers/src/snapshots.ts index 210439cde..8993cba46 100644 --- a/packages/helpers/src/snapshots.ts +++ b/packages/helpers/src/snapshots.ts @@ -24,10 +24,19 @@ export function getSnapshotTypeLabel(__: Translator, type: string | null | undef case "DATA": return __("Data"); case "NON_CONFORMITY_REGISTRIES": - return __("Non Conformity Registries"); + return __("Nonconformity Registries"); case "COMPLIANCE_REGISTRIES": return __("Compliance Registries"); default: return __("Unknown"); } } + +export function getSnapshotTypeUrlPath(type?: string): string { + switch (type) { + case "DATA": + return "/data"; + default: + return ""; + } +} diff --git a/pkg/coredata/control.go b/pkg/coredata/control.go index 470ea48d2..0707ff086 100644 --- a/pkg/coredata/control.go +++ b/pkg/coredata/control.go @@ -30,7 +30,6 @@ type ( Control struct { ID gid.GID `db:"id"` SectionTitle string `db:"section_title"` - TenantID gid.TenantID `db:"tenant_id"` FrameworkID gid.GID `db:"framework_id"` Name string `db:"name"` Description string `db:"description"` @@ -137,7 +136,6 @@ SELECT id, section_title, framework_id, - tenant_id, name, description, status, @@ -247,7 +245,6 @@ SELECT id, section_title, framework_id, - tenant_id, name, description, status, @@ -369,7 +366,6 @@ SELECT id, section_title, framework_id, - tenant_id, name, description, status, @@ -449,7 +445,6 @@ SELECT id, section_title, framework_id, - tenant_id, name, description, status, @@ -562,7 +557,6 @@ SELECT id, section_title, framework_id, - tenant_id, name, description, status, @@ -609,7 +603,6 @@ SELECT id, section_title, framework_id, - tenant_id, name, description, status, @@ -654,11 +647,10 @@ SELECT id, section_title, framework_id, - tenant_id, name, description, status, -exclusion_justification, + exclusion_justification, created_at, updated_at FROM @@ -777,7 +769,6 @@ WHERE %s RETURNING id, framework_id, - tenant_id, name, description, section_title, @@ -903,7 +894,6 @@ SELECT id, section_title, framework_id, - tenant_id, name, description, status, @@ -937,3 +927,113 @@ WHERE %s return nil } + +func (c *Controls) CountBySnapshotID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + snapshotID gid.GID, + filter *ControlFilter, +) (int, error) { + q := ` +WITH ctrl AS ( + SELECT + c.id, + c.tenant_id, + c.search_vector + FROM + controls c + INNER JOIN + controls_snapshots cs ON c.id = cs.control_id + WHERE + cs.snapshot_id = @snapshot_id +) +SELECT + COUNT(id) +FROM + ctrl +WHERE %s + AND %s +` + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) + + args := pgx.NamedArgs{"snapshot_id": snapshotID} + maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) + + row := conn.QueryRow(ctx, q, args) + + var count int + if err := row.Scan(&count); err != nil { + return 0, fmt.Errorf("cannot scan count: %w", err) + } + + return count, nil +} + +func (c *Controls) LoadBySnapshotID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + snapshotID gid.GID, + cursor *page.Cursor[ControlOrderField], + filter *ControlFilter, +) error { + q := ` +WITH ctrl AS ( + SELECT + c.id, + c.section_title, + c.framework_id, + c.tenant_id, + c.name, + c.description, + c.status, + c.exclusion_justification, + c.created_at, + c.updated_at, + c.search_vector + FROM + controls c + INNER JOIN + controls_snapshots cs ON c.id = cs.control_id + WHERE + cs.snapshot_id = @snapshot_id +) +SELECT + id, + section_title, + framework_id, + name, + description, + status, + exclusion_justification, + created_at, + updated_at +FROM + ctrl +WHERE %s + AND %s + AND %s +` + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment()) + + args := pgx.NamedArgs{"snapshot_id": snapshotID} + maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) + maps.Copy(args, cursor.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query controls: %w", err) + } + + controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Control]) + if err != nil { + return fmt.Errorf("cannot collect controls: %w", err) + } + + *c = controls + + return nil +} diff --git a/pkg/coredata/control_snapshot.go b/pkg/coredata/control_snapshot.go new file mode 100644 index 000000000..571f854a4 --- /dev/null +++ b/pkg/coredata/control_snapshot.go @@ -0,0 +1,168 @@ +// Copyright (c) 2025 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package coredata + +import ( + "context" + "fmt" + "maps" + "time" + + "github.com/getprobo/probo/pkg/gid" + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" +) + +type ( + ControlSnapshot struct { + ControlID gid.GID `db:"control_id"` + SnapshotID gid.GID `db:"snapshot_id"` + CreatedAt time.Time `db:"created_at"` + } + + ControlSnapshots []*ControlSnapshot +) + +func (cs ControlSnapshot) Upsert( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +INSERT INTO + controls_snapshots ( + control_id, + snapshot_id, + tenant_id, + created_at + ) +VALUES ( + @control_id, + @snapshot_id, + @tenant_id, + @created_at +) +ON CONFLICT (control_id, snapshot_id) DO NOTHING; +` + + args := pgx.StrictNamedArgs{ + "control_id": cs.ControlID, + "snapshot_id": cs.SnapshotID, + "tenant_id": scope.GetTenantID(), + "created_at": cs.CreatedAt, + } + _, err := conn.Exec(ctx, q, args) + return err +} + +func (cs ControlSnapshot) Delete( + ctx context.Context, + conn pg.Conn, + scope Scoper, + controlID gid.GID, + snapshotID gid.GID, +) error { + q := ` +DELETE +FROM + controls_snapshots +WHERE + %s + AND control_id = @control_id + AND snapshot_id = @snapshot_id; +` + + args := pgx.StrictNamedArgs{ + "control_id": controlID, + "snapshot_id": snapshotID, + } + maps.Copy(args, scope.SQLArguments()) + q = fmt.Sprintf(q, scope.SQLFragment()) + + _, err := conn.Exec(ctx, q, args) + return err +} + +func (css *ControlSnapshots) LoadByControlID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + controlID gid.GID, +) error { + q := ` +SELECT + control_id, + snapshot_id, + created_at +FROM + controls_snapshots +WHERE + %s + AND control_id = @control_id +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{"control_id": controlID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query controls_snapshots: %w", err) + } + + controlSnapshots, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlSnapshot]) + if err != nil { + return fmt.Errorf("cannot collect controls_snapshots: %w", err) + } + + *css = controlSnapshots + return nil +} + +func (css *ControlSnapshots) LoadBySnapshotID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + snapshotID gid.GID, +) error { + q := ` +SELECT + control_id, + snapshot_id, + created_at +FROM + controls_snapshots +WHERE + %s + AND snapshot_id = @snapshot_id +` + q = fmt.Sprintf(q, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{"snapshot_id": snapshotID} + maps.Copy(args, scope.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query controls_snapshots: %w", err) + } + + controlSnapshots, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlSnapshot]) + if err != nil { + return fmt.Errorf("cannot collect controls_snapshots: %w", err) + } + + *css = controlSnapshots + return nil +} diff --git a/pkg/coredata/migrations/20250826T174137Z.sql b/pkg/coredata/migrations/20250826T174137Z.sql new file mode 100644 index 000000000..55bbf7185 --- /dev/null +++ b/pkg/coredata/migrations/20250826T174137Z.sql @@ -0,0 +1,15 @@ +CREATE TABLE controls_snapshots ( + control_id TEXT NOT NULL REFERENCES controls(id) ON DELETE CASCADE ON UPDATE CASCADE, + snapshot_id TEXT NOT NULL REFERENCES snapshots(id) ON DELETE CASCADE ON UPDATE CASCADE, + tenant_id TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (control_id, snapshot_id) +); + +ALTER TABLE controls_audits DROP CONSTRAINT controls_audits_control_id_fkey; +ALTER TABLE controls_audits ADD CONSTRAINT controls_audits_control_id_fkey + FOREIGN KEY (control_id) REFERENCES controls(id) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE controls_audits DROP CONSTRAINT controls_audits_audit_id_fkey; +ALTER TABLE controls_audits ADD CONSTRAINT controls_audits_audit_id_fkey + FOREIGN KEY (audit_id) REFERENCES audits(id) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/pkg/coredata/snapshot.go b/pkg/coredata/snapshot.go index 106ca64af..56b9c04cb 100644 --- a/pkg/coredata/snapshot.go +++ b/pkg/coredata/snapshot.go @@ -237,3 +237,60 @@ WHERE return nil } + +func (s *Snapshots) LoadByControlID( + ctx context.Context, + conn pg.Conn, + scope Scoper, + controlID gid.GID, + cursor *page.Cursor[SnapshotOrderField], +) error { + q := ` +WITH snapshots_by_control AS ( + SELECT + s.id, + s.tenant_id, + s.organization_id, + s.name, + s.description, + s.type, + s.created_at + FROM + snapshots s + INNER JOIN + controls_snapshots cs ON s.id = cs.snapshot_id + WHERE + cs.control_id = @control_id +) +SELECT + id, + organization_id, + name, + description, + type, + created_at +FROM + snapshots_by_control +WHERE %s + AND %s +` + q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + + args := pgx.StrictNamedArgs{"control_id": controlID} + maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, cursor.SQLArguments()) + + rows, err := conn.Query(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot query snapshots: %w", err) + } + + snapshots, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Snapshot]) + if err != nil { + return fmt.Errorf("cannot collect snapshots: %w", err) + } + + *s = snapshots + + return nil +} diff --git a/pkg/probo/control_service.go b/pkg/probo/control_service.go index dabe82a14..71408767b 100644 --- a/pkg/probo/control_service.go +++ b/pkg/probo/control_service.go @@ -597,6 +597,111 @@ func (s ControlService) ListForAuditID( return page.NewPage([]*coredata.Control(controls), cursor), nil } +func (s ControlService) CreateSnapshotMapping( + ctx context.Context, + controlID gid.GID, + snapshotID gid.GID, +) (*coredata.Control, *coredata.Snapshot, error) { + controlSnapshot := &coredata.ControlSnapshot{ + ControlID: controlID, + SnapshotID: snapshotID, + CreatedAt: time.Now(), + } + + control := &coredata.Control{} + snapshot := &coredata.Snapshot{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := control.LoadByID(ctx, conn, s.svc.scope, controlID); err != nil { + return fmt.Errorf("cannot load control: %w", err) + } + + if err := snapshot.LoadByID(ctx, conn, s.svc.scope, snapshotID); err != nil { + return fmt.Errorf("cannot load snapshot: %w", err) + } + + if err := controlSnapshot.Upsert(ctx, conn, s.svc.scope); err != nil { + return fmt.Errorf("cannot create control snapshot mapping: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, nil, err + } + + return control, snapshot, nil +} + +func (s ControlService) DeleteSnapshotMapping( + ctx context.Context, + controlID gid.GID, + snapshotID gid.GID, +) (*coredata.Control, *coredata.Snapshot, error) { + control := &coredata.Control{} + snapshot := &coredata.Snapshot{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := control.LoadByID(ctx, conn, s.svc.scope, controlID); err != nil { + return fmt.Errorf("cannot load control: %w", err) + } + + if err := snapshot.LoadByID(ctx, conn, s.svc.scope, snapshotID); err != nil { + return fmt.Errorf("cannot load snapshot: %w", err) + } + + controlSnapshot := &coredata.ControlSnapshot{} + if err := controlSnapshot.Delete(ctx, conn, s.svc.scope, control.ID, snapshot.ID); err != nil { + return fmt.Errorf("cannot delete control snapshot mapping: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, nil, fmt.Errorf("cannot delete control snapshot mapping: %w", err) + } + + return control, snapshot, nil +} + +func (s ControlService) ListForSnapshotID( + ctx context.Context, + snapshotID gid.GID, + cursor *page.Cursor[coredata.ControlOrderField], + filter *coredata.ControlFilter, +) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) { + var controls coredata.Controls + snapshot := &coredata.Snapshot{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := snapshot.LoadByID(ctx, conn, s.svc.scope, snapshotID); err != nil { + return fmt.Errorf("cannot load snapshot: %w", err) + } + if err := controls.LoadBySnapshotID(ctx, conn, s.svc.scope, snapshotID, cursor, filter); err != nil { + return fmt.Errorf("cannot load controls: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage([]*coredata.Control(controls), cursor), nil +} + func (s ControlService) Create( ctx context.Context, req CreateControlRequest, @@ -607,7 +712,6 @@ func (s ControlService) Create( control := &coredata.Control{ ID: gid.New(s.svc.scope.GetTenantID(), coredata.ControlEntityType), FrameworkID: req.FrameworkID, - TenantID: s.svc.scope.GetTenantID(), Name: req.Name, Description: req.Description, SectionTitle: req.SectionTitle, diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go index 082087708..80958a0d6 100644 --- a/pkg/probo/framework_service.go +++ b/pkg/probo/framework_service.go @@ -248,7 +248,6 @@ func (s FrameworkService) Import( now := time.Now() control := &coredata.Control{ ID: controlID, - TenantID: organizationID.TenantID(), FrameworkID: frameworkID, SectionTitle: control.ID, Name: control.Name, diff --git a/pkg/probo/snapshot_service.go b/pkg/probo/snapshot_service.go index b8bd09bbb..d13f16a66 100644 --- a/pkg/probo/snapshot_service.go +++ b/pkg/probo/snapshot_service.go @@ -184,3 +184,34 @@ func (s *SnapshotService) CountForOrganizationID( return count, nil } + +func (s *SnapshotService) ListForControlID( + ctx context.Context, + controlID gid.GID, + cursor *page.Cursor[coredata.SnapshotOrderField], +) (*page.Page[*coredata.Snapshot, coredata.SnapshotOrderField], error) { + var snapshots coredata.Snapshots + control := &coredata.Control{} + + err := s.svc.pg.WithConn( + ctx, + func(conn pg.Conn) error { + if err := control.LoadByID(ctx, conn, s.svc.scope, controlID); err != nil { + return fmt.Errorf("cannot load control: %w", err) + } + + err := snapshots.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor) + if err != nil { + return fmt.Errorf("cannot load snapshots: %w", err) + } + + return nil + }, + ) + + if err != nil { + return nil, err + } + + return page.NewPage(snapshots, cursor), nil +} diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 222cae55f..283f94e02 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1300,6 +1300,14 @@ type Control implements Node { orderBy: AuditOrder ): AuditConnection! @goField(forceResolver: true) + snapshots( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: SnapshotOrder + ): SnapshotConnection! @goField(forceResolver: true) + createdAt: Datetime! updatedAt: Datetime! } @@ -1537,6 +1545,16 @@ type Snapshot implements Node { name: String! description: String type: SnapshotsType! + + controls( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: ControlOrder + filter: ControlFilter + ): ControlConnection! @goField(forceResolver: true) + createdAt: Datetime! } @@ -1982,6 +2000,12 @@ type Mutation { deleteControlAuditMapping( input: DeleteControlAuditMappingInput! ): DeleteControlAuditMappingPayload! + createControlSnapshotMapping( + input: CreateControlSnapshotMappingInput! + ): CreateControlSnapshotMappingPayload! + deleteControlSnapshotMapping( + input: DeleteControlSnapshotMappingInput! + ): DeleteControlSnapshotMappingPayload! # Task mutations createTask(input: CreateTaskInput!): CreateTaskPayload! @@ -2396,6 +2420,16 @@ input DeleteControlAuditMappingInput { auditId: ID! } +input CreateControlSnapshotMappingInput { + controlId: ID! + snapshotId: ID! +} + +input DeleteControlSnapshotMappingInput { + controlId: ID! + snapshotId: ID! +} + input CreateRiskInput { organizationId: ID! name: String! @@ -2865,6 +2899,16 @@ type DeleteControlAuditMappingPayload { deletedAuditId: ID! } +type CreateControlSnapshotMappingPayload { + controlEdge: ControlEdge! + snapshotEdge: SnapshotEdge! +} + +type DeleteControlSnapshotMappingPayload { + deletedControlId: ID! + deletedSnapshotId: ID! +} + type CreateRiskPayload { riskEdge: RiskEdge! } diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 1fffc08fb..944c3fd97 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -229,6 +229,7 @@ type ComplexityRoot struct { Measures func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) int Name func(childComplexity int) int SectionTitle func(childComplexity int) int + Snapshots func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SnapshotOrderBy) int Status func(childComplexity int) int UpdatedAt func(childComplexity int) int } @@ -275,6 +276,11 @@ type ComplexityRoot struct { ControlEdge func(childComplexity int) int } + CreateControlSnapshotMappingPayload struct { + ControlEdge func(childComplexity int) int + SnapshotEdge func(childComplexity int) int + } + CreateDatumPayload struct { DatumEdge func(childComplexity int) int } @@ -411,6 +417,11 @@ type ComplexityRoot struct { DeletedControlID func(childComplexity int) int } + DeleteControlSnapshotMappingPayload struct { + DeletedControlID func(childComplexity int) int + DeletedSnapshotID func(childComplexity int) int + } + DeleteDatumPayload struct { DeletedDatumID func(childComplexity int) int } @@ -686,6 +697,7 @@ type ComplexityRoot struct { CreateControlAuditMapping func(childComplexity int, input types.CreateControlAuditMappingInput) int CreateControlDocumentMapping func(childComplexity int, input types.CreateControlDocumentMappingInput) int CreateControlMeasureMapping func(childComplexity int, input types.CreateControlMeasureMappingInput) int + CreateControlSnapshotMapping func(childComplexity int, input types.CreateControlSnapshotMappingInput) int CreateDatum func(childComplexity int, input types.CreateDatumInput) int CreateDocument func(childComplexity int, input types.CreateDocumentInput) int CreateDraftDocumentVersion func(childComplexity int, input types.CreateDraftDocumentVersionInput) int @@ -712,6 +724,7 @@ type ComplexityRoot struct { DeleteControlAuditMapping func(childComplexity int, input types.DeleteControlAuditMappingInput) int DeleteControlDocumentMapping func(childComplexity int, input types.DeleteControlDocumentMappingInput) int DeleteControlMeasureMapping func(childComplexity int, input types.DeleteControlMeasureMappingInput) int + DeleteControlSnapshotMapping func(childComplexity int, input types.DeleteControlSnapshotMappingInput) int DeleteDatum func(childComplexity int, input types.DeleteDatumInput) int DeleteDocument func(childComplexity int, input types.DeleteDocumentInput) int DeleteDraftDocumentVersion func(childComplexity int, input types.DeleteDraftDocumentVersionInput) int @@ -946,6 +959,7 @@ type ComplexityRoot struct { } Snapshot struct { + Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) int CreatedAt func(childComplexity int) int Description func(childComplexity int) int ID func(childComplexity int) int @@ -1358,6 +1372,7 @@ type ControlResolver interface { Measures(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) (*types.MeasureConnection, error) Documents(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy, filter *types.DocumentFilter) (*types.DocumentConnection, error) Audits(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AuditOrderBy) (*types.AuditConnection, error) + Snapshots(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SnapshotOrderBy) (*types.SnapshotConnection, error) } type ControlConnectionResolver interface { TotalCount(ctx context.Context, obj *types.ControlConnection) (int, error) @@ -1459,6 +1474,8 @@ type MutationResolver interface { DeleteControlDocumentMapping(ctx context.Context, input types.DeleteControlDocumentMappingInput) (*types.DeleteControlDocumentMappingPayload, error) CreateControlAuditMapping(ctx context.Context, input types.CreateControlAuditMappingInput) (*types.CreateControlAuditMappingPayload, error) DeleteControlAuditMapping(ctx context.Context, input types.DeleteControlAuditMappingInput) (*types.DeleteControlAuditMappingPayload, error) + CreateControlSnapshotMapping(ctx context.Context, input types.CreateControlSnapshotMappingInput) (*types.CreateControlSnapshotMappingPayload, error) + DeleteControlSnapshotMapping(ctx context.Context, input types.DeleteControlSnapshotMappingInput) (*types.DeleteControlSnapshotMappingPayload, error) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) UpdateTask(ctx context.Context, input types.UpdateTaskInput) (*types.UpdateTaskPayload, error) DeleteTask(ctx context.Context, input types.DeleteTaskInput) (*types.DeleteTaskPayload, error) @@ -1573,6 +1590,8 @@ type RiskConnectionResolver interface { } type SnapshotResolver interface { Organization(ctx context.Context, obj *types.Snapshot) (*types.Organization, error) + + Controls(ctx context.Context, obj *types.Snapshot, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) } type SnapshotConnectionResolver interface { TotalCount(ctx context.Context, obj *types.SnapshotConnection) (int, error) @@ -2241,6 +2260,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Control.SectionTitle(childComplexity), true + case "Control.snapshots": + if e.complexity.Control.Snapshots == nil { + break + } + + args, err := ec.field_Control_snapshots_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Control.Snapshots(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.SnapshotOrderBy)), true + case "Control.status": if e.complexity.Control.Status == nil { break @@ -2360,6 +2391,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.CreateControlPayload.ControlEdge(childComplexity), true + case "CreateControlSnapshotMappingPayload.controlEdge": + if e.complexity.CreateControlSnapshotMappingPayload.ControlEdge == nil { + break + } + + return e.complexity.CreateControlSnapshotMappingPayload.ControlEdge(childComplexity), true + + case "CreateControlSnapshotMappingPayload.snapshotEdge": + if e.complexity.CreateControlSnapshotMappingPayload.SnapshotEdge == nil { + break + } + + return e.complexity.CreateControlSnapshotMappingPayload.SnapshotEdge(childComplexity), true + case "CreateDatumPayload.datumEdge": if e.complexity.CreateDatumPayload.DatumEdge == nil { break @@ -2687,6 +2732,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.DeleteControlPayload.DeletedControlID(childComplexity), true + case "DeleteControlSnapshotMappingPayload.deletedControlId": + if e.complexity.DeleteControlSnapshotMappingPayload.DeletedControlID == nil { + break + } + + return e.complexity.DeleteControlSnapshotMappingPayload.DeletedControlID(childComplexity), true + + case "DeleteControlSnapshotMappingPayload.deletedSnapshotId": + if e.complexity.DeleteControlSnapshotMappingPayload.DeletedSnapshotID == nil { + break + } + + return e.complexity.DeleteControlSnapshotMappingPayload.DeletedSnapshotID(childComplexity), true + case "DeleteDatumPayload.deletedDatumId": if e.complexity.DeleteDatumPayload.DeletedDatumID == nil { break @@ -3744,6 +3803,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Mutation.CreateControlMeasureMapping(childComplexity, args["input"].(types.CreateControlMeasureMappingInput)), true + case "Mutation.createControlSnapshotMapping": + if e.complexity.Mutation.CreateControlSnapshotMapping == nil { + break + } + + args, err := ec.field_Mutation_createControlSnapshotMapping_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateControlSnapshotMapping(childComplexity, args["input"].(types.CreateControlSnapshotMappingInput)), true + case "Mutation.createDatum": if e.complexity.Mutation.CreateDatum == nil { break @@ -4056,6 +4127,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Mutation.DeleteControlMeasureMapping(childComplexity, args["input"].(types.DeleteControlMeasureMappingInput)), true + case "Mutation.deleteControlSnapshotMapping": + if e.complexity.Mutation.DeleteControlSnapshotMapping == nil { + break + } + + args, err := ec.field_Mutation_deleteControlSnapshotMapping_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteControlSnapshotMapping(childComplexity, args["input"].(types.DeleteControlSnapshotMappingInput)), true + case "Mutation.deleteDatum": if e.complexity.Mutation.DeleteDatum == nil { break @@ -5642,6 +5725,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.Session.ID(childComplexity), true + case "Snapshot.controls": + if e.complexity.Snapshot.Controls == nil { + break + } + + args, err := ec.field_Snapshot_controls_args(ctx, rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Snapshot.Controls(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.ControlOrderBy), args["filter"].(*types.ControlFilter)), true + case "Snapshot.createdAt": if e.complexity.Snapshot.CreatedAt == nil { break @@ -7046,6 +7141,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputCreateControlDocumentMappingInput, ec.unmarshalInputCreateControlInput, ec.unmarshalInputCreateControlMeasureMappingInput, + ec.unmarshalInputCreateControlSnapshotMappingInput, ec.unmarshalInputCreateDatumInput, ec.unmarshalInputCreateDocumentInput, ec.unmarshalInputCreateDraftDocumentVersionInput, @@ -7075,6 +7171,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputDeleteControlDocumentMappingInput, ec.unmarshalInputDeleteControlInput, ec.unmarshalInputDeleteControlMeasureMappingInput, + ec.unmarshalInputDeleteControlSnapshotMappingInput, ec.unmarshalInputDeleteDatumInput, ec.unmarshalInputDeleteDocumentInput, ec.unmarshalInputDeleteDraftDocumentVersionInput, @@ -8561,6 +8658,14 @@ type Control implements Node { orderBy: AuditOrder ): AuditConnection! @goField(forceResolver: true) + snapshots( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: SnapshotOrder + ): SnapshotConnection! @goField(forceResolver: true) + createdAt: Datetime! updatedAt: Datetime! } @@ -8798,6 +8903,16 @@ type Snapshot implements Node { name: String! description: String type: SnapshotsType! + + controls( + first: Int + after: CursorKey + last: Int + before: CursorKey + orderBy: ControlOrder + filter: ControlFilter + ): ControlConnection! @goField(forceResolver: true) + createdAt: Datetime! } @@ -9243,6 +9358,12 @@ type Mutation { deleteControlAuditMapping( input: DeleteControlAuditMappingInput! ): DeleteControlAuditMappingPayload! + createControlSnapshotMapping( + input: CreateControlSnapshotMappingInput! + ): CreateControlSnapshotMappingPayload! + deleteControlSnapshotMapping( + input: DeleteControlSnapshotMappingInput! + ): DeleteControlSnapshotMappingPayload! # Task mutations createTask(input: CreateTaskInput!): CreateTaskPayload! @@ -9657,6 +9778,16 @@ input DeleteControlAuditMappingInput { auditId: ID! } +input CreateControlSnapshotMappingInput { + controlId: ID! + snapshotId: ID! +} + +input DeleteControlSnapshotMappingInput { + controlId: ID! + snapshotId: ID! +} + input CreateRiskInput { organizationId: ID! name: String! @@ -10126,6 +10257,16 @@ type DeleteControlAuditMappingPayload { deletedAuditId: ID! } +type CreateControlSnapshotMappingPayload { + controlEdge: ControlEdge! + snapshotEdge: SnapshotEdge! +} + +type DeleteControlSnapshotMappingPayload { + deletedControlId: ID! + deletedSnapshotId: ID! +} + type CreateRiskPayload { riskEdge: RiskEdge! } @@ -11211,6 +11352,101 @@ func (ec *executionContext) field_Control_measures_argsFilter( return zeroVal, nil } +func (ec *executionContext) field_Control_snapshots_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Control_snapshots_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_Control_snapshots_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_Control_snapshots_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_Control_snapshots_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + arg4, err := ec.field_Control_snapshots_argsOrderBy(ctx, rawArgs) + if err != nil { + return nil, err + } + args["orderBy"] = arg4 + return args, nil +} +func (ec *executionContext) field_Control_snapshots_argsFirst( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + if tmp, ok := rawArgs["first"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Control_snapshots_argsAfter( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + if tmp, ok := rawArgs["after"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Control_snapshots_argsLast( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) + if tmp, ok := rawArgs["last"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Control_snapshots_argsBefore( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + if tmp, ok := rawArgs["before"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Control_snapshots_argsOrderBy( + ctx context.Context, + rawArgs map[string]any, +) (*types.SnapshotOrderBy, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("orderBy")) + if tmp, ok := rawArgs["orderBy"]; ok { + return ec.unmarshalOSnapshotOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSnapshotOrderBy(ctx, tmp) + } + + var zeroVal *types.SnapshotOrderBy + return zeroVal, nil +} + func (ec *executionContext) field_Datum_vendors_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -12432,6 +12668,29 @@ func (ec *executionContext) field_Mutation_createControlMeasureMapping_argsInput return zeroVal, nil } +func (ec *executionContext) field_Mutation_createControlSnapshotMapping_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_createControlSnapshotMapping_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_createControlSnapshotMapping_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (types.CreateControlSnapshotMappingInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNCreateControlSnapshotMappingInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlSnapshotMappingInput(ctx, tmp) + } + + var zeroVal types.CreateControlSnapshotMappingInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_createControl_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -13030,6 +13289,29 @@ func (ec *executionContext) field_Mutation_deleteControlMeasureMapping_argsInput return zeroVal, nil } +func (ec *executionContext) field_Mutation_deleteControlSnapshotMapping_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_deleteControlSnapshotMapping_argsInput(ctx, rawArgs) + if err != nil { + return nil, err + } + args["input"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_deleteControlSnapshotMapping_argsInput( + ctx context.Context, + rawArgs map[string]any, +) (types.DeleteControlSnapshotMappingInput, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + if tmp, ok := rawArgs["input"]; ok { + return ec.unmarshalNDeleteControlSnapshotMappingInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteControlSnapshotMappingInput(ctx, tmp) + } + + var zeroVal types.DeleteControlSnapshotMappingInput + return zeroVal, nil +} + func (ec *executionContext) field_Mutation_deleteControl_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -16564,6 +16846,119 @@ func (ec *executionContext) field_Risk_measures_argsFilter( return zeroVal, nil } +func (ec *executionContext) field_Snapshot_controls_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Snapshot_controls_argsFirst(ctx, rawArgs) + if err != nil { + return nil, err + } + args["first"] = arg0 + arg1, err := ec.field_Snapshot_controls_argsAfter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["after"] = arg1 + arg2, err := ec.field_Snapshot_controls_argsLast(ctx, rawArgs) + if err != nil { + return nil, err + } + args["last"] = arg2 + arg3, err := ec.field_Snapshot_controls_argsBefore(ctx, rawArgs) + if err != nil { + return nil, err + } + args["before"] = arg3 + arg4, err := ec.field_Snapshot_controls_argsOrderBy(ctx, rawArgs) + if err != nil { + return nil, err + } + args["orderBy"] = arg4 + arg5, err := ec.field_Snapshot_controls_argsFilter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["filter"] = arg5 + return args, nil +} +func (ec *executionContext) field_Snapshot_controls_argsFirst( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("first")) + if tmp, ok := rawArgs["first"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Snapshot_controls_argsAfter( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("after")) + if tmp, ok := rawArgs["after"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Snapshot_controls_argsLast( + ctx context.Context, + rawArgs map[string]any, +) (*int, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("last")) + if tmp, ok := rawArgs["last"]; ok { + return ec.unmarshalOInt2ᚖint(ctx, tmp) + } + + var zeroVal *int + return zeroVal, nil +} + +func (ec *executionContext) field_Snapshot_controls_argsBefore( + ctx context.Context, + rawArgs map[string]any, +) (*page.CursorKey, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("before")) + if tmp, ok := rawArgs["before"]; ok { + return ec.unmarshalOCursorKey2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋpageᚐCursorKey(ctx, tmp) + } + + var zeroVal *page.CursorKey + return zeroVal, nil +} + +func (ec *executionContext) field_Snapshot_controls_argsOrderBy( + ctx context.Context, + rawArgs map[string]any, +) (*types.ControlOrderBy, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("orderBy")) + if tmp, ok := rawArgs["orderBy"]; ok { + return ec.unmarshalOControlOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐControlOrderBy(ctx, tmp) + } + + var zeroVal *types.ControlOrderBy + return zeroVal, nil +} + +func (ec *executionContext) field_Snapshot_controls_argsFilter( + ctx context.Context, + rawArgs map[string]any, +) (*types.ControlFilter, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("filter")) + if tmp, ok := rawArgs["filter"]; ok { + return ec.unmarshalOControlFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐControlFilter(ctx, tmp) + } + + var zeroVal *types.ControlFilter + return zeroVal, nil +} + func (ec *executionContext) field_Task_evidences_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -21470,6 +21865,69 @@ func (ec *executionContext) fieldContext_Control_audits(ctx context.Context, fie return fc, nil } +func (ec *executionContext) _Control_snapshots(ctx context.Context, field graphql.CollectedField, obj *types.Control) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Control_snapshots(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Control().Snapshots(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.SnapshotOrderBy)) + }) + 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.SnapshotConnection) + fc.Result = res + return ec.marshalNSnapshotConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSnapshotConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Control_snapshots(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Control", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "totalCount": + return ec.fieldContext_SnapshotConnection_totalCount(ctx, field) + case "edges": + return ec.fieldContext_SnapshotConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_SnapshotConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SnapshotConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Control_snapshots_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Control_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Control) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Control_createdAt(ctx, field) if err != nil { @@ -21809,6 +22267,8 @@ func (ec *executionContext) fieldContext_ControlEdge_node(_ context.Context, fie return ec.fieldContext_Control_documents(ctx, field) case "audits": return ec.fieldContext_Control_audits(ctx, field) + case "snapshots": + return ec.fieldContext_Control_snapshots(ctx, field) case "createdAt": return ec.fieldContext_Control_createdAt(ctx, field) case "updatedAt": @@ -22320,6 +22780,106 @@ func (ec *executionContext) fieldContext_CreateControlPayload_controlEdge(_ cont return fc, nil } +func (ec *executionContext) _CreateControlSnapshotMappingPayload_controlEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateControlSnapshotMappingPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CreateControlSnapshotMappingPayload_controlEdge(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.ControlEdge, 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.ControlEdge) + fc.Result = res + return ec.marshalNControlEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐControlEdge(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CreateControlSnapshotMappingPayload_controlEdge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CreateControlSnapshotMappingPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_ControlEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_ControlEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ControlEdge", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _CreateControlSnapshotMappingPayload_snapshotEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateControlSnapshotMappingPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CreateControlSnapshotMappingPayload_snapshotEdge(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.SnapshotEdge, 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.SnapshotEdge) + fc.Result = res + return ec.marshalNSnapshotEdge2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSnapshotEdge(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CreateControlSnapshotMappingPayload_snapshotEdge(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CreateControlSnapshotMappingPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "cursor": + return ec.fieldContext_SnapshotEdge_cursor(ctx, field) + case "node": + return ec.fieldContext_SnapshotEdge_node(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type SnapshotEdge", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _CreateDatumPayload_datumEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateDatumPayload) (ret graphql.Marshaler) { fc, err := ec.fieldContext_CreateDatumPayload_datumEdge(ctx, field) if err != nil { @@ -24625,6 +25185,94 @@ func (ec *executionContext) fieldContext_DeleteControlPayload_deletedControlId(_ return fc, nil } +func (ec *executionContext) _DeleteControlSnapshotMappingPayload_deletedControlId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteControlSnapshotMappingPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DeleteControlSnapshotMappingPayload_deletedControlId(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DeletedControlID, 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.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DeleteControlSnapshotMappingPayload_deletedControlId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DeleteControlSnapshotMappingPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _DeleteControlSnapshotMappingPayload_deletedSnapshotId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteControlSnapshotMappingPayload) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DeleteControlSnapshotMappingPayload_deletedSnapshotId(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.DeletedSnapshotID, 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.(gid.GID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DeleteControlSnapshotMappingPayload_deletedSnapshotId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DeleteControlSnapshotMappingPayload", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _DeleteDatumPayload_deletedDatumId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteDatumPayload) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DeleteDatumPayload_deletedDatumId(ctx, field) if err != nil { @@ -33098,6 +33746,128 @@ func (ec *executionContext) fieldContext_Mutation_deleteControlAuditMapping(ctx return fc, nil } +func (ec *executionContext) _Mutation_createControlSnapshotMapping(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createControlSnapshotMapping(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateControlSnapshotMapping(rctx, fc.Args["input"].(types.CreateControlSnapshotMappingInput)) + }) + 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.CreateControlSnapshotMappingPayload) + fc.Result = res + return ec.marshalNCreateControlSnapshotMappingPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlSnapshotMappingPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createControlSnapshotMapping(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 "controlEdge": + return ec.fieldContext_CreateControlSnapshotMappingPayload_controlEdge(ctx, field) + case "snapshotEdge": + return ec.fieldContext_CreateControlSnapshotMappingPayload_snapshotEdge(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type CreateControlSnapshotMappingPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createControlSnapshotMapping_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_deleteControlSnapshotMapping(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteControlSnapshotMapping(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteControlSnapshotMapping(rctx, fc.Args["input"].(types.DeleteControlSnapshotMappingInput)) + }) + 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.DeleteControlSnapshotMappingPayload) + fc.Result = res + return ec.marshalNDeleteControlSnapshotMappingPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteControlSnapshotMappingPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_deleteControlSnapshotMapping(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 "deletedControlId": + return ec.fieldContext_DeleteControlSnapshotMappingPayload_deletedControlId(ctx, field) + case "deletedSnapshotId": + return ec.fieldContext_DeleteControlSnapshotMappingPayload_deletedSnapshotId(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DeleteControlSnapshotMappingPayload", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteControlSnapshotMapping_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Mutation_createTask(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_createTask(ctx, field) if err != nil { @@ -42553,6 +43323,69 @@ func (ec *executionContext) fieldContext_Snapshot_type(_ context.Context, field return fc, nil } +func (ec *executionContext) _Snapshot_controls(ctx context.Context, field graphql.CollectedField, obj *types.Snapshot) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Snapshot_controls(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Snapshot().Controls(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.ControlOrderBy), fc.Args["filter"].(*types.ControlFilter)) + }) + 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.ControlConnection) + fc.Result = res + return ec.marshalNControlConnection2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐControlConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Snapshot_controls(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Snapshot", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "totalCount": + return ec.fieldContext_ControlConnection_totalCount(ctx, field) + case "edges": + return ec.fieldContext_ControlConnection_edges(ctx, field) + case "pageInfo": + return ec.fieldContext_ControlConnection_pageInfo(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ControlConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Snapshot_controls_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Snapshot_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Snapshot) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Snapshot_createdAt(ctx, field) if err != nil { @@ -42838,6 +43671,8 @@ func (ec *executionContext) fieldContext_SnapshotEdge_node(_ context.Context, fi return ec.fieldContext_Snapshot_description(ctx, field) case "type": return ec.fieldContext_Snapshot_type(ctx, field) + case "controls": + return ec.fieldContext_Snapshot_controls(ctx, field) case "createdAt": return ec.fieldContext_Snapshot_createdAt(ctx, field) } @@ -45130,6 +45965,8 @@ func (ec *executionContext) fieldContext_UpdateControlPayload_control(_ context. return ec.fieldContext_Control_documents(ctx, field) case "audits": return ec.fieldContext_Control_audits(ctx, field) + case "snapshots": + return ec.fieldContext_Control_snapshots(ctx, field) case "createdAt": return ec.fieldContext_Control_createdAt(ctx, field) case "updatedAt": @@ -55242,6 +56079,40 @@ func (ec *executionContext) unmarshalInputCreateControlMeasureMappingInput(ctx c return it, nil } +func (ec *executionContext) unmarshalInputCreateControlSnapshotMappingInput(ctx context.Context, obj any) (types.CreateControlSnapshotMappingInput, error) { + var it types.CreateControlSnapshotMappingInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"controlId", "snapshotId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "controlId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("controlId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.ControlID = data + case "snapshotId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("snapshotId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.SnapshotID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputCreateDatumInput(ctx context.Context, obj any) (types.CreateDatumInput, error) { var it types.CreateDatumInput asMap := map[string]any{} @@ -56683,6 +57554,40 @@ func (ec *executionContext) unmarshalInputDeleteControlMeasureMappingInput(ctx c return it, nil } +func (ec *executionContext) unmarshalInputDeleteControlSnapshotMappingInput(ctx context.Context, obj any) (types.DeleteControlSnapshotMappingInput, error) { + var it types.DeleteControlSnapshotMappingInput + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"controlId", "snapshotId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "controlId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("controlId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.ControlID = data + case "snapshotId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("snapshotId")) + data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.SnapshotID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDeleteDatumInput(ctx context.Context, obj any) (types.DeleteDatumInput, error) { var it types.DeleteDatumInput asMap := map[string]any{} @@ -61965,6 +62870,42 @@ func (ec *executionContext) _Control(ctx context.Context, sel ast.SelectionSet, continue } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "snapshots": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Control_snapshots(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "createdAt": out.Values[i] = ec._Control_createdAt(ctx, field, obj) @@ -62411,6 +63352,50 @@ func (ec *executionContext) _CreateControlPayload(ctx context.Context, sel ast.S return out } +var createControlSnapshotMappingPayloadImplementors = []string{"CreateControlSnapshotMappingPayload"} + +func (ec *executionContext) _CreateControlSnapshotMappingPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateControlSnapshotMappingPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, createControlSnapshotMappingPayloadImplementors) + + 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("CreateControlSnapshotMappingPayload") + case "controlEdge": + out.Values[i] = ec._CreateControlSnapshotMappingPayload_controlEdge(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "snapshotEdge": + out.Values[i] = ec._CreateControlSnapshotMappingPayload_snapshotEdge(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 createDatumPayloadImplementors = []string{"CreateDatumPayload"} func (ec *executionContext) _CreateDatumPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateDatumPayload) graphql.Marshaler { @@ -63785,6 +64770,50 @@ func (ec *executionContext) _DeleteControlPayload(ctx context.Context, sel ast.S return out } +var deleteControlSnapshotMappingPayloadImplementors = []string{"DeleteControlSnapshotMappingPayload"} + +func (ec *executionContext) _DeleteControlSnapshotMappingPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteControlSnapshotMappingPayload) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, deleteControlSnapshotMappingPayloadImplementors) + + 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("DeleteControlSnapshotMappingPayload") + case "deletedControlId": + out.Values[i] = ec._DeleteControlSnapshotMappingPayload_deletedControlId(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deletedSnapshotId": + out.Values[i] = ec._DeleteControlSnapshotMappingPayload_deletedSnapshotId(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 deleteDatumPayloadImplementors = []string{"DeleteDatumPayload"} func (ec *executionContext) _DeleteDatumPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteDatumPayload) graphql.Marshaler { @@ -66986,6 +68015,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "createControlSnapshotMapping": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createControlSnapshotMapping(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deleteControlSnapshotMapping": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteControlSnapshotMapping(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } case "createTask": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createTask(ctx, field) @@ -69697,6 +70740,42 @@ func (ec *executionContext) _Snapshot(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "controls": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Snapshot_controls(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "createdAt": out.Values[i] = ec._Snapshot_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -74817,6 +75896,25 @@ func (ec *executionContext) marshalNCreateControlPayload2ᚖgithubᚗcomᚋgetpr return ec._CreateControlPayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNCreateControlSnapshotMappingInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlSnapshotMappingInput(ctx context.Context, v any) (types.CreateControlSnapshotMappingInput, error) { + res, err := ec.unmarshalInputCreateControlSnapshotMappingInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNCreateControlSnapshotMappingPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlSnapshotMappingPayload(ctx context.Context, sel ast.SelectionSet, v types.CreateControlSnapshotMappingPayload) graphql.Marshaler { + return ec._CreateControlSnapshotMappingPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNCreateControlSnapshotMappingPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateControlSnapshotMappingPayload(ctx context.Context, sel ast.SelectionSet, v *types.CreateControlSnapshotMappingPayload) 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._CreateControlSnapshotMappingPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNCreateDatumInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateDatumInput(ctx context.Context, v any) (types.CreateDatumInput, error) { res, err := ec.unmarshalInputCreateDatumInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -75547,6 +76645,25 @@ func (ec *executionContext) marshalNDeleteControlPayload2ᚖgithubᚗcomᚋgetpr return ec._DeleteControlPayload(ctx, sel, v) } +func (ec *executionContext) unmarshalNDeleteControlSnapshotMappingInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteControlSnapshotMappingInput(ctx context.Context, v any) (types.DeleteControlSnapshotMappingInput, error) { + res, err := ec.unmarshalInputDeleteControlSnapshotMappingInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNDeleteControlSnapshotMappingPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteControlSnapshotMappingPayload(ctx context.Context, sel ast.SelectionSet, v types.DeleteControlSnapshotMappingPayload) graphql.Marshaler { + return ec._DeleteControlSnapshotMappingPayload(ctx, sel, &v) +} + +func (ec *executionContext) marshalNDeleteControlSnapshotMappingPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteControlSnapshotMappingPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteControlSnapshotMappingPayload) 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._DeleteControlSnapshotMappingPayload(ctx, sel, v) +} + func (ec *executionContext) unmarshalNDeleteDatumInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteDatumInput(ctx context.Context, v any) (types.DeleteDatumInput, error) { res, err := ec.unmarshalInputDeleteDatumInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index e5cb52c49..8399e7916 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -178,6 +178,7 @@ type Control struct { Measures *MeasureConnection `json:"measures"` Documents *DocumentConnection `json:"documents"` Audits *AuditConnection `json:"audits"` + Snapshots *SnapshotConnection `json:"snapshots"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } @@ -284,6 +285,16 @@ type CreateControlPayload struct { ControlEdge *ControlEdge `json:"controlEdge"` } +type CreateControlSnapshotMappingInput struct { + ControlID gid.GID `json:"controlId"` + SnapshotID gid.GID `json:"snapshotId"` +} + +type CreateControlSnapshotMappingPayload struct { + ControlEdge *ControlEdge `json:"controlEdge"` + SnapshotEdge *SnapshotEdge `json:"snapshotEdge"` +} + type CreateDatumInput struct { OrganizationID gid.GID `json:"organizationId"` Name string `json:"name"` @@ -622,6 +633,16 @@ type DeleteControlPayload struct { DeletedControlID gid.GID `json:"deletedControlId"` } +type DeleteControlSnapshotMappingInput struct { + ControlID gid.GID `json:"controlId"` + SnapshotID gid.GID `json:"snapshotId"` +} + +type DeleteControlSnapshotMappingPayload struct { + DeletedControlID gid.GID `json:"deletedControlId"` + DeletedSnapshotID gid.GID `json:"deletedSnapshotId"` +} + type DeleteDatumInput struct { DatumID gid.GID `json:"datumId"` } @@ -1231,6 +1252,7 @@ type Snapshot struct { Name string `json:"name"` Description *string `json:"description,omitempty"` Type coredata.SnapshotsType `json:"type"` + Controls *ControlConnection `json:"controls"` CreatedAt time.Time `json:"createdAt"` } diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 9bf67cd5e..1a42fe9bf 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -389,6 +389,32 @@ func (r *controlResolver) Audits(ctx context.Context, obj *types.Control, first return types.NewAuditConnection(page, r, obj.ID), nil } +// Snapshots is the resolver for the snapshots field. +func (r *controlResolver) Snapshots(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SnapshotOrderBy) (*types.SnapshotConnection, error) { + prb := r.ProboService(ctx, obj.ID.TenantID()) + + pageOrderBy := page.OrderBy[coredata.SnapshotOrderField]{ + Field: coredata.SnapshotOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + + if orderBy != nil { + pageOrderBy = page.OrderBy[coredata.SnapshotOrderField]{ + Field: orderBy.Field, + Direction: orderBy.Direction, + } + } + + cursor := types.NewCursor(first, after, last, before, pageOrderBy) + + page, err := prb.Snapshots.ListForControlID(ctx, obj.ID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list control snapshots: %w", err)) + } + + return types.NewSnapshotConnection(page, r, obj.ID), nil +} + // TotalCount is the resolver for the totalCount field. func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.ControlConnection) (int, error) { prb := r.ProboService(ctx, obj.ParentID.TenantID()) @@ -1823,6 +1849,36 @@ func (r *mutationResolver) DeleteControlAuditMapping(ctx context.Context, input }, nil } +// CreateControlSnapshotMapping is the resolver for the createControlSnapshotMapping field. +func (r *mutationResolver) CreateControlSnapshotMapping(ctx context.Context, input types.CreateControlSnapshotMappingInput) (*types.CreateControlSnapshotMappingPayload, error) { + prb := r.ProboService(ctx, input.SnapshotID.TenantID()) + + control, snapshot, err := prb.Controls.CreateSnapshotMapping(ctx, input.ControlID, input.SnapshotID) + if err != nil { + panic(fmt.Errorf("cannot create control snapshot mapping: %w", err)) + } + + return &types.CreateControlSnapshotMappingPayload{ + ControlEdge: types.NewControlEdge(control, coredata.ControlOrderFieldCreatedAt), + SnapshotEdge: types.NewSnapshotEdge(snapshot, coredata.SnapshotOrderFieldCreatedAt), + }, nil +} + +// DeleteControlSnapshotMapping is the resolver for the deleteControlSnapshotMapping field. +func (r *mutationResolver) DeleteControlSnapshotMapping(ctx context.Context, input types.DeleteControlSnapshotMappingInput) (*types.DeleteControlSnapshotMappingPayload, error) { + prb := r.ProboService(ctx, input.SnapshotID.TenantID()) + + control, snapshot, err := prb.Controls.DeleteSnapshotMapping(ctx, input.ControlID, input.SnapshotID) + if err != nil { + panic(fmt.Errorf("cannot delete control snapshot mapping: %w", err)) + } + + return &types.DeleteControlSnapshotMappingPayload{ + DeletedControlID: control.ID, + DeletedSnapshotID: snapshot.ID, + }, nil +} + // CreateTask is the resolver for the createTask field. func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) { prb := r.ProboService(ctx, input.MeasureID.TenantID()) @@ -3910,6 +3966,36 @@ func (r *snapshotResolver) Organization(ctx context.Context, obj *types.Snapshot return types.NewOrganization(organization), nil } +// Controls is the resolver for the controls field. +func (r *snapshotResolver) Controls(ctx context.Context, obj *types.Snapshot, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) { + prb := r.ProboService(ctx, obj.ID.TenantID()) + + pageOrderBy := page.OrderBy[coredata.ControlOrderField]{ + Field: coredata.ControlOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if orderBy != nil { + pageOrderBy = page.OrderBy[coredata.ControlOrderField]{ + Field: orderBy.Field, + Direction: orderBy.Direction, + } + } + + cursor := types.NewCursor(first, after, last, before, pageOrderBy) + + var controlFilter = coredata.NewControlFilter(nil) + if filter != nil { + controlFilter = coredata.NewControlFilter(filter.Query) + } + + page, err := prb.Controls.ListForSnapshotID(ctx, obj.ID, cursor, controlFilter) + if err != nil { + panic(fmt.Errorf("cannot list snapshot controls: %w", err)) + } + + return types.NewControlConnection(page, r, obj.ID, controlFilter), nil +} + // TotalCount is the resolver for the totalCount field. func (r *snapshotConnectionResolver) TotalCount(ctx context.Context, obj *types.SnapshotConnection) (int, error) { prb := r.ProboService(ctx, obj.ParentID.TenantID())