-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: Init Cal.com Salesforce package #26330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8aac966
6e50f46
dd776bf
49e3f33
2c897bf
4968b21
deeec8e
25ca942
7b178e9
09ac928
c0a71d2
22dbcfb
0264a9c
80144ab
8d0d61f
fd39eae
e83a8e4
b0eba94
a4796d3
ccbf84e
3ce313d
98af081
3303c2f
9cc2e5a
3fd9430
a6a4dc8
96802ed
bbce738
013c51e
327ea66
19c8423
652c87c
daad1d5
5d176bd
da07950
48fe98c
fece321
920bd80
6857276
8fc4eaf
6d98c8b
26889b6
3b9f334
eea3710
c43f6ad
9113610
53c50ef
24255e6
ef3b4a5
767e5dc
60f4171
cf0cbe0
1c0b033
04f60d7
08cd9a0
fec867f
947d91e
28ed00d
9b0d437
898542f
ad65b71
b288081
1a3abbc
fce660a
e37b5d8
a83c2e9
544877d
5c43536
8c38861
2eceb83
dda286d
ba5d4a4
ad1e092
4f1ce3f
c2252a6
e475921
2fc58dc
efa9260
e28dc1d
7e549d3
ebf508c
3c0a79d
6ff5680
d60966d
2d336b2
4578077
a8e4ede
dad327a
a41af8e
baca20b
4a516ad
1b88faf
1034c4f
f02a534
6001e66
5806f18
f08c12a
7cf9599
c3ff202
8a0ba2d
af72704
5cb22e1
e45a7ed
6a0ff44
c536b19
02f85cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { default as add } from "./add"; | ||
| export { default as callback } from "./callback"; | ||
| export { default as "user-sync" } from "./user-sync"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
Comment on lines
11
to
14
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scripts to help with package development |
||
| }, | ||
| "dependencies": { | ||
| "@calcom/lib": "workspace:*", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "orgName": "Cal.com Test Org", | ||
| "edition": "Developer", | ||
| "features": [], | ||
| "settings": {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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__/** |
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The files in |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| public with sharing class CalComCalloutQueueable implements Queueable, Database.AllowsCallouts { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For SF triggers, we need this class to call the endpoint on Cal |
||
|
|
||
| private static final String ENDPOINT_PATH = '/api/integrations/salesforce/user-sync'; | ||
|
|
||
| private List<UserChangePayload> payloads; | ||
|
|
||
| public CalComCalloutQueueable(List<UserChangePayload> payloads) { | ||
| this.payloads = payloads; | ||
| } | ||
|
|
||
| public void execute(QueueableContext context) { | ||
| String namedCredential = getNamedCredential(); | ||
|
|
||
| for (UserChangePayload payload : payloads) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Sequential HTTP callouts in a loop may cause performance issues or timeout errors with multiple payloads. Consider checking Limits.getCallouts() before each callout and potentially chaining to a new Queueable job if the limit is approached, or batch payloads into a single API request if the endpoint supports it. Prompt for AI agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid point. I think we should batch the payloads if possible and send multiple of them in a single salesforce/user-sync request. Queueable seems to have a limit of 100 callouts
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the feedback! I've saved this as a new learning to improve future reviews. |
||
| sendPayload(namedCredential, payload); | ||
| } | ||
| } | ||
|
|
||
| private String getNamedCredential() { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Named Credentials are like env variables set at the Salesforce level. This method gets either the production or the development URL to send the updated user data to. |
||
| 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()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we plan to handle failures in call out? Will we retry them
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's one option but I think warrants a discussion |
||
| } | ||
| } 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<String, Object> changedFields; | ||
| public String timestamp; | ||
| } | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Each class needs a metadata file for deployment. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
| <apiVersion>64.0</apiVersion> | ||
| <status>Active</status> | ||
| </ApexClass> |
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class determines which fields were changed and add it to the payload |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||||||||||||||||||||||||||
| public with sharing class UserUpdateHandler { | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| public static void handleAfterUpdate(Map<Id, User> oldMap, Map<Id, User> newMap) { | ||||||||||||||||||||||||||||||
| List<CalComCalloutQueueable.UserChangePayload> payloads = new List<CalComCalloutQueueable.UserChangePayload>(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| for (Id userId : newMap.keySet()) { | ||||||||||||||||||||||||||||||
| User oldUser = oldMap.get(userId); | ||||||||||||||||||||||||||||||
| User newUser = newMap.get(userId); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Map<String, Object> 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(); | ||||||||||||||||||||||||||||||
|
Comment on lines
+16
to
+17
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Cal's side, the mapping is associated with a credential on Cal's side. We can use the |
||||||||||||||||||||||||||||||
| 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<String, Object> getChangedFields(User oldUser, User newUser) { | ||||||||||||||||||||||||||||||
| Map<String, Object> changedFields = new Map<String, Object>(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Map<String, Object> oldFields = oldUser.getPopulatedFieldsAsMap(); | ||||||||||||||||||||||||||||||
| Map<String, Object> 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); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Change detection misses cleared/nullified fields. When a User field is set to null, it won't appear in Prompt for AI agentsFix confidence (alpha): 8/10
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems valid concern again @joeauyeung |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return changedFields; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
| <apiVersion>64.0</apiVersion> | ||
| <status>Active</status> | ||
| </ApexClass> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
| <fullName>CalCom_Development</fullName> | ||
| <label>Cal.com Development</label> | ||
| <endpoint>https://app.cal.com</endpoint> | ||
| <principalType>Anonymous</principalType> | ||
| <protocol>Custom</protocol> | ||
| <allowMergeFieldsInBody>false</allowMergeFieldsInBody> | ||
| <allowMergeFieldsInHeader>true</allowMergeFieldsInHeader> | ||
| <generateAuthorizationHeader>false</generateAuthorizationHeader> | ||
| <calloutStatus>Enabled</calloutStatus> | ||
| </NamedCredential> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
| <fullName>CalCom_Production</fullName> | ||
| <label>Cal.com Production</label> | ||
| <endpoint>https://app.cal.com</endpoint> | ||
| <principalType>Anonymous</principalType> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P0: Critical security vulnerability: Named Credential sends sensitive user data without authentication. The configuration uses principalType="Anonymous" and generateAuthorizationHeader="false", and the callout code doesn't add any Authorization header or API key. This allows anyone who discovers the endpoint to send unauthorized data to Cal.com. Add authentication using either Named Credential's per-user or per-org authentication, or implement API key authentication in the HTTP request headers. Prompt for AI agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is planned for stacked PR I believe |
||
| <protocol>Custom</protocol> | ||
| <allowMergeFieldsInBody>false</allowMergeFieldsInBody> | ||
| <allowMergeFieldsInHeader>true</allowMergeFieldsInHeader> | ||
| <generateAuthorizationHeader>false</generateAuthorizationHeader> | ||
| <calloutStatus>Enabled</calloutStatus> | ||
| </NamedCredential> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| trigger UserUpdateTrigger on User (after update) { | ||
| UserUpdateHandler.handleAfterUpdate(Trigger.oldMap, Trigger.newMap); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <ApexTrigger xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
| <apiVersion>64.0</apiVersion> | ||
| <status>Active</status> | ||
| </ApexTrigger> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "packageDirectories": [ | ||
| { | ||
| "path": "force-app", | ||
| "default": true | ||
| } | ||
| ], | ||
| "name": "calcom-user-sync", | ||
| "namespace": "", | ||
| "sfdcLoginUrl": "https://login.salesforce.com", | ||
| "sourceApiVersion": "64.0" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for this endpoint will be in a stacked PR