salesforce/lib/query.js (215 lines of code) (raw):

// Copyright Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. "use strict"; const _ = require("lodash/core"); const pluralize = require("pluralize"); const QueryBuilder = require("./query-builder"); const queryBuilder = new QueryBuilder(); const ResponseTargets = { "QueryResult": "QueryResult", "Records": "Records", "SingleRecord": "SingleRecord", "Count": "Count" }; /** * Query class to keep the API session information and manage requests * @public * @param {Object} [conn] - Connection Object * @param {Object|String} [config] - Query config * @param {Object} [options] - Query options */ module.exports = class { constructor(conn, config, options) { this._conn = conn; this._config = config || {}; if (_.isString(config)) { this._soql = config; this._config = {}; } this._options = _.defaults(options || {}, { maxFetch: 10000, scanAll: false, responseTarget: ResponseTargets.QueryResult }); } /** * Return SOQL expression for the query * @private * @returns {Promise.<String>} */ toSOQL() { return Promise.resolve(this._soql ? this._soql : queryBuilder.createQuery(this._config)); } /** * Executes Query * @public * @param {Object} [options] - Query options * @param {Number} [options.maxFetch] - Max fetching records in auto fetch mode * @param {String} [options.responseTarget] - SingleRecord|Records|Count and Default (QueryResult) * @param {Object} [options.headers] - Additional HTTP request headers sent in query request * @returns {Promise<QueryResult>} */ run(options) { options = options || {}; let logger = this._conn.logger; let responseTarget = options.responseTarget || this._options.responseTarget; // let maxFetch = options.maxFetch || this._options.maxFetch; return Promise.resolve( this._locator ? this._conn.baseUrl() + "/query/" + this._locator : this.toSOQL() .then((soql) => { this.totalFetched = 0; logger.debug("SOQL = " + soql); return this._conn.baseUrl() + "/query?q=" + encodeURIComponent(soql); }) ).then((url) => this._conn.request({ method: "GET", url: url, headers: options.headers || this._options.headers })).then((data) => { let response; switch (responseTarget) { case ResponseTargets.SingleRecord: response = data.records && data.records.length > 0 ? data.records[0] : null; break; case ResponseTargets.Records: response = data.records; break; case ResponseTargets.Count: response = data.totalSize; break; default: response = data; break; } if (data.nextRecordsUrl) { this._locator = data.nextRecordsUrl.split("/").pop(); } return response; }); } /** * Select fields to include in the returning result * @public * @param {Object|Array.<String>|String} fields - Fields to fetch. Format can be in JSON object (MongoDB-like), array of field names, or comma-separated field names. * @returns {Query.<T>} */ select(fields) { if (this._soql) { throw Error("Cannot set select fields for the query which has already built SOQL."); } if (_.isString(fields)) { fields = fields.split(/\s*,\s*/); } else if (_.isObject(fields) && !_.isArray(fields)) { let tempFields = []; for (let k in fields) { if (fields[k]) { tempFields.push(k); } } fields = tempFields; } this._config.fields = fields; return this; } /** * Select all fields to include in the returning result * @public * @returns {Promise} */ selectAll() { if (this._soql) { throw Error("Cannot set select fields for the query which has already built SOQL."); } return new Promise((resolve, reject) => { this._conn.getCollectionDetails(this._config.table).then(collection => { let fields = []; collection.fields.forEach(field => { fields.push(field.name); }); this._config.fields = fields; resolve(this); }).catch(err => reject(err)); }); } /** * Set query conditions to filter the result records * @public * @param {Object|String} conditions - Conditions in JSON object (MongoDB-like), or raw SOQL WHERE clause string. * @returns {Query.<T>} */ 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 * @public * @param {Number} limit - Maximum number of records the query will return. * @returns {Query.<T>} */ 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 the starting rows of the result * @public * @param {Number} rows - Number of rows to skip. * @returns {Query.<QueryResult>} */ skip(rows) { if (this._soql) { throw Error("Cannot set offset for the query which has already built SOQL."); } this._config.offset = rows; return this; } /** * Set query conditions to sort the result records * @public * @param {Object|String} conditions - Conditions in JSON object (MongoDB-like), or raw SOQL ORDER BY clause string. * @returns {Query.<T>} */ sortBy(conditions) { if (this._soql) { throw Error("Cannot write sort for the query which has already built SOQL."); } this._config.sort = conditions; return this; } /** * Set query conditions to create a new Record * @public * @param {Object} body - Request payload JSON object. * @returns {Promise} Returns request promise */ create(body) { return this._conn.request({ method: "POST", url: `${this._conn.baseUrl()}/sobjects/${this._config.table}`, headers: this._options.headers, body: body, json: true }); } /** * Set query conditions to update a new Record * @public * @param {String} id - Unique ID of the record to be updated. * @param {Object} body - Request payload JSON object. * @returns {Promise} Returns request promise */ update(id, body) { return this._conn.request({ method: "PATCH", url: `${this._conn.baseUrl()}/sobjects/${this._config.table}/${id}`, headers: this._options.headers, body: body, json: true }); } /** *Method delete * @public * @param {String} id - Id of the record to be deleted * @returns {Promise} - request method connection.js */ delete(id) { return new Promise((resolve, reject) => { this.exists(id).then(result => { if (result) { resolve(this._conn.request({ method: "DELETE", url: `${this._conn.baseUrl()}/sobjects/${this._config.table}/${id}`, headers: this._options.headers })); } else { reject("Entry to be deleted does not exist"); } }).catch(error => reject(error)); }); } /** * Method upsert(Insert if record doesn"t exist and update if exists) * @public * @param {Object} body - Request payload JSON object. * @returns {Promise} Returns request promise */ upsert(body) { return new Promise((resolve, reject) => { let id = body.Id; delete body.Id; if (id) { this.exists(id).then(result => { if (result) { resolve(this.update(id, body)); } else { resolve(this.create(body)); } }).catch(error => reject(error)); } else { resolve(this.create(body)); } }); } /** * Method to check if the record exists. * @public * @param {String} id - Id of the record to check whether there or not. * @returns {Promise} Returns request promise */ exists(id) { return new Promise((resolve, reject) => { this.select("Id, Name").where({ Id: id }).run().then(instance => { if (instance.records.length > 0) { resolve(true); } else { resolve(false); } }).catch(error => reject(error)); }); } /** * Method to populate Child > Parent (Standard Object) field * @public * @param {String|Array} options The parent table to populate * @returns {Query.<QueryResult>} */ populateChild(options) { if (_.isString(options)) { options = options.split(/\s*,\s*/); } let tempFields = []; options.forEach(option => { option += ".name"; tempFields.push(option); }); options = tempFields; this._config.fields = this._config.fields.concat(options); return this; } /** * Method to populate Parent > Child (Standard Object) field * @public * @param {Object} conditions * Format to be * { child: {table: "Table_Name", field: "Field1"}, parent: {field: "Field"}} * @returns {Query.<QueryResult>} */ populateParent(conditions) { this._config.innerjoin = conditions; return this; } /** * Joining Two or more tables * @public * @param {Array} options * Format to be * [{table: "Table_Name", fields: ["Field1", "Field2"]|"Field1, Field2"}, condition: "SOQL String"(optional field)] * @returns {Query<QueryResult>} */ joinTable(options) { let tempFields = []; options.forEach(option => { let condition = option.condition || ""; tempFields.push("(Select " + option.fields.map(field => field = option.table + "." + field).join(",") + " FROM " + pluralize(option.table).toLowerCase() + " " + condition + " )"); }); this._config.fields = this._config.fields.concat(tempFields); return this; } };