From 801a7aaaae5346c8c6617ba6c8dc5ca297a86f17 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Mon, 7 Feb 2022 12:00:24 -0700 Subject: [PATCH] fix: fix 3 data bugs * fix: @W-10290520@ '[object Object]' now stringified * fix: @W-10290524@ fix json parsing on record:update * chore: subqueries now display on the a single row * chore: fix conflicting query results and parsing * chore: recursively show query results * test: added tests for complex subquery * chore: try/catch around JSON parsing for data that contains {} * chore: try changing cron to ' to match other repos for dependabot automerge Co-authored-by: Shane McLaughlin --- .circleci/config.yml | 8 +- src/commands/force/data/soql/query.ts | 26 ++- src/dataCommand.ts | 25 ++- src/reporters.ts | 32 +++- .../force/data/record/dataRecord.nut.ts | 32 ++++ .../data/soql/query/dataSoqlQuery.nut.ts | 15 ++ .../soql/query/dataSoqlQueryCommand.test.ts | 71 ++++++-- test/reporter.test.ts | 55 +++++- test/test-files/soqlQuery.exemplars.ts | 168 ++++++++++++++++++ 9 files changed, 389 insertions(+), 43 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e271441f..2c1d171e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,15 +26,15 @@ parameters: description: | By default, the latest version of the standalone CLI will be installed. To install via npm, supply a version tag such as "latest" or "6". - default: "" + default: '' type: string repo_tag: description: The tag of the module repo to checkout, '' defaults to branch/PR - default: "" + default: '' type: string npm_module_name: description: The fully qualified npm module name, i.e. @salesforce/plugins-data - default: "" + default: '' type: string workflows: version: 2 @@ -109,7 +109,7 @@ workflows: dependabot-automerge: triggers: - schedule: - cron: "0 2,5,8,11 * * *" + cron: '0 2,5,8,11 * * *' filters: branches: only: diff --git a/src/commands/force/data/soql/query.ts b/src/commands/force/data/soql/query.ts index e9ab8191..a9ed9dd6 100644 --- a/src/commands/force/data/soql/query.ts +++ b/src/commands/force/data/soql/query.ts @@ -15,6 +15,7 @@ import { ensureString, getArray, isJsonArray, + JsonArray, toJsonMap, } from '@salesforce/ts-types'; import { Tooling } from '@salesforce/core/lib/connection'; @@ -66,8 +67,13 @@ export class SoqlQuery { // eslint-disable-next-line no-underscore-dangle const columnUrl = `${connection._baseUrl()}/query?q=${encodeURIComponent(query)}&columns=true`; const results = toJsonMap(await connection.request(columnUrl)); + + return this.recursivelyFindColumns(ensureJsonArray(results.columnMetadata)); + } + + private recursivelyFindColumns(data: JsonArray): Field[] { const columns: Field[] = []; - for (let column of ensureJsonArray(results.columnMetadata)) { + for (let column of data) { column = ensureJsonMap(column); const name = ensureString(column.columnName); @@ -78,12 +84,17 @@ export class SoqlQuery { name, fields: [], }; - for (const subcolumn of column.joinColumns) { - const f: Field = { - fieldType: FieldType.field, - name: ensureString(ensureJsonMap(subcolumn).columnName), - }; - if (field.fields) field.fields.push(f); + for (let subcolumn of column.joinColumns) { + subcolumn = ensureJsonMap(subcolumn); + if (isJsonArray(column.joinColumns) && column.joinColumns.length > 0) { + if (field.fields) field.fields.push(...this.recursivelyFindColumns([subcolumn])); + } else { + const f: Field = { + fieldType: FieldType.field, + name: ensureString(ensureJsonMap(subcolumn).columnName), + }; + if (field.fields) field.fields.push(f); + } } columns.push(field); } else { @@ -131,7 +142,6 @@ export class DataSoqlQueryCommand extends DataCommand { public static readonly description = messages.getMessage('description'); public static readonly requiresUsername = true; public static readonly examples = messages.getMessage('examples').split(os.EOL); - public static readonly flagsConfig: FlagsConfig = { query: flags.string({ char: 'q', diff --git a/src/dataCommand.ts b/src/dataCommand.ts index 9dc2bafb..f8883545 100644 --- a/src/dataCommand.ts +++ b/src/dataCommand.ts @@ -7,8 +7,8 @@ import { SfdxCommand } from '@salesforce/command'; import { AnyJson, Dictionary, get, Nullable } from '@salesforce/ts-types'; -import { fs, Messages, SfdxError, Org } from '@salesforce/core'; -import { BaseConnection, ErrorResult, Record, SObject } from 'jsforce'; +import { fs, Messages, Org, SfdxError } from '@salesforce/core'; +import { BaseConnection, ErrorResult, Record as jsforceRecord, SObject } from 'jsforce'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore because jsforce doesn't export http-api import * as HttpApi from 'jsforce/lib/http-api'; @@ -123,7 +123,7 @@ export abstract class DataCommand extends SfdxCommand { return connection; } - public async query(sobject: SObject, where: string): Promise> { + public async query(sobject: SObject, where: string): Promise> { const queryObject = this.stringToDictionary(where); const records = await sobject.find(queryObject, 'id'); if (!records || records.length === 0) { @@ -137,10 +137,10 @@ export abstract class DataCommand extends SfdxCommand { ); } - return this.normalize>(records); + return this.normalize>(records); } - protected stringToDictionary(str: string): Dictionary { + protected stringToDictionary(str: string): Dictionary> { const keyValuePairs = this.parseKeyValueSequence(str); return this.transformKeyValueSequence(keyValuePairs); } @@ -172,8 +172,8 @@ export abstract class DataCommand extends SfdxCommand { * * @param [keyValuePairs] - The list of key=value pair strings. */ - private transformKeyValueSequence(keyValuePairs: string[]): Dictionary { - const constructedObject: Dictionary = {}; + private transformKeyValueSequence(keyValuePairs: string[]): Dictionary> { + const constructedObject: Dictionary> = {}; keyValuePairs.forEach((pair) => { // Look for the *first* '=' and splits there, ignores any subsequent '=' for this pair @@ -182,7 +182,16 @@ export abstract class DataCommand extends SfdxCommand { throw new Error(messages.getMessage('TextUtilMalformedKeyValuePair', [pair])); } else { const key = pair.substr(0, eqPosition); - constructedObject[key] = this.convertToBooleanIfApplicable(pair.substr(eqPosition + 1)); + if (pair.includes('{') && pair.includes('}')) { + try { + constructedObject[key] = JSON.parse(pair.substr(eqPosition + 1)) as Record; + } catch { + // the data contained { and }, but wasn't valid JSON, default to parsing as-is + constructedObject[key] = this.convertToBooleanIfApplicable(pair.substr(eqPosition + 1)); + } + } else { + constructedObject[key] = this.convertToBooleanIfApplicable(pair.substr(eqPosition + 1)); + } } }); diff --git a/src/reporters.ts b/src/reporters.ts index 2e48f599..5b58b7f7 100644 --- a/src/reporters.ts +++ b/src/reporters.ts @@ -9,7 +9,7 @@ import { Logger, Messages } from '@salesforce/core'; import { UX } from '@salesforce/command'; import * as chalk from 'chalk'; import { get, getArray, getNumber, isString, Optional } from '@salesforce/ts-types'; -import { SoqlQueryResult, Field, FieldType } from './dataSoqlQueryTypes'; +import { Field, FieldType, SoqlQueryResult } from './dataSoqlQueryTypes'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-data', 'soql.query'); @@ -133,12 +133,28 @@ export class HumanReporter extends QueryReporter { // eslint-disable-next-line @typescript-eslint/no-explicit-any public massageRows(queryResults: unknown[], children: string[], aggregates: Field[]): any { + // some fields will return a JSON object that isn't accessible via the query (SELECT Metadata FROM RemoteProxy) + // some will return a JSON that IS accessible via the query (SELECT owner.Profile.Name FROM Lead) + // querying (SELECT Metadata.isActive FROM RemoteProxy) throws a SOQL validation error, so we have to display the entire Metadata object + queryResults.map((qr) => { + const result = qr as Record; + this.data.columns.forEach((col) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const entry = Reflect.get(result, col.name); + if (typeof entry === 'object' && col.fieldType === FieldType.field) { + Reflect.set(result, col.name, JSON.stringify(entry, null, 2)); + } else if (typeof entry === 'object' && col.fields?.length && entry) { + col.fields.forEach((field) => { + Reflect.set(result, `${col.name}.${field.name}`, get(result, `${col.name}.records[0].${field.name}`)); + }); + } + }); + }); + // There are subqueries or aggregates. Massage the data. let qr; if (children.length > 0 || aggregates.length > 0) { qr = queryResults.reduce((newResults: unknown[], result) => { - newResults.push(result); - // Aggregates are soql functions that aggregate data, like "SELECT avg(total)" and // are returned in the data as exprX. Aggregates can have aliases, like "avg(total) totalAverage" // and are returned in the data as the alias. @@ -167,15 +183,17 @@ export class HumanReporter extends QueryReporter { // eslint-disable-next-line @typescript-eslint/no-explicit-any childRecords.forEach((record: unknown) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const newRecord = Object.assign({}); Object.entries(record as never).forEach(([key, value]) => { - Reflect.defineProperty(newRecord, `${child.toString()}.${key}`, { value }); + // merge subqueries with the "parent" so they are on the same row + Reflect.defineProperty(result as Record, `${child.toString()}.${key}`, { + value: value ? value : chalk.bold('null'), + }); }); - newResults.push(newRecord); }); } }); } + newResults.push(result); return newResults; }, [] as unknown[]); } @@ -223,6 +241,8 @@ export class CsvReporter extends QueryReporter { const value = get(row, name); if (isString(value)) { return this.escape(value); + } else if (typeof value === 'object') { + return this.escape(JSON.stringify(value)); } return value; }); diff --git a/test/commands/force/data/record/dataRecord.nut.ts b/test/commands/force/data/record/dataRecord.nut.ts index d0fc38cb..81400b66 100644 --- a/test/commands/force/data/record/dataRecord.nut.ts +++ b/test/commands/force/data/record/dataRecord.nut.ts @@ -247,4 +247,36 @@ describe('data:record commands', () => { expect(result).to.have.property('Bool__c', false); }); }); + + it('will parse JSON correctly for update', () => { + const result = execCmd('force:data:soql:query -q "SELECT Id FROM RemoteProxy LIMIT 1" -t --json', { + ensureExitCode: 0, + }).jsonOutput?.result as { records: Array<{ Id: string }> }; + + const update = execCmd( + 'force:data:record:update ' + + '--sobjecttype RemoteProxy ' + + `--sobjectid ${result.records[0].Id} ` + + '--usetoolingapi ' + + '--values "Metadata=\'{\\"disableProtocolSecurity\\": false,\\"isActive\\": true,\\"url\\": \\"https://www.example.com\\",\\"urls\\": null,\\"description\\": null}\'"', + { ensureExitCode: 0 } + ); + expect(update).to.be.ok; + }); + + it('will parse invalid JSON data, but that contains {}', () => { + const result = execCmd('force:data:record:create -s Account -v "Name=Test" --json', { + ensureExitCode: 0, + }).jsonOutput?.result as { id: string }; + + const update = execCmd( + 'force:data:record:update ' + + '--sobjecttype Account ' + + `--sobjectid ${result.id} ` + + '--values "Description=\'my new description { with invalid } JSON\'"', + { ensureExitCode: 0 } + ); + + expect(update).to.be.ok; + }); }); diff --git a/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts b/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts index ed36c36d..6eaaa3c3 100644 --- a/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts +++ b/test/commands/force/data/soql/query/dataSoqlQuery.nut.ts @@ -170,5 +170,20 @@ describe('data:soql:query command', () => { toolingApi: true, }); }); + + it('should print JSON output correctly', () => { + const result = runQuery('select id, isActive, Metadata from RemoteProxy', { + ensureExitCode: 0, + json: false, + toolingApi: true, + }); + expect(result).to.not.include('[object Object]'); + // the Metadata object parsed correctly + expect(result).to.include('disableProtocolSecurity'); + expect(result).to.include('isActive'); + expect(result).to.include('url'); + expect(result).to.include('urls'); + expect(result).to.include('description'); + }); }); }); diff --git a/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts b/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts index 676642dd..ea947a97 100644 --- a/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts +++ b/test/commands/force/data/soql/query/dataSoqlQueryCommand.test.ts @@ -8,13 +8,12 @@ import { expect, test } from '@salesforce/command/lib/test'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; +import { SinonSandbox, SinonStub } from 'sinon'; +import { describe } from 'mocha'; import sinon = require('sinon'); -import { SinonSandbox } from 'sinon'; import { SoqlQuery } from '../../../../../../src/commands/force/data/soql/query'; import { soqlQueryExemplars } from '../../../../../test-files/soqlQuery.exemplars'; -/* eslint-disable @typescript-eslint/no-explicit-any */ - chai.use(chaiAsPromised); const QUERY_COMMAND = 'force:data:soql:query'; @@ -30,10 +29,10 @@ describe('Execute a SOQL statement', function (): void { sandbox = sinon.createSandbox(); }); describe('handle query results', () => { - let soqlQuerySpy: any; + let soqlQueryStub: SinonStub; describe('handle empty results', () => { beforeEach(() => { - soqlQuerySpy = sandbox + soqlQueryStub = sandbox .stub(SoqlQuery.prototype, 'runSoqlQuery') .callsFake(() => Promise.resolve(soqlQueryExemplars.emptyQuery.soqlQueryResult)); }); @@ -46,7 +45,7 @@ describe('Execute a SOQL statement', function (): void { .stderr() .command([QUERY_COMMAND, '--targetusername', 'test@org.com', '--query', 'select ']) .it('should have empty results', (ctx) => { - sinon.assert.calledOnce(soqlQuerySpy); + sinon.assert.calledOnce(soqlQueryStub); expect(ctx.stdout).to.include('records retrieved: 0'); }); test @@ -55,7 +54,7 @@ describe('Execute a SOQL statement', function (): void { .stderr() .command([QUERY_COMMAND, '--targetusername', 'test@org.com', '--query', 'select ', '--resultformat', 'json']) .it('should have 0 totalSize and 0 records for empty result with json reporter', (ctx) => { - sinon.assert.calledOnce(soqlQuerySpy); + sinon.assert.calledOnce(soqlQueryStub); const jsonResults = JSON.parse(ctx.stdout) as QueryResult; expect(jsonResults).to.have.property('status', 0); expect(jsonResults.result).to.have.property('totalSize', 0); @@ -64,7 +63,7 @@ describe('Execute a SOQL statement', function (): void { }); describe('reporters produce the correct results for subquery', () => { beforeEach(() => { - soqlQuerySpy = sandbox + soqlQueryStub = sandbox .stub(SoqlQuery.prototype, 'runSoqlQuery') .callsFake(() => Promise.resolve(soqlQueryExemplars.subqueryAccountsAndContacts.soqlQueryResult)); }); @@ -76,7 +75,7 @@ describe('Execute a SOQL statement', function (): void { .stdout() .command([QUERY_COMMAND, '--targetusername', 'test@org.com', '--query', 'select ', '--resultformat', 'csv']) .it('should have csv results', (ctx) => { - sinon.assert.calledOnce(soqlQuerySpy); + sinon.assert.calledOnce(soqlQueryStub); // test for expected snippet in output expect(ctx.stdout).to.include( 'Contacts.totalSize,Contacts.records.3.LastName\n"Cisco Systems, Inc.",,,,,,,,\nASSMANN Electronic GmbH,,,,,,,,\n' @@ -88,7 +87,7 @@ describe('Execute a SOQL statement', function (): void { .stderr() .command([QUERY_COMMAND, '--targetusername', 'test@org.com', '--query', 'select ', '--resultformat', 'json']) .it('should have json results', (ctx) => { - sinon.assert.calledOnce(soqlQuerySpy); + sinon.assert.calledOnce(soqlQueryStub); const jsonResults = JSON.parse(ctx.stdout) as QueryResult; expect(jsonResults).to.have.property('status', 0); expect(jsonResults.result).to.have.property('totalSize', 50); @@ -100,16 +99,58 @@ describe('Execute a SOQL statement', function (): void { .stderr() .command([QUERY_COMMAND, '--targetusername', 'test@org.com', '--query', 'select ', '--resultformat', 'human']) .it('should have human results', (ctx) => { - sinon.assert.calledOnce(soqlQuerySpy); + sinon.assert.calledOnce(soqlQueryStub); // test for expected snippet in output const stdout = ctx.stdout; - expect(/.*?United Oil & Gas, UK.*?\n.*?James.*?/.test(stdout)).to.be.true; + expect(/.*?United Oil & Gas, UK.*?James.*?/.test(stdout)).to.be.true; expect(ctx.stdout).to.include('records retrieved: 50'); }); }); + + describe('human readable output for complex subqueries', () => { + beforeEach(() => { + soqlQueryStub = sandbox + .stub(SoqlQuery.prototype, 'runSoqlQuery') + .callsFake(() => Promise.resolve(soqlQueryExemplars.complexSubQuery.soqlQueryResult)); + }); + + afterEach(() => { + sandbox.restore(); + }); + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .stderr() + .command([ + QUERY_COMMAND, + '--targetusername', + 'test@org.com', + '--query', + 'SELECT Amount, Id, Name,StageName, CloseDate, (SELECT Id, ListPrice, PriceBookEntry.UnitPrice, PricebookEntry.Name, PricebookEntry.Id, PricebookEntry.product2.Family FROM OpportunityLineItems) FROM Opportunity', + ]) + .it('should have human results for a complex subquery', (ctx) => { + sinon.assert.calledOnce(soqlQueryStub); + // test for expected snippet in output + const stdout = ctx.stdout; + // properly expanded columns from query + expect( + /.*?AMOUNT.*?ID.*?OPPORTUNITYLINEITEMS.ID.*?OPPORTUNITYLINEITEMS.PRICEBOOKENTRY.PRODUCT2.FAMILY.*?/.test( + stdout + ) + ).to.be.true; + // was able to parse the data for each column + expect( + /.*?1300.*?0063F00000RdvMKQAZ.*?My Opportunity.*?00k3F000007kBoDQAU.*?MyProduct.*?01u3F00000AwCfuQAF.*?/.test( + stdout + ) + ).to.be.true; + expect(ctx.stdout).to.include('records retrieved: 1'); + }); + }); describe('reporters produce the correct aggregate query', () => { beforeEach(() => { - soqlQuerySpy = sandbox + soqlQueryStub = sandbox .stub(SoqlQuery.prototype, 'runSoqlQuery') .callsFake(() => Promise.resolve(soqlQueryExemplars.queryWithAgregates.soqlQueryResult)); }); @@ -122,7 +163,7 @@ describe('Execute a SOQL statement', function (): void { .stderr() .command([QUERY_COMMAND, '--targetusername', 'test@org.com', '--query', 'select ', '--resultformat', 'json']) .it('should have json results', (ctx) => { - sinon.assert.calledOnce(soqlQuerySpy); + sinon.assert.calledOnce(soqlQueryStub); const jsonResults = JSON.parse(ctx.stdout) as QueryResult; expect(jsonResults).to.have.property('status', 0); expect(jsonResults.result).to.have.property('totalSize', 16); @@ -134,7 +175,7 @@ describe('Execute a SOQL statement', function (): void { .stderr() .command([QUERY_COMMAND, '--targetusername', 'test@org.com', '--query', 'select ', '--resultformat', 'human']) .it('should have human results', (ctx) => { - sinon.assert.calledOnce(soqlQuerySpy); + sinon.assert.calledOnce(soqlQueryStub); expect(/.*?United Oil & Gas Corp..*?5600000000.*/.test(ctx.stdout)).to.be.true; expect(ctx.stdout).to.include('records retrieved: 16'); }); diff --git a/test/reporter.test.ts b/test/reporter.test.ts index 7b160a41..46150d85 100644 --- a/test/reporter.test.ts +++ b/test/reporter.test.ts @@ -11,9 +11,9 @@ import * as chaiAsPromised from 'chai-as-promised'; import { UX } from '@salesforce/command'; import { Logger } from '@salesforce/core'; import { get, getPlainObject } from '@salesforce/ts-types'; +import { createSandbox } from 'sinon'; import { Field, SoqlQueryResult } from '../src/dataSoqlQueryTypes'; -import { HumanReporter } from '../src/reporters'; -import { CsvReporter } from '../src/reporters'; +import { CsvReporter, HumanReporter } from '../src/reporters'; import { soqlQueryExemplars } from './test-files/soqlQuery.exemplars'; chai.use(chaiAsPromised); @@ -47,6 +47,30 @@ describe('reporter tests', () => { reporter.prepNullValues(massagedRows); expect(massagedRows).to.be.ok; }); + + it('stringifies JSON results correctly', async () => { + queryData = soqlQueryExemplars.queryWithNestedObject.soqlQueryResult; + const dataSoqlQueryResult: SoqlQueryResult = { + columns: queryData.columns, + query: queryData.query, + result: queryData.result, + }; + const sb = createSandbox(); + const reflectSpy = sb.spy(Reflect, 'set'); + reporter = new HumanReporter(dataSoqlQueryResult, queryData.columns, await UX.create(), logger); + const { attributeNames, children, aggregates } = reporter.parseFields(); + expect(attributeNames).to.be.ok; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const massagedRows = reporter.massageRows(queryData.result.records, children, aggregates); + expect(massagedRows).to.be.deep.equal(queryData.result.records); + reporter.prepNullValues(massagedRows); + expect(massagedRows).to.be.ok; + // would be called 6 times if not set correctly in massageRows + expect(reflectSpy.callCount).to.equal(3); + expect(massagedRows).to.not.include('object Object'); + }); + it('preps columns for display values in result', () => {}); }); describe('csv reporter tests', () => { @@ -61,6 +85,33 @@ describe('reporter tests', () => { }; reporter = new CsvReporter(dataSoqlQueryResult, queryData.columns, await UX.create(), logger); }); + + it('stringifies JSON results correctly', async () => { + queryData = soqlQueryExemplars.queryWithNestedObject.soqlQueryResult; + const dataSoqlQueryResult: SoqlQueryResult = { + columns: queryData.columns, + query: queryData.query, + result: queryData.result, + }; + const sb = createSandbox(); + reporter = new CsvReporter(dataSoqlQueryResult, queryData.columns, await UX.create(), logger); + const escapeSpy = sb.spy(reporter, 'escape'); + const logStub = sb.stub(reporter, 'log'); + + const massagedRows = reporter.massageRows(); + const data = getPlainObject(reporter, 'data'); + reporter.display(); + // callCount would be 5 if were printing '[object Object]' instead of the string representation + expect(escapeSpy.callCount).to.equal(8); + expect(logStub.called).to.be.true; + expect(massagedRows).to.be.deep.equal( + soqlQueryExemplars.queryWithNestedObject.soqlQueryResult.columns.map((column: Field) => column.name) + ); + expect(get(data, 'result.records')).be.equal( + soqlQueryExemplars.queryWithNestedObject.soqlQueryResult.result.records + ); + }); + it('massages report results', () => { const massagedRows = reporter.massageRows(); const data = getPlainObject(reporter, 'data'); diff --git a/test/test-files/soqlQuery.exemplars.ts b/test/test-files/soqlQuery.exemplars.ts index 80dfa44e..07ead3c4 100644 --- a/test/test-files/soqlQuery.exemplars.ts +++ b/test/test-files/soqlQuery.exemplars.ts @@ -2242,4 +2242,172 @@ export const soqlQueryExemplars = { }, }, }, + queryWithNestedObject: { + soqlQueryResult: { + query: 'select id, Metadata from RemoteProxy', + columns: [ + { + fieldType: 0, + name: 'Id', + }, + { + fieldType: 0, + name: 'Metadata', + }, + ], + result: { + done: true, + totalSize: 3, + records: [ + { + attributes: { + type: 'RemoteProxy', + url: '/services/data/v53.0/tooling/sobjects/RemoteProxy/0rpJ0000000SIabIAG', + }, + Id: '0rpJ0000000SIabIAG', + Metadata: { + disableProtocolSecurity: false, + isActive: true, + url: 'http://www.apexdevnet.com', + urls: null, + description: null, + }, + }, + { + attributes: { + type: 'RemoteProxy', + url: '/services/data/v53.0/tooling/sobjects/RemoteProxy/0rpJ0000000SJdBIAW', + }, + Id: '0rpJ0000000SJdBIAW', + Metadata: { + disableProtocolSecurity: false, + isActive: true, + url: 'https://nominatim.openstreetmap.org', + urls: null, + description: null, + }, + }, + { + attributes: { + type: 'RemoteProxy', + url: '/services/data/v53.0/tooling/sobjects/RemoteProxy/0rpJ0000000SLlVIAW', + }, + Id: '0rpJ0000000SLlVIAW', + Metadata: { + disableProtocolSecurity: false, + isActive: false, + url: 'https://www.google.com', + urls: null, + description: null, + }, + }, + ], + }, + }, + }, + complexSubQuery: { + soqlQueryResult: { + query: + 'SELECT Amount, Id, Name,StageName, CloseDate, (SELECT Id, ListPrice, PriceBookEntry.UnitPrice, PricebookEntry.Name, PricebookEntry.Id, PricebookEntry.product2.Family FROM OpportunityLineItems) FROM Opportunity', + columns: [ + { + fieldType: 0, + name: 'Amount', + }, + { + fieldType: 0, + name: 'Id', + }, + { + fieldType: 0, + name: 'Name', + }, + { + fieldType: 0, + name: 'StageName', + }, + { + fieldType: 0, + name: 'CloseDate', + }, + { + fieldType: 1, + name: 'OpportunityLineItems', + fields: [ + { + fieldType: 0, + name: 'Id', + }, + { + fieldType: 0, + name: 'ListPrice', + }, + { + fieldType: 0, + name: 'PricebookEntry.UnitPrice', + }, + { + fieldType: 0, + name: 'PricebookEntry.Name', + }, + { + fieldType: 0, + name: 'PricebookEntry.Id', + }, + { + fieldType: 0, + name: 'PricebookEntry.Product2.Family', + }, + ], + }, + ], + result: { + done: true, + totalSize: 1, + records: [ + { + attributes: { + type: 'Opportunity', + url: '/services/data/v53.0/sobjects/Opportunity/0063F00000RdvMKQAZ', + }, + Amount: 1300, + Id: '0063F00000RdvMKQAZ', + Name: 'My Opportunity', + StageName: 'Prospecting', + CloseDate: '2022-02-01', + OpportunityLineItems: { + totalSize: 1, + done: true, + records: [ + { + attributes: { + type: 'OpportunityLineItem', + url: '/services/data/v53.0/sobjects/OpportunityLineItem/00k3F000007kBoDQAU', + }, + Id: '00k3F000007kBoDQAU', + ListPrice: 1300, + PricebookEntry: { + attributes: { + type: 'PricebookEntry', + url: '/services/data/v53.0/sobjects/PricebookEntry/01u3F00000AwCfuQAF', + }, + UnitPrice: 1300, + Name: 'MyProduct', + Id: '01u3F00000AwCfuQAF', + Product2: { + attributes: { + type: 'Product2', + url: '/services/data/v53.0/sobjects/Product2/01t3F00000AM2qaQAD', + }, + Family: 'None', + }, + }, + }, + ], + }, + }, + ], + }, + }, + }, };