-
Notifications
You must be signed in to change notification settings - Fork 662
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
@slack/web-api making request and response shapes type-safe, and chat.postMessage arguments prototype #1670
Conversation
…eaning up the readme. remove no longer used codecov and deno build npm run scripts. update dependencies as much as possible. fix linter errors. remove a test that is no longer applicable with new axios.
…g, unknown>`. Remove unneeded disabling of no-trailing-space rule. Replace use of `headers: any` in private `serializeApiCallOptions` method with `Record<string, string>`.
…allResult. Tweak fileUploadV2 return type as a result.
…lines up behaviour with WebClient.
@@ -64,10 +64,6 @@ export enum WebClientEvent { | |||
RATE_LIMITED = 'rate_limited', | |||
} | |||
|
|||
export interface WebAPICallOptions { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing this generic interface, which is the basis for all API request arguments. This is the key change for addressing #1323 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! but to proceed with this, we may need to review all the argument types to verify if they have latest full list. Have you already done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not, and wanted to get feedback on this point; I mentioned in my original post that this change is significant enough that maybe it requires testing all the endpoints. It sounds to me like you agree that testing is needed. So, I will proceed with that.
@@ -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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other key change to address #1323: request responses allow for arbitrary keys, making them type-unsafe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the currently generated response types miss some of response properties, this change may affect TypeScript users. That being said, we can improve it by providing complete typing as much as possible.
* @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> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any areas where we still want things to extend from a shapeless foundation, we can instead use Record<string, unknown>
or Record<string, any>
to communicate "an object with any shape but with root keys being strings".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, i don't see any issues with this change
* **#4**: Unless `request_file_info` set to false, call {@link https://api.slack.com/methods/files.info files.info} for | ||
* each file uploaded and returns that data. Requires that your app have `files:read` scope. | ||
* @param options | ||
*/ | ||
public async filesUploadV2(options: FilesUploadV2Arguments): Promise<WebAPICallResult> { | ||
public async filesUploadV2(options: FilesUploadV2Arguments): Promise< | ||
WebAPICallResult & { files: FilesCompleteUploadExternalResponse[] | WebAPICallResult[] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small change to make this part typesafe as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch
* @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 | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another small change to not have to use any
.
@@ -1593,25 +1641,174 @@ export interface ChatPostEphemeralArguments extends WebAPICallOptions, TokenOver | |||
icon_url?: string; // if specified, as_user must be false | |||
username?: string; // if specified, as_user must be false | |||
} | |||
export interface ChatPostMessageArguments extends WebAPICallOptions, TokenOverridable { | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the area of the code where I prototype modeling the chat.postMessage
arguments differently, using unions and intersections instead of optional properties.
// Controls whether a message is a thread reply or not | ||
ChatPostMessageArgumentsType; | ||
|
||
const textOnly: ChatPostMessageArguments = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are just tiny test cases, to show off how this woud look like.
|
||
type ChatPostMessageArgumentsType = ChatPostMessageArgumentsThreadReply | ChatPostMessageArgumentsConversationMessage; | ||
|
||
type ChatPostMessageArguments = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This intersection type shows off the core of this approach: assemble the entire shape of a payload using intersections (&
). Each constituent type within is a union (|
). This allows us to be able to finely control when certain properties should be available, or not, or optional, in different circumstances.
reply_broadcast?: never; | ||
} | ||
|
||
type ChatPostMessageArgumentsType = ChatPostMessageArgumentsThreadReply | ChatPostMessageArgumentsConversationMessage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This union (|
) type models a message that is either a conversation message, or a reply-in-thread. Either a message does not specify neither of thread_ts
and reply_broadcast
(to model a conversation message, in-channel or DM), or a message has a thread_ts
and an optional reply_broadcast
(to model a reply-in-thread).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great PR! Overall, the changes here look great to me
@@ -64,10 +64,6 @@ export enum WebClientEvent { | |||
RATE_LIMITED = 'rate_limited', | |||
} | |||
|
|||
export interface WebAPICallOptions { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! but to proceed with this, we may need to review all the argument types to verify if they have latest full list. Have you already done?
@@ -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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the currently generated response types miss some of response properties, this change may affect TypeScript users. That being said, we can improve it by providing complete typing as much as possible.
* @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> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, i don't see any issues with this change
* **#4**: Unless `request_file_info` set to false, call {@link https://api.slack.com/methods/files.info files.info} for | ||
* each file uploaded and returns that data. Requires that your app have `files:read` scope. | ||
* @param options | ||
*/ | ||
public async filesUploadV2(options: FilesUploadV2Arguments): Promise<WebAPICallResult> { | ||
public async filesUploadV2(options: FilesUploadV2Arguments): Promise< | ||
WebAPICallResult & { files: FilesCompleteUploadExternalResponse[] | WebAPICallResult[] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch
/** | ||
* @description The fallback text used in notifications. | ||
*/ | ||
text?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intentionally optional? In my undertstanding, having text
is still recommended even when having blocks
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is true. Technically, they are optional, but developers are strongly encouraged to provide one since this is an accessibility issue.
Do you think we should make it required in this SDK?
/** | ||
* @description `chat.postMessage` arguments for specifying the message icon using a URL. | ||
*/ | ||
interface ChatPostMessageArgumentsIconURL extends ChatPostMessageArgumentsIdentityBase { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
reply_broadcast: true, | ||
icon_url: '1234', | ||
}; | ||
console.log(textOnly, attachments, bs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: remove this (perhps the tests in this file too)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes exactly, this is just a draft to have a discussion.
Thank you for reviewing @seratch 🙇 I think what I will do is extract the changes that address just #1323 into a separate PR, and start testing each API endpoint to make sure things work, and the types are functional for developers (i.e. no missing properties in request or response payloads, no static type errors prevent issuing API calls, and so on). Separately / in parallel / bit by bit over time, I plan on moving API and event payloads as types into |
Closing this PR in favour of #1673 |
Purely Intended For Discussion
This draft PR prototypes addressing #1323: the web API arguments and responses not being type-safe due to allowing arbitrary properties in both request and response payloads.
This passes tests locally, and I also integrated with bolt-js,
@slack/oauth
and@slack/socket-mode
locally (vianpm link
) and the local unit tests in those packages also passed.TL;DR: should we do more fine-grained testing against production of this change to ensure this doesn't break anything?
I think manual testing might be needed before merging this change. There are many different HTTP API methods with a variety of arguments and responses. I think it may be worth testing every method to see if at least basic interactions with each API are possible. 😬 Not sure, what do you think?
On the flip side, if I am able to put the time into manual testing each method, I think there would be many benefits:
chat.postMessage
API requires one ofattachments
,blocks
ortext
. Currently we model these as optional properties, instead, we should model them as unions. I included a prototype of this in this PR - jump to this comment of mine to see what that looks like insrc/method.ts
.a. Could also do this for API response shapes.
b. This could also be left as an exercise for the future. For example, maybe such an audit, and modifying the request and response payloads for Slack's HTTP API, could be left as something to work on for an eventual web-api 8.0 release.
c. I am also considering moving these kinds of shapes to the
@slack/types
repo. Let me know if you have thoughts on this (I posted about this in our internal Slack team channel as well). Pulling the types out of this repo would ease re-use in other projects, such as the deno SDK projects.Depending on the discussion, I could see different potential outcomes / decisions and I am looking for feedback from the team on these:
@slack/types
and having@slack/web-api
pull them from there. Or maybe you don't and you think we should keep them here.