mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
fix(Salesforce Node): Use server-side search for owner selectors (#32815)
Co-authored-by: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
06ceccbb23
commit
8027463c34
@@ -272,15 +272,38 @@ export const accountFields: INodeProperties[] = [
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the account. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the account',
|
||||
},
|
||||
{
|
||||
displayName: 'Parent ID',
|
||||
@@ -553,15 +576,38 @@ export const accountFields: INodeProperties[] = [
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'ownerId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the account. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the account',
|
||||
},
|
||||
{
|
||||
displayName: 'Parent ID',
|
||||
@@ -875,15 +921,38 @@ export const accountFields: INodeProperties[] = [
|
||||
'Whether true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'ownerId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the user who owns the note. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the note',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -129,15 +129,38 @@ export const attachmentFields: INodeProperties[] = [
|
||||
'Whether this record is viewable only by the owner and administrators (true) or viewable by all otherwise-allowed users (false)',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the User who owns the attachment. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the attachment',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -204,15 +227,38 @@ export const attachmentFields: INodeProperties[] = [
|
||||
'Required. Name of the attached file. Maximum size is 255 characters. Label is File Name.',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the User who owns the attachment. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the attachment',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -171,15 +171,38 @@ export const caseFields: INodeProperties[] = [
|
||||
'The source of the case, such as Email, Phone, or Web. Label is Case Origin. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCaseOwners',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the case. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user or queue...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchCaseOwners',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'Owner ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user or queue that owns the case',
|
||||
},
|
||||
{
|
||||
displayName: 'Parent ID',
|
||||
@@ -381,15 +404,38 @@ export const caseFields: INodeProperties[] = [
|
||||
'The source of the case, such as Email, Phone, or Web. Label is Case Origin. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getCaseOwners',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the case. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user or queue...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchCaseOwners',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'Owner ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user or queue that owns the case',
|
||||
},
|
||||
{
|
||||
displayName: 'Parent ID',
|
||||
|
||||
@@ -383,15 +383,38 @@ export const contactFields: INodeProperties[] = [
|
||||
description: 'Street for alternate address',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the contact. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
@@ -725,15 +748,38 @@ export const contactFields: INodeProperties[] = [
|
||||
description: 'Street for alternate address',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the contact. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the contact',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
@@ -1061,15 +1107,38 @@ export const contactFields: INodeProperties[] = [
|
||||
'Whether only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the user who owns the note. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the note',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -86,15 +86,38 @@ export const documentFields: INodeProperties[] = [
|
||||
description: 'ID of the object you want to link this document to',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'ownerId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the owner of this document. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns this document',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -51,6 +51,20 @@ function getOptions(
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges extra request options onto the base options. Unlike a plain
|
||||
* `Object.assign`, headers are merged rather than replaced, so a caller can add
|
||||
* a header (e.g. `Sforce-Query-Options`) without dropping the `Content-Type`
|
||||
* header the request builder already set.
|
||||
*/
|
||||
function assignOptions(options: IRequestOptions | IHttpRequestOptions, option: IDataObject): void {
|
||||
const { headers: extraHeaders, ...rest } = option;
|
||||
if (extraHeaders) {
|
||||
options.headers = { ...options.headers, ...(extraHeaders as IDataObject) };
|
||||
}
|
||||
Object.assign(options, rest);
|
||||
}
|
||||
|
||||
export async function salesforceApiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
@@ -84,7 +98,7 @@ export async function salesforceApiRequest(
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
Object.assign(options, option);
|
||||
assignOptions(options, option);
|
||||
this.logger.debug(
|
||||
`Authentication for "Salesforce" node is using "jwt". Invoking URI ${options.url}`,
|
||||
);
|
||||
@@ -106,7 +120,7 @@ export async function salesforceApiRequest(
|
||||
this.logger.debug(
|
||||
`Authentication for "Salesforce" node is using "OAuth2". Invoking URI ${options.uri}`,
|
||||
);
|
||||
Object.assign(options, option);
|
||||
assignOptions(options, option);
|
||||
|
||||
return await this.helpers.requestOAuth2.call(this, credentialsType, options);
|
||||
}
|
||||
@@ -163,6 +177,23 @@ export async function salesforceApiRequestAllItems(
|
||||
return returnData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner fields accept two shapes for backward compatibility: a legacy `options`
|
||||
* field stored its value as a raw string, while the current `resourceLocator`
|
||||
* field stores `{ __rl, mode, value }`. This normalises both to the string id,
|
||||
* or `undefined` when empty/missing.
|
||||
*/
|
||||
export function getResourceLocatorValue(value: unknown): string | undefined {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'object' && '__rl' in value) {
|
||||
const inner = (value as { value?: unknown }).value;
|
||||
if (inner === undefined || inner === null || inner === '') return undefined;
|
||||
return String(inner);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the given options alphabetically
|
||||
*
|
||||
|
||||
@@ -307,15 +307,38 @@ export const leadFields: INodeProperties[] = [
|
||||
description: 'Number of employees at the lead’s company. Label is Employees.',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLeadOwners',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the lead. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user or queue...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchLeadOwners',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'Owner ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user or queue that owns the lead',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
@@ -598,15 +621,38 @@ export const leadFields: INodeProperties[] = [
|
||||
description: 'Number of employees at the lead’s company. Label is Employees.',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLeadOwners',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the lead. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user or queue...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchLeadOwners',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'Owner ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user or queue that owns the lead',
|
||||
},
|
||||
{
|
||||
displayName: 'Postal Code',
|
||||
@@ -965,15 +1011,38 @@ export const leadFields: INodeProperties[] = [
|
||||
'Whether true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the user who owns the note. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the note',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -267,15 +267,38 @@ export const opportunityFields: INodeProperties[] = [
|
||||
description: 'Description of next task in closing opportunity. Limit: 255 characters.',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the opportunity. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the opportunity',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
@@ -469,15 +492,38 @@ export const opportunityFields: INodeProperties[] = [
|
||||
description: 'Description of next task in closing opportunity. Limit: 255 characters.',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'The owner of the opportunity. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the opportunity',
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
@@ -753,15 +799,38 @@ export const opportunityFields: INodeProperties[] = [
|
||||
'Whether true, only the note owner or a user with the “Modify All Data” permission can view the note or query it via the API',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the user who owns the note. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the note',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodeListSearchItems,
|
||||
INodeListSearchResult,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
@@ -26,6 +28,7 @@ import { flowFields, flowOperations } from './FlowDescription';
|
||||
import {
|
||||
escapeSoqlString,
|
||||
getQuery,
|
||||
getResourceLocatorValue,
|
||||
salesforceApiRequest,
|
||||
salesforceApiRequestAllItems,
|
||||
sortOptions,
|
||||
@@ -40,6 +43,75 @@ import { taskFields, taskOperations } from './TaskDescription';
|
||||
import type { ITask } from './TaskInterface';
|
||||
import { userFields, userOperations } from './UserDescription';
|
||||
|
||||
// 200 is Salesforce's minimum query batchSize; smaller values are ignored.
|
||||
const USER_SEARCH_PAGE_SIZE = 200;
|
||||
|
||||
async function searchOwners(
|
||||
this: ILoadOptionsFunctions,
|
||||
queueSobjectType: 'Case' | 'Lead' | undefined,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const results: INodeListSearchItems[] = [];
|
||||
|
||||
if (queueSobjectType && !paginationToken) {
|
||||
// Owner queues have no SOQL typeahead, so fetch them all once (as the legacy
|
||||
// owner loaders did) and filter/sort in-memory alongside the users.
|
||||
const queueRecords = (await salesforceApiRequestAllItems.call(
|
||||
this,
|
||||
'records',
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
{
|
||||
q: `SELECT Queue.Id, Queue.Name FROM QueuesObject WHERE Queue.Type = 'Queue' AND SobjectType = '${queueSobjectType}'`,
|
||||
},
|
||||
)) as Array<{ Queue: { Id: string; Name: string } }>;
|
||||
const lowerFilter = (filter ?? '').toLowerCase();
|
||||
const queues = queueRecords
|
||||
.filter((record) => !lowerFilter || record.Queue.Name.toLowerCase().includes(lowerFilter))
|
||||
.map((record) => ({ name: `Queue: ${record.Queue.Name}`, value: record.Queue.Id }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
results.push(...queues);
|
||||
}
|
||||
|
||||
let userResponse: { records?: Array<{ Id: string; Name: string }>; nextRecordsUrl?: string };
|
||||
if (paginationToken) {
|
||||
// `nextRecordsUrl` is a full Salesforce path like
|
||||
// `/services/data/v59.0/query/01g4o00000abcdef-2000`. salesforceApiRequest
|
||||
// re-prefixes the API base itself, so we pass only the `/query/<locator>` suffix.
|
||||
const locator = paginationToken.split('/').pop();
|
||||
userResponse = (await salesforceApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/query/${locator}`,
|
||||
)) as typeof userResponse;
|
||||
} else {
|
||||
const escapedFilter = filter ? escapeSoqlString(filter) : '';
|
||||
const whereClause = escapedFilter ? `WHERE Name LIKE '%${escapedFilter}%' ` : '';
|
||||
// No LIMIT: it would cap the result below the batch size and suppress the
|
||||
// nextRecordsUrl cursor. batchSize bounds the page instead.
|
||||
userResponse = (await salesforceApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
{ q: `SELECT Id, Name FROM User ${whereClause}ORDER BY Name` },
|
||||
undefined,
|
||||
{ headers: { 'Sforce-Query-Options': `batchSize=${USER_SEARCH_PAGE_SIZE}` } },
|
||||
)) as typeof userResponse;
|
||||
}
|
||||
|
||||
// Prefix users with "User: " only when queues share this result (mirrors the legacy
|
||||
// labels); a list of just users — including any paginated page — stays unprefixed.
|
||||
const userPrefix = results.length > 0 ? 'User: ' : '';
|
||||
for (const user of userResponse.records ?? []) {
|
||||
results.push({ name: `${userPrefix}${user.Name}`, value: user.Id });
|
||||
}
|
||||
|
||||
return { results, paginationToken: userResponse.nextRecordsUrl };
|
||||
}
|
||||
|
||||
export class Salesforce implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Salesforce',
|
||||
@@ -218,124 +290,6 @@ export class Salesforce implements INodeType {
|
||||
sortOptions(returnData);
|
||||
return returnData;
|
||||
},
|
||||
// Get all the users to display them to user so that they can
|
||||
// select them easily
|
||||
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const qs = {
|
||||
q: 'SELECT id, Name FROM User',
|
||||
};
|
||||
const users = await salesforceApiRequestAllItems.call(
|
||||
this,
|
||||
'records',
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
for (const user of users) {
|
||||
const userName = user.Name;
|
||||
const userId = user.Id;
|
||||
returnData.push({
|
||||
name: userName,
|
||||
value: userId,
|
||||
});
|
||||
}
|
||||
sortOptions(returnData);
|
||||
return returnData;
|
||||
},
|
||||
// Get all the users and case queues to display them to user so that they can
|
||||
// select them easily
|
||||
async getCaseOwners(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const qsQueues = {
|
||||
q: "SELECT Queue.Id, Queue.Name FROM QueuesObject where Queue.Type='Queue' and SobjectType = 'Case'",
|
||||
};
|
||||
const queues = await salesforceApiRequestAllItems.call(
|
||||
this,
|
||||
'records',
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
qsQueues,
|
||||
);
|
||||
for (const queue of queues) {
|
||||
const queueName = queue.Queue.Name;
|
||||
const queueId = queue.Queue.Id;
|
||||
returnData.push({
|
||||
name: `Queue: ${queueName}`,
|
||||
value: queueId,
|
||||
});
|
||||
}
|
||||
const qsUsers = {
|
||||
q: 'SELECT id, Name FROM User',
|
||||
};
|
||||
const users = await salesforceApiRequestAllItems.call(
|
||||
this,
|
||||
'records',
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
qsUsers,
|
||||
);
|
||||
const userPrefix = returnData.length > 0 ? 'User: ' : '';
|
||||
for (const user of users) {
|
||||
const userName = user.Name;
|
||||
const userId = user.Id;
|
||||
returnData.push({
|
||||
name: userPrefix + (userName as string),
|
||||
value: userId,
|
||||
});
|
||||
}
|
||||
sortOptions(returnData);
|
||||
return returnData;
|
||||
},
|
||||
// Get all the users and lead queues to display them to user so that they can
|
||||
// select them easily
|
||||
async getLeadOwners(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const qsQueues = {
|
||||
q: "SELECT Queue.Id, Queue.Name FROM QueuesObject where Queue.Type='Queue' and SobjectType = 'Lead'",
|
||||
};
|
||||
const queues = await salesforceApiRequestAllItems.call(
|
||||
this,
|
||||
'records',
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
qsQueues,
|
||||
);
|
||||
for (const queue of queues) {
|
||||
const queueName = queue.Queue.Name;
|
||||
const queueId = queue.Queue.Id;
|
||||
returnData.push({
|
||||
name: `Queue: ${queueName}`,
|
||||
value: queueId,
|
||||
});
|
||||
}
|
||||
const qsUsers = {
|
||||
q: 'SELECT id, Name FROM User',
|
||||
};
|
||||
const users = await salesforceApiRequestAllItems.call(
|
||||
this,
|
||||
'records',
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
qsUsers,
|
||||
);
|
||||
const userPrefix = returnData.length > 0 ? 'User: ' : '';
|
||||
for (const user of users) {
|
||||
const userName = user.Name;
|
||||
const userId = user.Id;
|
||||
returnData.push({
|
||||
name: userPrefix + (userName as string),
|
||||
value: userId,
|
||||
});
|
||||
}
|
||||
sortOptions(returnData);
|
||||
return returnData;
|
||||
},
|
||||
// Get all the lead sources to display them to user so that they can
|
||||
// select them easily
|
||||
async getLeadSources(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
@@ -1040,6 +994,32 @@ export class Salesforce implements INodeType {
|
||||
// return returnData;
|
||||
// },
|
||||
},
|
||||
listSearch: {
|
||||
// Server-side typeahead for the owner (User) selectors.
|
||||
async searchUsers(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
return await searchOwners.call(this, undefined, filter, paginationToken);
|
||||
},
|
||||
// Owner selector for Case fields — users plus case queues.
|
||||
async searchCaseOwners(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
return await searchOwners.call(this, 'Case', filter, paginationToken);
|
||||
},
|
||||
// Owner selector for Lead fields — users plus lead queues.
|
||||
async searchLeadOwners(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
return await searchOwners.call(this, 'Lead', filter, paginationToken);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
@@ -1103,8 +1083,11 @@ export class Salesforce implements INodeType {
|
||||
if (additionalFields.country !== undefined) {
|
||||
body.Country = additionalFields.country as string;
|
||||
}
|
||||
if (additionalFields.owner !== undefined) {
|
||||
body.OwnerId = additionalFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(additionalFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (additionalFields.website !== undefined) {
|
||||
body.Website = additionalFields.website as string;
|
||||
@@ -1221,8 +1204,11 @@ export class Salesforce implements INodeType {
|
||||
if (updateFields.country !== undefined) {
|
||||
body.Country = updateFields.country as string;
|
||||
}
|
||||
if (updateFields.owner !== undefined) {
|
||||
body.OwnerId = updateFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(updateFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (updateFields.website !== undefined) {
|
||||
body.Website = updateFields.website as string;
|
||||
@@ -1363,8 +1349,11 @@ export class Salesforce implements INodeType {
|
||||
if (options.body) {
|
||||
body.Body = options.body as string;
|
||||
}
|
||||
if (options.owner) {
|
||||
body.OwnerId = options.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(options.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (options.isPrivate) {
|
||||
body.IsPrivate = options.isPrivate as boolean;
|
||||
@@ -1398,8 +1387,11 @@ export class Salesforce implements INodeType {
|
||||
if (additionalFields.recordTypeId !== undefined) {
|
||||
body.RecordTypeId = additionalFields.recordTypeId as string;
|
||||
}
|
||||
if (additionalFields.owner !== undefined) {
|
||||
body.OwnerId = additionalFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(additionalFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (additionalFields.acconuntId !== undefined) {
|
||||
body.AccountId = additionalFields.acconuntId as string;
|
||||
@@ -1543,8 +1535,11 @@ export class Salesforce implements INodeType {
|
||||
if (updateFields.jigsaw !== undefined) {
|
||||
body.Jigsaw = updateFields.jigsaw as string;
|
||||
}
|
||||
if (updateFields.owner !== undefined) {
|
||||
body.OwnerId = updateFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(updateFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (updateFields.acconuntId !== undefined) {
|
||||
body.AccountId = updateFields.acconuntId as string;
|
||||
@@ -1737,8 +1732,11 @@ export class Salesforce implements INodeType {
|
||||
if (options.body !== undefined) {
|
||||
body.Body = options.body as string;
|
||||
}
|
||||
if (options.owner !== undefined) {
|
||||
body.OwnerId = options.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(options.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (options.isPrivate !== undefined) {
|
||||
body.IsPrivate = options.isPrivate as boolean;
|
||||
@@ -1867,8 +1865,11 @@ export class Salesforce implements INodeType {
|
||||
ContentLocation: 'S',
|
||||
},
|
||||
};
|
||||
if (additionalFields.ownerId) {
|
||||
body.entity_content.ownerId = additionalFields.ownerId as string;
|
||||
{
|
||||
const ownerId = getResourceLocatorValue(additionalFields.ownerId);
|
||||
if (ownerId !== undefined) {
|
||||
body.entity_content.ownerId = ownerId;
|
||||
}
|
||||
}
|
||||
if (additionalFields.linkToObjectId) {
|
||||
body.entity_content.FirstPublishLocationId =
|
||||
@@ -1923,8 +1924,11 @@ export class Salesforce implements INodeType {
|
||||
if (additionalFields.amount !== undefined) {
|
||||
body.Amount = additionalFields.amount as number;
|
||||
}
|
||||
if (additionalFields.owner !== undefined) {
|
||||
body.OwnerId = additionalFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(additionalFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (additionalFields.nextStep !== undefined) {
|
||||
body.NextStep = additionalFields.nextStep as string;
|
||||
@@ -1992,8 +1996,11 @@ export class Salesforce implements INodeType {
|
||||
if (updateFields.amount !== undefined) {
|
||||
body.Amount = updateFields.amount as number;
|
||||
}
|
||||
if (updateFields.owner !== undefined) {
|
||||
body.OwnerId = updateFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(updateFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (updateFields.nextStep !== undefined) {
|
||||
body.NextStep = updateFields.nextStep as string;
|
||||
@@ -2104,8 +2111,11 @@ export class Salesforce implements INodeType {
|
||||
if (options.body !== undefined) {
|
||||
body.Body = options.body as string;
|
||||
}
|
||||
if (options.owner !== undefined) {
|
||||
body.OwnerId = options.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(options.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (options.isPrivate !== undefined) {
|
||||
body.IsPrivate = options.isPrivate as boolean;
|
||||
@@ -2133,8 +2143,11 @@ export class Salesforce implements INodeType {
|
||||
if (additionalFields.phone !== undefined) {
|
||||
body.Phone = additionalFields.phone as string;
|
||||
}
|
||||
if (additionalFields.owner !== undefined) {
|
||||
body.OwnerId = additionalFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(additionalFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (additionalFields.sicDesc !== undefined) {
|
||||
body.SicDesc = additionalFields.sicDesc as string;
|
||||
@@ -2241,8 +2254,11 @@ export class Salesforce implements INodeType {
|
||||
if (updateFields.phone !== undefined) {
|
||||
body.Phone = updateFields.phone as string;
|
||||
}
|
||||
if (updateFields.ownerId !== undefined) {
|
||||
body.OwnerId = updateFields.ownerId as string;
|
||||
{
|
||||
const ownerId = getResourceLocatorValue(updateFields.ownerId);
|
||||
if (ownerId !== undefined) {
|
||||
body.OwnerId = ownerId;
|
||||
}
|
||||
}
|
||||
if (updateFields.sicDesc !== undefined) {
|
||||
body.SicDesc = updateFields.sicDesc as string;
|
||||
@@ -2392,8 +2408,11 @@ export class Salesforce implements INodeType {
|
||||
if (options.body !== undefined) {
|
||||
body.Body = options.body as string;
|
||||
}
|
||||
if (options.owner !== undefined) {
|
||||
body.OwnerId = options.owner as string;
|
||||
{
|
||||
const ownerId = getResourceLocatorValue(options.ownerId);
|
||||
if (ownerId !== undefined) {
|
||||
body.OwnerId = ownerId;
|
||||
}
|
||||
}
|
||||
if (options.isPrivate !== undefined) {
|
||||
body.IsPrivate = options.isPrivate as boolean;
|
||||
@@ -2418,8 +2437,11 @@ export class Salesforce implements INodeType {
|
||||
if (additionalFields.status !== undefined) {
|
||||
body.Status = additionalFields.status as string;
|
||||
}
|
||||
if (additionalFields.owner !== undefined) {
|
||||
body.OwnerId = additionalFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(additionalFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (additionalFields.subject !== undefined) {
|
||||
body.Subject = additionalFields.subject as string;
|
||||
@@ -2486,8 +2508,11 @@ export class Salesforce implements INodeType {
|
||||
if (updateFields.status !== undefined) {
|
||||
body.Status = updateFields.status as string;
|
||||
}
|
||||
if (updateFields.owner !== undefined) {
|
||||
body.OwnerId = updateFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(updateFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (updateFields.subject !== undefined) {
|
||||
body.Subject = updateFields.subject as string;
|
||||
@@ -2633,8 +2658,11 @@ export class Salesforce implements INodeType {
|
||||
if (additionalFields.whatId !== undefined) {
|
||||
body.WhatId = additionalFields.whatId as string;
|
||||
}
|
||||
if (additionalFields.owner !== undefined) {
|
||||
body.OwnerId = additionalFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(additionalFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (additionalFields.subject !== undefined) {
|
||||
body.Subject = additionalFields.subject as string;
|
||||
@@ -2725,8 +2753,11 @@ export class Salesforce implements INodeType {
|
||||
if (updateFields.whatId !== undefined) {
|
||||
body.WhatId = updateFields.whatId as string;
|
||||
}
|
||||
if (updateFields.owner !== undefined) {
|
||||
body.OwnerId = updateFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(updateFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (updateFields.subject !== undefined) {
|
||||
body.Subject = updateFields.subject as string;
|
||||
@@ -2883,8 +2914,11 @@ export class Salesforce implements INodeType {
|
||||
if (additionalFields.description !== undefined) {
|
||||
body.Description = additionalFields.description as string;
|
||||
}
|
||||
if (additionalFields.owner !== undefined) {
|
||||
body.OwnerId = additionalFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(additionalFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (additionalFields.isPrivate !== undefined) {
|
||||
body.IsPrivate = additionalFields.isPrivate as boolean;
|
||||
@@ -2920,8 +2954,11 @@ export class Salesforce implements INodeType {
|
||||
if (updateFields.description !== undefined) {
|
||||
body.Description = updateFields.description as string;
|
||||
}
|
||||
if (updateFields.owner !== undefined) {
|
||||
body.OwnerId = updateFields.owner as string;
|
||||
{
|
||||
const owner = getResourceLocatorValue(updateFields.owner);
|
||||
if (owner !== undefined) {
|
||||
body.OwnerId = owner;
|
||||
}
|
||||
}
|
||||
if (updateFields.isPrivate !== undefined) {
|
||||
body.IsPrivate = updateFields.isPrivate as boolean;
|
||||
|
||||
@@ -183,15 +183,38 @@ export const taskFields: INodeProperties[] = [
|
||||
description: 'Whether a popup reminder has been set for the task (true) or not (false)',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the User who owns the record. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the record',
|
||||
},
|
||||
{
|
||||
displayName: 'Priority Name or ID',
|
||||
@@ -526,15 +549,38 @@ export const taskFields: INodeProperties[] = [
|
||||
description: 'Whether a popup reminder has been set for the task (true) or not (false)',
|
||||
},
|
||||
{
|
||||
displayName: 'Owner Name or ID',
|
||||
displayName: 'Owner',
|
||||
name: 'owner',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUsers',
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'ID of the User who owns the record. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a user...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchUsers',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'By ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '0051700000ABCDE',
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: '^(?:[a-zA-Z0-9]{15}|[a-zA-Z0-9]{18})$',
|
||||
errorMessage: 'User ID must be 15 or 18 alphanumeric characters',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
description: 'The user who owns the record',
|
||||
},
|
||||
{
|
||||
displayName: 'Priority Name or ID',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
filterAndManageProcessedItems,
|
||||
salesforceApiRequest,
|
||||
escapeSoqlString,
|
||||
getResourceLocatorValue,
|
||||
validateSoqlFieldName,
|
||||
validateSoqlOperator,
|
||||
validateSoqlObjectName,
|
||||
@@ -839,6 +840,40 @@ describe('Salesforce -> GenericFunctions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResourceLocatorValue', () => {
|
||||
it('returns a raw string unchanged (legacy options-field shape)', () => {
|
||||
expect(getResourceLocatorValue('0051700000ABCDE')).toBe('0051700000ABCDE');
|
||||
});
|
||||
|
||||
it('extracts the value from a resourceLocator object', () => {
|
||||
expect(
|
||||
getResourceLocatorValue({ __rl: true, mode: 'list', value: '0051700000ABCDE' }),
|
||||
).toBe('0051700000ABCDE');
|
||||
expect(getResourceLocatorValue({ __rl: true, mode: 'id', value: '0051700000ABCDE' })).toBe(
|
||||
'0051700000ABCDE',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined for missing or empty values', () => {
|
||||
expect(getResourceLocatorValue(undefined)).toBeUndefined();
|
||||
expect(getResourceLocatorValue(null)).toBeUndefined();
|
||||
expect(getResourceLocatorValue('')).toBeUndefined();
|
||||
expect(getResourceLocatorValue({ __rl: true, mode: 'list', value: '' })).toBeUndefined();
|
||||
expect(getResourceLocatorValue({ __rl: true, mode: 'list', value: null })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('coerces non-string inner values to strings', () => {
|
||||
expect(getResourceLocatorValue({ __rl: true, mode: 'id', value: 12345 })).toBe('12345');
|
||||
});
|
||||
|
||||
it('returns undefined for unrecognised shapes', () => {
|
||||
expect(getResourceLocatorValue({})).toBeUndefined();
|
||||
expect(getResourceLocatorValue({ foo: 'bar' })).toBeUndefined();
|
||||
expect(getResourceLocatorValue(42)).toBeUndefined();
|
||||
expect(getResourceLocatorValue(true)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Security Functions: escaping', () => {
|
||||
describe('escapeSoqlString', () => {
|
||||
it('should escape single quotes', () => {
|
||||
|
||||
@@ -121,98 +121,180 @@ describe('Salesforce', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUsers', () => {
|
||||
it('should return users with sorted options', async () => {
|
||||
const mockUsers = [
|
||||
{ Id: 'user1', Name: 'John Doe' },
|
||||
{ Id: 'user2', Name: 'Jane Smith' },
|
||||
];
|
||||
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue(mockUsers);
|
||||
sortOptionsSpy.mockImplementation((options) => {
|
||||
return options.sort((a, b) => a.name.localeCompare(b.name));
|
||||
describe('searchUsers (listSearch)', () => {
|
||||
it('should run a filtered SOQL query with a batchSize header and map results', async () => {
|
||||
salesforceApiRequestSpy.mockResolvedValue({
|
||||
records: [
|
||||
{ Id: 'user1', Name: 'John Doe' },
|
||||
{ Id: 'user2', Name: 'Jane Smith' },
|
||||
],
|
||||
nextRecordsUrl: '/services/data/v59.0/query/01g-2000',
|
||||
});
|
||||
|
||||
const result = await node.methods.loadOptions.getUsers.call(mockLoadOptionsFunctions);
|
||||
const result = await node.methods.listSearch.searchUsers.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'jo',
|
||||
);
|
||||
|
||||
expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'records',
|
||||
expect(salesforceApiRequestAllItemsSpy).not.toHaveBeenCalled();
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
{ q: 'SELECT id, Name FROM User' },
|
||||
{ q: "SELECT Id, Name FROM User WHERE Name LIKE '%jo%' ORDER BY Name" },
|
||||
undefined,
|
||||
{ headers: { 'Sforce-Query-Options': 'batchSize=200' } },
|
||||
);
|
||||
expect(result).toEqual({
|
||||
results: [
|
||||
{ name: 'John Doe', value: 'user1' },
|
||||
{ name: 'Jane Smith', value: 'user2' },
|
||||
],
|
||||
paginationToken: '/services/data/v59.0/query/01g-2000',
|
||||
});
|
||||
});
|
||||
|
||||
it('should omit the WHERE clause when no filter is given', async () => {
|
||||
salesforceApiRequestSpy.mockResolvedValue({ records: [] });
|
||||
|
||||
const result = await node.methods.listSearch.searchUsers.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
{ q: 'SELECT Id, Name FROM User ORDER BY Name' },
|
||||
undefined,
|
||||
{ headers: { 'Sforce-Query-Options': 'batchSize=200' } },
|
||||
);
|
||||
expect(result).toEqual({ results: [], paginationToken: undefined });
|
||||
});
|
||||
|
||||
it('should escape single quotes in the filter without applying a LIMIT', async () => {
|
||||
salesforceApiRequestSpy.mockResolvedValue({ records: [] });
|
||||
|
||||
await node.methods.listSearch.searchUsers.call(mockLoadOptionsFunctions, "O'Brien");
|
||||
|
||||
const qs = salesforceApiRequestSpy.mock.calls[0][3] as { q: string };
|
||||
expect(qs.q).toBe("SELECT Id, Name FROM User WHERE Name LIKE '%O\\'Brien%' ORDER BY Name");
|
||||
expect(qs.q).not.toContain('LIMIT');
|
||||
});
|
||||
|
||||
it('should follow the pagination cursor instead of re-querying', async () => {
|
||||
salesforceApiRequestSpy.mockResolvedValue({ records: [{ Id: 'user3', Name: 'Zoe' }] });
|
||||
|
||||
const result = await node.methods.listSearch.searchUsers.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'',
|
||||
'/services/data/v59.0/query/01g-2000',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'Jane Smith', value: 'user2' },
|
||||
{ name: 'John Doe', value: 'user1' },
|
||||
]);
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/query/01g-2000');
|
||||
expect(result).toEqual({
|
||||
results: [{ name: 'Zoe', value: 'user3' }],
|
||||
paginationToken: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCaseOwners', () => {
|
||||
it('should return case owners with queues and users', async () => {
|
||||
const mockQueues = [
|
||||
describe('searchCaseOwners (listSearch)', () => {
|
||||
it('should combine case queues and users with prefixed labels', async () => {
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Queue: { Id: 'queue1', Name: 'Support Queue' } },
|
||||
{ Queue: { Id: 'queue2', Name: 'Sales Queue' } },
|
||||
];
|
||||
const mockUsers = [
|
||||
{ Id: 'user1', Name: 'John Doe' },
|
||||
{ Id: 'user2', Name: 'Jane Smith' },
|
||||
];
|
||||
]);
|
||||
salesforceApiRequestSpy.mockResolvedValue({ records: [{ Id: 'user1', Name: 'John Doe' }] });
|
||||
|
||||
salesforceApiRequestAllItemsSpy
|
||||
.mockResolvedValueOnce(mockQueues)
|
||||
.mockResolvedValueOnce(mockUsers);
|
||||
|
||||
const result = await node.methods.loadOptions.getCaseOwners.call(mockLoadOptionsFunctions);
|
||||
const result = await node.methods.listSearch.searchCaseOwners.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'',
|
||||
);
|
||||
|
||||
// Queues are fetched in full (legacy behaviour), users via a capped search.
|
||||
expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'records',
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
{
|
||||
q: "SELECT Queue.Id, Queue.Name FROM QueuesObject where Queue.Type='Queue' and SobjectType = 'Case'",
|
||||
q: "SELECT Queue.Id, Queue.Name FROM QueuesObject WHERE Queue.Type = 'Queue' AND SobjectType = 'Case'",
|
||||
},
|
||||
);
|
||||
expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'records',
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/query',
|
||||
{},
|
||||
{ q: 'SELECT id, Name FROM User' },
|
||||
{ q: 'SELECT Id, Name FROM User ORDER BY Name' },
|
||||
undefined,
|
||||
{ headers: { 'Sforce-Query-Options': 'batchSize=200' } },
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'Queue: Support Queue', value: 'queue1' },
|
||||
// Queues are sorted by label; users follow in their SOQL order.
|
||||
expect(result.results).toEqual([
|
||||
{ name: 'Queue: Sales Queue', value: 'queue2' },
|
||||
{ name: 'Queue: Support Queue', value: 'queue1' },
|
||||
{ name: 'User: John Doe', value: 'user1' },
|
||||
{ name: 'User: Jane Smith', value: 'user2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle users without queue prefix when no queues exist', async () => {
|
||||
salesforceApiRequestAllItemsSpy
|
||||
.mockResolvedValueOnce([])
|
||||
.mockResolvedValueOnce([{ Id: 'user1', Name: 'John Doe' }]);
|
||||
it('should not prefix users when the filter matches no queues', async () => {
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Queue: { Id: 'queue1', Name: 'Support Queue' } },
|
||||
]);
|
||||
salesforceApiRequestSpy.mockResolvedValue({ records: [{ Id: 'user1', Name: 'John Doe' }] });
|
||||
|
||||
const result = await node.methods.loadOptions.getCaseOwners.call(mockLoadOptionsFunctions);
|
||||
const result = await node.methods.listSearch.searchCaseOwners.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'doe',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ name: 'John Doe', value: 'user1' }]);
|
||||
// No queue matched 'doe', so the users render without the "User: " prefix.
|
||||
expect(result.results).toEqual([{ name: 'John Doe', value: 'user1' }]);
|
||||
});
|
||||
|
||||
it('should filter queues in-memory by the search term', async () => {
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Queue: { Id: 'queue1', Name: 'Support Queue' } },
|
||||
{ Queue: { Id: 'queue2', Name: 'Sales Queue' } },
|
||||
]);
|
||||
salesforceApiRequestSpy.mockResolvedValue({ records: [] });
|
||||
|
||||
const result = await node.methods.listSearch.searchCaseOwners.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'support',
|
||||
);
|
||||
|
||||
expect(result.results).toEqual([{ name: 'Queue: Support Queue', value: 'queue1' }]);
|
||||
});
|
||||
|
||||
it('should skip the queue query when following a pagination cursor', async () => {
|
||||
salesforceApiRequestSpy.mockResolvedValue({ records: [{ Id: 'user1', Name: 'John Doe' }] });
|
||||
|
||||
const result = await node.methods.listSearch.searchCaseOwners.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'',
|
||||
'/services/data/v59.0/query/01g-2000',
|
||||
);
|
||||
|
||||
expect(salesforceApiRequestAllItemsSpy).not.toHaveBeenCalled();
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledWith('GET', '/query/01g-2000');
|
||||
// Paginated pages carry no queues, so users stay unprefixed.
|
||||
expect(result.results).toEqual([{ name: 'John Doe', value: 'user1' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLeadOwners', () => {
|
||||
it('should return lead owners with queues and users', async () => {
|
||||
const mockQueues = [{ Queue: { Id: 'queue1', Name: 'Lead Queue' } }];
|
||||
const mockUsers = [{ Id: 'user1', Name: 'John Doe' }];
|
||||
describe('searchLeadOwners (listSearch)', () => {
|
||||
it('should query the Lead queues and prefix users', async () => {
|
||||
salesforceApiRequestAllItemsSpy.mockResolvedValue([
|
||||
{ Queue: { Id: 'queue1', Name: 'Lead Queue' } },
|
||||
]);
|
||||
salesforceApiRequestSpy.mockResolvedValue({ records: [{ Id: 'user1', Name: 'John Doe' }] });
|
||||
|
||||
salesforceApiRequestAllItemsSpy
|
||||
.mockResolvedValueOnce(mockQueues)
|
||||
.mockResolvedValueOnce(mockUsers);
|
||||
|
||||
const result = await node.methods.loadOptions.getLeadOwners.call(mockLoadOptionsFunctions);
|
||||
const result = await node.methods.listSearch.searchLeadOwners.call(
|
||||
mockLoadOptionsFunctions,
|
||||
'',
|
||||
);
|
||||
|
||||
expect(salesforceApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'records',
|
||||
@@ -220,11 +302,10 @@ describe('Salesforce', () => {
|
||||
'/query',
|
||||
{},
|
||||
{
|
||||
q: "SELECT Queue.Id, Queue.Name FROM QueuesObject where Queue.Type='Queue' and SobjectType = 'Lead'",
|
||||
q: "SELECT Queue.Id, Queue.Name FROM QueuesObject WHERE Queue.Type = 'Queue' AND SobjectType = 'Lead'",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
expect(result.results).toEqual([
|
||||
{ name: 'Queue: Lead Queue', value: 'queue1' },
|
||||
{ name: 'User: John Doe', value: 'user1' },
|
||||
]);
|
||||
@@ -1291,6 +1372,31 @@ describe('Salesforce', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve a resourceLocator owner value to OwnerId', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
|
||||
const params: Record<string, unknown> = {
|
||||
resource: 'lead',
|
||||
operation: 'create',
|
||||
company: 'ACME Corp',
|
||||
lastname: 'Doe',
|
||||
additionalFields: {
|
||||
owner: { __rl: true, mode: 'id', value: 'user123' },
|
||||
},
|
||||
};
|
||||
return params[param];
|
||||
});
|
||||
|
||||
salesforceApiRequestSpy.mockResolvedValue({ id: 'lead456', success: true });
|
||||
|
||||
await node.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/sobjects/lead',
|
||||
expect.objectContaining({ OwnerId: 'user123' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle lead upsert operation', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(param: string, index?: number): any => {
|
||||
@@ -3288,6 +3394,39 @@ describe('Salesforce', () => {
|
||||
});
|
||||
|
||||
describe('Execute Method - Account Resource Extended Fields', () => {
|
||||
describe('Account Add Note Operation', () => {
|
||||
it('should set the note owner from the ownerId option', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
|
||||
const params: Record<string, unknown> = {
|
||||
resource: 'account',
|
||||
operation: 'addNote',
|
||||
accountId: 'acc123',
|
||||
title: 'Important Note',
|
||||
options: {
|
||||
body: 'Note body',
|
||||
ownerId: { __rl: true, mode: 'id', value: 'user789' },
|
||||
},
|
||||
};
|
||||
return params[param];
|
||||
});
|
||||
|
||||
salesforceApiRequestSpy.mockResolvedValue({ id: 'note123', success: true });
|
||||
|
||||
await node.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(salesforceApiRequestSpy).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/sobjects/note',
|
||||
expect.objectContaining({
|
||||
Title: 'Important Note',
|
||||
ParentId: 'acc123',
|
||||
Body: 'Note body',
|
||||
OwnerId: 'user789',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Account Create Operation - Additional Fields', () => {
|
||||
it('should handle account create with all additional fields', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((param: string): any => {
|
||||
|
||||
Reference in New Issue
Block a user