refactor: Use Salesforce GraphQL for account ownership routing (#20655)
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
co-authored by
Omar López
parent
1b04fe82a7
commit
17f1932467
@@ -0,0 +1,5 @@
|
||||
src/gql/fragment-masking.ts
|
||||
# src/gql/gql.ts
|
||||
src/gql/graphql.ts
|
||||
src/gql/index.ts
|
||||
schema.graphql
|
||||
@@ -0,0 +1,8 @@
|
||||
## 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.
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: "./schema.graphql",
|
||||
documents: ["./lib/graphql/documents/queries.ts"],
|
||||
ignoreNoDocuments: false,
|
||||
generates: {
|
||||
"./src/gql/": {
|
||||
preset: "client",
|
||||
},
|
||||
},
|
||||
debug: true,
|
||||
verbose: true,
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,6 @@
|
||||
const config = {
|
||||
schema: "./schema.graphql",
|
||||
documents: ["./lib/graphql/documents/queries.ts"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,6 @@
|
||||
const config = {
|
||||
schema: "./schema.graphql",
|
||||
documents: "**/lib/graphql/documents/*.{ts}",
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -78,6 +78,14 @@ const accountQueryResponse = {
|
||||
],
|
||||
};
|
||||
|
||||
const mockSalesforceGraphQLClientQuery = vi.fn();
|
||||
|
||||
vi.mock("./graphql/SalesforceGraphQLClient", () => ({
|
||||
SalesforceGraphQLClient: class {
|
||||
GetAccountRecordsForRRSkip = mockSalesforceGraphQLClientQuery;
|
||||
},
|
||||
}));
|
||||
|
||||
describe("SalesforceCRMService", () => {
|
||||
let service: SalesforceCRMService;
|
||||
let mockConnection: { query: any; sobject: any };
|
||||
@@ -484,11 +492,16 @@ describe("SalesforceCRMService", () => {
|
||||
mockAppOptions({
|
||||
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.ACCOUNT,
|
||||
});
|
||||
const querySpy = vi.spyOn(mockConnection, "query");
|
||||
querySpy
|
||||
.mockResolvedValueOnce(contactUnderAccountQueryResponse)
|
||||
.mockResolvedValueOnce(accountQueryResponse)
|
||||
.mockResolvedValueOnce(ownerQueryResponse);
|
||||
|
||||
mockSalesforceGraphQLClientQuery.mockResolvedValueOnce([
|
||||
{
|
||||
id: "acc001",
|
||||
email: "test@example.com",
|
||||
ownerId: "owner001",
|
||||
ownerEmail: "owner@example.com",
|
||||
recordType: SalesforceRecordEnum.ACCOUNT,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: "test@example.com",
|
||||
@@ -504,11 +517,6 @@ describe("SalesforceCRMService", () => {
|
||||
recordType: "Account",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(querySpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"SELECT Id, Email, OwnerId, AccountId, Account.Owner.Email, Account.Website FROM Contact WHERE Email = 'test@example.com' AND AccountId != null"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -572,19 +580,4 @@ describe("SalesforceCRMService", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAccountBasedOnEmailDomainOfContacts", () => {
|
||||
it("should return the account ID based on email domain", async () => {
|
||||
const querySpy = vi.spyOn(mockConnection, "query");
|
||||
querySpy.mockResolvedValueOnce(accountQueryResponse);
|
||||
|
||||
await service.getAccountBasedOnEmailDomainOfContacts("test@example.com");
|
||||
expect(querySpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
`SELECT Id, OwnerId, Owner.Email, Website FROM Account WHERE Website IN (${service.getAllPossibleAccountWebsiteFromEmailDomain(
|
||||
"example.com"
|
||||
)}) LIMIT 1`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
RoutingReasons,
|
||||
} from "./enums";
|
||||
import { getSalesforceAppKeys } from "./getSalesforceAppKeys";
|
||||
import { SalesforceGraphQLClient } from "./graphql/SalesforceGraphQLClient";
|
||||
import getAllPossibleWebsiteValuesFromEmailDomain from "./utils/getAllPossibleWebsiteValuesFromEmailDomain";
|
||||
import getDominantAccountId from "./utils/getDominantAccountId";
|
||||
import type { GetDominantAccountIdInput } from "./utils/getDominantAccountId";
|
||||
|
||||
class SFObjectToUpdateNotFoundError extends RetryableError {
|
||||
constructor(message: string) {
|
||||
@@ -78,6 +82,8 @@ export default class SalesforceCRMService implements CRM {
|
||||
private appOptions: z.infer<typeof appDataSchema>;
|
||||
private doNotCreateEvent = false;
|
||||
private fallbackToContact = false;
|
||||
private accessToken: string;
|
||||
private instanceUrl: string;
|
||||
|
||||
constructor(credential: CredentialPayload, appOptions: z.infer<typeof appDataSchema>, testMode = false) {
|
||||
this.integrationName = "salesforce_other_calendar";
|
||||
@@ -86,6 +92,9 @@ export default class SalesforceCRMService implements CRM {
|
||||
}
|
||||
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
this.appOptions = appOptions;
|
||||
const credentialKey = credential.key as unknown as ExtendedTokenResponse;
|
||||
this.accessToken = credentialKey.access_token;
|
||||
this.instanceUrl = credentialKey.instance_url;
|
||||
}
|
||||
|
||||
public getAppOptions() {
|
||||
@@ -394,6 +403,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
forRoundRobinSkip?: boolean;
|
||||
}) {
|
||||
const log = logger.getSubLogger({ prefix: [`[getContacts]:${emails}`] });
|
||||
|
||||
try {
|
||||
const conn = await this.conn;
|
||||
const emailArray = Array.isArray(emails) ? emails : [emails];
|
||||
@@ -416,6 +426,20 @@ export default class SalesforceCRMService implements CRM {
|
||||
})
|
||||
);
|
||||
|
||||
if (recordToSearch === SalesforceRecordEnum.ACCOUNT && forRoundRobinSkip) {
|
||||
try {
|
||||
const client = new SalesforceGraphQLClient({
|
||||
accessToken: this.accessToken,
|
||||
instanceUrl: this.instanceUrl,
|
||||
});
|
||||
|
||||
return await client.GetAccountRecordsForRRSkip(emailArray[0]);
|
||||
} catch (error) {
|
||||
log.error("Error getting account records for round robin skip", safeStringify({ error }));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Account record type
|
||||
if (recordToSearch === SalesforceRecordEnum.ACCOUNT) {
|
||||
// For an account let's assume that the first email is the one we should be querying against
|
||||
@@ -494,15 +518,6 @@ export default class SalesforceCRMService implements CRM {
|
||||
records = results.records as ContactRecord[];
|
||||
}
|
||||
|
||||
if (recordToSearch === SalesforceRecordEnum.ACCOUNT && forRoundRobinSkip && !results.records.length) {
|
||||
const attendeeEmail = emailArray[0];
|
||||
// If we can't find the exact contact, then we need to search for an account where the contacts share the same email domain
|
||||
const account = await this.getAccountBasedOnEmailDomainOfContacts(attendeeEmail);
|
||||
if (account) {
|
||||
records = [account];
|
||||
}
|
||||
}
|
||||
|
||||
if (!records.length) {
|
||||
log.info("No records found");
|
||||
return [];
|
||||
@@ -772,33 +787,8 @@ export default class SalesforceCRMService implements CRM {
|
||||
return this.doNotCreateEvent;
|
||||
}
|
||||
|
||||
private getDominantAccountId(contacts: { AccountId: string }[]) {
|
||||
const log = logger.getSubLogger({ prefix: [`[getDominantAccountId]:${contacts}`] });
|
||||
// To get the dominant AccountId we only need to iterate through half the array
|
||||
const iterateLength = Math.ceil(contacts.length / 2);
|
||||
// Store AccountId frequencies
|
||||
const accountIdCounts: { [accountId: string]: number } = {};
|
||||
|
||||
for (const contact of contacts) {
|
||||
const accountId = contact.AccountId;
|
||||
accountIdCounts[accountId] = (accountIdCounts[accountId] || 0) + 1;
|
||||
// If the number of AccountIds makes up 50% of the array length then return early
|
||||
if (accountIdCounts[accountId] > iterateLength) return accountId;
|
||||
}
|
||||
|
||||
// Else figure out which AccountId occurs the most
|
||||
let dominantAccountId;
|
||||
let highestCount = 0;
|
||||
|
||||
for (const accountId in accountIdCounts) {
|
||||
if (accountIdCounts[accountId] > highestCount) {
|
||||
highestCount = accountIdCounts[accountId];
|
||||
dominantAccountId = accountId;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Dominant AccountId", safeStringify({ dominantAccountId }));
|
||||
return dominantAccountId;
|
||||
private getDominantAccountId(contacts: GetDominantAccountIdInput) {
|
||||
return getDominantAccountId(contacts);
|
||||
}
|
||||
|
||||
private async createAttendeeRecord({
|
||||
@@ -946,14 +936,8 @@ export default class SalesforceCRMService implements CRM {
|
||||
}
|
||||
|
||||
public getAllPossibleAccountWebsiteFromEmailDomain(emailDomain: string) {
|
||||
const websites = [
|
||||
emailDomain,
|
||||
`www.${emailDomain}`,
|
||||
`http://www.${emailDomain}`,
|
||||
`http://${emailDomain}`,
|
||||
`https://www.${emailDomain}`,
|
||||
`https://${emailDomain}`,
|
||||
];
|
||||
const websites = getAllPossibleWebsiteValuesFromEmailDomain(emailDomain);
|
||||
// Format for SOQL query
|
||||
return websites.map((website) => `'${website}'`).join(", ");
|
||||
}
|
||||
|
||||
@@ -993,72 +977,6 @@ export default class SalesforceCRMService implements CRM {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public async getAccountBasedOnEmailDomainOfContacts(email: string) {
|
||||
const conn = await this.conn;
|
||||
const emailDomain = email.split("@")[1];
|
||||
const log = logger.getSubLogger({ prefix: [`[getAccountBasedOnEmailDomainOfContacts]:${email}`] });
|
||||
log.info("Querying first account matching email domain", safeStringify({ emailDomain }));
|
||||
// First check if an account has the same website as the email domain of the attendee
|
||||
const accountQuery = await conn.query(
|
||||
`SELECT Id, OwnerId, Owner.Email, Website FROM Account WHERE Website IN (${this.getAllPossibleAccountWebsiteFromEmailDomain(
|
||||
emailDomain
|
||||
)}) LIMIT 1`
|
||||
);
|
||||
|
||||
if (accountQuery.records.length > 0) {
|
||||
const account = accountQuery.records[0] as {
|
||||
Id?: string;
|
||||
OwnerId?: string;
|
||||
Owner?: { Email?: string };
|
||||
Website?: string;
|
||||
};
|
||||
|
||||
log.info(
|
||||
"Found account matching email domain",
|
||||
safeStringify({
|
||||
emailDomain,
|
||||
accountWebsite: account.Website,
|
||||
accountOwnerEmail: account.Owner?.Email,
|
||||
accountOwnerId: account.OwnerId,
|
||||
accountId: account.Id,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...account,
|
||||
Email: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to querying which account the majority of contacts are under
|
||||
const contactQuery = await conn.query(
|
||||
`SELECT Id, Email, AccountId, Account.OwnerId, Account.Owner.Email FROM Contact WHERE Email LIKE '%@${emailDomain}' AND AccountId != null`
|
||||
);
|
||||
|
||||
const contacts = contactQuery?.records as {
|
||||
AccountId: string;
|
||||
Account: { OwnerId?: string; Owner: { Email: string } };
|
||||
}[];
|
||||
if (!contacts) return;
|
||||
|
||||
const dominantAccountId = this.getDominantAccountId(contacts);
|
||||
|
||||
const contactUnderAccount = contacts.find((contact) => contact.AccountId === dominantAccountId);
|
||||
log.info("Using dominant account's owner", safeStringify({ dominantAccountId }));
|
||||
|
||||
return {
|
||||
Id: dominantAccountId,
|
||||
Email: undefined,
|
||||
OwnerId: contactUnderAccount?.Account?.OwnerId,
|
||||
Owner: {
|
||||
Email: contactUnderAccount?.Account?.Owner?.Email,
|
||||
},
|
||||
attributes: {
|
||||
type: SalesforceRecordEnum.ACCOUNT,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private setFallbackToContact(boolean: boolean) {
|
||||
this.fallbackToContact = boolean;
|
||||
}
|
||||
@@ -1149,9 +1067,9 @@ export default class SalesforceCRMService implements CRM {
|
||||
}): Promise<Record<string, any>> {
|
||||
const log = logger.getSubLogger({ prefix: [`[buildRecordUpdatePayload] ${recordId}`] });
|
||||
const writeOnRecordBody: Record<string, any> = {};
|
||||
let fieldTypeHandled = false;
|
||||
|
||||
for (const field of existingFields) {
|
||||
let fieldTypeHandled = false;
|
||||
const fieldConfig = fieldsToWriteTo[field.name];
|
||||
|
||||
if (!fieldConfig) {
|
||||
|
||||
@@ -13,8 +13,23 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import SalesforceCRMService from "../CrmService";
|
||||
import { SalesforceRecordEnum } from "../enums";
|
||||
import {
|
||||
mockValueOfAccountOwnershipQueryMatchingContact,
|
||||
mockValueOfAccountOwnershipQueryMatchingAccountWebsite,
|
||||
mockValueOfAccountOwnershipQueryMatchingRelatedContacts,
|
||||
} from "../graphql/__tests__/urqlMock";
|
||||
import { createSalesforceMock } from "./salesforceMock";
|
||||
|
||||
const mockUrqlQuery = vi.fn();
|
||||
|
||||
vi.mock("@urql/core", () => ({
|
||||
Client: class {
|
||||
query = mockUrqlQuery;
|
||||
},
|
||||
cacheExchange: vi.fn(),
|
||||
fetchExchange: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/constants", () => {
|
||||
return {
|
||||
WEBAPP_URL: "https://app.cal.com",
|
||||
@@ -116,28 +131,10 @@ describe("SalesforceCRMService", () => {
|
||||
describe("getContacts: forRoundRobinSkip=true", () => {
|
||||
const forRoundRobinSkip = true;
|
||||
it("(Lookup-1) should return contact's account owner's email as ownerEmail if the contact is found directly by email", async () => {
|
||||
mockUrqlQuery.mockResolvedValue(mockValueOfAccountOwnershipQueryMatchingContact());
|
||||
const crmService = new SalesforceCRMService(credential, appOptions);
|
||||
const contactAccountOwnerEmail = "contact-account-owner@acme.com";
|
||||
const contactOwnerEmail = "contact-owner@acme.com";
|
||||
const lookingForEmail = "test@example.com";
|
||||
|
||||
const account = salesforceMock.addAccount({
|
||||
Id: "test-account-id",
|
||||
Owner: {
|
||||
Email: contactAccountOwnerEmail,
|
||||
},
|
||||
Website: "https://anything",
|
||||
});
|
||||
|
||||
salesforceMock.addContact({
|
||||
Email: lookingForEmail,
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
AccountId: account.Id,
|
||||
Owner: {
|
||||
Email: contactOwnerEmail,
|
||||
},
|
||||
});
|
||||
const contactAccountOwnerEmail = "owner@test.com";
|
||||
const lookingForEmail = "contact@email.com";
|
||||
|
||||
const contacts = await crmService.getContacts({
|
||||
emails: lookingForEmail,
|
||||
@@ -148,21 +145,15 @@ describe("SalesforceCRMService", () => {
|
||||
expect(contacts[0].ownerEmail).toBe(contactAccountOwnerEmail);
|
||||
expect(contacts[0].email).toBe(lookingForEmail);
|
||||
expect(contacts[0].recordType).toBe(SalesforceRecordEnum.ACCOUNT);
|
||||
expect(contacts[0].id).toBe(account.Id);
|
||||
expect(contacts[0].id).toBe("accountId");
|
||||
});
|
||||
|
||||
it("(Lookup-2) should fallback to account(and use the account owner email as ownerEmail) matched by emailDomain if the contact is not found(i.e Lookup-1 fails)", async () => {
|
||||
mockUrqlQuery.mockResolvedValue(mockValueOfAccountOwnershipQueryMatchingAccountWebsite());
|
||||
const crmService = new SalesforceCRMService(credential, appOptions);
|
||||
const contactAccountOwnerEmail = "contact-account-owner@example.com";
|
||||
const contactAccountOwnerEmail = "owner@test.com";
|
||||
const emailDomain = "example.com";
|
||||
const lookingForEmail = `test@${emailDomain}`;
|
||||
const account = salesforceMock.addAccount({
|
||||
Id: "test-account-id",
|
||||
Owner: {
|
||||
Email: contactAccountOwnerEmail,
|
||||
},
|
||||
Website: `https://${emailDomain}`,
|
||||
});
|
||||
|
||||
const contacts = await crmService.getContacts({
|
||||
emails: lookingForEmail,
|
||||
@@ -179,47 +170,11 @@ describe("SalesforceCRMService", () => {
|
||||
});
|
||||
|
||||
it("(Lookup-3) should fallback to account having most number of contacts matched by emailDomain when Lookup-1 and Lookup-2 fails", async () => {
|
||||
mockUrqlQuery.mockResolvedValue(mockValueOfAccountOwnershipQueryMatchingRelatedContacts());
|
||||
const crmService = new SalesforceCRMService(credential, appOptions);
|
||||
const account1OwnerEmail = "account1-owner@example.com";
|
||||
const account2OwnerEmail = "account2-owner@example.com";
|
||||
const accountOwnerEmail = "owner1@test.com";
|
||||
const emailDomain = "example.com";
|
||||
const lookingForEmail = `test@${emailDomain}`;
|
||||
const account1 = salesforceMock.addAccount({
|
||||
Id: "test-account-id1",
|
||||
Owner: {
|
||||
Email: account1OwnerEmail,
|
||||
},
|
||||
Website: `https://anything`,
|
||||
});
|
||||
|
||||
const dominatingAccount = salesforceMock.addAccount({
|
||||
Id: "test-account-id2",
|
||||
Owner: {
|
||||
Email: account2OwnerEmail,
|
||||
},
|
||||
Website: `https://anything`,
|
||||
});
|
||||
|
||||
salesforceMock.addContact({
|
||||
Email: `some-other-contact_acc2_1@${emailDomain}`,
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
AccountId: dominatingAccount.Id,
|
||||
});
|
||||
|
||||
salesforceMock.addContact({
|
||||
Email: `some-other-contact_acc2_2@${emailDomain}`,
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
AccountId: dominatingAccount.Id,
|
||||
});
|
||||
|
||||
salesforceMock.addContact({
|
||||
Email: `some-other-contact_acc1_1@${emailDomain}`,
|
||||
FirstName: "Test",
|
||||
LastName: "User",
|
||||
AccountId: account1.Id,
|
||||
});
|
||||
|
||||
const contacts = await crmService.getContacts({
|
||||
emails: lookingForEmail,
|
||||
@@ -227,7 +182,7 @@ describe("SalesforceCRMService", () => {
|
||||
});
|
||||
|
||||
expect(contacts).toHaveLength(1);
|
||||
expect(contacts[0].ownerEmail).toBe(account2OwnerEmail);
|
||||
expect(contacts[0].ownerEmail).toBe(accountOwnerEmail);
|
||||
// Account doesn't have email
|
||||
expect(contacts[0].email).toBe("");
|
||||
expect(contacts[0].recordType).toBe(SalesforceRecordEnum.ACCOUNT);
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { Client, cacheExchange, fetchExchange } from "@urql/core";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type { Contact } from "@calcom/types/CrmService";
|
||||
|
||||
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;
|
||||
this.client = new Client({
|
||||
url: `${instanceUrl}/services/data/${this.version}/graphql`,
|
||||
exchanges: [cacheExchange, fetchExchange],
|
||||
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): 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 query = await this.client.query(GetAccountRecordsForRRSkip, {
|
||||
email,
|
||||
websites,
|
||||
emailDomain: `%@${emailDomain}`,
|
||||
});
|
||||
|
||||
const queryData = query?.data;
|
||||
|
||||
if (query?.error) {
|
||||
const errors = query.error;
|
||||
if (errors.graphQLErrors.length) {
|
||||
log.error("GraphQL error", safeStringify({ errors }));
|
||||
}
|
||||
if (errors.networkError) {
|
||||
log.error("Network error", safeStringify({ errors }));
|
||||
}
|
||||
}
|
||||
|
||||
if (!queryData) {
|
||||
log.error("No query data found", safeStringify({ error: query?.error }));
|
||||
return [];
|
||||
}
|
||||
|
||||
// If there is an existing contact, return the owner of the account
|
||||
if (queryData.uiapi.query.Contact) {
|
||||
const contact = queryData.uiapi.query.Contact?.edges?.[0]?.node;
|
||||
|
||||
if (contact) {
|
||||
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 account = queryData.uiapi.query.Account?.edges?.[0]?.node;
|
||||
if (account) {
|
||||
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 [];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - in CD/CI pipeline this will have any type
|
||||
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;
|
||||
}
|
||||
|
||||
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 }[]);
|
||||
|
||||
const dominantAccountId = getDominantAccountId(relatedContacts);
|
||||
|
||||
if (!dominantAccountId) {
|
||||
log.error(
|
||||
"Could not find dominant account id with the following contacts",
|
||||
safeStringify({ relatedContacts })
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get a contact from the dominant account
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - in CD/CI pipeline this will have any type
|
||||
const contactUnderAccount = relatedContacts.find((contact) => contact.AccountId === dominantAccountId);
|
||||
if (!contactUnderAccount) {
|
||||
log.error(
|
||||
`Could not find a contact under the dominant account id ${dominantAccountId}`,
|
||||
safeStringify({ relatedContacts })
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
log.info(`Account found via related contacts with account id ${dominantAccountId}`);
|
||||
return [
|
||||
{
|
||||
id: dominantAccountId,
|
||||
email: "",
|
||||
ownerId: contactUnderAccount.ownerId,
|
||||
ownerEmail: contactUnderAccount.ownerEmail,
|
||||
recordType: SalesforceRecordEnum.ACCOUNT,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
log.info("No account found for attendee");
|
||||
return [];
|
||||
}
|
||||
|
||||
private getAllPossibleAccountWebsiteFromEmailDomain(emailDomain: string) {
|
||||
return getAllPossibleWebsiteValuesFromEmailDomain(emailDomain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, it, vi, expect, beforeEach } from "vitest";
|
||||
|
||||
import { SalesforceRecordEnum } from "../../enums";
|
||||
import { SalesforceGraphQLClient } from "../SalesforceGraphQLClient";
|
||||
import {
|
||||
mockValueOfAccountOwnershipQueryMatchingContact,
|
||||
mockValueOfAccountOwnershipQueryMatchingAccountWebsite,
|
||||
mockValueOfAccountOwnershipQueryMatchingRelatedContacts,
|
||||
} from "./urqlMock";
|
||||
|
||||
const mockUrqlQuery = vi.fn();
|
||||
|
||||
vi.mock("@urql/core", () => ({
|
||||
Client: class {
|
||||
query = mockUrqlQuery;
|
||||
},
|
||||
cacheExchange: vi.fn(),
|
||||
fetchExchange: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("SalesforceGraphQLClient", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return the account owner if a contact exists", async () => {
|
||||
mockUrqlQuery.mockResolvedValue(mockValueOfAccountOwnershipQueryMatchingContact());
|
||||
|
||||
const client = new SalesforceGraphQLClient({ accessToken: "", instanceUrl: "" });
|
||||
|
||||
const owner = await client.GetAccountRecordsForRRSkip("test@example.com");
|
||||
|
||||
expect(owner).toEqual([
|
||||
{
|
||||
id: "accountId",
|
||||
email: "contact@email.com",
|
||||
ownerId: "ownerId",
|
||||
ownerEmail: "owner@test.com",
|
||||
recordType: SalesforceRecordEnum.ACCOUNT,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return the account owner if email domain matches account website", async () => {
|
||||
mockUrqlQuery.mockResolvedValue(mockValueOfAccountOwnershipQueryMatchingAccountWebsite());
|
||||
|
||||
const client = new SalesforceGraphQLClient({ accessToken: "", instanceUrl: "" });
|
||||
|
||||
const owner = await client.GetAccountRecordsForRRSkip("test@example.com");
|
||||
|
||||
expect(owner).toEqual([
|
||||
{
|
||||
id: "accountId",
|
||||
email: "",
|
||||
ownerId: "ownerId",
|
||||
ownerEmail: "owner@test.com",
|
||||
recordType: SalesforceRecordEnum.ACCOUNT,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return the account owner based on related contacts", async () => {
|
||||
mockUrqlQuery.mockResolvedValue(mockValueOfAccountOwnershipQueryMatchingRelatedContacts());
|
||||
|
||||
const client = new SalesforceGraphQLClient({ accessToken: "", instanceUrl: "" });
|
||||
|
||||
const owner = await client.GetAccountRecordsForRRSkip("test@example.com");
|
||||
|
||||
expect(owner).toEqual([
|
||||
{
|
||||
id: "accountId1",
|
||||
email: "",
|
||||
ownerId: "owner1",
|
||||
ownerEmail: "owner1@test.com",
|
||||
recordType: SalesforceRecordEnum.ACCOUNT,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
export const generateMockResponse = (children: any) => {
|
||||
return {
|
||||
data: {
|
||||
uiapi: {
|
||||
query: {
|
||||
...children,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const mockValueOfAccountOwnershipQueryMatchingContact = () => {
|
||||
return generateMockResponse({
|
||||
Contact: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
Id: "contactId",
|
||||
Email: {
|
||||
value: "contact@email.com",
|
||||
},
|
||||
AccountId: {
|
||||
value: "accountId",
|
||||
},
|
||||
Account: {
|
||||
Owner: {
|
||||
Id: "ownerId",
|
||||
Email: {
|
||||
value: "owner@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// This shouldn't be hit
|
||||
Account: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
Id: "accountId",
|
||||
Owner: {
|
||||
Id: "ownerId",
|
||||
Email: {
|
||||
value: "owner@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const mockValueOfAccountOwnershipQueryMatchingAccountWebsite = () => {
|
||||
return generateMockResponse({
|
||||
Contact: {
|
||||
edges: [],
|
||||
},
|
||||
Account: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
Id: "accountId",
|
||||
Owner: {
|
||||
Id: "ownerId",
|
||||
Email: {
|
||||
value: "owner@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const mockValueOfAccountOwnershipQueryMatchingRelatedContacts = () => {
|
||||
return generateMockResponse({
|
||||
Contact: {
|
||||
edges: [],
|
||||
},
|
||||
Account: {
|
||||
edges: [],
|
||||
},
|
||||
relatedContacts: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
Id: "contact1",
|
||||
AccountId: {
|
||||
value: "accountId1",
|
||||
},
|
||||
Account: {
|
||||
Owner: {
|
||||
Id: "owner1",
|
||||
Email: {
|
||||
value: "owner1@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
Id: "contact2",
|
||||
AccountId: {
|
||||
value: "accountId2",
|
||||
},
|
||||
Account: {
|
||||
Owner: {
|
||||
Id: "owner2",
|
||||
Email: {
|
||||
value: "owner2@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
Id: "contact3",
|
||||
AccountId: {
|
||||
value: "accountId2",
|
||||
},
|
||||
Account: {
|
||||
Owner: {
|
||||
Id: "owner2",
|
||||
Email: {
|
||||
value: "owner2@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
Id: "contact4",
|
||||
AccountId: {
|
||||
value: "accountId1",
|
||||
},
|
||||
Account: {
|
||||
Owner: {
|
||||
Id: "owner1",
|
||||
Email: {
|
||||
value: "owner1@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
Id: "contact5",
|
||||
AccountId: {
|
||||
value: "accountId1",
|
||||
},
|
||||
Account: {
|
||||
Owner: {
|
||||
Id: "owner1",
|
||||
Email: {
|
||||
value: "owner1@test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { graphql } from "../../../src/gql/gql";
|
||||
|
||||
export const GetAccountRecordsForRRSkip = graphql(`
|
||||
query GetAccountRecordsForRRSkip($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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Account(where: { Website: { in: $websites } }) {
|
||||
edges {
|
||||
node {
|
||||
Id
|
||||
Website {
|
||||
value
|
||||
}
|
||||
Owner {
|
||||
Id
|
||||
Email {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import getAllPossibleWebsiteValuesFromEmailDomain from "../getAllPossibleWebsiteValuesFromEmailDomain";
|
||||
|
||||
describe("getAllPossibleWebsiteValuesFromEmailDomain", () => {
|
||||
it("should return all possible website values from email domain", () => {
|
||||
const emailDomain = "example.com";
|
||||
|
||||
const websiteValues = getAllPossibleWebsiteValuesFromEmailDomain(emailDomain);
|
||||
|
||||
expect(websiteValues).toEqual([
|
||||
emailDomain,
|
||||
`www.${emailDomain}`,
|
||||
`http://www.${emailDomain}`,
|
||||
`http://${emailDomain}`,
|
||||
`https://www.${emailDomain}`,
|
||||
`https://${emailDomain}`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import getDominantAccountId from "../getDominantAccountId";
|
||||
|
||||
describe("getDominantAccountId", () => {
|
||||
it("should return the dominant account ID", () => {
|
||||
const accounts = [
|
||||
{ AccountId: "acc001" },
|
||||
{ AccountId: "acc003" },
|
||||
{ AccountId: "acc002" },
|
||||
{ AccountId: "acc003" },
|
||||
];
|
||||
|
||||
const dominantAccountId = getDominantAccountId(accounts);
|
||||
|
||||
expect(dominantAccountId).toEqual("acc003");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function getAllPossibleWebsiteValuesFromEmailDomain(emailDomain: string) {
|
||||
return [
|
||||
emailDomain,
|
||||
`www.${emailDomain}`,
|
||||
`http://www.${emailDomain}`,
|
||||
`http://${emailDomain}`,
|
||||
`https://www.${emailDomain}`,
|
||||
`https://${emailDomain}`,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
|
||||
export type GetDominantAccountIdInput = {
|
||||
AccountId: string;
|
||||
}[];
|
||||
|
||||
export default function getDominantAccountId(contacts: GetDominantAccountIdInput) {
|
||||
const log = logger.getSubLogger({ prefix: [`[getDominantAccountId]:${contacts}`] });
|
||||
// To get the dominant AccountId we only need to iterate through half the array
|
||||
const iterateLength = Math.ceil(contacts.length / 2);
|
||||
// Store AccountId frequencies
|
||||
const accountIdCounts: { [accountId: string]: number } = {};
|
||||
|
||||
for (const contact of contacts) {
|
||||
const accountId = contact.AccountId;
|
||||
accountIdCounts[accountId] = (accountIdCounts[accountId] || 0) + 1;
|
||||
// If the number of AccountIds makes up 50% of the array length then return early
|
||||
if (accountIdCounts[accountId] > iterateLength) return accountId;
|
||||
}
|
||||
|
||||
// Else figure out which AccountId occurs the most
|
||||
let dominantAccountId;
|
||||
let highestCount = 0;
|
||||
|
||||
for (const accountId in accountIdCounts) {
|
||||
if (accountIdCounts[accountId] > highestCount) {
|
||||
highestCount = accountIdCounts[accountId];
|
||||
dominantAccountId = accountId;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Dominant AccountId", safeStringify({ dominantAccountId }));
|
||||
return dominantAccountId;
|
||||
}
|
||||
@@ -4,13 +4,23 @@
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"description": "Salesforce (Sales Cloud) is a cloud-based application designed to help your salespeople sell smarter and faster by centralizing customer information, logging their interactions with your company, and automating many of the tasks salespeople do every day.",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@jetstreamapp/soql-parser-js": "^6.1.0",
|
||||
"@jsforce/jsforce-node": "^3.6.3"
|
||||
"@jsforce/jsforce-node": "^3.6.3",
|
||||
"@urql/core": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
"@calcom/types": "*",
|
||||
"@graphql-codegen/cli": "^5.0.5",
|
||||
"@parcel/watcher": "^2.5.1",
|
||||
"graphql-config": "^5.1.3",
|
||||
"graphql-introspection-json-to-sdl": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable */
|
||||
// import * as types from './graphql';
|
||||
import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
|
||||
|
||||
import * as types from "./types";
|
||||
|
||||
/**
|
||||
* Map of all GraphQL operations in the project.
|
||||
*
|
||||
* This map has several performance disadvantages:
|
||||
* 1. It is not tree-shakeable, so it will include all operations in the project.
|
||||
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
|
||||
* 3. It does not support dead code elimination, so it will add unused operations.
|
||||
*
|
||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
|
||||
*/
|
||||
type Documents = {
|
||||
"\n query GetAccountRecordsForRRSkip($email: Email!, $websites: [Url!]!, $emailDomain: Email!) {\n uiapi {\n query {\n Contact(where: { Email: { eq: $email } }) {\n edges {\n node {\n Id\n Email {\n value\n }\n OwnerId {\n value\n }\n AccountId {\n value\n }\n Account {\n Website {\n value\n }\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n }\n Account(where: { Website: { in: $websites } }) {\n edges {\n node {\n Id\n Website {\n value\n }\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n relatedContacts: Contact(\n where: {\n and: [{ Email: { like: $emailDomain } }, { Email: { ne: $email } }, { AccountId: { ne: null } }]\n }\n ) {\n edges {\n node {\n Id\n Email {\n value\n }\n AccountId {\n value\n }\n Account {\n Id\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": typeof types.GetAccountRecordsForRrSkipDocument;
|
||||
};
|
||||
const documents: Documents = {
|
||||
"\n query GetAccountRecordsForRRSkip($email: Email!, $websites: [Url!]!, $emailDomain: Email!) {\n uiapi {\n query {\n Contact(where: { Email: { eq: $email } }) {\n edges {\n node {\n Id\n Email {\n value\n }\n OwnerId {\n value\n }\n AccountId {\n value\n }\n Account {\n Website {\n value\n }\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n }\n Account(where: { Website: { in: $websites } }) {\n edges {\n node {\n Id\n Website {\n value\n }\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n relatedContacts: Contact(\n where: {\n and: [{ Email: { like: $emailDomain } }, { Email: { ne: $email } }, { AccountId: { ne: null } }]\n }\n ) {\n edges {\n node {\n Id\n Email {\n value\n }\n AccountId {\n value\n }\n Account {\n Id\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n }\n }\n }\n }\n":
|
||||
types.GetAccountRecordsForRrSkipDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
|
||||
* ```
|
||||
*
|
||||
* The query argument is unknown!
|
||||
* Please regenerate the types.
|
||||
*/
|
||||
export function graphql(source: string): unknown;
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: "\n query GetAccountRecordsForRRSkip($email: Email!, $websites: [Url!]!, $emailDomain: Email!) {\n uiapi {\n query {\n Contact(where: { Email: { eq: $email } }) {\n edges {\n node {\n Id\n Email {\n value\n }\n OwnerId {\n value\n }\n AccountId {\n value\n }\n Account {\n Website {\n value\n }\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n }\n Account(where: { Website: { in: $websites } }) {\n edges {\n node {\n Id\n Website {\n value\n }\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n relatedContacts: Contact(\n where: {\n and: [{ Email: { like: $emailDomain } }, { Email: { ne: $email } }, { AccountId: { ne: null } }]\n }\n ) {\n edges {\n node {\n Id\n Email {\n value\n }\n AccountId {\n value\n }\n Account {\n Id\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"
|
||||
): (typeof documents)["\n query GetAccountRecordsForRRSkip($email: Email!, $websites: [Url!]!, $emailDomain: Email!) {\n uiapi {\n query {\n Contact(where: { Email: { eq: $email } }) {\n edges {\n node {\n Id\n Email {\n value\n }\n OwnerId {\n value\n }\n AccountId {\n value\n }\n Account {\n Website {\n value\n }\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n }\n Account(where: { Website: { in: $websites } }) {\n edges {\n node {\n Id\n Website {\n value\n }\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n relatedContacts: Contact(\n where: {\n and: [{ Email: { like: $emailDomain } }, { Email: { ne: $email } }, { AccountId: { ne: null } }]\n }\n ) {\n edges {\n node {\n Id\n Email {\n value\n }\n AccountId {\n value\n }\n Account {\n Id\n Owner {\n Id\n Email {\n value\n }\n }\n }\n }\n }\n }\n }\n }\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
}
|
||||
|
||||
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode<
|
||||
infer TType,
|
||||
any
|
||||
>
|
||||
? TType
|
||||
: never;
|
||||
@@ -0,0 +1,542 @@
|
||||
import type { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
|
||||
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
export type Scalars = {
|
||||
ID: { input: string; output: string };
|
||||
String: { input: string; output: string };
|
||||
Boolean: { input: boolean; output: boolean };
|
||||
Int: { input: number; output: number };
|
||||
Float: { input: number; output: number };
|
||||
Base64: { input: any; output: any };
|
||||
Currency: { input: any; output: any };
|
||||
Date: { input: any; output: any };
|
||||
DateTime: { input: any; output: any };
|
||||
Double: { input: any; output: any };
|
||||
Email: { input: any; output: any };
|
||||
EncryptedString: { input: any; output: any };
|
||||
/** Can be set to an ID or a Reference to the result of another mutation operation. */
|
||||
IdOrRef: { input: any; output: any };
|
||||
JSON: { input: any; output: any };
|
||||
Latitude: { input: any; output: any };
|
||||
/** Long type */
|
||||
Long: { input: any; output: any };
|
||||
LongTextArea: { input: any; output: any };
|
||||
Longitude: { input: any; output: any };
|
||||
MultiPicklist: { input: any; output: any };
|
||||
Percent: { input: any; output: any };
|
||||
PhoneNumber: { input: any; output: any };
|
||||
Picklist: { input: any; output: any };
|
||||
RichTextArea: { input: any; output: any };
|
||||
TextArea: { input: any; output: any };
|
||||
Time: { input: any; output: any };
|
||||
Url: { input: any; output: any };
|
||||
};
|
||||
|
||||
export type GetAccountRecordsForRrSkipQueryVariables = Exact<{
|
||||
email: Scalars["Email"]["input"];
|
||||
websites: Array<Scalars["Url"]["input"]> | Scalars["Url"]["input"];
|
||||
emailDomain: Scalars["Email"]["input"];
|
||||
}>;
|
||||
export type GetAccountRecordsForRrSkipQuery = {
|
||||
__typename?: "Query";
|
||||
uiapi: {
|
||||
__typename?: "UIAPI";
|
||||
query: {
|
||||
__typename?: "RecordQuery";
|
||||
Contact?: {
|
||||
__typename?: "ContactConnection";
|
||||
edges?: Array<{
|
||||
__typename?: "ContactEdge";
|
||||
node?: {
|
||||
__typename?: "Contact";
|
||||
Id: string;
|
||||
Email?: { __typename?: "EmailValue"; value?: any | null } | null;
|
||||
OwnerId?: { __typename?: "IDValue"; value?: string | null } | null;
|
||||
AccountId?: { __typename?: "IDValue"; value?: string | null } | null;
|
||||
Account?: {
|
||||
__typename?: "Account";
|
||||
Website?: { __typename?: "UrlValue"; value?: any | null } | null;
|
||||
Owner?: {
|
||||
__typename?: "User";
|
||||
Id: string;
|
||||
Email?: { __typename?: "EmailValue"; value?: any | null } | null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null> | null;
|
||||
} | null;
|
||||
Account?: {
|
||||
__typename?: "AccountConnection";
|
||||
edges?: Array<{
|
||||
__typename?: "AccountEdge";
|
||||
node?: {
|
||||
__typename?: "Account";
|
||||
Id: string;
|
||||
Website?: { __typename?: "UrlValue"; value?: any | null } | null;
|
||||
Owner?: {
|
||||
__typename?: "User";
|
||||
Id: string;
|
||||
Email?: { __typename?: "EmailValue"; value?: any | null } | null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null> | null;
|
||||
} | null;
|
||||
relatedContacts?: {
|
||||
__typename?: "ContactConnection";
|
||||
edges?: Array<{
|
||||
__typename?: "ContactEdge";
|
||||
node?: {
|
||||
__typename?: "Contact";
|
||||
Id: string;
|
||||
Email?: { __typename?: "EmailValue"; value?: any | null } | null;
|
||||
AccountId?: { __typename?: "IDValue"; value?: string | null } | null;
|
||||
Account?: {
|
||||
__typename?: "Account";
|
||||
Id: string;
|
||||
Owner?: {
|
||||
__typename?: "User";
|
||||
Id: string;
|
||||
Email?: { __typename?: "EmailValue"; value?: any | null } | null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null> | null;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
export const GetAccountRecordsForRrSkipDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "OperationDefinition",
|
||||
operation: "query",
|
||||
name: { kind: "Name", value: "GetAccountRecordsForRRSkip" },
|
||||
variableDefinitions: [
|
||||
{
|
||||
kind: "VariableDefinition",
|
||||
variable: { kind: "Variable", name: { kind: "Name", value: "email" } },
|
||||
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "Email" } } },
|
||||
},
|
||||
{
|
||||
kind: "VariableDefinition",
|
||||
variable: { kind: "Variable", name: { kind: "Name", value: "websites" } },
|
||||
type: {
|
||||
kind: "NonNullType",
|
||||
type: {
|
||||
kind: "ListType",
|
||||
type: {
|
||||
kind: "NonNullType",
|
||||
type: { kind: "NamedType", name: { kind: "Name", value: "Url" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "VariableDefinition",
|
||||
variable: { kind: "Variable", name: { kind: "Name", value: "emailDomain" } },
|
||||
type: { kind: "NonNullType", type: { kind: "NamedType", name: { kind: "Name", value: "Email" } } },
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "uiapi" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "query" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Contact" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "where" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "Email" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "eq" },
|
||||
value: { kind: "Variable", name: { kind: "Name", value: "email" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "edges" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "node" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "Id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Email" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "value" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "OwnerId" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "value" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "AccountId" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "value" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Account" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Website" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "value" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Owner" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "Id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Email" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "value" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Account" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "where" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "Website" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "in" },
|
||||
value: {
|
||||
kind: "Variable",
|
||||
name: { kind: "Name", value: "websites" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "edges" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "node" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "Id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Website" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "value" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Owner" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "Id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Email" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "value" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
alias: { kind: "Name", value: "relatedContacts" },
|
||||
name: { kind: "Name", value: "Contact" },
|
||||
arguments: [
|
||||
{
|
||||
kind: "Argument",
|
||||
name: { kind: "Name", value: "where" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "and" },
|
||||
value: {
|
||||
kind: "ListValue",
|
||||
values: [
|
||||
{
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "Email" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "like" },
|
||||
value: {
|
||||
kind: "Variable",
|
||||
name: { kind: "Name", value: "emailDomain" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "Email" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "ne" },
|
||||
value: {
|
||||
kind: "Variable",
|
||||
name: { kind: "Name", value: "email" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "AccountId" },
|
||||
value: {
|
||||
kind: "ObjectValue",
|
||||
fields: [
|
||||
{
|
||||
kind: "ObjectField",
|
||||
name: { kind: "Name", value: "ne" },
|
||||
value: { kind: "NullValue" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "edges" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "node" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "Id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Email" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "value" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "AccountId" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "value" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Account" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "Id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Owner" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "Id" } },
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "Email" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "value" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<GetAccountRecordsForRrSkipQuery, GetAccountRecordsForRrSkipQueryVariables>;
|
||||
Reference in New Issue
Block a user