@jsforce/jsforce-node
Version:
Salesforce API Library for JavaScript
819 lines (818 loc) • 28.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubQuery = exports.Query = exports.ResponseTargets = void 0;
/**
* @file Manages query for records in Salesforce
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
*/
const events_1 = require("events");
const logger_1 = require("./util/logger");
const record_stream_1 = __importStar(require("./record-stream"));
const soql_builder_1 = require("./soql-builder");
const ResponseTargetValues = [
'QueryResult',
'Records',
'SingleRecord',
'Count',
];
exports.ResponseTargets = ResponseTargetValues.reduce((values, target) => ({ ...values, [target]: target }), {});
/**
*
*/
const DEFAULT_BULK_THRESHOLD = 200;
const DEFAULT_BULK_API_VERSION = 1;
/**
* Query
*/
class Query extends events_1.EventEmitter {
static _logger = (0, logger_1.getLogger)('query');
_conn;
_logger;
_soql;
_locator;
_config = {};
_children = [];
_options;
_executed = false;
_finished = false;
_chaining = false;
_promise;
_stream;
totalSize = 0;
totalFetched = 0;
records = [];
/**
*
*/
constructor(conn, config, options) {
super();
this._conn = conn;
this._logger = conn._logLevel
? Query._logger.createInstance(conn._logLevel)
: Query._logger;
if (typeof config === 'string') {
this._soql = config;
this._logger.debug(`config is soql: ${config}`);
}
else if (typeof config.locator === 'string') {
const locator = config.locator;
this._logger.debug(`config is locator: ${locator}`);
this._locator = locator.includes('/')
? this.urlToLocator(locator)
: locator;
}
else {
this._logger.debug(`config is QueryConfig: ${JSON.stringify(config)}`);
const { fields, includes, sort, ..._config } = config;
this._config = _config;
this.select(fields);
if (includes) {
this.includeChildren(includes);
}
if (sort) {
this.sort(sort);
}
}
this._options = {
headers: {},
maxFetch: 10000,
autoFetch: false,
scanAll: false,
responseTarget: 'QueryResult',
...(options || {}),
};
// promise instance
this._promise = new Promise((resolve, reject) => {
this.on('response', resolve);
this.on('error', reject);
});
this._stream = new record_stream_1.Serializable();
this.on('record', (record) => this._stream.push(record));
this.on('end', () => this._stream.push(null));
this.on('error', (err) => {
try {
this._stream.emit('error', err);
}
catch (e) {
// eslint-disable-line no-empty
}
});
}
/**
* Select fields to include in the returning result
*/
select(fields = '*') {
if (this._soql) {
throw Error('Cannot set select fields for the query which has already built SOQL.');
}
function toFieldArray(fields) {
return typeof fields === 'string'
? fields.split(/\s*,\s*/)
: Array.isArray(fields)
? fields
.map(toFieldArray)
.reduce((fs, f) => [...fs, ...f], [])
: Object.entries(fields)
.map(([f, v]) => {
if (typeof v === 'number' || typeof v === 'boolean') {
return v ? [f] : [];
}
else {
return toFieldArray(v).map((p) => `${f}.${p}`);
}
})
.reduce((fs, f) => [...fs, ...f], []);
}
if (fields) {
this._config.fields = toFieldArray(fields);
}
// force convert query record type without changing instance;
return this;
}
/**
* Set query conditions to filter the result records
*/
where(conditions) {
if (this._soql) {
throw Error('Cannot set where conditions for the query which has already built SOQL.');
}
this._config.conditions = conditions;
return this;
}
/**
* Limit the returning result
*/
limit(limit) {
if (this._soql) {
throw Error('Cannot set limit for the query which has already built SOQL.');
}
this._config.limit = limit;
return this;
}
/**
* Skip records
*/
skip(offset) {
if (this._soql) {
throw Error('Cannot set skip/offset for the query which has already built SOQL.');
}
this._config.offset = offset;
return this;
}
/**
* Synonym of Query#skip()
*/
offset = this.skip;
sort(sort, dir) {
if (this._soql) {
throw Error('Cannot set sort for the query which has already built SOQL.');
}
if (typeof sort === 'string' && typeof dir !== 'undefined') {
this._config.sort = [[sort, dir]];
}
else {
this._config.sort = sort;
}
return this;
}
/**
* Synonym of Query#sort()
*/
orderby = this.sort;
include(childRelName, conditions, fields, options = {}) {
if (this._soql) {
throw Error('Cannot include child relationship into the query which has already built SOQL.');
}
const childConfig = {
fields: fields === null ? undefined : fields,
table: childRelName,
conditions: conditions === null ? undefined : conditions,
limit: options.limit,
offset: options.offset,
sort: options.sort,
};
// eslint-disable-next-line no-use-before-define
const childQuery = new SubQuery(this._conn, childRelName, childConfig, this);
this._children.push(childQuery);
return childQuery;
}
/**
* Include child relationship queries, but not moving down to the children context
*/
includeChildren(includes) {
if (this._soql) {
throw Error('Cannot include child relationship into the query which has already built SOQL.');
}
for (const crname of Object.keys(includes)) {
const { conditions, fields, ...options } = includes[crname];
this.include(crname, conditions, fields, options);
}
return this;
}
/**
* Setting maxFetch query option
*/
maxFetch(maxFetch) {
this._options.maxFetch = maxFetch;
return this;
}
/**
* Switching auto fetch mode
*/
autoFetch(autoFetch) {
this._options.autoFetch = autoFetch;
return this;
}
/**
* Set flag to scan all records including deleted and archived.
*/
scanAll(scanAll) {
this._options.scanAll = scanAll;
return this;
}
/**
*
*/
setResponseTarget(responseTarget) {
if (responseTarget in exports.ResponseTargets) {
this._options.responseTarget = responseTarget;
}
// force change query response target without changing instance
return this;
}
/**
* Execute query and fetch records from server.
*/
execute(options_ = {}) {
if (this._executed) {
throw new Error('re-executing already executed query');
}
if (this._finished) {
throw new Error('executing already closed query');
}
const options = {
headers: options_.headers || this._options.headers,
responseTarget: options_.responseTarget || this._options.responseTarget,
autoFetch: options_.autoFetch || this._options.autoFetch,
maxFetch: options_.maxFetch || this._options.maxFetch,
scanAll: options_.scanAll || this._options.scanAll,
};
// collect fetched records in array
// only when response target is Records and
// either callback or chaining promises are available to this query.
this.once('fetch', () => {
if (options.responseTarget === exports.ResponseTargets.Records &&
this._chaining) {
this._logger.debug('--- collecting all fetched records ---');
const records = [];
const onRecord = (record) => records.push(record);
this.on('record', onRecord);
this.once('end', () => {
this.removeListener('record', onRecord);
this.emit('response', records, this);
});
}
});
// flag to prevent re-execution
this._executed = true;
(async () => {
// start actual query
this._logger.debug('>>> Query start >>>');
try {
await this._execute(options);
this._logger.debug('*** Query finished ***');
}
catch (error) {
this._logger.debug('--- Query error ---', error);
this.emit('error', error);
}
})();
// return Query instance for chaining
return this;
}
/**
* Synonym of Query#execute()
*/
exec = this.execute;
/**
* Synonym of Query#execute()
*/
run = this.execute;
locatorToUrl() {
return this._locator
? [this._conn._baseUrl(), '/query/', this._locator].join('')
: '';
}
urlToLocator(url) {
return url.split('/').pop();
}
constructResponse(rawDone, responseTarget) {
switch (responseTarget) {
case 'Count':
return this.totalSize;
case 'SingleRecord':
return this.records?.[0] ?? null;
case 'Records':
return this.records;
// QueryResult is default response target
default:
return {
...{
records: this.records,
totalSize: this.totalSize,
done: rawDone ?? true, // when no records, done is omitted
},
...(this._locator ? { nextRecordsUrl: this.locatorToUrl() } : {}),
};
}
}
/**
* @private
*/
async _execute(options) {
const { headers, responseTarget, autoFetch, maxFetch, scanAll } = options;
this._logger.debug('execute with options', options);
let url;
if (this._locator) {
url = this.locatorToUrl();
}
else {
const soql = await this.toSOQL();
this._logger.debug(`SOQL = ${soql}`);
url = [
this._conn._baseUrl(),
'/',
scanAll ? 'queryAll' : 'query',
'?q=',
encodeURIComponent(soql),
].join('');
}
const data = await this._conn.request({ method: 'GET', url, headers });
this.emit('fetch');
this.totalSize = data.totalSize;
this.records = this.records?.concat(maxFetch - this.records.length > data.records.length
? data.records
: data.records.slice(0, maxFetch - this.records.length));
this._locator = data.nextRecordsUrl
? this.urlToLocator(data.nextRecordsUrl)
: undefined;
this._finished =
this._finished ||
data.done ||
!autoFetch ||
this.records.length === maxFetch ||
// this is what the response looks like when there are no results
(data.records.length === 0 && data.done === undefined);
// streaming record instances
const numRecords = data.records?.length ?? 0;
let totalFetched = this.totalFetched;
for (let i = 0; i < numRecords; i++) {
if (totalFetched >= maxFetch) {
this._finished = true;
break;
}
const record = data.records[i];
this.emit('record', record, totalFetched, this);
totalFetched += 1;
}
this.totalFetched = totalFetched;
if (this._finished) {
const response = this.constructResponse(data.done, responseTarget);
// only fire response event when it should be notified per fetch
if (responseTarget !== exports.ResponseTargets.Records) {
this.emit('response', response, this);
}
this.emit('end');
return response;
}
else {
return this._execute(options);
}
}
stream(type = 'csv') {
if (!this._finished && !this._executed) {
this.execute({ autoFetch: true });
}
return type === 'record' ? this._stream : this._stream.stream(type);
}
/**
* Pipe the queried records to another stream
* This is for backward compatibility; Query is not a record stream instance anymore in 2.0.
* If you want a record stream instance, use `Query#stream('record')`.
*/
pipe(stream) {
return this.stream('record').pipe(stream);
}
/**
* @protected
*/
async _expandFields(sobject_) {
if (this._soql) {
throw new Error('Cannot expand fields for the query which has already built SOQL.');
}
const { fields = [], table = '' } = this._config;
const sobject = sobject_ || table;
this._logger.debug(`_expandFields: sobject = ${sobject}, fields = ${fields.join(', ')}`);
const [efields] = await Promise.all([
this._expandAsteriskFields(sobject, fields),
...this._children.map(async (childQuery) => {
await childQuery._expandFields();
return [];
}),
]);
this._config.fields = efields;
this._config.includes = this._children
.map((cquery) => {
const cconfig = cquery._query._config;
return [cconfig.table, cconfig];
})
.reduce((includes, [ctable, cconfig]) => ({
...includes,
[ctable]: cconfig,
}), {});
}
/**
*
*/
async _findRelationObject(relName) {
const table = this._config.table;
if (!table) {
throw new Error('No table information provided in the query');
}
this._logger.debug(`finding table for relation "${relName}" in "${table}"...`);
const sobject = await this._conn.describe$(table);
const upperRname = relName.toUpperCase();
for (const cr of sobject.childRelationships) {
if ((cr.relationshipName || '').toUpperCase() === upperRname &&
cr.childSObject) {
return cr.childSObject;
}
}
throw new Error(`No child relationship found: ${relName}`);
}
/**
*
*/
async _expandAsteriskFields(sobject, fields) {
const expandedFields = await Promise.all(fields.map(async (field) => this._expandAsteriskField(sobject, field)));
return expandedFields.reduce((eflds, flds) => [...eflds, ...flds], []);
}
/**
*
*/
async _expandAsteriskField(sobject, field) {
this._logger.debug(`expanding field "${field}" in "${sobject}"...`);
const fpath = field.split('.');
if (fpath[fpath.length - 1] === '*') {
const so = await this._conn.describe$(sobject);
this._logger.debug(`table ${sobject} has been described`);
if (fpath.length > 1) {
const rname = fpath.shift();
for (const f of so.fields) {
if (f.relationshipName &&
rname &&
f.relationshipName.toUpperCase() === rname.toUpperCase()) {
const rfield = f;
const referenceTo = rfield.referenceTo || [];
const rtable = referenceTo.length === 1 ? referenceTo[0] : 'Name';
const fpaths = await this._expandAsteriskField(rtable, fpath.join('.'));
return fpaths.map((fp) => `${rname}.${fp}`);
}
}
return [];
}
return so.fields.map((f) => f.name);
}
return [field];
}
/**
* Explain plan for executing query
*/
async explain() {
const soql = await this.toSOQL();
this._logger.debug(`SOQL = ${soql}`);
const url = `/query/?explain=${encodeURIComponent(soql)}`;
return this._conn.request(url);
}
/**
* Return SOQL expression for the query
*/
async toSOQL() {
if (this._soql) {
return this._soql;
}
await this._expandFields();
return (0, soql_builder_1.createSOQL)(this._config);
}
/**
* Promise/A+ interface
* http://promises-aplus.github.io/promises-spec/
*
* Delegate to deferred promise, return promise instance for query result
*/
then(onResolve, onReject) {
this._chaining = true;
if (!this._finished && !this._executed) {
this.execute();
}
if (!this._promise) {
throw new Error('invalid state: promise is not set after query execution');
}
return this._promise.then(onResolve, onReject);
}
catch(onReject) {
return this.then(null, onReject);
}
promise() {
// TODO(cristian): verify this is correct
return Promise.resolve(this);
}
destroy(type, options) {
if (typeof type === 'object' && type !== null) {
options = type;
type = undefined;
}
options = options || {};
const type_ = type || this._config.table;
if (!type_) {
throw new Error('SOQL based query needs SObject type information to bulk delete.');
}
// Set the threshold number to pass to bulk API
const thresholdNum = options.allowBulk === false
? -1
: typeof options.bulkThreshold === 'number'
? options.bulkThreshold
: // determine threshold if the connection version supports SObject collection API or not
this._conn._ensureVersion(42)
? DEFAULT_BULK_THRESHOLD
: this._conn._maxRequest / 2;
const bulkApiVersion = options.bulkApiVersion ?? DEFAULT_BULK_API_VERSION;
return new Promise((resolve, reject) => {
const createBatch = () => this._conn
.sobject(type_)
.deleteBulk()
.on('response', resolve)
.on('error', reject);
let records = [];
let batch = null;
const handleRecord = (rec) => {
if (!rec.Id) {
const err = new Error('Queried record does not include Salesforce record ID.');
this.emit('error', err);
return;
}
const record = { Id: rec.Id };
if (batch) {
batch.write(record);
}
else {
records.push(record);
if (thresholdNum >= 0 &&
records.length > thresholdNum &&
bulkApiVersion === 1) {
// Use bulk delete instead of SObject REST API
batch = createBatch();
for (const record of records) {
batch.write(record);
}
records = [];
}
}
};
const handleEnd = () => {
if (batch) {
batch.end();
}
else {
const ids = records.map((record) => record.Id);
if (records.length > thresholdNum && bulkApiVersion === 2) {
this._conn.bulk2
.loadAndWaitForResults({
object: type_,
operation: 'delete',
input: records,
})
.then((allResults) => resolve(this.mapBulkV2ResultsToSaveResults(allResults)), reject);
}
else {
this._conn
.sobject(type_)
.destroy(ids, { allowRecursive: true })
.then(resolve, reject);
}
}
};
this.stream('record')
.on('data', handleRecord)
.on('end', handleEnd)
.on('error', reject);
});
}
/**
* Synonym of Query#destroy()
*/
delete = this.destroy;
/**
* Synonym of Query#destroy()
*/
del = this.destroy;
update(mapping, type, options) {
if (typeof type === 'object' && type !== null) {
options = type;
type = undefined;
}
options = options || {};
const type_ = type || (this._config && this._config.table);
if (!type_) {
throw new Error('SOQL based query needs SObject type information to bulk update.');
}
const updateStream = typeof mapping === 'function'
? record_stream_1.default.map(mapping)
: record_stream_1.default.recordMapStream(mapping, options.skipRecordTemplateEval);
// Set the threshold number to pass to bulk API
const thresholdNum = options.allowBulk === false
? -1
: typeof options.bulkThreshold === 'number'
? options.bulkThreshold
: // determine threshold if the connection version supports SObject collection API or not
this._conn._ensureVersion(42)
? DEFAULT_BULK_THRESHOLD
: this._conn._maxRequest / 2;
const bulkApiVersion = options.bulkApiVersion ?? DEFAULT_BULK_API_VERSION;
return new Promise((resolve, reject) => {
const createBatch = () => this._conn
.sobject(type_)
.updateBulk()
.on('response', resolve)
.on('error', reject);
let records = [];
let batch = null;
const handleRecord = (record) => {
if (batch) {
batch.write(record);
}
else {
records.push(record);
}
if (thresholdNum >= 0 &&
records.length > thresholdNum &&
bulkApiVersion === 1) {
// Use bulk update instead of SObject REST API
batch = createBatch();
for (const record of records) {
batch.write(record);
}
records = [];
}
};
const handleEnd = () => {
if (batch) {
batch.end();
}
else {
if (records.length > thresholdNum && bulkApiVersion === 2) {
this._conn.bulk2
.loadAndWaitForResults({
object: type_,
operation: 'update',
input: records,
})
.then((allResults) => resolve(this.mapBulkV2ResultsToSaveResults(allResults)), reject);
}
else {
this._conn
.sobject(type_)
.update(records, { allowRecursive: true })
.then(resolve, reject);
}
}
};
this.stream('record')
.on('error', reject)
.pipe(updateStream)
.on('data', handleRecord)
.on('end', handleEnd)
.on('error', reject);
});
}
mapBulkV2ResultsToSaveResults(bulkJobAllResults) {
const successSaveResults = bulkJobAllResults.successfulResults.map((r) => {
const saveResult = {
id: r.sf__Id,
success: true,
errors: [],
};
return saveResult;
});
const failedSaveResults = bulkJobAllResults.failedResults.map((r) => {
const saveResult = {
success: false,
errors: [
{
errorCode: r.sf__Error,
message: r.sf__Error,
},
],
};
return saveResult;
});
return [...successSaveResults, ...failedSaveResults];
}
}
exports.Query = Query;
/*--------------------------------------------*/
/**
* SubQuery object for representing child relationship query
*/
class SubQuery {
_relName;
_query;
_parent;
/**
*
*/
constructor(conn, relName, config, parent) {
this._relName = relName;
this._query = new Query(conn, config);
this._parent = parent;
}
/**
*
*/
select(fields) {
// force convert query record type without changing instance
this._query = this._query.select(fields);
return this;
}
/**
*
*/
where(conditions) {
this._query = this._query.where(conditions);
return this;
}
/**
* Limit the returning result
*/
limit(limit) {
this._query = this._query.limit(limit);
return this;
}
/**
* Skip records
*/
skip(offset) {
this._query = this._query.skip(offset);
return this;
}
/**
* Synonym of SubQuery#skip()
*/
offset = this.skip;
sort(sort, dir) {
this._query = this._query.sort(sort, dir);
return this;
}
/**
* Synonym of SubQuery#sort()
*/
orderby = this.sort;
/**
*
*/
async _expandFields() {
const sobject = await this._parent._findRelationObject(this._relName);
return this._query._expandFields(sobject);
}
/**
* Back the context to parent query object
*/
end() {
return this._parent;
}
}
exports.SubQuery = SubQuery;
exports.default = Query;