Skip to content

Commit

Permalink
feat: singleRecordQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Jan 4, 2021
1 parent 4ad08f8 commit ea72d9f
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 1 deletion.
40 changes: 40 additions & 0 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,46 @@ export class Connection extends JSForceConnection {
});
});
}

/**
* Executes a query using either standard REST or Tooling API, returning a single record.
* Will throw if either zero records are found OR multiple records are found.
*
* @param soql The SOQL string.
* @param options The query options.
*/
public async singleRecordQuery<T>(
soql: string,
options: SingleRecordQueryOptions = {
choiceField: 'Name',
}
): Promise<T> {
const result = options.tooling ? await this.tooling.query<T>(soql) : await this.query<T>(soql);
if (result.totalSize === 0) {
throw new SfdxError(`No records found for ${soql}`, SingleRecordQueryErrors.NoRecords);
}
if (result.totalSize > 1) {
throw new SfdxError(
options.returnChoicesOnMultiple && options.choiceField
? `Multiple records found. ${result.records
.map((item) => item[(options.choiceField ?? 'Name') as keyof T])
.join(',')}`
: 'The query returned more than 1 record',
SingleRecordQueryErrors.MultipleRecords
);
}
return result.records[0];
}
}

export const SingleRecordQueryErrors = {
NoRecords: 'SingleRecordQuery_NoRecords',
MultipleRecords: 'SingleRecordQuery_MultipleRecords',
};
export interface SingleRecordQueryOptions {
tooling?: boolean;
returnChoicesOnMultiple?: boolean;
choiceField?: string; // defaults to Name
}

export namespace Connection {
Expand Down
72 changes: 71 additions & 1 deletion test/unit/connectionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { assert, expect } from 'chai';
import * as jsforce from 'jsforce';
import { AuthInfo } from '../../src/authInfo';
import { ConfigAggregator, ConfigInfo } from '../../src/config/configAggregator';
import { Connection, SFDX_HTTP_HEADERS } from '../../src/connection';
import { Connection, SFDX_HTTP_HEADERS, SingleRecordQueryErrors } from '../../src/connection';
import { testSetup } from '../../src/testSetup';

// Setup the test environment.
Expand Down Expand Up @@ -206,4 +206,74 @@ describe('Connection', () => {
expect(err.message).to.equal(errorMsg);
}
});

it('singleRecordQuery returns single-record result properly', async () => {
const mockSingleRecord = {
id: '123',
name: 'testName',
};
requestMock.returns(Promise.resolve({ totalSize: 1, records: [mockSingleRecord] }));
const soql = 'TEST_SOQL';

const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const queryResult = await conn.singleRecordQuery(soql);
expect(queryResult).to.deep.equal({
...mockSingleRecord,
});
});

it('singleRecordQuery throws on no-records', async () => {
requestMock.returns(Promise.resolve({ totalSize: 0, records: [] }));
const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });

try {
await conn.singleRecordQuery('TEST_SOQL');
assert.fail('SingleRecordQuery query should have errored.');
} catch (err) {
expect(err.name).to.equal(SingleRecordQueryErrors.NoRecords);
}
});

it('singleRecordQuery throws on multiple records', async () => {
requestMock.returns(Promise.resolve({ totalSize: 2, records: [{ id: 1 }, { id: 2 }] }));
const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });

try {
await conn.singleRecordQuery('TEST_SOQL');
assert.fail('singleRecordQuery should have errored.');
} catch (err) {
expect(err.name).to.equal(SingleRecordQueryErrors.MultipleRecords);
}
});

it('singleRecordQuery throws on multiple records with options', async () => {
requestMock.returns(Promise.resolve({ totalSize: 2, records: [{ id: 1 }, { id: 2 }] }));
const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });

try {
await conn.singleRecordQuery('TEST_SOQL', { returnChoicesOnMultiple: true, choiceField: 'id' });
assert.fail('singleRecordQuery should have errored.');
} catch (err) {
expect(err.name).to.equal(SingleRecordQueryErrors.MultipleRecords);
expect(err.message).to.include('1,2');
}
});

it('singleRecordQuery handles tooling api flag', async () => {
const mockSingleRecord = {
id: '123',
name: 'testName',
};
requestMock.returns(Promise.resolve({ totalSize: 1, records: [mockSingleRecord] }));
// const querySpy = $$.SANDBOX.spy(jsforce.Connection.prototype, 'query');
const soql = 'TEST_SOQL';

const conn = await Connection.create({ authInfo: testAuthInfo as AuthInfo });
const toolingQuerySpy = $$.SANDBOX.spy(conn.tooling, 'query');
const queryResults = await conn.singleRecordQuery(soql, { tooling: true });
expect(queryResults).to.deep.equal({
...mockSingleRecord,
});
expect(toolingQuerySpy.firstCall.args[0]).to.equal(soql);
});
});

0 comments on commit ea72d9f

Please sign in to comment.