From 95ff251c20d445e754484d77842d048f38b42f1b Mon Sep 17 00:00:00 2001 From: bencmbrook <7354176+bencmbrook@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:42:06 -0400 Subject: [PATCH] Add CLI sync support for consent resources. Port the legacy CLI work for purposes, preference options, and consent workflow triggers into the monorepo, including the new GraphQL sync helpers, resource wiring, generated docs, and schema updates. Made-with: Cursor --- .changeset/tame-years-pull.md | 7 + packages/cli/README.md | 148 +++++----- .../examples/consent-workflow-triggers.yml | 19 ++ .../schema/transcend-yml-schema-latest.json | 75 +++++ .../cli/schema/transcend-yml-schema-v9.json | 75 +++++ packages/cli/src/codecs.ts | 54 ++++ packages/cli/src/constants.ts | 10 + packages/cli/src/enums.ts | 2 + .../docgen/createPullResourceScopesTable.ts | 15 + .../fetchAllConsentWorkflowTriggers.ts | 72 +++++ .../graphql/fetchAllPreferenceOptionValues.ts | 54 ++++ .../graphql/gqls/consentWorkflowTrigger.ts | 48 ++++ packages/cli/src/lib/graphql/gqls/index.ts | 1 + .../src/lib/graphql/gqls/preferenceTopic.ts | 39 +++ packages/cli/src/lib/graphql/gqls/purpose.ts | 24 ++ packages/cli/src/lib/graphql/index.ts | 5 + .../lib/graphql/pullTranscendConfiguration.ts | 47 ++++ .../graphql/syncConfigurationToTranscend.ts | 29 ++ .../graphql/syncConsentWorkflowTriggers.ts | 139 ++++++++++ .../lib/graphql/syncPreferenceOptionValues.ts | 85 ++++++ packages/cli/src/lib/graphql/syncPurposes.ts | 262 ++++++++++++++++++ 21 files changed, 1138 insertions(+), 72 deletions(-) create mode 100644 .changeset/tame-years-pull.md create mode 100644 packages/cli/examples/consent-workflow-triggers.yml create mode 100644 packages/cli/src/lib/graphql/fetchAllConsentWorkflowTriggers.ts create mode 100644 packages/cli/src/lib/graphql/fetchAllPreferenceOptionValues.ts create mode 100644 packages/cli/src/lib/graphql/gqls/consentWorkflowTrigger.ts create mode 100644 packages/cli/src/lib/graphql/syncConsentWorkflowTriggers.ts create mode 100644 packages/cli/src/lib/graphql/syncPreferenceOptionValues.ts create mode 100644 packages/cli/src/lib/graphql/syncPurposes.ts diff --git a/.changeset/tame-years-pull.md b/.changeset/tame-years-pull.md new file mode 100644 index 00000000..71ee8ad6 --- /dev/null +++ b/.changeset/tame-years-pull.md @@ -0,0 +1,7 @@ +--- +'@transcend-io/cli': minor +--- + +Add sync support for purposes, preference options, and consent workflow triggers. + +This ports the legacy CLI resource sync work into the monorepo, including new pull/push wiring, GraphQL sync helpers, and an example consent workflow trigger config. diff --git a/packages/cli/README.md b/packages/cli/README.md index 233e7161..c7dd8e8c 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2399,7 +2399,7 @@ transcend consent delete-preference-records \ ```txt USAGE - transcend inventory pull (--auth value) [--resources all|apiKeys|customFields|templates|dataSilos|enrichers|dataFlows|businessEntities|processingActivities|actions|dataSubjects|identifiers|cookies|consentManager|partitions|prompts|promptPartials|promptGroups|agents|agentFunctions|agentFiles|vendors|dataCategories|processingPurposes|actionItems|actionItemCollections|teams|privacyCenters|policies|messages|assessments|assessmentTemplates|purposes|systemDiscovery] [--file value] [--transcendUrl value] [--dataSiloIds value]... [--integrationNames value]... [--trackerStatuses LIVE|NEEDS_REVIEW] [--pageSize value] [--skipDatapoints] [--skipSubDatapoints] [--includeGuessedCategories] [--debug] + transcend inventory pull (--auth value) [--resources all|apiKeys|customFields|templates|dataSilos|enrichers|dataFlows|businessEntities|processingActivities|actions|dataSubjects|identifiers|cookies|consentManager|partitions|prompts|promptPartials|promptGroups|agents|agentFunctions|agentFiles|vendors|dataCategories|processingPurposes|actionItems|actionItemCollections|teams|privacyCenters|policies|messages|assessments|assessmentTemplates|purposes|preferenceOptions|systemDiscovery|consentWorkflowTriggers] [--file value] [--transcendUrl value] [--dataSiloIds value]... [--integrationNames value]... [--trackerStatuses LIVE|NEEDS_REVIEW] [--pageSize value] [--skipDatapoints] [--skipSubDatapoints] [--includeGuessedCategories] [--debug] transcend inventory pull --help Generates a transcend.yml by pulling the configuration from your Transcend instance. @@ -2413,7 +2413,7 @@ This command can be helpful if you are looking to: FLAGS --auth The Transcend API key. The scopes required will vary depending on the operation performed. If in doubt, the Full Admin scope will always work. - [--resources] The different resource types to pull in. Defaults to dataSilos,enrichers,templates,apiKeys. [all|apiKeys|customFields|templates|dataSilos|enrichers|dataFlows|businessEntities|processingActivities|actions|dataSubjects|identifiers|cookies|consentManager|partitions|prompts|promptPartials|promptGroups|agents|agentFunctions|agentFiles|vendors|dataCategories|processingPurposes|actionItems|actionItemCollections|teams|privacyCenters|policies|messages|assessments|assessmentTemplates|purposes|systemDiscovery, separator = ,] + [--resources] The different resource types to pull in. Defaults to dataSilos,enrichers,templates,apiKeys. [all|apiKeys|customFields|templates|dataSilos|enrichers|dataFlows|businessEntities|processingActivities|actions|dataSubjects|identifiers|cookies|consentManager|partitions|prompts|promptPartials|promptGroups|agents|agentFunctions|agentFiles|vendors|dataCategories|processingPurposes|actionItems|actionItemCollections|teams|privacyCenters|policies|messages|assessments|assessmentTemplates|purposes|preferenceOptions|systemDiscovery|consentWorkflowTriggers, separator = ,] [--file] Path to the YAML file to pull into [default = ./transcend.yml] [--transcendUrl] URL of the Transcend backend. Use https://api.us.transcend.io for US hosting [default = https://api.transcend.io] [--dataSiloIds]... The UUIDs of the data silos that should be pulled into the YAML file [separator = ,] @@ -2431,41 +2431,43 @@ FLAGS The API key permissions for this command vary based on the `resources` argument: -| Resource | Key in `transcend.yml` | Description | Scopes | Link | -| ----------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `apiKeys` | `api-keys` | API Key definitions assigned to Data Systems (formerly "Data Silos"). API keys cannot be created through the CLI, but you can map API key usage to Data Systems. | View API Keys | [Developer Tools -> API keys](https://app.transcend.io/infrastructure/api-keys) | -| `customFields` | `attributes` | Custom Field definitions that define extra metadata for each table in the Admin Dashboard. | View Global Attributes | [Custom Fields](https://app.transcend.io/infrastructure/attributes) | -| `templates` | `templates` | Email templates. Only template titles can be created and mapped to other resources. | View Email Templates | [DSR Automation -> Email Settings -> Templates](https://app.transcend.io/privacy-requests/email-settings/templates) | -| `dataSilos` | `data-silos` | The Data System (formerly "Data Silo") definitions. | View Data Map, View Data Subject Request Settings | [Data Inventory -> Data Systems](https://app.transcend.io/data-map/data-inventory/data-silos)
[Infrastructure -> Integrations](https://app.transcend.io/infrastructure/integrations) | -| `enrichers` | `enrichers` | The Privacy Request enricher configurations. | View Identity Verification Settings | [DSR Automation -> Identifiers](https://app.transcend.io/privacy-requests/identifiers) | -| `dataFlows` | `data-flows` | Consent Manager Data Flow definitions. | View Data Flows | [Consent Management -> Data Flows](https://app.transcend.io/consent-manager/data-flows/approved) | -| `businessEntities` | `business-entities` | The business entities in the Data Inventory. | View Data Inventory | [Data Inventory -> Business Entities](https://app.transcend.io/data-map/data-inventory/business-entities) | -| `processingActivities` | `processing-activities` | The processing activities in the Data Inventory. | View Data Inventory | [Data Inventory -> Processing Activities](https://app.transcend.io/data-map/data-inventory/processing-activities) | -| `actions` | `actions` | The privacy request action settings. | View Data Subject Request Settings | [DSR Automation -> Request Settings -> Data Actions](https://app.transcend.io/privacy-requests/settings/data-actions) | -| `dataSubjects` | `data-subjects` | The privacy request data subject settings. | View Data Subject Request Settings | [DSR Automation -> Request Settings -> Data Subjects](https://app.transcend.io/privacy-requests/settings/data-subjects) | -| `identifiers` | `identifiers` | The privacy request identifier configurations. | View Identity Verification Settings | [DSR Automation -> Identifiers](https://app.transcend.io/privacy-requests/identifiers) | -| `cookies` | `cookies` | Consent Manager Cookie definitions. | View Data Flows | [Consent Management -> Cookies](https://app.transcend.io/consent-manager/cookies/approved) | -| `consentManager` | `consent-manager` | Consent Manager general settings, including domain list. | View Consent Manager | [Consent Management -> Developer Settings](https://app.transcend.io/consent-manager/developer-settings) | -| `partitions` | `partitions` | The partitions in the account (often representative of separate data controllers). | View Consent Manager | [Consent Management -> Developer Settings -> Advanced Settings](https://app.transcend.io/consent-manager/developer-settings/advanced-settings) | -| `prompts` | `prompts` | The Transcend AI prompts | View Prompts | [Prompt Manager -> Browse](https://app.transcend.io/prompts/browse) | -| `promptPartials` | `prompt-partials` | The Transcend AI prompt partials | View Prompts | [Prompt Manager -> Partials](https://app.transcend.io/prompts/partials) | -| `promptGroups` | `prompt-groups` | The Transcend AI prompt groups | View Prompts | [Prompt Manager -> Groups](https://app.transcend.io/prompts/groups) | -| `agents` | `agents` | The agents in Pathfinder. | View Pathfinder | [Pathfinder -> Agents](https://app.transcend.io/pathfinder/agents) | -| `agentFunctions` | `agent-functions` | The agent functions in Pathfinder. | View Pathfinder | [Pathfinder -> Agent Functions](https://app.transcend.io/pathfinder/agent-functions) | -| `agentFiles` | `agent-files` | The agent files in Pathfinder. | View Pathfinder | [Pathfinder -> Agent Files](https://app.transcend.io/pathfinder/agent-files) | -| `vendors` | `vendors` | The vendors in the Data Inventory. | View Data Inventory | [Data Inventory -> Vendors](https://app.transcend.io/data-map/data-inventory/vendors) | -| `dataCategories` | `data-categories` | The data categories in the Data Inventory. | View Data Inventory | [Data Inventory -> Data Categories](https://app.transcend.io/data-map/data-inventory/data-categories) | -| `processingPurposes` | `processing-purposes` | The processing purposes in the Data Inventory. | View Data Inventory | [Data Inventory -> Processing Purposes](https://app.transcend.io/data-map/data-inventory/purposes) | -| `actionItems` | `action-items` | Onboarding-related action items | View All Action Items | [Action Items](https://app.transcend.io/action-items/all) | -| `actionItemCollections` | `action-item-collections` | Onboarding-related action item group names | View All Action Items | [Action Items](https://app.transcend.io/action-items/all) | -| `teams` | `teams` | Team definitions of users and scope groupings | View Scopes | [Administration -> Teams](https://app.transcend.io/admin/teams) | -| `privacyCenters` | `privacy-center` | The Privacy Center settings. | View Privacy Center Layout | [Privacy Center](https://app.transcend.io/privacy-center/general-settings) | -| `policies` | `policies` | The Privacy Center policies. | View Policies | [Privacy Center -> Policies](https://app.transcend.io/privacy-center/policies) | -| `messages` | `messages` | Message definitions used across Consent Management, the Privacy Center, email templates and more. | View Internationalization Messages | [Privacy Center -> Messages & Internationalization](https://app.transcend.io/privacy-center/messages-internationalization)
[Consent Management -> Display Settings -> Messages](https://app.transcend.io/consent-manager/display-settings/messages) | -| `assessments` | `assessments` | Assessment responses. | View Assessments | [Assessments -> Assessments](https://app.transcend.io/assessments/groups) | -| `assessmentTemplates` | `assessment-templates` | Assessment template configurations. | View Assessments | [Assessment -> Templates](https://app.transcend.io/assessments/form-templates) | -| `purposes` | `purposes` | Consent purposes and related preference management topics. | View Consent Manager, View Preference Store Settings | [Consent Management -> Regional Experiences -> Purposes](https://app.transcend.io/consent-manager/regional-experiences/purposes) | -| `systemDiscovery` | `system-discovery` | System discovery results | View Data Map | [System Discovery](https://app.transcend.io/data-map/data-inventory/silo-discovery) | +| Resource | Key in `transcend.yml` | Description | Scopes | Link | +| ------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `apiKeys` | `api-keys` | API Key definitions assigned to Data Systems (formerly "Data Silos"). API keys cannot be created through the CLI, but you can map API key usage to Data Systems. | View API Keys | [Developer Tools -> API keys](https://app.transcend.io/infrastructure/api-keys) | +| `customFields` | `attributes` | Custom Field definitions that define extra metadata for each table in the Admin Dashboard. | View Global Attributes | [Custom Fields](https://app.transcend.io/infrastructure/attributes) | +| `templates` | `templates` | Email templates. Only template titles can be created and mapped to other resources. | View Email Templates | [DSR Automation -> Email Settings -> Templates](https://app.transcend.io/privacy-requests/email-settings/templates) | +| `dataSilos` | `data-silos` | The Data System (formerly "Data Silo") definitions. | View Data Map, View Data Subject Request Settings | [Data Inventory -> Data Systems](https://app.transcend.io/data-map/data-inventory/data-silos)
[Infrastructure -> Integrations](https://app.transcend.io/infrastructure/integrations) | +| `enrichers` | `enrichers` | The Privacy Request enricher configurations. | View Identity Verification Settings | [DSR Automation -> Identifiers](https://app.transcend.io/privacy-requests/identifiers) | +| `dataFlows` | `data-flows` | Consent Manager Data Flow definitions. | View Data Flows | [Consent Management -> Data Flows](https://app.transcend.io/consent-manager/data-flows/approved) | +| `businessEntities` | `business-entities` | The business entities in the Data Inventory. | View Data Inventory | [Data Inventory -> Business Entities](https://app.transcend.io/data-map/data-inventory/business-entities) | +| `processingActivities` | `processing-activities` | The processing activities in the Data Inventory. | View Data Inventory | [Data Inventory -> Processing Activities](https://app.transcend.io/data-map/data-inventory/processing-activities) | +| `actions` | `actions` | The privacy request action settings. | View Data Subject Request Settings | [DSR Automation -> Request Settings -> Data Actions](https://app.transcend.io/privacy-requests/settings/data-actions) | +| `dataSubjects` | `data-subjects` | The privacy request data subject settings. | View Data Subject Request Settings | [DSR Automation -> Request Settings -> Data Subjects](https://app.transcend.io/privacy-requests/settings/data-subjects) | +| `identifiers` | `identifiers` | The privacy request identifier configurations. | View Identity Verification Settings | [DSR Automation -> Identifiers](https://app.transcend.io/privacy-requests/identifiers) | +| `cookies` | `cookies` | Consent Manager Cookie definitions. | View Data Flows | [Consent Management -> Cookies](https://app.transcend.io/consent-manager/cookies/approved) | +| `consentManager` | `consent-manager` | Consent Manager general settings, including domain list. | View Consent Manager | [Consent Management -> Developer Settings](https://app.transcend.io/consent-manager/developer-settings) | +| `partitions` | `partitions` | The partitions in the account (often representative of separate data controllers). | View Consent Manager | [Consent Management -> Developer Settings -> Advanced Settings](https://app.transcend.io/consent-manager/developer-settings/advanced-settings) | +| `prompts` | `prompts` | The Transcend AI prompts | View Prompts | [Prompt Manager -> Browse](https://app.transcend.io/prompts/browse) | +| `promptPartials` | `prompt-partials` | The Transcend AI prompt partials | View Prompts | [Prompt Manager -> Partials](https://app.transcend.io/prompts/partials) | +| `promptGroups` | `prompt-groups` | The Transcend AI prompt groups | View Prompts | [Prompt Manager -> Groups](https://app.transcend.io/prompts/groups) | +| `agents` | `agents` | The agents in Pathfinder. | View Pathfinder | [Pathfinder -> Agents](https://app.transcend.io/pathfinder/agents) | +| `agentFunctions` | `agent-functions` | The agent functions in Pathfinder. | View Pathfinder | [Pathfinder -> Agent Functions](https://app.transcend.io/pathfinder/agent-functions) | +| `agentFiles` | `agent-files` | The agent files in Pathfinder. | View Pathfinder | [Pathfinder -> Agent Files](https://app.transcend.io/pathfinder/agent-files) | +| `vendors` | `vendors` | The vendors in the Data Inventory. | View Data Inventory | [Data Inventory -> Vendors](https://app.transcend.io/data-map/data-inventory/vendors) | +| `dataCategories` | `data-categories` | The data categories in the Data Inventory. | View Data Inventory | [Data Inventory -> Data Categories](https://app.transcend.io/data-map/data-inventory/data-categories) | +| `processingPurposes` | `processing-purposes` | The processing purposes in the Data Inventory. | View Data Inventory | [Data Inventory -> Processing Purposes](https://app.transcend.io/data-map/data-inventory/purposes) | +| `actionItems` | `action-items` | Onboarding-related action items | View All Action Items | [Action Items](https://app.transcend.io/action-items/all) | +| `actionItemCollections` | `action-item-collections` | Onboarding-related action item group names | View All Action Items | [Action Items](https://app.transcend.io/action-items/all) | +| `teams` | `teams` | Team definitions of users and scope groupings | View Scopes | [Administration -> Teams](https://app.transcend.io/admin/teams) | +| `privacyCenters` | `privacy-center` | The Privacy Center settings. | View Privacy Center Layout | [Privacy Center](https://app.transcend.io/privacy-center/general-settings) | +| `policies` | `policies` | The Privacy Center policies. | View Policies | [Privacy Center -> Policies](https://app.transcend.io/privacy-center/policies) | +| `messages` | `messages` | Message definitions used across Consent Management, the Privacy Center, email templates and more. | View Internationalization Messages | [Privacy Center -> Messages & Internationalization](https://app.transcend.io/privacy-center/messages-internationalization)
[Consent Management -> Display Settings -> Messages](https://app.transcend.io/consent-manager/display-settings/messages) | +| `assessments` | `assessments` | Assessment responses. | View Assessments | [Assessments -> Assessments](https://app.transcend.io/assessments/groups) | +| `assessmentTemplates` | `assessment-templates` | Assessment template configurations. | View Assessments | [Assessment -> Templates](https://app.transcend.io/assessments/form-templates) | +| `purposes` | `purposes` | Consent purposes and related preference management topics. | View Consent Manager, View Preference Store Settings | [Consent Management -> Regional Experiences -> Purposes](https://app.transcend.io/consent-manager/regional-experiences/purposes) | +| `preferenceOptions` | `preference-options` | Preference management options for multi and single select preference topics. | View Preference Store Settings | [Preference Management -> Preference Topics -> Options](https://app.transcend.io/preference-store/preference-topics/preference-options) | +| `systemDiscovery` | `system-discovery` | System discovery results | View Data Map | [System Discovery](https://app.transcend.io/data-map/data-inventory/silo-discovery) | +| `consentWorkflowTriggers` | `consent-workflow-triggers` | Consent workflow trigger definitions that automate privacy request workflows based on consent state changes. | View Consent Manager | [Consent Management -> Consent Workflows](https://app.transcend.io/consent-manager/consent-workflows) | #### Examples @@ -2643,41 +2645,43 @@ FLAGS The API key permissions for this command vary based on the resources declared as top-level keys in your [`transcend.yml`](#transcendyml) file: -| Resource | Key in `transcend.yml` | Description | Scopes | Link | -| ----------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `apiKeys` | `api-keys` | API Key definitions assigned to Data Systems (formerly "Data Silos"). API keys cannot be created through the CLI, but you can map API key usage to Data Systems. | View API Keys | [Developer Tools -> API keys](https://app.transcend.io/infrastructure/api-keys) | -| `customFields` | `attributes` | Custom Field definitions that define extra metadata for each table in the Admin Dashboard. | Manage Global Attributes | [Custom Fields](https://app.transcend.io/infrastructure/attributes) | -| `templates` | `templates` | Email templates. Only template titles can be created and mapped to other resources. | Manage Email Templates | [DSR Automation -> Email Settings -> Templates](https://app.transcend.io/privacy-requests/email-settings/templates) | -| `dataSilos` | `data-silos` | The Data System (formerly "Data Silo") definitions. | Manage Data Map, Connect Data Silos | [Data Inventory -> Data Systems](https://app.transcend.io/data-map/data-inventory/data-silos)
[Infrastructure -> Integrations](https://app.transcend.io/infrastructure/integrations) | -| `enrichers` | `enrichers` | The Privacy Request enricher configurations. | Manage Request Identity Verification | [DSR Automation -> Identifiers](https://app.transcend.io/privacy-requests/identifiers) | -| `dataFlows` | `data-flows` | Consent Manager Data Flow definitions. | Manage Data Flows | [Consent Management -> Data Flows](https://app.transcend.io/consent-manager/data-flows/approved) | -| `businessEntities` | `business-entities` | The business entities in the Data Inventory. | Manage Data Inventory | [Data Inventory -> Business Entities](https://app.transcend.io/data-map/data-inventory/business-entities) | -| `processingActivities` | `processing-activities` | The processing activities in the Data Inventory. | Manage Data Map | [Data Inventory -> Processing Activities](https://app.transcend.io/data-map/data-inventory/processing-activities) | -| `actions` | `actions` | The privacy request action settings. | Manage Data Subject Request Settings | [DSR Automation -> Request Settings -> Data Actions](https://app.transcend.io/privacy-requests/settings/data-actions) | -| `dataSubjects` | `data-subjects` | The privacy request data subject settings. | Manage Data Subject Request Settings | [DSR Automation -> Request Settings -> Data Subjects](https://app.transcend.io/privacy-requests/settings/data-subjects) | -| `identifiers` | `identifiers` | The privacy request identifier configurations. | Manage Request Identity Verification | [DSR Automation -> Identifiers](https://app.transcend.io/privacy-requests/identifiers) | -| `cookies` | `cookies` | Consent Manager Cookie definitions. | Manage Data Flows | [Consent Management -> Cookies](https://app.transcend.io/consent-manager/cookies/approved) | -| `consentManager` | `consent-manager` | Consent Manager general settings, including domain list. | Manage Consent Manager Developer Settings | [Consent Management -> Developer Settings](https://app.transcend.io/consent-manager/developer-settings) | -| `partitions` | `partitions` | The partitions in the account (often representative of separate data controllers). | Manage Consent Manager Developer Settings | [Consent Management -> Developer Settings -> Advanced Settings](https://app.transcend.io/consent-manager/developer-settings/advanced-settings) | -| `prompts` | `prompts` | The Transcend AI prompts | Manage Prompts | [Prompt Manager -> Browse](https://app.transcend.io/prompts/browse) | -| `promptPartials` | `prompt-partials` | The Transcend AI prompt partials | Manage Prompts | [Prompt Manager -> Partials](https://app.transcend.io/prompts/partials) | -| `promptGroups` | `prompt-groups` | The Transcend AI prompt groups | Manage Prompts | [Prompt Manager -> Groups](https://app.transcend.io/prompts/groups) | -| `agents` | `agents` | The agents in Pathfinder. | Manage Pathfinder | [Pathfinder -> Agents](https://app.transcend.io/pathfinder/agents) | -| `agentFunctions` | `agent-functions` | The agent functions in Pathfinder. | Manage Pathfinder | [Pathfinder -> Agent Functions](https://app.transcend.io/pathfinder/agent-functions) | -| `agentFiles` | `agent-files` | The agent files in Pathfinder. | Manage Pathfinder | [Pathfinder -> Agent Files](https://app.transcend.io/pathfinder/agent-files) | -| `vendors` | `vendors` | The vendors in the Data Inventory. | Manage Data Inventory | [Data Inventory -> Vendors](https://app.transcend.io/data-map/data-inventory/vendors) | -| `dataCategories` | `data-categories` | The data categories in the Data Inventory. | Manage Data Inventory | [Data Inventory -> Data Categories](https://app.transcend.io/data-map/data-inventory/data-categories) | -| `processingPurposes` | `processing-purposes` | The processing purposes in the Data Inventory. | Manage Data Inventory | [Data Inventory -> Processing Purposes](https://app.transcend.io/data-map/data-inventory/purposes) | -| `actionItems` | `action-items` | Onboarding-related action items | Manage All Action Items, View Global Attributes | [Action Items](https://app.transcend.io/action-items/all) | -| `actionItemCollections` | `action-item-collections` | Onboarding-related action item group names | Manage Action Item Collections | [Action Items](https://app.transcend.io/action-items/all) | -| `teams` | `teams` | Team definitions of users and scope groupings | Manage Access Controls | [Administration -> Teams](https://app.transcend.io/admin/teams) | -| `privacyCenters` | `privacy-center` | The Privacy Center settings. | Manage Privacy Center Layout | [Privacy Center](https://app.transcend.io/privacy-center/general-settings) | -| `policies` | `policies` | The Privacy Center policies. | Manage Policies | [Privacy Center -> Policies](https://app.transcend.io/privacy-center/policies) | -| `messages` | `messages` | Message definitions used across Consent Management, the Privacy Center, email templates and more. | Manage Internationalization Messages | [Privacy Center -> Messages & Internationalization](https://app.transcend.io/privacy-center/messages-internationalization)
[Consent Management -> Display Settings -> Messages](https://app.transcend.io/consent-manager/display-settings/messages) | -| `assessments` | `assessments` | Assessment responses. | Manage Assessments | [Assessments -> Assessments](https://app.transcend.io/assessments/groups) | -| `assessmentTemplates` | `assessment-templates` | Assessment template configurations. | Manage Assessments | [Assessment -> Templates](https://app.transcend.io/assessments/form-templates) | -| `purposes` | `purposes` | Consent purposes and related preference management topics. | Manage Consent Manager, Manage Preference Store Settings | [Consent Management -> Regional Experiences -> Purposes](https://app.transcend.io/consent-manager/regional-experiences/purposes) | -| `systemDiscovery` | `system-discovery` | System discovery results | Manage Data Map | [System Discovery](https://app.transcend.io/data-map/data-inventory/silo-discovery) | +| Resource | Key in `transcend.yml` | Description | Scopes | Link | +| ------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `apiKeys` | `api-keys` | API Key definitions assigned to Data Systems (formerly "Data Silos"). API keys cannot be created through the CLI, but you can map API key usage to Data Systems. | View API Keys | [Developer Tools -> API keys](https://app.transcend.io/infrastructure/api-keys) | +| `customFields` | `attributes` | Custom Field definitions that define extra metadata for each table in the Admin Dashboard. | Manage Global Attributes | [Custom Fields](https://app.transcend.io/infrastructure/attributes) | +| `templates` | `templates` | Email templates. Only template titles can be created and mapped to other resources. | Manage Email Templates | [DSR Automation -> Email Settings -> Templates](https://app.transcend.io/privacy-requests/email-settings/templates) | +| `dataSilos` | `data-silos` | The Data System (formerly "Data Silo") definitions. | Manage Data Map, Connect Data Silos | [Data Inventory -> Data Systems](https://app.transcend.io/data-map/data-inventory/data-silos)
[Infrastructure -> Integrations](https://app.transcend.io/infrastructure/integrations) | +| `enrichers` | `enrichers` | The Privacy Request enricher configurations. | Manage Request Identity Verification | [DSR Automation -> Identifiers](https://app.transcend.io/privacy-requests/identifiers) | +| `dataFlows` | `data-flows` | Consent Manager Data Flow definitions. | Manage Data Flows | [Consent Management -> Data Flows](https://app.transcend.io/consent-manager/data-flows/approved) | +| `businessEntities` | `business-entities` | The business entities in the Data Inventory. | Manage Data Inventory | [Data Inventory -> Business Entities](https://app.transcend.io/data-map/data-inventory/business-entities) | +| `processingActivities` | `processing-activities` | The processing activities in the Data Inventory. | Manage Data Map | [Data Inventory -> Processing Activities](https://app.transcend.io/data-map/data-inventory/processing-activities) | +| `actions` | `actions` | The privacy request action settings. | Manage Data Subject Request Settings | [DSR Automation -> Request Settings -> Data Actions](https://app.transcend.io/privacy-requests/settings/data-actions) | +| `dataSubjects` | `data-subjects` | The privacy request data subject settings. | Manage Data Subject Request Settings | [DSR Automation -> Request Settings -> Data Subjects](https://app.transcend.io/privacy-requests/settings/data-subjects) | +| `identifiers` | `identifiers` | The privacy request identifier configurations. | Manage Request Identity Verification | [DSR Automation -> Identifiers](https://app.transcend.io/privacy-requests/identifiers) | +| `cookies` | `cookies` | Consent Manager Cookie definitions. | Manage Data Flows | [Consent Management -> Cookies](https://app.transcend.io/consent-manager/cookies/approved) | +| `consentManager` | `consent-manager` | Consent Manager general settings, including domain list. | Manage Consent Manager Developer Settings | [Consent Management -> Developer Settings](https://app.transcend.io/consent-manager/developer-settings) | +| `partitions` | `partitions` | The partitions in the account (often representative of separate data controllers). | Manage Consent Manager Developer Settings | [Consent Management -> Developer Settings -> Advanced Settings](https://app.transcend.io/consent-manager/developer-settings/advanced-settings) | +| `prompts` | `prompts` | The Transcend AI prompts | Manage Prompts | [Prompt Manager -> Browse](https://app.transcend.io/prompts/browse) | +| `promptPartials` | `prompt-partials` | The Transcend AI prompt partials | Manage Prompts | [Prompt Manager -> Partials](https://app.transcend.io/prompts/partials) | +| `promptGroups` | `prompt-groups` | The Transcend AI prompt groups | Manage Prompts | [Prompt Manager -> Groups](https://app.transcend.io/prompts/groups) | +| `agents` | `agents` | The agents in Pathfinder. | Manage Pathfinder | [Pathfinder -> Agents](https://app.transcend.io/pathfinder/agents) | +| `agentFunctions` | `agent-functions` | The agent functions in Pathfinder. | Manage Pathfinder | [Pathfinder -> Agent Functions](https://app.transcend.io/pathfinder/agent-functions) | +| `agentFiles` | `agent-files` | The agent files in Pathfinder. | Manage Pathfinder | [Pathfinder -> Agent Files](https://app.transcend.io/pathfinder/agent-files) | +| `vendors` | `vendors` | The vendors in the Data Inventory. | Manage Data Inventory | [Data Inventory -> Vendors](https://app.transcend.io/data-map/data-inventory/vendors) | +| `dataCategories` | `data-categories` | The data categories in the Data Inventory. | Manage Data Inventory | [Data Inventory -> Data Categories](https://app.transcend.io/data-map/data-inventory/data-categories) | +| `processingPurposes` | `processing-purposes` | The processing purposes in the Data Inventory. | Manage Data Inventory | [Data Inventory -> Processing Purposes](https://app.transcend.io/data-map/data-inventory/purposes) | +| `actionItems` | `action-items` | Onboarding-related action items | Manage All Action Items, View Global Attributes | [Action Items](https://app.transcend.io/action-items/all) | +| `actionItemCollections` | `action-item-collections` | Onboarding-related action item group names | Manage Action Item Collections | [Action Items](https://app.transcend.io/action-items/all) | +| `teams` | `teams` | Team definitions of users and scope groupings | Manage Access Controls | [Administration -> Teams](https://app.transcend.io/admin/teams) | +| `privacyCenters` | `privacy-center` | The Privacy Center settings. | Manage Privacy Center Layout | [Privacy Center](https://app.transcend.io/privacy-center/general-settings) | +| `policies` | `policies` | The Privacy Center policies. | Manage Policies | [Privacy Center -> Policies](https://app.transcend.io/privacy-center/policies) | +| `messages` | `messages` | Message definitions used across Consent Management, the Privacy Center, email templates and more. | Manage Internationalization Messages | [Privacy Center -> Messages & Internationalization](https://app.transcend.io/privacy-center/messages-internationalization)
[Consent Management -> Display Settings -> Messages](https://app.transcend.io/consent-manager/display-settings/messages) | +| `assessments` | `assessments` | Assessment responses. | Manage Assessments | [Assessments -> Assessments](https://app.transcend.io/assessments/groups) | +| `assessmentTemplates` | `assessment-templates` | Assessment template configurations. | Manage Assessments | [Assessment -> Templates](https://app.transcend.io/assessments/form-templates) | +| `purposes` | `purposes` | Consent purposes and related preference management topics. | Manage Consent Manager, Manage Preference Store Settings | [Consent Management -> Regional Experiences -> Purposes](https://app.transcend.io/consent-manager/regional-experiences/purposes) | +| `preferenceOptions` | `preference-options` | Preference management options for multi and single select preference topics. | Manage Preference Store Settings | [Preference Management -> Preference Topics -> Options](https://app.transcend.io/preference-store/preference-topics/preference-options) | +| `systemDiscovery` | `system-discovery` | System discovery results | Manage Data Map | [System Discovery](https://app.transcend.io/data-map/data-inventory/silo-discovery) | +| `consentWorkflowTriggers` | `consent-workflow-triggers` | Consent workflow trigger definitions that automate privacy request workflows based on consent state changes. | Manage Consent Manager, View Data Subject Request Settings, View Consent Manager | [Consent Management -> Consent Workflows](https://app.transcend.io/consent-manager/consent-workflows) | #### Examples diff --git a/packages/cli/examples/consent-workflow-triggers.yml b/packages/cli/examples/consent-workflow-triggers.yml new file mode 100644 index 00000000..30414f82 --- /dev/null +++ b/packages/cli/examples/consent-workflow-triggers.yml @@ -0,0 +1,19 @@ +consent-workflow-triggers: + - name: Erasure on opt-out + action-type: ERASURE + data-subject-type: Customer + is-silent: true + allow-unauthenticated: false + is-active: true + purposes: + - tracking-type: Advertising + matching-state: false + - name: Access on request + action-type: ACCESS + data-subject-type: Customer + is-silent: false + allow-unauthenticated: false + is-active: true + purposes: + - tracking-type: Analytics + matching-state: true diff --git a/packages/cli/schema/transcend-yml-schema-latest.json b/packages/cli/schema/transcend-yml-schema-latest.json index 00fd31d3..f035caf4 100644 --- a/packages/cli/schema/transcend-yml-schema-latest.json +++ b/packages/cli/schema/transcend-yml-schema-latest.json @@ -59585,6 +59585,81 @@ } ] } + }, + "consent-workflow-triggers": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + } + } + }, + { + "type": "object", + "properties": { + "trigger-condition": { + "type": "string" + }, + "action-type": { + "type": "string" + }, + "data-subject-type": { + "type": "string" + }, + "is-silent": { + "type": "boolean" + }, + "allow-unauthenticated": { + "type": "boolean" + }, + "is-active": { + "type": "boolean" + }, + "data-silo-titles": { + "type": "array", + "items": { + "type": "string" + } + }, + "purposes": { + "type": "array", + "items": { + "type": "object", + "required": ["tracking-type", "matching-state"], + "properties": { + "tracking-type": { + "type": "string" + }, + "matching-state": { + "type": "boolean" + } + } + } + } + } + } + ] + } + }, + "preference-options": { + "type": "array", + "items": { + "type": "object", + "required": ["title", "slug"], + "properties": { + "title": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + } } } } diff --git a/packages/cli/schema/transcend-yml-schema-v9.json b/packages/cli/schema/transcend-yml-schema-v9.json index aa164439..e12ee922 100644 --- a/packages/cli/schema/transcend-yml-schema-v9.json +++ b/packages/cli/schema/transcend-yml-schema-v9.json @@ -59585,6 +59585,81 @@ } ] } + }, + "consent-workflow-triggers": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + } + } + }, + { + "type": "object", + "properties": { + "trigger-condition": { + "type": "string" + }, + "action-type": { + "type": "string" + }, + "data-subject-type": { + "type": "string" + }, + "is-silent": { + "type": "boolean" + }, + "allow-unauthenticated": { + "type": "boolean" + }, + "is-active": { + "type": "boolean" + }, + "data-silo-titles": { + "type": "array", + "items": { + "type": "string" + } + }, + "purposes": { + "type": "array", + "items": { + "type": "object", + "required": ["tracking-type", "matching-state"], + "properties": { + "tracking-type": { + "type": "string" + }, + "matching-state": { + "type": "boolean" + } + } + } + } + } + } + ] + } + }, + "preference-options": { + "type": "array", + "items": { + "type": "object", + "required": ["title", "slug"], + "properties": { + "title": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + } } } } diff --git a/packages/cli/src/codecs.ts b/packages/cli/src/codecs.ts index d268ddef..a4e355ba 100644 --- a/packages/cli/src/codecs.ts +++ b/packages/cli/src/codecs.ts @@ -1932,6 +1932,52 @@ export const SiloDiscoveryResultInput = t.intersection([ /** Type override */ export type SiloDiscoveryResultInput = t.TypeOf; +/** + * Input for a purpose associated with a consent workflow trigger + */ +export const ConsentWorkflowTriggerPurposeInput = t.type({ + /** The tracking type slug of the purpose */ + 'tracking-type': t.string, + /** The matching consent state for the purpose */ + 'matching-state': t.boolean, +}); + +/** Type override */ +export type ConsentWorkflowTriggerPurposeInput = t.TypeOf< + typeof ConsentWorkflowTriggerPurposeInput +>; + +/** + * Input to define a consent workflow trigger + */ +export const ConsentWorkflowTriggerInput = t.intersection([ + t.type({ + /** The name of the consent workflow trigger */ + name: t.string, + }), + t.partial({ + /** The trigger condition as a JSON string */ + 'trigger-condition': t.string, + /** The action type (e.g. ERASURE, ACCESS) */ + 'action-type': t.string, + /** The data subject type */ + 'data-subject-type': t.string, + /** Whether the trigger runs silently */ + 'is-silent': t.boolean, + /** Whether unauthenticated requests are allowed */ + 'allow-unauthenticated': t.boolean, + /** Whether the trigger is active */ + 'is-active': t.boolean, + /** Titles of data silos associated with this trigger */ + 'data-silo-titles': t.array(t.string), + /** Purposes and their matching consent states */ + purposes: t.array(ConsentWorkflowTriggerPurposeInput), + }), +]); + +/** Type override */ +export type ConsentWorkflowTriggerInput = t.TypeOf; + export const TranscendInput = t.partial({ /** * Action items @@ -2061,6 +2107,14 @@ export const TranscendInput = t.partial({ * The full list of silo discovery results */ 'system-discovery': t.array(SiloDiscoveryResultInput), + /** + * Consent workflow trigger definitions + */ + 'consent-workflow-triggers': t.array(ConsentWorkflowTriggerInput), + /** + * Preference management options for multi and single selects + */ + 'preference-options': t.array(ConsentPreferenceTopicOptionValue), }); /** Type override */ diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 376f176f..9ecd3389 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -72,7 +72,13 @@ export const TR_PUSH_RESOURCE_SCOPE_MAP: { ScopeName.ManageConsentManager, ScopeName.ManagePreferenceStoreSettings, ], + [TranscendPullResource.PreferenceOptions]: [ScopeName.ManagePreferenceStoreSettings], [TranscendPullResource.SystemDiscovery]: [ScopeName.ManageDataMap], + [TranscendPullResource.ConsentWorkflowTriggers]: [ + ScopeName.ManageConsentManager, + ScopeName.ViewDataSubjectRequestSettings, + ScopeName.ViewConsentManager, + ], }; /** @@ -119,7 +125,9 @@ export const TR_PULL_RESOURCE_SCOPE_MAP: { ScopeName.ViewConsentManager, ScopeName.ViewPreferenceStoreSettings, ], + [TranscendPullResource.PreferenceOptions]: [ScopeName.ViewPreferenceStoreSettings], [TranscendPullResource.SystemDiscovery]: [ScopeName.ViewDataMap], + [TranscendPullResource.ConsentWorkflowTriggers]: [ScopeName.ViewConsentManager], }; export const TR_YML_RESOURCE_TO_FIELD_NAME: Record = { @@ -155,7 +163,9 @@ export const TR_YML_RESOURCE_TO_FIELD_NAME: Record Preference Topics -> Options]\ +(https://app.transcend.io/preference-store/preference-topics/preference-options)', + ], + }, [TranscendPullResource.SystemDiscovery]: { description: 'System discovery results', markdownLinks: [ @@ -209,6 +216,14 @@ const RESOURCE_DOCUMENTATION: Record< (https://app.transcend.io/data-map/data-inventory/silo-discovery)', ], }, + [TranscendPullResource.ConsentWorkflowTriggers]: { + description: + 'Consent workflow trigger definitions that automate privacy request workflows based on consent state changes.', + markdownLinks: [ + '[Consent Management -> Consent Workflows]\ +(https://app.transcend.io/consent-manager/consent-workflows)', + ], + }, }; /** diff --git a/packages/cli/src/lib/graphql/fetchAllConsentWorkflowTriggers.ts b/packages/cli/src/lib/graphql/fetchAllConsentWorkflowTriggers.ts new file mode 100644 index 00000000..a63dcccf --- /dev/null +++ b/packages/cli/src/lib/graphql/fetchAllConsentWorkflowTriggers.ts @@ -0,0 +1,72 @@ +import { GraphQLClient } from 'graphql-request'; + +import { CONSENT_WORKFLOW_TRIGGERS } from './gqls/index.js'; +import { makeGraphQLRequest } from './makeGraphQLRequest.js'; + +export interface ConsentWorkflowTrigger { + /** ID of the trigger */ + id: string; + /** Name of the trigger */ + name: string; + /** JSON string of the trigger condition */ + triggerCondition: string | null; + /** Whether the trigger runs silently */ + isSilent: boolean; + /** Whether unauthenticated requests are allowed */ + allowUnauthenticated: boolean; + /** Whether the trigger is active */ + isActive: boolean; + /** The workflow config ID */ + workflowConfigId: string | null; + /** The request action associated with the trigger */ + action: { + /** Action type (e.g. ERASURE, ACCESS) */ + type: string; + }; + /** The data subject associated with the trigger */ + subject: { + /** Data subject type */ + type: string; + }; + /** Data silos associated with this trigger */ + dataSilos: { + /** Title of data silo */ + title: string; + }[]; +} + +const PAGE_SIZE = 20; + +/** + * Fetch all consent workflow triggers in the organization + * + * @param client - GraphQL client + * @returns All consent workflow triggers in the organization + */ +export async function fetchAllConsentWorkflowTriggers( + client: GraphQLClient, +): Promise { + const triggers: ConsentWorkflowTrigger[] = []; + let offset = 0; + + let shouldContinue = false; + do { + const { + consentWorkflowTriggers: { nodes }, + } = await makeGraphQLRequest<{ + /** Consent workflow triggers */ + consentWorkflowTriggers: { + /** List */ + nodes: ConsentWorkflowTrigger[]; + }; + }>(client, CONSENT_WORKFLOW_TRIGGERS, { + first: PAGE_SIZE, + offset, + }); + triggers.push(...nodes); + offset += PAGE_SIZE; + shouldContinue = nodes.length === PAGE_SIZE; + } while (shouldContinue); + + return triggers.sort((a, b) => a.name.localeCompare(b.name)); +} diff --git a/packages/cli/src/lib/graphql/fetchAllPreferenceOptionValues.ts b/packages/cli/src/lib/graphql/fetchAllPreferenceOptionValues.ts new file mode 100644 index 00000000..5c6f8ce9 --- /dev/null +++ b/packages/cli/src/lib/graphql/fetchAllPreferenceOptionValues.ts @@ -0,0 +1,54 @@ +import { GraphQLClient } from 'graphql-request'; + +import { PREFERENCE_OPTION_VALUES } from './gqls/index.js'; +import { makeGraphQLRequest } from './makeGraphQLRequest.js'; + +export interface PreferenceOptionValue { + /** ID of preference option value */ + id: string; + /** Slug of preference option value */ + slug: string; + /** Title of preference option value */ + title: { + /** ID */ + id: string; + /** Default message */ + defaultMessage: string; + }; +} + +const PAGE_SIZE = 20; + +/** + * Fetch all preference option values in the organization + * + * @param client - GraphQL client + * @returns All preference option values in the organization + */ +export async function fetchAllPreferenceOptionValues( + client: GraphQLClient, +): Promise { + const preferenceOptionValues: PreferenceOptionValue[] = []; + let offset = 0; + + let shouldContinue = false; + do { + const { + preferenceOptionValues: { nodes }, + } = await makeGraphQLRequest<{ + /** Preference option values */ + preferenceOptionValues: { + /** List */ + nodes: PreferenceOptionValue[]; + }; + }>(client, PREFERENCE_OPTION_VALUES, { + first: PAGE_SIZE, + offset, + }); + preferenceOptionValues.push(...nodes); + offset += PAGE_SIZE; + shouldContinue = nodes.length === PAGE_SIZE; + } while (shouldContinue); + + return preferenceOptionValues.sort((a, b) => a.slug.localeCompare(b.slug)); +} diff --git a/packages/cli/src/lib/graphql/gqls/consentWorkflowTrigger.ts b/packages/cli/src/lib/graphql/gqls/consentWorkflowTrigger.ts new file mode 100644 index 00000000..6fbe047e --- /dev/null +++ b/packages/cli/src/lib/graphql/gqls/consentWorkflowTrigger.ts @@ -0,0 +1,48 @@ +import { gql } from 'graphql-request'; + +export const CONSENT_WORKFLOW_TRIGGERS = gql` + query TranscendCliConsentWorkflowTriggers($first: Int!, $offset: Int!) { + consentWorkflowTriggers(first: $first, offset: $offset) { + nodes { + id + name + triggerCondition + isSilent + allowUnauthenticated + isActive + workflowConfigId + action { + type + } + subject { + type + } + dataSilos { + title + } + } + totalCount + } + } +`; + +export const CREATE_OR_UPDATE_CONSENT_WORKFLOW_TRIGGER = gql` + mutation TranscendCliCreateOrUpdateConsentWorkflowTrigger( + $input: CreateOrUpdateConsentWorkflowTriggerInput! + ) { + createOrUpdateConsentWorkflowTrigger(input: $input) { + consentWorkflowTrigger { + id + name + } + } + } +`; + +export const DELETE_CONSENT_WORKFLOW_TRIGGERS = gql` + mutation TranscendCliDeleteConsentWorkflowTriggers($ids: [ID!]!) { + deleteConsentWorkflowTriggers(ids: $ids) { + clientMutationId + } + } +`; diff --git a/packages/cli/src/lib/graphql/gqls/index.ts b/packages/cli/src/lib/graphql/gqls/index.ts index 44a12c34..4130783f 100644 --- a/packages/cli/src/lib/graphql/gqls/index.ts +++ b/packages/cli/src/lib/graphql/gqls/index.ts @@ -50,3 +50,4 @@ export * from './processingPurpose.js'; export * from './processingActivity.js'; export * from './sombraVersion.js'; export * from './siloDiscoveryResult.js'; +export * from './consentWorkflowTrigger.js'; diff --git a/packages/cli/src/lib/graphql/gqls/preferenceTopic.ts b/packages/cli/src/lib/graphql/gqls/preferenceTopic.ts index 6aa0fcc2..02b5eb26 100644 --- a/packages/cli/src/lib/graphql/gqls/preferenceTopic.ts +++ b/packages/cli/src/lib/graphql/gqls/preferenceTopic.ts @@ -42,3 +42,42 @@ export const PREFERENCE_TOPICS = gql` } } `; + +export const CREATE_OR_UPDATE_PREFERENCE_OPTION_VALUES = gql` + mutation TranscendCliCreateOrUpdatePreferenceOptionValues( + $input: CreateOrUpdatePreferenceOptionValuesInput! + ) { + createOrUpdatePreferenceOptionValues(input: $input) { + preferenceOptionValues { + id + slug + } + } + } +`; + +export const PREFERENCE_OPTION_VALUES = gql` + query TranscendCliPreferenceOptionValues($first: Int!, $offset: Int!) { + preferenceOptionValues(first: $first, offset: $offset) { + nodes { + id + title { + id + defaultMessage + } + slug + } + } + } +`; + +export const CREATE_OR_UPDATE_PREFERENCE_TOPIC = gql` + mutation TranscendCliCreateOrUpdatePreferenceTopic($input: CreateOrUpdatePreferenceTopicInput!) { + createOrUpdatePreferenceTopic(input: $input) { + preferenceTopic { + id + slug + } + } + } +`; diff --git a/packages/cli/src/lib/graphql/gqls/purpose.ts b/packages/cli/src/lib/graphql/gqls/purpose.ts index ecd89bb6..3ec6b953 100644 --- a/packages/cli/src/lib/graphql/gqls/purpose.ts +++ b/packages/cli/src/lib/graphql/gqls/purpose.ts @@ -35,3 +35,27 @@ export const PURPOSES = gql` } } `; + +export const CREATE_PURPOSE = gql` + mutation TranscendCliCreatePurpose($input: TrackingPurposeCreateInput!) { + createPurpose(input: $input) { + trackingPurpose { + id + name + trackingType + } + } + } +`; + +export const UPDATE_PURPOSE = gql` + mutation TranscendCliUpdatePurpose($input: TrackingPurposeUpdateInput!) { + updatePurpose(input: $input) { + trackingPurpose { + id + name + trackingType + } + } + } +`; diff --git a/packages/cli/src/lib/graphql/index.ts b/packages/cli/src/lib/graphql/index.ts index c14e6cec..5881e7de 100644 --- a/packages/cli/src/lib/graphql/index.ts +++ b/packages/cli/src/lib/graphql/index.ts @@ -89,3 +89,8 @@ export * from './syncTemplates.js'; export * from './syncVendors.js'; export * from './uploadSiloDiscoveryResults.js'; export * from './fetchAllSiloDiscoveryResults.js'; +export * from './fetchAllConsentWorkflowTriggers.js'; +export * from './fetchAllPreferenceOptionValues.js'; +export * from './syncConsentWorkflowTriggers.js'; +export * from './syncPreferenceOptionValues.js'; +export * from './syncPurposes.js'; diff --git a/packages/cli/src/lib/graphql/pullTranscendConfiguration.ts b/packages/cli/src/lib/graphql/pullTranscendConfiguration.ts index 61e60df9..2fefed09 100644 --- a/packages/cli/src/lib/graphql/pullTranscendConfiguration.ts +++ b/packages/cli/src/lib/graphql/pullTranscendConfiguration.ts @@ -45,7 +45,9 @@ import { AssessmentSectionQuestionInput, RiskLogicInput, ConsentPurpose, + type ConsentPreferenceTopicOptionValue, type SiloDiscoveryResultInput, + type ConsentWorkflowTriggerInput, } from '../../codecs.js'; import { TranscendPullResource } from '../../enums.js'; import { logger } from '../../logger.js'; @@ -59,11 +61,13 @@ import { fetchAllAssessments } from './fetchAllAssessments.js'; import { fetchAllAssessmentTemplates } from './fetchAllAssessmentTemplates.js'; import { fetchAllAttributes } from './fetchAllAttributes.js'; import { fetchAllBusinessEntities } from './fetchAllBusinessEntities.js'; +import { fetchAllConsentWorkflowTriggers } from './fetchAllConsentWorkflowTriggers.js'; import { fetchAllCookies } from './fetchAllCookies.js'; import { fetchAllDataCategories } from './fetchAllDataCategories.js'; import { fetchAllDataFlows } from './fetchAllDataFlows.js'; import { fetchAllMessages } from './fetchAllMessages.js'; import { fetchAllPolicies } from './fetchAllPolicies.js'; +import { fetchAllPreferenceOptionValues } from './fetchAllPreferenceOptionValues.js'; import { fetchAllPrivacyCenters } from './fetchAllPrivacyCenters.js'; import { fetchAllProcessingActivities } from './fetchAllProcessingActivities.js'; import { fetchAllProcessingPurposes } from './fetchAllProcessingPurposes.js'; @@ -181,7 +185,9 @@ export async function pullTranscendConfiguration( assessments, assessmentTemplates, purposes, + preferenceOptionValues, siloDiscoveryResults, + consentWorkflowTriggers, ] = await Promise.all([ // Grab all data subjects in the organization resources.includes(TranscendPullResource.DataSilos) || @@ -298,10 +304,18 @@ export async function pullTranscendConfiguration( resources.includes(TranscendPullResource.Purposes) ? fetchAllPurposesAndPreferences(client) : [], + // Fetch preference option values + resources.includes(TranscendPullResource.PreferenceOptions) + ? fetchAllPreferenceOptionValues(client) + : [], // Fetch silo discovery results resources.includes(TranscendPullResource.SystemDiscovery) ? fetchAllSiloDiscoveryResults(client) : [], + // Fetch consent workflow triggers + resources.includes(TranscendPullResource.ConsentWorkflowTriggers) + ? fetchAllConsentWorkflowTriggers(client) + : [], ]); const consentManagerTheme = @@ -1333,6 +1347,39 @@ export async function pullTranscendConfiguration( ); } + // Save preference options + if ( + preferenceOptionValues.length > 0 && + resources.includes(TranscendPullResource.PreferenceOptions) + ) { + result['preference-options'] = preferenceOptionValues.map( + ({ slug, title }): ConsentPreferenceTopicOptionValue => ({ + slug, + title: title.defaultMessage, + }), + ); + } + + // Save consent workflow triggers + if ( + consentWorkflowTriggers.length > 0 && + resources.includes(TranscendPullResource.ConsentWorkflowTriggers) + ) { + result['consent-workflow-triggers'] = consentWorkflowTriggers.map( + (trigger): ConsentWorkflowTriggerInput => ({ + name: trigger.name, + 'trigger-condition': trigger.triggerCondition || undefined, + 'action-type': trigger.action.type, + 'data-subject-type': trigger.subject.type, + 'is-silent': trigger.isSilent, + 'allow-unauthenticated': trigger.allowUnauthenticated, + 'is-active': trigger.isActive, + 'data-silo-titles': + trigger.dataSilos.length > 0 ? trigger.dataSilos.map((ds) => ds.title) : undefined, + }), + ); + } + // save email templates if ( dataSiloIds.length === 0 && diff --git a/packages/cli/src/lib/graphql/syncConfigurationToTranscend.ts b/packages/cli/src/lib/graphql/syncConfigurationToTranscend.ts index 2a0ecf46..2166249b 100644 --- a/packages/cli/src/lib/graphql/syncConfigurationToTranscend.ts +++ b/packages/cli/src/lib/graphql/syncConfigurationToTranscend.ts @@ -19,6 +19,7 @@ import { syncAgents } from './syncAgents.js'; import { syncAttribute } from './syncAttribute.js'; import { syncBusinessEntities } from './syncBusinessEntities.js'; import { syncConsentManager } from './syncConsentManager.js'; +import { syncConsentWorkflowTriggers } from './syncConsentWorkflowTriggers.js'; import { syncCookies } from './syncCookies.js'; import { syncDataCategories } from './syncDataCategories.js'; import { syncDataFlows } from './syncDataFlows.js'; @@ -29,12 +30,14 @@ import { syncIdentifier } from './syncIdentifier.js'; import { syncIntlMessages } from './syncIntlMessages.js'; import { syncPartitions } from './syncPartitions.js'; import { syncPolicies } from './syncPolicies.js'; +import { syncPreferenceOptionValues } from './syncPreferenceOptionValues.js'; import { syncPrivacyCenter } from './syncPrivacyCenter.js'; import { syncProcessingActivities } from './syncProcessingActivities.js'; import { syncProcessingPurposes } from './syncProcessingPurposes.js'; import { syncPromptGroups } from './syncPromptGroups.js'; import { syncPromptPartials } from './syncPromptPartials.js'; import { syncPrompts } from './syncPrompts.js'; +import { syncPurposes } from './syncPurposes.js'; import { syncTeams } from './syncTeams.js'; import { syncTemplate } from './syncTemplates.js'; import { syncVendors } from './syncVendors.js'; @@ -102,6 +105,8 @@ export async function syncConfigurationToTranscend( messages, policies, partitions, + 'consent-workflow-triggers': consentWorkflowTriggers, + purposes, } = input; const [identifierByName, dataSubjectsByName, apiKeyTitleMap] = await Promise.all([ @@ -134,6 +139,30 @@ export async function syncConfigurationToTranscend( } } + // Sync preference option values (before purposes, since purposes may reference them) + if (input['preference-options']) { + const preferenceOptionsSuccess = await syncPreferenceOptionValues( + client, + input['preference-options'], + ); + encounteredError = encounteredError || !preferenceOptionsSuccess; + } + + // Sync purposes (and nested preference topics) + if (purposes) { + const purposesSuccess = await syncPurposes(client, purposes); + encounteredError = encounteredError || !purposesSuccess; + } + + // Sync consent workflow triggers + if (consentWorkflowTriggers) { + const consentWorkflowTriggersSuccess = await syncConsentWorkflowTriggers( + client, + consentWorkflowTriggers, + ); + encounteredError = encounteredError || !consentWorkflowTriggersSuccess; + } + // Sync prompts if (prompts) { const promptsSuccess = await syncPrompts(client, prompts); diff --git a/packages/cli/src/lib/graphql/syncConsentWorkflowTriggers.ts b/packages/cli/src/lib/graphql/syncConsentWorkflowTriggers.ts new file mode 100644 index 00000000..c5be5764 --- /dev/null +++ b/packages/cli/src/lib/graphql/syncConsentWorkflowTriggers.ts @@ -0,0 +1,139 @@ +import colors from 'colors'; +import { GraphQLClient } from 'graphql-request'; +import { keyBy } from 'lodash-es'; + +import { ConsentWorkflowTriggerInput } from '../../codecs.js'; +import { logger } from '../../logger.js'; +import { mapSeries } from '../bluebird.js'; +import { fetchAllActions, type Action } from './fetchAllActions.js'; +import { fetchAllConsentWorkflowTriggers } from './fetchAllConsentWorkflowTriggers.js'; +import { fetchAllPurposes, type Purpose } from './fetchAllPurposes.js'; +import { fetchAllDataSubjects, type DataSubject } from './fetchDataSubjects.js'; +import { CREATE_OR_UPDATE_CONSENT_WORKFLOW_TRIGGER } from './gqls/index.js'; +import { makeGraphQLRequest } from './makeGraphQLRequest.js'; + +/** + * Sync consent workflow triggers to Transcend + * + * @param client - GraphQL client + * @param inputs - Consent workflow trigger inputs from YAML + * @returns True if run without error, returns false if an error occurred + */ +export async function syncConsentWorkflowTriggers( + client: GraphQLClient, + inputs: ConsentWorkflowTriggerInput[], +): Promise { + logger.info(colors.magenta(`Syncing "${inputs.length}" consent workflow triggers...`)); + + let encounteredError = false; + + const needsActions = inputs.some((t) => t['action-type']); + const needsSubjects = inputs.some((t) => t['data-subject-type']); + const needsPurposes = inputs.some((t) => t.purposes?.length); + + const [existingTriggers, actions, dataSubjects, purposes] = await Promise.all([ + fetchAllConsentWorkflowTriggers(client), + needsActions ? fetchAllActions(client) : ([] as Action[]), + needsSubjects ? fetchAllDataSubjects(client) : ([] as DataSubject[]), + needsPurposes ? fetchAllPurposes(client) : ([] as Purpose[]), + ]); + + const triggerByName = keyBy(existingTriggers, 'name'); + const actionByType = keyBy(actions, 'type') as Record; + const dataSubjectByType = keyBy(dataSubjects, 'type') as Record; + const purposeByTrackingType = keyBy(purposes, 'trackingType') as Record; + + await mapSeries(inputs, async (trigger) => { + try { + const existingTrigger = triggerByName[trigger.name]; + + // Resolve action type to ID + let actionId: string | undefined; + if (trigger['action-type']) { + const action = actionByType[trigger['action-type']]; + if (!action) { + throw new Error(`Failed to find action with type: ${trigger['action-type']}`); + } + actionId = action.id; + } + + // Resolve data subject type to ID + let dataSubjectId: string | undefined; + if (trigger['data-subject-type']) { + const subject = dataSubjectByType[trigger['data-subject-type']]; + if (!subject) { + throw new Error(`Failed to find data subject with type: ${trigger['data-subject-type']}`); + } + dataSubjectId = subject.id; + } + + // Resolve purpose tracking types to purpose IDs with matching states + const consentWorkflowTriggerPurposes = trigger.purposes?.map((purposeInput) => { + const purpose = purposeByTrackingType[purposeInput['tracking-type']]; + if (!purpose) { + throw new Error( + `Failed to find purpose with trackingType: ${purposeInput['tracking-type']}`, + ); + } + return { + purposeId: purpose.id, + matchingState: purposeInput['matching-state'], + }; + }); + + const input: Record = { + name: trigger.name, + ...(existingTrigger ? { id: existingTrigger.id } : {}), + triggerCondition: trigger['trigger-condition'] ?? '{}', + ...(actionId ? { actionId } : {}), + ...(dataSubjectId ? { dataSubjectId } : {}), + ...(trigger['is-silent'] !== undefined ? { isSilent: trigger['is-silent'] } : {}), + ...(trigger['allow-unauthenticated'] !== undefined + ? { allowUnauthenticated: trigger['allow-unauthenticated'] } + : {}), + ...(trigger['is-active'] !== undefined ? { isActive: trigger['is-active'] } : {}), + ...(existingTrigger && consentWorkflowTriggerPurposes + ? { consentWorkflowTriggerPurposes } + : {}), + }; + + const { + createOrUpdateConsentWorkflowTrigger: { + consentWorkflowTrigger: { id: triggerId }, + }, + } = await makeGraphQLRequest<{ + /** Mutation result */ + createOrUpdateConsentWorkflowTrigger: { + /** Created or updated trigger */ + consentWorkflowTrigger: { + /** Trigger ID */ + id: string; + /** Trigger name */ + name: string; + }; + }; + }>(client, CREATE_OR_UPDATE_CONSENT_WORKFLOW_TRIGGER, { input }); + + // For newly created triggers, purposes must be attached via a follow-up update + if (!existingTrigger && consentWorkflowTriggerPurposes?.length) { + await makeGraphQLRequest(client, CREATE_OR_UPDATE_CONSENT_WORKFLOW_TRIGGER, { + input: { + id: triggerId, + consentWorkflowTriggerPurposes, + }, + }); + } + + logger.info(colors.green(`Successfully synced consent workflow trigger "${trigger.name}"!`)); + } catch (err) { + encounteredError = true; + logger.info( + colors.red(`Failed to sync consent workflow trigger "${trigger.name}"! - ${err.message}`), + ); + } + }); + + logger.info(colors.green(`Synced "${inputs.length}" consent workflow triggers!`)); + + return !encounteredError; +} diff --git a/packages/cli/src/lib/graphql/syncPreferenceOptionValues.ts b/packages/cli/src/lib/graphql/syncPreferenceOptionValues.ts new file mode 100644 index 00000000..44a56f48 --- /dev/null +++ b/packages/cli/src/lib/graphql/syncPreferenceOptionValues.ts @@ -0,0 +1,85 @@ +import colors from 'colors'; +import { GraphQLClient } from 'graphql-request'; +import { keyBy } from 'lodash-es'; + +import { ConsentPreferenceTopicOptionValue } from '../../codecs.js'; +import { logger } from '../../logger.js'; +import { + fetchAllPreferenceOptionValues, + type PreferenceOptionValue, +} from './fetchAllPreferenceOptionValues.js'; +import { CREATE_OR_UPDATE_PREFERENCE_OPTION_VALUES } from './gqls/index.js'; +import { makeGraphQLRequest } from './makeGraphQLRequest.js'; + +/** + * Create or update preference option values + * + * @param client - GraphQL client + * @param optionValues - Preference option values paired with existing IDs + * @returns Created/updated preference option values + */ +export async function createOrUpdatePreferenceOptionValues( + client: GraphQLClient, + optionValues: [ConsentPreferenceTopicOptionValue, string | undefined][], +): Promise { + const result = await makeGraphQLRequest<{ + /** createOrUpdatePreferenceOptionValues mutation */ + createOrUpdatePreferenceOptionValues: { + /** Preference option values */ + preferenceOptionValues: PreferenceOptionValue[]; + }; + }>(client, CREATE_OR_UPDATE_PREFERENCE_OPTION_VALUES, { + input: { + input: { + preferenceOptionValues: optionValues.map(([optionValue, id]) => ({ + ...optionValue, + id, + })), + }, + }, + }); + return result.createOrUpdatePreferenceOptionValues.preferenceOptionValues; +} + +/** + * Sync the preference option values + * + * @param client - GraphQL client + * @param optionValues - Preference option values + * @returns True if synced successfully + */ +export async function syncPreferenceOptionValues( + client: GraphQLClient, + optionValues: ConsentPreferenceTopicOptionValue[], +): Promise { + let encounteredError = false; + logger.info(colors.magenta(`Syncing "${optionValues.length}" preference option values...`)); + + const existing = await fetchAllPreferenceOptionValues(client); + const optionValueBySlug = keyBy(existing, 'slug'); + + try { + logger.info( + colors.magenta( + `Performing bulk create or update for "${optionValues.length}" preference option values...`, + ), + ); + + await createOrUpdatePreferenceOptionValues( + client, + optionValues.map((optionValueInput) => [ + optionValueInput, + optionValueBySlug[optionValueInput.slug]?.id, + ]), + ); + + logger.info( + colors.green(`Successfully synced "${optionValues.length}" preference option values!`), + ); + } catch (err) { + encounteredError = true; + logger.info(colors.red(`Failed to sync preference option values! - ${err.message}`)); + } + + return !encounteredError; +} diff --git a/packages/cli/src/lib/graphql/syncPurposes.ts b/packages/cli/src/lib/graphql/syncPurposes.ts new file mode 100644 index 00000000..8620096f --- /dev/null +++ b/packages/cli/src/lib/graphql/syncPurposes.ts @@ -0,0 +1,262 @@ +import colors from 'colors'; +import { GraphQLClient } from 'graphql-request'; +import { keyBy } from 'lodash-es'; + +import { ConsentPreferenceTopic, ConsentPurpose } from '../../codecs.js'; +import { logger } from '../../logger.js'; +import { map } from '../bluebird.js'; +import { + fetchAllPreferenceOptionValues, + type PreferenceOptionValue, +} from './fetchAllPreferenceOptionValues.js'; +import { PreferenceTopic } from './fetchAllPreferenceTopics.js'; +import { + PurposeWithPreferences, + fetchAllPurposesAndPreferences, +} from './fetchAllPurposesAndPreferences.js'; +import { UPDATE_PURPOSE, CREATE_PURPOSE, CREATE_OR_UPDATE_PREFERENCE_TOPIC } from './gqls/index.js'; +import { makeGraphQLRequest } from './makeGraphQLRequest.js'; + +export interface PreferenceTopicSyncOptions { + /** Purpose ID */ + purposeId: string; + /** Preference option values indexed by slug */ + optionValuesBySlug: Record; + /** Existing preference topics indexed by slug */ + topicsBySlug: Record; + /** Concurrency for upload */ + concurrency: number; +} + +/** + * Create or update preference topics for a purpose + * + * @param client - GraphQL client + * @param topics - Preference topics to create or update + * @param options - Options + */ +export async function createOrUpdatePreferenceTopics( + client: GraphQLClient, + topics: ConsentPreferenceTopic[], + { purposeId, optionValuesBySlug, topicsBySlug, concurrency = 20 }: PreferenceTopicSyncOptions, +): Promise { + await map( + topics, + async (topic) => { + const existingTopic = topicsBySlug[topic.title]; + await makeGraphQLRequest(client, CREATE_OR_UPDATE_PREFERENCE_TOPIC, { + input: { + type: topic.type, + title: topic.title, + showInPrivacyCenter: topic['show-in-privacy-center'], + purposeId, + ...(topic.options + ? { + preferenceOptionValueIds: topic.options.map((option) => { + const result = optionValuesBySlug[option.slug]; + if (!result) { + throw new Error( + `Preference option value with slug "${option.slug}" not found.`, + ); + } + return result.id; + }), + } + : {}), + ...(existingTopic ? { id: existingTopic.id } : {}), + displayDescription: topic.description, + defaultConfiguration: topic['default-configuration'], + }, + }); + }, + { concurrency }, + ); +} + +/** + * Create a new purpose + * + * @param client - GraphQL client + * @param input - Purpose input + * @param options - Options for syncing preference topics + * @returns Purpose ID + */ +export async function createPurpose( + client: GraphQLClient, + input: ConsentPurpose, + options: Omit, +): Promise { + const { + createPurpose: { trackingPurpose }, + } = await makeGraphQLRequest<{ + /** createPurpose mutation */ + createPurpose: { + /** Purpose */ + trackingPurpose: { + /** ID */ + id: string; + }; + }; + }>(client, CREATE_PURPOSE, { + input: { + trackingType: input.trackingType, + showInPrivacyCenter: input['show-in-privacy-center'], + showInConsentManager: input['show-in-consent-manager'], + optOutSignals: input['opt-out-signals'], + name: input.title, + isActive: input['is-active'], + description: input.description, + displayOrder: input['display-order'], + configurable: input.configurable, + authLevel: input['auth-level'], + }, + }); + logger.info(colors.green(`Successfully created purpose "${input.title}"!`)); + + if (input['preference-topics'] && input['preference-topics'].length > 0) { + await createOrUpdatePreferenceTopics(client, input['preference-topics'], { + ...options, + purposeId: trackingPurpose.id, + topicsBySlug: {}, + }); + logger.info( + colors.green( + `Successfully synced ${input['preference-topics'].length} preference topics for purpose "${input.title}"!`, + ), + ); + } + return trackingPurpose.id; +} + +/** + * Update an existing purpose + * + * @param client - GraphQL client + * @param input - Purpose input + * @param options - Options for syncing preference topics + */ +export async function updatePurpose( + client: GraphQLClient, + input: ConsentPurpose, + options: PreferenceTopicSyncOptions, +): Promise { + await makeGraphQLRequest(client, UPDATE_PURPOSE, { + input: { + id: options.purposeId, + title: input.title, + showInPrivacyCenter: input['show-in-privacy-center'], + showInConsentManager: input['show-in-consent-manager'], + configurable: input.configurable, + optOutSignals: input['opt-out-signals'], + name: input.title, + isActive: input['is-active'], + displayOrder: input['display-order'], + description: input.description, + authLevel: input['auth-level'], + }, + }); + logger.info( + colors.green( + `Successfully updated purpose: ${options.purposeId}:${input.title || input.trackingType}!`, + ), + ); + + if (input['preference-topics'] && input['preference-topics'].length > 0) { + await createOrUpdatePreferenceTopics(client, input['preference-topics'], options); + logger.info( + colors.green( + `Successfully synced ${input['preference-topics'].length} preference topics for purpose "${ + input.title || input.trackingType + }"!`, + ), + ); + } +} + +/** + * Sync the purposes + * + * @param client - GraphQL client + * @param purposes - Purposes + * @param concurrency - Concurrency + * @returns True if synced successfully + */ +export async function syncPurposes( + client: GraphQLClient, + purposes: ConsentPurpose[], + concurrency = 20, +): Promise { + let encounteredError = false; + logger.info(colors.magenta(`Syncing "${purposes.length}" purposes...`)); + + const [existing, existingOptions] = await Promise.all([ + fetchAllPurposesAndPreferences(client), + fetchAllPreferenceOptionValues(client), + ]); + const purposeByTrackingType = keyBy(existing, 'trackingType'); + const optionValuesBySlug = keyBy(existingOptions, 'slug'); + + const mapPurposesToExisting = purposes.map((purposeInput) => [ + purposeInput, + purposeByTrackingType[purposeInput.trackingType], + ]); + + // Create new purposes + const newPurposes = mapPurposesToExisting + .filter(([, existing]) => !existing) + .map(([purposeInput]) => purposeInput as ConsentPurpose); + try { + logger.info(colors.magenta(`Creating "${newPurposes.length}" new purposes...`)); + await map( + newPurposes, + async (purpose) => { + await createPurpose(client, purpose, { + concurrency, + optionValuesBySlug, + }); + }, + { concurrency }, + ); + logger.info(colors.green(`Successfully created ${newPurposes.length} purposes!`)); + } catch (err) { + encounteredError = true; + logger.info(colors.red(`Failed to create purposes! - ${err.message}`)); + } + + // Update existing purposes + const existingPurposes = mapPurposesToExisting.filter( + (x): x is [ConsentPurpose, PurposeWithPreferences] => !!x[1], + ); + try { + logger.info(colors.magenta(`Updating "${existingPurposes.length}" purposes...`)); + await map( + existingPurposes, + async ([purposeInput, existingPurpose]) => { + try { + await updatePurpose(client, purposeInput, { + concurrency, + optionValuesBySlug, + purposeId: existingPurpose.id, + topicsBySlug: keyBy(existingPurpose.topics, 'slug'), + }); + } catch (err) { + encounteredError = true; + logger.info( + colors.red( + `Failed to update purpose "${existingPurpose.id}" (${purposeInput.trackingType})! - ${err.message}`, + ), + ); + } + }, + { concurrency }, + ); + logger.info(colors.green(`Successfully updated "${existingPurposes.length}" purposes!`)); + } catch (err) { + encounteredError = true; + logger.info(colors.red(`Failed to update purposes! - ${err.message}`)); + } + + logger.info(colors.green(`Synced "${purposes.length}" purposes!`)); + + return !encounteredError; +}