@jsforce/jsforce-node
Version:
Salesforce API Library for JavaScript
1,172 lines (1,171 loc) • 42.4 kB
JavaScript
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;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Connection = void 0;
/**
*
*/
const events_1 = require("events");
const jsforce_1 = __importDefault(require("./jsforce"));
const transport_1 = __importStar(require("./transport"));
const logger_1 = require("./util/logger");
const oauth2_1 = __importDefault(require("./oauth2"));
const cache_1 = __importDefault(require("./cache"));
const http_api_1 = __importDefault(require("./http-api"));
const session_refresh_delegate_1 = __importDefault(require("./session-refresh-delegate"));
const query_1 = __importDefault(require("./query"));
const sobject_1 = __importDefault(require("./sobject"));
const quick_action_1 = __importDefault(require("./quick-action"));
const process_1 = __importDefault(require("./process"));
const formatter_1 = require("./util/formatter");
const form_data_1 = __importDefault(require("form-data"));
/**
*
*/
const defaultConnectionConfig = {
loginUrl: 'https://login.salesforce.com',
instanceUrl: '',
version: '50.0',
logLevel: 'NONE',
maxRequest: 10,
};
/**
*
*/
function esc(str) {
return String(str || '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
*
*/
function parseSignedRequest(sr) {
if (typeof sr === 'string') {
if (sr.startsWith('{')) {
// might be JSON
return JSON.parse(sr);
} // might be original base64-encoded signed request
const msg = sr.split('.').pop(); // retrieve latter part
if (!msg) {
throw new Error('Invalid signed request');
}
const json = Buffer.from(msg, 'base64').toString('utf-8');
return JSON.parse(json);
}
return sr;
}
/** @private **/
function parseIdUrl(url) {
const [organizationId, id] = url.split('/').slice(-2);
return { id, organizationId, url };
}
/**
* Session Refresh delegate function for OAuth2 authz code flow
* @private
*/
async function oauthRefreshFn(conn, callback) {
try {
if (!conn.refreshToken) {
throw new Error('No refresh token found in the connection');
}
const res = await conn.oauth2.refreshToken(conn.refreshToken);
const userInfo = parseIdUrl(res.id);
conn._establish({
instanceUrl: res.instance_url,
accessToken: res.access_token,
userInfo,
});
callback(undefined, res.access_token, res);
}
catch (err) {
if (err instanceof Error) {
callback(err);
}
else {
throw err;
}
}
}
/**
* Session Refresh delegate function for username/password login
* @private
*/
function createUsernamePasswordRefreshFn(username, password) {
return async (conn, callback) => {
try {
await conn.login(username, password);
if (!conn.accessToken) {
throw new Error('Access token not found after login');
}
callback(null, conn.accessToken);
}
catch (err) {
if (err instanceof Error) {
callback(err);
}
else {
throw err;
}
}
};
}
/**
* @private
*/
function toSaveResult(err) {
return {
success: false,
errors: [err],
};
}
/**
*
*/
function raiseNoModuleError(name) {
throw new Error(`API module '${name}' is not loaded, load 'jsforce/api/${name}' explicitly`);
}
/*
* Constant of maximum records num in DML operation (update/delete)
*/
const MAX_DML_COUNT = 200;
/**
*
*/
class Connection extends events_1.EventEmitter {
static _logger = (0, logger_1.getLogger)('connection');
version;
loginUrl;
instanceUrl;
accessToken;
refreshToken;
userInfo;
limitInfo = {};
oauth2;
sobjects = {};
cache;
_callOptions;
_maxRequest;
_logger;
_logLevel;
_transport;
_sessionType;
_refreshDelegate;
// describe: (name: string) => Promise<DescribeSObjectResult>;
describe$;
describe$$;
describeSObject;
describeSObject$;
describeSObject$$;
// describeGlobal: () => Promise<DescribeGlobalResult>;
describeGlobal$;
describeGlobal$$;
// API libs are not instantiated here so that core module to remain without dependencies to them
// It is responsible for developers to import api libs explicitly if they are using 'jsforce/core' instead of 'jsforce'.
get analytics() {
return raiseNoModuleError('analytics');
}
get apex() {
return raiseNoModuleError('apex');
}
get bulk() {
return raiseNoModuleError('bulk');
}
get bulk2() {
return raiseNoModuleError('bulk2');
}
get chatter() {
return raiseNoModuleError('chatter');
}
get metadata() {
return raiseNoModuleError('metadata');
}
get soap() {
return raiseNoModuleError('soap');
}
get streaming() {
return raiseNoModuleError('streaming');
}
get tooling() {
return raiseNoModuleError('tooling');
}
/**
*
*/
constructor(config = {}) {
super();
const { loginUrl, instanceUrl, version, oauth2, maxRequest, logLevel, proxyUrl, httpProxy, } = config;
this.loginUrl = loginUrl || defaultConnectionConfig.loginUrl;
this.instanceUrl = instanceUrl || defaultConnectionConfig.instanceUrl;
if (this.isLightningInstance()) {
throw new Error('lightning URLs are not valid as instance URLs');
}
this.version = version || defaultConnectionConfig.version;
this.oauth2 =
oauth2 instanceof oauth2_1.default
? oauth2
: new oauth2_1.default({
loginUrl: this.loginUrl,
proxyUrl,
httpProxy,
...oauth2,
});
let refreshFn = config.refreshFn;
if (!refreshFn && this.oauth2.clientId) {
refreshFn = oauthRefreshFn;
}
if (refreshFn) {
this._refreshDelegate = new session_refresh_delegate_1.default(this, refreshFn);
}
this._maxRequest = maxRequest || defaultConnectionConfig.maxRequest;
this._logger = logLevel
? Connection._logger.createInstance(logLevel)
: Connection._logger;
this._logLevel = logLevel;
this._transport = proxyUrl
? new transport_1.XdProxyTransport(proxyUrl)
: httpProxy
? new transport_1.HttpProxyTransport(httpProxy)
: new transport_1.default();
this._callOptions = config.callOptions;
this.cache = new cache_1.default();
const describeCacheKey = (type) => type ? `describe.${type}` : 'describe';
const describe = Connection.prototype.describe;
this.describe = this.cache.createCachedFunction(describe, this, {
key: describeCacheKey,
strategy: 'NOCACHE',
});
this.describe$ = this.cache.createCachedFunction(describe, this, {
key: describeCacheKey,
strategy: 'HIT',
});
this.describe$$ = this.cache.createCachedFunction(describe, this, {
key: describeCacheKey,
strategy: 'IMMEDIATE',
});
this.describeSObject = this.describe;
this.describeSObject$ = this.describe$;
this.describeSObject$$ = this.describe$$;
const describeGlobal = Connection.prototype.describeGlobal;
this.describeGlobal = this.cache.createCachedFunction(describeGlobal, this, { key: 'describeGlobal', strategy: 'NOCACHE' });
this.describeGlobal$ = this.cache.createCachedFunction(describeGlobal, this, { key: 'describeGlobal', strategy: 'HIT' });
this.describeGlobal$$ = this.cache.createCachedFunction(describeGlobal, this, { key: 'describeGlobal', strategy: 'IMMEDIATE' });
const { accessToken, refreshToken, sessionId, serverUrl, signedRequest, } = config;
this._establish({
accessToken,
refreshToken,
instanceUrl,
sessionId,
serverUrl,
signedRequest,
});
jsforce_1.default.emit('connection:new', this);
}
/* @private */
_establish(options) {
const { accessToken, refreshToken, instanceUrl, sessionId, serverUrl, signedRequest, userInfo, } = options;
this.instanceUrl = serverUrl
? serverUrl.split('/').slice(0, 3).join('/')
: instanceUrl || this.instanceUrl;
this.accessToken = sessionId || accessToken || this.accessToken;
this.refreshToken = refreshToken || this.refreshToken;
if (this.refreshToken && !this._refreshDelegate) {
throw new Error('Refresh token is specified without oauth2 client information or refresh function');
}
const signedRequestObject = signedRequest && parseSignedRequest(signedRequest);
if (signedRequestObject) {
this.accessToken = signedRequestObject.client.oauthToken;
if (transport_1.CanvasTransport.supported) {
this._transport = new transport_1.CanvasTransport(signedRequestObject);
}
}
this.userInfo = userInfo || this.userInfo;
this._sessionType = sessionId ? 'soap' : 'oauth2';
this._resetInstance();
}
/* @priveate */
_clearSession() {
this.accessToken = null;
this.refreshToken = null;
this.instanceUrl = defaultConnectionConfig.instanceUrl;
this.userInfo = null;
this._sessionType = null;
}
/* @priveate */
_resetInstance() {
this.limitInfo = {};
this.sobjects = {};
// TODO impl cache
this.cache.clear();
this.cache.get('describeGlobal').removeAllListeners('value');
this.cache.get('describeGlobal').on('value', ({ result }) => {
if (result) {
for (const so of result.sobjects) {
this.sobject(so.name);
}
}
});
/*
if (this.tooling) {
this.tooling._resetInstance();
}
*/
}
/**
* Authorize the connection using OAuth2 flow.
* Typically, just pass the code returned from authorization server in the first argument to complete authorization.
* If you want to authorize with grant types other than `authorization_code`, you can also pass params object with the grant type.
*
* @returns {Promise<UserInfo>} An object that contains the user ID, org ID and identity URL.
*
*/
async authorize(codeOrParams, params = {}) {
const res = await this.oauth2.requestToken(codeOrParams, params);
const userInfo = parseIdUrl(res.id);
this._establish({
instanceUrl: res.instance_url,
accessToken: res.access_token,
refreshToken: res.refresh_token,
userInfo,
});
this._logger.debug(`<login> completed. user id = ${userInfo.id}, org id = ${userInfo.organizationId}`);
return userInfo;
}
/**
*
*/
async login(username, password) {
this._refreshDelegate = new session_refresh_delegate_1.default(this, createUsernamePasswordRefreshFn(username, password));
if (this.oauth2?.clientId && this.oauth2.clientSecret) {
return this.loginByOAuth2(username, password);
}
return this.loginBySoap(username, password);
}
/**
* Login by OAuth2 username & password flow
*/
async loginByOAuth2(username, password) {
const res = await this.oauth2.authenticate(username, password);
const userInfo = parseIdUrl(res.id);
this._establish({
instanceUrl: res.instance_url,
accessToken: res.access_token,
userInfo,
});
this._logger.info(`<login> completed. user id = ${userInfo.id}, org id = ${userInfo.organizationId}`);
return userInfo;
}
/**
*
*/
async loginBySoap(username, password) {
if (!username || !password) {
return Promise.reject(new Error('no username password given'));
}
const body = [
'<se:Envelope xmlns:se="http://schemas.xmlsoap.org/soap/envelope/">',
'<se:Header/>',
'<se:Body>',
'<login xmlns="urn:partner.soap.sforce.com">',
`<username>${esc(username)}</username>`,
`<password>${esc(password)}</password>`,
'</login>',
'</se:Body>',
'</se:Envelope>',
].join('');
const soapLoginEndpoint = [
this.loginUrl,
'services/Soap/u',
this.version,
].join('/');
const response = await this._transport.httpRequest({
method: 'POST',
url: soapLoginEndpoint,
body,
headers: {
'Content-Type': 'text/xml',
SOAPAction: '""',
},
});
let m;
if (response.statusCode >= 400) {
m = response.body.match(/<faultstring>([^<]+)<\/faultstring>/);
const faultstring = m && m[1];
throw new Error(faultstring || response.body);
}
// the API will return 200 and a restriced token when using an expired password:
// https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_login_loginresult.htm
//
// we need to throw here to avoid a possible infinite loop with session refresh where:
// 1. login happens, `this.accessToken` is set to the restricted token
// 2. requests happen, get back 401
// 3. trigger session-refresh (username/password login has a default session refresh delegate function)
// 4. gets stuck refreshing a restricted token
if (response.body.match(/<passwordExpired>true<\/passwordExpired>/g)) {
throw new Error('Unable to login because the used password has expired.');
}
this._logger.debug(`SOAP response = ${response.body}`);
m = response.body.match(/<serverUrl>([^<]+)<\/serverUrl>/);
const serverUrl = m && m[1];
m = response.body.match(/<sessionId>([^<]+)<\/sessionId>/);
const sessionId = m && m[1];
m = response.body.match(/<userId>([^<]+)<\/userId>/);
const userId = m && m[1];
m = response.body.match(/<organizationId>([^<]+)<\/organizationId>/);
const organizationId = m && m[1];
if (!serverUrl || !sessionId || !userId || !organizationId) {
throw new Error('could not extract session information from login response');
}
const idUrl = [this.loginUrl, 'id', organizationId, userId].join('/');
const userInfo = { id: userId, organizationId, url: idUrl };
this._establish({
serverUrl: serverUrl.split('/').slice(0, 3).join('/'),
sessionId,
userInfo,
});
this._logger.info(`<login> completed. user id = ${userId}, org id = ${organizationId}`);
return userInfo;
}
/**
* Logout the current session
*/
async logout(revoke) {
this._refreshDelegate = undefined;
if (this._sessionType === 'oauth2') {
return this.logoutByOAuth2(revoke);
}
return this.logoutBySoap(revoke);
}
/**
* Logout the current session by revoking access token via OAuth2 session revoke
*/
async logoutByOAuth2(revoke) {
const token = revoke ? this.refreshToken : this.accessToken;
if (token) {
await this.oauth2.revokeToken(token);
}
// Destroy the session bound to this connection
this._clearSession();
this._resetInstance();
}
/**
* Logout the session by using SOAP web service API
*/
async logoutBySoap(revoke) {
const body = [
'<se:Envelope xmlns:se="http://schemas.xmlsoap.org/soap/envelope/">',
'<se:Header>',
'<SessionHeader xmlns="urn:partner.soap.sforce.com">',
`<sessionId>${esc(revoke ? this.refreshToken : this.accessToken)}</sessionId>`,
'</SessionHeader>',
'</se:Header>',
'<se:Body>',
'<logout xmlns="urn:partner.soap.sforce.com"/>',
'</se:Body>',
'</se:Envelope>',
].join('');
const response = await this._transport.httpRequest({
method: 'POST',
url: [this.instanceUrl, 'services/Soap/u', this.version].join('/'),
body,
headers: {
'Content-Type': 'text/xml',
SOAPAction: '""',
},
});
this._logger.debug(`SOAP statusCode = ${response.statusCode}, response = ${response.body}`);
if (response.statusCode >= 400) {
const m = response.body.match(/<faultstring>([^<]+)<\/faultstring>/);
const faultstring = m && m[1];
throw new Error(faultstring || response.body);
}
// Destroy the session bound to this connection
this._clearSession();
this._resetInstance();
}
/**
* Send REST API request with given HTTP request info, with connected session information.
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*/
request(request, options = {}) {
// if request is simple string, regard it as url in GET method
let request_ = typeof request === 'string' ? { method: 'GET', url: request } : request;
// if url is given in relative path, prepend base url or instance url before.
request_ = {
...request_,
url: this._normalizeUrl(request_.url),
};
const httpApi = new http_api_1.default(this, options);
// log api usage and its quota
httpApi.on('response', (response) => {
if (response.headers && response.headers['sforce-limit-info']) {
const apiUsage = response.headers['sforce-limit-info'].match(/api-usage=(\d+)\/(\d+)/);
if (apiUsage) {
this.limitInfo = {
apiUsage: {
used: parseInt(apiUsage[1], 10),
limit: parseInt(apiUsage[2], 10),
},
};
}
}
});
return httpApi.request(request_);
}
/**
* Send HTTP GET request
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*/
requestGet(url, options) {
const request = { method: 'GET', url };
return this.request(request, options);
}
/**
* Send HTTP POST request with JSON body, with connected session information
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*/
requestPost(url, body, options) {
const request = {
method: 'POST',
url,
body: JSON.stringify(body),
headers: { 'content-type': 'application/json' },
};
return this.request(request, options);
}
/**
* Send HTTP PUT request with JSON body, with connected session information
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*/
requestPut(url, body, options) {
const request = {
method: 'PUT',
url,
body: JSON.stringify(body),
headers: { 'content-type': 'application/json' },
};
return this.request(request, options);
}
/**
* Send HTTP PATCH request with JSON body
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*/
requestPatch(url, body, options) {
const request = {
method: 'PATCH',
url,
body: JSON.stringify(body),
headers: { 'content-type': 'application/json' },
};
return this.request(request, options);
}
/**
* Send HTTP DELETE request
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*/
requestDelete(url, options) {
const request = { method: 'DELETE', url };
return this.request(request, options);
}
/** @private **/
_baseUrl() {
return [this.instanceUrl, 'services/data', `v${this.version}`].join('/');
}
/**
* Convert path to absolute url
* @private
*/
_normalizeUrl(url) {
if (url.startsWith('/')) {
if (url.startsWith(this.instanceUrl + '/services/')) {
return url;
}
if (url.startsWith('/services/')) {
return this.instanceUrl + url;
}
return this._baseUrl() + url;
}
return url;
}
/**
*
*/
query(soql, options) {
return new query_1.default(this, soql, options);
}
/**
* Execute search by SOSL
*
* @param {String} sosl - SOSL string
* @param {Callback.<Array.<RecordResult>>} [callback] - Callback function
* @returns {Promise.<Array.<RecordResult>>}
*/
search(sosl) {
const url = this._baseUrl() + '/search?q=' + encodeURIComponent(sosl);
return this.request(url);
}
/**
*
*/
queryMore(locator, options) {
return new query_1.default(this, { locator }, options);
}
/* */
_ensureVersion(majorVersion) {
const versions = this.version.split('.');
return parseInt(versions[0], 10) >= majorVersion;
}
/* */
_supports(feature) {
switch (feature) {
case 'sobject-collection': // sobject collection is available only in API ver 42.0+
return this._ensureVersion(42);
default:
return false;
}
}
async retrieve(type, ids, options = {}) {
return Array.isArray(ids)
? // check the version whether SObject collection API is supported (42.0)
this._ensureVersion(42)
? this._retrieveMany(type, ids, options)
: this._retrieveParallel(type, ids, options)
: this._retrieveSingle(type, ids, options);
}
/** @private */
async _retrieveSingle(type, id, options) {
if (!id) {
throw new Error('Invalid record ID. Specify valid record ID value');
}
let url = [this._baseUrl(), 'sobjects', type, id].join('/');
const { fields, headers } = options;
if (fields) {
url += `?fields=${fields.join(',')}`;
}
return this.request({ method: 'GET', url, headers });
}
/** @private */
async _retrieveParallel(type, ids, options) {
if (ids.length > this._maxRequest) {
throw new Error('Exceeded max limit of concurrent call');
}
return Promise.all(ids.map((id) => this._retrieveSingle(type, id, options).catch((err) => {
if (options.allOrNone || err.errorCode !== 'NOT_FOUND') {
throw err;
}
return null;
})));
}
/** @private */
async _retrieveMany(type, ids, options) {
if (ids.length === 0) {
return [];
}
const url = [this._baseUrl(), 'composite', 'sobjects', type].join('/');
const fields = options.fields ||
(await this.describe$(type)).fields.map((field) => field.name);
return this.request({
method: 'POST',
url,
body: JSON.stringify({ ids, fields }),
headers: {
...(options.headers || {}),
'content-type': 'application/json',
},
});
}
/**
* @param type
* @param records
* @param options
*/
async create(type, records, options = {}) {
const ret = Array.isArray(records)
? // check the version whether SObject collection API is supported (42.0)
this._ensureVersion(42)
? await this._createMany(type, records, options)
: await this._createParallel(type, records, options)
: await this._createSingle(type, records, options);
return ret;
}
/** @private */
async _createSingle(type, record, options) {
const { Id, type: rtype, attributes, ...rec } = record;
const sobjectType = type || attributes?.type || rtype;
if (!sobjectType) {
throw new Error('No SObject Type defined in record');
}
const url = [this._baseUrl(), 'sobjects', sobjectType].join('/');
let contentType, body;
if (options?.multipartFileFields) {
// Send the record as a multipart/form-data request. Useful for fields containing large binary blobs.
const form = new form_data_1.default();
// Extract the fields requested to be sent separately from the JSON
Object.entries(options.multipartFileFields).forEach(([fieldName, fileDetails]) => {
form.append(fieldName, Buffer.from(rec[fieldName], 'base64'), fileDetails);
delete rec[fieldName];
});
// Serialize the remaining fields as JSON
form.append(type, JSON.stringify(rec), {
contentType: 'application/json',
});
contentType = form.getHeaders()['content-type']; // This is necessary to ensure the 'boundary' is present
body = form;
}
else {
// Default behavior: send the request as JSON
contentType = 'application/json';
body = JSON.stringify(rec);
}
return this.request({
method: 'POST',
url,
body: body,
headers: {
...(options.headers || {}),
'content-type': contentType,
},
});
}
/** @private */
async _createParallel(type, records, options) {
if (records.length > this._maxRequest) {
throw new Error('Exceeded max limit of concurrent call');
}
return Promise.all(records.map((record) => this._createSingle(type, record, options).catch((err) => {
// be aware that allOrNone in parallel mode will not revert the other successful requests
// it only raises error when met at least one failed request.
if (options.allOrNone || !err.errorCode) {
throw err;
}
return toSaveResult(err);
})));
}
/** @private */
async _createMany(type, records, options) {
if (records.length === 0) {
return Promise.resolve([]);
}
if (records.length > MAX_DML_COUNT && options.allowRecursive) {
return [
...(await this._createMany(type, records.slice(0, MAX_DML_COUNT), options)),
...(await this._createMany(type, records.slice(MAX_DML_COUNT), options)),
];
}
const _records = records.map((record) => {
const { Id, type: rtype, attributes, ...rec } = record;
const sobjectType = type || attributes?.type || rtype;
if (!sobjectType) {
throw new Error('No SObject Type defined in record');
}
return { attributes: { type: sobjectType }, ...rec };
});
const url = [this._baseUrl(), 'composite', 'sobjects'].join('/');
return this.request({
method: 'POST',
url,
body: JSON.stringify({
allOrNone: options.allOrNone || false,
records: _records,
}),
headers: {
...(options.headers || {}),
'content-type': 'application/json',
},
});
}
/**
* Synonym of Connection#create()
*/
insert = this.create;
/**
* @param type
* @param records
* @param options
*/
update(type, records, options = {}) {
return Array.isArray(records)
? // check the version whether SObject collection API is supported (42.0)
this._ensureVersion(42)
? this._updateMany(type, records, options)
: this._updateParallel(type, records, options)
: this._updateSingle(type, records, options);
}
/** @private */
async _updateSingle(type, record, options) {
const { Id: id, type: rtype, attributes, ...rec } = record;
if (!id) {
throw new Error('Record id is not found in record.');
}
const sobjectType = type || attributes?.type || rtype;
if (!sobjectType) {
throw new Error('No SObject Type defined in record');
}
const url = [this._baseUrl(), 'sobjects', sobjectType, id].join('/');
return this.request({
method: 'PATCH',
url,
body: JSON.stringify(rec),
headers: {
...(options.headers || {}),
'content-type': 'application/json',
},
}, {
noContentResponse: { id, success: true, errors: [] },
});
}
/** @private */
async _updateParallel(type, records, options) {
if (records.length > this._maxRequest) {
throw new Error('Exceeded max limit of concurrent call');
}
return Promise.all(records.map((record) => this._updateSingle(type, record, options).catch((err) => {
// be aware that allOrNone in parallel mode will not revert the other successful requests
// it only raises error when met at least one failed request.
if (options.allOrNone || !err.errorCode) {
throw err;
}
return toSaveResult(err);
})));
}
/** @private */
async _updateMany(type, records, options) {
if (records.length === 0) {
return [];
}
if (records.length > MAX_DML_COUNT && options.allowRecursive) {
return [
...(await this._updateMany(type, records.slice(0, MAX_DML_COUNT), options)),
...(await this._updateMany(type, records.slice(MAX_DML_COUNT), options)),
];
}
const _records = records.map((record) => {
const { Id: id, type: rtype, attributes, ...rec } = record;
if (!id) {
throw new Error('Record id is not found in record.');
}
const sobjectType = type || attributes?.type || rtype;
if (!sobjectType) {
throw new Error('No SObject Type defined in record');
}
return { id, attributes: { type: sobjectType }, ...rec };
});
const url = [this._baseUrl(), 'composite', 'sobjects'].join('/');
return this.request({
method: 'PATCH',
url,
body: JSON.stringify({
allOrNone: options.allOrNone || false,
records: _records,
}),
headers: {
...(options.headers || {}),
'content-type': 'application/json',
},
});
}
/**
*
* @param type
* @param records
* @param extIdField
* @param options
*/
async upsert(type, records, extIdField, options = {}) {
const isArray = Array.isArray(records);
const _records = Array.isArray(records) ? records : [records];
if (_records.length > this._maxRequest) {
throw new Error('Exceeded max limit of concurrent call');
}
const results = await Promise.all(_records.map((record) => {
const { [extIdField]: extId, type: rtype, attributes, ...rec } = record;
const url = [this._baseUrl(), 'sobjects', type, extIdField, extId].join('/');
return this.request({
method: 'PATCH',
url,
body: JSON.stringify(rec),
headers: {
...(options.headers || {}),
'content-type': 'application/json',
},
}, {
noContentResponse: { success: true, errors: [] },
}).catch((err) => {
// Be aware that `allOrNone` option in upsert method
// will not revert the other successful requests.
// It only raises error when met at least one failed request.
if (!isArray || options.allOrNone || !err.errorCode) {
throw err;
}
return toSaveResult(err);
});
}));
return isArray ? results : results[0];
}
/**
* @param type
* @param ids
* @param options
*/
async destroy(type, ids, options = {}) {
return Array.isArray(ids)
? // check the version whether SObject collection API is supported (42.0)
this._ensureVersion(42)
? this._destroyMany(type, ids, options)
: this._destroyParallel(type, ids, options)
: this._destroySingle(type, ids, options);
}
/** @private */
async _destroySingle(type, id, options) {
const url = [this._baseUrl(), 'sobjects', type, id].join('/');
return this.request({
method: 'DELETE',
url,
headers: options.headers || {},
}, {
noContentResponse: { id, success: true, errors: [] },
});
}
/** @private */
async _destroyParallel(type, ids, options) {
if (ids.length > this._maxRequest) {
throw new Error('Exceeded max limit of concurrent call');
}
return Promise.all(ids.map((id) => this._destroySingle(type, id, options).catch((err) => {
// Be aware that `allOrNone` option in parallel mode
// will not revert the other successful requests.
// It only raises error when met at least one failed request.
if (options.allOrNone || !err.errorCode) {
throw err;
}
return toSaveResult(err);
})));
}
/** @private */
async _destroyMany(type, ids, options) {
if (ids.length === 0) {
return [];
}
if (ids.length > MAX_DML_COUNT && options.allowRecursive) {
return [
...(await this._destroyMany(type, ids.slice(0, MAX_DML_COUNT), options)),
...(await this._destroyMany(type, ids.slice(MAX_DML_COUNT), options)),
];
}
let url = [this._baseUrl(), 'composite', 'sobjects?ids='].join('/') + ids.join(',');
if (options.allOrNone) {
url += '&allOrNone=true';
}
return this.request({
method: 'DELETE',
url,
headers: options.headers || {},
});
}
/**
* Synonym of Connection#destroy()
*/
delete = this.destroy;
/**
* Synonym of Connection#destroy()
*/
del = this.destroy;
/**
* Describe SObject metadata
*/
async describe(type) {
const url = [this._baseUrl(), 'sobjects', type, 'describe'].join('/');
const body = await this.request(url);
return body;
}
/**
* Describe global SObjects
*/
async describeGlobal() {
const url = `${this._baseUrl()}/sobjects`;
const body = await this.request(url);
return body;
}
sobject(type) {
const so = this.sobjects[type] || new sobject_1.default(this, type);
this.sobjects[type] = so;
return so;
}
/**
* Get identity information of current user
*/
async identity(options = {}) {
let url = this.userInfo?.url;
if (!url) {
const res = await this.request({
method: 'GET',
url: this._baseUrl(),
headers: options.headers,
});
url = res.identity;
}
url += '?format=json';
if (this.accessToken) {
url += `&oauth_token=${encodeURIComponent(this.accessToken)}`;
}
const res = await this.request({ method: 'GET', url });
this.userInfo = {
id: res.user_id,
organizationId: res.organization_id,
url: res.id,
};
return res;
}
/**
* List recently viewed records
*/
async recent(type, limit) {
/* eslint-disable no-param-reassign */
if (typeof type === 'number') {
limit = type;
type = undefined;
}
let url;
if (type) {
url = [this._baseUrl(), 'sobjects', type].join('/');
const { recentItems } = await this.request(url);
return limit ? recentItems.slice(0, limit) : recentItems;
}
url = `${this._baseUrl()}/recent`;
if (limit) {
url += `?limit=${limit}`;
}
return this.request(url);
}
/**
* Retrieve updated records
*/
async updated(type, start, end) {
/* eslint-disable no-param-reassign */
let url = [this._baseUrl(), 'sobjects', type, 'updated'].join('/');
if (typeof start === 'string') {
start = new Date(start);
}
start = (0, formatter_1.formatDate)(start);
url += `?start=${encodeURIComponent(start)}`;
if (typeof end === 'string') {
end = new Date(end);
}
end = (0, formatter_1.formatDate)(end);
url += `&end=${encodeURIComponent(end)}`;
const body = await this.request(url);
return body;
}
/**
* Retrieve deleted records
*/
async deleted(type, start, end) {
/* eslint-disable no-param-reassign */
let url = [this._baseUrl(), 'sobjects', type, 'deleted'].join('/');
if (typeof start === 'string') {
start = new Date(start);
}
start = (0, formatter_1.formatDate)(start);
url += `?start=${encodeURIComponent(start)}`;
if (typeof end === 'string') {
end = new Date(end);
}
end = (0, formatter_1.formatDate)(end);
url += `&end=${encodeURIComponent(end)}`;
const body = await this.request(url);
return body;
}
/**
* Returns a list of all tabs
*/
async tabs() {
const url = [this._baseUrl(), 'tabs'].join('/');
const body = await this.request(url);
return body;
}
/**
* Returns current system limit in the organization
*/
async limits() {
const url = [this._baseUrl(), 'limits'].join('/');
const body = await this.request(url);
return body;
}
/**
* Returns a theme info
*/
async theme() {
const url = [this._baseUrl(), 'theme'].join('/');
const body = await this.request(url);
return body;
}
/**
* Returns all registered global quick actions
*/
async quickActions() {
const body = await this.request('/quickActions');
return body;
}
/**
* Get reference for specified global quick action
*/
quickAction(actionName) {
return new quick_action_1.default(this, `/quickActions/${actionName}`);
}
/**
* Module which manages process rules and approval processes
*/
process = new process_1.default(this);
isLightningInstance() {
return (this.instanceUrl.includes('.lightning.force.com') ||
this.instanceUrl.includes('.lightning.crmforce.mil') ||
this.instanceUrl.includes('.lightning.sfcrmapps.cn'));
}
}
exports.Connection = Connection;
exports.default = Connection;
;