From bc74bacf347022d31a614e04cc5895a33430bf24 Mon Sep 17 00:00:00 2001 From: Abraham White Date: Thu, 9 Aug 2018 17:28:37 -0600 Subject: [PATCH] Write tests for SObject and move Callback back to connection.d.ts --- types/jsforce/connection.d.ts | 31 ++- types/jsforce/global.d.ts | 1 - types/jsforce/http-api.d.ts | 5 + types/jsforce/index.d.ts | 2 +- types/jsforce/jsforce-tests.ts | 363 ++++++++++++++++++++++----- types/jsforce/quick-action.d.ts | 2 +- types/jsforce/salesforce-object.d.ts | 25 +- 7 files changed, 350 insertions(+), 79 deletions(-) delete mode 100644 types/jsforce/global.d.ts create mode 100644 types/jsforce/http-api.d.ts diff --git a/types/jsforce/connection.d.ts b/types/jsforce/connection.d.ts index fcf614eb92..e7fd593b01 100644 --- a/types/jsforce/connection.d.ts +++ b/types/jsforce/connection.d.ts @@ -10,8 +10,16 @@ import { Metadata } from './api/metadata'; import { Bulk } from './bulk'; import { Cache } from './cache' import { OAuth2, Streaming } from '.'; +import { HttpApiOptions } from './http-api' -export type Callback = (err: Error, result: T) => void; +export type Callback = (err: Error | null, result: T) => void; +// The type for these options was determined by looking at the usage +// of the options object in Connection.create and other methods +// go to http://jsforce.github.io/jsforce/doc/connection.js.html#line568 +// and search for options +export interface RestApiOptions { + headers?: { [x: string]: string } +} // These are pulled out because according to http://jsforce.github.io/jsforce/doc/connection.js.html#line49 // the oauth options can either be in the `oauth2` proeprty OR spread across the main connection @@ -93,24 +101,24 @@ export type ConnectionEvent = "refresh"; */ export abstract class BaseConnection extends EventEmitter { _baseUrl(): string; - request(info: RequestInfo | string, options?: Object, callback?: (err: Error, Object: object) => void): Promise; + request(info: RequestInfo | string, options?: HttpApiOptions, callback?: (err: Error, Object: object) => void): Promise; query(soql: string, options?: ExecuteOptions, callback?: (err: Error, result: QueryResult) => void): Query>; queryMore(locator: string, options?: ExecuteOptions, callback?: (err: Error, result: QueryResult) => void): Promise>; - create(type: string, records: Record | Array>, options?: Object, + create(type: string, records: Record | Array>, options?: RestApiOptions, callback?: (err: Error, result: RecordResult | RecordResult[]) => void): Promise<(RecordResult | RecordResult[])>; - insert(type: string, records: Record | Array>, options?: Object, + insert(type: string, records: Record | Array>, options?: RestApiOptions, callback?: (err: Error, result: RecordResult | RecordResult[]) => void): Promise<(RecordResult | RecordResult[])>; - retrieve(type: string, ids: string | string[], options?: Object, + retrieve(type: string, ids: string | string[], options?: RestApiOptions, callback?: (err: Error, result: Record | Array>) => void): Promise<(Record | Array>)>; - update(type: string, records: Record | Array>, options?: Object, + update(type: string, records: Record | Array>, options?: RestApiOptions, callback?: (err: Error, result: RecordResult | Array>) => void): Promise<(RecordResult | RecordResult[])>; - upsert(type: string, records: Record | Array>, extIdField: string, options?: Object, + upsert(type: string, records: Record | Array>, extIdField: string, options?: RestApiOptions, callback?: (err: Error, result: RecordResult | RecordResult[]) => void): Promise<(RecordResult | RecordResult[])>; - del(type: string, ids: string | string[], options?: Object, + del(type: string, ids: string | string[], options?: RestApiOptions, callback?: (err: Error, result: RecordResult | RecordResult[]) => void): Promise<(RecordResult | RecordResult[])>; - delete(type: string, ids: string | string[], options?: Object, + delete(type: string, ids: string | string[], options?: RestApiOptions, callback?: (err: Error, result: RecordResult | RecordResult[]) => void): Promise<(RecordResult | RecordResult[])>; - destroy(type: string, ids: string | string[], options?: Object, + destroy(type: string, ids: string | string[], options?: RestApiOptions, callback?: (err: Error, result: RecordResult | RecordResult[]) => void): Promise<(RecordResult | RecordResult[])>; describe$: { /** Returns a value from the cache if it exists, otherwise calls Connection.describe */ @@ -124,7 +132,8 @@ export abstract class BaseConnection extends EventEmitter { clear(): void; } describeGlobal(callback?: (err: Error, result: DescribeGlobalResult) => void): Promise; - sobject(resource: string): SObject; + // we want any object to be accepted if the user doesn't decide to give an explicit type + sobject(resource: string): SObject; } export class Connection extends BaseConnection { diff --git a/types/jsforce/global.d.ts b/types/jsforce/global.d.ts deleted file mode 100644 index 2c30ae64bb..0000000000 --- a/types/jsforce/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -export type Callback = (err: Error | null, ret: T) => void; diff --git a/types/jsforce/http-api.d.ts b/types/jsforce/http-api.d.ts new file mode 100644 index 0000000000..1986ce0c75 --- /dev/null +++ b/types/jsforce/http-api.d.ts @@ -0,0 +1,5 @@ +export interface HttpApiOptions { + responseType?: string; + transport?: object; + noContentResponse?: object +} diff --git a/types/jsforce/index.d.ts b/types/jsforce/index.d.ts index ddcb0d8260..d92677a5eb 100644 --- a/types/jsforce/index.d.ts +++ b/types/jsforce/index.d.ts @@ -6,7 +6,7 @@ // Tim Noonan // Abraham White // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.2 +// TypeScript Version: 2.3 export * from './api/analytics'; export * from './api/chatter'; diff --git a/types/jsforce/jsforce-tests.ts b/types/jsforce/jsforce-tests.ts index 8eb97aebc7..886bd012c0 100644 --- a/types/jsforce/jsforce-tests.ts +++ b/types/jsforce/jsforce-tests.ts @@ -4,12 +4,9 @@ import * as express from 'express'; import * as glob from 'glob'; import * as sf from 'jsforce'; - -export interface DummyRecord { - thing: boolean; - other: number; - person: string; -} +import { RecordReference, Record } from 'jsforce/record'; +import { SObject } from 'jsforce/salesforce-object'; +import { RecordResult } from 'jsforce/record-result'; const salesforceConnection: sf.Connection = new sf.Connection({ instanceUrl: '', @@ -20,7 +17,304 @@ const salesforceConnection: sf.Connection = new sf.Connection({ }, }); -salesforceConnection.sobject("Dummy").select(["thing", "other"]); +async function testSObject(connection: sf.Connection) { + interface DummyRecord { + thing: boolean; + other: number; + person: string; + } + + const dummySObject: SObject = connection.sobject('Dummy'); + + // currently untyped, but some future change may make this stricter + const restApiOptions = { + headers: { Bearer: 'I have no idea what this wants' } + }; + + { // Test SObject.record + // $ExpectType RecordReference + dummySObject.record('50130000000014C'); + } + + { // Test SObject.retrieve + // with single id + // $ExpectType Record + await dummySObject.retrieve('50130000000014C'); + // with single id and rest api options + // $ExpectType Record + await dummySObject.retrieve('50130000000014C', restApiOptions); + + // with single id and callback + dummySObject.retrieve('50130000000014C', restApiOptions, (err, res) => { + err; // $ExpectType Error | null + res; // $ExpectType Record + }); + + // with ids array + // $ExpectType Record[] + await dummySObject.retrieve(['IIIIDDD']); + // with ids array and rest api options + // $ExpectType Record[] + await dummySObject.retrieve(['IIIIDDD'], restApiOptions); + + // with ids array and callback + dummySObject.retrieve(['50130000000014C'], restApiOptions, (err, res) => { + err; // $ExpectType Error | null + res; // $ExpectType Record[] + }); + + salesforceConnection.sobject("ContentVersion").retrieve("world", { + test: "test" + }, (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType any + }); + } + + { // Test SObject.update + // if we require that records have an id field this will fail + // //$ExpectError + await dummySObject.update({ thing: false }); + + // If we require that the records have an Id field + // await dummySObject.update({ thing: false, Id: 'asdf' }); // $ExpectType RecordResult + + // invalid field + // $ExpectError + await dummySObject.update({ asdf: false }); + + // with rest api options + // $ExpectType RecordResult + await dummySObject.update({ thing: false }, restApiOptions); + + // with callback + dummySObject.update({ thing: false }, (err, res) => { + err; // $ExpectType Error | null + res; // $ExpectType RecordResult + }); + + dummySObject.update({ thing: false }, restApiOptions, (err, res) => { + err; // $ExpectType Error | null + res; // $ExpectType RecordResult + }); + + // with multiple records + // $ExpectType RecordResult[] + await dummySObject.update([{ thing: false }]); + + // with multiple records and api options + // $ExpectType RecordResult[] + await dummySObject.update([{ thing: false }], restApiOptions); + + // with multiple records and callback + dummySObject.update([{ thing: false }], restApiOptions, (err, res) => { + err; // $ExpectType Error | null + res; // $ExpectType RecordResult[] + }); + + dummySObject.update([{ thing: false }], (err, res) => { + err; // $ExpectType Error | null + res; // $ExpectType RecordResult[] + }); + } + + { // Test SObject.updated + // $ExpectType UpdatedRecordsInfo + await dummySObject.updated(new Date(), new Date()); + + // $ExpectType UpdatedRecordsInfo + await dummySObject.updated(new Date(), 'hi'); + + // $ExpectType UpdatedRecordsInfo + await dummySObject.updated('hi', new Date()); + + // $ExpectType UpdatedRecordsInfo + await dummySObject.updated('hi', 'hi'); + + dummySObject.updated(new Date(), 'hi', (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType UpdatedRecordsInfo + }); + } + + { // Test SObject.upsert + const updateData = { + Id: 'Some ID', + thing: true, + other: 1, + person: 'hi' + }; + // $ExpectType RecordResult + await dummySObject.upsert(updateData, 'Id'); + + // $ExpectType RecordResult + await dummySObject.upsert(updateData, 'Id', restApiOptions); + + // $ExpectType RecordResult[] + await dummySObject.upsert([updateData], 'Id'); + + // $ExpectType RecordResult[] + await dummySObject.upsert([updateData], 'Id', restApiOptions); + + dummySObject.upsert(updateData, 'Id', (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult + }); + + dummySObject.upsert(updateData, 'Id', restApiOptions, (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult + }); + + dummySObject.upsert([updateData], 'Id', (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult[] + }); + + dummySObject.upsert([updateData], 'Id', restApiOptions, (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult[] + }); + } + + { // Test SObject.find + } + + { // Test SObject.findOne + salesforceConnection.sobject("ContentVersion").findOne({ Id: '' }, (err, contentVersion) => { + err; // $ExpectType Error | null + contentVersion; // $ExpectType any + }); + } + + { // Test SObject.select + + dummySObject.select(["thing", "other"]); + + // note the following should never compile: + // $ExpectError + dummySObject.select(["lol"]); + } + + { // Test SObject.create + // $ExpectType RecordResult + await dummySObject.create({ + thing: true, + other: 1, + person: 'hi' + }); + + // $ExpectType RecordResult + await dummySObject.create({ + thing: true, + other: 1, + person: 'hi' + }, restApiOptions); + + // $ExpectType RecordResult[] + await dummySObject.create([{ + thing: true, + other: 1, + person: 'hi' + }]); + + // $ExpectType RecordResult[] + await dummySObject.create([{ + thing: true, + other: 1, + person: 'hi' + }], restApiOptions); + + dummySObject.create([{ + thing: true, + other: 1, + person: 'hi' + }], (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult[] + }); + + dummySObject.create([{ + thing: true, + other: 1, + person: 'hi' + }], restApiOptions, (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult[] + }); + + salesforceConnection.sobject("Account").create({ + Name: "Test Acc 2", + BillingStreet: "Maplestory street", + BillingPostalCode: "ME4 666" + }, (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult + }); + + // callback and rest api options + salesforceConnection.sobject("ContentVersion").create({ + OwnerId: '', + Title: 'hello', + PathOnClient: './hello-world.jpg', + VersionData: '{ Test: Data }' + }, restApiOptions, (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult + }); + + salesforceConnection.sobject("ContentDocumentLink").create({ + ContentDocumentId: '', + LinkedEntityId: '', + ShareType: "I" + }, (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult + }); + } + + { // Test SObject.destroy and aliases + // $ExpectType RecordResult + await dummySObject.del('Id'); + // $ExpectType RecordResult + await dummySObject.destroy('Id'); + // $ExpectType RecordResult + await dummySObject.delete('Id'); + + // $ExpectType RecordResult[] + await dummySObject.del(['Id']); + // $ExpectType RecordResult[] + await dummySObject.destroy(['Id']); + // $ExpectType RecordResult[] + await dummySObject.delete(['Id']); + + dummySObject.del('Id', (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult + }); + dummySObject.destroy('Id', (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult + }); + dummySObject.delete('Id', (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult + }); + + dummySObject.del(['Id'], (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult[] + }); + dummySObject.destroy(['Id'], (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult[] + }); + dummySObject.delete(['Id'], (err, ret) => { + err; // $ExpectType Error | null + ret; // $ExpectType RecordResult[] + }); + } +} const requestInfo: sf.RequestInfo = { body: '', @@ -38,51 +332,6 @@ const queryOptions: sf.ExecuteOptions = { }; salesforceConnection.query('SELECT Id, Name FROM Account', queryOptions); -// note the following should never compile: -// salesforceConnection.sobject("Dummy").select(["lol"]); - -salesforceConnection.sobject("Account").create({ - Name: "Test Acc 2", - BillingStreet: "Maplestory street", - BillingPostalCode: "ME4 666" -}, (err: Error, ret: sf.RecordResult | sf.RecordResult[]) => { - if (err || !Array.isArray(ret) && !ret.success) { - return; - } -}); - -salesforceConnection.sobject("ContentVersion").create({ - OwnerId: '', - Title: 'hello', - PathOnClient: './hello-world.jpg', - VersionData: '{ Test: Data }' -}, (err: Error, ret: sf.RecordResult | sf.RecordResult[]) => { - if (err || !Array.isArray(ret) && !ret.success) { - return; - } -}); - -salesforceConnection.sobject("ContentVersion").retrieve("world", { - test: "test" -}, (err: Error, ret) => { - if (err) { - return; - } -}); - -salesforceConnection.sobject("ContentVersion").findOne({ Id: '' }, (err, contentVersion) => { -}); - -salesforceConnection.sobject("ContentDocumentLink").create({ - ContentDocumentId: '', - LinkedEntityId: '', - ShareType: "I" -}, (err: Error, ret: sf.RecordResult | sf.RecordResult[]) => { - if (err || !Array.isArray(ret) && !ret.success) { - return; - } -}); - sf.Date.YESTERDAY; salesforceConnection.sobject('Coverage__c') @@ -230,7 +479,7 @@ async function testChatter(conn: sf.Connection): Promise { }, feedElementType: 'FeedItem', subjectId: 'me' - }, (err: Error, result: any) => { + }, (err: Error | null, result: any) => { if (err) { throw err; } @@ -242,7 +491,7 @@ async function testChatter(conn: sf.Connection): Promise { text: 'This is new comment on the post' }] } - }, (err: Error, result: any) => { + }, (err: Error | null, result: any) => { if (err) { throw err; } @@ -366,13 +615,15 @@ async function testDescribe() { object.fields.forEach(field => { const type: sf.FieldType = field.type; // following should never compile - // const fail = type === 'hey' + // $ExpectError + const fail = type === 'hey'; const isString = type === 'string'; }); // following should never compile (if StrictNullChecks is on) - // object.keyPrefix.length; + // $ExpectError + object.keyPrefix.length; console.log(`${sobject.name} Label: `, object.label); diff --git a/types/jsforce/quick-action.d.ts b/types/jsforce/quick-action.d.ts index aa6036993a..a67f4d4e08 100644 --- a/types/jsforce/quick-action.d.ts +++ b/types/jsforce/quick-action.d.ts @@ -1,4 +1,4 @@ -import { Callback } from './global'; +import { Callback } from './connection'; import { Record } from './record'; export class QuickAction { diff --git a/types/jsforce/salesforce-object.d.ts b/types/jsforce/salesforce-object.d.ts index ce2f996829..2015f5d79b 100644 --- a/types/jsforce/salesforce-object.d.ts +++ b/types/jsforce/salesforce-object.d.ts @@ -5,22 +5,29 @@ import { DescribeSObjectResult } from './describe-result'; import { Query } from './query'; import { Record, RecordReference } from './record'; import { RecordResult } from './record-result'; -import { Connection, Callback } from './connection'; +import { Connection, RestApiOptions, Callback } from './connection'; import { SalesforceId } from './salesforce-id'; import { Batch, BatchResultInfo } from './batch'; import { QuickAction, QuickActionInfo } from './quick-action'; export class SObject { record(id: SalesforceId): RecordReference; - retrieve(id: SalesforceId, options?: Object, callback?: Callback>): Promise>; - retrieve(ids: SalesforceId[], options?: Object, callback?: Callback>>): Promise>>; - update(record: Partial, options?: Object, callback?: Callback): Promise; - update(records: Array>, options?: Object, callback?: Callback): Promise; + retrieve(id: SalesforceId, callback?: Callback>): Promise>; + retrieve(id: SalesforceId, options?: object, callback?: Callback>): Promise>; + retrieve(ids: SalesforceId[], callback?: Callback>>): Promise>>; + retrieve(ids: SalesforceId[], options?: object, callback?: Callback>>): Promise>>; + // Should update require that the record Id field be provided? + update(record: Partial, callback?: Callback): Promise; + update(record: Partial, options?: RestApiOptions, callback?: Callback): Promise; + update(records: Array>, callback?: Callback): Promise; + update(records: Array>, options?: RestApiOptions, callback?: Callback): Promise; // FIXME: should input really be optional? the documentation says so, but how can you actually update without it? updateBulk(input?: Record[] | stream.Stream | string, callback?: Callback): Batch; updated(start: string | Date, end: string | Date, callback?: Callback): Promise; - upsert(records: Record, extIdField: SalesforceId, options?: Object, callback?: Callback): Promise; - upsert(records: Array>, extIdField: SalesforceId, options?: Object, callback?: Callback): Promise; + upsert(records: Record, extIdField: string, callback?: Callback): Promise; + upsert(records: Record, extIdField: string, options?: RestApiOptions, callback?: Callback): Promise; + upsert(records: Array>, extIdField: string, callback?: Callback): Promise; + upsert(records: Array>, extIdField: string, options?: RestApiOptions, callback?: Callback): Promise; upsertBulk(input?: Array> | stream.Stream | string, callback?: Callback): Batch; find(query?: object | string, callback?: Callback>>): Query>>; @@ -45,9 +52,9 @@ export class SObject { } compactLayouts(callback?: Callback): Promise; count(conditions?: Object | string, callback?: Callback): Query; - create(record: T, options: object, callback?: Callback): Promise; + create(record: T, options?: RestApiOptions, callback?: Callback): Promise; create(record: T, callback?: Callback): Promise; - create(record: Array, options: object, callback?: Callback): Promise; + create(record: Array, options?: RestApiOptions, callback?: Callback): Promise; create(record: Array, callback?: Callback): Promise; // FIXME: why does the callback return a single RecordResult instead of an array as in the documentation? createBulk(input?: Array> | stream.Stream | string, callback?: Callback): Batch;