From f7966e7c4a15ec24f870d69ed920bf1daa75dddb Mon Sep 17 00:00:00 2001 From: Alissa Renz Date: Wed, 7 Aug 2024 14:45:03 -0700 Subject: [PATCH 1/3] Fixes and polish for stable release --- src/App.ts | 16 ++++++++++++---- src/CustomFunction.spec.ts | 3 ++- src/CustomFunction.ts | 16 +++++++++++++--- src/errors.ts | 10 ++++++++++ src/types/events/base-events.ts | 8 ++++---- src/types/middleware.ts | 22 +++++++++++++++++++++- 6 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/App.ts b/src/App.ts index 9902de838..da0e490e8 100644 --- a/src/App.ts +++ b/src/App.ts @@ -54,6 +54,7 @@ import { SlashCommand, WorkflowStepEdit, SlackOptions, + FunctionInputs, } from './types'; import { IncomingEventType, getTypeAndConversation, assertNever, isBodyWithTypeEnterpriseInstall, isEventTypeToSkipAuthorize } from './helpers'; import { CodedError, asCodedError, AppInitializationError, MultipleListenerError, ErrorCode, InvalidCustomPropertyError } from './errors'; @@ -964,9 +965,12 @@ export default class App retryReason: event.retryReason, }; - // Extract function-related information and augment to context - const { functionExecutionId, functionBotAccessToken } = extractFunctionContext(body); - if (functionExecutionId) { context.functionExecutionId = functionExecutionId; } + // Extract function-related information and augment context + const { functionExecutionId, functionBotAccessToken, functionInputs } = extractFunctionContext(body); + if (functionExecutionId) { + context.functionExecutionId = functionExecutionId; + if (functionInputs) { context.functionInputs = functionInputs; } + } if (this.attachFunctionToken) { if (functionBotAccessToken) { context.functionBotAccessToken = functionBotAccessToken; } @@ -1029,6 +1033,7 @@ export default class App ack?: AckFn; complete?: FunctionCompleteFn; fail?: FunctionFailFn; + inputs?: FunctionInputs; } = { body: bodyArg, payload, @@ -1088,6 +1093,7 @@ export default class App if (type === IncomingEventType.Action && context.functionExecutionId !== undefined) { listenerArgs.complete = CustomFunction.createFunctionComplete(context, client); listenerArgs.fail = CustomFunction.createFunctionFail(context, client); + listenerArgs.inputs = context.functionInputs; } if (token !== undefined) { @@ -1599,6 +1605,7 @@ function escapeHtml(input: string | undefined | null): string { function extractFunctionContext(body: StringIndexed) { let functionExecutionId; let functionBotAccessToken; + let functionInputs; // function_executed event if (body.event && body.event.type === 'function_executed' && body.event.function_execution_id) { @@ -1610,9 +1617,10 @@ function extractFunctionContext(body: StringIndexed) { if (body.function_data) { functionExecutionId = body.function_data.execution_id; functionBotAccessToken = body.bot_access_token; + functionInputs = body.function_data.inputs; } - return { functionExecutionId, functionBotAccessToken }; + return { functionExecutionId, functionBotAccessToken, functionInputs }; } // ---------------------------- diff --git a/src/CustomFunction.spec.ts b/src/CustomFunction.spec.ts index 5b5a5429f..3668e19bc 100644 --- a/src/CustomFunction.spec.ts +++ b/src/CustomFunction.spec.ts @@ -65,7 +65,7 @@ describe('CustomFunction class', () => { assert(fakeNext.called); }); - it('should call next if not a workflow step event', async () => { + it('should call next if not a function executed event', async () => { const fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE); const middleware = fn.getMiddleware(); const fakeViewArgs = createFakeViewEvent() as unknown as @@ -215,6 +215,7 @@ function createFakeFunctionExecutedEvent() { }, context: { functionBotAccessToken: 'xwfp-123', + functionExecutionId: 'test_executed_callback_id', }, }; } diff --git a/src/CustomFunction.ts b/src/CustomFunction.ts index 8f52c7e97..50ee1e2d2 100644 --- a/src/CustomFunction.ts +++ b/src/CustomFunction.ts @@ -13,7 +13,7 @@ import { FunctionExecutedEvent, } from './types'; import processMiddleware from './middleware/process'; -import { CustomFunctionInitializationError } from './errors'; +import { CustomFunctionCompleteFailError, CustomFunctionCompleteSuccessError, CustomFunctionInitializationError } from './errors'; /** Interfaces */ @@ -105,6 +105,11 @@ export class CustomFunction { const token = selectToken(context); const { functionExecutionId } = context; + if (!functionExecutionId) { + const errorMsg = 'No function_execution_id found'; + throw new CustomFunctionCompleteSuccessError(errorMsg); + } + return (params: Parameters[0] = {}) => client.functions.completeSuccess({ token, outputs: params.outputs || {}, @@ -123,6 +128,11 @@ export class CustomFunction { const { error } = params ?? {}; const { functionExecutionId } = context; + if (!functionExecutionId) { + const errorMsg = 'No function_execution_id found'; + throw new CustomFunctionCompleteFailError(errorMsg); + } + return client.functions.completeError({ token, error, @@ -158,8 +168,8 @@ export function validate(callbackId: string, middleware: CustomFunctionExecuteMi } /** - * `processFunctionMiddleware()` invokes each callback for lifecycle event - * @param args workflow_step_edit action + * `processFunctionMiddleware()` invokes each listener middleware + * @param args function_executed event */ export async function processFunctionMiddleware( args: AllCustomFunctionMiddlewareArgs, diff --git a/src/errors.ts b/src/errors.ts index c270b9c35..09a5a8b7f 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -41,6 +41,8 @@ export enum ErrorCode { WorkflowStepInitializationError = 'slack_bolt_workflow_step_initialization_error', CustomFunctionInitializationError = 'slack_bolt_custom_function_initialization_error', + CustomFunctionCompleteSuccessError = 'slack_bolt_custom_function_complete_success_error', + CustomFunctionCompleteFailError = 'slack_bolt_custom_function_complete_fail_error', } export class UnknownError extends Error implements CodedError { @@ -149,3 +151,11 @@ export class WorkflowStepInitializationError extends Error implements CodedError export class CustomFunctionInitializationError extends Error implements CodedError { public code = ErrorCode.CustomFunctionInitializationError; } + +export class CustomFunctionCompleteSuccessError extends Error implements CodedError { + public code = ErrorCode.CustomFunctionCompleteSuccessError; +} + +export class CustomFunctionCompleteFailError extends Error implements CodedError { + public code = ErrorCode.CustomFunctionCompleteFailError; +} diff --git a/src/types/events/base-events.ts b/src/types/events/base-events.ts index 32d9ae528..9abc99558 100644 --- a/src/types/events/base-events.ts +++ b/src/types/events/base-events.ts @@ -432,11 +432,11 @@ export interface FileUnsharedEvent { } export interface FunctionParams { - type?: string; - name?: string; + type: string; + name: string; description?: string; title?: string; - is_required?: boolean; + is_required: boolean; } export interface FunctionInputs { @@ -451,7 +451,7 @@ export interface FunctionExecutedEvent { id: string; callback_id: string; title: string; - description: string; + description?: string; type: string; input_parameters: FunctionParams[]; output_parameters: FunctionParams[]; diff --git a/src/types/middleware.ts b/src/types/middleware.ts index a36fddc62..d1d8878df 100644 --- a/src/types/middleware.ts +++ b/src/types/middleware.ts @@ -1,7 +1,7 @@ import { WebClient } from '@slack/web-api'; import { Logger } from '@slack/logger'; import { StringIndexed } from './helpers'; -import { SlackEventMiddlewareArgs } from './events'; +import { FunctionInputs, SlackEventMiddlewareArgs } from './events'; import { SlackActionMiddlewareArgs } from './actions'; import { SlackCommandMiddlewareArgs } from './command'; import { SlackOptionsMiddlewareArgs } from './options'; @@ -73,6 +73,23 @@ export interface Context extends StringIndexed { */ isEnterpriseInstall: boolean, + /** + * A JIT and function-specific token that, when used to make API calls, + * creates an association between a function's execution and subsequent actions + * (e.g., buttons and other interactivity) + */ + functionBotAccessToken?: string; + + /** + * Function execution ID associated with the event + */ + functionExecutionId?: string; + + /** + * Inputs that were provided to a function when it was executed + */ + functionInputs?: FunctionInputs; + /** * Retry count of an Events API request (this property does not exist for other requests) */ @@ -90,6 +107,9 @@ export const contextBuiltinKeys: string[] = [ 'botUserId', 'teamId', 'enterpriseId', + 'functionBotAccessToken', + 'functionExecutionId', + 'functionInputs', 'retryNum', 'retryReason', ]; From d390d8f3d341832879dea8f8410b56ec060bc4cc Mon Sep 17 00:00:00 2001 From: Fil Maj Date: Thu, 8 Aug 2024 19:36:31 +0000 Subject: [PATCH 2/3] Added complete, fail and inputs parameters on `SlackActionMiddlewareArgs` (#2189) --- src/types/actions/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/types/actions/index.ts b/src/types/actions/index.ts index c94a8673c..ac7f74d07 100644 --- a/src/types/actions/index.ts +++ b/src/types/actions/index.ts @@ -3,6 +3,8 @@ import { InteractiveMessage } from './interactive-message'; import { WorkflowStepEdit } from './workflow-step-edit'; import { DialogSubmitAction, DialogValidation } from './dialog-action'; import { SayFn, SayArguments, RespondFn, AckFn } from '../utilities'; +import { FunctionCompleteFn, FunctionFailFn } from '../../CustomFunction'; +import { FunctionInputs } from '../events'; export * from './block-action'; export * from './interactive-message'; @@ -45,6 +47,9 @@ export interface SlackActionMiddlewareArgs ? SayFn : never; respond: RespondFn; ack: ActionAckFn; + complete?: FunctionCompleteFn; + fail?: FunctionFailFn; + inputs?: FunctionInputs; } /** From 7ee465beaa3223e6af70269832ca4447c4bce3f3 Mon Sep 17 00:00:00 2001 From: Fil Maj Date: Mon, 12 Aug 2024 17:50:20 +0000 Subject: [PATCH 3/3] Remote Functions: Change how webclient options are constructed in CustomFunction (#2190) --- package.json | 3 +- src/App.ts | 6 +- src/CustomFunction.spec.ts | 141 ++++++++++++++++++++++++++---------- src/CustomFunction.ts | 32 ++++---- src/middleware/builtin.ts | 24 +++--- src/middleware/process.ts | 2 +- src/types/actions/index.ts | 2 +- src/types/events/index.ts | 8 +- src/types/helpers.ts | 2 + tsconfig.eslint.json | 2 +- types-tests/event.test-d.ts | 20 ++--- 11 files changed, 161 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index 2ac6247ae..40c180517 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output", "lint": "eslint --fix --ext .ts src", "mocha": "TS_NODE_PROJECT=tsconfig.json nyc mocha --config .mocharc.json \"src/**/*.spec.ts\"", - "test": "npm run lint && npm run mocha && npm run test:types", + "test": "npm run build && npm run lint && npm run mocha && npm run test:types", + "test:coverage": "npm run mocha && nyc report --reporter=text", "test:types": "tsd", "watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build" }, diff --git a/src/App.ts b/src/App.ts index da0e490e8..ac4cbefd7 100644 --- a/src/App.ts +++ b/src/App.ts @@ -503,6 +503,10 @@ export default class App } } + public get webClientOptions(): WebClientOptions { + return this.clientOptions; + } + /** * Register a new middleware, processed in the order registered. * @@ -530,7 +534,7 @@ export default class App * Register CustomFunction middleware */ public function(callbackId: string, ...listeners: CustomFunctionMiddleware): this { - const fn = new CustomFunction(callbackId, listeners); + const fn = new CustomFunction(callbackId, listeners, this.webClientOptions); const m = fn.getMiddleware(); this.middleware.push(m); return this; diff --git a/src/CustomFunction.spec.ts b/src/CustomFunction.spec.ts index 3668e19bc..24dc00059 100644 --- a/src/CustomFunction.spec.ts +++ b/src/CustomFunction.spec.ts @@ -2,6 +2,7 @@ import 'mocha'; import { assert } from 'chai'; import sinon from 'sinon'; import rewiremock from 'rewiremock'; +import { WebClient } from '@slack/web-api'; import { CustomFunction, SlackCustomFunctionMiddlewareArgs, @@ -9,8 +10,8 @@ import { CustomFunctionMiddleware, CustomFunctionExecuteMiddlewareArgs, } from './CustomFunction'; -import { Override } from './test-helpers'; -import { AllMiddlewareArgs, AnyMiddlewareArgs, Middleware } from './types'; +import { createFakeLogger, Override } from './test-helpers'; +import { AllMiddlewareArgs, Middleware } from './types'; import { CustomFunctionInitializationError } from './errors'; async function importCustomFunction(overrides: Override = {}): Promise { @@ -26,36 +27,35 @@ const MOCK_MIDDLEWARE_MULTIPLE = [MOCK_FN, MOCK_FN_2]; describe('CustomFunction class', () => { describe('constructor', () => { it('should accept single function as middleware', async () => { - const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE); + const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE, {}); assert.isNotNull(fn); }); it('should accept multiple functions as middleware', async () => { - const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE); + const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE, {}); assert.isNotNull(fn); }); }); describe('getMiddleware', () => { it('should not call next if a function_executed event', async () => { - const fn = new CustomFunction('test_executed_callback_id', MOCK_MIDDLEWARE_SINGLE); + const cbId = 'test_executed_callback_id'; + const fn = new CustomFunction(cbId, MOCK_MIDDLEWARE_SINGLE, {}); const middleware = fn.getMiddleware(); - const fakeEditArgs = createFakeFunctionExecutedEvent() as unknown as - SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs; + const fakeEditArgs = createFakeFunctionExecutedEvent(cbId); const fakeNext = sinon.spy(); fakeEditArgs.next = fakeNext; await middleware(fakeEditArgs); - assert(fakeNext.notCalled); + assert(fakeNext.notCalled, 'next called!'); }); it('should call next if valid custom function but mismatched callback_id', async () => { - const fn = new CustomFunction('bad_executed_callback_id', MOCK_MIDDLEWARE_SINGLE); + const fn = new CustomFunction('bad_executed_callback_id', MOCK_MIDDLEWARE_SINGLE, {}); const middleware = fn.getMiddleware(); - const fakeEditArgs = createFakeFunctionExecutedEvent() as unknown as - SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs; + const fakeEditArgs = createFakeFunctionExecutedEvent(); const fakeNext = sinon.spy(); fakeEditArgs.next = fakeNext; @@ -66,7 +66,7 @@ describe('CustomFunction class', () => { }); it('should call next if not a function executed event', async () => { - const fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE); + const fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE, {}); const middleware = fn.getMiddleware(); const fakeViewArgs = createFakeViewEvent() as unknown as SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs; @@ -120,8 +120,7 @@ describe('CustomFunction class', () => { describe('isFunctionEvent', () => { it('should return true if recognized function_executed payload type', async () => { - const fakeExecuteArgs = createFakeFunctionExecutedEvent() as unknown as SlackCustomFunctionMiddlewareArgs - & AllMiddlewareArgs; + const fakeExecuteArgs = createFakeFunctionExecutedEvent(); const { isFunctionEvent } = await importCustomFunction(); const eventIsFunctionExcuted = isFunctionEvent(fakeExecuteArgs); @@ -130,7 +129,8 @@ describe('CustomFunction class', () => { }); it('should return false if not a function_executed payload type', async () => { - const fakeExecutedEvent = createFakeFunctionExecutedEvent() as unknown as AnyMiddlewareArgs; + const fakeExecutedEvent = createFakeFunctionExecutedEvent(); + // @ts-expect-error expected invalid payload type fakeExecutedEvent.payload.type = 'invalid_type'; const { isFunctionEvent } = await importCustomFunction(); @@ -142,10 +142,10 @@ describe('CustomFunction class', () => { describe('enrichFunctionArgs', () => { it('should remove next() from all original event args', async () => { - const fakeExecutedEvent = createFakeFunctionExecutedEvent() as unknown as AnyMiddlewareArgs; + const fakeExecutedEvent = createFakeFunctionExecutedEvent(); const { enrichFunctionArgs } = await importCustomFunction(); - const executeFunctionArgs = enrichFunctionArgs(fakeExecutedEvent); + const executeFunctionArgs = enrichFunctionArgs(fakeExecutedEvent, {}); assert.notExists(executeFunctionArgs.next); }); @@ -154,7 +154,7 @@ describe('CustomFunction class', () => { const fakeArgs = createFakeFunctionExecutedEvent(); const { enrichFunctionArgs } = await importCustomFunction(); - const functionArgs = enrichFunctionArgs(fakeArgs); + const functionArgs = enrichFunctionArgs(fakeArgs, {}); assert.exists(functionArgs.inputs); assert.exists(functionArgs.complete); @@ -163,19 +163,43 @@ describe('CustomFunction class', () => { }); describe('custom function utility functions', () => { - it('complete should call functions.completeSuccess', async () => { - // TODO + describe('`complete` factory function', () => { + it('complete should call functions.completeSuccess', async () => { + const client = new WebClient('sometoken'); + const completeMock = sinon.stub(client.functions, 'completeSuccess').resolves(); + const complete = CustomFunction.createFunctionComplete({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client); + await complete(); + assert(completeMock.called, 'client.functions.completeSuccess not called!'); + }); + it('should throw if no functionExecutionId present on context', () => { + const client = new WebClient('sometoken'); + assert.throws(() => { + CustomFunction.createFunctionComplete({ isEnterpriseInstall: false }, client); + }); + }); }); - it('fail should call functions.completeError', async () => { - // TODO + describe('`fail` factory function', () => { + it('fail should call functions.completeError', async () => { + const client = new WebClient('sometoken'); + const completeMock = sinon.stub(client.functions, 'completeError').resolves(); + const complete = CustomFunction.createFunctionFail({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client); + await complete({ error: 'boom' }); + assert(completeMock.called, 'client.functions.completeError not called!'); + }); + it('should throw if no functionExecutionId present on context', () => { + const client = new WebClient('sometoken'); + assert.throws(() => { + CustomFunction.createFunctionFail({ isEnterpriseInstall: false }, client); + }); + }); }); it('inputs should map to function payload inputs', async () => { - const fakeExecuteArgs = createFakeFunctionExecutedEvent() as unknown as AllCustomFunctionMiddlewareArgs; + const fakeExecuteArgs = createFakeFunctionExecutedEvent(); const { enrichFunctionArgs } = await importCustomFunction(); - const enrichedArgs = enrichFunctionArgs(fakeExecuteArgs); + const enrichedArgs = enrichFunctionArgs(fakeExecuteArgs, {}); assert.isTrue(enrichedArgs.inputs === fakeExecuteArgs.event.inputs); }); @@ -183,40 +207,79 @@ describe('CustomFunction class', () => { describe('processFunctionMiddleware', () => { it('should call each callback in user-provided middleware', async () => { - const { ...fakeArgs } = createFakeFunctionExecutedEvent() as unknown as AllCustomFunctionMiddlewareArgs; + const { ...fakeArgs } = createFakeFunctionExecutedEvent(); const { processFunctionMiddleware } = await importCustomFunction(); const fn1 = sinon.spy((async ({ next: continuation }) => { await continuation(); }) as Middleware); - const fn2 = sinon.spy(async () => {}); + const fn2 = sinon.spy(async () => { + }); const fakeMiddleware = [fn1, fn2] as CustomFunctionMiddleware; await processFunctionMiddleware(fakeArgs, fakeMiddleware); - assert(fn1.called); - assert(fn2.called); + assert(fn1.called, 'first user-provided middleware not called!'); + assert(fn2.called, 'second user-provided middleware not called!'); }); }); }); -function createFakeFunctionExecutedEvent() { +function createFakeFunctionExecutedEvent(callbackId?: string): AllCustomFunctionMiddlewareArgs { + const func = { + type: 'function', + id: 'somefunc', + callback_id: callbackId || 'callback_id', + title: 'My dope function', + input_parameters: [], + output_parameters: [], + app_id: 'A1234', + date_created: 123456, + date_deleted: 0, + date_updated: 123456, + }; + const base = { + bot_access_token: 'xoxb-abcd-1234', + event_ts: '123456.789', + function_execution_id: 'Fx1234', + workflow_execution_id: 'Wf1234', + type: 'function_executed', + } as const; + const inputs = { message: 'test123', recipient: 'U012345' }; + const event = { + function: func, + inputs, + ...base, + } as const; return { - event: { - inputs: { message: 'test123', recipient: 'U012345' }, - }, - payload: { - type: 'function_executed', - function: { - callback_id: 'test_executed_callback_id', - }, - inputs: { message: 'test123', recipient: 'U012345' }, - bot_access_token: 'xwfp-123', + body: { + api_app_id: 'A1234', + event, + event_id: 'E1234', + event_time: 123456, + team_id: 'T1234', + token: 'xoxb-1234', + type: 'event_callback', }, + client: new WebClient('faketoken'), + complete: () => Promise.resolve({ ok: true }), context: { functionBotAccessToken: 'xwfp-123', functionExecutionId: 'test_executed_callback_id', + isEnterpriseInstall: false, + }, + event, + fail: () => Promise.resolve({ ok: true }), + inputs, + logger: createFakeLogger(), + message: undefined, + next: () => Promise.resolve(), + payload: { + function: func, + inputs: { message: 'test123', recipient: 'U012345' }, + ...base, }, + say: undefined, }; } diff --git a/src/CustomFunction.ts b/src/CustomFunction.ts index 50ee1e2d2..4033bc0d9 100644 --- a/src/CustomFunction.ts +++ b/src/CustomFunction.ts @@ -3,6 +3,7 @@ import { WebClient, FunctionsCompleteErrorResponse, FunctionsCompleteSuccessResponse, + WebClientOptions, } from '@slack/web-api'; import { Middleware, @@ -62,14 +63,18 @@ export class CustomFunction { /** Function callback_id */ public callbackId: string; + private appWebClientOptions: WebClientOptions; + private middleware: CustomFunctionMiddleware; public constructor( callbackId: string, middleware: CustomFunctionExecuteMiddleware, + clientOptions: WebClientOptions, ) { validate(callbackId, middleware); + this.appWebClientOptions = clientOptions; this.callbackId = callbackId; this.middleware = middleware; } @@ -88,7 +93,7 @@ export class CustomFunction { } private async processEvent(args: AllCustomFunctionMiddlewareArgs): Promise { - const functionArgs = enrichFunctionArgs(args); + const functionArgs = enrichFunctionArgs(args, this.appWebClientOptions); const functionMiddleware = this.getFunctionMiddleware(); return processFunctionMiddleware(functionArgs, functionMiddleware); } @@ -99,7 +104,6 @@ export class CustomFunction { /** * Factory for `complete()` utility - * @param args function_executed event */ public static createFunctionComplete(context: Context, client: WebClient): FunctionCompleteFn { const token = selectToken(context); @@ -119,19 +123,18 @@ export class CustomFunction { /** * Factory for `fail()` utility - * @param args function_executed event */ public static createFunctionFail(context: Context, client: WebClient): FunctionFailFn { const token = selectToken(context); + const { functionExecutionId } = context; + + if (!functionExecutionId) { + const errorMsg = 'No function_execution_id found'; + throw new CustomFunctionCompleteFailError(errorMsg); + } return (params: Parameters[0]) => { const { error } = params ?? {}; - const { functionExecutionId } = context; - - if (!functionExecutionId) { - const errorMsg = 'No function_execution_id found'; - throw new CustomFunctionCompleteFailError(errorMsg); - } return client.functions.completeError({ token, @@ -169,7 +172,6 @@ export function validate(callbackId: string, middleware: CustomFunctionExecuteMi /** * `processFunctionMiddleware()` invokes each listener middleware - * @param args function_executed event */ export async function processFunctionMiddleware( args: AllCustomFunctionMiddlewareArgs, @@ -202,14 +204,16 @@ function selectToken(context: Context): string | undefined { * - events will *not* continue down global middleware chain to subsequent listeners * 2. augments args with step lifecycle-specific properties/utilities * */ -export function enrichFunctionArgs(args: any): AllCustomFunctionMiddlewareArgs { +export function enrichFunctionArgs( + args: AllCustomFunctionMiddlewareArgs, webClientOptions: WebClientOptions, +): AllCustomFunctionMiddlewareArgs { const { next: _next, ...functionArgs } = args; - const enrichedArgs: any = { ...functionArgs }; + const enrichedArgs = { ...functionArgs }; const token = selectToken(functionArgs.context); // Making calls with a functionBotAccessToken establishes continuity between // a function_executed event and subsequent interactive events (actions) - const client = new WebClient(token, { ...functionArgs.client }); + const client = new WebClient(token, webClientOptions); enrichedArgs.client = client; // Utility args @@ -217,5 +221,5 @@ export function enrichFunctionArgs(args: any): AllCustomFunctionMiddlewareArgs { enrichedArgs.complete = CustomFunction.createFunctionComplete(enrichedArgs.context, client); enrichedArgs.fail = CustomFunction.createFunctionFail(enrichedArgs.context, client); - return enrichedArgs; + return enrichedArgs as AllCustomFunctionMiddlewareArgs; // TODO: dangerous casting as it obfuscates missing `next()` } diff --git a/src/middleware/builtin.ts b/src/middleware/builtin.ts index 69f908c12..6152c3af4 100644 --- a/src/middleware/builtin.ts +++ b/src/middleware/builtin.ts @@ -299,15 +299,19 @@ export function ignoreSelf(): Middleware { const botUserId = args.context.botUserId !== undefined ? (args.context.botUserId as string) : undefined; if (isEventArgs(args)) { - // Once we've narrowed the type down to SlackEventMiddlewareArgs, there's no way to further narrow it down to - // SlackEventMiddlewareArgs<'message'> without a cast, so the following couple lines do that. - if (args.message !== undefined) { - const message = args.message as SlackEventMiddlewareArgs<'message'>['message']; + if (args.event.type === 'message') { + // Once we've narrowed the type down to SlackEventMiddlewareArgs, there's no way to further narrow it down to + // SlackEventMiddlewareArgs<'message'> without a cast, so the following couple lines do that. + // TODO: there must be a better way; generics-based types for event and middleware arguments likely the issue + // should instead use a discriminated union + const message = args.message as unknown as SlackEventMiddlewareArgs<'message'>['message']; + if (message !== undefined) { // TODO: revisit this once we have all the message subtypes defined to see if we can do this better with // type narrowing // Look for an event that is identified as a bot message from the same bot ID as this app, and return to skip - if (message.subtype === 'bot_message' && message.bot_id === botId) { - return; + if (message.subtype === 'bot_message' && message.bot_id === botId) { + return; + } } } @@ -331,7 +335,7 @@ export function ignoreSelf(): Middleware { */ export function subtype(subtype1: string): Middleware> { return async ({ message, next }) => { - if (message.subtype === subtype1) { + if (message && message.subtype === subtype1) { await next(); } }; @@ -354,7 +358,7 @@ export function directMention(): Middleware> ); } - if (!('text' in message) || message.text === undefined) { + if (!message || !('text' in message) || message.text === undefined) { return; } @@ -406,6 +410,8 @@ function isViewBody( return (body as SlackViewAction).view !== undefined; } -function isEventArgs(args: AnyMiddlewareArgs): args is SlackEventMiddlewareArgs { +function isEventArgs( + args: AnyMiddlewareArgs, +): args is SlackEventMiddlewareArgs { return (args as SlackEventMiddlewareArgs).event !== undefined; } diff --git a/src/middleware/process.ts b/src/middleware/process.ts index b183f46b6..cc963d8f3 100644 --- a/src/middleware/process.ts +++ b/src/middleware/process.ts @@ -21,8 +21,8 @@ export default async function processMiddleware( if (toCallMiddlewareIndex < middleware.length) { lastCalledMiddlewareIndex = toCallMiddlewareIndex; return middleware[toCallMiddlewareIndex]({ - next: () => invokeMiddleware(toCallMiddlewareIndex + 1), ...initialArgs, + next: () => invokeMiddleware(toCallMiddlewareIndex + 1), context, client, logger, diff --git a/src/types/actions/index.ts b/src/types/actions/index.ts index ac7f74d07..5c7e7841a 100644 --- a/src/types/actions/index.ts +++ b/src/types/actions/index.ts @@ -44,7 +44,7 @@ export interface SlackActionMiddlewareArgs ? SayFn : never; + say: Action extends Exclude ? SayFn : undefined; respond: RespondFn; ack: ActionAckFn; complete?: FunctionCompleteFn; diff --git a/src/types/events/index.ts b/src/types/events/index.ts index b888b4a00..0b2c0ef47 100644 --- a/src/types/events/index.ts +++ b/src/types/events/index.ts @@ -29,11 +29,11 @@ export { export interface SlackEventMiddlewareArgs { payload: EventFromType; event: this['payload']; - message: EventType extends 'message' ? this['payload'] : never; + message: EventType extends 'message' ? this['payload'] : undefined; body: EnvelopedEvent; say: WhenEventHasChannelContext; // Add `ack` as undefined for global middleware in TypeScript - ack: undefined; + ack?: undefined; } /** @@ -78,8 +78,8 @@ export type KnownEventFromType = Extract = Event extends { channel: string } | { item: { channel: string } } ? Type - : never; + : undefined; diff --git a/src/types/helpers.ts b/src/types/helpers.ts index 3a7a8bb9d..124f8423b 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -1,9 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +// TODO: breaking change: remove, unnecessary abstraction, just use Record directly /** * Extend this interface to build a type that is treated as an open set of properties, where each key is a string. */ export type StringIndexed = Record; +// TODO: breaking change: no longer used! remove /** * @deprecated No longer works in TypeScript 4.3 */ diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 776c2cfb7..d554c4b36 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -14,6 +14,7 @@ ".eslintrc.js", "docs/**/*", "examples/**/*", + "types-tests/**/*" ], "exclude": [ // Overwrite exclude from the base config to clear the value @@ -21,6 +22,5 @@ // Contains external module type definitions, which are not subject to this project's style rules "types/**/*", // Contain intentional type checking issues for the purpose of testing the typechecker's output - "types-tests/**/*" ] } diff --git a/types-tests/event.test-d.ts b/types-tests/event.test-d.ts index b24a6687a..06a27ab8f 100644 --- a/types-tests/event.test-d.ts +++ b/types-tests/event.test-d.ts @@ -8,7 +8,7 @@ expectType( expectType(event); expectNotType(event); await Promise.resolve(event); - }) + }), ); expectType( @@ -16,7 +16,7 @@ expectType( expectType(event); expectNotType(event); await Promise.resolve(event); - }) + }), ); expectType( @@ -24,7 +24,7 @@ expectType( expectType(event); expectNotType(event); await Promise.resolve(event); - }) + }), ); expectType( @@ -32,7 +32,7 @@ expectType( expectType(event); expectNotType(event); await Promise.resolve(event); - }) + }), ); expectType( @@ -40,7 +40,7 @@ expectType( expectType(event); expectNotType(event); await Promise.resolve(event); - }) + }), ); expectType( @@ -48,7 +48,7 @@ expectType( expectType(event); expectNotType(event); await Promise.resolve(event); - }) + }), ); expectType( @@ -56,7 +56,7 @@ expectType( expectType(say); expectType(event); await Promise.resolve(event); - }) + }), ); expectType( @@ -64,19 +64,19 @@ expectType( expectType(say); expectType(event); await Promise.resolve(event); - }) + }), ); expectType( app.event('reaction_added', async ({ say, event }) => { expectType(say); await Promise.resolve(event); - }) + }), ); expectType( app.event('reaction_removed', async ({ say, event }) => { expectType(say); await Promise.resolve(event); - }) + }), );