Files
Benny JooandDevin AI ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

402 lines
14 KiB
TypeScript

import process from "node:process";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import type { Contact } from "@calcom/types/CrmService";
import { Client, cacheExchange, fetchExchange } from "@urql/core";
import { retryExchange } from "@urql/exchange-retry";
import type { RRSkipFieldRule } from "../../zod";
import { RRSkipFieldRuleActionEnum } from "../../zod";
import { SalesforceRecordEnum } from "../enums";
import getAllPossibleWebsiteValuesFromEmailDomain from "../utils/getAllPossibleWebsiteValuesFromEmailDomain";
import getDominantAccountId from "../utils/getDominantAccountId";
import { GetAccountRecordsForRRSkip } from "./documents/queries";
export class SalesforceGraphQLClient {
private log: typeof logger;
private version = "v63.0";
private accessToken: string;
private client: Client;
constructor({
accessToken,
instanceUrl,
}: {
accessToken: string;
instanceUrl: string;
}) {
this.accessToken = accessToken;
const exchanges = [cacheExchange, fetchExchange];
if (
process.env.SALESFORCE_GRAPHQL_DELAY_MS &&
process.env.SALESFORCE_GRAPHQL_MAX_DELAY_MS &&
process.env.SALESFORCE_GRAPHQL_MAX_RETRIES
) {
const retryOptions = {
maxRetries: 3,
initialDelayMs: Number(process.env.SALESFORCE_GRAPHQL_DELAY_MS),
maxDelayMs: Number(process.env.SALESFORCE_GRAPHQL_MAX_DELAY_MS),
randomDelay: true,
maxNumberAttempts: Number(process.env.SALESFORCE_GRAPHQL_MAX_RETRIES),
};
exchanges.push(retryExchange(retryOptions));
}
this.client = new Client({
url: `${instanceUrl}/services/data/${this.version}/graphql`,
exchanges,
fetchOptions: () => {
return {
headers: { authorization: `Bearer ${this.accessToken}` },
"Content-Type": "application/json",
};
},
});
this.log = logger.getSubLogger({ prefix: ["[SalesforceGraphQLClient]"] });
}
/**
* Returns the owner of an account. There are three methods we use to the find the account owner
* 1. If there is a contact with that matches the email, return the account owner
* 2. If no contact is found, then find an account that is an exact match of the email domain
* 3. If no account is found, then find contacts that match the email domain and find the account that the majority of contacts are connect to
*/
async GetAccountRecordsForRRSkip(email: string, fieldRules?: RRSkipFieldRule[]): Promise<Contact[]> {
const log = logger.getSubLogger({
prefix: [`[getAccountRecordsForRRSkip]:${email}`],
});
const emailDomain = email.split("@")[1];
const websites = this.getAllPossibleAccountWebsiteFromEmailDomain(emailDomain);
log.info(`Query against email and email domain of ${emailDomain}`);
const hasFieldRules = fieldRules && fieldRules.length > 0;
const fieldRuleFields = hasFieldRules ? Array.from(new Set(fieldRules.map((r) => r.field))) : [];
log.info(`fieldRuleFields`, fieldRuleFields);
if (hasFieldRules) {
log.info("Using dynamic query with field rule fields", safeStringify({ fieldRuleFields, fieldRules }));
}
// Use dynamic query if field rules require extra fields, otherwise use static typed query
const queryDoc = hasFieldRules
? this.buildDynamicAccountQuery(fieldRuleFields)
: GetAccountRecordsForRRSkip;
const query = await this.client.query(queryDoc, {
email,
websites,
emailDomain: `%@${emailDomain}`,
});
const queryData = query?.data;
if (query?.error) {
const errors = query.error;
if (errors.graphQLErrors.length) {
log.error("GraphQL error", errors.graphQLErrors);
}
if (errors.networkError) {
log.error("Network error", errors.networkError);
}
}
if (!queryData) {
log.error("No query data found", query?.error);
return [];
}
// If there is an existing contact, return the owner of the account
if (queryData.uiapi.query.Contact) {
const contactEdges = queryData.uiapi.query.Contact?.edges ?? [];
for (const edge of contactEdges) {
const contact = edge?.node;
if (!contact) continue;
const accountNode = contact.Account;
// Check field rules against the contact's account
if (hasFieldRules && accountNode) {
const failedRule = this.getFailingFieldRule(accountNode, fieldRules);
if (failedRule) {
log.info(`Contact ${contact.Id}'s account filtered out by field rules, checking next contact`);
continue;
}
}
log.info(`Existing contact found with id ${contact.Id}`);
return [
{
id: contact.AccountId?.value || contact.Id,
email: contact.Email?.value,
ownerId: contact.Account?.Owner?.Id,
ownerEmail: contact.Account?.Owner?.Email?.value,
recordType: SalesforceRecordEnum.ACCOUNT,
},
];
}
}
// If no contact is found, query for accounts based on website field
if (queryData.uiapi.query.Account) {
const accountEdges = queryData.uiapi.query.Account?.edges ?? [];
for (const edge of accountEdges) {
const account = edge?.node;
if (!account) continue;
// Check field rules against the account
if (hasFieldRules) {
const failedRule = this.getFailingFieldRule(account, fieldRules);
if (failedRule) {
log.info(`Account ${account.Id} filtered out by field rules, checking next account`);
continue;
}
}
log.info(
`Existing account with website that matches email domain of ${emailDomain} found with id ${account.Id}`
);
return [
{
id: account.Id,
email: "",
ownerId: account.Owner?.Id,
ownerEmail: account.Owner?.Email?.value,
recordType: SalesforceRecordEnum.ACCOUNT,
},
];
}
}
// If no account is found, find an account based on existing contacts
if (queryData.uiapi.query.relatedContacts) {
const relatedContactsResults = queryData.uiapi.query.relatedContacts?.edges;
if (!relatedContactsResults) return [];
// Collect all valid related contacts and store their Account nodes for field rule checks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountNodesByAccountId = new Map<string, Record<string, any>>();
const relatedContacts = relatedContactsResults.reduce(
(contacts, edge) => {
const node = edge?.node;
if (!node) {
log.error("A related contact query didn't include a node");
return contacts;
}
if (!node.AccountId?.value) {
log.error(`A related contact with id ${node.Id} didn't have an account id`);
return contacts;
}
if (!node.Account?.Owner?.Id) {
log.error(`A related contact with id ${node.Id} didn't have an account owner id`);
return contacts;
}
if (!node.Account?.Owner?.Email?.value) {
log.error(`A related contact with id ${node.Id} didn't have an account owner email`);
return contacts;
}
// Store the Account node for field rule evaluation later
if (!accountNodesByAccountId.has(node.AccountId.value)) {
accountNodesByAccountId.set(node.AccountId.value, node.Account);
}
contacts.push({
id: node.Id,
AccountId: node.AccountId.value,
ownerId: node.Account.Owner.Id,
ownerEmail: node.Account.Owner.Email.value,
});
return contacts;
},
[] as {
id: string;
AccountId: string;
ownerId: string;
ownerEmail: string;
}[]
);
if (relatedContacts.length === 0) {
return [];
}
// Rank accounts by contact count (most contacts first)
const accountCounts = new Map<string, number>();
for (const contact of relatedContacts) {
accountCounts.set(contact.AccountId, (accountCounts.get(contact.AccountId) || 0) + 1);
}
const rankedAccountIds = Array.from(accountCounts.entries())
.sort((a, b) => b[1] - a[1])
.map(([accountId]) => accountId);
// Try each account in order of dominance, applying field rules
for (const accountId of rankedAccountIds) {
// Check field rules against the account node
if (hasFieldRules) {
const accountNode = accountNodesByAccountId.get(accountId);
if (accountNode) {
const failedRule = this.getFailingFieldRule(accountNode, fieldRules);
if (failedRule) {
log.info(`Dominant account ${accountId} filtered out by field rules, trying next account`);
continue;
}
}
}
const contactUnderAccount = relatedContacts.find((contact) => contact.AccountId === accountId);
if (!contactUnderAccount) {
log.error(
`Could not find a contact under account id ${accountId}`,
safeStringify({ relatedContacts })
);
continue;
}
log.info(`Account found via related contacts with account id ${accountId}`);
return [
{
id: accountId,
email: "",
ownerId: contactUnderAccount.ownerId,
ownerEmail: contactUnderAccount.ownerEmail,
recordType: SalesforceRecordEnum.ACCOUNT,
},
];
}
log.info("All related contact accounts filtered out by field rules");
return [];
}
log.info("No account found for attendee");
return [];
}
/**
* Checks whether an Account node passes all configured field rules.
* Account nodes use the UIAPI pattern where field values are wrapped in { value }.
*
* @returns `null` if all rules pass, or the first failing rule if filtered.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private getFailingFieldRule(
accountNode: Record<string, any>,
fieldRules: RRSkipFieldRule[]
): RRSkipFieldRule | null {
const log = logger.getSubLogger({ prefix: [`[getFailingFieldRule]`] });
for (const rule of fieldRules) {
const fieldData = accountNode[rule.field];
const actualValue = String(fieldData?.value ?? "").toLowerCase();
const ruleValue = rule.value.toLowerCase();
const matches = actualValue === ruleValue;
log.info(
`Evaluating rule: field="${rule.field}" action="${
rule.action
}" ruleValue="${ruleValue}" actualValue="${actualValue}" rawFieldData=${safeStringify(
fieldData
)} matches=${matches}`
);
if (rule.action === RRSkipFieldRuleActionEnum.IGNORE && matches) {
log.info(`FILTERED: ignore rule matched — field "${rule.field}" equals "${ruleValue}"`);
return rule;
}
if (rule.action === RRSkipFieldRuleActionEnum.MUST_INCLUDE && !matches) {
log.info(
`FILTERED: must_include rule failed — field "${rule.field}" is "${actualValue}", expected "${ruleValue}"`
);
return rule;
}
}
log.info("All field rules passed");
return null;
}
/**
* Builds a GraphQL query string that includes additional Account fields
* for field rule evaluation. Mirrors the static GetAccountRecordsForRRSkip query
* but injects extra fields into all Account selections.
*/
private buildDynamicAccountQuery(fieldRuleFields: string[]): string {
const extraFields = fieldRuleFields.map((field) => `${field} { value }`).join("\n ");
return `
query GetAccountRecordsForRRSkipWithFields($email: Email!, $websites: [Url!]!, $emailDomain: Email!) {
uiapi {
query {
Contact(where: { Email: { eq: $email } }) {
edges {
node {
Id
Email { value }
OwnerId { value }
AccountId { value }
Account {
Website { value }
Owner {
Id
Email { value }
}
${extraFields}
}
}
}
}
Account(where: { Website: { in: $websites } }) {
edges {
node {
Id
Website { value }
Owner {
Id
Email { value }
}
${extraFields}
}
}
}
relatedContacts: Contact(
where: {
and: [
{ Email: { like: $emailDomain } }
{ Email: { ne: $email } }
{ AccountId: { ne: null } }
]
}
) {
edges {
node {
Id
Email { value }
AccountId { value }
Account {
Id
Owner {
Id
Email { value }
}
${extraFields}
}
}
}
}
}
}
}
`;
}
private getAllPossibleAccountWebsiteFromEmailDomain(emailDomain: string) {
return getAllPossibleWebsiteValuesFromEmailDomain(emailDomain);
}
}