/** * @file Manages Salesforce Chatter REST API calls * @author Shinichi Tomita */ import { registerModule } from '../jsforce'; import Connection from '../connection'; import { HttpRequest, Schema } from '../types'; import { isObject } from '../util/function'; /** * */ export type ChatterRequestParams = Omit & { body?: string | object | null; }; export type BatchRequestParams = { method: string; url: string; richInput?: any; }; type BatchRequestTupple = { [K in keyof RT]: Request; }; type BatchResultTupple = { [K in keyof RT]: { statusCode: number; result: RT[K]; }; }; export type BatchResponse = { hasErrors: boolean; results: BatchResultTupple; }; /*--------------------------------------------*/ /** * A class representing chatter API request */ class Request { _chatter: Chatter; _request: ChatterRequestParams; _promise: Promise | undefined; constructor(chatter: Chatter, request: ChatterRequestParams) { this._chatter = chatter; this._request = request; } /** * Retrieve parameters in batch request form */ batchParams() { const { method, url, body } = this._request; return { method, url: this._chatter._normalizeUrl(url), ...(typeof body !== 'undefined' ? { richInput: body } : {}), }; } /** * Retrieve parameters in batch request form * * @method Chatter~Request#promise * @returns {Promise.} */ promise() { return ( this._promise || (this._promise = this._chatter._request(this._request)) ); } /** * Returns Node.js Stream object for request * * @method Chatter~Request#stream * @returns {stream.Stream} */ stream() { return this._chatter._request(this._request).stream(); } /** * Promise/A+ interface * http://promises-aplus.github.io/promises-spec/ * * Delegate to deferred promise, return promise instance for batch result */ then( onResolve?: (value: R) => U | PromiseLike, onReject?: (e: any) => U | PromiseLike, ) { return this.promise().then(onResolve, onReject); } } function apppendQueryParamsToUrl( url: string, queryParams?: { [name: string]: string | number | boolean | null } | null, ) { if (queryParams) { const qstring = Object.keys(queryParams) .map( (name) => `${name}=${encodeURIComponent(String(queryParams[name] ?? ''))}`, ) .join('&'); url += (url.indexOf('?') > 0 ? '&' : '?') + qstring; } return url; } /*------------------------------*/ export class Resource extends Request { _url: string; /** * */ constructor( chatter: Chatter, url: string, queryParams?: { [name: string]: string | number | boolean | null } | null, ) { super(chatter, { method: 'GET', url: apppendQueryParamsToUrl(url, queryParams), }); this._url = this._request.url; } /** * Create a new resource */ create(data: string | object | null) { return this._chatter.request({ method: 'POST', url: this._url, body: data, }); } /** * Retrieve resource content */ retrieve() { return this._chatter.request({ method: 'GET', url: this._url, }); } /** * Update specified resource */ update(data: object) { return this._chatter.request({ method: 'POST', url: this._url, body: data, }); } /** * Delete specified resource */ destroy() { return this._chatter.request({ method: 'DELETE', url: this._url, }); } /** * Synonym of Resource#destroy() */ delete = this.destroy; /** * Synonym of Resource#destroy() */ del = this.destroy; } /*------------------------------*/ /** * API class for Chatter REST API call */ export class Chatter { _conn: Connection; /** * */ constructor(conn: Connection) { this._conn = conn; } /** * Sending request to API endpoint * @private */ _request(req_: ChatterRequestParams) { const { method, url: url_, headers: headers_, body: body_ } = req_; let headers = headers_ ?? {}; let body; if (/^(put|post|patch)$/i.test(method)) { if (isObject(body_)) { headers = { ...headers_, 'Content-Type': 'application/json', }; body = JSON.stringify(body_); } else { body = body_; } } const url = this._normalizeUrl(url_); return this._conn.request({ method, url, headers, body, }); } /** * Convert path to site root relative url * @private */ _normalizeUrl(url: string) { if (url.startsWith('/chatter/') || url.startsWith('/connect/')) { return '/services/data/v' + this._conn.version + url; } else if (/^\/v[\d]+\.[\d]+\//.test(url)) { return '/services/data' + url; } else if (!url.startsWith('/services/') && url.startsWith('/')) { return '/services/data/v' + this._conn.version + '/chatter' + url; } else { return url; } } /** * Make a request for chatter API resource */ request(req: ChatterRequestParams) { return new Request(this, req); } /** * Make a resource request to chatter API */ resource( url: string, queryParams?: { [name: string]: string | number | boolean | null } | null, ) { return new Resource(this, url, queryParams); } /** * Make a batch request to chatter API */ async batch( requests: BatchRequestTupple, ): Promise> { const deferreds = requests.map((request) => { const deferred = defer(); request._promise = deferred.promise; return deferred; }); const res = await this.request>({ method: 'POST', url: this._normalizeUrl('/connect/batch'), body: { batchRequests: requests.map((request) => request.batchParams()), }, }); res.results.forEach((result, i) => { const deferred = deferreds[i]; if (result.statusCode >= 400) { deferred.reject(result.result); } else { deferred.resolve(result.result); } }); return res; } } function defer() { let resolve_: (r: T | PromiseLike) => void = () => {}; let reject_: (e: any) => void = () => {}; const promise = new Promise((resolve, reject) => { resolve_ = resolve; reject_ = reject; }); return { promise, resolve: resolve_, reject: reject_, }; } /*--------------------------------------------*/ /* * Register hook in connection instantiation for dynamically adding this API module features */ registerModule('chatter', (conn) => new Chatter(conn)); export default Chatter;