diff --git a/.pnp.cjs b/.pnp.cjs index e0dc0339..e7071bb5 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -32,7 +32,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/handlebars-utils", "npm:1.1.0"],\ ["@transcend-io/internationalization", "npm:1.6.0"],\ ["@transcend-io/persisted-state", "npm:1.0.4"],\ - ["@transcend-io/privacy-types", "npm:4.93.0"],\ + ["@transcend-io/privacy-types", "npm:4.94.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ ["@transcend-io/type-utils", "npm:1.5.0"],\ ["@types/bluebird", "npm:3.5.38"],\ @@ -682,7 +682,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@transcend-io/handlebars-utils", "npm:1.1.0"],\ ["@transcend-io/internationalization", "npm:1.6.0"],\ ["@transcend-io/persisted-state", "npm:1.0.4"],\ - ["@transcend-io/privacy-types", "npm:4.93.0"],\ + ["@transcend-io/privacy-types", "npm:4.94.0"],\ ["@transcend-io/secret-value", "npm:1.2.0"],\ ["@transcend-io/type-utils", "npm:1.5.0"],\ ["@types/bluebird", "npm:3.5.38"],\ @@ -781,10 +781,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@transcend-io/privacy-types", [\ - ["npm:4.93.0", {\ - "packageLocation": "./.yarn/cache/@transcend-io-privacy-types-npm-4.93.0-18808935f7-dda3743a4b.zip/node_modules/@transcend-io/privacy-types/",\ + ["npm:4.94.0", {\ + "packageLocation": "./.yarn/cache/@transcend-io-privacy-types-npm-4.94.0-c5ce6b7558-8cc1a8dd54.zip/node_modules/@transcend-io/privacy-types/",\ "packageDependencies": [\ - ["@transcend-io/privacy-types", "npm:4.93.0"],\ + ["@transcend-io/privacy-types", "npm:4.94.0"],\ ["@transcend-io/type-utils", "npm:1.0.5"],\ ["fp-ts", "npm:2.16.1"],\ ["io-ts", "virtual:a57afaf9d13087a7202de8c93ac4854c9e2828bad7709250829ec4c7bc9dc95ecc2858c25612aa1774c986aedc232c76957076a1da3156fd2ab63ae5551b086f#npm:2.2.21"]\ diff --git a/.yarn/cache/@transcend-io-privacy-types-npm-4.93.0-18808935f7-dda3743a4b.zip b/.yarn/cache/@transcend-io-privacy-types-npm-4.94.0-c5ce6b7558-8cc1a8dd54.zip similarity index 85% rename from .yarn/cache/@transcend-io-privacy-types-npm-4.93.0-18808935f7-dda3743a4b.zip rename to .yarn/cache/@transcend-io-privacy-types-npm-4.94.0-c5ce6b7558-8cc1a8dd54.zip index 0545256e..0a9da4e9 100644 Binary files a/.yarn/cache/@transcend-io-privacy-types-npm-4.93.0-18808935f7-dda3743a4b.zip and b/.yarn/cache/@transcend-io-privacy-types-npm-4.94.0-c5ce6b7558-8cc1a8dd54.zip differ diff --git a/package.json b/package.json index cfb4a75d..158b6158 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@transcend-io/handlebars-utils": "^1.1.0", "@transcend-io/internationalization": "^1.6.0", "@transcend-io/persisted-state": "^1.0.4", - "@transcend-io/privacy-types": "^4.93.0", + "@transcend-io/privacy-types": "^4.94.0", "@transcend-io/secret-value": "^1.2.0", "@transcend-io/type-utils": "^1.5.0", "bluebird": "^3.7.2", diff --git a/src/codecs.ts b/src/codecs.ts index c0859c12..a15e0f65 100644 --- a/src/codecs.ts +++ b/src/codecs.ts @@ -1526,18 +1526,6 @@ export const RiskAssignmentInput = t.partial({ /** Type override */ export type RiskAssignmentInput = t.TypeOf; -export const RiskLogicInput = t.type({ - /** The risk to assign to this question if the response matches the provided logic */ - 'risk-assignment': RiskAssignmentInput, - /** The operator to use when comparing the response to the operands */ - 'comparison-operator': valuesOf(ComparisonOperator), - /** The values to compare the response to */ - 'comparison-operands': t.array(t.string), -}); - -/** Type override */ -export type RiskLogicInput = t.TypeOf; - export const AssessmentAnswerOptionInput = t.type({ /** Value of answer */ value: t.string, @@ -1569,7 +1557,7 @@ export const AssessmentSectionQuestionInput = t.intersection([ /** Display logic for the question */ 'display-logic': AssessmentDisplayLogicInput, /** Risk logic for the question */ - 'risk-logic': t.array(RiskLogicInput), + 'risk-logic': t.array(t.string), /** Risk category titles for the question */ 'risk-categories': t.array(t.string), /** Risk framework titles for the question */ @@ -1598,12 +1586,24 @@ export type AssessmentSectionQuestionInput = t.TypeOf< typeof AssessmentSectionQuestionInput >; -export const AssessmentSectionInput = t.type({ - /** The title of the assessment section */ - title: t.string, - /** The questions in the assessment section */ - questions: t.array(AssessmentSectionQuestionInput), -}); +export const AssessmentSectionInput = t.intersection([ + t.type({ + /** The title of the assessment section */ + title: t.string, + /** The questions in the assessment section */ + questions: t.array(AssessmentSectionQuestionInput), + }), + t.partial({ + /** Email address of those assigned */ + assignees: t.array(t.string), + /** Email address of those externally assigned */ + 'external-assignees': t.array(t.string), + /** Status of section */ + status: t.string, + /** Whether assessment is reviewed */ + 'is-reviewed': t.boolean, + }), +]); /** Type override */ export type AssessmentSectionInput = t.TypeOf; @@ -1640,6 +1640,8 @@ export const AssessmentTemplateInput = t.intersection([ creator: t.string, /** Whether the template is in a locked status */ locked: t.boolean, + /** ID of parent template this was cloned from */ + 'parent-id': t.string, /** Whether the template is archived */ archived: t.boolean, /** The date that the assessment was created */ diff --git a/src/graphql/fetchAllAssessmentTemplates.ts b/src/graphql/fetchAllAssessmentTemplates.ts new file mode 100644 index 00000000..42a0ccac --- /dev/null +++ b/src/graphql/fetchAllAssessmentTemplates.ts @@ -0,0 +1,83 @@ +import { GraphQLClient } from 'graphql-request'; +import { ASSESSMENT_TEMPLATES } from './gqls'; +import { makeGraphQLRequest } from './makeGraphQLRequest'; +import type { + AssessmentSection, + RetentionSchedule, + UserPreview, +} from './fetchAllAssessments'; +import { + AssessmentFormTemplateSource, + AssessmentFormTemplateStatus, +} from '@transcend-io/privacy-types'; + +/** + * Represents an assessment template with various properties and metadata. + */ +export interface AssessmentTemplate { + /** The ID of the assessment template */ + id: string; + /** The user who created the assessment template */ + creator: UserPreview; + /** The user who last edited the assessment template */ + lastEditor: UserPreview; + /** The title of the assessment template */ + title: string; + /** The description of the assessment template */ + description: string; + /** The current status of the assessment template */ + status: AssessmentFormTemplateStatus; + /** The source fo the form template */ + source: AssessmentFormTemplateSource; + /** ID of parent template */ + parentId: string; + /** Indicates if the assessment template is locked */ + isLocked: boolean; + /** Indicates if the assessment template is archived */ + isArchived: boolean; + /** The date when the assessment template was created */ + createdAt: string; + /** The date when the assessment template was last updated */ + updatedAt: string; + /** The retention schedule of the assessment template */ + retentionSchedule: RetentionSchedule; + /** The sections of the assessment template */ + sections: AssessmentSection[]; +} + +const PAGE_SIZE = 20; + +/** + * Fetch all assessment templates in the organization + * + * @param client - GraphQL client + * @returns All assessment templates in the organization + */ +export async function fetchAllAssessmentTemplates( + client: GraphQLClient, +): Promise { + const assessmentTemplates: AssessmentTemplate[] = []; + let offset = 0; + + let shouldContinue = false; + do { + const { + assessmentFormTemplates: { nodes }, + // eslint-disable-next-line no-await-in-loop + } = await makeGraphQLRequest<{ + /** Templates */ + assessmentFormTemplates: { + /** Nodes */ + nodes: AssessmentTemplate[]; + }; + }>(client, ASSESSMENT_TEMPLATES, { + first: PAGE_SIZE, + offset, + }); + assessmentTemplates.push(...nodes); + offset += PAGE_SIZE; + shouldContinue = nodes.length === PAGE_SIZE; + } while (shouldContinue); + + return assessmentTemplates.sort((a, b) => a.title.localeCompare(b.title)); +} diff --git a/src/graphql/fetchAllAssessments.ts b/src/graphql/fetchAllAssessments.ts new file mode 100644 index 00000000..4be3a020 --- /dev/null +++ b/src/graphql/fetchAllAssessments.ts @@ -0,0 +1,405 @@ +/* eslint-disable max-lines */ +import { GraphQLClient } from 'graphql-request'; +import { ASSESSMENTS } from './gqls'; +import { makeGraphQLRequest } from './makeGraphQLRequest'; +import { + AssessmentFormStatus, + AssessmentQuestionSubType, + AssessmentQuestionType, + AssessmentSyncColumn, + AssessmentSyncModel, + AssessmentsDisplayLogicAction, + AttributeSupportedResourceType, + ComparisonOperator, + DataCategoryType, + LogicOperator, + ProcessingPurpose, + RetentionScheduleOperation, + RetentionScheduleType, +} from '@transcend-io/privacy-types'; + +/** + * Represents an assessment with various properties and metadata. + */ +export interface Assessment { + /** The ID of the assessment */ + id: string; + /** The user who created the assessment */ + creator: UserPreview; + /** The user who last edited the assessment */ + lastEditor: UserPreview; + /** The title of the assessment */ + title: string; + /** The description of the assessment */ + description: string; + /** The current status of the assessment */ + status: AssessmentFormStatus; + /** The users assigned to the assessment */ + assignees: UserPreview[]; + /** The external users assigned to the assessment */ + externalAssignees: ExternalUser[]; + /** The users who are reviewers of the assessment */ + reviewers: UserPreview[]; + /** Indicates if the assessment is locked */ + isLocked: boolean; + /** Indicates if the assessment is archived */ + isArchived: boolean; + /** Indicates if the assessment was created externally */ + isExternallyCreated: boolean; + /** The due date of the assessment */ + dueDate: string; + /** The date when the assessment was created */ + createdAt: string; + /** The date when the assessment was last updated */ + updatedAt: string; + /** The date when the assessment was assigned */ + assignedAt: string; + /** The date when the assessment was submitted */ + submittedAt: string; + /** The date when the assessment was approved */ + approvedAt: string; + /** The date when the assessment was rejected */ + rejectedAt: string; + /** Indicates if the title of the assessment is internal */ + titleIsInternal: boolean; + /** The retention schedule of the assessment */ + retentionSchedule: RetentionSchedule; + /** The attribute values associated with the assessment */ + attributeValues: AttributeValue[]; + /** The sections of the assessment */ + sections: AssessmentSection[]; + /** The group to which the assessment belongs */ + assessmentGroup: AssessmentGroup; + /** The resources associated with the assessment */ + resources: AssessmentResource[]; + /** The rows that are synced with the assessment */ + syncedRows: AssessmentResource[]; +} + +export interface UserPreview { + /** ID of user */ + id: string; + /** Email of user */ + email: string; + /** Name of user */ + name: string; +} + +export interface ExternalUser { + /** ID of external user */ + id: string; + /** Email of external user */ + email: string; +} + +export interface RetentionSchedule { + /** ID of retention schedule */ + id: string; + /** Type */ + type: RetentionScheduleType; + /** Duration of retention schedule */ + durationDays: number; + /** The operation to perform on the retention schedule */ + operation: RetentionScheduleOperation; +} + +interface AttributeValue { + /** Name of attribute value */ + name: string; + /** Key */ + attributeKey: { + /** Name of key */ + name: string; + }; +} + +export interface AssessmentSection { + /** ID of section */ + id: string; + /** Title of section */ + title: string; + /** Status of section */ + status: string; + /** Index of section */ + index: number; + /** Questions */ + questions: AssessmentQuestion[]; + /** Assignees */ + assignees: UserPreview[]; + /** External assignees */ + externalAssignees: ExternalUser[]; + /** Whether is reviewed */ + isReviewed: boolean; +} + +/** + * Represents a question in the assessment. + */ +export interface AssessmentQuestion { + /** + * Unique identifier for the question. + */ + id: string; + /** Title of the question */ + title: string; + /** Index of the question in the assessment */ + index: number; + /** Type of the question */ + type: AssessmentQuestionType; + /** Subtype of the question */ + subType: AssessmentQuestionSubType; + /** Placeholder text for the question */ + placeholder: string; + /** Description of the question */ + description: string; + /** Indicates if the question is required */ + isRequired: boolean; + /** Logic for displaying the question */ + displayLogic?: { + /** Action to take */ + action: AssessmentsDisplayLogicAction; + /** Rule logic */ + rule?: AssessmentRuleInputResponse; + /** Nested rule logic */ + nestedRule?: AssessmentNestedRuleInputResponse; + }; + /** Logic for assessing risk related to the question */ + riskLogic: string[]; + /** Indicates if risk evaluation is required for the question */ + requireRiskEvaluation: boolean; + /** Indicates if risk matrix evaluation is required for the question */ + requireRiskMatrixEvaluation: boolean; + /** Categories of risk associated with the question */ + riskCategories: RiskCategory[]; + /** Framework used for risk assessment */ + riskFramework?: RiskFramework; + /** Level of risk associated with the question */ + riskLevel?: RiskLevel; + /** Risk level assigned by the reviewer */ + reviewerRiskLevel?: RiskLevel; + /** Risk level derived from the risk matrix */ + riskLevelFromRiskMatrix?: RiskLevel; + /** Options available for answering the question */ + answerOptions: AssessmentAnswerOption[]; + /** Answers selected for the question */ + selectedAnswers: AssessmentAnswer[]; + /** User who responded to the question */ + respondent: UserPreview; + /** Key attribute associated with the question */ + attributeKey?: { + /** Name of key */ + name: string; + }; + /** Email of the external respondent */ + externalRespondentEmail?: string; + /** Comments related to the question */ + comments: Comment[]; + /** Allowed MIME types for file uploads in the question */ + allowedMimeTypes: string[]; + /** Timestamp of the last update to the question */ + updatedAt: string; + /** Reference identifier for the question */ + referenceId: string; + /** Previous submissions related to the question */ + previousSubmissions: AssessmentPreviousSubmission[]; + /** Indicates if selecting "Other" is allowed for the question */ + allowSelectOther: boolean; + /** Model used for synchronization */ + syncModel: AssessmentSyncModel; + /** Column used for synchronization */ + syncColumn: AssessmentSyncColumn; + /** Row IDs used for synchronization */ + syncRowIds: string[]; + /** Indicates if synchronization override is allowed */ + syncOverride: boolean; +} + +export interface RiskCategory { + /** ID of category */ + id: string; + /** Title of category */ + title: string; +} + +export interface RiskFramework { + /** ID of framework */ + id: string; + /** Title of framework */ + title: string; + /** Description of framework */ + description: string; + /** Risk levels */ + riskLevels: RiskLevel[]; + /** Risk categories */ + riskCategories: RiskCategory[]; + /** Risk matrix columns */ + riskMatrixColumns: RiskMatrixColumn[]; + /** Risk matrix rows */ + riskMatrixRows: RiskMatrixRow[]; + /** Risk matrix settings */ + riskMatrix: RiskMatrix[][]; + /** Creator of risk framework */ + creator?: UserPreview; + /** Risk matrix row title */ + riskMatrixRowTitle: string; + /** Risk matrix column title */ + riskMatrixColumnTitle: string; +} + +/** + * Represents the input for an assessment rule. + */ +export interface AssessmentRuleInputResponse { + /** Reference ID of the question this rule depends on */ + dependsOnQuestionReferenceId: string; + /** Operator used for comparison */ + comparisonOperator: ComparisonOperator; + /** Operands used for comparison */ + comparisonOperands?: string[]; +} + +/** + * Represents the input for a nested assessment rule. + */ +export interface AssessmentNestedRuleInputResponse { + /** Logical operator for combining rules */ + logicOperator: LogicOperator; + /** List of rules */ + rules: AssessmentRuleInputResponse[]; + /** List of nested rules */ + nestedRules?: AssessmentNestedRuleInputResponse[]; +} + +export interface RiskLevel { + /** ID of risk level */ + id: string; + /** Title of risk level */ + title: string; +} + +export interface RiskMatrix { + /** ID of risk matrix */ + id: string; + /** Title of risk matrix */ + title: string; +} + +export interface RiskMatrixColumn { + /** ID of column */ + id: string; + /** Title of column */ + title: string; +} + +export interface RiskMatrixRow { + /** ID of row */ + id: string; + /** Title of row */ + title: string; +} + +export interface AssessmentAnswerOption { + /** ID of answer option */ + id: string; + /** Index of answer option */ + index: number; + /** Value of answer */ + value: string; +} + +export interface AssessmentAnswer { + /** ID of answer */ + id: string; + /** Index of answer */ + index: number; + /** Value of answer */ + value: string; +} + +export interface AssessmentComment { + /** ID of comment */ + id: string; + /** Content of comment */ + content: string; + /** Date comment made */ + createdAt: string; + /** Date comment updated */ + updatedAt: string; + /** Author of comment */ + author?: UserPreview; +} + +export interface AssessmentPreviousSubmission { + /** Id of submission */ + id: string; + /** Date updated */ + updatedAt: string; + /** ID of question */ + assessmentQuestionId: string; + /** Answers */ + answers: AssessmentAnswer[]; +} + +export interface AssessmentGroup { + /** ID of group */ + id: string; + /** Title of group */ + title: string; + /** Description of group */ + description: string; +} + +export interface AssessmentResource { + /** Type of resource */ + resourceType: AttributeSupportedResourceType; + /** ID of resource */ + id: string; + /** Title of resource */ + title?: string; + /** Name of resource */ + name?: string; + /** Category of resource */ + category?: DataCategoryType; + /** Purpose of resource */ + purpose?: ProcessingPurpose; + /** Type of integration */ + type?: string; +} + +const PAGE_SIZE = 20; + +/** + * Fetch all assessments in the organization + * + * @param client - GraphQL client + * @returns All assessments in the organization + */ +export async function fetchAllAssessments( + client: GraphQLClient, +): Promise { + const assessments: Assessment[] = []; + let offset = 0; + + let shouldContinue = false; + do { + const { + assessmentForms: { nodes }, + // eslint-disable-next-line no-await-in-loop + } = await makeGraphQLRequest<{ + /** Forms */ + assessmentForms: { + /** Nodes */ + nodes: Assessment[]; + }; + }>(client, ASSESSMENTS, { + first: PAGE_SIZE, + offset, + }); + assessments.push(...nodes); + offset += PAGE_SIZE; + shouldContinue = nodes.length === PAGE_SIZE; + } while (shouldContinue); + + return assessments.sort((a, b) => a.title.localeCompare(b.title)); +} +/* eslint-enable max-lines */ diff --git a/src/graphql/gqls/assessment.ts b/src/graphql/gqls/assessment.ts new file mode 100644 index 00000000..2994ebfc --- /dev/null +++ b/src/graphql/gqls/assessment.ts @@ -0,0 +1,308 @@ +import { gql } from 'graphql-request'; + +export const ASSESSMENT_SECTION_FIELDS = ` + id + title + status + index + questions { + id + title + index + type + subType + placeholder + description + isRequired + displayLogic { + action + rule { + dependsOnQuestionReferenceId + comparisonOperator + comparisonOperands + } + nestedRule { + logicOperator + rules { + dependsOnQuestionReferenceId + comparisonOperator + comparisonOperands + } + nestedRules { + logicOperator + rules { + dependsOnQuestionReferenceId + comparisonOperator + comparisonOperands + } + } + } + } + riskLogic + requireRiskEvaluation + requireRiskMatrixEvaluation + riskCategories { + id + title + } + riskFramework { + id + title + description + riskLevels { + id + title + } + riskCategories { + id + title + } + riskMatrixColumns { + id + title + } + riskMatrixRows { + id + title + } + riskMatrix { + id + title + } + creator { + id + email + name + } + riskMatrixRowTitle + riskMatrixColumnTitle + } + riskLevel { + id + title + } + reviewerRiskLevel { + id + title + } + riskLevelFromRiskMatrix { + id + title + } + answerOptions { + id + index + value + } + selectedAnswers { + ... on AssessmentAnswerInterface { + id + index + value + } + } + respondent { + id + email + name + } + attributeKey { + name + } + externalRespondentEmail + comments { + id + content + createdAt + updatedAt + author { + id + email + name + } + } + allowedMimeTypes + updatedAt + referenceId + previousSubmissions { + id + updatedAt + assessmentQuestionId + answers { + ... on AssessmentAnswerInterface { + id + index + value + } + } + } + allowSelectOther + syncModel + syncColumn + syncRowIds + syncOverride + } + assignees { + id + email + name + } + externalAssignees { + id + email + } + isReviewed +`; + +// TODO: https://transcend.height.app/T-27909 - enable optimizations +// isExportCsv: true +// useMaster: false +// orderBy: [ +// { field: createdAt, direction: ASC } +// { field: name, direction: ASC } +// ] +export const ASSESSMENTS = gql` + query TranscendCliAssessments( + $first: Int! + $offset: Int! + $filterBy: AssessmentFormFiltersInput + ) { + assessmentForms(first: $first, offset: $offset, filterBy: $filterBy) { + nodes { + id + creator { + id + email + name + } + lastEditor { + id + email + name + } + title + description + status + assignees { + id + email + name + } + externalAssignees { + id + email + } + reviewers { + id + email + name + } + isLocked + isArchived + isExternallyCreated + dueDate + createdAt + updatedAt + assignedAt + submittedAt + approvedAt + rejectedAt + titleIsInternal + retentionSchedule { + id + type + durationDays + operation + } + attributeValues { + name + attributeKey { + name + } + } + sections { + ${ASSESSMENT_SECTION_FIELDS} + } + assessmentGroup { + id + title + description + } + resources { + resourceType + ... on AttributeBusinessEntityResource { + id + title + } + ... on AttributeDataSiloResource { + id + title + } + ... on AttributeDataSubCategoryResource { + id + name + category + } + ... on AttributeSubDataPointResource { + id + name + } + ... on AttributeProcessingPurposeSubCategoryResource { + id + name + purpose + } + ... on AttributeRequestResource { + id + type + } + ... on AttributeVendorResource { + id + title + } + ... on AttributePromptResource { + id + title + } + ... on AttributePromptRunResource { + id + title + } + ... on AttributePromptGroupResource { + id + title + } + } + syncedRows { + resourceType + ... on AttributeBusinessEntityResource { + id + title + } + ... on AttributeDataSiloResource { + id + title + } + ... on AttributeDataSubCategoryResource { + id + name + category + } + ... on AttributeSubDataPointResource { + id + name + } + ... on AttributeProcessingPurposeSubCategoryResource { + id + name + purpose + } + ... on AttributeVendorResource { + id + title + } + } + } + } + } +`; diff --git a/src/graphql/gqls/assessmentTemplate.ts b/src/graphql/gqls/assessmentTemplate.ts new file mode 100644 index 00000000..ad3e8598 --- /dev/null +++ b/src/graphql/gqls/assessmentTemplate.ts @@ -0,0 +1,67 @@ +import { gql } from 'graphql-request'; +import { ASSESSMENT_SECTION_FIELDS } from './assessment'; + +// TODO: https://transcend.height.app/T-27909 - enable optimizations +// isExportCsv: true +// useMaster: false +// orderBy: [ +// { field: createdAt, direction: ASC } +// { field: name, direction: ASC } +// ] +export const ASSESSMENT_TEMPLATES = gql` + query TranscendCliAssessmentTemplates( + $first: Int! + $offset: Int! + $filterBy: AssessmentFormTemplateFiltersInput + ) { + assessmentFormTemplates( + first: $first + offset: $offset + filterBy: $filterBy + ) { + nodes { + id + creator { + id + email + name + } + lastEditor { + id + email + name + } + title + description + status + source + parentId + isLocked + isArchived + createdAt + updatedAt + retentionSchedule { + id + type + durationDays + operation + createdAt + updatedAt + } + assessmentEmailSet { + id + title + description + isDefault + templates { + id + title + } + } + sections { + ${ASSESSMENT_SECTION_FIELDS} + } + } + } + } +`; diff --git a/src/graphql/gqls/index.ts b/src/graphql/gqls/index.ts index daea78e4..b39f98b3 100644 --- a/src/graphql/gqls/index.ts +++ b/src/graphql/gqls/index.ts @@ -17,6 +17,8 @@ export * from './policy'; export * from './request'; export * from './message'; export * from './RequestEnricher'; +export * from './assessment'; +export * from './assessmentTemplate'; export * from './prompt'; export * from './RequestEnricher'; export * from './RequestDataSilo'; diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 6b451aae..3139599e 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -19,6 +19,8 @@ export * from './setResourceAttributes'; export * from './buildTranscendGraphQLClient'; export * from './retryRequestEnricher'; export * from './gqls'; +export * from './fetchAllAssessmentTemplates'; +export * from './fetchAllAssessments'; export * from './fetchPromptThreads'; export * from './fetchAllPolicies'; export * from './fetchAllRequestIdentifierMetadata'; diff --git a/src/graphql/pullTranscendConfiguration.ts b/src/graphql/pullTranscendConfiguration.ts index f566b20b..d442fefc 100644 --- a/src/graphql/pullTranscendConfiguration.ts +++ b/src/graphql/pullTranscendConfiguration.ts @@ -27,6 +27,10 @@ import { ActionItemInput, TeamInput, ActionItemCollectionInput, + AssessmentInput, + AssessmentTemplateInput, + AssessmentSectionInput, + AssessmentSectionQuestionInput, } from '../codecs'; import { RequestAction, @@ -77,6 +81,8 @@ import { fetchAllTeams } from './fetchAllTeams'; import { fetchAllActionItemCollections } from './fetchAllActionItemCollections'; import { LanguageKey } from '@transcend-io/internationalization'; import { fetchPartitions } from './syncPartitions'; +import { fetchAllAssessments } from './fetchAllAssessments'; +import { fetchAllAssessmentTemplates } from './fetchAllAssessmentTemplates'; export const DEFAULT_TRANSCEND_PULL_RESOURCES = [ TranscendPullResource.DataSilos, @@ -166,6 +172,8 @@ export async function pullTranscendConfiguration( privacyCenters, messages, partitions, + assessments, + assessmentTemplates, ] = await Promise.all([ // Grab all data subjects in the organization resources.includes(TranscendPullResource.DataSilos) || @@ -306,6 +314,14 @@ export async function pullTranscendConfiguration( resources.includes(TranscendPullResource.Partitions) ? fetchPartitions(client) : [], + // Fetch assessments + resources.includes(TranscendPullResource.Assessments) + ? fetchAllAssessments(client) + : [], + // Fetch assessmentTemplates + resources.includes(TranscendPullResource.AssessmentTemplates) + ? fetchAllAssessmentTemplates(client) + : [], ]); const consentManagerTheme = @@ -404,6 +420,286 @@ export async function pullTranscendConfiguration( }; } + // Save assessments + if ( + assessments.length > 0 && + resources.includes(TranscendPullResource.Assessments) + ) { + result.assessments = assessments.map( + ({ + title, + assessmentGroup, + sections, + creator, + description, + status, + assignees, + externalAssignees, + reviewers, + isLocked, + isArchived, + isExternallyCreated, + dueDate, + createdAt, + assignedAt, + submittedAt, + approvedAt, + rejectedAt, + titleIsInternal, + retentionSchedule, + attributeValues, + resources, + syncedRows, + }): AssessmentInput => ({ + title, + group: assessmentGroup.title, + sections: sections.map( + ({ + title, + status, + questions, + assignees, + isReviewed, + externalAssignees, + }): AssessmentSectionInput => ({ + title, + status, + questions: questions.map( + ({ + title, + type, + subType, + placeholder, + description, + isRequired, + referenceId, + displayLogic, + riskLogic, + riskCategories, + riskFramework, + answerOptions, + allowedMimeTypes, + allowSelectOther, + syncModel, + syncColumn, + attributeKey, + requireRiskEvaluation, + requireRiskMatrixEvaluation, + }): AssessmentSectionQuestionInput => ({ + title, + type, + 'sub-type': subType, + placeholder, + description, + 'is-required': isRequired, + 'reference-id': referenceId, + 'display-logic': displayLogic + ? { + action: displayLogic.action, + rule: displayLogic.rule + ? { + 'depends-on-question-reference-id': + displayLogic.rule.dependsOnQuestionReferenceId, + 'comparison-operator': + displayLogic.rule.comparisonOperator, + 'comparison-operands': + displayLogic.rule.comparisonOperands, + } + : undefined, + 'nested-rule': displayLogic.nestedRule + ? { + 'logic-operator': + displayLogic.nestedRule.logicOperator, + rules: displayLogic.nestedRule.rules.map( + (rule) => ({ + 'depends-on-question-reference-id': + rule.dependsOnQuestionReferenceId, + 'comparison-operator': rule.comparisonOperator, + 'comparison-operands': rule.comparisonOperands, + }), + ), + } + : undefined, + } + : undefined, + 'risk-logic': riskLogic, + 'risk-categories': riskCategories.map(({ title }) => title), + 'risk-framework': riskFramework?.title, + 'answer-options': answerOptions.map(({ value }) => ({ value })), + 'allowed-mime-types': allowedMimeTypes, + 'allow-select-other': allowSelectOther, + 'sync-model': syncModel, + 'sync-column': syncColumn, + 'attribute-key': attributeKey?.name, + 'require-risk-evaluation': requireRiskEvaluation, + 'require-risk-matrix-evaluation': requireRiskMatrixEvaluation, + }), + ), + assignees: assignees.map(({ email }) => email), + 'external-assignees': externalAssignees.map(({ email }) => email), + 'is-reviewed': isReviewed, + }), + ), + creator: creator.email, + description, + status, + assignees: assignees.map(({ email }) => email), + 'external-assignees': externalAssignees.map(({ email }) => email), + reviewers: reviewers.map(({ email }) => email), + locked: isLocked, + archived: isArchived, + external: isExternallyCreated, + 'title-is-internal': titleIsInternal, + 'due-date': dueDate, + 'created-at': createdAt, + 'assigned-at': assignedAt, + 'submitted-at': submittedAt, + 'approved-at': approvedAt, + 'rejected-at': rejectedAt, + 'retention-schedule': { + type: retentionSchedule.type, + 'duration-days': retentionSchedule.durationDays, + operand: retentionSchedule.operation, + }, + attributes: + attributeValues !== undefined && attributeValues.length > 0 + ? formatAttributeValues(attributeValues) + : undefined, + resources: resources.map( + ({ resourceType, title, name, category, type, purpose }) => ({ + type: resourceType, + title: category + ? `${category} - ${name}` + : purpose + ? `${purpose} - ${name}` + : title || name || type || '', + }), + ), + rows: syncedRows.map( + ({ resourceType, title, name, category, type, purpose }) => ({ + type: resourceType, + title: category + ? `${category} - ${name}` + : purpose + ? `${purpose} - ${name}` + : title || name || type || '', + }), + ), + }), + ); + } + + // Save assessmentTemplates + if ( + assessmentTemplates.length > 0 && + resources.includes(TranscendPullResource.AssessmentTemplates) + ) { + result['assessment-templates'] = assessmentTemplates.map( + ({ + title, + description, + sections, + status, + source, + creator, + isLocked, + isArchived, + createdAt, + retentionSchedule, + }): AssessmentTemplateInput => ({ + title, + description, + sections: sections.map( + ({ title, questions }): AssessmentSectionInput => ({ + title, + questions: questions.map( + ({ + title, + type, + subType, + placeholder, + description, + isRequired, + referenceId, + displayLogic, + riskLogic, + riskCategories, + riskFramework, + answerOptions, + allowedMimeTypes, + allowSelectOther, + syncModel, + syncColumn, + attributeKey, + requireRiskEvaluation, + requireRiskMatrixEvaluation, + }): AssessmentSectionQuestionInput => ({ + title, + type, + 'sub-type': subType, + placeholder, + description, + 'is-required': isRequired, + 'reference-id': referenceId, + 'display-logic': displayLogic + ? { + action: displayLogic.action, + rule: displayLogic.rule + ? { + 'depends-on-question-reference-id': + displayLogic.rule.dependsOnQuestionReferenceId, + 'comparison-operator': + displayLogic.rule.comparisonOperator, + 'comparison-operands': + displayLogic.rule.comparisonOperands, + } + : undefined, + 'nested-rule': displayLogic.nestedRule + ? { + 'logic-operator': + displayLogic.nestedRule.logicOperator, + rules: displayLogic.nestedRule.rules.map( + (rule) => ({ + 'depends-on-question-reference-id': + rule.dependsOnQuestionReferenceId, + 'comparison-operator': rule.comparisonOperator, + 'comparison-operands': rule.comparisonOperands, + }), + ), + } + : undefined, + } + : undefined, + 'risk-logic': riskLogic, + 'risk-categories': riskCategories.map(({ title }) => title), + 'risk-framework': riskFramework?.title, + 'answer-options': answerOptions.map(({ value }) => ({ value })), + 'allowed-mime-types': allowedMimeTypes, + 'allow-select-other': allowSelectOther, + 'sync-model': syncModel, + 'sync-column': syncColumn, + 'attribute-key': attributeKey?.name, + 'require-risk-evaluation': requireRiskEvaluation, + 'require-risk-matrix-evaluation': requireRiskMatrixEvaluation, + }), + ), + }), + ), + status, + source, + creator: creator.email, + locked: isLocked, + archived: isArchived, + 'created-at': createdAt, + 'retention-schedule': { + type: retentionSchedule.type, + 'duration-days': retentionSchedule.durationDays, + operand: retentionSchedule.operation, + }, + }), + ); + } + // Save prompts if (prompts.length > 0 && resources.includes(TranscendPullResource.Prompts)) { result.prompts = prompts.map( diff --git a/yarn.lock b/yarn.lock index c0ed2b1f..02ff583d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -515,7 +515,7 @@ __metadata: "@transcend-io/handlebars-utils": ^1.1.0 "@transcend-io/internationalization": ^1.6.0 "@transcend-io/persisted-state": ^1.0.4 - "@transcend-io/privacy-types": ^4.93.0 + "@transcend-io/privacy-types": ^4.94.0 "@transcend-io/secret-value": ^1.2.0 "@transcend-io/type-utils": ^1.5.0 "@types/bluebird": ^3.5.38 @@ -643,14 +643,14 @@ __metadata: languageName: node linkType: hard -"@transcend-io/privacy-types@npm:^4.93.0": - version: 4.93.0 - resolution: "@transcend-io/privacy-types@npm:4.93.0" +"@transcend-io/privacy-types@npm:^4.94.0": + version: 4.94.0 + resolution: "@transcend-io/privacy-types@npm:4.94.0" dependencies: "@transcend-io/type-utils": ^1.0.5 fp-ts: ^2.16.1 io-ts: ^2.2.21 - checksum: dda3743a4b33dc84f36020d7b7d72791b1aff0497a0b3fc8047414a2873773150c5832112df994df52bace513f0bd12ff4c0787b41274c66a09541b29363628b + checksum: 8cc1a8dd546f7660a7363937c50bf217026c7e492e5a3ef9c7a482c70bd71f4b51db85dcc264128e5100698dac99d99508f9403e6709e47ee1d4f0c3ae743a86 languageName: node linkType: hard