From 7d80a7d5f45b9efade8e6d6134f905f8c355bd9b Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Thu, 28 Aug 2025 17:22:20 +0200 Subject: [PATCH] Add noncompliance snapshots Signed-off-by: Sacha Al Himdani --- .../console/src/components/SnapshotBanner.tsx | 2 +- .../LinkedSnapshotsCardFragment.graphql.ts | 4 +- .../LinkedSnapshotsDialogFragment.graphql.ts | 4 +- .../hooks/graph/NonconformityRegistryGraph.ts | 4 +- ...onformityRegistryGraphListQuery.graphql.ts | 62 +++++--- .../SnapshotGraphCreateMutation.graphql.ts | 4 +- .../SnapshotGraphNodeQuery.graphql.ts | 4 +- .../NonconformityRegistriesPage.tsx | 71 ++++++--- .../NonconformityRegistryDetailsPage.tsx | 53 +++++-- ...onformityRegistriesPageFragment.graphql.ts | 23 ++- ...onconformityRegistriesPageQuery.graphql.ts | 62 +++++--- ...rmityRegistriesPageRefetchQuery.graphql.ts | 142 +++++++++++------- .../SnapshotsPageFragment.graphql.ts | 4 +- .../snapshots/dialog/SnapshotFormDialog.tsx | 2 +- ...napshotFormDialogCreateMutation.graphql.ts | 4 +- .../src/routes/nonconformityRegistryRoutes.ts | 30 +++- packages/helpers/src/snapshots.ts | 5 +- pkg/coredata/migrations/20250828T142256Z.sql | 15 ++ pkg/coredata/nonconformity_registry.go | 80 +++++++++- pkg/coredata/nonconformity_registry_filter.go | 60 ++++++++ pkg/coredata/snapshottable.go | 2 + pkg/probo/nonconformity_registry_service.go | 6 +- pkg/server/api/console/v1/schema.graphql | 5 + pkg/server/api/console/v1/schema/schema.go | 67 ++++++++- .../v1/types/nonconformity_registry.go | 3 + pkg/server/api/console/v1/types/types.go | 4 + pkg/server/api/console/v1/v1_resolver.go | 18 ++- 27 files changed, 574 insertions(+), 166 deletions(-) create mode 100644 pkg/coredata/migrations/20250828T142256Z.sql create mode 100644 pkg/coredata/nonconformity_registry_filter.go diff --git a/apps/console/src/components/SnapshotBanner.tsx b/apps/console/src/components/SnapshotBanner.tsx index e24c19c8c..2764b2346 100644 --- a/apps/console/src/components/SnapshotBanner.tsx +++ b/apps/console/src/components/SnapshotBanner.tsx @@ -37,7 +37,7 @@ export function SnapshotBanner({ snapshotId }: Props) { {__("Snapshot")} {snapshot.name}

- {__("You are viewing a snapshot of data from")} {dateFormat(snapshot.createdAt, { year: "numeric", month: "short", day: "numeric" })} + {__("You are viewing a snapshot from")} {dateFormat(snapshot.createdAt, { year: "numeric", month: "short", day: "numeric" })}

diff --git a/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsCardFragment.graphql.ts b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsCardFragment.graphql.ts index b6cbc3b60..a6873648f 100644 --- a/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsCardFragment.graphql.ts +++ b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsCardFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<32724438ee7e1d53f9c53938a8129e4b>> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; -export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NON_CONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; +export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NONCONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; import { FragmentRefs } from "relay-runtime"; export type LinkedSnapshotsCardFragment$data = { readonly createdAt: any; diff --git a/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogFragment.graphql.ts b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogFragment.graphql.ts index 05c07a1e3..ac3f00441 100644 --- a/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogFragment.graphql.ts +++ b/apps/console/src/components/snapshots/__generated__/LinkedSnapshotsDialogFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; -export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NON_CONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; +export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NONCONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; import { FragmentRefs } from "relay-runtime"; export type LinkedSnapshotsDialogFragment$data = { readonly id: string; diff --git a/apps/console/src/hooks/graph/NonconformityRegistryGraph.ts b/apps/console/src/hooks/graph/NonconformityRegistryGraph.ts index 59f161535..dd668a760 100644 --- a/apps/console/src/hooks/graph/NonconformityRegistryGraph.ts +++ b/apps/console/src/hooks/graph/NonconformityRegistryGraph.ts @@ -8,10 +8,10 @@ import { useMutationWithToasts } from "../useMutationWithToasts"; export const RegistriesConnectionKey = "RegistriesPage_nonconformityRegistries"; export const nonconformityRegistriesQuery = graphql` - query NonconformityRegistryGraphListQuery($organizationId: ID!) { + query NonconformityRegistryGraphListQuery($organizationId: ID!, $snapshotId: ID) { node(id: $organizationId) { ... on Organization { - ...NonconformityRegistriesPageFragment + ...NonconformityRegistriesPageFragment @arguments(snapshotId: $snapshotId) } } } diff --git a/apps/console/src/hooks/graph/__generated__/NonconformityRegistryGraphListQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/NonconformityRegistryGraphListQuery.graphql.ts index a78fb00d7..79d6c5f76 100644 --- a/apps/console/src/hooks/graph/__generated__/NonconformityRegistryGraphListQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/NonconformityRegistryGraphListQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type NonconformityRegistryGraphListQuery$variables = { organizationId: string; + snapshotId?: string | null | undefined; }; export type NonconformityRegistryGraphListQuery$data = { readonly node: { @@ -29,6 +30,11 @@ var v0 = [ "defaultValue": null, "kind": "LocalArgument", "name": "organizationId" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" } ], v1 = [ @@ -38,28 +44,40 @@ v1 = [ "variableName": "organizationId" } ], -v2 = { +v2 = [ + { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" + } +], +v3 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v3 = { +v4 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v4 = [ +v5 = [ + { + "fields": (v2/*: any*/), + "kind": "ObjectValue", + "name": "filter" + }, { "kind": "Literal", "name": "first", "value": 10 } ], -v5 = { +v6 = { "alias": null, "args": null, "kind": "ScalarField", @@ -85,7 +103,7 @@ return { "kind": "InlineFragment", "selections": [ { - "args": null, + "args": (v2/*: any*/), "kind": "FragmentSpread", "name": "NonconformityRegistriesPageFragment" } @@ -114,14 +132,14 @@ return { "name": "node", "plural": false, "selections": [ - (v2/*: any*/), (v3/*: any*/), + (v4/*: any*/), { "kind": "InlineFragment", "selections": [ { "alias": null, - "args": (v4/*: any*/), + "args": (v5/*: any*/), "concreteType": "NonconformityRegistryConnection", "kind": "LinkedField", "name": "nonconformityRegistries", @@ -150,7 +168,7 @@ return { "name": "node", "plural": false, "selections": [ - (v3/*: any*/), + (v4/*: any*/), { "alias": null, "args": null, @@ -215,8 +233,8 @@ return { "name": "audit", "plural": false, "selections": [ - (v3/*: any*/), - (v5/*: any*/), + (v4/*: any*/), + (v6/*: any*/), { "alias": null, "args": null, @@ -225,8 +243,8 @@ return { "name": "framework", "plural": false, "selections": [ - (v3/*: any*/), - (v5/*: any*/) + (v4/*: any*/), + (v6/*: any*/) ], "storageKey": null } @@ -241,7 +259,7 @@ return { "name": "owner", "plural": false, "selections": [ - (v3/*: any*/), + (v4/*: any*/), { "alias": null, "args": null, @@ -266,7 +284,7 @@ return { "name": "updatedAt", "storageKey": null }, - (v2/*: any*/) + (v3/*: any*/) ], "storageKey": null }, @@ -318,12 +336,14 @@ return { ] } ], - "storageKey": "nonconformityRegistries(first:10)" + "storageKey": null }, { "alias": null, - "args": (v4/*: any*/), - "filters": null, + "args": (v5/*: any*/), + "filters": [ + "filter" + ], "handle": "connection", "key": "RegistriesPage_nonconformityRegistries", "kind": "LinkedHandle", @@ -339,16 +359,16 @@ return { ] }, "params": { - "cacheID": "2196012e8a68c64e7d479a2b85021ffd", + "cacheID": "727537500ac037fec1a9ffd04a5c651f", "id": null, "metadata": {}, "name": "NonconformityRegistryGraphListQuery", "operationKind": "query", - "text": "query NonconformityRegistryGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n ...NonconformityRegistriesPageFragment\n }\n id\n }\n}\n\nfragment NonconformityRegistriesPageFragment on Organization {\n id\n nonconformityRegistries(first: 10) {\n totalCount\n edges {\n node {\n id\n referenceId\n description\n status\n dateIdentified\n dueDate\n rootCause\n correctiveAction\n effectivenessCheck\n audit {\n id\n name\n framework {\n id\n name\n }\n }\n owner {\n id\n fullName\n }\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n" + "text": "query NonconformityRegistryGraphListQuery(\n $organizationId: ID!\n $snapshotId: ID\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n ...NonconformityRegistriesPageFragment_3iomuz\n }\n id\n }\n}\n\nfragment NonconformityRegistriesPageFragment_3iomuz on Organization {\n id\n nonconformityRegistries(first: 10, filter: {snapshotId: $snapshotId}) {\n totalCount\n edges {\n node {\n id\n referenceId\n description\n status\n dateIdentified\n dueDate\n rootCause\n correctiveAction\n effectivenessCheck\n audit {\n id\n name\n framework {\n id\n name\n }\n }\n owner {\n id\n fullName\n }\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n" } }; })(); -(node as any).hash = "b8104dc2e4ccb9eecfe1f0d494084975"; +(node as any).hash = "2bb77e5c9adce5860e4dd2c31f1155e4"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/SnapshotGraphCreateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/SnapshotGraphCreateMutation.graphql.ts index afe8f6027..b9af1ffef 100644 --- a/apps/console/src/hooks/graph/__generated__/SnapshotGraphCreateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/SnapshotGraphCreateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<459d0a122659971be9b24c79f47691e7>> + * @generated SignedSource<<9f17487d2517912ef9f93b0026eaa555>> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NON_CONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; +export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NONCONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; export type CreateSnapshotInput = { description?: string | null | undefined; name: string; diff --git a/apps/console/src/hooks/graph/__generated__/SnapshotGraphNodeQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/SnapshotGraphNodeQuery.graphql.ts index d891b20fb..1fcf75c00 100644 --- a/apps/console/src/hooks/graph/__generated__/SnapshotGraphNodeQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/SnapshotGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<5bcc9f9b743d01fc99fd749b970dccd3>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NON_CONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; +export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NONCONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; export type SnapshotGraphNodeQuery$variables = { snapshotId: string; }; diff --git a/apps/console/src/pages/organizations/nonconformityRegistries/NonconformityRegistriesPage.tsx b/apps/console/src/pages/organizations/nonconformityRegistries/NonconformityRegistriesPage.tsx index da52ef05b..14cd8f9b9 100644 --- a/apps/console/src/pages/organizations/nonconformityRegistries/NonconformityRegistriesPage.tsx +++ b/apps/console/src/pages/organizations/nonconformityRegistries/NonconformityRegistriesPage.tsx @@ -29,6 +29,8 @@ import { useOrganizationId } from "/hooks/useOrganizationId"; import { CreateNonconformityRegistryDialog } from "./dialogs/CreateNonconformityRegistryDialog"; import { deleteNonconformityRegistryMutation, RegistriesConnectionKey } from "../../../hooks/graph/NonconformityRegistryGraph"; import { sprintf, promisifyMutation, getStatusVariant, getStatusLabel } from "@probo/helpers"; +import { SnapshotBanner } from "/components/SnapshotBanner"; +import { useParams } from "react-router"; import type { NonconformityRegistriesPageQuery } from "./__generated__/NonconformityRegistriesPageQuery.graphql"; import type { NonconformityRegistriesPageFragment$key, @@ -47,10 +49,15 @@ const nonconformityRegistriesPageFragment = graphql` @argumentDefinitions( first: { type: "Int", defaultValue: 10 } after: { type: "CursorKey" } + snapshotId: { type: "ID", defaultValue: null } ) { id - nonconformityRegistries(first: $first, after: $after) - @connection(key: "RegistriesPage_nonconformityRegistries") { + nonconformityRegistries( + first: $first + after: $after + filter: { snapshotId: $snapshotId } + ) + @connection(key: "RegistriesPage_nonconformityRegistries", filters: ["filter"]) { __id totalCount edges { @@ -91,15 +98,17 @@ const nonconformityRegistriesPageFragment = graphql` export default function NonconformityRegistriesPage({ queryRef }: NonconformityRegistriesPageProps) { const { __ } = useTranslate(); const organizationId = useOrganizationId(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); usePageTitle(__("Nonconformity Registries")); const organization = usePreloadedQuery( graphql` - query NonconformityRegistriesPageQuery($organizationId: ID!) { + query NonconformityRegistriesPageQuery($organizationId: ID!, $snapshotId: ID) { node(id: $organizationId) { ... on Organization { - ...NonconformityRegistriesPageFragment + ...NonconformityRegistriesPageFragment @arguments(snapshotId: $snapshotId) } } } @@ -112,20 +121,29 @@ export default function NonconformityRegistriesPage({ queryRef }: NonconformityR organization.node as NonconformityRegistriesPageFragment$key ); - const connectionId = ConnectionHandler.getConnectionID(organizationId, RegistriesConnectionKey); + const connectionId = ConnectionHandler.getConnectionID( + organizationId, + RegistriesConnectionKey, + { filter: { snapshotId: snapshotId || null } } + ); const registries: NonconformityRegistry[] = registriesData?.nonconformityRegistries?.edges?.map((edge) => edge.node) ?? []; return (
+ {isSnapshotMode && ( + + )} - - - + {!isSnapshotMode && ( + + + + )} {registries.length === 0 ? ( @@ -150,7 +168,7 @@ export default function NonconformityRegistriesPage({ queryRef }: NonconformityR {__("Audit")} {__("Owner")} {__("Due Date")} - {__("Actions")} + {!isSnapshotMode && ({__("Actions")})} @@ -159,6 +177,8 @@ export default function NonconformityRegistriesPage({ queryRef }: NonconformityR key={registry.id} registry={registry} connectionId={connectionId} + isSnapshotMode={isSnapshotMode} + snapshotId={snapshotId} /> ))} @@ -186,9 +206,13 @@ export default function NonconformityRegistriesPage({ queryRef }: NonconformityR function RegistryRow({ registry, connectionId, + isSnapshotMode, + snapshotId, }: { registry: NonconformityRegistry; connectionId: string; + isSnapshotMode: boolean; + snapshotId?: string; }) { const organizationId = useOrganizationId(); const { __ } = useTranslate(); @@ -199,6 +223,10 @@ function RegistryRow({ return new Date(dateString).toLocaleDateString(); }; + const registryDetailUrl = isSnapshotMode + ? `/organizations/${organizationId}/snapshots/${snapshotId}/nonconformity-registries/${registry.id}` + : `/organizations/${organizationId}/nonconformity-registries/${registry.id}`; + const handleDeleteRegistry = (registry: NonconformityRegistry) => { if (!connectionId) return; @@ -225,7 +253,7 @@ function RegistryRow({ }; return ( - + {registry.referenceId} @@ -257,17 +285,18 @@ function RegistryRow({ {__("No due date")} )} - - - handleDeleteRegistry(registry)} - > - {__("Delete")} - - - + {!isSnapshotMode && ( + + handleDeleteRegistry(registry)} + > + {__("Delete")} + + + + )} ); } diff --git a/apps/console/src/pages/organizations/nonconformityRegistries/NonconformityRegistryDetailsPage.tsx b/apps/console/src/pages/organizations/nonconformityRegistries/NonconformityRegistryDetailsPage.tsx index e856cccc8..0fdce356a 100644 --- a/apps/console/src/pages/organizations/nonconformityRegistries/NonconformityRegistryDetailsPage.tsx +++ b/apps/console/src/pages/organizations/nonconformityRegistries/NonconformityRegistryDetailsPage.tsx @@ -26,6 +26,8 @@ import { import { useTranslate } from "@probo/i18n"; import { useOrganizationId } from "/hooks/useOrganizationId"; import { ControlledField } from "/components/form/ControlledField"; +import { SnapshotBanner } from "/components/SnapshotBanner"; +import { useParams } from "react-router"; import { PeopleSelectField } from "/components/form/PeopleSelectField"; import { AuditSelectField } from "/components/form/AuditSelectField"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; @@ -58,10 +60,16 @@ export default function NonconformityRegistryDetailsPage(props: Props) { const registry = data.node; const { __ } = useTranslate(); const organizationId = useOrganizationId(); + const { snapshotId } = useParams<{ snapshotId?: string }>(); + const isSnapshotMode = Boolean(snapshotId); const deleteRegistry = useDeleteNonconformityRegistry( { id: registry.id!, referenceId: registry.referenceId! }, - ConnectionHandler.getConnectionID(organizationId, RegistriesConnectionKey) + ConnectionHandler.getConnectionID( + organizationId, + RegistriesConnectionKey, + { filter: { snapshotId: snapshotId || null } } + ) ); const { control, formState, handleSubmit, register, reset } = useFormWithSchema( @@ -119,13 +127,20 @@ export default function NonconformityRegistryDetailsPage(props: Props) { const statusOptions = getNonconformityRegistryStatusOptions(__); + const breadcrumbNonconformityRegistriesUrl = isSnapshotMode + ? `/organizations/${organizationId}/snapshots/${snapshotId}/nonconformity-registries` + : `/organizations/${organizationId}/nonconformity-registries`; + return (
+ {isSnapshotMode && ( + + )}
- - - {__("Delete")} - - + {!isSnapshotMode && ( + + + {__("Delete")} + + + )}
@@ -164,6 +181,7 @@ export default function NonconformityRegistryDetailsPage(props: Props) { @@ -174,6 +192,7 @@ export default function NonconformityRegistryDetailsPage(props: Props) { label={__("Audit")} error={formState.errors.auditId?.message} required + disabled={isSnapshotMode} /> @@ -181,6 +200,7 @@ export default function NonconformityRegistryDetailsPage(props: Props) { {...register("description")} placeholder={__("Enter description")} rows={3} + disabled={isSnapshotMode} /> @@ -191,6 +211,7 @@ export default function NonconformityRegistryDetailsPage(props: Props) { type="select" label={__("Status")} required + disabled={isSnapshotMode} > {statusOptions.map((option) => (
- + - +
@@ -228,6 +250,7 @@ export default function NonconformityRegistryDetailsPage(props: Props) { {...register("rootCause")} placeholder={__("Enter root cause")} rows={3} + disabled={isSnapshotMode} /> @@ -236,6 +259,7 @@ export default function NonconformityRegistryDetailsPage(props: Props) { {...register("correctiveAction")} placeholder={__("Enter corrective action")} rows={3} + disabled={isSnapshotMode} /> @@ -244,11 +268,12 @@ export default function NonconformityRegistryDetailsPage(props: Props) { {...register("effectivenessCheck")} placeholder={__("Enter effectiveness check details")} rows={3} + disabled={isSnapshotMode} />
- {formState.isDirty && ( + {formState.isDirty && !isSnapshotMode && ( diff --git a/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageFragment.graphql.ts b/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageFragment.graphql.ts index 2a3bf08a6..2c20345da 100644 --- a/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageFragment.graphql.ts +++ b/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -86,6 +86,11 @@ return { "defaultValue": 10, "kind": "LocalArgument", "name": "first" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" } ], "kind": "Fragment", @@ -122,7 +127,19 @@ return { (v1/*: any*/), { "alias": "nonconformityRegistries", - "args": null, + "args": [ + { + "fields": [ + { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" + } + ], + "kind": "ObjectValue", + "name": "filter" + } + ], "concreteType": "NonconformityRegistryConnection", "kind": "LinkedField", "name": "__RegistriesPage_nonconformityRegistries_connection", @@ -333,6 +350,6 @@ return { }; })(); -(node as any).hash = "34c06ec98cb50738cf479e6bd7cdc83e"; +(node as any).hash = "ebde2010138ec4b17adb214f7bd24ac6"; export default node; diff --git a/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageQuery.graphql.ts b/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageQuery.graphql.ts index 5ebc22bcb..492ece656 100644 --- a/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageQuery.graphql.ts +++ b/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<2720ef53c549871eb0b99b8a0214367d>> + * @generated SignedSource<<1ea20f794eca2a8b357e64ccf3ac6deb>> * @lightSyntaxTransform * @nogrep */ @@ -12,6 +12,7 @@ import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type NonconformityRegistriesPageQuery$variables = { organizationId: string; + snapshotId?: string | null | undefined; }; export type NonconformityRegistriesPageQuery$data = { readonly node: { @@ -29,6 +30,11 @@ var v0 = [ "defaultValue": null, "kind": "LocalArgument", "name": "organizationId" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" } ], v1 = [ @@ -38,28 +44,40 @@ v1 = [ "variableName": "organizationId" } ], -v2 = { +v2 = [ + { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" + } +], +v3 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v3 = { +v4 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v4 = [ +v5 = [ + { + "fields": (v2/*: any*/), + "kind": "ObjectValue", + "name": "filter" + }, { "kind": "Literal", "name": "first", "value": 10 } ], -v5 = { +v6 = { "alias": null, "args": null, "kind": "ScalarField", @@ -85,7 +103,7 @@ return { "kind": "InlineFragment", "selections": [ { - "args": null, + "args": (v2/*: any*/), "kind": "FragmentSpread", "name": "NonconformityRegistriesPageFragment" } @@ -114,14 +132,14 @@ return { "name": "node", "plural": false, "selections": [ - (v2/*: any*/), (v3/*: any*/), + (v4/*: any*/), { "kind": "InlineFragment", "selections": [ { "alias": null, - "args": (v4/*: any*/), + "args": (v5/*: any*/), "concreteType": "NonconformityRegistryConnection", "kind": "LinkedField", "name": "nonconformityRegistries", @@ -150,7 +168,7 @@ return { "name": "node", "plural": false, "selections": [ - (v3/*: any*/), + (v4/*: any*/), { "alias": null, "args": null, @@ -215,8 +233,8 @@ return { "name": "audit", "plural": false, "selections": [ - (v3/*: any*/), - (v5/*: any*/), + (v4/*: any*/), + (v6/*: any*/), { "alias": null, "args": null, @@ -225,8 +243,8 @@ return { "name": "framework", "plural": false, "selections": [ - (v3/*: any*/), - (v5/*: any*/) + (v4/*: any*/), + (v6/*: any*/) ], "storageKey": null } @@ -241,7 +259,7 @@ return { "name": "owner", "plural": false, "selections": [ - (v3/*: any*/), + (v4/*: any*/), { "alias": null, "args": null, @@ -266,7 +284,7 @@ return { "name": "updatedAt", "storageKey": null }, - (v2/*: any*/) + (v3/*: any*/) ], "storageKey": null }, @@ -318,12 +336,14 @@ return { ] } ], - "storageKey": "nonconformityRegistries(first:10)" + "storageKey": null }, { "alias": null, - "args": (v4/*: any*/), - "filters": null, + "args": (v5/*: any*/), + "filters": [ + "filter" + ], "handle": "connection", "key": "RegistriesPage_nonconformityRegistries", "kind": "LinkedHandle", @@ -339,16 +359,16 @@ return { ] }, "params": { - "cacheID": "93352a54352d7b4fd8d2367bbe43e288", + "cacheID": "f2ec31e8e8ec0d13aeac353f53044f97", "id": null, "metadata": {}, "name": "NonconformityRegistriesPageQuery", "operationKind": "query", - "text": "query NonconformityRegistriesPageQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n ...NonconformityRegistriesPageFragment\n }\n id\n }\n}\n\nfragment NonconformityRegistriesPageFragment on Organization {\n id\n nonconformityRegistries(first: 10) {\n totalCount\n edges {\n node {\n id\n referenceId\n description\n status\n dateIdentified\n dueDate\n rootCause\n correctiveAction\n effectivenessCheck\n audit {\n id\n name\n framework {\n id\n name\n }\n }\n owner {\n id\n fullName\n }\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n" + "text": "query NonconformityRegistriesPageQuery(\n $organizationId: ID!\n $snapshotId: ID\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n ...NonconformityRegistriesPageFragment_3iomuz\n }\n id\n }\n}\n\nfragment NonconformityRegistriesPageFragment_3iomuz on Organization {\n id\n nonconformityRegistries(first: 10, filter: {snapshotId: $snapshotId}) {\n totalCount\n edges {\n node {\n id\n referenceId\n description\n status\n dateIdentified\n dueDate\n rootCause\n correctiveAction\n effectivenessCheck\n audit {\n id\n name\n framework {\n id\n name\n }\n }\n owner {\n id\n fullName\n }\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n" } }; })(); -(node as any).hash = "22c4716894e332336d2e165756a2d021"; +(node as any).hash = "180821c7fad7b6071d78444e1eac0742"; export default node; diff --git a/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageRefetchQuery.graphql.ts b/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageRefetchQuery.graphql.ts index 76bb9cad5..6a4da041e 100644 --- a/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageRefetchQuery.graphql.ts +++ b/apps/console/src/pages/organizations/nonconformityRegistries/__generated__/NonconformityRegistriesPageRefetchQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<9c75b7a7e17dd904d30ab786c40015d4>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -14,6 +14,7 @@ export type NonconformityRegistriesPageRefetchQuery$variables = { after?: any | null | undefined; first?: number | null | undefined; id: string; + snapshotId?: string | null | undefined; }; export type NonconformityRegistriesPageRefetchQuery$data = { readonly node: { @@ -26,57 +27,74 @@ export type NonconformityRegistriesPageRefetchQuery = { }; const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "after" - }, - { - "defaultValue": 10, - "kind": "LocalArgument", - "name": "first" - }, - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "id" - } -], -v1 = [ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "after" +}, +v1 = { + "defaultValue": 10, + "kind": "LocalArgument", + "name": "first" +}, +v2 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v3 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "snapshotId" +}, +v4 = [ { "kind": "Variable", "name": "id", "variableName": "id" } ], -v2 = [ - { - "kind": "Variable", - "name": "after", - "variableName": "after" - }, - { - "kind": "Variable", - "name": "first", - "variableName": "first" - } -], -v3 = { +v5 = { + "kind": "Variable", + "name": "after", + "variableName": "after" +}, +v6 = { + "kind": "Variable", + "name": "first", + "variableName": "first" +}, +v7 = { + "kind": "Variable", + "name": "snapshotId", + "variableName": "snapshotId" +}, +v8 = { "alias": null, "args": null, "kind": "ScalarField", "name": "__typename", "storageKey": null }, -v4 = { +v9 = { "alias": null, "args": null, "kind": "ScalarField", "name": "id", "storageKey": null }, -v5 = { +v10 = [ + (v5/*: any*/), + { + "fields": [ + (v7/*: any*/) + ], + "kind": "ObjectValue", + "name": "filter" + }, + (v6/*: any*/) +], +v11 = { "alias": null, "args": null, "kind": "ScalarField", @@ -85,21 +103,30 @@ v5 = { }; return { "fragment": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v2/*: any*/), + (v3/*: any*/) + ], "kind": "Fragment", "metadata": null, "name": "NonconformityRegistriesPageRefetchQuery", "selections": [ { "alias": null, - "args": (v1/*: any*/), + "args": (v4/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ { - "args": (v2/*: any*/), + "args": [ + (v5/*: any*/), + (v6/*: any*/), + (v7/*: any*/) + ], "kind": "FragmentSpread", "name": "NonconformityRegistriesPageFragment" } @@ -112,26 +139,31 @@ return { }, "kind": "Request", "operation": { - "argumentDefinitions": (v0/*: any*/), + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/), + (v3/*: any*/), + (v2/*: any*/) + ], "kind": "Operation", "name": "NonconformityRegistriesPageRefetchQuery", "selections": [ { "alias": null, - "args": (v1/*: any*/), + "args": (v4/*: any*/), "concreteType": null, "kind": "LinkedField", "name": "node", "plural": false, "selections": [ - (v3/*: any*/), - (v4/*: any*/), + (v8/*: any*/), + (v9/*: any*/), { "kind": "InlineFragment", "selections": [ { "alias": null, - "args": (v2/*: any*/), + "args": (v10/*: any*/), "concreteType": "NonconformityRegistryConnection", "kind": "LinkedField", "name": "nonconformityRegistries", @@ -160,7 +192,7 @@ return { "name": "node", "plural": false, "selections": [ - (v4/*: any*/), + (v9/*: any*/), { "alias": null, "args": null, @@ -225,8 +257,8 @@ return { "name": "audit", "plural": false, "selections": [ - (v4/*: any*/), - (v5/*: any*/), + (v9/*: any*/), + (v11/*: any*/), { "alias": null, "args": null, @@ -235,8 +267,8 @@ return { "name": "framework", "plural": false, "selections": [ - (v4/*: any*/), - (v5/*: any*/) + (v9/*: any*/), + (v11/*: any*/) ], "storageKey": null } @@ -251,7 +283,7 @@ return { "name": "owner", "plural": false, "selections": [ - (v4/*: any*/), + (v9/*: any*/), { "alias": null, "args": null, @@ -276,7 +308,7 @@ return { "name": "updatedAt", "storageKey": null }, - (v3/*: any*/) + (v8/*: any*/) ], "storageKey": null }, @@ -332,8 +364,10 @@ return { }, { "alias": null, - "args": (v2/*: any*/), - "filters": null, + "args": (v10/*: any*/), + "filters": [ + "filter" + ], "handle": "connection", "key": "RegistriesPage_nonconformityRegistries", "kind": "LinkedHandle", @@ -349,16 +383,16 @@ return { ] }, "params": { - "cacheID": "c8a903834590297e238ea54aeea0dfbe", + "cacheID": "50e5771b4ff22e23d81c31bf7c92e619", "id": null, "metadata": {}, "name": "NonconformityRegistriesPageRefetchQuery", "operationKind": "query", - "text": "query NonconformityRegistriesPageRefetchQuery(\n $after: CursorKey\n $first: Int = 10\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...NonconformityRegistriesPageFragment_2HEEH6\n id\n }\n}\n\nfragment NonconformityRegistriesPageFragment_2HEEH6 on Organization {\n id\n nonconformityRegistries(first: $first, after: $after) {\n totalCount\n edges {\n node {\n id\n referenceId\n description\n status\n dateIdentified\n dueDate\n rootCause\n correctiveAction\n effectivenessCheck\n audit {\n id\n name\n framework {\n id\n name\n }\n }\n owner {\n id\n fullName\n }\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n" + "text": "query NonconformityRegistriesPageRefetchQuery(\n $after: CursorKey\n $first: Int = 10\n $snapshotId: ID = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...NonconformityRegistriesPageFragment_35e0S5\n id\n }\n}\n\nfragment NonconformityRegistriesPageFragment_35e0S5 on Organization {\n id\n nonconformityRegistries(first: $first, after: $after, filter: {snapshotId: $snapshotId}) {\n totalCount\n edges {\n node {\n id\n referenceId\n description\n status\n dateIdentified\n dueDate\n rootCause\n correctiveAction\n effectivenessCheck\n audit {\n id\n name\n framework {\n id\n name\n }\n }\n owner {\n id\n fullName\n }\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n" } }; })(); -(node as any).hash = "34c06ec98cb50738cf479e6bd7cdc83e"; +(node as any).hash = "ebde2010138ec4b17adb214f7bd24ac6"; export default node; diff --git a/apps/console/src/pages/organizations/snapshots/__generated__/SnapshotsPageFragment.graphql.ts b/apps/console/src/pages/organizations/snapshots/__generated__/SnapshotsPageFragment.graphql.ts index 979872e74..08c9df7e7 100644 --- a/apps/console/src/pages/organizations/snapshots/__generated__/SnapshotsPageFragment.graphql.ts +++ b/apps/console/src/pages/organizations/snapshots/__generated__/SnapshotsPageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<05658205222e432a115cecc9c8f34d05>> + * @generated SignedSource<<515e309ba0b61b3cacd37ebd567203c4>> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; -export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NON_CONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; +export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NONCONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; import { FragmentRefs } from "relay-runtime"; export type SnapshotsPageFragment$data = { readonly snapshots: { diff --git a/apps/console/src/pages/organizations/snapshots/dialog/SnapshotFormDialog.tsx b/apps/console/src/pages/organizations/snapshots/dialog/SnapshotFormDialog.tsx index 090d58c9e..8a9653458 100644 --- a/apps/console/src/pages/organizations/snapshots/dialog/SnapshotFormDialog.tsx +++ b/apps/console/src/pages/organizations/snapshots/dialog/SnapshotFormDialog.tsx @@ -42,7 +42,7 @@ const snapshotCreateMutation = graphql` const snapshotSchema = z.object({ name: z.string().min(2, { message: "Name is required" }), description: z.string().optional(), - type: z.enum(["DATA"]), + type: z.enum(["DATA", "NONCONFORMITY_REGISTRIES"]), }); type Props = { diff --git a/apps/console/src/pages/organizations/snapshots/dialog/__generated__/SnapshotFormDialogCreateMutation.graphql.ts b/apps/console/src/pages/organizations/snapshots/dialog/__generated__/SnapshotFormDialogCreateMutation.graphql.ts index 93a14aee7..2b3a23c21 100644 --- a/apps/console/src/pages/organizations/snapshots/dialog/__generated__/SnapshotFormDialogCreateMutation.graphql.ts +++ b/apps/console/src/pages/organizations/snapshots/dialog/__generated__/SnapshotFormDialogCreateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<22c84eaaec91c3555b99f0e067bea54f>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -9,7 +9,7 @@ // @ts-nocheck import { ConcreteRequest } from 'relay-runtime'; -export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NON_CONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; +export type SnapshotsType = "ASSETS" | "COMPLIANCE_REGISTRIES" | "DATA" | "NONCONFORMITY_REGISTRIES" | "RISKS" | "VENDORS"; export type CreateSnapshotInput = { description?: string | null | undefined; name: string; diff --git a/apps/console/src/routes/nonconformityRegistryRoutes.ts b/apps/console/src/routes/nonconformityRegistryRoutes.ts index 0c69ccb93..24112c4d8 100644 --- a/apps/console/src/routes/nonconformityRegistryRoutes.ts +++ b/apps/console/src/routes/nonconformityRegistryRoutes.ts @@ -9,8 +9,23 @@ export const nonconformityRegistryRoutes= [ { path: "nonconformity-registries", fallback: PageSkeleton, - queryLoader: ({ organizationId }: { organizationId: string }) => - loadQuery(relayEnvironment, nonconformityRegistriesQuery, { organizationId }), + queryLoader: (params: Record) => + loadQuery(relayEnvironment, nonconformityRegistriesQuery, { + organizationId: params.organizationId, + snapshotId: null + }), + Component: lazy( + () => import("/pages/organizations/nonconformityRegistries/NonconformityRegistriesPage") + ), + }, + { + path: "snapshots/:snapshotId/nonconformity-registries", + fallback: PageSkeleton, + queryLoader: (params: Record) => + loadQuery(relayEnvironment, nonconformityRegistriesQuery, { + organizationId: params.organizationId, + snapshotId: params.snapshotId + }), Component: lazy( () => import("/pages/organizations/nonconformityRegistries/NonconformityRegistriesPage") ), @@ -26,4 +41,15 @@ export const nonconformityRegistryRoutes= [ () => import("/pages/organizations/nonconformityRegistries/NonconformityRegistryDetailsPage") ), }, + { + path: "snapshots/:snapshotId/nonconformity-registries/:registryId", + fallback: PageSkeleton, + queryLoader: (params: Record) => + loadQuery(relayEnvironment, nonconformityRegistryNodeQuery, { + nonconformityRegistryId: params.registryId + }), + Component: lazy( + () => import("/pages/organizations/nonconformityRegistries/NonconformityRegistryDetailsPage") + ), + }, ] satisfies AppRoute[]; diff --git a/packages/helpers/src/snapshots.ts b/packages/helpers/src/snapshots.ts index d65d62466..8948ba3aa 100644 --- a/packages/helpers/src/snapshots.ts +++ b/packages/helpers/src/snapshots.ts @@ -2,6 +2,7 @@ type Translator = (s: string) => string; export const snapshotTypes = [ "DATA", + "NONCONFORMITY_REGISTRIES", ] as const; export function getSnapshotTypeLabel(__: Translator, type: string | null | undefined) { @@ -18,7 +19,7 @@ export function getSnapshotTypeLabel(__: Translator, type: string | null | undef return __("Assets"); case "DATA": return __("Data"); - case "NON_CONFORMITY_REGISTRIES": + case "NONCONFORMITY_REGISTRIES": return __("Nonconformity Registries"); case "COMPLIANCE_REGISTRIES": return __("Compliance Registries"); @@ -31,6 +32,8 @@ export function getSnapshotTypeUrlPath(type?: string): string { switch (type) { case "DATA": return "/data"; + case "NONCONFORMITY_REGISTRIES": + return "/nonconformity-registries"; default: return ""; } diff --git a/pkg/coredata/migrations/20250828T142256Z.sql b/pkg/coredata/migrations/20250828T142256Z.sql new file mode 100644 index 000000000..c4d71e0fe --- /dev/null +++ b/pkg/coredata/migrations/20250828T142256Z.sql @@ -0,0 +1,15 @@ +ALTER TABLE nonconformity_registries ADD COLUMN snapshot_id TEXT; +ALTER TABLE nonconformity_registries ADD COLUMN source_id TEXT; + +ALTER TABLE nonconformity_registries DROP CONSTRAINT IF EXISTS nonconformity_registries_audit_id_fkey; +ALTER TABLE nonconformity_registries ADD CONSTRAINT nonconformity_registries_audit_id_fkey + FOREIGN KEY (audit_id) REFERENCES audits(id) ON UPDATE CASCADE ON DELETE RESTRICT; + +ALTER TABLE nonconformity_registries ADD CONSTRAINT nonconformity_registries_snapshot_id_fkey + FOREIGN KEY (snapshot_id) + REFERENCES snapshots(id) + ON UPDATE CASCADE + ON DELETE CASCADE; + +ALTER TABLE nonconformity_registries ADD CONSTRAINT nonconformity_registries_source_id_snapshot_id_key + UNIQUE (source_id, snapshot_id); diff --git a/pkg/coredata/nonconformity_registry.go b/pkg/coredata/nonconformity_registry.go index ee233303d..86bb53ebe 100644 --- a/pkg/coredata/nonconformity_registry.go +++ b/pkg/coredata/nonconformity_registry.go @@ -30,6 +30,8 @@ type ( NonconformityRegistry struct { ID gid.GID `db:"id"` OrganizationID gid.GID `db:"organization_id"` + SnapshotID *gid.GID `db:"snapshot_id"` + SourceID *gid.GID `db:"source_id"` ReferenceID string `db:"reference_id"` Description *string `db:"description"` AuditID gid.GID `db:"audit_id"` @@ -74,6 +76,8 @@ func (nr *NonconformityRegistry) LoadByID( SELECT id, organization_id, + snapshot_id, + source_id, reference_id, description, audit_id, @@ -119,6 +123,7 @@ func (nrs *NonconformityRegistries) CountByOrganizationID( conn pg.Conn, scope Scoper, organizationID gid.GID, + filter *NonconformityRegistryFilter, ) (int, error) { q := ` SELECT @@ -128,12 +133,14 @@ FROM WHERE %s AND organization_id = @organization_id + AND %s ` - q = fmt.Sprintf(q, scope.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) row := conn.QueryRow(ctx, q, args) @@ -152,11 +159,14 @@ func (nrs *NonconformityRegistries) LoadByOrganizationID( scope Scoper, organizationID gid.GID, cursor *page.Cursor[NonconformityRegistryOrderField], + filter *NonconformityRegistryFilter, ) error { q := ` SELECT id, organization_id, + snapshot_id, + source_id, reference_id, description, audit_id, @@ -175,12 +185,14 @@ WHERE %s AND organization_id = @organization_id AND %s + AND %s ` - q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment()) + q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment()) args := pgx.StrictNamedArgs{"organization_id": organizationID} maps.Copy(args, scope.SQLArguments()) + maps.Copy(args, filter.SQLArguments()) maps.Copy(args, cursor.SQLArguments()) rows, err := conn.Query(ctx, q, args) @@ -287,6 +299,7 @@ SET WHERE %s AND id = @id + AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -324,7 +337,7 @@ func (nr *NonconformityRegistry) Delete( DELETE FROM nonconformity_registries WHERE %s - AND id = @id + AND id = @id AND snapshot_id IS NULL ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -339,3 +352,64 @@ WHERE return nil } + +func (nrs NonconformityRegistries) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error { + query := ` +INSERT INTO nonconformity_registries ( + id, + tenant_id, + snapshot_id, + source_id, + organization_id, + reference_id, + description, + audit_id, + date_identified, + root_cause, + corrective_action, + owner_id, + due_date, + status, + effectiveness_check, + created_at, + updated_at +) +SELECT + generate_gid(decode_base64_unpadded(@tenant_id), @nonconformity_registry_entity_type), + @tenant_id, + @snapshot_id, + r.id, + r.organization_id, + r.reference_id, + r.description, + r.audit_id, + r.date_identified, + r.root_cause, + r.corrective_action, + r.owner_id, + r.due_date, + r.status, + r.effectiveness_check, + r.created_at, + r.updated_at +FROM nonconformity_registries r +WHERE %s AND r.organization_id = @organization_id AND r.snapshot_id IS NULL + ` + + query = fmt.Sprintf(query, scope.SQLFragment()) + + args := pgx.StrictNamedArgs{ + "tenant_id": scope.GetTenantID(), + "snapshot_id": snapshotID, + "organization_id": organizationID, + "nonconformity_registry_entity_type": NonconformityRegistryEntityType, + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, query, args) + if err != nil { + return fmt.Errorf("cannot insert data snapshots: %w", err) + } + + return nil +} diff --git a/pkg/coredata/nonconformity_registry_filter.go b/pkg/coredata/nonconformity_registry_filter.go new file mode 100644 index 000000000..bc5aa2b84 --- /dev/null +++ b/pkg/coredata/nonconformity_registry_filter.go @@ -0,0 +1,60 @@ +// 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 ( + "github.com/getprobo/probo/pkg/gid" + "github.com/jackc/pgx/v5" +) + +type ( + NonconformityRegistryFilter struct { + snapshotID **gid.GID + } +) + +func NewNonconformityRegistryFilter() *NonconformityRegistryFilter { + return &NonconformityRegistryFilter{ + snapshotID: nil, + } +} + +func NewNonconformityRegistryFilterBySnapshotID(snapshotID **gid.GID) *NonconformityRegistryFilter { + return &NonconformityRegistryFilter{ + snapshotID: snapshotID, + } +} + +func (f *NonconformityRegistryFilter) SQLArguments() pgx.NamedArgs { + args := pgx.NamedArgs{} + + if f.snapshotID != nil && *f.snapshotID != nil { + args["filter_snapshot_id"] = **f.snapshotID + } + + return args +} + +func (f *NonconformityRegistryFilter) SQLFragment() string { + if f.snapshotID == nil { + return "TRUE" + } + + if *f.snapshotID == nil { + return "snapshot_id IS NULL" + } else { + return "snapshot_id = @filter_snapshot_id" + } +} diff --git a/pkg/coredata/snapshottable.go b/pkg/coredata/snapshottable.go index 0fd3b99e2..75ad8e005 100644 --- a/pkg/coredata/snapshottable.go +++ b/pkg/coredata/snapshottable.go @@ -30,6 +30,8 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) { switch snapshotType { case SnapshotsTypeData: return Data{}, nil + case SnapshotsTypeNonConformityRegistries: + return NonconformityRegistries{}, nil default: return nil, fmt.Errorf("unsupported snapshot type: %s", snapshotType) } diff --git a/pkg/probo/nonconformity_registry_service.go b/pkg/probo/nonconformity_registry_service.go index e0925225f..bc9a044c0 100644 --- a/pkg/probo/nonconformity_registry_service.go +++ b/pkg/probo/nonconformity_registry_service.go @@ -221,13 +221,14 @@ func (s NonconformityRegistryService) ListForOrganizationID( ctx context.Context, organizationID gid.GID, cursor *page.Cursor[coredata.NonconformityRegistryOrderField], + filter *coredata.NonconformityRegistryFilter, ) (*page.Page[*coredata.NonconformityRegistry, coredata.NonconformityRegistryOrderField], error) { var registries coredata.NonconformityRegistries err := s.svc.pg.WithConn( ctx, func(conn pg.Conn) error { - err := registries.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor) + err := registries.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter) if err != nil { return fmt.Errorf("cannot load nonconformity registries: %w", err) } @@ -246,6 +247,7 @@ func (s NonconformityRegistryService) ListForOrganizationID( func (s NonconformityRegistryService) CountForOrganizationID( ctx context.Context, organizationID gid.GID, + filter *coredata.NonconformityRegistryFilter, ) (int, error) { var count int @@ -253,7 +255,7 @@ func (s NonconformityRegistryService) CountForOrganizationID( ctx, func(conn pg.Conn) (err error) { registries := coredata.NonconformityRegistries{} - count, err = registries.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID) + count, err = registries.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter) if err != nil { return fmt.Errorf("cannot count nonconformity registries: %w", err) } diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 169b97fc5..25ce434a8 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1106,6 +1106,10 @@ input DatumFilter { snapshotId: ID } +input NonconformityRegistryFilter { + snapshotId: ID +} + # Core Types type TrustCenter implements Node { id: ID! @@ -1245,6 +1249,7 @@ type Organization implements Node { last: Int before: CursorKey orderBy: NonconformityRegistryOrder + filter: NonconformityRegistryFilter ): NonconformityRegistryConnection! @goField(forceResolver: true) complianceRegistries( diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 0167ab783..b49966185 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -881,7 +881,7 @@ type ComplexityRoot struct { LogoURL func(childComplexity int) int 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 - NonconformityRegistries func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.NonconformityRegistryOrderBy) int + NonconformityRegistries func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.NonconformityRegistryOrderBy, filter *types.NonconformityRegistryFilter) int Peoples func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy, filter *types.PeopleFilter) int ProcessingActivityRegistries func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProcessingActivityRegistryOrderBy) int Risks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy, filter *types.RiskFilter) int @@ -1666,7 +1666,7 @@ type OrganizationResolver interface { Assets(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy) (*types.AssetConnection, error) Data(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DatumOrderBy, filter *types.DatumFilter) (*types.DatumConnection, error) Audits(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AuditOrderBy) (*types.AuditConnection, error) - NonconformityRegistries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.NonconformityRegistryOrderBy) (*types.NonconformityRegistryConnection, error) + NonconformityRegistries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.NonconformityRegistryOrderBy, filter *types.NonconformityRegistryFilter) (*types.NonconformityRegistryConnection, error) ComplianceRegistries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ComplianceRegistryOrderBy) (*types.ComplianceRegistryConnection, error) ContinualImprovementRegistries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ContinualImprovementRegistriesOrderBy) (*types.ContinualImprovementRegistryConnection, error) ProcessingActivityRegistries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProcessingActivityRegistryOrderBy) (*types.ProcessingActivityRegistryConnection, error) @@ -5478,7 +5478,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Organization.NonconformityRegistries(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.NonconformityRegistryOrderBy)), true + return e.complexity.Organization.NonconformityRegistries(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.NonconformityRegistryOrderBy), args["filter"].(*types.NonconformityRegistryFilter)), true case "Organization.peoples": if e.complexity.Organization.Peoples == nil { @@ -7724,6 +7724,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputInviteUserInput, ec.unmarshalInputMeasureFilter, ec.unmarshalInputMeasureOrder, + ec.unmarshalInputNonconformityRegistryFilter, ec.unmarshalInputNonconformityRegistryOrder, ec.unmarshalInputOrganizationFilter, ec.unmarshalInputOrganizationOrder, @@ -8981,6 +8982,10 @@ input DatumFilter { snapshotId: ID } +input NonconformityRegistryFilter { + snapshotId: ID +} + # Core Types type TrustCenter implements Node { id: ID! @@ -9120,6 +9125,7 @@ type Organization implements Node { last: Int before: CursorKey orderBy: NonconformityRegistryOrder + filter: NonconformityRegistryFilter ): NonconformityRegistryConnection! @goField(forceResolver: true) complianceRegistries( @@ -16801,6 +16807,11 @@ func (ec *executionContext) field_Organization_nonconformityRegistries_args(ctx return nil, err } args["orderBy"] = arg4 + arg5, err := ec.field_Organization_nonconformityRegistries_argsFilter(ctx, rawArgs) + if err != nil { + return nil, err + } + args["filter"] = arg5 return args, nil } func (ec *executionContext) field_Organization_nonconformityRegistries_argsFirst( @@ -16868,6 +16879,19 @@ func (ec *executionContext) field_Organization_nonconformityRegistries_argsOrder return zeroVal, nil } +func (ec *executionContext) field_Organization_nonconformityRegistries_argsFilter( + ctx context.Context, + rawArgs map[string]any, +) (*types.NonconformityRegistryFilter, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("filter")) + if tmp, ok := rawArgs["filter"]; ok { + return ec.unmarshalONonconformityRegistryFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐNonconformityRegistryFilter(ctx, tmp) + } + + var zeroVal *types.NonconformityRegistryFilter + return zeroVal, nil +} + func (ec *executionContext) field_Organization_peoples_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -41754,7 +41778,7 @@ func (ec *executionContext) _Organization_nonconformityRegistries(ctx context.Co }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Organization().NonconformityRegistries(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.NonconformityRegistryOrderBy)) + return ec.resolvers.Organization().NonconformityRegistries(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.NonconformityRegistryOrderBy), fc.Args["filter"].(*types.NonconformityRegistryFilter)) }) if err != nil { ec.Error(ctx, err) @@ -62770,6 +62794,33 @@ func (ec *executionContext) unmarshalInputMeasureOrder(ctx context.Context, obj return it, nil } +func (ec *executionContext) unmarshalInputNonconformityRegistryFilter(ctx context.Context, obj any) (types.NonconformityRegistryFilter, error) { + var it types.NonconformityRegistryFilter + asMap := map[string]any{} + for k, v := range obj.(map[string]any) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"snapshotId"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "snapshotId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("snapshotId")) + data, err := ec.unmarshalOID2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v) + if err != nil { + return it, err + } + it.SnapshotID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputNonconformityRegistryOrder(ctx context.Context, obj any) (types.NonconformityRegistryOrderBy, error) { var it types.NonconformityRegistryOrderBy asMap := map[string]any{} @@ -87078,6 +87129,14 @@ var ( } ) +func (ec *executionContext) unmarshalONonconformityRegistryFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐNonconformityRegistryFilter(ctx context.Context, v any) (*types.NonconformityRegistryFilter, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputNonconformityRegistryFilter(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalONonconformityRegistryOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐNonconformityRegistryOrderBy(ctx context.Context, v any) (*types.NonconformityRegistryOrderBy, error) { if v == nil { return nil, nil diff --git a/pkg/server/api/console/v1/types/nonconformity_registry.go b/pkg/server/api/console/v1/types/nonconformity_registry.go index 28f4fb3f4..a71b9a846 100644 --- a/pkg/server/api/console/v1/types/nonconformity_registry.go +++ b/pkg/server/api/console/v1/types/nonconformity_registry.go @@ -30,6 +30,7 @@ type ( Resolver any ParentID gid.GID + Filter *NonconformityRegistryFilter } ) @@ -37,6 +38,7 @@ func NewNonconformityRegistryConnection( p *page.Page[*coredata.NonconformityRegistry, coredata.NonconformityRegistryOrderField], parentType any, parentID gid.GID, + filter *NonconformityRegistryFilter, ) *NonconformityRegistryConnection { edges := make([]*NonconformityRegistryEdge, len(p.Data)) for i, registry := range p.Data { @@ -49,6 +51,7 @@ func NewNonconformityRegistryConnection( Resolver: parentType, ParentID: parentID, + Filter: filter, } } diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index 55cffed57..ee0b99e1f 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -1138,6 +1138,10 @@ type NonconformityRegistryEdge struct { Node *NonconformityRegistry `json:"node"` } +type NonconformityRegistryFilter struct { + SnapshotID *gid.GID `json:"snapshotId,omitempty"` +} + type Organization struct { ID gid.GID `json:"id"` Name string `json:"name"` diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 12a4e2338..6e6ee50fc 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -3313,7 +3313,12 @@ func (r *nonconformityRegistryConnectionResolver) TotalCount(ctx context.Context switch obj.Resolver.(type) { case *organizationResolver: - count, err := prb.NonconformityRegistries.CountForOrganizationID(ctx, obj.ParentID) + nonconformityRegistryFilter := coredata.NewNonconformityRegistryFilterBySnapshotID(nil) + if obj.Filter != nil { + nonconformityRegistryFilter = coredata.NewNonconformityRegistryFilterBySnapshotID(&obj.Filter.SnapshotID) + } + + count, err := prb.NonconformityRegistries.CountForOrganizationID(ctx, obj.ParentID, nonconformityRegistryFilter) if err != nil { return 0, fmt.Errorf("cannot count nonconformity registries: %w", err) } @@ -3686,7 +3691,7 @@ func (r *organizationResolver) Audits(ctx context.Context, obj *types.Organizati } // NonconformityRegistries is the resolver for the nonconformityRegistries field. -func (r *organizationResolver) NonconformityRegistries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.NonconformityRegistryOrderBy) (*types.NonconformityRegistryConnection, error) { +func (r *organizationResolver) NonconformityRegistries(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.NonconformityRegistryOrderBy, filter *types.NonconformityRegistryFilter) (*types.NonconformityRegistryConnection, error) { prb := r.ProboService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.NonconformityRegistryOrderField]{ @@ -3702,12 +3707,17 @@ func (r *organizationResolver) NonconformityRegistries(ctx context.Context, obj cursor := types.NewCursor(first, after, last, before, pageOrderBy) - page, err := prb.NonconformityRegistries.ListForOrganizationID(ctx, obj.ID, cursor) + nonconformityRegistryFilter := coredata.NewNonconformityRegistryFilterBySnapshotID(nil) + if filter != nil { + nonconformityRegistryFilter = coredata.NewNonconformityRegistryFilterBySnapshotID(&filter.SnapshotID) + } + + page, err := prb.NonconformityRegistries.ListForOrganizationID(ctx, obj.ID, cursor, nonconformityRegistryFilter) if err != nil { return nil, fmt.Errorf("cannot list organization nonconformity registries: %w", err) } - return types.NewNonconformityRegistryConnection(page, r, obj.ID), nil + return types.NewNonconformityRegistryConnection(page, r, obj.ID, filter), nil } // ComplianceRegistries is the resolver for the complianceRegistries field.