From b424c03be0254da26c5c06e41bb9725945411224 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 14 Jan 2025 04:19:20 +0000 Subject: [PATCH] add ability to sync to Transcend --- src/cli-sync-ot.ts | 31 +++++++-- src/codecs.ts | 28 +++++++++ src/graphql/gqls/assessment.ts | 13 ++++ src/oneTrust/helpers/index.ts | 2 +- .../helpers/parseCliSyncOtArguments.ts | 38 ++++++----- ...ent.ts => syncOneTrustAssessmentToDisk.ts} | 2 +- .../syncOneTrustAssessmentToTranscend.ts | 63 +++++++++++++++++++ .../helpers/syncOneTrustAssessments.ts | 24 +++++-- 8 files changed, 174 insertions(+), 27 deletions(-) rename src/oneTrust/helpers/{writeOneTrustAssessment.ts => syncOneTrustAssessmentToDisk.ts} (96%) create mode 100644 src/oneTrust/helpers/syncOneTrustAssessmentToTranscend.ts diff --git a/src/cli-sync-ot.ts b/src/cli-sync-ot.ts index e01eae11..751981ac 100644 --- a/src/cli-sync-ot.ts +++ b/src/cli-sync-ot.ts @@ -5,6 +5,7 @@ import colors from 'colors'; import { parseCliSyncOtArguments, createOneTrustGotInstance } from './oneTrust'; import { OneTrustPullResource } from './enums'; import { syncOneTrustAssessments } from './oneTrust/helpers/syncOneTrustAssessments'; +import { buildTranscendGraphQLClient } from './graphql'; /** * Pull configuration from OneTrust down locally to disk @@ -16,15 +17,37 @@ import { syncOneTrustAssessments } from './oneTrust/helpers/syncOneTrustAssessme * yarn cli-sync-ot --hostname=customer.my.onetrust.com --auth=$ONE_TRUST_OAUTH_TOKEN --file=./oneTrustAssessment.json */ async function main(): Promise { - const { file, fileFormat, hostname, auth, resource, debug, dryRun } = - parseCliSyncOtArguments(); + const { + file, + fileFormat, + hostname, + oneTrustAuth, + transcendAuth, + transcendUrl, + resource, + debug, + dryRun, + } = parseCliSyncOtArguments(); // use the hostname and auth token to instantiate a client to talk to OneTrust - const oneTrust = createOneTrustGotInstance({ hostname, auth }); + const oneTrust = createOneTrustGotInstance({ hostname, auth: oneTrustAuth }); try { if (resource === OneTrustPullResource.Assessments) { - await syncOneTrustAssessments({ oneTrust, file, fileFormat, dryRun }); + await syncOneTrustAssessments({ + oneTrust, + file, + fileFormat, + dryRun, + ...(transcendAuth && transcendUrl + ? { + transcend: buildTranscendGraphQLClient( + transcendUrl, + transcendAuth, + ), + } + : {}), + }); } } catch (err) { logger.error( diff --git a/src/codecs.ts b/src/codecs.ts index ae052b36..ef63a602 100644 --- a/src/codecs.ts +++ b/src/codecs.ts @@ -2073,3 +2073,31 @@ export const PathfinderPromptRunMetadata = t.partial({ export type PathfinderPromptRunMetadata = t.TypeOf< typeof PathfinderPromptRunMetadata >; + +/** The columns of a row of a OneTrust Assessment form to import into Transcend. */ +const OneTrustAssessmentColumnInput = t.intersection([ + t.type({ + /** The title of the column */ + title: t.string, + }), + t.partial({ + /** The optional value of the column */ + value: t.string, + }), +]); + +/** A row with information of the OneTrust assessment form to import into Transcend */ +const OneTrustAssessmentRowInput = t.type({ + /** A list of columns within this row. */ + columns: t.array(OneTrustAssessmentColumnInput), +}); + +/** Input for importing multiple OneTrust assessment forms into Transcend */ +export const ImportOnetrustAssessmentsInput = t.type({ + /** 'The rows of the CSV file.' */ + rows: t.array(OneTrustAssessmentRowInput), +}); +/** Type override */ +export type ImportOnetrustAssessmentsInput = t.TypeOf< + typeof ImportOnetrustAssessmentsInput +>; diff --git a/src/graphql/gqls/assessment.ts b/src/graphql/gqls/assessment.ts index 6b8121fc..d0fe9313 100644 --- a/src/graphql/gqls/assessment.ts +++ b/src/graphql/gqls/assessment.ts @@ -283,3 +283,16 @@ export const ASSESSMENTS = gql` } } `; + +export const IMPORT_ONE_TRUST_ASSESSMENT_FORMS = gql` + mutation TranscendCliImportOneTrustAssessmentForms( + $input: ImportOnetrustAssessmentsInput! + ) { + importOneTrustAssessmentForms(input: $input) { + assessmentForms { + id + title + } + } + } +`; diff --git a/src/oneTrust/helpers/index.ts b/src/oneTrust/helpers/index.ts index 504bb11e..cb7534f6 100644 --- a/src/oneTrust/helpers/index.ts +++ b/src/oneTrust/helpers/index.ts @@ -1,3 +1,3 @@ export * from './flattenOneTrustAssessment'; export * from './parseCliSyncOtArguments'; -export * from './writeOneTrustAssessment'; +export * from './syncOneTrustAssessmentToDisk'; diff --git a/src/oneTrust/helpers/parseCliSyncOtArguments.ts b/src/oneTrust/helpers/parseCliSyncOtArguments.ts index b7a59a6c..f6c8463c 100644 --- a/src/oneTrust/helpers/parseCliSyncOtArguments.ts +++ b/src/oneTrust/helpers/parseCliSyncOtArguments.ts @@ -15,6 +15,8 @@ interface OneTrustCliArguments { oneTrustAuth: string; /** The Transcend API key to authenticate the requests to Transcend */ transcendAuth: string; + /** The Transcend URL where to forward requests */ + transcendUrl: string; /** The resource to pull from OneTrust */ resource: OneTrustPullResource; /** Whether to enable debugging while reporting errors */ @@ -114,22 +116,25 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { return process.exit(1); } - const splitFile = file.split('.'); - if (splitFile.length < 2) { - logger.error( - colors.red( - 'The "file" parameter has an invalid format. Expected a path with extensions. e.g. --file=./pathToFile.json.', - ), - ); - return process.exit(1); - } - if (splitFile.at(-1) !== fileFormat) { - logger.error( - colors.red( - `The "file" and "fileFormat" parameters must specify the same format! Got file=${file} and fileFormat=${fileFormat}`, - ), - ); - return process.exit(1); + if (file) { + const splitFile = file.split('.'); + if (splitFile.length < 2) { + logger.error( + colors.red( + 'The "file" parameter has an invalid format. Expected a path with extensions. e.g. --file=./pathToFile.json.', + ), + ); + return process.exit(1); + } + if (splitFile.at(-1) !== fileFormat) { + logger.error( + colors.red( + // eslint-disable-next-line max-len + `The "file" and "fileFormat" parameters must specify the same format! Got file=${file} and fileFormat=${fileFormat}`, + ), + ); + return process.exit(1); + } } if (!hostname) { @@ -179,5 +184,6 @@ export const parseCliSyncOtArguments = (): OneTrustCliArguments => { fileFormat, dryRun, transcendAuth, + transcendUrl, }; }; diff --git a/src/oneTrust/helpers/writeOneTrustAssessment.ts b/src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts similarity index 96% rename from src/oneTrust/helpers/writeOneTrustAssessment.ts rename to src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts index df7b320b..504515e9 100644 --- a/src/oneTrust/helpers/writeOneTrustAssessment.ts +++ b/src/oneTrust/helpers/syncOneTrustAssessmentToDisk.ts @@ -12,7 +12,7 @@ import { oneTrustAssessmentToCsv } from './oneTrustAssessmentToCsv'; * * @param param - information about the assessment to write */ -export const writeOneTrustAssessment = ({ +export const syncOneTrustAssessmentToDisk = ({ file, fileFormat, assessment, diff --git a/src/oneTrust/helpers/syncOneTrustAssessmentToTranscend.ts b/src/oneTrust/helpers/syncOneTrustAssessmentToTranscend.ts new file mode 100644 index 00000000..f728ad58 --- /dev/null +++ b/src/oneTrust/helpers/syncOneTrustAssessmentToTranscend.ts @@ -0,0 +1,63 @@ +import { logger } from '../../logger'; +import { oneTrustAssessmentToCsvRecord } from './oneTrustAssessmentToCsvRecord'; +import { GraphQLClient } from 'graphql-request'; +import { + IMPORT_ONE_TRUST_ASSESSMENT_FORMS, + makeGraphQLRequest, +} from '../../graphql'; +import { ImportOnetrustAssessmentsInput } from '../../codecs'; +import { OneTrustEnrichedAssessment } from '../codecs'; + +export interface AssessmentForm { + /** ID of Assessment Form */ + id: string; + /** Title of Assessment Form */ + name: string; +} + +/** + * Write the assessment to a Transcend instance. + * + * + * @param param - information about the assessment and Transcend instance to write to + */ +export const syncOneTrustAssessmentToTranscend = async ({ + transcend, + assessment, +}: { + /** the Transcend client instance */ + transcend: GraphQLClient; + /** the assessment to sync to Transcend */ + assessment: OneTrustEnrichedAssessment; +}): Promise => { + logger.info(); + + // convert the OneTrust assessment object into a CSV Record (a map from the csv header to values) + const csvRecord = oneTrustAssessmentToCsvRecord(assessment); + + // transform the csv record into a valid input to the mutation + const input: ImportOnetrustAssessmentsInput = { + rows: [ + { + columns: Object.entries(csvRecord).map(([key, value]) => ({ + title: key, + value: value.toString(), + })), + }, + ], + }; + + const { + importOneTrustAssessmentForms: { assessmentForms }, + } = await makeGraphQLRequest<{ + /** the importOneTrustAssessmentForms mutation */ + importOneTrustAssessmentForms: { + /** Created Assessment Forms */ + assessmentForms: AssessmentForm[]; + }; + }>(transcend, IMPORT_ONE_TRUST_ASSESSMENT_FORMS, { + input, + }); + + return assessmentForms[0]; +}; diff --git a/src/oneTrust/helpers/syncOneTrustAssessments.ts b/src/oneTrust/helpers/syncOneTrustAssessments.ts index 2ebb4fc9..b4894cdd 100644 --- a/src/oneTrust/helpers/syncOneTrustAssessments.ts +++ b/src/oneTrust/helpers/syncOneTrustAssessments.ts @@ -13,18 +13,29 @@ import { } from '@transcend-io/privacy-types'; import uniq from 'lodash/uniq'; import { enrichOneTrustAssessment } from './enrichOneTrustAssessment'; -import { writeOneTrustAssessment } from './writeOneTrustAssessment'; +import { syncOneTrustAssessmentToDisk } from './syncOneTrustAssessmentToDisk'; import { OneTrustFileFormat } from '../../enums'; -import { oneTrustAssessmentToCsvRecord } from './oneTrustAssessmentToCsvRecord'; +import { GraphQLClient } from 'graphql-request'; +import { syncOneTrustAssessmentToTranscend } from './syncOneTrustAssessmentToTranscend'; + +export interface AssessmentForm { + /** ID of Assessment Form */ + id: string; + /** Title of Assessment Form */ + name: string; +} export const syncOneTrustAssessments = async ({ oneTrust, file, fileFormat, dryRun, + transcend, }: { /** the OneTrust client instance */ oneTrust: Got; + /** the Transcend client instance */ + transcend?: GraphQLClient; /** Whether to write to file instead of syncing to Transcend */ dryRun: boolean; /** the path to the file in case dryRun is true */ @@ -84,16 +95,19 @@ export const syncOneTrustAssessments = async ({ if (dryRun && file && fileFormat) { // sync to file - writeOneTrustAssessment({ + syncOneTrustAssessmentToDisk({ assessment: enrichedAssessment, index, total: assessments.length, file, fileFormat, }); - } else if (fileFormat === OneTrustFileFormat.Csv) { + } else if (fileFormat === OneTrustFileFormat.Csv && transcend) { // sync to transcend - // const csvEntry = oneTrustAssessmentToCsvRecord(enrichedAssessment); + await syncOneTrustAssessmentToTranscend({ + assessment: enrichedAssessment, + transcend, + }); } }); };