Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web API Type Safety #1673

Merged
merged 125 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
ec34ce6
Remove WebAPICallOptions, replace where it is used with `Record<strin…
filmaj Oct 4, 2023
79aaf6e
@slack/web-api: Remove inclusion of arbitrary properties from WebAPIC…
filmaj Oct 4, 2023
62ffc7f
Remove optionality on abstract generic Method interface options arg, …
filmaj Oct 4, 2023
3cb6c62
adjust return type for fileuploadv2 based on recent change.
filmaj Oct 5, 2023
16464e5
adding notes on future type improvements to getFile API args
filmaj Oct 5, 2023
b4837a4
Remove note about getFile response type being generated. Tweak some m…
filmaj Oct 5, 2023
e2d2faa
Tweaking web-api arguments as I audit them, adding TODOs for future b…
filmaj Oct 5, 2023
bc25c1f
Audit of `admin.*` methods complete, added TODOs and updating argumen…
filmaj Oct 6, 2023
41a0d79
audited api.test and apps.* method arguments.
filmaj Oct 6, 2023
931c06f
bots and auth
filmaj Oct 6, 2023
2e9f906
audit bookmarks.*
filmaj Oct 6, 2023
3a4a7a2
remove deprecated channels methods, audit chat.* methods.
filmaj Oct 18, 2023
7e7b16c
conversations and dialogs methods
filmaj Oct 18, 2023
4e0a739
dnd methods
filmaj Oct 18, 2023
a3cfd8b
files methods
filmaj Oct 18, 2023
124e5f6
remove deprecated groups.* methods.
filmaj Oct 18, 2023
2bf4dc9
remove deprecated im.* methods.
filmaj Oct 18, 2023
b9c63c8
removed deprecated mpim.* methods.
filmaj Oct 18, 2023
daed47d
oauth methods
filmaj Oct 18, 2023
202a321
pin methods
filmaj Oct 18, 2023
4fca944
reaction methods
filmaj Oct 18, 2023
d64c44d
audit reminders methods and factor out optional team id into own mixin.
filmaj Oct 18, 2023
fd17314
rtm methods
filmaj Oct 18, 2023
6813605
search methods
filmaj Oct 18, 2023
6d26fda
stars methods
filmaj Oct 18, 2023
a526bec
team methods
filmaj Oct 18, 2023
6afd88a
usergroups methods
filmaj Oct 18, 2023
0a2692d
users methods
filmaj Oct 18, 2023
75ef263
views methods
filmaj Oct 18, 2023
9dea3ab
workflows methods
filmaj Oct 18, 2023
6ce56f3
Remove unnecessary comment.
filmaj Oct 19, 2023
f172551
Address a comment.
filmaj Oct 19, 2023
8766ead
move web-api response types to a types subdir, so we can reach into t…
filmaj Oct 20, 2023
5864f31
module refs post types reorg
filmaj Oct 20, 2023
822496f
Update deprecation warning to remove old deprecated methods, and star…
filmaj Oct 20, 2023
9f8df3c
update type tests after moving response/request objects around
filmaj Oct 20, 2023
5345456
Start splitting out request interfaces into src/types/request and sep…
filmaj Oct 20, 2023
fe414b3
Fix import
filmaj Oct 20, 2023
6860612
Move request arguments for users.* apis to own file. Removed the warn…
filmaj Oct 24, 2023
206078c
do not apply naming convention linter rules to imports; those might c…
filmaj Oct 24, 2023
64950e5
remove tests related to cursorPagination warning, which was removed.
filmaj Oct 24, 2023
82b82f6
Describe `users.*` API calls with JSDoc. Document cursor pagination p…
filmaj Oct 26, 2023
539ee1d
Document and deprecate workflows.* API methods.
filmaj Oct 26, 2023
e6adeb0
Document views.* API methods.
filmaj Oct 26, 2023
4a97841
Document search.* API methods.
filmaj Oct 26, 2023
98f22c2
Split out usergroups.* API arguments into own file. Document usergrou…
filmaj Oct 26, 2023
ac6ca14
Split out team.* API arguments into own file. Document team.* API met…
filmaj Oct 26, 2023
fa547db
Split out stars.* API arguments into own file and refactor. Document …
filmaj Oct 30, 2023
08b4d0f
Split out rtm.* API arguments into own file and refactor. Document rt…
filmaj Oct 30, 2023
b7749a5
Split out reminders.* API arguments into own file. Document reminder …
filmaj Oct 31, 2023
896edd4
Bump `@slack/types` dev dependencies to fix the build.
filmaj Oct 31, 2023
7a0724f
Factor message/file/file-comment arguments into common.ts for easy re…
filmaj Oct 31, 2023
a52a5ce
Bump `@slack/rtm-api` dev dependencies to fix the build and fix eslin…
filmaj Oct 31, 2023
753d15d
Bump `@slack/socket-mode` dev dependencies to fix the build and fix e…
filmaj Oct 31, 2023
53c0034
Bump `@slack/oauth` dev dependencies to fix the build and fix eslint …
filmaj Oct 31, 2023
5a5cdb1
Split out pins.* API arguments into own file. Document pin API methods.
filmaj Oct 31, 2023
11c69d2
Split out openid.* API arguments into own file. Document openid API m…
filmaj Oct 31, 2023
0e24e84
Factor out common OAuth/OpenID arguments into common.ts. Add descript…
filmaj Oct 31, 2023
352f985
Document migration.* methods and split out their arguments into own f…
filmaj Oct 31, 2023
336c62d
halfway through file uploads refactor
filmaj Nov 2, 2023
3c8d14a
Fix up the build related to reworked file upload arguments.
filmaj Nov 2, 2023
9446539
Document files.* APIs and finish rework of files API argument shapes.
filmaj Nov 2, 2023
f35fb45
Playing around with the type tests for files.
filmaj Nov 2, 2023
59026ca
lint tweaks for type tests, first stab at type tests.
filmaj Nov 3, 2023
3eaf51b
merge main branch in
filmaj Nov 3, 2023
2996db8
Add special import rules for type tests: allow reaching into src/ dir…
filmaj Nov 3, 2023
e9ac27a
tweaking some files.* API type tests.
filmaj Nov 14, 2023
7065af8
merging in latest `main`
filmaj Nov 15, 2023
7efacf7
fixes post-merge
filmaj Nov 15, 2023
d2a7882
small refactor of files.* API arguments and finished types tests for …
filmaj Nov 15, 2023
bb042dc
add type tests for views.* API arguments
filmaj Nov 16, 2023
13f0b0e
add type tests for users.* methods
filmaj Nov 16, 2023
a8c8dc5
usergroups.update only requires `usergroup` parameter, all other are …
filmaj Nov 16, 2023
524493d
new file for tooling.* API argument types, test types for it too. JSD…
filmaj Nov 16, 2023
3f5a661
team.* API argument type tests.
filmaj Nov 16, 2023
0bdd2af
search sort and sort dir args are optional, actually (thank you, test…
filmaj Nov 16, 2023
75a1e95
type tests for rtm.* APIs
filmaj Nov 16, 2023
1db4e03
added reminders.* API type tests.
filmaj Nov 17, 2023
4bea006
added reactions.* API type tests.
filmaj Nov 17, 2023
c3af002
added pins.* API type tests.
filmaj Nov 17, 2023
5a1a354
added openid.* API type tests.
filmaj Nov 17, 2023
cd81272
added oauth.* API type tests.
filmaj Nov 17, 2023
2ae4033
added migration.* API type tests.
filmaj Nov 17, 2023
a50ae8e
JSdoc, test types and separate request argument type file for emoji.l…
filmaj Nov 20, 2023
d887ef1
JSdoc, test types and separate request argument type file for dnd.* APIs
filmaj Nov 20, 2023
4d83293
JSdoc, test types and separate request argument type file for dialog.…
filmaj Nov 20, 2023
2ffa1bd
JSdoc, test types and separate request argument type file for convers…
filmaj Nov 20, 2023
587b71c
forgot one jsdoc
filmaj Nov 20, 2023
658cf45
JSdoc chat.* API methods
filmaj Nov 22, 2023
a29e77a
first pass at chat.* API method arg refactor and initial type tests.
filmaj Nov 22, 2023
6cc5f84
OK back in action: extensive postEphemeral type tests and tweaks to t…
filmaj Nov 22, 2023
4956cc8
extensive postMessage type tests and tweaks to types to make them pass
filmaj Nov 22, 2023
efec9ea
Factor out unfurl_* props into Unfurls interface for chat.* method ar…
filmaj Nov 22, 2023
71fd621
When assembling composite types, re-order them so that required prope…
filmaj Nov 23, 2023
38b1a4e
rejigging types around to get type tests passing - TS is weird.
filmaj Nov 23, 2023
40f706c
finished arg refactor and type tests for chat.* APIs.
filmaj Nov 23, 2023
c3aaac4
Refactor arguments and type tests for calls.* APIs.
filmaj Nov 23, 2023
73e1a9e
Refactor arguments and type tests for bots.* and bookmarks.* APIs.
filmaj Nov 23, 2023
64529a0
Refactor arguments and type tests for auth.* APIs.
filmaj Nov 23, 2023
6323f6e
jsdoc and some type tests for apps.* API, but manifest APIs needs som…
filmaj Nov 24, 2023
f453f2e
Refactor arguments and type tests for api.test API.
filmaj Nov 24, 2023
a3db517
stubbing out bits for admin.analytics.getFile API but need access to …
filmaj Nov 24, 2023
80c3d5e
Refactor arguments and type tests for admin.apps.* APIs.
filmaj Nov 24, 2023
d548bff
Refactor arguments and type tests for admin.auth.* APIs.
filmaj Nov 24, 2023
1d94a70
start of admin.barrier api rework
filmaj Nov 27, 2023
3687c89
Refactor arguments and type tests for admin.barriers.* APIs.
filmaj Nov 27, 2023
94c7c57
Refactor arguments and type tests for admin.analytics.getFile API.
filmaj Nov 28, 2023
08d1413
part of the way there for admin.conversations APIs
filmaj Nov 30, 2023
7d03359
Finishing up admin.conversations.* APIs.
filmaj Dec 5, 2023
38dfcde
Refactor arguments and type tests for admin.emoji.* APIs.
filmaj Dec 5, 2023
692792f
Refactor arguments and type tests for admin.functions.* APIs.
filmaj Dec 7, 2023
456c49b
merge in latest main (support for functions.* APIs)
filmaj Dec 7, 2023
2f1f9da
Refactor arguments and type tests for functions.* APIs.
filmaj Dec 7, 2023
22efb5a
fix bad reference
filmaj Dec 7, 2023
0bec7f6
Refactor arguments and type tests for admin.inviteRequests.* APIs.
filmaj Dec 7, 2023
0f59b96
Refactor arguments and type tests for admin.roles.* APIs.
filmaj Dec 7, 2023
fcebf0e
Refactor arguments and type tests for admin.teams.* APIs.
filmaj Dec 7, 2023
b6f1a88
users.profile.set should accept an object for the `profile` argument.
filmaj Dec 11, 2023
d0526bf
Refactor arguments and type tests for admin.usergroups.* APIs.
filmaj Dec 12, 2023
260e242
Refactor arguments and type tests for admin.users.* APIs.
filmaj Dec 12, 2023
a193b6e
Refactor arguments and type tests for admin.workflows.* APIs.
filmaj Dec 12, 2023
f01e0e6
Refactor arguments and type tests for apps.manifest.* APIs. Add first…
filmaj Dec 13, 2023
920c3fb
Update packages/web-api/src/types/request/bots.ts
filmaj Dec 14, 2023
6e2d515
7.0.0-rc.0 (#1704)
filmaj Dec 14, 2023
f340482
Tweaks to some JSDocs and being extra careful to allow for `text` wit…
filmaj Dec 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 38 additions & 41 deletions packages/web-api/src/WebClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@ export enum WebClientEvent {
RATE_LIMITED = 'rate_limited',
}

export interface WebAPICallOptions {
Copy link
Contributor Author

@filmaj filmaj Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this interface makes the arguments to each API method now 'type safe' (more or less) in that developers can't provide whatever arguments they want anymore (at least not without overriding/ignoring TypeScript).

We can more native-TypeScript-y instead use Record<string, unknown> to model any shapeless object in TypeScript (which the rest of this PR does where applicable).

[argument: string]: unknown;
}

export interface WebAPICallResult {
ok: boolean;
error?: string;
Expand All @@ -82,7 +78,6 @@ export interface WebAPICallResult {
// `chat.postMessage` returns an array of error messages (e.g., "messages": ["[ERROR] invalid_keys"])
messages?: string[];
};
[key: string]: unknown;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this change now prevents responses from being, basically, any shape.

}

// NOTE: should there be an async predicate?
Expand Down Expand Up @@ -224,7 +219,7 @@ export class WebClient extends Methods {
* @param method - the Web API method to call {@link https://api.slack.com/methods}
* @param options - options
*/
public async apiCall(method: string, options: WebAPICallOptions = {}): Promise<WebAPICallResult> {
public async apiCall(method: string, options: Record<string, unknown> = {}): Promise<WebAPICallResult> {
this.logger.debug(`apiCall('${method}') start`);

warnDeprecations(method, this.logger);
Expand Down Expand Up @@ -304,21 +299,21 @@ export class WebClient extends Methods {
* @param shouldStop - a predicate that is called with each page, and should return true when pagination can end.
* @param reduce - a callback that can be used to accumulate a value that the return promise is resolved to
*/
public paginate(method: string, options?: WebAPICallOptions): AsyncIterable<WebAPICallResult>;
public paginate(method: string, options?: Record<string, unknown>): AsyncIterable<WebAPICallResult>;
public paginate(
method: string,
options: WebAPICallOptions,
options: Record<string, unknown>,
shouldStop: PaginatePredicate,
): Promise<void>;
public paginate<R extends PageReducer, A extends PageAccumulator<R>>(
method: string,
options: WebAPICallOptions,
options: Record<string, unknown>,
shouldStop: PaginatePredicate,
reduce?: PageReducer<A>,
): Promise<A>;
public paginate<R extends PageReducer, A extends PageAccumulator<R>>(
method: string,
options?: WebAPICallOptions,
options?: Record<string, unknown>,
shouldStop?: PaginatePredicate,
reduce?: PageReducer<A>,
): (Promise<A> | AsyncIterable<WebAPICallResult>) {
Expand Down Expand Up @@ -394,22 +389,22 @@ export class WebClient extends Methods {
})();
}

/* eslint-disable no-trailing-spaces */
/**
* This wrapper method provides an easy way to upload files using the following endpoints:
*
*
* **#1**: For each file submitted with this method, submit filenames
* and file metadata to {@link https://api.slack.com/methods/files.getUploadURLExternal files.getUploadURLExternal} to request a URL to
* which to send the file data to and an id for the file
*
*
* **#2**: for each returned file `upload_url`, upload corresponding file to
* URLs returned from step 1 (e.g. https://files.slack.com/upload/v1/...\")
*
*
* **#3**: Complete uploads {@link https://api.slack.com/methods/files.completeUploadExternal files.completeUploadExternal}
*
* @param options
*/
public async filesUploadV2(options: FilesUploadV2Arguments): Promise<WebAPICallResult> {
public async filesUploadV2(options: FilesUploadV2Arguments): Promise<
WebAPICallResult & { files: FilesCompleteUploadExternalResponse[] }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small tweak to make this API a bit more type safe with its response.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch!

> {
this.logger.debug('files.uploadV2() start');
// 1
const fileUploads = await this.getAllFileUploads(options);
Expand All @@ -425,7 +420,7 @@ export class WebClient extends Methods {

// 3
const completion = await this.completeFileUploads(fileUploads);

return { ok: true, files: completion };
}

Expand Down Expand Up @@ -456,7 +451,7 @@ export class WebClient extends Methods {
* @returns
*/
private async completeFileUploads(fileUploads: FileUploadV2Job[]):
Promise<Array<FilesCompleteUploadExternalResponse>> {
Promise<Array<FilesCompleteUploadExternalResponse>> {
const toComplete: FilesCompleteUploadExternalArguments[] = Object.values(getAllFileUploadsToComplete(fileUploads));
return Promise.all(
toComplete.map((job: FilesCompleteUploadExternalArguments) => this.files.completeUploadExternal(job)),
Expand Down Expand Up @@ -485,10 +480,10 @@ export class WebClient extends Methods {
}, headers);
if (uploadRes.status !== 200) {
return Promise.reject(Error(`Failed to upload file (id:${file_id}, filename: ${filename})`));
}
}
const returnData = { ok: true, body: uploadRes.data } as WebAPICallResult;
return Promise.resolve(returnData);
}
}
return Promise.reject(Error(`No upload url found for file (id: ${file_id}, filename: ${filename}`));
}));
}
Expand All @@ -504,7 +499,7 @@ export class WebClient extends Methods {
if (options.file || options.content) {
fileUploads.push(await getFileUploadJob(options, this.logger));
}

// add multiple files data when file_uploads is supplied
if (options.file_uploads) {
fileUploads = fileUploads.concat(await getMultipleFileUploadJobs(options, this.logger));
Expand Down Expand Up @@ -594,8 +589,8 @@ export class WebClient extends Methods {
* @param options - arguments for the Web API method
* @param headers - a mutable object representing the HTTP headers for the outgoing request
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private serializeApiCallOptions(options: WebAPICallOptions, headers?: any): string | Readable {
private serializeApiCallOptions(options: Record<string, unknown>, headers?: Record<string, string>): string |
Readable {
// The following operation both flattens complex objects into a JSON-encoded strings and searches the values for
// binary content
let containsBinaryData: boolean = false;
Expand Down Expand Up @@ -648,18 +643,20 @@ export class WebClient extends Methods {
},
new FormData(),
);
// Copying FormData-generated headers into headers param
// not reassigning to headers param since it is passed by reference and behaves as an inout param
Object.entries(form.getHeaders()).forEach(([header, value]) => {
// eslint-disable-next-line no-param-reassign
headers[header] = value;
});
if (headers) {
// Copying FormData-generated headers into headers param
// not reassigning to headers param since it is passed by reference and behaves as an inout param
Object.entries(form.getHeaders()).forEach(([header, value]) => {
// eslint-disable-next-line no-param-reassign
headers[header] = value;
});
}
return form;
}

// Otherwise, a simple key-value object is returned
// eslint-disable-next-line no-param-reassign
headers['Content-Type'] = 'application/x-www-form-urlencoded';
if (headers) headers['Content-Type'] = 'application/x-www-form-urlencoded';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const initialValue: { [key: string]: any; } = {};
return qsStringify(flattened.reduce(
Expand Down Expand Up @@ -823,16 +820,16 @@ function warnDeprecations(method: string, logger: Logger): void {
* @param logger instance of we clients logger
* @param options arguments for the Web API method
*/
function warnIfFallbackIsMissing(method: string, logger: Logger, options?: WebAPICallOptions): void {
function warnIfFallbackIsMissing(method: string, logger: Logger, options?: Record<string, unknown>): void {
const targetMethods = ['chat.postEphemeral', 'chat.postMessage', 'chat.scheduleMessage'];
const isTargetMethod = targetMethods.includes(method);

const hasAttachments = (args: WebAPICallOptions) => Array.isArray(args.attachments) && args.attachments.length;
const hasAttachments = (args: Record<string, unknown>) => Array.isArray(args.attachments) && args.attachments.length;

const missingAttachmentFallbackDetected = (args: WebAPICallOptions) => Array.isArray(args.attachments) &&
const missingAttachmentFallbackDetected = (args: Record<string, unknown>) => Array.isArray(args.attachments) &&
args.attachments.some((attachment) => !attachment.fallback || attachment.fallback.trim() === '');

const isEmptyText = (args: WebAPICallOptions) => args.text === undefined || args.text === null || args.text === '';
const isEmptyText = (args: Record<string, unknown>) => args.text === undefined || args.text === null || args.text === '';

const buildMissingTextWarning = () => `The top-level \`text\` argument is missing in the request payload for a ${method} call - ` +
'It\'s a best practice to always provide a `text` argument when posting a message. ' +
Expand Down Expand Up @@ -860,23 +857,23 @@ function warnIfFallbackIsMissing(method: string, logger: Logger, options?: WebAP
* @param logger instance of web clients logger
* @param options arguments for the Web API method
*/
function warnIfThreadTsIsNotString(method: string, logger: Logger, options?: WebAPICallOptions): void {
function warnIfThreadTsIsNotString(method: string, logger: Logger, options?: Record<string, unknown>): void {
const targetMethods = ['chat.postEphemeral', 'chat.postMessage', 'chat.scheduleMessage', 'files.upload'];
const isTargetMethod = targetMethods.includes(method);

if (isTargetMethod && options?.thread_ts !== undefined && typeof options?.thread_ts !== 'string') {
logger.warn(buildThreadTsWarningMessage(method));
}
}

export function buildThreadTsWarningMessage(method: string): string {
export function buildThreadTsWarningMessage(method: string): string {
return `The given thread_ts value in the request payload for a ${method} call is a float value. We highly recommend using a string value instead.`;
}

/**
* Takes an object and redacts specific items
* @param body
* @returns
* @param body
* @returns
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function redact(body: any): any {
Expand All @@ -886,7 +883,7 @@ function redact(body: any): any {
if (value === undefined || value === null) {
return [];
}

let serializedValue = value;

// redact possible tokens
Expand All @@ -903,7 +900,7 @@ function redact(body: any): any {
return [key, serializedValue];
});

// return as object
// return as object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const initialValue: { [key: string]: any; } = {};
return flattened.reduce(
Expand Down
1 change: 0 additions & 1 deletion packages/web-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
export {
WebClient,
WebClientOptions,
WebAPICallOptions,
WebAPICallResult,
PageAccumulator,
PageReducer,
Expand Down
Loading
Loading