File refs in one table

Added a file service
Refactored evidence and vendor compliance associated services and resolvers
Created SQL migration scripts
Updated GraphQL schema accordingly and the UI components
Removed unused methods and resolvers for evidence service

Signed-off-by: Yannis Varni <yannis@edinomis.fr>
This commit is contained in:
Yannis Varni
2025-10-03 15:38:00 +02:00
committed by Sacha Al Himdani
parent 3822b1916c
commit ff7ba08fc1
35 changed files with 1586 additions and 1658 deletions

View File

@@ -5,9 +5,12 @@ export const evidenceFileQuery = graphql`
node(id: $evidenceId) { node(id: $evidenceId) {
... on Evidence { ... on Evidence {
id id
mimeType file {
filename mimeType
fileUrl fileName
size
downloadUrl
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<0c4d6a4bd477e15ae3c627cf338a99dd>> * @generated SignedSource<<843df409ed1ee9ba19760921dbd6e8df>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -14,10 +14,13 @@ export type EvidenceGraphFileQuery$variables = {
}; };
export type EvidenceGraphFileQuery$data = { export type EvidenceGraphFileQuery$data = {
readonly node: { readonly node: {
readonly fileUrl?: string | null | undefined; readonly file?: {
readonly filename?: string; readonly downloadUrl: string;
readonly fileName: string;
readonly mimeType: string;
readonly size: any;
} | null | undefined;
readonly id?: string; readonly id?: string;
readonly mimeType?: string;
}; };
}; };
export type EvidenceGraphFileQuery = { export type EvidenceGraphFileQuery = {
@@ -58,14 +61,21 @@ v4 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "filename", "name": "fileName",
"storageKey": null "storageKey": null
}, },
v5 = { v5 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fileUrl", "name": "size",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "downloadUrl",
"storageKey": null "storageKey": null
}; };
return { return {
@@ -87,9 +97,21 @@ return {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v2/*: any*/), (v2/*: any*/),
(v3/*: any*/), {
(v4/*: any*/), "alias": null,
(v5/*: any*/) "args": null,
"concreteType": "File",
"kind": "LinkedField",
"name": "file",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/)
],
"storageKey": null
}
], ],
"type": "Evidence", "type": "Evidence",
"abstractKey": null "abstractKey": null
@@ -126,9 +148,22 @@ return {
{ {
"kind": "InlineFragment", "kind": "InlineFragment",
"selections": [ "selections": [
(v3/*: any*/), {
(v4/*: any*/), "alias": null,
(v5/*: any*/) "args": null,
"concreteType": "File",
"kind": "LinkedField",
"name": "file",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v2/*: any*/)
],
"storageKey": null
}
], ],
"type": "Evidence", "type": "Evidence",
"abstractKey": null "abstractKey": null
@@ -139,16 +174,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "db46f9bf993514c7bc89845e6d6459f8", "cacheID": "780e108a52d2fcbde6653de5d2ee058b",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "EvidenceGraphFileQuery", "name": "EvidenceGraphFileQuery",
"operationKind": "query", "operationKind": "query",
"text": "query EvidenceGraphFileQuery(\n $evidenceId: ID!\n) {\n node(id: $evidenceId) {\n __typename\n ... on Evidence {\n id\n mimeType\n filename\n fileUrl\n }\n id\n }\n}\n" "text": "query EvidenceGraphFileQuery(\n $evidenceId: ID!\n) {\n node(id: $evidenceId) {\n __typename\n ... on Evidence {\n id\n file {\n mimeType\n fileName\n size\n downloadUrl\n id\n }\n }\n id\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "3c9aa6ff338d2380a7a03b8ea61e83e0"; (node as any).hash = "186386e82f82feb9c5b5a54f9d937903";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<074d65be2a59b6b8284ff6be0f75707d>> * @generated SignedSource<<1f413a42110dae78274cf3250fa4db4e>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -594,22 +594,34 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "filename", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "mimeType", "kind": "ScalarField",
"storageKey": null "name": "fileName",
}, "storageKey": null
{ },
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "size", "kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
(v2/*: any*/)
],
"storageKey": null "storageKey": null
}, },
{ {
@@ -660,12 +672,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "6146a6fada0328c7b69652646212be05", "cacheID": "d20815cee9ca8dec2e09fcefdd54a68a",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "MeasureGraphNodeQuery", "name": "MeasureGraphNodeQuery",
"operationKind": "query", "operationKind": "query",
"text": "query MeasureGraphNodeQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n name\n description\n state\n category\n evidencesInfos: evidences(first: 0) {\n totalCount\n }\n risksInfos: risks(first: 0) {\n totalCount\n }\n tasksInfos: tasks(first: 0) {\n totalCount\n }\n controlsInfos: controls(first: 0) {\n totalCount\n }\n ...MeasureRisksTabFragment\n ...MeasureTasksTabFragment\n ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n\nfragment LinkedRisksCardFragment on Risk {\n id\n name\n inherentRiskScore\n residualRiskScore\n}\n\nfragment MeasureControlsTabFragment on Measure {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\n filename\n mimeType\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n filename\n size\n type\n createdAt\n mimeType\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasureRisksTabFragment on Measure {\n id\n risks(first: 100) {\n edges {\n node {\n id\n ...LinkedRisksCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment MeasureTasksTabFragment on Measure {\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n" "text": "query MeasureGraphNodeQuery(\n $measureId: ID!\n) {\n node(id: $measureId) {\n __typename\n ... on Measure {\n id\n name\n description\n state\n category\n evidencesInfos: evidences(first: 0) {\n totalCount\n }\n risksInfos: risks(first: 0) {\n totalCount\n }\n tasksInfos: tasks(first: 0) {\n totalCount\n }\n controlsInfos: controls(first: 0) {\n totalCount\n }\n ...MeasureRisksTabFragment\n ...MeasureTasksTabFragment\n ...MeasureControlsTabFragment\n ...MeasureFormDialogMeasureFragment\n ...MeasureEvidencesTabFragment\n }\n id\n }\n}\n\nfragment LinkedControlsCardFragment on Control {\n id\n name\n sectionTitle\n framework {\n id\n name\n }\n}\n\nfragment LinkedRisksCardFragment on Risk {\n id\n name\n inherentRiskScore\n residualRiskScore\n}\n\nfragment MeasureControlsTabFragment on Measure {\n id\n controls(first: 20) {\n edges {\n node {\n id\n ...LinkedControlsCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment on Measure {\n id\n evidences(first: 50) {\n edges {\n node {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n type\n createdAt\n}\n\nfragment MeasureFormDialogMeasureFragment on Measure {\n id\n description\n name\n category\n state\n}\n\nfragment MeasureRisksTabFragment on Measure {\n id\n risks(first: 100) {\n edges {\n node {\n id\n ...LinkedRisksCardFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment MeasureTasksTabFragment on Measure {\n tasks(first: 100) {\n edges {\n node {\n id\n name\n state\n description\n ...TaskFormDialogFragment\n assignedTo {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment TaskFormDialogFragment on Task {\n id\n description\n name\n state\n timeEstimate\n deadline\n assignedTo {\n id\n }\n measure {\n id\n }\n}\n"
} }
}; };
})(); })();

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<d72032da6df08b84ce9c7ec87b950d08>> * @generated SignedSource<<fdfadfbc4f5fdecf5f70a8c92d3ded88>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -145,7 +145,7 @@ v13 = {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fileUrl", "name": "fileName",
"storageKey": null "storageKey": null
}, },
v14 = { v14 = {
@@ -229,14 +229,14 @@ v23 = {
}, },
v24 = [ v24 = [
(v3/*: any*/), (v3/*: any*/),
(v13/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "kind": "ScalarField",
"name": "fileName", "name": "fileUrl",
"storageKey": null "storageKey": null
}, },
(v13/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
@@ -500,12 +500,31 @@ return {
"name": "reportName", "name": "reportName",
"storageKey": null "storageKey": null
}, },
(v13/*: any*/),
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "fileSize", "kind": "LinkedField",
"name": "file",
"plural": false,
"selections": [
(v13/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null "storageKey": null
}, },
(v9/*: any*/) (v9/*: any*/)
@@ -791,12 +810,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "4a112f861864990f5022d279ba8d43d4", "cacheID": "faee4f059ef9baab46d5f2420e36bb60",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "VendorGraphNodeQuery", "name": "VendorGraphNodeQuery",
"operationKind": "query", "operationKind": "query",
"text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n snapshotId\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorContactsTabFragment\n ...VendorServicesTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n ...VendorOverviewTabDataPrivacyAgreementFragment\n }\n id\n }\n viewer {\n user {\n id\n people(organizationId: $organizationId) {\n id\n }\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n\nfragment VendorContactsTabFragment on Vendor {\n contacts(first: 50) {\n edges {\n node {\n id\n ...VendorContactsTabFragment_contact\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorContactsTabFragment_contact on VendorContact {\n id\n fullName\n email\n phone\n role\n createdAt\n updatedAt\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor {\n dataPrivacyAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n createdAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment VendorServicesTabFragment on Vendor {\n services(first: 50) {\n edges {\n node {\n id\n ...VendorServicesTabFragment_service\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorServicesTabFragment_service on VendorService {\n id\n name\n description\n createdAt\n updatedAt\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n category\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n countries\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" "text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n snapshotId\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorContactsTabFragment\n ...VendorServicesTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n ...VendorOverviewTabDataPrivacyAgreementFragment\n }\n id\n }\n viewer {\n user {\n id\n people(organizationId: $organizationId) {\n id\n }\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n file {\n fileName\n mimeType\n size\n id\n }\n}\n\nfragment VendorContactsTabFragment on Vendor {\n contacts(first: 50) {\n edges {\n node {\n id\n ...VendorContactsTabFragment_contact\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorContactsTabFragment_contact on VendorContact {\n id\n fullName\n email\n phone\n role\n createdAt\n updatedAt\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor {\n dataPrivacyAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n createdAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment VendorServicesTabFragment on Vendor {\n services(first: 50) {\n edges {\n node {\n id\n ...VendorServicesTabFragment_service\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorServicesTabFragment_service on VendorService {\n id\n name\n description\n createdAt\n updatedAt\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n category\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n countries\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n"
} }
}; };
})(); })();

View File

@@ -50,7 +50,7 @@ function DownloadLink({ evidenceId, onClose }: Props) {
const evidence = data.node; const evidence = data.node;
useEffect(() => { useEffect(() => {
downloadFile(evidence.fileUrl, evidence.filename ?? "evidence"); downloadFile(evidence.file?.downloadUrl, evidence.file?.fileName ?? "evidence");
onClose(); onClose();
}, [evidence]); }, [evidence]);

View File

@@ -75,13 +75,13 @@ function EvidencePreviewContent({
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const isUriFile = const isUriFile =
evidence.mimeType === "text/uri-list" || evidence.mimeType === "text/uri"; evidence.file?.mimeType === "text/uri-list" || evidence.file?.mimeType === "text/uri";
useEffect(() => { useEffect(() => {
if (!isUriFile) { if (!isUriFile) {
return; return;
} }
const abortController = new AbortController(); const abortController = new AbortController();
fetchUrlFromUriFile(evidence.fileUrl ?? "", { fetchUrlFromUriFile(evidence.file?.downloadUrl ?? "", {
signal: abortController.signal, signal: abortController.signal,
}) })
.then((url) => { .then((url) => {
@@ -101,9 +101,9 @@ function EvidencePreviewContent({
return () => { return () => {
abortController.abort(); abortController.abort();
}; };
}, [evidence.fileUrl, isUriFile]); }, [evidence.file?.downloadUrl, isUriFile]);
if (!evidence.fileUrl) { if (!evidence.file?.downloadUrl) {
return null; return null;
} }
@@ -115,22 +115,22 @@ function EvidencePreviewContent({
); );
} }
if (evidence.mimeType?.startsWith("image/")) { if (evidence.file.mimeType?.startsWith("image/")) {
return ( return (
<img <img
src={evidence.fileUrl} src={evidence.file.downloadUrl}
alt={evidence.filename} alt={evidence.file.fileName}
className="max-h-[70vh] object-contain" className="max-h-[70vh] object-contain"
/> />
); );
} }
if (evidence.mimeType?.includes("pdf")) { if (evidence.file.mimeType?.includes("pdf")) {
return ( return (
<iframe <iframe
src={evidence.fileUrl} src={evidence.file.downloadUrl}
className="w-full h-[70vh]" className="w-full h-[70vh]"
title={evidence.filename} title={evidence.file.fileName}
/> />
); );
} }
@@ -141,10 +141,10 @@ function EvidencePreviewContent({
<p className="text-txt-secondary text-center"> <p className="text-txt-secondary text-center">
{__("Preview not available for this file type") + {__("Preview not available for this file type") +
" " + " " +
evidence.mimeType} evidence.file.mimeType}
</p> </p>
<Button asChild variant="secondary" icon={IconArrowInbox}> <Button asChild variant="secondary" icon={IconArrowInbox}>
<a href={evidence.fileUrl} target="_blank"> <a href={evidence.file.downloadUrl} target="_blank">
{__("Download File")} {__("Download File")}
</a> </a>
</Button> </Button>

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<9297f85591b8ab4723a207f6db6a27f6>> * @generated SignedSource<<3ed9b93b29f7a5117adbb33ad359f34e>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -148,15 +148,34 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "filename", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "size", "kind": "ScalarField",
"name": "fileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null "storageKey": null
}, },
{ {
@@ -172,13 +191,6 @@ return {
"kind": "ScalarField", "kind": "ScalarField",
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
} }
], ],
"storageKey": null "storageKey": null
@@ -208,12 +220,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "5206b6576cacb45f5b42305bb1bb7df1", "cacheID": "4224b79c60bb9be5f4768da436659656",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "CreateEvidenceDialogUploadMutation", "name": "CreateEvidenceDialogUploadMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation CreateEvidenceDialogUploadMutation(\n $input: UploadMeasureEvidenceInput!\n) {\n uploadMeasureEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n ...MeasureEvidencesTabFragment_evidence\n }\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n filename\n size\n type\n createdAt\n mimeType\n}\n" "text": "mutation CreateEvidenceDialogUploadMutation(\n $input: UploadMeasureEvidenceInput!\n) {\n uploadMeasureEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n ...MeasureEvidencesTabFragment_evidence\n }\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n type\n createdAt\n}\n"
} }
}; };
})(); })();

View File

@@ -56,8 +56,11 @@ export const evidencesFragment = graphql`
edges { edges {
node { node {
id id
filename file {
mimeType fileName
mimeType
size
}
...MeasureEvidencesTabFragment_evidence ...MeasureEvidencesTabFragment_evidence
} }
} }
@@ -68,11 +71,13 @@ export const evidencesFragment = graphql`
export const evidenceFragment = graphql` export const evidenceFragment = graphql`
fragment MeasureEvidencesTabFragment_evidence on Evidence { fragment MeasureEvidencesTabFragment_evidence on Evidence {
id id
filename file {
size fileName
mimeType
size
}
type type
createdAt createdAt
mimeType
} }
`; `;
@@ -150,7 +155,7 @@ export default function MeasureEvidencesTab() {
navigate(baseUrl); navigate(baseUrl);
}} }}
evidenceId={evidence.id} evidenceId={evidence.id}
filename={evidence.filename} filename={evidence.file?.fileName || ""}
/> />
)} )}
{!isSnapshotMode && ( {!isSnapshotMode && (
@@ -207,7 +212,7 @@ function EvidenceRow(props: {
__( __(
'This will permanently delete the evidence "%s". This action cannot be undone.' 'This will permanently delete the evidence "%s". This action cannot be undone.'
), ),
evidence.filename evidence.file?.fileName
), ),
} }
); );
@@ -226,9 +231,9 @@ function EvidenceRow(props: {
/> />
)} )}
<Tr to={evidenceUrl}> <Tr to={evidenceUrl}>
<Td>{evidence.filename}</Td> <Td>{evidence.file?.fileName}</Td>
<Td>{fileType(__, evidence)}</Td> <Td>{fileType(__, {type: evidence.type, mimeType: evidence.file?.mimeType || ''})}</Td>
<Td>{fileSize(__, evidence.size)}</Td> <Td>{fileSize(__, evidence.file?.size || 0)}</Td>
<Td>{formatDate(evidence.createdAt)}</Td> <Td>{formatDate(evidence.createdAt)}</Td>
<Td noLink> <Td noLink>
{!props.hideActions && ( {!props.hideActions && (

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<6c96b0b565302f1ec1601561087b30bd>> * @generated SignedSource<<95a549108a1a26ccfaf2d8aae42cea73>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -15,9 +15,12 @@ export type MeasureEvidencesTabFragment$data = {
readonly __id: string; readonly __id: string;
readonly edges: ReadonlyArray<{ readonly edges: ReadonlyArray<{
readonly node: { readonly node: {
readonly filename: string; readonly file: {
readonly fileName: string;
readonly mimeType: string;
readonly size: any;
} | null | undefined;
readonly id: string; readonly id: string;
readonly mimeType: string;
readonly " $fragmentSpreads": FragmentRefs<"MeasureEvidencesTabFragment_evidence">; readonly " $fragmentSpreads": FragmentRefs<"MeasureEvidencesTabFragment_evidence">;
}; };
}>; }>;
@@ -140,15 +143,33 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "filename", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "mimeType", "kind": "ScalarField",
"name": "fileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
}
],
"storageKey": null "storageKey": null
}, },
{ {
@@ -236,6 +257,6 @@ return {
}; };
})(); })();
(node as any).hash = "8e97649ca88af04aa1184e68a126cc36"; (node as any).hash = "c80a159cbb4ac8b8855875eff00d1255";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<60897aba5bbafb70288358e56b0a442a>> * @generated SignedSource<<0fb98c755d9aacac0ef1d508af9e7edc>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -13,10 +13,12 @@ export type EvidenceType = "FILE" | "LINK";
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type MeasureEvidencesTabFragment_evidence$data = { export type MeasureEvidencesTabFragment_evidence$data = {
readonly createdAt: any; readonly createdAt: any;
readonly filename: string; readonly file: {
readonly fileName: string;
readonly mimeType: string;
readonly size: any;
} | null | undefined;
readonly id: string; readonly id: string;
readonly mimeType: string;
readonly size: number;
readonly type: EvidenceType; readonly type: EvidenceType;
readonly " $fragmentType": "MeasureEvidencesTabFragment_evidence"; readonly " $fragmentType": "MeasureEvidencesTabFragment_evidence";
}; };
@@ -41,15 +43,33 @@ const node: ReaderFragment = {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "filename", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "size", "kind": "ScalarField",
"name": "fileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
}
],
"storageKey": null "storageKey": null
}, },
{ {
@@ -65,19 +85,12 @@ const node: ReaderFragment = {
"kind": "ScalarField", "kind": "ScalarField",
"name": "createdAt", "name": "createdAt",
"storageKey": null "storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
} }
], ],
"type": "Evidence", "type": "Evidence",
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "4b3289f3cb9d29cb48f1a370d943cd0e"; (node as any).hash = "441ff1dd2dd62fea27050c5f550b27ea";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<b2d32283d91e7ef9da2eeda3c789867f>> * @generated SignedSource<<b182b11e656579608aba8fb03faa0ef6>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -215,22 +215,34 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "filename", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "mimeType", "kind": "ScalarField",
"storageKey": null "name": "fileName",
}, "storageKey": null
{ },
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "size", "kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
(v12/*: any*/)
],
"storageKey": null "storageKey": null
}, },
{ {
@@ -336,16 +348,16 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "9899e64b590ff4c76ca994845611d77a", "cacheID": "45ba257dabcf1cc7dd4aa05b699c280e",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "MeasureEvidencesTabQuery", "name": "MeasureEvidencesTabQuery",
"operationKind": "query", "operationKind": "query",
"text": "query MeasureEvidencesTabQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: EvidenceOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MeasureEvidencesTabFragment_16fISc\n id\n }\n}\n\nfragment MeasureEvidencesTabFragment_16fISc on Measure {\n id\n evidences(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n filename\n mimeType\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n filename\n size\n type\n createdAt\n mimeType\n}\n" "text": "query MeasureEvidencesTabQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: EvidenceOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MeasureEvidencesTabFragment_16fISc\n id\n }\n}\n\nfragment MeasureEvidencesTabFragment_16fISc on Measure {\n id\n evidences(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n ...MeasureEvidencesTabFragment_evidence\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment MeasureEvidencesTabFragment_evidence on Evidence {\n id\n file {\n fileName\n mimeType\n size\n id\n }\n type\n createdAt\n}\n"
} }
}; };
})(); })();
(node as any).hash = "8e97649ca88af04aa1184e68a126cc36"; (node as any).hash = "c80a159cbb4ac8b8855875eff00d1255";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<22e7411298e2df9b5f5eae17bb0a510f>> * @generated SignedSource<<f851ea2f7b0fa09f081a26967bb7766e>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -371,15 +371,34 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "fileUrl", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "fileSize", "kind": "ScalarField",
"name": "fileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
(v2/*: any*/)
],
"storageKey": null "storageKey": null
}, },
(v7/*: any*/) (v7/*: any*/)
@@ -523,12 +542,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "0cb0f301c3022d933fd9ee0d6319d3e4", "cacheID": "d0d8cc2e128b7a7b31f5e9c86e427a7f",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "ImportAssessmentDialogMutation", "name": "ImportAssessmentDialogMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation ImportAssessmentDialogMutation(\n $input: AssessVendorInput!\n) {\n assessVendor(input: $input) {\n vendor {\n id\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorRiskAssessmentTabFragment\n }\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n createdAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n category\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n countries\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n" "text": "mutation ImportAssessmentDialogMutation(\n $input: AssessVendorInput!\n) {\n assessVendor(input: $input) {\n vendor {\n id\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorRiskAssessmentTabFragment\n }\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n file {\n fileName\n mimeType\n size\n id\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n createdAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n category\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n countries\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n"
} }
}; };
})(); })();

View File

@@ -55,8 +55,11 @@ const complianceReportFragment = graphql`
reportDate reportDate
validUntil validUntil
reportName reportName
fileUrl file {
fileSize fileName
mimeType
size
}
} }
`; `;
@@ -208,7 +211,7 @@ function ReportRow(props: ReportRowProps) {
<Td>{report.reportName}</Td> <Td>{report.reportName}</Td>
<Td>{formatDate(report.reportDate)}</Td> <Td>{formatDate(report.reportDate)}</Td>
<Td>{formatDate(report.validUntil)}</Td> <Td>{formatDate(report.validUntil)}</Td>
<Td>{fileSize(__, report.fileSize)}</Td> <Td>{fileSize(__, report.file?.size)}</Td>
{!props.isSnapshotMode && ( {!props.isSnapshotMode && (
<Td width={50} className="text-end"> <Td width={50} className="text-end">
<ActionDropdown> <ActionDropdown>

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<10fdd09e95fc5f2995b91c73cf9fb945>> * @generated SignedSource<<5fd3a3293bd92112440eef3fd292fdc1>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -236,15 +236,34 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "fileUrl", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "fileSize", "kind": "ScalarField",
"name": "fileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
(v12/*: any*/)
],
"storageKey": null "storageKey": null
}, },
(v11/*: any*/) (v11/*: any*/)
@@ -336,12 +355,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "c819e002dfe9f20004fb5bfb4f95b143", "cacheID": "594e7edf24b110430566c9a21d1b5d29",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "ComplianceReportListQuery", "name": "ComplianceReportListQuery",
"operationKind": "query", "operationKind": "query",
"text": "query ComplianceReportListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: VendorComplianceReportOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorComplianceTabFragment_16fISc\n id\n }\n}\n\nfragment VendorComplianceTabFragment_16fISc on Vendor {\n complianceReports(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n" "text": "query ComplianceReportListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: VendorComplianceReportOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorComplianceTabFragment_16fISc\n id\n }\n}\n\nfragment VendorComplianceTabFragment_16fISc on Vendor {\n complianceReports(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n file {\n fileName\n mimeType\n size\n id\n }\n}\n"
} }
}; };
})(); })();

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<4c861a832ad2f79bd9d6ddcc364f6898>> * @generated SignedSource<<8cf7478142bb658e6c8d189652bbdfea>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -11,8 +11,11 @@
import { ReaderFragment } from 'relay-runtime'; import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime"; import { FragmentRefs } from "relay-runtime";
export type VendorComplianceTabFragment_report$data = { export type VendorComplianceTabFragment_report$data = {
readonly fileSize: any; readonly file: {
readonly fileUrl: string; readonly fileName: string;
readonly mimeType: string;
readonly size: any;
} | null | undefined;
readonly id: string; readonly id: string;
readonly reportDate: any; readonly reportDate: any;
readonly reportName: string; readonly reportName: string;
@@ -61,15 +64,33 @@ const node: ReaderFragment = {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "fileUrl", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "fileSize", "kind": "ScalarField",
"name": "fileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
}
],
"storageKey": null "storageKey": null
} }
], ],
@@ -77,6 +98,6 @@ const node: ReaderFragment = {
"abstractKey": null "abstractKey": null
}; };
(node as any).hash = "92ad78c9e4b4add9b1f622485d81a6ff"; (node as any).hash = "18f52de61577c8b1c61784d2404c24ae";
export default node; export default node;

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<6093b0bc54fa90962376639ec87fa149>> * @generated SignedSource<<68f2a5a67287a554c3f45ddfc856b6ea>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -172,15 +172,34 @@ return {
{ {
"alias": null, "alias": null,
"args": null, "args": null,
"kind": "ScalarField", "concreteType": "File",
"name": "fileUrl", "kind": "LinkedField",
"storageKey": null "name": "file",
}, "plural": false,
{ "selections": [
"alias": null, {
"args": null, "alias": null,
"kind": "ScalarField", "args": null,
"name": "fileSize", "kind": "ScalarField",
"name": "fileName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null "storageKey": null
} }
], ],
@@ -211,12 +230,12 @@ return {
] ]
}, },
"params": { "params": {
"cacheID": "fb9be687deba8567502e207b0969b2b5", "cacheID": "193fdb014a3493e0b0d56a96309afae3",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "VendorComplianceTabUploadReportMutation", "name": "VendorComplianceTabUploadReportMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation VendorComplianceTabUploadReportMutation(\n $input: UploadVendorComplianceReportInput!\n) {\n uploadVendorComplianceReport(input: $input) {\n vendorComplianceReportEdge {\n node {\n id\n ...VendorComplianceTabFragment_report\n }\n }\n }\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n fileUrl\n fileSize\n}\n" "text": "mutation VendorComplianceTabUploadReportMutation(\n $input: UploadVendorComplianceReportInput!\n) {\n uploadVendorComplianceReport(input: $input) {\n vendorComplianceReportEdge {\n node {\n id\n ...VendorComplianceTabFragment_report\n }\n }\n }\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n file {\n fileName\n mimeType\n size\n id\n }\n}\n"
} }
}; };
})(); })();

View File

@@ -28,20 +28,17 @@ import (
type ( type (
Evidence struct { Evidence struct {
ID gid.GID `db:"id"` ID gid.GID `db:"id"`
MeasureID gid.GID `db:"measure_id"` MeasureID gid.GID `db:"measure_id"`
TaskID *gid.GID `db:"task_id"` TaskID *gid.GID `db:"task_id"`
State EvidenceState `db:"state"` State EvidenceState `db:"state"`
ReferenceID string `db:"reference_id"` ReferenceID string `db:"reference_id"`
Type EvidenceType `db:"type"` Type EvidenceType `db:"type"`
ObjectKey string `db:"object_key"` URL string `db:"url"`
MimeType string `db:"mime_type"` EvidenceFileId *gid.GID `db:"evidence_file_id"`
Size uint64 `db:"size"` Description string `db:"description"`
Filename string `db:"filename"` CreatedAt time.Time `db:"created_at"`
URL string `db:"url"` UpdatedAt time.Time `db:"updated_at"`
Description string `db:"description"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
} }
Evidences []*Evidence Evidences []*Evidence
@@ -69,13 +66,10 @@ INSERT INTO
measure_id, measure_id,
task_id, task_id,
reference_id, reference_id,
object_key,
mime_type,
size,
state, state,
type, type,
filename,
url, url,
evidence_file_id,
description, description,
created_at, created_at,
updated_at updated_at
@@ -86,13 +80,10 @@ VALUES (
@measure_id, @measure_id,
@task_id, @task_id,
@reference_id, @reference_id,
@object_key,
@mime_type,
@size,
@state, @state,
@type, @type,
@filename,
@url, @url,
@evidence_file_id,
@description, @description,
@created_at, @created_at,
@updated_at @updated_at
@@ -105,21 +96,18 @@ WHERE evidences.state = 'REQUESTED';
` `
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(), "tenant_id": scope.GetTenantID(),
"evidence_id": e.ID, "evidence_id": e.ID,
"measure_id": e.MeasureID, "measure_id": e.MeasureID,
"task_id": e.TaskID, "task_id": e.TaskID,
"reference_id": e.ReferenceID, "reference_id": e.ReferenceID,
"object_key": e.ObjectKey, "evidence_file_id": e.EvidenceFileId,
"mime_type": e.MimeType, "created_at": e.CreatedAt,
"size": e.Size, "updated_at": e.UpdatedAt,
"filename": e.Filename, "state": e.State,
"created_at": e.CreatedAt, "type": e.Type,
"updated_at": e.UpdatedAt, "url": e.URL,
"state": e.State, "description": e.Description,
"type": e.Type,
"url": e.URL,
"description": e.Description,
} }
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
@@ -138,13 +126,10 @@ INSERT INTO
measure_id, measure_id,
task_id, task_id,
reference_id, reference_id,
object_key,
mime_type,
size,
state, state,
type, type,
filename,
url, url,
evidence_file_id,
description, description,
created_at, created_at,
updated_at updated_at
@@ -155,13 +140,10 @@ VALUES (
@measure_id, @measure_id,
@task_id, @task_id,
@reference_id, @reference_id,
@object_key,
@mime_type,
@size,
@state, @state,
@type, @type,
@filename,
@url, @url,
@evidence_file_id,
@description, @description,
@created_at, @created_at,
@updated_at @updated_at
@@ -169,21 +151,18 @@ VALUES (
` `
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(), "tenant_id": scope.GetTenantID(),
"evidence_id": e.ID, "evidence_id": e.ID,
"measure_id": e.MeasureID, "measure_id": e.MeasureID,
"task_id": e.TaskID, "task_id": e.TaskID,
"reference_id": e.ReferenceID, "reference_id": e.ReferenceID,
"object_key": e.ObjectKey, "evidence_file_id": e.EvidenceFileId,
"mime_type": e.MimeType, "created_at": e.CreatedAt,
"size": e.Size, "updated_at": e.UpdatedAt,
"filename": e.Filename, "state": e.State,
"created_at": e.CreatedAt, "type": e.Type,
"updated_at": e.UpdatedAt, "url": e.URL,
"state": e.State, "description": e.Description,
"type": e.Type,
"url": e.URL,
"description": e.Description,
} }
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
@@ -203,11 +182,8 @@ SELECT
reference_id, reference_id,
state, state,
type, type,
object_key,
mime_type,
size,
filename,
url, url,
evidence_file_id,
description, description,
created_at, created_at,
updated_at updated_at
@@ -286,11 +262,8 @@ SELECT
reference_id, reference_id,
state, state,
type, type,
object_key,
mime_type,
size,
filename,
url, url,
evidence_file_id,
description, description,
created_at, created_at,
updated_at updated_at
@@ -370,11 +343,8 @@ SELECT
reference_id, reference_id,
state, state,
type, type,
object_key,
mime_type,
size,
filename,
url, url,
evidence_file_id,
description, description,
created_at, created_at,
updated_at updated_at
@@ -418,10 +388,7 @@ UPDATE
SET SET
type = @type, type = @type,
state = @state, state = @state,
object_key = @object_key, evidence_file_id = @evidence_file_id,
mime_type = @mime_type,
size = @size,
filename = @filename,
url = @url, url = @url,
description = @description, description = @description,
updated_at = @updated_at updated_at = @updated_at
@@ -433,16 +400,13 @@ WHERE
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"evidence_id": e.ID, "evidence_id": e.ID,
"type": e.Type, "type": e.Type,
"state": e.State, "state": e.State,
"object_key": e.ObjectKey, "evidence_file_id": e.EvidenceFileId,
"mime_type": e.MimeType, "url": e.URL,
"size": e.Size, "description": e.Description,
"filename": e.Filename, "updated_at": e.UpdatedAt,
"url": e.URL,
"description": e.Description,
"updated_at": e.UpdatedAt,
} }
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())
@@ -454,13 +418,14 @@ func (e Evidence) Delete(
ctx context.Context, ctx context.Context,
conn pg.Conn, conn pg.Conn,
scope Scoper, scope Scoper,
) error { ) (*string, error) {
q := ` q := `
DELETE FROM DELETE FROM
evidences evidences
WHERE WHERE
%s %s
AND id = @evidence_id AND id = @evidence_id
RETURNING evidence_file_id;
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -468,6 +433,23 @@ WHERE
args := pgx.StrictNamedArgs{"evidence_id": e.ID} args := pgx.StrictNamedArgs{"evidence_id": e.ID}
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args) var evidenceFileId *gid.GID
return err err := conn.QueryRow(ctx, q, args).Scan(&evidenceFileId)
if err != nil {
return nil, fmt.Errorf("failed to delete evidence: %w", err)
}
if evidenceFileId != nil {
var err error
var fileKey *string
file := &File{ID: *evidenceFileId}
if fileKey, err = file.HardDelete(ctx, conn, scope); err != nil {
return nil, fmt.Errorf("failed to hard delete evidence file: %w", err)
}
return fileKey, nil
}
return nil, err
} }

View File

@@ -135,3 +135,39 @@ VALUES (
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
return err return err
} }
func (f File) SoftDelete(ctx context.Context, conn pg.Conn, scope Scoper) error {
q := `
UPDATE files
SET deleted_at = NOW()
WHERE %s
AND id = @file_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"file_id": f.ID}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
return err
}
func (f File) HardDelete(ctx context.Context, conn pg.Conn, scope Scoper) (*string, error) {
q := `
DELETE FROM files
WHERE %s
AND id = @file_id
RETURNING file_key
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"file_id": f.ID}
maps.Copy(args, scope.SQLArguments())
var vcrFileKey *string
err := conn.QueryRow(ctx, q, args).Scan(&vcrFileKey)
return vcrFileKey, err
}

View File

@@ -0,0 +1,41 @@
ALTER TABLE evidences
ADD COLUMN evidence_file_id text;
/* 10 is for FileEntityType */
WITH
evidence_files AS (
SELECT
e.id as evidence_id,
generate_gid(decode_base64_unpadded(e.tenant_id), 25) as file_id,
e.tenant_id,
'probod' as bucket_name,
e.mime_type,
e.filename,
e.object_key,
e.size,
e.created_at,
e.updated_at
FROM evidences e
),
inserted_files AS (
INSERT INTO files (id, tenant_id, bucket_name, mime_type, file_name, file_key, file_size, created_at, updated_at)
SELECT file_id, tenant_id, bucket_name, mime_type, filename, object_key::uuid, size, created_at, updated_at
FROM evidence_files
RETURNING id, tenant_id
)
SELECT ef.evidence_id, ef.file_id
INTO TEMP TABLE file_evidence_mapping
FROM evidence_files ef;
UPDATE evidences
SET evidence_file_id = fm.file_id
FROM file_evidence_mapping fm
WHERE evidences.id = fm.evidence_id;
ALTER TABLE evidences
ALTER COLUMN filename DROP NOT NULL,
ALTER COLUMN mime_type DROP NOT NULL,
ALTER COLUMN size DROP NOT NULL,
ALTER COLUMN oject_key DROP NOT NULL;

View File

@@ -0,0 +1,41 @@
ALTER TABLE
vendor_compliance_reports
ADD COLUMN report_file_id text
REFERENCES files(id)
ON UPDATE CASCADE
ON DELETE RESTRICT;
WITH
/* 25 is for FileEntityType */
vcr_files AS (
SELECT
vcr.id as report_id,
generate_gid(decode_base64_unpadded(vcr.tenant_id), 25) as file_id,
vcr.tenant_id,
'probod' as bucket_name,
'application/pdf' as mime_type,
vcr.report_name,
vcr.file_key,
vcr.file_size,
vcr.created_at,
vcr.updated_at
FROM vendor_compliance_reports vcr
),
inserted_files AS (
INSERT INTO files (id, tenant_id, bucket_name, mime_type, file_name, file_key, file_size, created_at, updated_at)
SELECT file_id, tenant_id, bucket_name, mime_type, report_name, file_key::uuid, file_size, created_at, updated_at
FROM vcr_files
RETURNING id, tenant_id
)
SELECT vf.report_id, vf.file_id
INTO TEMP TABLE file_vcr_mapping
FROM vcr_files vf;
UPDATE vendor_compliance_reports
SET report_file_id = fv.file_id
FROM file_vcr_mapping fv
WHERE vendor_compliance_reports.id = fv.report_id;
ALTER TABLE vendor_compliance_reports
ALTER COLUMN file_key DROP NOT NULL,
ALTER COLUMN file_size DROP NOT NULL;

View File

@@ -0,0 +1,7 @@
ALTER TABLE
evidences
ADD CONSTRAINT fk_evidence_file
FOREIGN KEY (evidence_file_id)
REFERENCES files(id)
ON UPDATE CASCADE
ON DELETE RESTRICT;

View File

@@ -28,17 +28,16 @@ import (
type ( type (
VendorComplianceReport struct { VendorComplianceReport struct {
ID gid.GID ID gid.GID
VendorID gid.GID VendorID gid.GID
ReportDate time.Time ReportDate time.Time
ValidUntil *time.Time ValidUntil *time.Time
ReportName string ReportName string
FileKey string ReportFileId *gid.GID
FileSize int64 SnapshotID *gid.GID
SnapshotID *gid.GID SourceID *gid.GID
SourceID *gid.GID CreatedAt time.Time
CreatedAt time.Time UpdatedAt time.Time
UpdatedAt time.Time
} }
VendorComplianceReports []*VendorComplianceReport VendorComplianceReports []*VendorComplianceReport
@@ -69,8 +68,7 @@ SELECT
report_date, report_date,
valid_until, valid_until,
report_name, report_name,
file_key, report_file_id,
file_size,
snapshot_id, snapshot_id,
source_id, source_id,
created_at, created_at,
@@ -117,8 +115,7 @@ SELECT
report_date, report_date,
valid_until, valid_until,
report_name, report_name,
file_key, report_file_id,
file_size,
snapshot_id, snapshot_id,
source_id, source_id,
created_at, created_at,
@@ -165,8 +162,7 @@ INSERT INTO
report_date, report_date,
valid_until, valid_until,
report_name, report_name,
file_key, report_file_id,
file_size,
created_at, created_at,
updated_at updated_at
) )
@@ -177,23 +173,21 @@ VALUES (
@report_date, @report_date,
@valid_until, @valid_until,
@report_name, @report_name,
@file_key, @report_file_id,
@file_size,
@created_at, @created_at,
@updated_at @updated_at
) )
` `
args := pgx.NamedArgs{ args := pgx.NamedArgs{
"id": vcr.ID, "id": vcr.ID,
"tenant_id": scope.GetTenantID(), "tenant_id": scope.GetTenantID(),
"vendor_id": vcr.VendorID, "vendor_id": vcr.VendorID,
"report_date": vcr.ReportDate, "report_date": vcr.ReportDate,
"valid_until": vcr.ValidUntil, "valid_until": vcr.ValidUntil,
"report_name": vcr.ReportName, "report_name": vcr.ReportName,
"file_key": vcr.FileKey, "report_file_id": vcr.ReportFileId,
"file_size": vcr.FileSize, "created_at": vcr.CreatedAt,
"created_at": vcr.CreatedAt, "updated_at": vcr.UpdatedAt,
"updated_at": vcr.UpdatedAt,
} }
_, err := conn.Exec(ctx, q, args) _, err := conn.Exec(ctx, q, args)
@@ -213,6 +207,7 @@ WHERE
%s %s
AND id = @id AND id = @id
AND snapshot_id IS NULL AND snapshot_id IS NULL
RETURNING report_file_id
` `
q = fmt.Sprintf(q, scope.SQLFragment()) q = fmt.Sprintf(q, scope.SQLFragment())
@@ -220,8 +215,20 @@ WHERE
args := pgx.StrictNamedArgs{"id": vcr.ID} args := pgx.StrictNamedArgs{"id": vcr.ID}
maps.Copy(args, scope.SQLArguments()) maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args) var vcrFileId *gid.GID
return err err := conn.QueryRow(ctx, q, args).Scan(&vcrFileId)
if err != nil {
return fmt.Errorf("failed to delete vendor compliance report: %w", err)
}
if vcrFileId != nil {
file := &File{ID: *vcrFileId}
if err = file.SoftDelete(ctx, conn, scope); err != nil {
return fmt.Errorf("failed to soft delete vendor compliance file: %w", err)
}
}
return nil
} }
func (vcrs VendorComplianceReports) InsertVendorSnapshots( func (vcrs VendorComplianceReports) InsertVendorSnapshots(
@@ -247,8 +254,7 @@ INSERT INTO vendor_compliance_reports (
report_date, report_date,
valid_until, valid_until,
report_name, report_name,
file_key, report_file_id,
file_size,
created_at, created_at,
updated_at updated_at
) )
@@ -261,8 +267,7 @@ SELECT
vcr.report_date, vcr.report_date,
vcr.valid_until, vcr.valid_until,
vcr.report_name, vcr.report_name,
vcr.file_key, vcr.report_file_id,
vcr.file_size,
vcr.created_at, vcr.created_at,
vcr.updated_at vcr.updated_at
FROM vendor_compliance_reports vcr FROM vendor_compliance_reports vcr

View File

@@ -65,6 +65,7 @@ var FileTypes = []FileType{
{MimeType: "text/plain", Extensions: []string{".txt"}, Category: CategoryText}, {MimeType: "text/plain", Extensions: []string{".txt"}, Category: CategoryText},
{MimeType: "text/x-log", Extensions: []string{".log"}, Category: CategoryText}, {MimeType: "text/x-log", Extensions: []string{".log"}, Category: CategoryText},
{MimeType: "text/uri-list", Extensions: []string{".uri"}, Category: CategoryText}, {MimeType: "text/uri-list", Extensions: []string{".uri"}, Category: CategoryText},
{MimeType: "text/uri-list; charset=utf-8", Extensions: []string{".uri"}, Category: CategoryText},
// Image types // Image types
{MimeType: "image/jpeg", Extensions: []string{".jpg", ".jpeg"}, Category: CategoryImage}, {MimeType: "image/jpeg", Extensions: []string{".jpg", ".jpeg"}, Category: CategoryImage},

View File

@@ -15,17 +15,10 @@
package probo package probo
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"mime"
"net/url"
"path/filepath"
"time" "time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/filevalidation" "github.com/getprobo/probo/pkg/filevalidation"
"github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/gid"
@@ -40,36 +33,10 @@ type (
fileValidator *filevalidation.FileValidator fileValidator *filevalidation.FileValidator
} }
File struct {
Content io.Reader
Filename string
Size int64
ContentType string
}
RequestEvidenceRequest struct {
MeasureID *gid.GID
TaskID *gid.GID
Type coredata.EvidenceType
Name string
Description string
}
FulfilledEvidenceRequest struct {
EvidenceID gid.GID
File io.Reader
URL *string
Filename *string
}
UploadTaskEvidenceRequest struct {
TaskID gid.GID
File File
}
UploadMeasureEvidenceRequest struct { UploadMeasureEvidenceRequest struct {
MeasureID gid.GID MeasureID gid.GID
File File URL *string
File FileUpload
} }
) )
@@ -82,256 +49,8 @@ func (s EvidenceService) Get(
err := s.svc.pg.WithConn( err := s.svc.pg.WithConn(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
return evidence.LoadByID(ctx, conn, s.svc.scope, evidenceID) if err := evidence.LoadByID(ctx, conn, s.svc.scope, evidenceID); err != nil {
}, return fmt.Errorf("cannot load evidence %w", err)
)
if err != nil {
return nil, err
}
return evidence, nil
}
func (s EvidenceService) Request(
ctx context.Context,
req RequestEvidenceRequest,
) (*coredata.Evidence, error) {
evidenceID := gid.New(s.svc.scope.GetTenantID(), coredata.EvidenceEntityType)
now := time.Now()
evidence := &coredata.Evidence{
ID: evidenceID,
State: coredata.EvidenceStateRequested,
Type: req.Type,
Filename: req.Name,
Description: req.Description,
CreatedAt: now,
UpdatedAt: now,
}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
task := &coredata.Task{}
if req.TaskID != nil {
if err := task.LoadByID(ctx, conn, s.svc.scope, *req.TaskID); err != nil {
return fmt.Errorf("cannot load task: %w", err)
}
if task.MeasureID == nil {
return fmt.Errorf("task %q has no measure", req.TaskID)
}
evidence.TaskID = req.TaskID
evidence.MeasureID = *task.MeasureID
} else if req.MeasureID != nil {
evidence.MeasureID = *req.MeasureID
} else {
return fmt.Errorf("measure id or task id is required")
}
return evidence.Insert(ctx, conn, s.svc.scope)
},
)
if err != nil {
return nil, fmt.Errorf("cannot insert evidence: %w", err)
}
return evidence, nil
}
func (s EvidenceService) Fulfill(
ctx context.Context,
req FulfilledEvidenceRequest,
) (*coredata.Evidence, error) {
evidence := &coredata.Evidence{}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := evidence.LoadByID(ctx, conn, s.svc.scope, req.EvidenceID); err != nil {
return fmt.Errorf("cannot load evidence: %w", err)
}
measure := &coredata.Measure{}
if err := measure.LoadByID(ctx, conn, s.svc.scope, evidence.MeasureID); err != nil {
return fmt.Errorf("cannot load measure: %w", err)
}
evidence.State = coredata.EvidenceStateFulfilled
if req.File != nil {
evidence.Type = coredata.EvidenceTypeFile
var fileSize int64
var fileContent io.ReadSeeker
if seeker, ok := req.File.(io.Seeker); ok {
size, err := seeker.Seek(0, io.SeekEnd)
if err != nil {
return fmt.Errorf("cannot determine file size: %w", err)
}
_, err = seeker.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("cannot reset file position: %w", err)
}
fileSize = size
fileContent = req.File.(io.ReadSeeker)
} else {
buf, err := io.ReadAll(req.File)
if err != nil {
return fmt.Errorf("cannot read file: %w", err)
}
fileSize = int64(len(buf))
fileContent = bytes.NewReader(buf)
}
contentType := "application/octet-stream"
if req.Filename != nil {
evidence.Filename = *req.Filename
if detectedType := mime.TypeByExtension(filepath.Ext(*req.Filename)); detectedType != "" {
contentType = detectedType
}
}
if err := s.fileValidator.Validate(evidence.Filename, contentType, fileSize); err != nil {
return err
}
objectKey, err := uuid.NewV7()
if err != nil {
return fmt.Errorf("cannot generate object key: %w", err)
}
_, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
Body: fileContent,
ContentType: aws.String(contentType),
Metadata: map[string]string{
"type": "evidence",
"evidence-id": req.EvidenceID.String(),
"organization-id": measure.OrganizationID.String(),
},
})
if err != nil {
return fmt.Errorf("cannot upload file to S3: %w", err)
}
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
})
if err != nil {
return fmt.Errorf("cannot get object metadata: %w", err)
}
evidence.ObjectKey = objectKey.String()
evidence.MimeType = contentType
evidence.Size = uint64(*headOutput.ContentLength)
} else if req.URL != nil {
evidence.Type = coredata.EvidenceTypeLink
evidence.URL = *req.URL
}
evidence.UpdatedAt = time.Now()
return evidence.Update(ctx, conn, s.svc.scope)
},
)
if err != nil {
return nil, fmt.Errorf("cannot update evidence: %w", err)
}
return evidence, nil
}
func (s EvidenceService) UploadTaskEvidence(
ctx context.Context,
req UploadTaskEvidenceRequest,
) (*coredata.Evidence, error) {
now := time.Now()
evidenceID := gid.New(s.svc.scope.GetTenantID(), coredata.EvidenceEntityType)
referenceID, err := uuid.NewV4()
if err != nil {
return nil, fmt.Errorf("cannot generate reference id: %w", err)
}
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: &req.TaskID,
State: coredata.EvidenceStateFulfilled,
ReferenceID: "custom-evidence-" + referenceID.String(),
Type: coredata.EvidenceTypeFile,
Filename: req.File.Filename,
CreatedAt: now,
UpdatedAt: now,
}
if req.File.ContentType == "" {
req.File.ContentType = "application/octet-stream"
}
if err := s.fileValidator.Validate(req.File.Filename, req.File.ContentType, req.File.Size); err != nil {
return nil, err
}
var measureOrganizationID gid.GID
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
task := &coredata.Task{}
if err := task.LoadByID(ctx, conn, s.svc.scope, req.TaskID); err != nil {
return fmt.Errorf("cannot load task %q: %w", req.TaskID, err)
}
if task.MeasureID == nil {
return fmt.Errorf("task %q has no measure", req.TaskID)
}
measure := &coredata.Measure{}
if err := measure.LoadByID(ctx, conn, s.svc.scope, *task.MeasureID); err != nil {
return fmt.Errorf("cannot load measure: %w", err)
}
measureOrganizationID = measure.OrganizationID
objectKey, err := uuid.NewV7()
if err != nil {
return fmt.Errorf("cannot generate object key: %w", err)
}
_, err = s.svc.s3.PutObject(
ctx,
&s3.PutObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
Body: req.File.Content,
ContentType: aws.String(req.File.ContentType),
Metadata: map[string]string{
"type": "evidence",
"evidence-id": evidenceID.String(),
"organization-id": measureOrganizationID.String(),
},
},
)
if err != nil {
return fmt.Errorf("cannot upload file to S3: %w", err)
}
evidence.ObjectKey = objectKey.String()
evidence.MimeType = req.File.ContentType
evidence.Size = uint64(req.File.Size)
evidence.MeasureID = *task.MeasureID
if err := evidence.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert evidence: %w", err)
} }
return nil return nil
@@ -339,8 +58,7 @@ func (s EvidenceService) UploadTaskEvidence(
) )
if err != nil { if err != nil {
// TODO try do delete file from s3 if it's a file type return nil, fmt.Errorf("cannot load evidence: %w", err)
return nil, err
} }
return evidence, nil return evidence, nil
@@ -364,53 +82,37 @@ func (s EvidenceService) UploadMeasureEvidence(
State: coredata.EvidenceStateFulfilled, State: coredata.EvidenceStateFulfilled,
ReferenceID: "custom-evidence-" + referenceID.String(), ReferenceID: "custom-evidence-" + referenceID.String(),
Type: coredata.EvidenceTypeFile, Type: coredata.EvidenceTypeFile,
Filename: req.File.Filename,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
if req.File.ContentType == "" {
req.File.ContentType = "application/octet-stream"
}
if err := s.fileValidator.Validate(req.File.Filename, req.File.ContentType, req.File.Size); err != nil {
return nil, err
}
err = s.svc.pg.WithTx( err = s.svc.pg.WithTx(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
measure := &coredata.Measure{} measure := &coredata.Measure{}
var file *coredata.File
var err error
file, err = s.svc.Files.UploadAndSaveFile(
ctx,
s.fileValidator,
map[string]string{
"type": "evidence",
"evidence-id": evidenceID.String(),
"organization-id": measure.OrganizationID.String(),
},
&req.File)
if err != nil {
return fmt.Errorf("cannot upload or file: %w", err)
}
evidence.EvidenceFileId = &file.ID
if err := measure.LoadByID(ctx, conn, s.svc.scope, req.MeasureID); err != nil { if err := measure.LoadByID(ctx, conn, s.svc.scope, req.MeasureID); err != nil {
return fmt.Errorf("cannot load measure %q: %w", req.MeasureID, err) return fmt.Errorf("cannot load measure %q: %w", req.MeasureID, err)
} }
objectKey, err := uuid.NewV7()
if err != nil {
return fmt.Errorf("cannot generate object key: %w", err)
}
_, err = s.svc.s3.PutObject(
ctx,
&s3.PutObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
Body: req.File.Content,
ContentType: aws.String(req.File.ContentType),
Metadata: map[string]string{
"type": "evidence",
"evidence-id": evidenceID.String(),
"organization-id": measure.OrganizationID.String(),
},
},
)
if err != nil {
return fmt.Errorf("cannot upload file to S3: %w", err)
}
evidence.ObjectKey = objectKey.String()
evidence.MimeType = req.File.ContentType
evidence.Size = uint64(req.File.Size)
evidence.MeasureID = req.MeasureID evidence.MeasureID = req.MeasureID
if err := evidence.Insert(ctx, conn, s.svc.scope); err != nil { if err := evidence.Insert(ctx, conn, s.svc.scope); err != nil {
@@ -429,43 +131,6 @@ func (s EvidenceService) UploadMeasureEvidence(
return evidence, nil return evidence, nil
} }
func (s EvidenceService) GenerateFileURL(
ctx context.Context,
evidenceID gid.GID,
expiresIn time.Duration,
) (*string, error) {
evidence, err := s.Get(ctx, evidenceID)
if err != nil {
return nil, fmt.Errorf("cannot get evidence: %w", err)
}
if evidence.Type == coredata.EvidenceTypeLink {
return nil, fmt.Errorf("cannot generate file URL for link type evidence")
}
presignClient := s3.NewPresignClient(s.svc.s3)
// Use RFC 6266/5987 encoding for filename with UTF-8 support
encodedFilename := url.QueryEscape(evidence.Filename)
contentDisposition := fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s",
encodedFilename, encodedFilename)
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(evidence.ObjectKey),
ResponseCacheControl: aws.String("max-age=3600, public"),
ResponseContentType: aws.String(evidence.MimeType),
ResponseContentDisposition: aws.String(contentDisposition),
}, func(opts *s3.PresignOptions) {
opts.Expires = expiresIn
})
if err != nil {
return nil, fmt.Errorf("cannot presign GetObject request: %w", err)
}
return &presignedReq.URL, nil
}
func (s EvidenceService) CountForMeasureID( func (s EvidenceService) CountForMeasureID(
ctx context.Context, ctx context.Context,
measureID gid.GID, measureID gid.GID,
@@ -581,9 +246,15 @@ func (s *EvidenceService) Delete(
return s.svc.pg.WithTx( return s.svc.pg.WithTx(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
if err := evidence.Delete(ctx, conn, s.svc.scope); err != nil { var fileKey *string
var err error
if fileKey, err = evidence.Delete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot delete evidence: %w", err) return fmt.Errorf("cannot delete evidence: %w", err)
} }
if err = s.svc.Files.DeleteFileFromS3(ctx, *fileKey); err != nil {
return err
}
return nil return nil
}, },

187
pkg/probo/file_service.go Normal file
View File

@@ -0,0 +1,187 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// 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 probo
import (
"context"
"fmt"
"io"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/filevalidation"
"github.com/getprobo/probo/pkg/gid"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
)
type (
FileService struct {
svc *TenantService
}
File struct {
Content io.Reader
Filename string
Size int64
ContentType string
}
FileUpload struct {
Content io.Reader
Filename string
Size int64
ContentType string
}
)
func (s FileService) Get(
ctx context.Context,
fileID gid.GID,
) (*coredata.File, error) {
file := &coredata.File{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := file.LoadByID(ctx, conn, s.svc.scope, fileID); err != nil {
return fmt.Errorf("cannot load file %w", err)
}
return nil
},
)
if err != nil {
return nil, fmt.Errorf("cannot load file: %w", err)
}
return file, nil
}
func (s FileService) UploadAndSaveFile(
ctx context.Context,
fileValidator *filevalidation.FileValidator,
s3Metadata map[string]string,
req *FileUpload) (*coredata.File, error) {
objectKey, err := uuid.NewV7()
if err != nil {
return nil, fmt.Errorf("cannot generate object key: %w", err)
}
mimeType := req.ContentType
if mimeType == "" {
mimeType = "application/octet-stream"
}
if err := fileValidator.Validate(req.Filename, mimeType, req.Size); err != nil {
return nil, fmt.Errorf("cannot validate file: %w", err)
}
_, err = s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: &s.svc.bucket,
Key: aws.String(objectKey.String()),
Body: req.Content,
Metadata: s3Metadata,
ContentType: aws.String(mimeType),
})
if err != nil {
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
}
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
})
if err != nil {
return nil, fmt.Errorf("cannot get object metadata: %w", err)
}
now := time.Now()
fileID := gid.New(s.svc.scope.GetTenantID(), coredata.FileEntityType)
var file *coredata.File
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
file = &coredata.File{
ID: fileID,
BucketName: s.svc.bucket,
MimeType: mimeType,
FileName: req.Filename,
FileKey: objectKey.String(),
FileSize: *headOutput.ContentLength,
CreatedAt: now,
UpdatedAt: now,
}
if err := file.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return file, nil
}
func (s FileService) DeleteFileFromS3(ctx context.Context, fileKey string) error {
_, err := s.svc.s3.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(fileKey),
})
if err != nil {
return fmt.Errorf("failed to delete object from S3: %w", err)
}
return nil
}
func (s FileService) GenerateFileTempURL(
ctx context.Context,
fileID gid.GID,
expiresIn time.Duration,
) (string, error) {
file, err := s.Get(ctx, fileID)
if err != nil {
return "", fmt.Errorf("cannot get file: %w", err)
}
presignClient := s3.NewPresignClient(s.svc.s3)
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(file.FileKey),
ResponseCacheControl: aws.String("max-age=3600, public"),
ResponseContentType: aws.String(file.MimeType),
ResponseContentDisposition: aws.String(fmt.Sprintf("attachment; filename=\"%s\"", file.FileName)),
}, func(opts *s3.PresignOptions) {
opts.Expires = expiresIn
})
if err != nil {
return "", fmt.Errorf("cannot presign GetObject request: %w", err)
}
return presignedReq.URL, nil
}

View File

@@ -223,15 +223,20 @@ func (s FrameworkService) Export(
for _, evidence := range evidences { for _, evidence := range evidences {
if evidence.Type != coredata.EvidenceTypeFile || if evidence.Type != coredata.EvidenceTypeFile ||
evidence.State != coredata.EvidenceStateFulfilled || evidence.State != coredata.EvidenceStateFulfilled ||
evidence.ObjectKey == "" { evidence.EvidenceFileId == nil {
continue continue
} }
evidence_file := &coredata.File{}
if err := evidence_file.LoadByID(ctx, conn, s.svc.scope, *evidence.EvidenceFileId); err != nil {
return fmt.Errorf("cannot load evidence file: %w", err)
}
object, err := s.svc.s3.GetObject( object, err := s.svc.s3.GetObject(
ctx, ctx,
&s3.GetObjectInput{ &s3.GetObjectInput{
Bucket: aws.String(s.svc.bucket), Bucket: aws.String(s.svc.bucket),
Key: aws.String(evidence.ObjectKey), Key: aws.String(evidence_file.FileKey),
}, },
) )
if err != nil { if err != nil {
@@ -239,7 +244,7 @@ func (s FrameworkService) Export(
} }
defer object.Body.Close() defer object.Body.Close()
w, err := archive.Create(fmt.Sprintf("%s/%s/%s/%s", framework.Name, control.SectionTitle, measure.Name, evidence.Filename)) w, err := archive.Create(fmt.Sprintf("%s/%s/%s/%s", framework.Name, control.SectionTitle, measure.Name, evidence_file.FileName))
if err != nil { if err != nil {
return fmt.Errorf("cannot create evidence in archive: %w", err) return fmt.Errorf("cannot create evidence in archive: %w", err)
} }

View File

@@ -94,6 +94,7 @@ type (
Snapshots *SnapshotService Snapshots *SnapshotService
ContinualImprovements *ContinualImprovementService ContinualImprovements *ContinualImprovementService
ProcessingActivities *ProcessingActivityService ProcessingActivities *ProcessingActivityService
Files *FileService
} }
) )
@@ -175,7 +176,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
} }
tenantService.Controls = &ControlService{svc: tenantService} tenantService.Controls = &ControlService{svc: tenantService}
tenantService.Risks = &RiskService{svc: tenantService} tenantService.Risks = &RiskService{svc: tenantService}
tenantService.VendorComplianceReports = &VendorComplianceReportService{svc: tenantService} tenantService.VendorComplianceReports = &VendorComplianceReportService{svc: tenantService, fileValidator: filevalidation.NewValidator(filevalidation.CategoryDocument)}
tenantService.VendorBusinessAssociateAgreements = &VendorBusinessAssociateAgreementService{svc: tenantService} tenantService.VendorBusinessAssociateAgreements = &VendorBusinessAssociateAgreementService{svc: tenantService}
tenantService.VendorContacts = &VendorContactService{svc: tenantService} tenantService.VendorContacts = &VendorContactService{svc: tenantService}
tenantService.VendorDataPrivacyAgreements = &VendorDataPrivacyAgreementService{svc: tenantService} tenantService.VendorDataPrivacyAgreements = &VendorDataPrivacyAgreementService{svc: tenantService}
@@ -193,6 +194,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
tenantService.Snapshots = &SnapshotService{svc: tenantService} tenantService.Snapshots = &SnapshotService{svc: tenantService}
tenantService.ContinualImprovements = &ContinualImprovementService{svc: tenantService} tenantService.ContinualImprovements = &ContinualImprovementService{svc: tenantService}
tenantService.ProcessingActivities = &ProcessingActivityService{svc: tenantService} tenantService.ProcessingActivities = &ProcessingActivityService{svc: tenantService}
tenantService.Files = &FileService{svc: tenantService}
return tenantService return tenantService
} }

View File

@@ -17,28 +17,23 @@ package probo
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"mime"
"net/url"
"path/filepath"
"time" "time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/filevalidation"
"github.com/getprobo/probo/pkg/gid" "github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page" "github.com/getprobo/probo/pkg/page"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg" "go.gearno.de/kit/pg"
) )
type ( type (
VendorComplianceReportService struct { VendorComplianceReportService struct {
svc *TenantService svc *TenantService
fileValidator *filevalidation.FileValidator
} }
VendorComplianceReportCreateRequest struct { VendorComplianceReportCreateRequest struct {
File io.Reader File FileUpload
ReportDate time.Time ReportDate time.Time
ValidUntil *time.Time ValidUntil *time.Time
ReportName string ReportName string
@@ -71,61 +66,43 @@ func (s VendorComplianceReportService) Upload(
vendorID gid.GID, vendorID gid.GID,
req *VendorComplianceReportCreateRequest, req *VendorComplianceReportCreateRequest,
) (*coredata.VendorComplianceReport, error) { ) (*coredata.VendorComplianceReport, error) {
objectKey, err := uuid.NewV7() vendor, err := s.svc.Vendors.Get(ctx, vendorID)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot generate object key: %w", err) return nil, fmt.Errorf("cannot get vendor: %w", err)
} }
var vendorComplianceReport *coredata.VendorComplianceReport f, err := s.svc.Files.UploadAndSaveFile(
ctx,
s.fileValidator,
map[string]string{
"type": "vendor-compliance-report",
"vendor-id": vendorID.String(),
"organization-id": vendor.OrganizationID.String(),
},
&req.File)
err = s.svc.pg.WithTx( if err != nil {
return nil, err
}
now := time.Now()
vendorComplianceReportID := gid.New(s.svc.scope.GetTenantID(), coredata.VendorComplianceReportEntityType)
vendorComplianceReport := &coredata.VendorComplianceReport{
ID: vendorComplianceReportID,
VendorID: vendorID,
ReportDate: req.ReportDate,
ValidUntil: req.ValidUntil,
ReportName: req.ReportName,
ReportFileId: &f.ID,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithConn(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
vendor := &coredata.Vendor{}
if err := vendor.LoadByID(ctx, conn, s.svc.scope, vendorID); err != nil {
return fmt.Errorf("cannot load vendor: %w", err)
}
mimeType := mime.TypeByExtension(filepath.Ext(req.ReportName))
_, err := s.svc.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: &s.svc.bucket,
Key: aws.String(objectKey.String()),
Body: req.File,
ContentType: &mimeType,
Metadata: map[string]string{
"type": "vendor-compliance-report",
"vendor-id": vendorID.String(),
"organization-id": vendor.OrganizationID.String(),
},
})
if err != nil {
return fmt.Errorf("cannot upload file to S3: %w", err)
}
headOutput, err := s.svc.s3.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(objectKey.String()),
})
if err != nil {
return fmt.Errorf("cannot get object metadata: %w", err)
}
now := time.Now()
vendorComplianceReportID := gid.New(s.svc.scope.GetTenantID(), coredata.VendorComplianceReportEntityType)
vendorComplianceReport = &coredata.VendorComplianceReport{
ID: vendorComplianceReportID,
VendorID: vendorID,
ReportDate: req.ReportDate,
ValidUntil: req.ValidUntil,
ReportName: req.ReportName,
FileKey: objectKey.String(),
FileSize: *headOutput.ContentLength,
CreatedAt: now,
UpdatedAt: now,
}
return vendorComplianceReport.Insert(ctx, conn, s.svc.scope) return vendorComplianceReport.Insert(ctx, conn, s.svc.scope)
}, },
) )
@@ -157,38 +134,6 @@ func (s VendorComplianceReportService) Get(
return vendorComplianceReport, nil return vendorComplianceReport, nil
} }
func (s VendorComplianceReportService) GenerateFileURL(
ctx context.Context,
vendorComplianceReportID gid.GID,
expiresIn time.Duration,
) (string, error) {
vendorComplianceReport, err := s.Get(ctx, vendorComplianceReportID)
if err != nil {
return "", fmt.Errorf("cannot get vendor compliance report: %w", err)
}
presignClient := s3.NewPresignClient(s.svc.s3)
// Use RFC 6266/5987 encoding for filename with UTF-8 support
encodedFilename := url.QueryEscape(vendorComplianceReport.ReportName)
contentDisposition := fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s",
encodedFilename, encodedFilename)
presignedReq, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.svc.bucket),
Key: aws.String(vendorComplianceReport.FileKey),
ResponseCacheControl: aws.String("max-age=3600, public"),
ResponseContentDisposition: aws.String(contentDisposition),
}, func(opts *s3.PresignOptions) {
opts.Expires = expiresIn
})
if err != nil {
return "", fmt.Errorf("cannot presign GetObject request: %w", err)
}
return presignedReq.URL, nil
}
func (s VendorComplianceReportService) Delete( func (s VendorComplianceReportService) Delete(
ctx context.Context, ctx context.Context,
vendorComplianceReportID gid.GID, vendorComplianceReportID gid.GID,
@@ -198,7 +143,11 @@ func (s VendorComplianceReportService) Delete(
err := s.svc.pg.WithConn( err := s.svc.pg.WithConn(
ctx, ctx,
func(conn pg.Conn) error { func(conn pg.Conn) error {
return vendorComplianceReport.Delete(ctx, conn, s.svc.scope) if err := vendorComplianceReport.Delete(ctx, conn, s.svc.scope); err != nil {
return err
}
return nil
}, },
) )

View File

@@ -1755,8 +1755,7 @@ type VendorComplianceReport implements Node {
reportDate: Datetime! reportDate: Datetime!
validUntil: Datetime validUntil: Datetime
reportName: String! reportName: String!
fileUrl: String! @goField(forceResolver: true) file: File @goField(forceResolver: true)
fileSize: BigInt!
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
} }
@@ -1944,12 +1943,10 @@ type Task implements Node {
type Evidence implements Node { type Evidence implements Node {
id: ID! id: ID!
fileUrl: String @goField(forceResolver: true)
mimeType: String!
size: Int! size: Int!
state: EvidenceState! state: EvidenceState!
type: EvidenceType! type: EvidenceType!
filename: String! file: File @goField(forceResolver: true)
url: String url: String
description: String! description: String!
@@ -2590,6 +2587,16 @@ type SnapshotEdge {
node: Snapshot! node: Snapshot!
} }
type File {
id: ID!
mimeType: String!
fileName: String!
size: BigInt!
downloadUrl: String! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
# Root Types # Root Types
type Query { type Query {
node(id: ID!): Node! node(id: ID!): Node!
@@ -2751,12 +2758,7 @@ type Mutation {
): DeleteRiskObligationMappingPayload! ): DeleteRiskObligationMappingPayload!
# Evidence mutations # Evidence mutations
requestEvidence(input: RequestEvidenceInput!): RequestEvidencePayload!
fulfillEvidence(input: FulfillEvidenceInput!): FulfillEvidencePayload!
deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload! deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload!
uploadTaskEvidence(
input: UploadTaskEvidenceInput!
): UploadTaskEvidencePayload!
uploadMeasureEvidence( uploadMeasureEvidence(
input: UploadMeasureEvidenceInput! input: UploadMeasureEvidenceInput!
): UploadMeasureEvidencePayload! ): UploadMeasureEvidencePayload!
@@ -4142,19 +4144,10 @@ type CancelSignatureRequestPayload {
deletedDocumentVersionSignatureId: ID! deletedDocumentVersionSignatureId: ID!
} }
type UploadTaskEvidencePayload {
evidenceEdge: EvidenceEdge!
}
type UploadMeasureEvidencePayload { type UploadMeasureEvidencePayload {
evidenceEdge: EvidenceEdge! evidenceEdge: EvidenceEdge!
} }
input UploadTaskEvidenceInput {
taskId: ID!
file: Upload!
}
input UploadMeasureEvidenceInput { input UploadMeasureEvidenceInput {
measureId: ID! measureId: ID!
file: Upload! file: Upload!

File diff suppressed because it is too large Load Diff

View File

@@ -61,8 +61,6 @@ func NewEvidenceEdge(e *coredata.Evidence, orderBy coredata.EvidenceOrderField)
} }
func NewEvidence(e *coredata.Evidence) *Evidence { func NewEvidence(e *coredata.Evidence) *Evidence {
var fileURL *string = nil
var urlPtr *string = nil var urlPtr *string = nil
if e.URL != "" { if e.URL != "" {
urlCopy := e.URL urlCopy := e.URL
@@ -73,10 +71,6 @@ func NewEvidence(e *coredata.Evidence) *Evidence {
ID: e.ID, ID: e.ID,
State: e.State, State: e.State,
Type: e.Type, Type: e.Type,
FileURL: fileURL,
Filename: e.Filename,
MimeType: e.MimeType,
Size: int(e.Size),
URL: urlPtr, URL: urlPtr,
Description: e.Description, Description: e.Description,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// 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 types
import (
"github.com/getprobo/probo/pkg/coredata"
)
func NewFile(r *coredata.File) *File {
return &File{
ID: r.ID,
MimeType: r.MimeType,
FileName: r.FileName,
Size: r.FileSize,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
}

View File

@@ -1034,12 +1034,10 @@ type DocumentVersionSignatureOrder struct {
type Evidence struct { type Evidence struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
FileURL *string `json:"fileUrl,omitempty"`
MimeType string `json:"mimeType"`
Size int `json:"size"` Size int `json:"size"`
State coredata.EvidenceState `json:"state"` State coredata.EvidenceState `json:"state"`
Type coredata.EvidenceType `json:"type"` Type coredata.EvidenceType `json:"type"`
Filename string `json:"filename"` File *File `json:"file,omitempty"`
URL *string `json:"url,omitempty"` URL *string `json:"url,omitempty"`
Description string `json:"description"` Description string `json:"description"`
Task *Task `json:"task,omitempty"` Task *Task `json:"task,omitempty"`
@@ -1075,6 +1073,16 @@ type ExportFrameworkPayload struct {
ExportJobID gid.GID `json:"exportJobId"` ExportJobID gid.GID `json:"exportJobId"`
} }
type File struct {
ID gid.GID `json:"id"`
MimeType string `json:"mimeType"`
FileName string `json:"fileName"`
Size int64 `json:"size"`
DownloadURL string `json:"downloadUrl"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type Framework struct { type Framework struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -1955,15 +1963,6 @@ type UploadMeasureEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"` EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
} }
type UploadTaskEvidenceInput struct {
TaskID gid.GID `json:"taskId"`
File graphql.Upload `json:"file"`
}
type UploadTaskEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type UploadTrustCenterNDAInput struct { type UploadTrustCenterNDAInput struct {
TrustCenterID gid.GID `json:"trustCenterId"` TrustCenterID gid.GID `json:"trustCenterId"`
FileName string `json:"fileName"` FileName string `json:"fileName"`
@@ -2090,8 +2089,7 @@ type VendorComplianceReport struct {
ReportDate time.Time `json:"reportDate"` ReportDate time.Time `json:"reportDate"`
ValidUntil *time.Time `json:"validUntil,omitempty"` ValidUntil *time.Time `json:"validUntil,omitempty"`
ReportName string `json:"reportName"` ReportName string `json:"reportName"`
FileURL string `json:"fileUrl"` File *File `json:"file,omitempty"`
FileSize int64 `json:"fileSize"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }

View File

@@ -49,7 +49,6 @@ func NewVendorComplianceReport(c *coredata.VendorComplianceReport) *VendorCompli
ReportDate: c.ReportDate, ReportDate: c.ReportDate,
ValidUntil: c.ValidUntil, ValidUntil: c.ValidUntil,
ReportName: c.ReportName, ReportName: c.ReportName,
FileSize: c.FileSize,
CreatedAt: c.CreatedAt, CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt, UpdatedAt: c.UpdatedAt,
} }

View File

@@ -734,21 +734,25 @@ func (r *documentVersionSignatureResolver) SignedBy(ctx context.Context, obj *ty
return types.NewPeople(people), nil return types.NewPeople(people), nil
} }
// FileURL is the resolver for the fileUrl field. // File is the resolver for the file field.
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (*string, error) { func (r *evidenceResolver) File(ctx context.Context, obj *types.Evidence) (*types.File, error) {
prb := r.ProboService(ctx, obj.ID.TenantID()) prb := r.ProboService(ctx, obj.ID.TenantID())
if obj.Type == coredata.EvidenceTypeLink { evidence, err := prb.Evidences.Get(ctx, obj.ID)
return obj.URL, nil
}
fileURL, err := prb.Evidences.GenerateFileURL(ctx, obj.ID, 15*time.Minute)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot generate file URL: %w", err) return nil, fmt.Errorf("cannot load evidence: %w", err)
} }
result := *fileURL if evidence.EvidenceFileId == nil {
return &result, nil return nil, nil
}
file, err := prb.Files.Get(ctx, *evidence.EvidenceFileId)
if err != nil {
return nil, fmt.Errorf("cannot load evidence file: %w", err)
}
return types.NewFile(file), nil
} }
// Task is the resolver for the task field. // Task is the resolver for the task field.
@@ -811,6 +815,18 @@ func (r *evidenceConnectionResolver) TotalCount(ctx context.Context, obj *types.
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver)) panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
} }
// DownloadURL is the resolver for the downloadUrl field.
func (r *fileResolver) DownloadURL(ctx context.Context, obj *types.File) (string, error) {
prb := r.ProboService(ctx, obj.ID.TenantID())
downloadUrl, err := prb.Files.GenerateFileTempURL(ctx, obj.ID, 60*time.Second)
if err != nil {
return "", fmt.Errorf("cannot generate download url: %w", err)
}
return downloadUrl, nil
}
// Organization is the resolver for the organization field. // Organization is the resolver for the organization field.
func (r *frameworkResolver) Organization(ctx context.Context, obj *types.Framework) (*types.Organization, error) { func (r *frameworkResolver) Organization(ctx context.Context, obj *types.Framework) (*types.Organization, error) {
prb := r.ProboService(ctx, obj.ID.TenantID()) prb := r.ProboService(ctx, obj.ID.TenantID())
@@ -2227,55 +2243,6 @@ func (r *mutationResolver) DeleteRiskObligationMapping(ctx context.Context, inpu
}, nil }, nil
} }
// RequestEvidence is the resolver for the requestEvidence field.
func (r *mutationResolver) RequestEvidence(ctx context.Context, input types.RequestEvidenceInput) (*types.RequestEvidencePayload, error) {
prb := r.ProboService(ctx, input.TaskID.TenantID())
evidence, err := prb.Evidences.Request(
ctx,
probo.RequestEvidenceRequest{
TaskID: &input.TaskID,
Name: input.Name,
Type: input.Type,
Description: input.Description,
},
)
if err != nil {
panic(fmt.Errorf("cannot request evidence: %w", err))
}
return &types.RequestEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// FulfillEvidence is the resolver for the fulfillEvidence field.
func (r *mutationResolver) FulfillEvidence(ctx context.Context, input types.FulfillEvidenceInput) (*types.FulfillEvidencePayload, error) {
prb := r.ProboService(ctx, input.EvidenceID.TenantID())
req := probo.FulfilledEvidenceRequest{
EvidenceID: input.EvidenceID,
}
if input.File != nil {
req.File = input.File.File
req.Filename = &input.File.Filename
}
if input.URL != nil {
req.URL = input.URL
}
evidence, err := prb.Evidences.Fulfill(ctx, req)
if err != nil {
panic(fmt.Errorf("cannot fulfill evidence: %w", err))
}
return &types.FulfillEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// DeleteEvidence is the resolver for the deleteEvidence field. // DeleteEvidence is the resolver for the deleteEvidence field.
func (r *mutationResolver) DeleteEvidence(ctx context.Context, input types.DeleteEvidenceInput) (*types.DeleteEvidencePayload, error) { func (r *mutationResolver) DeleteEvidence(ctx context.Context, input types.DeleteEvidenceInput) (*types.DeleteEvidencePayload, error) {
prb := r.ProboService(ctx, input.EvidenceID.TenantID()) prb := r.ProboService(ctx, input.EvidenceID.TenantID())
@@ -2290,31 +2257,6 @@ func (r *mutationResolver) DeleteEvidence(ctx context.Context, input types.Delet
}, nil }, nil
} }
// UploadTaskEvidence is the resolver for the uploadTaskEvidence field.
func (r *mutationResolver) UploadTaskEvidence(ctx context.Context, input types.UploadTaskEvidenceInput) (*types.UploadTaskEvidencePayload, error) {
prb := r.ProboService(ctx, input.TaskID.TenantID())
evidence, err := prb.Evidences.UploadTaskEvidence(
ctx,
probo.UploadTaskEvidenceRequest{
TaskID: input.TaskID,
File: probo.File{
Content: input.File.File,
Filename: input.File.Filename,
Size: input.File.Size,
ContentType: input.File.ContentType,
},
},
)
if err != nil {
panic(fmt.Errorf("cannot upload task evidence: %w", err))
}
return &types.UploadTaskEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// UploadMeasureEvidence is the resolver for the uploadMeasureEvidence field. // UploadMeasureEvidence is the resolver for the uploadMeasureEvidence field.
func (r *mutationResolver) UploadMeasureEvidence(ctx context.Context, input types.UploadMeasureEvidenceInput) (*types.UploadMeasureEvidencePayload, error) { func (r *mutationResolver) UploadMeasureEvidence(ctx context.Context, input types.UploadMeasureEvidenceInput) (*types.UploadMeasureEvidencePayload, error) {
prb := r.ProboService(ctx, input.MeasureID.TenantID()) prb := r.ProboService(ctx, input.MeasureID.TenantID())
@@ -2323,7 +2265,7 @@ func (r *mutationResolver) UploadMeasureEvidence(ctx context.Context, input type
ctx, ctx,
probo.UploadMeasureEvidenceRequest{ probo.UploadMeasureEvidenceRequest{
MeasureID: input.MeasureID, MeasureID: input.MeasureID,
File: probo.File{ File: probo.FileUpload{
Content: input.File.File, Content: input.File.File,
Filename: input.File.Filename, Filename: input.File.Filename,
Size: input.File.Size, Size: input.File.Size,
@@ -2348,7 +2290,7 @@ func (r *mutationResolver) UploadVendorComplianceReport(ctx context.Context, inp
ctx, ctx,
input.VendorID, input.VendorID,
&probo.VendorComplianceReportCreateRequest{ &probo.VendorComplianceReportCreateRequest{
File: input.File.File, File: probo.FileUpload{Filename: input.File.Filename, Size: input.File.Size, Content: input.File.File, ContentType: input.File.ContentType},
ReportDate: input.ReportDate, ReportDate: input.ReportDate,
ValidUntil: input.ValidUntil, ValidUntil: input.ValidUntil,
ReportName: input.ReportName, ReportName: input.ReportName,
@@ -5065,16 +5007,25 @@ func (r *vendorComplianceReportResolver) Vendor(ctx context.Context, obj *types.
return types.NewVendor(vendor), nil return types.NewVendor(vendor), nil
} }
// FileURL is the resolver for the fileUrl field. // File is the resolver for the file field.
func (r *vendorComplianceReportResolver) FileURL(ctx context.Context, obj *types.VendorComplianceReport) (string, error) { func (r *vendorComplianceReportResolver) File(ctx context.Context, obj *types.VendorComplianceReport) (*types.File, error) {
prb := r.ProboService(ctx, obj.ID.TenantID()) prb := r.ProboService(ctx, obj.ID.TenantID())
fileURL, err := prb.VendorComplianceReports.GenerateFileURL(ctx, obj.ID, 1*time.Hour) evidence, err := prb.VendorComplianceReports.Get(ctx, obj.ID)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to generate file URL: %w", err)) return nil, fmt.Errorf("cannot load evidence: %w", err)
} }
return fileURL, nil if evidence.ReportFileId == nil {
return nil, nil
}
file, err := prb.Files.Get(ctx, *evidence.ReportFileId)
if err != nil {
return nil, fmt.Errorf("cannot load evidence file: %w", err)
}
return types.NewFile(file), nil
} }
// TotalCount is the resolver for the totalCount field. // TotalCount is the resolver for the totalCount field.
@@ -5271,6 +5222,9 @@ func (r *Resolver) EvidenceConnection() schema.EvidenceConnectionResolver {
return &evidenceConnectionResolver{r} return &evidenceConnectionResolver{r}
} }
// File returns schema.FileResolver implementation.
func (r *Resolver) File() schema.FileResolver { return &fileResolver{r} }
// Framework returns schema.FrameworkResolver implementation. // Framework returns schema.FrameworkResolver implementation.
func (r *Resolver) Framework() schema.FrameworkResolver { return &frameworkResolver{r} } func (r *Resolver) Framework() schema.FrameworkResolver { return &frameworkResolver{r} }
@@ -5434,6 +5388,7 @@ type documentVersionResolver struct{ *Resolver }
type documentVersionSignatureResolver struct{ *Resolver } type documentVersionSignatureResolver struct{ *Resolver }
type evidenceResolver struct{ *Resolver } type evidenceResolver struct{ *Resolver }
type evidenceConnectionResolver struct{ *Resolver } type evidenceConnectionResolver struct{ *Resolver }
type fileResolver struct{ *Resolver }
type frameworkResolver struct{ *Resolver } type frameworkResolver struct{ *Resolver }
type frameworkConnectionResolver struct{ *Resolver } type frameworkConnectionResolver struct{ *Resolver }
type measureResolver struct{ *Resolver } type measureResolver struct{ *Resolver }