diff --git a/package.json b/package.json index 27db2d72..bc79a35c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vesselapi/integrations", - "version": "1.0.68", + "version": "1.0.69", "description": "Vessel integrations", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/src/platforms/salesforce/client.ts b/src/platforms/salesforce/client.ts index d3661be2..b299c450 100644 --- a/src/platforms/salesforce/client.ts +++ b/src/platforms/salesforce/client.ts @@ -67,6 +67,7 @@ import { salesforceTaskRelationalSelect, SalesforceTaskUpdate, salesforceUser, + salesforceUserPermissions, } from './schemas'; const request = makeRequestFactory(async (auth, options) => { @@ -246,6 +247,17 @@ export const client = { schema: salesforceConnectOrganizationResponse, })), }, + userPermissions: request(({}) => ({ + url: `/query/`, + method: 'GET', + query: { + q: formatQuery(`SELECT FIELDS(ALL) FROM UserPermissionAccess LIMIT 1`), + }, + schema: z.object({ + records: z.array(salesforceUserPermissions), + totalSize: z.number(), + }), + })), query: request(({ query }: { query: string }) => ({ url: `/query/`, method: 'GET', diff --git a/src/platforms/salesforce/index.ts b/src/platforms/salesforce/index.ts index fc987b78..ad5c94e9 100644 --- a/src/platforms/salesforce/index.ts +++ b/src/platforms/salesforce/index.ts @@ -2,6 +2,7 @@ import { client } from '@/platforms/salesforce/client'; import * as constants from '@/platforms/salesforce/constants'; import boxIcon from '@/platforms/salesforce/logos/box'; import fullIcon from '@/platforms/salesforce/logos/full'; +import { permissions } from '@/platforms/salesforce/permissions'; import { auth, platform } from '@/sdk'; import { SalesforceAccountType, @@ -67,6 +68,7 @@ export default platform('salesforce', { categories: ['crm'], }, client, + permissions, constants, actions: { query, diff --git a/src/platforms/salesforce/permissions.ts b/src/platforms/salesforce/permissions.ts new file mode 100644 index 00000000..ee59975d --- /dev/null +++ b/src/platforms/salesforce/permissions.ts @@ -0,0 +1,93 @@ +import { first, isArray, parallel, sift, tryit } from 'radash'; +import { PlatformPermissions } from '../../sdk'; +import { client } from './client'; +import { + isSalesforceSupportedObjectType, + SalesforceSupportedObjectType, +} from './schemas'; + +// NOTE: These will be used in the UI, so they should be user-friendly. +const USER_PERMISSIONS_API_DISABLED_ERROR_MESSAGE = + 'API access is disabled. Please check with your system administrator.'; +const MISSING_OBJECTS_ERROR_MESSAGE = + 'Missing permissions. Please ensure you have access to the following objects: '; + +export const makePermissions = (): PlatformPermissions => { + return { + validate: async ({ resources, auth }) => { + const [err, result] = await tryit(client.userPermissions)(auth, {}); + if (err) { + console.warn({ + message: 'Failed to validate permissions', + metadata: { + context: err, + }, + }); + return { + errorMessage: USER_PERMISSIONS_API_DISABLED_ERROR_MESSAGE, + valid: false, + }; + } + const { + data: { records }, + } = result; + if (!first(records)?.permissionsApiEnabled) { + return { + errorMessage: USER_PERMISSIONS_API_DISABLED_ERROR_MESSAGE, + valid: false, + }; + } + + const describe = async (objectType: SalesforceSupportedObjectType) => { + try { + return await client.sobjects.describe(auth, { objectType }); + } catch (err) { + const errBody = (err as any).body; + const { error } = isArray(errBody) ? errBody[0] : errBody; + if ( + !['INVALID_TYPE', 'OBJECT_NOT_FOUND', 'NOT_FOUND'].includes(error) + ) { + console.warn({ + message: `Unknown permissions error for ${objectType}`, + error, + metadata: { + context: err, + }, + }); + } + return { + data: { createable: false, updateable: false, retrieveable: false }, + }; + } + }; + + const check = async (objectType: string) => { + // Content Notes aren't available for all accounts. + if (objectType === 'ContentNote') return null; + if (!isSalesforceSupportedObjectType(objectType)) return null; + + const { + data: { createable, updateable, retrieveable }, + } = await describe(objectType); + + // Users and ListViews only need to be retrievable. + if (objectType === 'User' && retrieveable) return null; + if (objectType === 'ListView' && retrieveable) return null; + + if (!retrieveable || !createable || !updateable) return objectType; + }; + + const missingObjects = sift(await parallel(5, resources, check)); + if (missingObjects.length === 0) { + return { errorMessage: null, valid: true }; + } + + return { + errorMessage: MISSING_OBJECTS_ERROR_MESSAGE + missingObjects.join(', '), + valid: false, + }; + }, + }; +}; + +export const permissions = makePermissions(); diff --git a/src/platforms/salesforce/schemas.ts b/src/platforms/salesforce/schemas.ts index d9c85832..7f666477 100644 --- a/src/platforms/salesforce/schemas.ts +++ b/src/platforms/salesforce/schemas.ts @@ -17,6 +17,11 @@ const requiredFields = { // - // SObjects // - +export const isSalesforceSupportedObjectType = ( + value: unknown, +): value is SalesforceSupportedObjectType => + SALESFORCE_SUPPORTED_OBJECT_TYPE.includes(value as any); + export const salesforceSupportedObjectType = z.enum( SALESFORCE_SUPPORTED_OBJECT_TYPE, ); @@ -68,6 +73,9 @@ export const salesforceField = z.object({ export const salesforceDescribeResponse = z.object({ fields: z.array(salesforceField), + createable: z.boolean(), + updateable: z.boolean(), + retrieveable: z.boolean(), }); // - @@ -85,6 +93,15 @@ export const salesforceQueryResponse = z.object({ totalSize: z.number(), }); +// - +// Permissions +// - +export const salesforceUserPermissions = z + .object({ + permissionsApiEnabled: z.boolean().nullable(), + }) + .partial(); + // - // Jobs // - @@ -988,6 +1005,9 @@ export const salesforceOAuthUrlsByAccountType: Record< export type SalesforceSupportedObjectType = (typeof SALESFORCE_SUPPORTED_OBJECT_TYPE)[number]; export type SalesforceSObject = z.infer; +export type SalesforceUserPermissions = z.infer< + typeof salesforceUserPermissions +>; export type SalesforceField = z.infer; export type SalesforceQueryRecord = z.infer; export type SalesforceUser = z.infer; diff --git a/src/sdk/platform.ts b/src/sdk/platform.ts index e498a928..92d7fb3e 100644 --- a/src/sdk/platform.ts +++ b/src/sdk/platform.ts @@ -8,6 +8,7 @@ import { PlatformClient, PlatformConstants, PlatformDisplayConfig, + PlatformPermissions, } from './types'; export type PlatformOptions< @@ -40,6 +41,7 @@ export type PlatformOptions< actions: TActions; display: PlatformDisplayConfig; client: TClient; + permissions?: PlatformPermissions; }; export const platform = < @@ -110,6 +112,7 @@ export const platform = < id, client: options.client, auth: authConfigs, + permissions: options.permissions, display: options.display, rawActions: Object.values(options.actions), constants: options.constants, diff --git a/src/sdk/types.ts b/src/sdk/types.ts index 648fbc82..f89cc50e 100644 --- a/src/sdk/types.ts +++ b/src/sdk/types.ts @@ -248,6 +248,19 @@ export type PlatformDisplayConfig = { categories: Category[]; }; +export type PlatformPermissions = { + validate: ({ + resources, + auth, + }: { + resources: string[]; + auth: Auth; + }) => Promise<{ + errorMessage: string | null; + valid: boolean; + }>; +}; + export type PlatformConstants = Record; export type Platform< TActions extends Record>, @@ -270,6 +283,7 @@ export type Platform< constants: TConstants; actions: TActions; display: PlatformDisplayConfig; + permissions?: PlatformPermissions; }; export type ActionFunction<