diff --git a/package.json b/package.json index 6907d6b..c49069d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knocklabs/node", - "version": "0.6.4", + "version": "0.6.5", "description": "Library for interacting with the Knock API", "homepage": "https://github.com/knocklabs/knock-node", "author": "@knocklabs", diff --git a/src/common/fetchClient.ts b/src/common/fetchClient.ts index 51045b7..57dc620 100644 --- a/src/common/fetchClient.ts +++ b/src/common/fetchClient.ts @@ -90,17 +90,32 @@ export default class FetchClient { private buildUrl(path: string, params?: FetchRequestConfig["params"]): URL { const url = new URL(this.config.baseURL + path); - if (params) { - Object.entries(params).forEach(([key, value]) => { + function appendParams(key: string, value: any, parentKey?: string) { + const fullKey = parentKey ? `${parentKey}[${key}]` : key; + + if ( + typeof value === "object" && + value !== null && + !(value instanceof Date) && + !(value instanceof Array) + ) { + // If value is an object, recurse + Object.entries(value).forEach(([nestedKey, nestedValue]) => { + appendParams(nestedKey, nestedValue, fullKey); + }); + } else if (Array.isArray(value)) { // Send array values as individual values instead of a comma separated list // e.g. key[]=1&key[]=2&key[]=3 instead of key=1,2,3 - if (Array.isArray(value)) { - for (const val of value) { - url.searchParams.append(`${key}[]`, val); - } - } else { - url.searchParams.append(key, value); - } + value.forEach((val) => url.searchParams.append(`${fullKey}[]`, val)); + } else { + // For primitive values, simply append them + url.searchParams.append(fullKey, value); + } + } + + if (params) { + Object.entries(params).forEach(([key, value]) => { + appendParams(key, value); }); } diff --git a/src/index.ts b/src/index.ts index 97a4ecd..ddda1b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from "./resources/workflows/interfaces"; export * from "./resources/users/interfaces"; export * from "./resources/preferences/interfaces"; +export * from "./resources/slack/interfaces"; export * from "./common/interfaces"; export { Knock } from "./knock"; diff --git a/src/knock.ts b/src/knock.ts index 22acd66..aab43d8 100644 --- a/src/knock.ts +++ b/src/knock.ts @@ -30,6 +30,7 @@ import { TokenGrant, TokenGrantOptions, } from "./common/userTokens"; +import { Slack } from "./resources/slack"; const DEFAULT_HOSTNAME = "https://api.knock.app"; @@ -44,6 +45,7 @@ class Knock { readonly objects = new Objects(this); readonly messages = new Messages(this); readonly tenants = new Tenants(this); + readonly slack = new Slack(this); constructor(readonly key?: string, readonly options: KnockOptions = {}) { if (!key) { diff --git a/src/resources/slack/index.ts b/src/resources/slack/index.ts new file mode 100644 index 0000000..c484afc --- /dev/null +++ b/src/resources/slack/index.ts @@ -0,0 +1,85 @@ +import { Knock } from "../../knock"; +import { + AuthCheckInput, + GetSlackChannelsInput, + PaginatedSlackChannelResponse, + RevokeAccessTokenInput, +} from "./interfaces"; + +const TENANT_COLLECTION = "$tenants"; + +function removeNullKeys(obj: T): Partial { + Object.keys(obj).forEach((key) => { + if (obj[key as keyof T] === null) { + delete obj[key as keyof T]; + } + }); + return obj; +} + +export class Slack { + constructor(readonly knock: Knock) {} + + async getChannels( + input: GetSlackChannelsInput, + ): Promise { + const { knockChannelId, tenant } = input; + const queryOptions = input.queryOptions || {}; + + const params = { + access_token_object: { + object_id: tenant, + collection: TENANT_COLLECTION, + }, + channel_id: knockChannelId, + query_options: removeNullKeys({ + cursor: queryOptions.cursor || null, + limit: queryOptions.limit || null, + exclude_archived: queryOptions.excludeArchived || null, + team_id: queryOptions.teamId || null, + types: queryOptions.types || null, + }), + }; + + const { data } = await this.knock.get( + `/v1/providers/slack/${knockChannelId}/channels`, + params, + ); + + return data; + } + + async authCheck({ tenant, knockChannelId }: AuthCheckInput) { + const params = { + access_token_object: { + object_id: tenant, + collection: TENANT_COLLECTION, + }, + channel_id: knockChannelId, + }; + + const { data } = await this.knock.get( + `/v1/providers/slack/${knockChannelId}/auth_check`, + params, + ); + + return data; + } + + async revokeAccessToken({ tenant, knockChannelId }: RevokeAccessTokenInput) { + const params = { + access_token_object: { + object_id: tenant, + collection: TENANT_COLLECTION, + }, + channel_id: knockChannelId, + }; + + const { data } = await this.knock.get( + `/v1/providers/slack/${knockChannelId}/revoke_access`, + params, + ); + + return data; + } +} diff --git a/src/resources/slack/interfaces.ts b/src/resources/slack/interfaces.ts new file mode 100644 index 0000000..1077def --- /dev/null +++ b/src/resources/slack/interfaces.ts @@ -0,0 +1,34 @@ +export type GetSlackChannelsInput = { + tenant: string; + knockChannelId: string; + queryOptions?: { + limit?: number; + cursor?: string; + excludeArchived?: boolean; + teamId?: string; + types?: string; + }; +}; + +export type SlackChannel = { + name: string; + id: string; + is_private: boolean; + is_im: boolean; + context_team_id: boolean; +}; + +export type PaginatedSlackChannelResponse = { + slack_channels: SlackChannel[]; + next_cursor: string; +}; + +export type AuthCheckInput = { + tenant: string; + knockChannelId: string; +}; + +export type RevokeAccessTokenInput = { + tenant: string; + knockChannelId: string; +};