Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"json",
"loglevel",
"perflog",
"skip-assignment-rules",
"sobject",
"target-org",
"use-tooling-api",
Expand Down Expand Up @@ -236,6 +237,7 @@
"loglevel",
"perflog",
"record-id",
"skip-assignment-rules",
"sobject",
"target-org",
"use-tooling-api",
Expand Down
4 changes: 4 additions & 0 deletions messages/record.create.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Values for the flags in the form <fieldName>=<value>, separate multiple pairs wi

Use Tooling API so you can insert a record in a Tooling API object.

# flags.skip-assignment-rules.summary

Don't apply active assignment rules when creating the record; applies to Account, Case, and Lead records.

# examples

- Insert a record into the Account object of your default org; only the required Name field has a value:
Expand Down
4 changes: 4 additions & 0 deletions messages/record.update.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Use Tooling API so you can update a record in a Tooling API object.

Fields that you're updating, in the format of <fieldName>=<value> pairs.

# flags.skip-assignment-rules.summary

Don't apply active assignment rules when updating the record; applies to Account, Case, and Lead records.

# examples

- Update the Name field of an Account record with the specified (truncated) ID:
Expand Down
8 changes: 7 additions & 1 deletion src/commands/data/create/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export default class Create extends SfCommand<SaveResult> {
deprecateAliases: true,
}),
perflog: perflogFlag,
'skip-assignment-rules': Flags.boolean({
summary: messages.getMessage('flags.skip-assignment-rules.summary'),
}),
};

public async run(): Promise<SaveResult> {
Expand All @@ -62,7 +65,10 @@ export default class Create extends SfCommand<SaveResult> {
: flags['target-org'].getConnection(flags['api-version'])
).sobject(flags.sobject);
const values = stringToDictionary(flags.values);
const result = await sobject.insert(values);
const result = await sobject.insert(
values,
flags['skip-assignment-rules'] ? { headers: { 'Sforce-Auto-Assign': 'FALSE' } } : {}
);
if (result.success) {
this.log(messages.getMessage('createSuccess', [result.id || 'unknown id']));
this.spinner.stop();
Expand Down
7 changes: 6 additions & 1 deletion src/commands/data/update/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export default class Update extends SfCommand<SaveResult> {
version: '57',
},
}),
'skip-assignment-rules': Flags.boolean({
summary: messages.getMessage('flags.skip-assignment-rules.summary'),
}),
};

public async run(): Promise<SaveResult> {
Expand All @@ -86,7 +89,9 @@ export default class Update extends SfCommand<SaveResult> {
const sObjectId = flags['record-id'] ?? ((await query(conn, flags.sobject, flags.where as string)).Id as string);
try {
const updateObject = { ...stringToDictionary(flags.values), Id: sObjectId };
const result = await conn.sobject(flags.sobject).update(updateObject);
const result = await conn
.sobject(flags.sobject)
.update(updateObject, flags['skip-assignment-rules'] ? { headers: { 'Sforce-Auto-Assign': 'FALSE' } } : {});
if (result.success) {
this.log(messages.getMessage('updateSuccess', [sObjectId]));
} else {
Expand Down
32 changes: 32 additions & 0 deletions test/commands/data/create/record.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@ describe('data:create:record', () => {
const config = new Config({
root: resolve(dirname(fileURLToPath(import.meta.url)), '../../../package.json'),
});
let capturedHeaders: Record<string, string> | undefined;

beforeEach(async () => {
await $$.stubAuths(testOrg);
await config.load();
capturedHeaders = undefined;
$$.fakeConnectionRequest = (request: AnyJson): Promise<SaveResult> => {
const requestWithUrl = ensureJsonMap(request);
if (request && ensureString(requestWithUrl.url).includes('Account')) {
capturedHeaders = requestWithUrl.headers as Record<string, string> | undefined;
return Promise.resolve({
id: sObjectId,
success: true,
Expand All @@ -56,4 +59,33 @@ describe('data:create:record', () => {
const result = await cmd.run();
expect(result.id).to.equal(sObjectId);
});

it('should not send the Sforce-Auto-Assign header by default', async () => {
const cmd = new Create(
['--target-org', 'test@org.com', '--sobject', 'Account', '-v', '"Name=Acme"', '--json'],
config
);

await cmd.run();
expect(capturedHeaders).to.not.have.property('Sforce-Auto-Assign');
});

it('should send Sforce-Auto-Assign: FALSE when --skip-assignment-rules is set', async () => {
const cmd = new Create(
[
'--target-org',
'test@org.com',
'--sobject',
'Account',
'-v',
'"Name=Acme"',
'--skip-assignment-rules',
'--json',
],
config
);

await cmd.run();
expect(capturedHeaders).to.have.property('Sforce-Auto-Assign', 'FALSE');
});
});
85 changes: 85 additions & 0 deletions test/commands/data/record/dataRecord.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,91 @@ describe('data:record commands', () => {
});
});

describe('skip-assignment-rules flag', () => {
// The Case assignment rule in the test project routes Cases whose Subject starts with
// "AssignmentRuleTest" to TestCaseQueue. When --skip-assignment-rules is passed, the record
// should stay owned by the running user instead of being reassigned to the queue.
type CaseRecord = { Id: string; OwnerId: string; Subject: string };
type QueueRecord = { records: Array<{ Id: string }> };

let queueId: string;
let runningUserId: string;

before(() => {
const queue = execCmd<QueueRecord>(
"data:query --query \"SELECT Id FROM Group WHERE Type='Queue' AND DeveloperName='TestCaseQueue'\" --json",
{ ensureExitCode: 0 }
).jsonOutput?.result;
assert(queue?.records?.[0]?.Id, 'TestCaseQueue must exist in the scratch org');
queueId = queue.records[0].Id;

// Determine the running user by creating a Case that does NOT match the assignment rule
// and reading its OwnerId — avoids brittle username/alias lookups.
const seed = execCmd<SaveResult>(
`data:create:record --sobject Case --values "Subject='NoRuleMatchSeed-${genUniqueString()}'" --json`,
{ ensureExitCode: 0 }
).jsonOutput?.result;
assert(seed?.id);
const seedRecord = execCmd<CaseRecord>(`data:get:record --sobject Case --record-id ${seed.id} --json`, {
ensureExitCode: 0,
}).jsonOutput?.result;
assert(seedRecord?.OwnerId);
runningUserId = seedRecord.OwnerId;
});

it('assigns Case to the queue when --skip-assignment-rules is NOT set', () => {
const subject = `AssignmentRuleTest-${genUniqueString()}`;
const createResponse = execCmd<SaveResult>(
`data:create:record --sobject Case --values "Subject='${subject}'" --json`,
{ ensureExitCode: 0 }
).jsonOutput?.result;
assert(createResponse?.id);

const getResponse = execCmd<CaseRecord>(
`data:get:record --sobject Case --record-id ${createResponse.id} --json`,
{ ensureExitCode: 0 }
).jsonOutput?.result;
expect(getResponse).to.have.property('OwnerId', queueId);
});

it('leaves Case owned by the running user when --skip-assignment-rules is set on create', () => {
const subject = `AssignmentRuleTest-${genUniqueString()}`;
const createResponse = execCmd<SaveResult>(
`data:create:record --sobject Case --values "Subject='${subject}'" --skip-assignment-rules --json`,
{ ensureExitCode: 0 }
).jsonOutput?.result;
assert(createResponse?.id);

const getResponse = execCmd<CaseRecord>(
`data:get:record --sobject Case --record-id ${createResponse.id} --json`,
{ ensureExitCode: 0 }
).jsonOutput?.result;
expect(getResponse).to.have.property('OwnerId', runningUserId);
});

it('leaves Case owner unchanged on update when --skip-assignment-rules is set', () => {
// Seed a Case that does NOT match the rule (so it's owned by the running user).
const seedSubject = `NoRuleMatch-${genUniqueString()}`;
const seed = execCmd<SaveResult>(
`data:create:record --sobject Case --values "Subject='${seedSubject}'" --skip-assignment-rules --json`,
{ ensureExitCode: 0 }
).jsonOutput?.result;
assert(seed?.id);

// Update to a subject that would match the assignment rule, but suppress it.
const newSubject = `AssignmentRuleTest-${genUniqueString()}`;
execCmd<SaveResult>(
`data:update:record --sobject Case --record-id ${seed.id} --values "Subject='${newSubject}'" --skip-assignment-rules --json`,
{ ensureExitCode: 0 }
);

const getResponse = execCmd<CaseRecord>(`data:get:record --sobject Case --record-id ${seed.id} --json`, {
ensureExitCode: 0,
}).jsonOutput?.result;
expect(getResponse).to.have.property('OwnerId', runningUserId);
});
});

describe('json parsing', () => {
it('will parse JSON correctly for update', () => {
const result = execCmd<{ records: Array<{ Id: string }> }>(
Expand Down
65 changes: 65 additions & 0 deletions test/commands/data/update/record.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,71 @@ describe('data:update:record', () => {
}
});

it('should not send the Sforce-Auto-Assign header by default', async () => {
let capturedHeaders: Record<string, string> | undefined;
$$.fakeConnectionRequest = (request: AnyJson): Promise<AnyJson> => {
const requestMap = ensureJsonMap(request);
if (ensureString(requestMap.url).includes('query')) {
return Promise.resolve({ records: [{ Id: sObjectId }] });
}
if (ensureString(requestMap.url).includes('Account')) {
capturedHeaders = requestMap.headers as Record<string, string> | undefined;
return Promise.resolve({ id: sObjectId, success: true, errors: [] });
}
return Promise.resolve({});
};

const cmd = new Update(
[
'--target-org',
'test@org.com',
'--sobject',
'Account',
'--record-id',
sObjectId,
'-v',
'"Name=NewName"',
'--json',
],
config
);
await cmd.run();
expect(capturedHeaders).to.not.have.property('Sforce-Auto-Assign');
});

it('should send Sforce-Auto-Assign: FALSE when --skip-assignment-rules is set', async () => {
let capturedHeaders: Record<string, string> | undefined;
$$.fakeConnectionRequest = (request: AnyJson): Promise<AnyJson> => {
const requestMap = ensureJsonMap(request);
if (ensureString(requestMap.url).includes('query')) {
return Promise.resolve({ records: [{ Id: sObjectId }] });
}
if (ensureString(requestMap.url).includes('Account')) {
capturedHeaders = requestMap.headers as Record<string, string> | undefined;
return Promise.resolve({ id: sObjectId, success: true, errors: [] });
}
return Promise.resolve({});
};

const cmd = new Update(
[
'--target-org',
'test@org.com',
'--sobject',
'Account',
'--record-id',
sObjectId,
'-v',
'"Name=NewName"',
'--skip-assignment-rules',
'--json',
],
config
);
await cmd.run();
expect(capturedHeaders).to.have.property('Sforce-Auto-Assign', 'FALSE');
});

it('should throw an error if both --where and --record-id are provided', async () => {
const cmd = new Update(
[
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<AssignmentRules xmlns="http://soap.sforce.com/2006/04/metadata">
<assignmentRule>
<fullName>TestCaseAssignmentRule</fullName>
<active>true</active>
<ruleEntry>
<assignedTo>TestCaseQueue</assignedTo>
<assignedToType>Queue</assignedToType>
<booleanFilter>1</booleanFilter>
<criteriaItems>
<field>Case.Subject</field>
<operation>startsWith</operation>
<value>AssignmentRuleTest</value>
</criteriaItems>
</ruleEntry>
</assignmentRule>
</AssignmentRules>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<Queue xmlns="http://soap.sforce.com/2006/04/metadata">
<name>Test Case Queue</name>
<doesSendEmailToMembers>false</doesSendEmailToMembers>
<queueSobject>
<sobjectType>Case</sobjectType>
</queueSobject>
</Queue>
Loading