Move evidence to measure level

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-05-03 16:57:51 -07:00
parent 5b65109387
commit 3bfb3ba485
14 changed files with 4568 additions and 1633 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<63dbc912431a9ea23a2ce515ba95af86>>
* @generated SignedSource<<03d8e2eca1cc7ffaf797c66ef7fa11c1>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -20,6 +20,22 @@ export type MeasureViewQuery$data = {
readonly measure: {
readonly category?: string;
readonly description?: string;
readonly evidences?: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: string;
readonly description: string;
readonly filename: string;
readonly id: string;
readonly mimeType: string;
readonly size: number;
readonly state: EvidenceState;
readonly type: EvidenceType;
readonly url: string | null | undefined;
};
}>;
};
readonly id: string;
readonly name?: string;
readonly state?: MeasureState;
@@ -114,53 +130,20 @@ v6 = {
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
}
],
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v10 = {
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v11 = {
v9 = {
"alias": null,
"args": null,
"concreteType": "PageInfo",
@@ -185,7 +168,7 @@ v11 = {
],
"storageKey": null
},
v12 = {
v10 = {
"kind": "ClientExtension",
"selections": [
{
@@ -197,7 +180,7 @@ v12 = {
}
]
},
v13 = [
v11 = [
{
"alias": null,
"args": null,
@@ -259,17 +242,50 @@ v13 = [
"storageKey": null
},
(v4/*: any*/),
(v9/*: any*/)
(v7/*: any*/)
],
"storageKey": null
},
(v10/*: any*/)
(v8/*: any*/)
],
"storageKey": null
},
(v11/*: any*/),
(v12/*: any*/)
(v9/*: any*/),
(v10/*: any*/)
],
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "timeEstimate",
"storageKey": null
},
v13 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "assignedTo",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
}
],
"storageKey": null
},
v14 = [
{
"kind": "Literal",
@@ -307,6 +323,16 @@ return {
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
{
"alias": "evidences",
"args": null,
"concreteType": "EvidenceConnection",
"kind": "LinkedField",
"name": "__MeasureView_evidences_connection",
"plural": false,
"selections": (v11/*: any*/),
"storageKey": null
},
{
"alias": "tasks",
"args": null,
@@ -335,28 +361,28 @@ return {
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v12/*: any*/),
(v13/*: any*/),
{
"alias": "evidences",
"args": null,
"concreteType": "EvidenceConnection",
"kind": "LinkedField",
"name": "__MeasureView_evidences_connection",
"name": "__MeasureView_task_evidences_connection",
"plural": false,
"selections": (v13/*: any*/),
"selections": (v11/*: any*/),
"storageKey": null
},
(v9/*: any*/)
(v7/*: any*/)
],
"storageKey": null
},
(v10/*: any*/)
(v8/*: any*/)
],
"storageKey": null
},
(v11/*: any*/),
(v12/*: any*/)
(v9/*: any*/),
(v10/*: any*/)
],
"storageKey": null
}
@@ -385,7 +411,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v9/*: any*/),
(v7/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
@@ -394,6 +420,25 @@ return {
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
{
"alias": null,
"args": (v14/*: any*/),
"concreteType": "EvidenceConnection",
"kind": "LinkedField",
"name": "evidences",
"plural": false,
"selections": (v11/*: any*/),
"storageKey": "evidences(first:100)"
},
{
"alias": null,
"args": (v14/*: any*/),
"filters": null,
"handle": "connection",
"key": "MeasureView_evidences",
"kind": "LinkedHandle",
"name": "evidences"
},
{
"alias": null,
"args": (v14/*: any*/),
@@ -422,8 +467,8 @@ return {
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v12/*: any*/),
(v13/*: any*/),
{
"alias": null,
"args": (v15/*: any*/),
@@ -431,7 +476,7 @@ return {
"kind": "LinkedField",
"name": "evidences",
"plural": false,
"selections": (v13/*: any*/),
"selections": (v11/*: any*/),
"storageKey": "evidences(first:50)"
},
{
@@ -439,20 +484,20 @@ return {
"args": (v15/*: any*/),
"filters": null,
"handle": "connection",
"key": "MeasureView_evidences",
"key": "MeasureView_task_evidences",
"kind": "LinkedHandle",
"name": "evidences"
},
(v9/*: any*/)
(v7/*: any*/)
],
"storageKey": null
},
(v10/*: any*/)
(v8/*: any*/)
],
"storageKey": null
},
(v11/*: any*/),
(v12/*: any*/)
(v9/*: any*/),
(v10/*: any*/)
],
"storageKey": "tasks(first:100)"
},
@@ -475,10 +520,19 @@ return {
]
},
"params": {
"cacheID": "9c4b0f0032102448259ea5deb2b9d57a",
"cacheID": "d3e4d9723d12bce8158b83eb76d05a90",
"id": null,
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"measure",
"evidences"
]
},
{
"count": null,
"cursor": null,
@@ -498,11 +552,11 @@ return {
},
"name": "MeasureViewQuery",
"operationKind": "query",
"text": "query MeasureViewQuery(\n $measureId: ID!\n) {\n measure: node(id: $measureId) {\n __typename\n id\n ... on Measure {\n name\n description\n state\n category\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n description\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query MeasureViewQuery(\n $measureId: ID!\n) {\n measure: node(id: $measureId) {\n __typename\n id\n ... on Measure {\n name\n description\n state\n category\n evidences(first: 100) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n description\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n tasks(first: 100) {\n edges {\n node {\n id\n name\n description\n state\n timeEstimate\n assignedTo {\n id\n fullName\n primaryEmailAddress\n }\n evidences(first: 50) {\n edges {\n node {\n id\n mimeType\n filename\n size\n state\n type\n url\n createdAt\n description\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "0de1c02cbaf76feae0ecdbd93f530fa7";
(node as any).hash = "288412cccb1f4c14b1852504e5536af1";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e5042f900955d846dd86ad6b78d60518>>
* @generated SignedSource<<2f767da45cb75ea7cc61932efa7bf802>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,20 +11,18 @@
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type CreateEvidenceInput = {
export type RequestEvidenceInput = {
description: string;
file?: any | null | undefined;
name: string;
taskId: string;
type: EvidenceType;
url?: string | null | undefined;
};
export type MeasureViewCreateEvidenceMutation$variables = {
export type MeasureViewRequestEvidenceMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateEvidenceInput;
input: RequestEvidenceInput;
};
export type MeasureViewCreateEvidenceMutation$data = {
readonly createEvidence: {
export type MeasureViewRequestEvidenceMutation$data = {
readonly requestEvidence: {
readonly evidenceEdge: {
readonly node: {
readonly createdAt: string;
@@ -41,9 +39,9 @@ export type MeasureViewCreateEvidenceMutation$data = {
};
};
};
export type MeasureViewCreateEvidenceMutation = {
response: MeasureViewCreateEvidenceMutation$data;
variables: MeasureViewCreateEvidenceMutation$variables;
export type MeasureViewRequestEvidenceMutation = {
response: MeasureViewRequestEvidenceMutation$data;
variables: MeasureViewRequestEvidenceMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -164,14 +162,14 @@ return {
],
"kind": "Fragment",
"metadata": null,
"name": "MeasureViewCreateEvidenceMutation",
"name": "MeasureViewRequestEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateEvidencePayload",
"concreteType": "RequestEvidencePayload",
"kind": "LinkedField",
"name": "createEvidence",
"name": "requestEvidence",
"plural": false,
"selections": [
(v3/*: any*/)
@@ -189,14 +187,14 @@ return {
(v0/*: any*/)
],
"kind": "Operation",
"name": "MeasureViewCreateEvidenceMutation",
"name": "MeasureViewRequestEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateEvidencePayload",
"concreteType": "RequestEvidencePayload",
"kind": "LinkedField",
"name": "createEvidence",
"name": "requestEvidence",
"plural": false,
"selections": [
(v3/*: any*/),
@@ -222,16 +220,16 @@ return {
]
},
"params": {
"cacheID": "25b9947aa09d7b4c60886f149b06cc6e",
"cacheID": "69f2291555b3826b1b29fea6d2380e04",
"id": null,
"metadata": {},
"name": "MeasureViewCreateEvidenceMutation",
"name": "MeasureViewRequestEvidenceMutation",
"operationKind": "mutation",
"text": "mutation MeasureViewCreateEvidenceMutation(\n $input: CreateEvidenceInput!\n) {\n createEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n description\n }\n }\n }\n}\n"
"text": "mutation MeasureViewRequestEvidenceMutation(\n $input: RequestEvidenceInput!\n) {\n requestEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n description\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "bc59ec67230776eeb60a389a8360b0af";
(node as any).hash = "7921deec93c923a9cd14969dad6ecfd7";
export default node;

View File

@@ -0,0 +1,233 @@
/**
* @generated SignedSource<<97ff04190e08df7c9b3288e067787348>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type UploadMeasureEvidenceInput = {
file: any;
measureId: string;
};
export type MeasureViewUploadMeasureEvidenceMutation$variables = {
connections: ReadonlyArray<string>;
input: UploadMeasureEvidenceInput;
};
export type MeasureViewUploadMeasureEvidenceMutation$data = {
readonly uploadMeasureEvidence: {
readonly evidenceEdge: {
readonly node: {
readonly createdAt: string;
readonly description: string;
readonly fileUrl: string | null | undefined;
readonly filename: string;
readonly id: string;
readonly mimeType: string;
readonly size: number;
readonly state: EvidenceState;
readonly type: EvidenceType;
readonly url: string | null | undefined;
};
};
};
};
export type MeasureViewUploadMeasureEvidenceMutation = {
response: MeasureViewUploadMeasureEvidenceMutation$data;
variables: MeasureViewUploadMeasureEvidenceMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "EvidenceEdge",
"kind": "LinkedField",
"name": "evidenceEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Evidence",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fileUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MeasureViewUploadMeasureEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "UploadMeasureEvidencePayload",
"kind": "LinkedField",
"name": "uploadMeasureEvidence",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MeasureViewUploadMeasureEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "UploadMeasureEvidencePayload",
"kind": "LinkedField",
"name": "uploadMeasureEvidence",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "evidenceEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "2f033ec4a33f6d9c980702630822c8ca",
"id": null,
"metadata": {},
"name": "MeasureViewUploadMeasureEvidenceMutation",
"operationKind": "mutation",
"text": "mutation MeasureViewUploadMeasureEvidenceMutation(\n $input: UploadMeasureEvidenceInput!\n) {\n uploadMeasureEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n description\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "af05defd4ec39b5d3f17aebce2a84c65";
export default node;

View File

@@ -0,0 +1,233 @@
/**
* @generated SignedSource<<4539152ef13af3aca3a79891c5b81a79>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EvidenceState = "FULFILLED" | "REQUESTED";
export type EvidenceType = "FILE" | "LINK";
export type UploadTaskEvidenceInput = {
file: any;
taskId: string;
};
export type MeasureViewUploadTaskEvidenceMutation$variables = {
connections: ReadonlyArray<string>;
input: UploadTaskEvidenceInput;
};
export type MeasureViewUploadTaskEvidenceMutation$data = {
readonly uploadTaskEvidence: {
readonly evidenceEdge: {
readonly node: {
readonly createdAt: string;
readonly description: string;
readonly fileUrl: string | null | undefined;
readonly filename: string;
readonly id: string;
readonly mimeType: string;
readonly size: number;
readonly state: EvidenceState;
readonly type: EvidenceType;
readonly url: string | null | undefined;
};
};
};
};
export type MeasureViewUploadTaskEvidenceMutation = {
response: MeasureViewUploadTaskEvidenceMutation$data;
variables: MeasureViewUploadTaskEvidenceMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "EvidenceEdge",
"kind": "LinkedField",
"name": "evidenceEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Evidence",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fileUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "mimeType",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "url",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "size",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "MeasureViewUploadTaskEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "UploadTaskEvidencePayload",
"kind": "LinkedField",
"name": "uploadTaskEvidence",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "MeasureViewUploadTaskEvidenceMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "UploadTaskEvidencePayload",
"kind": "LinkedField",
"name": "uploadTaskEvidence",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "appendEdge",
"key": "",
"kind": "LinkedHandle",
"name": "evidenceEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "2c18eec6e7e3d428ad6fbdf4c52ccf94",
"id": null,
"metadata": {},
"name": "MeasureViewUploadTaskEvidenceMutation",
"operationKind": "mutation",
"text": "mutation MeasureViewUploadTaskEvidenceMutation(\n $input: UploadTaskEvidenceInput!\n) {\n uploadTaskEvidence(input: $input) {\n evidenceEdge {\n node {\n id\n filename\n fileUrl\n mimeType\n type\n url\n size\n state\n createdAt\n description\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "3649fc872c9f58d4ffd0181b0316c812";
export default node;

View File

@@ -29,7 +29,8 @@ import (
type (
Evidence struct {
ID gid.GID `db:"id"`
TaskID gid.GID `db:"task_id"`
MeasureID gid.GID `db:"measure_id"`
TaskID *gid.GID `db:"task_id"`
State EvidenceState `db:"state"`
ReferenceID string `db:"reference_id"`
Type EvidenceType `db:"type"`
@@ -65,6 +66,7 @@ INSERT INTO
evidences (
tenant_id,
id,
measure_id,
task_id,
reference_id,
object_key,
@@ -81,6 +83,7 @@ INSERT INTO
VALUES (
@tenant_id,
@evidence_id,
@measure_id,
@task_id,
@reference_id,
@object_key,
@@ -104,6 +107,7 @@ WHERE evidences.state = 'REQUESTED';
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"evidence_id": e.ID,
"measure_id": e.MeasureID,
"task_id": e.TaskID,
"reference_id": e.ReferenceID,
"object_key": e.ObjectKey,
@@ -131,6 +135,7 @@ INSERT INTO
evidences (
tenant_id,
id,
measure_id,
task_id,
reference_id,
object_key,
@@ -147,6 +152,7 @@ INSERT INTO
VALUES (
@tenant_id,
@evidence_id,
@measure_id,
@task_id,
@reference_id,
@object_key,
@@ -165,6 +171,7 @@ VALUES (
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"evidence_id": e.ID,
"measure_id": e.MeasureID,
"task_id": e.TaskID,
"reference_id": e.ReferenceID,
"object_key": e.ObjectKey,
@@ -192,6 +199,7 @@ func (e *Evidence) LoadByID(
SELECT
id,
task_id,
measure_id,
reference_id,
state,
type,
@@ -231,6 +239,58 @@ LIMIT 1;
return nil
}
func (e *Evidences) LoadByMeasureID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
measureID gid.GID,
cursor *page.Cursor[EvidenceOrderField],
) error {
q := `
SELECT
id,
measure_id,
task_id,
reference_id,
state,
type,
object_key,
mime_type,
size,
filename,
url,
description,
created_at,
updated_at
FROM
evidences
WHERE
%s
AND measure_id = @measure_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"measure_id": measureID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query evidence: %w", err)
}
evidences, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Evidence])
if err != nil {
return fmt.Errorf("cannot collect evidence: %w", err)
}
*e = evidences
return nil
}
func (e *Evidences) LoadByTaskID(
ctx context.Context,
conn pg.Conn,
@@ -241,6 +301,7 @@ func (e *Evidences) LoadByTaskID(
q := `
SELECT
id,
measure_id,
task_id,
reference_id,
state,

View File

@@ -0,0 +1,9 @@
ALTER TABLE evidences ALTER COLUMN task_id DROP NOT NULL;
ALTER TABLE evidences ADD COLUMN measure_id TEXT REFERENCES measures(id);
UPDATE evidences e
SET measure_id = t.measure_id
FROM tasks t
WHERE e.task_id = t.id AND e.measure_id IS NULL;
ALTER TABLE evidences ALTER COLUMN measure_id SET NOT NULL;

View File

@@ -37,8 +37,16 @@ type (
svc *TenantService
}
File struct {
Content io.Reader
Filename string
Size int64
ContentType string
}
RequestEvidenceRequest struct {
TaskID gid.GID
MeasureID *gid.GID
TaskID *gid.GID
Type coredata.EvidenceType
Name string
Description string
@@ -51,13 +59,14 @@ type (
Filename *string
}
CreateEvidenceRequest struct {
TaskID gid.GID
Name string
Type coredata.EvidenceType
File io.Reader
URL string
Description string
UploadTaskEvidenceRequest struct {
TaskID gid.GID
File File
}
UploadMeasureEvidenceRequest struct {
MeasureID gid.GID
File File
}
)
@@ -90,7 +99,6 @@ func (s EvidenceService) Request(
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: req.TaskID,
State: coredata.EvidenceStateRequested,
Type: req.Type,
Filename: req.Name,
@@ -99,9 +107,23 @@ func (s EvidenceService) Request(
UpdatedAt: now,
}
err := s.svc.pg.WithConn(
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)
}
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)
},
)
@@ -183,9 +205,9 @@ func (s EvidenceService) Fulfill(
return evidence, nil
}
func (s EvidenceService) Create(
func (s EvidenceService) UploadTaskEvidence(
ctx context.Context,
req CreateEvidenceRequest,
req UploadTaskEvidenceRequest,
) (*coredata.Evidence, error) {
now := time.Now()
evidenceID := gid.New(s.svc.scope.GetTenantID(), coredata.EvidenceEntityType)
@@ -197,66 +219,130 @@ func (s EvidenceService) Create(
evidence := &coredata.Evidence{
ID: evidenceID,
TaskID: req.TaskID,
TaskID: &req.TaskID,
State: coredata.EvidenceStateFulfilled,
ReferenceID: "custom-evidence-" + referenceID.String(),
Type: req.Type,
Filename: req.Name,
URL: req.URL,
Description: req.Description,
Type: coredata.EvidenceTypeFile,
Filename: req.File.Filename,
CreatedAt: now,
UpdatedAt: now,
}
if req.Type == coredata.EvidenceTypeFile {
contentType := "application/octet-stream"
if req.Name != "" {
if detectedType := mime.TypeByExtension(filepath.Ext(req.Name)); detectedType != "" {
contentType = detectedType
}
}
objectKey, err := uuid.NewV7()
if err != nil {
return nil, 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,
ContentType: aws.String(contentType),
})
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)
}
evidence.ObjectKey = objectKey.String()
evidence.MimeType = contentType
evidence.Size = uint64(*headOutput.ContentLength)
} else if req.Type == coredata.EvidenceTypeLink {
evidence.MimeType = "text/uri-list"
evidence.Size = uint64(len(req.URL))
evidence.ObjectKey = ""
// TODO validate content type
if req.File.ContentType == "" {
req.File.ContentType = "application/octet-stream"
}
task := &coredata.Task{}
objectKey, err := uuid.NewV7()
if err != nil {
return nil, 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),
},
)
if err != nil {
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
}
evidence.ObjectKey = objectKey.String()
evidence.MimeType = req.File.ContentType
evidence.Size = uint64(req.File.Size)
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)
}
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
},
)
if err != nil {
// TODO try do delete file from s3 if it's a file type
return nil, err
}
return evidence, nil
}
func (s EvidenceService) UploadMeasureEvidence(
ctx context.Context,
req UploadMeasureEvidenceRequest,
) (*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,
MeasureID: req.MeasureID,
State: coredata.EvidenceStateFulfilled,
ReferenceID: "custom-evidence-" + referenceID.String(),
Type: coredata.EvidenceTypeFile,
Filename: req.File.Filename,
CreatedAt: now,
UpdatedAt: now,
}
// TODO validate content type
if req.File.ContentType == "" {
req.File.ContentType = "application/octet-stream"
}
objectKey, err := uuid.NewV7()
if err != nil {
return nil, 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),
},
)
if err != nil {
return nil, fmt.Errorf("cannot upload file to S3: %w", err)
}
evidence.ObjectKey = objectKey.String()
evidence.MimeType = req.File.ContentType
evidence.Size = uint64(req.File.Size)
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
measure := &coredata.Measure{}
if err := measure.LoadByID(ctx, conn, s.svc.scope, req.MeasureID); err != nil {
return fmt.Errorf("cannot load measure %q: %w", req.MeasureID, err)
}
evidence.MeasureID = req.MeasureID
if err := evidence.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert evidence: %w", err)
}
@@ -310,6 +396,33 @@ func (s EvidenceService) GenerateFileURL(
return &presignedReq.URL, nil
}
func (s EvidenceService) ListForMeasureID(
ctx context.Context,
measureID gid.GID,
cursor *page.Cursor[coredata.EvidenceOrderField],
) (*page.Page[*coredata.Evidence, coredata.EvidenceOrderField], error) {
var evidences coredata.Evidences
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return evidences.LoadByMeasureID(
ctx,
conn,
s.svc.scope,
measureID,
cursor,
)
},
)
if err != nil {
return nil, err
}
return page.NewPage(evidences, cursor), nil
}
func (s EvidenceService) ListForTaskID(
ctx context.Context,
taskID gid.GID,

View File

@@ -188,7 +188,7 @@ func (s MeasureService) Import(
evidence := &coredata.Evidence{
State: coredata.EvidenceStateRequested,
ID: evidenceID,
TaskID: task.ID,
TaskID: &task.ID,
ReferenceID: req.Measures[i].Tasks[j].RequestedEvidences[k].ReferenceID,
Type: req.Measures[i].Tasks[j].RequestedEvidences[k].Type,
Description: req.Measures[i].Tasks[j].RequestedEvidences[k].Name,

View File

@@ -596,6 +596,14 @@ type Measure implements Node {
description: String!
state: MeasureState!
evidences(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: EvidenceOrder
): EvidenceConnection! @goField(forceResolver: true)
tasks(
first: Int
after: CursorKey
@@ -987,8 +995,9 @@ type Mutation {
# Evidence mutations
requestEvidence(input: RequestEvidenceInput!): RequestEvidencePayload!
fulfillEvidence(input: FulfillEvidenceInput!): FulfillEvidencePayload!
createEvidence(input: CreateEvidenceInput!): CreateEvidencePayload!
deleteEvidence(input: DeleteEvidenceInput!): DeleteEvidencePayload!
uploadTaskEvidence(input: UploadTaskEvidenceInput!): UploadTaskEvidencePayload!
uploadMeasureEvidence(input: UploadMeasureEvidenceInput!): UploadMeasureEvidencePayload!
# Vendor Compliance Report mutations
uploadVendorComplianceReport(
@@ -1651,4 +1660,22 @@ input SendSigningNotificationsInput {
type SendSigningNotificationsPayload {
success: Boolean!
}
}
type UploadTaskEvidencePayload {
evidenceEdge: EvidenceEdge!
}
type UploadMeasureEvidencePayload {
evidenceEdge: EvidenceEdge!
}
input UploadTaskEvidenceInput {
taskId: ID!
file: Upload!
}
input UploadMeasureEvidenceInput {
measureId: ID!
file: Upload!
}

File diff suppressed because it is too large Load Diff

View File

@@ -473,6 +473,7 @@ type Measure struct {
Name string `json:"name"`
Description string `json:"description"`
State coredata.MeasureState `json:"state"`
Evidences *EvidenceConnection `json:"evidences"`
Tasks *TaskConnection `json:"tasks"`
Risks *RiskConnection `json:"risks"`
Controls *ControlConnection `json:"controls"`
@@ -886,6 +887,24 @@ type UpdateVendorPayload struct {
Vendor *Vendor `json:"vendor"`
}
type UploadMeasureEvidenceInput struct {
MeasureID gid.GID `json:"measureId"`
File graphql.Upload `json:"file"`
}
type UploadMeasureEvidencePayload struct {
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 UploadVendorComplianceReportInput struct {
VendorID gid.GID `json:"vendorId"`
ReportDate time.Time `json:"reportDate"`

View File

@@ -112,6 +112,31 @@ func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework,
return types.NewControlConnection(page), nil
}
// Evidences is the resolver for the evidences field.
func (r *measureResolver) Evidences(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) (*types.EvidenceConnection, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.EvidenceOrderField]{
Field: coredata.EvidenceOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.EvidenceOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Evidences.ListForMeasureID(ctx, obj.ID, cursor)
if err != nil {
return nil, fmt.Errorf("cannot list measure evidences: %w", err)
}
return types.NewEvidenceConnection(page), nil
}
// Tasks is the resolver for the tasks field.
func (r *measureResolver) Tasks(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) (*types.TaskConnection, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
@@ -875,7 +900,7 @@ func (r *mutationResolver) RequestEvidence(ctx context.Context, input types.Requ
evidence, err := svc.Evidences.Request(
ctx,
probo.RequestEvidenceRequest{
TaskID: input.TaskID,
TaskID: &input.TaskID,
Name: input.Name,
Type: input.Type,
Description: input.Description,
@@ -917,39 +942,6 @@ func (r *mutationResolver) FulfillEvidence(ctx context.Context, input types.Fulf
}, nil
}
// CreateEvidence is the resolver for the createEvidence field.
func (r *mutationResolver) CreateEvidence(ctx context.Context, input types.CreateEvidenceInput) (*types.CreateEvidencePayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.TaskID.TenantID())
req := probo.CreateEvidenceRequest{
TaskID: input.TaskID,
Name: input.Name,
Type: input.Type,
Description: input.Description,
}
if input.Type == coredata.EvidenceTypeFile {
if input.File == nil {
return nil, fmt.Errorf("file is required for FILE type evidence")
}
req.File = input.File.File
} else if input.Type == coredata.EvidenceTypeLink {
if input.URL == nil || *input.URL == "" {
return nil, fmt.Errorf("URL is required for LINK type evidence")
}
req.URL = *input.URL
}
evidence, err := svc.Evidences.Create(ctx, req)
if err != nil {
panic(fmt.Errorf("failed to create evidence: %w", err))
}
return &types.CreateEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// DeleteEvidence is the resolver for the deleteEvidence field.
func (r *mutationResolver) DeleteEvidence(ctx context.Context, input types.DeleteEvidenceInput) (*types.DeleteEvidencePayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.EvidenceID.TenantID())
@@ -964,6 +956,56 @@ func (r *mutationResolver) DeleteEvidence(ctx context.Context, input types.Delet
}, nil
}
// UploadTaskEvidence is the resolver for the uploadTaskEvidence field.
func (r *mutationResolver) UploadTaskEvidence(ctx context.Context, input types.UploadTaskEvidenceInput) (*types.UploadTaskEvidencePayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.TaskID.TenantID())
evidence, err := svc.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.
func (r *mutationResolver) UploadMeasureEvidence(ctx context.Context, input types.UploadMeasureEvidenceInput) (*types.UploadMeasureEvidencePayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.MeasureID.TenantID())
evidence, err := svc.Evidences.UploadMeasureEvidence(
ctx,
probo.UploadMeasureEvidenceRequest{
MeasureID: input.MeasureID,
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 measure evidence: %w", err))
}
return &types.UploadMeasureEvidencePayload{
EvidenceEdge: types.NewEvidenceEdge(evidence, coredata.EvidenceOrderFieldCreatedAt),
}, nil
}
// UploadVendorComplianceReport is the resolver for the uploadVendorComplianceReport field.
func (r *mutationResolver) UploadVendorComplianceReport(ctx context.Context, input types.UploadVendorComplianceReportInput) (*types.UploadVendorComplianceReportPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.VendorID.TenantID())