From 2664eb459bdf3205a3d15b3349931dd70cecb454 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:01:21 -0500 Subject: [PATCH] feat: Init Cal.com Salesforce package (#26330) * Add db schema * Add `CredentialRepository.findByTeamIdAndSlugs` * Add enabled app slugs for attribute syncing * Create repository for `IntegrationAttributeSync` * Create zod schemas * Create `AttributeSyncUserRuleOutputMapper` * Create `IntegrationAttributeSyncService` * Create DI contianer * Create trpc endpoints * Create page * Include team name in `CredentialRepository.findByTeamIdAndSlugs` * Update schema and relations * Update types and schemas * Add more methods to IntegrationAttributeSyncRepository * Add more services to `IntegrationAttributeSyncService` - getById - Init updateIncludeRulesAndMappings * Refactor `getTeams.handler` to use repository * Create `createAttributeSync` trpc endpoint * Create `updateAttributeSync` trpc endpoint * Add router to trpc * Create attribute sync child components * Pass custom actions to `FormCard` * Create `IntegrationAttributeSyncCard` * Pass inital props via server side * Fix prop * Only refetch on mutation * Fixes * Add form error when duplicate field and attribute combo * Add `updateTransactionWithRUleAndMappings` logic * Adjust zod schemas * Service add `updateIncludeRulesAndMappings` * Pass orgId from server to component * Rename types * Add deleteById method to repository * Add name to integrationAttributeSync * Add deleteById method to service * Rename method * Add deleteAttributeSync trpc endpoint * Make the IntegrationAttributeSyncCard a dummy component * test: add tests for IntegrationAttributeSync feature Co-Authored-By: joe@cal.com * Move creating a attribute sync record to the service * Add i18n strings * Safe select credential in find by id and team * Fix default credentialId value in form * Update repository return types * Add i18n string * Make credentialId optional for form schema * Fix label * Add cascade deletes * Add verification that syncs belong to org * Create mapper for repository output * Type fixes * Remove old test file * Pass `attributeOptions` from parent to children * Infer types from zod schema * Type fixes * Type fix * Clean up * Add i18n strings * Remove unused file * Address feedback * Add migration file * Address feedback * Add validation for new integration values * Remove unused router * Move away from z.infer to z.ZodType * Clean up comments * Type fix * Type fixes * Type fix * fix: add passthrough to syncFormDataSchema to preserve extra fields Co-Authored-By: joe@cal.com * fix: remove incorrect test that expected extra fields to pass through syncFormDataSchema Co-Authored-By: joe@cal.com * Add endpoint for SF to call * Create scratch org config * Create sf cli scripts * Create package logic * Update README * Remove unused file * Add indexes * Add aria label * Address feedback - consistent validation * Fix import paths for attribute types * refactor: change attributeSyncRules array to singular attributeSyncRule The database schema enforces a one-to-one relationship between IntegrationAttributeSync and AttributeSyncRule (via @unique constraint), and the UI only supports a single rule. This change makes the TypeScript type match the database schema and UI behavior. Changes: - Update IntegrationAttributeSync interface to use attributeSyncRule: AttributeSyncRule | null - Update mapper to return singular rule instead of wrapping in array - Update UI component to access sync.attributeSyncRule directly - Update IIntegrationAttributeSyncUpdateParams Omit type - Update tests to use attributeSyncRule: null instead of attributeSyncRules: [] Co-Authored-By: joe@cal.com * feat: implement FeatureOptInService (#25805) * feat: implement FeatureOptInService WIP * clean up * feat: consolidate feature repositories and add updateFeatureForUser - Implement updateFeatureForUser in FeaturesRepository (similar to updateFeatureForTeam) - Move getUserFeatureState and getTeamFeatureState from PrismaFeatureOptInRepository to FeaturesRepository - Update FeatureOptInService to use only FeaturesRepository - Add setUserFeatureState and setTeamFeatureState methods to FeatureOptInService - Update _router.ts to remove PrismaFeatureOptInRepository usage - Remove PrismaFeatureOptInRepository.ts and FeatureOptInRepositoryInterface.ts - Update features.repository.interface.ts and features.repository.mock.ts - Add integration tests for updateFeatureForUser, getUserFeatureState, getTeamFeatureState - Update service.integration-test.ts to use FeaturesRepository Co-Authored-By: eunjae@cal.com * refactor: rename updateFeatureForUser to setUserFeatureState Rename to match the convention used for setTeamFeatureState Co-Authored-By: eunjae@cal.com * refactor: return FeatureState type from getUserFeatureState and getTeamFeatureState * fix integration tests * clean up logics * update services and router * refactor: change getUserFeatureState and getTeamFeatureState to accept featureIds array - Renamed getUserFeatureState to getUserFeatureStates - Renamed getTeamFeatureState to getTeamFeatureStates - Changed parameter from featureId: string to featureIds: string[] - Changed return type from FeatureState to Record - Updated FeatureOptInService to use the new batch methods - Added tests for querying multiple features in a single call - Optimized listFeaturesForTeam to fetch all feature states in one query Co-Authored-By: eunjae@cal.com * feat: add getFeatureStateForTeams for batch querying multiple teams - Added getFeatureStateForTeams method to query a single feature across multiple teams in one call - Updated FeatureOptInService.resolveFeatureStateAcrossTeams to use the new batch method - Replaces N+1 queries with a single database query for team states - Added comprehensive integration tests for the new method Co-Authored-By: eunjae@cal.com * refactor: combine org and team state queries into single call - Include orgId in the teamIds array passed to getFeatureStateForTeams - Extract org state and team states from the combined result - Reduces database queries from 3 to 2 in resolveFeatureStateAcrossTeams Co-Authored-By: eunjae@cal.com * refactor: use team.isOrganization and clarify computeEffectiveState comment Co-Authored-By: eunjae@cal.com * refactor: use MembershipRepository.findAllByUserId with isOrganization Co-Authored-By: eunjae@cal.com * feat: add featureId validation using isOptInFeature type guard Co-Authored-By: eunjae@cal.com * less queries * add fallback value * fix type error * move files * add autoOptInFeatures column * use autoOptInFeatures flag within FeatureOptInService * add setUserAutoOptIn and setTeamAutoOptIn * fix computeEffectiveState logic * rewrite computeEffectiveState * clean up integration tests * clean up in afterEach * fix type error * refactor: use FeaturesRepository methods instead of direct Prisma calls Replace all manual userFeatures and teamFeatures Prisma operations with the new setUserFeatureState and setTeamFeatureState repository methods. Changes include: - Admin handlers (assignFeatureToTeam, unassignFeatureFromTeam) - Test fixtures and integration tests - Playwright fixtures - Development scripts This ensures consistent feature flag management through the repository pattern and supports the new tri-state semantics (enabled/disabled/inherit). Co-Authored-By: eunjae@cal.com * clean up * fix the logic * extract some logic into applyAutoOptIn() * remove wrong code * refactor: convert setUserFeatureState and setTeamFeatureState to object params with discriminated union - Convert multiple positional parameters to single object parameter - Use discriminated union types: assignedBy required for enabled/disabled, omitted for inherit - Update all callers across repository, service, handlers, fixtures, and tests * fix type error * use Promise.all * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Prevent duplicate field and attribute mappings * Add validation that attribute belongs to the org * fix: address Cubic AI review feedback - Add @@index([credentialId]) to IntegrationAttributeSync model for efficient cascade deletes and credential-based queries (confidence: 9/10) - Fix translation key from 'credential_required' to 'attribute_sync_credential_required' to match existing locale definition (confidence: 10/10) Co-Authored-By: joe@cal.com * fix: address remaining cubic review feedback - RuleBuilder.tsx: Use unique IDs for React keys instead of array index to prevent reconciliation bugs when removing conditions - updateAttributeSync.handler.ts: Add early organization check before service calls for consistency - createAttributeSync.handler.ts: Add CredentialNotFoundError class for type-safe error handling instead of string matching - IntegrationAttributeSyncService.test.ts: Replace expect.fail() with proper Vitest rejects.toSatisfy() pattern Co-Authored-By: joe@cal.com * Pull test file from main * fix: address Cubic AI review feedback (confidence >= 9/10) - Remove sensitive fields (email, changedFields) from logging in user-sync.ts - Rename scratch-org:open to scratch-org:start in package.json to match README Co-Authored-By: unknown <> * Update README --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Eunjae Lee Co-authored-by: Hariom Balhara --- packages/app-store/salesforce/README.md | 16 +++++- packages/app-store/salesforce/api/index.ts | 1 + .../app-store/salesforce/api/user-sync.ts | 25 +++++++++ packages/app-store/salesforce/package.json | 6 ++- .../app-store/salesforce/scratch-org-def.json | 6 +++ .../salesforce/sfdc-package/.forceignore | 12 +++++ .../classes/CalComCalloutQueueable.cls | 54 +++++++++++++++++++ .../CalComCalloutQueueable.cls-meta.xml | 5 ++ .../default/classes/UserUpdateHandler.cls | 46 ++++++++++++++++ .../classes/UserUpdateHandler.cls-meta.xml | 5 ++ ...alCom_Development.namedCredential-meta.xml | 12 +++++ ...CalCom_Production.namedCredential-meta.xml | 12 +++++ .../triggers/UserUpdateTrigger.trigger | 3 ++ .../UserUpdateTrigger.trigger-meta.xml | 5 ++ .../salesforce/sfdc-package/sfdx-project.json | 12 +++++ 15 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 packages/app-store/salesforce/api/user-sync.ts create mode 100644 packages/app-store/salesforce/scratch-org-def.json create mode 100644 packages/app-store/salesforce/sfdc-package/.forceignore create mode 100644 packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls create mode 100644 packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls-meta.xml create mode 100644 packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls create mode 100644 packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls-meta.xml create mode 100644 packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Development.namedCredential-meta.xml create mode 100644 packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Production.namedCredential-meta.xml create mode 100644 packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger create mode 100644 packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger-meta.xml create mode 100644 packages/app-store/salesforce/sfdc-package/sfdx-project.json diff --git a/packages/app-store/salesforce/README.md b/packages/app-store/salesforce/README.md index 760e44d0c6..1d79b7cb77 100644 --- a/packages/app-store/salesforce/README.md +++ b/packages/app-store/salesforce/README.md @@ -1,8 +1,20 @@ -## Working With GraphQL +# Creating a Salesforce test org +You must have the [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) installed to create a test org. Once installed, you can create a test org using the following command `yarn scratch-org:create` + +This will create a scratch org with the configuration specified in the `project-scratch-def.json` file. + +To open a browser tab to the org, run `yarn scratch-org:start` + +# Working With GraphQL This package utilizes [`GraphQL Codegen`](https://the-guild.dev/graphql/codegen#graphql-codegen) to generate types and queries from the Salesforce schema. ### Generating GraphQL Schema Currently v63 of the Salesforce graphql endpoint throws an error when trying to generate files. This is due to the `Setup__JoinInput` type not generating any fields. To work around this, the `schema.json` file comes from the [Salesforce graphql introspection query](https://www.postman.com/salesforce-developers/salesforce-developers/request/sy8qaf9/introspection-query). This file is then converted to a SDL file using the [graphql-introspection-json-to-sdl](https://github.com/Calcom/graphql-introspection-json-to-sdl) package. You can generate the SDL file by running `yarn generate:schema`. ### Generating Queries -When working with graphql files ensure that `yarn codegen:watch` is running in the background. This will generate the types and queries from the SDL file. \ No newline at end of file +When working with graphql files ensure that `yarn codegen:watch` is running in the background. This will generate the types and queries from the SDL file. + +# Developing the SFDC package +The SFDC package is written using Apex. To develop this package, you need to have the Salesforce CLI installed. Then you can run `yarn sfdc:deploy:preview` to see what changes will be deployed to the scratch org. Running `yarn sfdc:deploy` will deploy the changes to the scratch org. + +Note that if you want to call your local development instances you need to change the "Named Credential" on the scratch org settings to point the `CalCom_Development` credential to the local instance. diff --git a/packages/app-store/salesforce/api/index.ts b/packages/app-store/salesforce/api/index.ts index eb12c1b4ed..44c6f48858 100644 --- a/packages/app-store/salesforce/api/index.ts +++ b/packages/app-store/salesforce/api/index.ts @@ -1,2 +1,3 @@ export { default as add } from "./add"; export { default as callback } from "./callback"; +export { default as "user-sync" } from "./user-sync"; diff --git a/packages/app-store/salesforce/api/user-sync.ts b/packages/app-store/salesforce/api/user-sync.ts new file mode 100644 index 0000000000..2f4081bec0 --- /dev/null +++ b/packages/app-store/salesforce/api/user-sync.ts @@ -0,0 +1,25 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import logger from "@calcom/lib/logger"; + +const log = logger.getSubLogger({ prefix: ["[salesforce/user-sync]"] }); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { instanceUrl, orgId, salesforceUserId, email, changedFields, timestamp } = req.body; + + log.info("Received user sync request", { + instanceUrl, + orgId, + salesforceUserId, + timestamp, + }); + + // TODO: Validate instanceUrl + orgId against stored credentials + // TODO: Sync changedFields to Cal.com user + + return res.status(200).json({ success: true }); +} diff --git a/packages/app-store/salesforce/package.json b/packages/app-store/salesforce/package.json index 749802f2b4..16813b4605 100644 --- a/packages/app-store/salesforce/package.json +++ b/packages/app-store/salesforce/package.json @@ -7,7 +7,11 @@ "scripts": { "generate:schema": "graphql-introspection-json-to-sdl ./schema.json > ./schema.graphql", "codegen": "graphql-codegen --config codegen.ts", - "codegen:watch": "graphql-codegen --config codegen.ts --watch" + "codegen:watch": "graphql-codegen --config codegen.ts --watch", + "scratch-org:create": "sf org create scratch --definition-file scratch-org-def.json --alias calcom-test --duration-days 7 --set-default", + "scratch-org:start": "sf org open --target-org calcom-test", + "sfdc:deploy": "cd sfdc-package && sf project deploy start", + "sfdc:deploy:preview": "cd sfdc-package && sf project deploy preview" }, "dependencies": { "@calcom/lib": "workspace:*", diff --git a/packages/app-store/salesforce/scratch-org-def.json b/packages/app-store/salesforce/scratch-org-def.json new file mode 100644 index 0000000000..5134172dd3 --- /dev/null +++ b/packages/app-store/salesforce/scratch-org-def.json @@ -0,0 +1,6 @@ +{ + "orgName": "Cal.com Test Org", + "edition": "Developer", + "features": [], + "settings": {} +} diff --git a/packages/app-store/salesforce/sfdc-package/.forceignore b/packages/app-store/salesforce/sfdc-package/.forceignore new file mode 100644 index 0000000000..7b5b5a71fd --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/.forceignore @@ -0,0 +1,12 @@ +# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status +# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm +# + +package.xml + +# LWC configuration files +**/jsconfig.json +**/.eslintrc.json + +# LWC Jest +**/__tests__/** \ No newline at end of file diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls new file mode 100644 index 0000000000..17e8349831 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls @@ -0,0 +1,54 @@ +public with sharing class CalComCalloutQueueable implements Queueable, Database.AllowsCallouts { + + private static final String ENDPOINT_PATH = '/api/integrations/salesforce/user-sync'; + + private List payloads; + + public CalComCalloutQueueable(List payloads) { + this.payloads = payloads; + } + + public void execute(QueueableContext context) { + String namedCredential = getNamedCredential(); + + for (UserChangePayload payload : payloads) { + sendPayload(namedCredential, payload); + } + } + + private String getNamedCredential() { + Boolean isSandbox = [SELECT IsSandbox FROM Organization LIMIT 1].IsSandbox; + return isSandbox ? 'callout:CalCom_Development' : 'callout:CalCom_Production'; + } + + private void sendPayload(String namedCredential, UserChangePayload payload) { + try { + HttpRequest req = new HttpRequest(); + req.setEndpoint(namedCredential + ENDPOINT_PATH); + req.setMethod('POST'); + req.setHeader('Content-Type', 'application/json'); + req.setBody(JSON.serialize(payload)); + req.setTimeout(30000); + + Http http = new Http(); + HttpResponse res = http.send(req); + + if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) { + System.debug(LoggingLevel.INFO, 'Successfully sent user update to Cal.com for user: ' + payload.salesforceUserId); + } else { + System.debug(LoggingLevel.ERROR, 'Failed to send user update to Cal.com. Status: ' + res.getStatusCode() + ' Body: ' + res.getBody()); + } + } catch (Exception e) { + System.debug(LoggingLevel.ERROR, 'Exception sending user update to Cal.com: ' + e.getMessage()); + } + } + + public class UserChangePayload { + public Id salesforceUserId; + public String email; + public String instanceUrl; + public String orgId; + public Map changedFields; + public String timestamp; + } +} diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls-meta.xml new file mode 100644 index 0000000000..1e7de94088 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/CalComCalloutQueueable.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls new file mode 100644 index 0000000000..f0c7b1368a --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls @@ -0,0 +1,46 @@ +public with sharing class UserUpdateHandler { + + public static void handleAfterUpdate(Map oldMap, Map newMap) { + List payloads = new List(); + + for (Id userId : newMap.keySet()) { + User oldUser = oldMap.get(userId); + User newUser = newMap.get(userId); + + Map changedFields = getChangedFields(oldUser, newUser); + + if (!changedFields.isEmpty()) { + CalComCalloutQueueable.UserChangePayload payload = new CalComCalloutQueueable.UserChangePayload(); + payload.salesforceUserId = userId; + payload.email = newUser.Email; + payload.instanceUrl = URL.getOrgDomainUrl().toExternalForm(); + payload.orgId = UserInfo.getOrganizationId(); + payload.changedFields = changedFields; + payload.timestamp = Datetime.now().formatGMT('yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\''); + payloads.add(payload); + } + } + + if (!payloads.isEmpty()) { + System.enqueueJob(new CalComCalloutQueueable(payloads)); + } + } + + private static Map getChangedFields(User oldUser, User newUser) { + Map changedFields = new Map(); + + Map oldFields = oldUser.getPopulatedFieldsAsMap(); + Map newFields = newUser.getPopulatedFieldsAsMap(); + + for (String fieldName : newFields.keySet()) { + Object oldValue = oldFields.get(fieldName); + Object newValue = newFields.get(fieldName); + + if (oldValue != newValue) { + changedFields.put(fieldName, newValue); + } + } + + return changedFields; + } +} diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls-meta.xml new file mode 100644 index 0000000000..1e7de94088 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/classes/UserUpdateHandler.cls-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Development.namedCredential-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Development.namedCredential-meta.xml new file mode 100644 index 0000000000..0c1d1c556d --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Development.namedCredential-meta.xml @@ -0,0 +1,12 @@ + + + CalCom_Development + + https://app.cal.com + Anonymous + Custom + false + true + false + Enabled + diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Production.namedCredential-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Production.namedCredential-meta.xml new file mode 100644 index 0000000000..831dbb1ca9 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/namedCredentials/CalCom_Production.namedCredential-meta.xml @@ -0,0 +1,12 @@ + + + CalCom_Production + + https://app.cal.com + Anonymous + Custom + false + true + false + Enabled + diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger b/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger new file mode 100644 index 0000000000..7ed18f8617 --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger @@ -0,0 +1,3 @@ +trigger UserUpdateTrigger on User (after update) { + UserUpdateHandler.handleAfterUpdate(Trigger.oldMap, Trigger.newMap); +} diff --git a/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger-meta.xml b/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger-meta.xml new file mode 100644 index 0000000000..36cb41a89a --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/force-app/main/default/triggers/UserUpdateTrigger.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 64.0 + Active + diff --git a/packages/app-store/salesforce/sfdc-package/sfdx-project.json b/packages/app-store/salesforce/sfdc-package/sfdx-project.json new file mode 100644 index 0000000000..591afc134d --- /dev/null +++ b/packages/app-store/salesforce/sfdc-package/sfdx-project.json @@ -0,0 +1,12 @@ +{ + "packageDirectories": [ + { + "path": "force-app", + "default": true + } + ], + "name": "calcom-user-sync", + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "64.0" +}