UNPKG

@jsforce/jsforce-node

Version:

Salesforce API Library for JavaScript

819 lines (818 loc) 28.1 kB
"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;