Repository: slackapi/deno-slack-sdk Branch: main Commit: a27bd9ec642d Files: 194 Total size: 626.6 KB Directory structure: gitextract_v0ijlb09/ ├── .github/ │ ├── CODEOWNERS │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.md │ │ ├── feature.md │ │ └── question.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── maintainers_guide.md │ └── workflows/ │ ├── deno.yml │ ├── dependencies.yml │ ├── e2e.yml │ ├── npm-publish.yml │ ├── npm.yml │ ├── publish.yml │ └── samples.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── deno.jsonc ├── docs/ │ ├── datastores.md │ ├── events.md │ ├── functions-action-handlers.md │ ├── functions-suggestion-handlers.md │ ├── functions-view-handlers.md │ ├── functions.md │ ├── manifest.md │ ├── types.md │ └── workflows.md ├── scripts/ │ ├── build_npm.ts │ ├── bundle.ts │ └── imports/ │ └── update.ts ├── src/ │ ├── README.md │ ├── datastore/ │ │ ├── datastore_test.ts │ │ ├── mod.ts │ │ └── types.ts │ ├── deps.ts │ ├── dev_deps.ts │ ├── events/ │ │ ├── events_test.ts │ │ ├── mod.ts │ │ └── types.ts │ ├── functions/ │ │ ├── definitions/ │ │ │ ├── connector-function.ts │ │ │ ├── connector-function_test.ts │ │ │ ├── mod.ts │ │ │ ├── slack-function.ts │ │ │ └── slack-function_test.ts │ │ ├── enrich-context.ts │ │ ├── enrich-context_test.ts │ │ ├── interactivity/ │ │ │ ├── block_actions_router.ts │ │ │ ├── block_actions_router_test.ts │ │ │ ├── block_actions_types.ts │ │ │ ├── block_kit_types.ts │ │ │ ├── block_suggestion_router.ts │ │ │ ├── block_suggestion_router_test.ts │ │ │ ├── block_suggestion_types.ts │ │ │ ├── matchers.ts │ │ │ ├── mod.ts │ │ │ ├── types.ts │ │ │ ├── view_router.ts │ │ │ ├── view_router_test.ts │ │ │ └── view_types.ts │ │ ├── mod.ts │ │ ├── slack-function.ts │ │ ├── slack-function_test.ts │ │ ├── tester/ │ │ │ ├── function_tester_test.ts │ │ │ ├── mod.ts │ │ │ └── types.ts │ │ ├── types.ts │ │ ├── types_base_runtime_function_handler_test.ts │ │ ├── types_runtime_slack_function_handler_test.ts │ │ └── unhandled-event-error.ts │ ├── manifest/ │ │ ├── errors.ts │ │ ├── errors_test.ts │ │ ├── manifest_schema.ts │ │ ├── manifest_test.ts │ │ ├── mod.ts │ │ ├── types.ts │ │ └── types_util.ts │ ├── mod.ts │ ├── mod_test.ts │ ├── parameters/ │ │ ├── define_property.ts │ │ ├── define_property_test.ts │ │ ├── definition_types.ts │ │ ├── mod.ts │ │ ├── param.ts │ │ ├── param_test.ts │ │ ├── parameter-variable_test.ts │ │ ├── types.ts │ │ ├── with-untyped-object-proxy.ts │ │ └── with-untyped-object-proxy_test.ts │ ├── providers/ │ │ └── oauth2/ │ │ ├── mod.ts │ │ ├── oauth2_test.ts │ │ └── types.ts │ ├── schema/ │ │ ├── mod.ts │ │ ├── providers/ │ │ │ ├── mod.ts │ │ │ └── oauth2/ │ │ │ ├── mod.ts │ │ │ └── types.ts │ │ ├── schema_types.ts │ │ ├── slack/ │ │ │ ├── functions/ │ │ │ │ ├── _scripts/ │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── README.md │ │ │ │ │ ├── generate │ │ │ │ │ └── src/ │ │ │ │ │ ├── templates/ │ │ │ │ │ │ ├── mod.ts │ │ │ │ │ │ ├── template_function.ts │ │ │ │ │ │ ├── template_mod.ts │ │ │ │ │ │ ├── test_template.ts │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ ├── utils.ts │ │ │ │ │ │ └── utils_test.ts │ │ │ │ │ ├── test/ │ │ │ │ │ │ └── data/ │ │ │ │ │ │ └── function.json │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ ├── utils_test.ts │ │ │ │ │ └── write_function_files.ts │ │ │ │ ├── add_bookmark.ts │ │ │ │ ├── add_bookmark_test.ts │ │ │ │ ├── add_pin.ts │ │ │ │ ├── add_pin_test.ts │ │ │ │ ├── add_reaction.ts │ │ │ │ ├── add_reaction_test.ts │ │ │ │ ├── add_user_to_usergroup.ts │ │ │ │ ├── add_user_to_usergroup_test.ts │ │ │ │ ├── archive_channel.ts │ │ │ │ ├── archive_channel_test.ts │ │ │ │ ├── canvas_copy.ts │ │ │ │ ├── canvas_copy_test.ts │ │ │ │ ├── canvas_create.ts │ │ │ │ ├── canvas_create_test.ts │ │ │ │ ├── canvas_update_content.ts │ │ │ │ ├── canvas_update_content_test.ts │ │ │ │ ├── channel_canvas_create.ts │ │ │ │ ├── channel_canvas_create_test.ts │ │ │ │ ├── create_channel.ts │ │ │ │ ├── create_channel_test.ts │ │ │ │ ├── create_usergroup.ts │ │ │ │ ├── create_usergroup_test.ts │ │ │ │ ├── delay.ts │ │ │ │ ├── delay_test.ts │ │ │ │ ├── invite_user_to_channel.ts │ │ │ │ ├── invite_user_to_channel_test.ts │ │ │ │ ├── mod.ts │ │ │ │ ├── open_form.ts │ │ │ │ ├── open_form_test.ts │ │ │ │ ├── remove_reaction.ts │ │ │ │ ├── remove_reaction_test.ts │ │ │ │ ├── remove_user_from_usergroup.ts │ │ │ │ ├── remove_user_from_usergroup_test.ts │ │ │ │ ├── reply_in_thread.ts │ │ │ │ ├── reply_in_thread_test.ts │ │ │ │ ├── send_dm.ts │ │ │ │ ├── send_dm_test.ts │ │ │ │ ├── send_ephemeral_message.ts │ │ │ │ ├── send_ephemeral_message_test.ts │ │ │ │ ├── send_message.ts │ │ │ │ ├── send_message_test.ts │ │ │ │ ├── share_canvas.ts │ │ │ │ ├── share_canvas_in_thread.ts │ │ │ │ ├── share_canvas_in_thread_test.ts │ │ │ │ ├── share_canvas_test.ts │ │ │ │ ├── update_channel_topic.ts │ │ │ │ └── update_channel_topic_test.ts │ │ │ ├── mod.ts │ │ │ ├── schema_types.ts │ │ │ └── types/ │ │ │ ├── custom/ │ │ │ │ ├── custom_slack_types_test.ts │ │ │ │ ├── form_input.ts │ │ │ │ ├── interactivity.ts │ │ │ │ ├── message_context.ts │ │ │ │ ├── mod.ts │ │ │ │ └── user_context.ts │ │ │ └── mod.ts │ │ └── types.ts │ ├── test_utils.ts │ ├── test_utils_test.ts │ ├── type_utils.ts │ ├── types/ │ │ ├── mod.ts │ │ ├── types.ts │ │ └── types_test.ts │ ├── types.ts │ └── workflows/ │ ├── mod.ts │ ├── types.ts │ └── workflow-step.ts └── tests/ └── integration/ ├── functions/ │ └── runtime_context/ │ ├── array_parameters_test.ts │ ├── custom_type_parameters_test.ts │ ├── empty_undefined_parameters_test.ts │ ├── incomplete_error_status_test.ts │ ├── input_parameter_optionality_test.ts │ ├── input_parameters_test.ts │ ├── output_parameter_optionality_test.ts │ ├── output_parameters_test.ts │ ├── typed_object_property_test.ts │ └── untyped_object_property_test.ts ├── parameters/ │ ├── parameter_variable_test.ts │ └── parameter_variable_unwrapped_test.ts ├── schema/ │ └── slack/ │ └── functions/ │ └── _scripts/ │ └── write_function_files_test.ts └── workflows/ └── workflows_test.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ # Salesforce Open Source project configuration # Learn more: https://github.com/salesforce/oss-template #ECCN:Open Source #GUSINFO:Open Source,Open Source Workflow # @slackapi/denosaurs # are code reviewers for all changes in this repo. * @slackapi/denosaurs # @slackapi/developer-education # are code reviewers for changes in the `/docs` directory. /docs/ @slackapi/developer-education ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributors Guide Interested in contributing? Awesome! Before you do though, please read our [Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very seriously, and expect that you will as well. There are many ways you can contribute! :heart: ## :bug: Bug Reports and Fixes - If you find a bug, please search for it in the [Issues](https://github.com/slackapi/deno-slack-sdk/issues), and if it isn't already tracked, [create a new Bug Report Issue](https://github.com/slackapi/deno-slack-sdk/issues/new/choose). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still be reviewed. - Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`. - If you'd like to submit a fix for a bug, [send a Pull Request](#creating-a-pull-request) and mention the Issue number. - Include tests that isolate the bug and verifies that it was fixed. ## :bulb: New Features - If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Feature Request Issue](https://github.com/slackapi/deno-slack-sdk/issues/new/choose). - Issues that have been identified as a feature request will be labelled `enhancement`. - If you'd like to implement the new feature, please wait for feedback from the project maintainers before spending too much time writing the code. In some cases, `enhancement`s may not align well with the project objectives at the time. ## :mag: Tests, :books: Documentation,:sparkles: Miscellaneous - If you'd like to improve the tests, you want to make the documentation clearer, you have an alternative implementation of something that may have advantages over the way its currently done, or you have any other change, we would be happy to hear about it! - If its a trivial change, go ahead and [send a Pull Request](#creating-a-pull-request) with the changes you have in mind. - If not, [open an Issue](https://github.com/slackapi/deno-slack-sdk/issues/new) to discuss the idea first. If you're new to our project and looking for some way to make your first contribution, look for Issues labelled `good first contribution`. ## Requirements For your contribution to be accepted: - [x] You must have signed the [Contributor License Agreement (CLA)](https://cla.salesforce.com/sign-cla). - [x] The test suite must be complete and pass. - [x] The changes must be approved by code review. - [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number. If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created. [Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z) ## Creating a Pull Request 1. :fork_and_knife: Fork the repository on GitHub. 2. :runner: Clone/fetch your fork to your local development machine. It's a good idea to run the tests just to make sure everything is in order. 3. :herb: Create a new branch and check it out. 4. :crystal_ball: Make your changes and commit them locally. Magic happens here! 5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`). 6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `main` in this repository. ## Maintainers There are more details about processes and workflow in the [Maintainer's Guide](./maintainers_guide.md). ================================================ FILE: .github/ISSUE_TEMPLATE/bug.md ================================================ --- name: Bug Report about: Report a bug encountered while using this project title: '[BUG] ' --- <!-- If you find a bug, please search for it in the [Issues](https://github.com/slackapi/deno-slack-sdk/issues), and if it isn't already tracked then create a new issue --> **The `deno-slack` versions** <!-- Paste the output of `cat import_map.json | grep deno-slack` --> **Deno runtime version** <!-- Paste the output of `deno --version` --> **OS info** <!-- Paste the output of `sw_vers && uname -v` on macOS/Linux or `ver` on Windows OS --> **Describe the bug** <!-- A clear and concise description of what the bug is. --> **Steps to reproduce** <!-- Share the commands to run, source code, and project settings --> 1. 2. 3. **Expected result** <!-- Tell what you expected to happen --> **Actual result** <!-- Tell what actually happened with logs, screenshots --> **Requirements** Please read the [Contributing guidelines](https://github.com/slackapi/deno-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. ================================================ FILE: .github/ISSUE_TEMPLATE/feature.md ================================================ --- name: Feature request about: Suggest a new feature for this project title: '[FEATURE] <title>' --- <!-- If you have a feature request, please search for it in the [Issues](https://github.com/slackapi/deno-slack-sdk/issues), and if it isn't already tracked then create a new issue --> **Description of the problem being solved** <!-- Please describe the problem you want to solve --> **Alternative solutions** <!-- Please describe the solutions you've considered --> **Requirements** Please read the [Contributing guidelines](https://github.com/slackapi/deno-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: Ask a question about this project title: '[QUERY] <title>' label: question --- <!-- If you have a question, please search for it in the [Issues](https://github.com/slackapi/deno-slack-sdk/issues), and if it isn't already tracked then create a new issue --> **Question** <!-- A clear and concise question with steps to reproduce --> **Context** <!-- Any additional context to your question --> **Environment** <!-- Paste the output of `cat import_map.json | grep deno-slack` --> <!-- Paste the output of `deno --version` --> <!-- Paste the output of `sw_vers && uname -v` on macOS/Linux or `ver` on Windows OS --> **Requirements** Please read the [Contributing guidelines](https://github.com/slackapi/deno-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ <!-- Thanks for sending a pull request! Friendly reminder: this project is open sourced, the internet can see it, make sure all the info/links shared in this pull request are public information. --> ### Summary <!-- A high level description of the change that will make it easier for your reviewer to make sense of the changes --> ### Testing <!-- Describe what steps a reviewer should follow to test your changes. --> ### Special notes <!-- Any special notes reviewers should be aware of. --> ### Requirements <!-- place an `x` in each `[ ]` --> * [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/deno-slack-sdk/blob/main/.github/CONTRIBUTING.md) and have done my best effort to follow them. * [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). * [ ] I've ran `deno task test` after making the changes. ================================================ FILE: .github/dependabot.yml ================================================ # Please see the documentation for all configuration options: # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" ================================================ FILE: .github/maintainers_guide.md ================================================ # Maintainers Guide This document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain this project. If you use this package within your own software as is but don't plan on modifying it, this guide is **not** for you. ## Tools All you need to work on this project is a recent version of [Deno](https://deno.land/) <details> <summary>Note</summary> - You can set up shell completion by following the [Shell Completion](https://deno.land/manual/getting_started/setup_your_environment#shell-completions) guidelines. </details> ## Tasks ### Testing with Deno In-code tests can be run directly with Deno: ```zsh deno task test ``` You can also run a test coverage report with: ```zsh deno task coverage ``` ### Testing with a sample app Sometimes you may need to test out changes in this SDK with a sample app or project. A modified SDK version can be used by updating the `deno-slack-sdk` import url in the app's `import_map.json` file. > After making changes to your imports, you may need to > [reload your modules](https://deno.land/manual@v1.29.1/basics/modules/reloading_modules) > in case they've been cached. #### Using local changes To use your own code as the SDK, change the import url to the `src/` directory of your local `deno-slack-sdk` repo: ```json { "imports": { "deno-slack-sdk/": "../../tools/deno-slack-sdk/src/", "deno-slack-api/": "jsr:@slack/api@2.9.0/" } } ``` #### With remote changes To test with unreleased changes on a remote repo, commit your intended history to a remote branch and note the full commit SHA. (e.g. `fc0a0a1f0722e28fecb7782513d045522d7c0d6f`). Then in your sample app's `import_map.json` file, replace the `deno-slack-sdk` import url with: ```json { "imports": { "deno-slack-sdk/": "https://raw.githubusercontent.com/slackapi/deno-slack-sdk/<commit-SHA-goes-here>/src/", "deno-slack-api/": "jsr:@slack/api@2.9.0/" } } ``` ### Lint and format The linting and formatting rules are defined in the `deno.jsonc` file, your IDE can be set up to follow these rules: 1. Refer to the [Deno Set Up Your Environment](https://deno.land/manual/getting_started/setup_your_environment) guidelines to set up your IDE with the proper plugin. 2. Ensure that the `deno.jsonc` file is set as the configuration file for your IDE plugin - If you are using VS code [this](https://deno.land/manual/references/vscode_deno#using-a-configuration-file) is already configured in `.vscode/settings.json` #### Linting The list of linting rules can be found in [the linting deno docs](https://lint.deno.land/). Currently we apply all recommended rules. #### Format The list of format options is defined in the `deno.jsonc` file. They closely resemble the default values. ### Releasing Releases for this library are automatically generated off of git tags. Before creating a new release, ensure that everything on the `main` branch since the last tag is in a releasable state! At a minimum, [run the tests](#testing-with-deno) and validate that the package meets JSR publishing requirements by doing a dry run of the publish command: ```zsh deno task publish:dry-run ``` To create a new release: 1. Determine the new release version. You can start off by incrementing the version to reflect a patch (i.e. 1.0.0 -> 1.0.1). - Review the pull request labels of the changes since the last release (i.e. `semver:minor`, `semver:patch`, `semver:major`). Tip: Your release version should be based on the tag of the largest change, so if the changes include a `semver:minor`, the release version should be upgraded to reflect a minor. - Ensure that this version adheres to [semantic versioning][semver]. See [Versioning](#versioning-and-tags) for correct version format. Version tags should match the following pattern: `1.0.1` (no `v` preceding the number). 2. Create a new branch from `main` named after the release version (e.g. `1.0.1`). 3. Bump the `version` field in `deno.jsonc` to the new release version. 4. Open a pull request from the version branch into `main` and get it approved/merged. 1. `git commit -m 'chore(release): version 1.0.1'` 2. `git push -u origin 1.0.1` 5. Create a new GitHub Release from the [Releases page](https://github.com/slackapi/deno-slack-sdk/releases) by clicking the "Draft a new release" button. 6. Input a new version manually into the "Choose a tag" input. - After you input the new version, click the "Create a new tag: x.x.x on publish" button. This won't create your tag immediately. - Auto-generate the release notes by clicking the "Auto-generate release notes" button. This will pull in changes that will be included in your release. - Edit the resulting notes to ensure they have decent messaging that are understandable by non-contributors, but each commit should still have it's own line. 7. Set the "Target" input to the "main" branch. 8. Name the release title after the version tag. 9. Make any adjustments to generated release notes to make sure they are accessible and approachable and that an end-user with little context about this project could still understand. 10. Publish the release by clicking the "Publish release" button! 11. After a few minutes, the corresponding version will be available on <https://deno.land/x/deno_slack_sdk> and <https://jsr.io/@slack/sdk>. ## Workflow ### Versioning and Tags This project is versioned using [Semantic Versioning][semver]. ### Branches > Describe any specific branching workflow. For example: `main` is where active > development occurs. Long running branches named feature branches are > occasionally created for collaboration on a feature that has a large scope > (because everyone cannot push commits to another person's open Pull Request) ### Issue Management Labels are used to run issues through an organized workflow. Here are the basic definitions: - `bug`: A confirmed bug report. A bug is considered confirmed when reproduction steps have been documented and the issue has been reproduced. - `enhancement`: A feature request for something this package might not already do. - `docs`: An issue that is purely about documentation work. - `tests`: An issue that is purely about testing work. - `needs feedback`: An issue that may have claimed to be a bug but was not reproducible, or was otherwise missing some information. - `discussion`: An issue that is purely meant to hold a discussion. Typically the maintainers are looking for feedback in this issues. - `question`: An issue that is like a support request because the user's usage was not correct. - `semver:major|minor|patch`: Metadata about how resolving this issue would affect the version number. - `security`: An issue that has special consideration for security reasons. - `good first contribution`: An issue that has a well-defined relatively-small scope, with clear expectations. It helps when the testing approach is also known. - `duplicate`: An issue that is functionally the same as another issue. Apply this only if you've linked the other issue by number. **Triage** is the process of taking new issues that aren't yet "seen" and marking them with a basic level of information with labels. An issue should have **one** of the following labels applied: `bug`, `enhancement`, `question`, `needs feedback`, `docs`, `tests`, or `discussion`. Issues are closed when a resolution has been reached. If for any reason a closed issue seems relevant once again, reopening is great and better than creating a duplicate issue. ## Dependency Graph <!-- https://mermaid.js.org/syntax/flowchart.html --> <!-- Link in mermaid are not supported on github https://github.com/mermaid-js/mermaid/issues/3077 --> ```mermaid flowchart TD samples --> deno-slack-sdk samples --> deno-slack-api samples -- start hook --> deno-slack-runtime samples --> deno-slack-hooks deno-slack-hooks -. start hook .-> deno-slack-runtime deno-slack-sdk --> deno-slack-api deno-slack-hooks --> deno-slack-protocols deno-slack-runtime --> deno-slack-protocols ``` | Links | | :----------------------------------------------------------------------: | | [samples](https://github.com/slack-samples/deno-hello-world) | | [deno-slack-sdk](https://github.com/slackapi/deno-slack-sdk) | | [deno-slack-api](https://github.com/slackapi/deno-slack-api) | | [deno-slack-runtime](https://github.com/slackapi/deno-slack-runtime) | | [deno-slack-hooks](https://github.com/slackapi/deno-slack-hooks) | | [deno-slack-protocols](https://github.com/slackapi/deno-slack-protocols) | ## Everything else When in doubt, find the other maintainers and ask. [semver]: http://semver.org/ ================================================ FILE: .github/workflows/deno.yml ================================================ name: Deno on: push: branches: - main pull_request: branches: - main jobs: deno: runs-on: ubuntu-latest strategy: fail-fast: false matrix: deno-version: - v1.x - v2.x permissions: contents: read steps: - name: Setup repo uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup Deno ${{ matrix.deno-version }} uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 with: deno-version: ${{ matrix.deno-version }} - name: Run tests run: deno task test - name: Generate CodeCov-friendly coverage report run: deno task coverage - name: Upload coverage to CodeCov uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 if: matrix.deno-version == 'v2.x' with: files: ./lcov.info token: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/dependencies.yml ================================================ name: Merge updates to dependencies on: pull_request: jobs: dependabot: name: "@dependabot" if: github.event.pull_request.user.login == 'dependabot[bot]' runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Collect metadata id: metadata uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Approve if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' run: gh pr review --approve "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Automerge if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/e2e.yml ================================================ # This workflow invokes and waits for the result of Slack's private E2E CI system name: Internal E2E CI on: push: branches: - main pull_request: jobs: e2e: runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout the sdk uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set environment variables run: | # Short name for current branch. For PRs, use source branch (GITHUB_HEAD_REF) GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} echo "Identified deno-slack-sdk branch name: ${GIT_BRANCH}; will invoke CI system to test this branch."; echo "GIT_BRANCH=$GIT_BRANCH" >> $GITHUB_ENV - name: Kick off platform-devpx-test pipeline env: CCI_PAT: ${{ secrets.CCHEN_CIRCLECI_PERSONAL_TOKEN }} run: | IMPORT_URL="https://raw.githubusercontent.com/slackapi/deno-slack-sdk/refs/heads/${GIT_BRANCH}/src/"; echo "Import URL: ${IMPORT_URL}"; # https://app.circleci.com/settings/organization/github/slackapi/contexts TEST_PAYLOAD=$(curl --location --request POST 'https://circleci.com/api/v2/project/gh/slackapi/platform-devxp-test/pipeline' \ --header 'Content-Type: application/json' \ -u "${CCI_PAT}:" \ --data "{\"branch\":\"main\",\"parameters\":{\"deno_sdk_import_url\":\"${IMPORT_URL}\"}}") echo $TEST_PAYLOAD; TEST_JOB_WORKFLOW_ID=$(echo $TEST_PAYLOAD | jq --raw-output '.id'); if [ $TEST_JOB_WORKFLOW_ID = "null" ]; then echo "Problem extracting job ID from invocation API call! Aborting!"; exit 1; fi echo "e2e test workflow started with id: $TEST_JOB_WORKFLOW_ID" echo "TEST_JOB_WORKFLOW_ID=${TEST_JOB_WORKFLOW_ID}" >> $GITHUB_ENV - name: Wait for platform-devxp-test E2E run to complete env: CCI_PAT: ${{ secrets.CCHEN_CIRCLECI_PERSONAL_TOKEN }} run: | E2E_RESULT="{}" E2E_STATUS="running" # possible status values: success, running, not_run, failed, error, failing, on_hold, canceled, unauthorized while [[ $E2E_STATUS != "failed" && $E2E_STATUS != "canceled" && $E2E_STATUS != "success" && $E2E_STATUS != "not_run" && $E2E_STATUS != "error" && $E2E_STATUS != "unauthorized" ]] do sleep 30s echo "Polling test job ${TEST_JOB_WORKFLOW_ID}..." E2E_RESULT=$(curl --location -sS --request GET "https://circleci.com/api/v2/pipeline/${TEST_JOB_WORKFLOW_ID}/workflow" --header "Circle-Token: ${CCI_PAT}") echo $E2E_RESULT; E2E_STATUS=$(echo $E2E_RESULT | jq --raw-output '.items[0].status') if [ $E2E_STATUS = "null" ]; then echo "Problem extracting status from workflow API! Aborting!"; exit 1 fi echo "Status is now: $E2E_STATUS" done if [ $E2E_STATUS = "failed" ] || [ $E2E_STATUS = "error" ]; then E2E_PIPE_NUM=$(echo $E2E_RESULT | jq '.items[0].pipeline_number') E2E_WORKFLOW_ID=$(echo $E2E_RESULT | jq -r '.items[0].id') CIRCLE_FAIL_LINK="https://app.circleci.com/pipelines/github/slackapi/platform-devxp-test/${E2E_PIPE_NUM}/workflows/${E2E_WORKFLOW_ID}" echo "Tests failed! Visit $CIRCLE_FAIL_LINK for more info." exit 1 elif [ "$E2E_STATUS" = "canceled" ] || [ "$E2E_STATUS" = "unauthorized" ] || [ $E2E_STATUS = "not_run" ]; then echo "Tests have been ${E2E_STATUS} and did not finish!" exit 1 else echo "Tests passed woot 🎉" fi ================================================ FILE: .github/workflows/npm-publish.yml ================================================ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages name: Build & Deploy to NPM on: push: tags: - "*.*.*" jobs: build: runs-on: macos-latest permissions: contents: read steps: - name: Actions checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: latest registry-url: https://registry.npmjs.org/ - name: Setup Deno uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 with: deno-version: v2.x - name: Get the tag name id: get_tag_name run: echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - name: Run build_npm.ts run: deno run -A scripts/build_npm.ts "$TAG" env: TAG: ${{steps.get_tag_name.outputs.TAG}} - name: Publish to NPM run: cd npm && npm publish --access=public env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} ================================================ FILE: .github/workflows/npm.yml ================================================ # This workflow runs a test build for npm against changes on main or PRs name: Npm Build on: push: branches: - main pull_request: branches: - main jobs: build: runs-on: macos-latest strategy: fail-fast: false matrix: deno-version: - v1.x - v2.x permissions: contents: read steps: - name: Actions checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: latest registry-url: https://registry.npmjs.org/ - name: Setup Deno ${{ matrix.deno-version }} uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 with: deno-version: ${{ matrix.deno-version }} - name: Run build_npm.ts run: deno run -A scripts/build_npm.ts ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: push: tags: - '[0-9]+.[0-9]+.[0-9]+' jobs: publish: runs-on: ubuntu-latest permissions: contents: read id-token: write # The OIDC ID token is used for authentication with JSR. steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - run: npx jsr publish --allow-slow-types ================================================ FILE: .github/workflows/samples.yml ================================================ # This workflow runs a `deno check` against slack sample apps name: Samples Integration Type-checking on: push: branches: - main pull_request: branches: - main jobs: samples: runs-on: ubuntu-latest strategy: fail-fast: false matrix: sample: - slack-samples/deno-issue-submission - slack-samples/deno-starter-template - slack-samples/deno-blank-template - slack-samples/deno-message-translator - slack-samples/deno-request-time-off - slack-samples/deno-simple-survey deno-version: - v1.x - v2.x permissions: contents: read steps: - name: Setup Deno ${{ matrix.deno-version }} uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 with: deno-version: ${{ matrix.deno-version }} - name: Checkout the sdk uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: path: ./deno-slack-sdk persist-credentials: false - name: Checkout the ${{ matrix.sample }} sample uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: ${{ matrix.sample }} path: ./sample persist-credentials: false - name: Set imports.deno-slack-sdk/ to ../deno-slack-sdk/src/ in imports run: > deno run --allow-read --allow-write deno-slack-sdk/scripts/imports/update.ts --import-file "./sample/deno.jsonc" --sdk "./deno-slack-sdk/" - name: Deno check **/*.ts working-directory: ./sample run: find . -type f -regex ".*\.ts" | xargs deno check -r ================================================ FILE: .gitignore ================================================ .DS_Store .coverage .vim lcov.info npm/ ================================================ FILE: .vscode/settings.json ================================================ { "deno.enable": true, "deno.lint": true, "deno.config": "./deno.jsonc", "[typescript]": { "editor.formatOnSave": true, "editor.defaultFormatter": "denoland.vscode-deno" } } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Slack Technologies, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ <h1 align="center"> Deno Slack SDK <br> </h1> <p align="center"> <img alt="deno.land version" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fdeno-visualizer.danopia.net%2Fshields%2Flatest-version%2Fx%2Fdeno_slack_sdk%2Fmod.ts"> <img alt="deno dependencies" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fdeno-visualizer.danopia.net%2Fshields%2Fupdates%2Fx%2Fdeno_slack_sdk%2Fmod.ts"> <img alt="Samples Integration Type-checking" src="https://github.com/slackapi/deno-slack-sdk/workflows/Samples%20Integration%20Type-checking/badge.svg"> </a> </p> A Deno SDK to build Slack apps with the latest platform features. Read the [quickstart guide](https://api.slack.com/automation/quickstart) and look at our [code samples](https://api.slack.com/automation/samples) to learn how to build apps. ## Versioning Releases for this repository follow the [SemVer](https://semver.org/) versioning scheme. The SDK's contract is determined by the top-level exports from `src/mod.ts` and `src/types.ts`. Exports not included in these files are deemed internal and any modifications will not be treated as breaking changes. As such, internal exports should be treated as unstable and used at your own risk. ## Setup Make sure you have a development workspace where you have permission to install apps. **Please note that the features in this project require that the workspace be part of [a Slack paid plan](https://slack.com/pricing).** ### Install the Slack CLI You need to install and configure the Slack CLI. Step-by-step instructions can be found on our [install & authorize page](https://api.slack.com/automation/cli/install). ## Creating an app Create a blank project by executing the following command: ```zsh slack create my-app --template slack-samples/deno-blank-template cd my-app/ ``` The `manifest.ts` file contains the app's configuration. This file defines attributes like app name, description and functions. ### Create a [function](https://api.slack.com/automation/functions/custom) ```zsh mkdir ./functions && touch ./functions/hello_world.ts ``` ```ts // Contents of ./functions/hello_world.ts import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; export const HelloWorldFunctionDef = DefineFunction({ callback_id: "hello_world_function", title: "Hello World", source_file: "functions/hello_world.ts", input_parameters: { properties: {}, required: [], }, output_parameters: { properties: { message: { type: Schema.types.string, description: "Hello world message", }, }, required: ["message"], }, }); export default SlackFunction( HelloWorldFunctionDef, () => { return { outputs: { message: "Hello World!" }, }; }, ); ``` `DefineFunction` is used to define a custom function and provide Slack with the information required to use it. `SlackFunction` uses the definition returned by `DefineFunction` and your custom executable code to export a Slack-usable custom function. ### Create a [workflow](https://api.slack.com/automation/workflows) ```zsh mkdir ./workflows && touch ./workflows/hello_world.ts ``` ```ts // Contents of ./workflows/hello_world.ts import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; import { HelloWorldFunctionDef } from "../functions/hello_world.ts"; const HelloWorldWorkflowDef = DefineWorkflow({ callback_id: "hello_world_workflow", title: "Hello World Workflow", input_parameters: { properties: { channel: { type: Schema.slack.types.channel_id, }, }, required: ["channel"], }, }); const helloWorldStep = HelloWorldWorkflowDef.addStep(HelloWorldFunctionDef, {}); HelloWorldWorkflowDef.addStep(Schema.slack.functions.SendMessage, { channel_id: HelloWorldWorkflowDef.inputs.channel, message: helloWorldStep.outputs.message, }); export default HelloWorldWorkflowDef; ``` `DefineWorkflow` is used to define a workflow and provide Slack with the information required to use it. `HelloWorldWorkflow.addStep` is used to add a step to the workflow; here we add the `HelloWorldFunction` and then the `SendMessage` Slack Function that will post the `message` to a Slack channel. ### Update the [manifest](https://api.slack.com/automation/manifest) ```ts // Contents of manifest.ts import { Manifest } from "deno-slack-sdk/mod.ts"; import HelloWorldWorkflow from "./workflows/hello_world.ts"; export default Manifest({ name: "my-app", description: "A Hello World app", icon: "assets/default_new_app_icon.png", workflows: [HelloWorldWorkflow], outgoingDomains: [], botScopes: ["chat:write", "chat:write.public"], }); ``` `Manifest` is used to define your apps [manifest](https://api.slack.com/automation/manifest) and provides Slack with the information required to manage it. ### Create a [trigger](https://api.slack.com/automation/triggers) ```zsh mkdir ./triggers && touch ./triggers/hello_world.ts ``` ```ts // Contents of ./triggers/hello_world.ts import { Trigger } from "deno-slack-sdk/types.ts"; import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts"; import HelloWorldWorkflow from "../workflows/hello_world.ts"; const trigger: Trigger<typeof HelloWorldWorkflow.definition> = { type: TriggerTypes.Shortcut, name: "Reverse a string", description: "Starts the workflow to reverse a string", workflow: `#/workflows/${HelloWorldWorkflow.definition.callback_id}`, inputs: { channel: { value: TriggerContextData.Shortcut.channel_id, }, }, }; export default trigger; ``` The `Trigger` object is used to define a trigger that will invoke the `HelloWorldWorkflow`. The Slack CLI will detect this file and prompt you for its creation. ## Running an app ```zsh slack run ``` When prompted, create the `triggers/hello_world.ts` trigger. This will send your trigger configuration to Slack. Post the `Hello world shortcut trigger` in a slack message and **use it** ## Getting Help [This documentation](https://api.slack.com/automation) has more information on basic and advanced concepts of the SDK. Information on how to get started developing with Deno can be found in [this documentation](https://api.slack.com/automation/deno/develop). If you get stuck, we're here to help. The following are the best ways to get assistance working through your issue: - [Issue Tracker](https://github.com/slackapi/deno-slack-sdk/issues?q=is%3Aissue) for questions, bug reports, feature requests, and general discussion. **Try searching for an existing issue before creating a new one.** - Email our developer support team: `support@slack.com` ## Contributing Contributions are more than welcome. Please look at the [contributing guidelines](https://github.com/slackapi/deno-slack-sdk/blob/main/.github/CONTRIBUTING.md) for more info! ================================================ FILE: deno.jsonc ================================================ { "$schema": "https://deno.land/x/deno/cli/schemas/config-file.v1.json", "name": "@slack/sdk", "version": "2.15.2", "exports": { ".": "./src/mod.ts", "./mod.ts": "./src/mod.ts", "./types.ts": "./src/types.ts" }, "publish": { "exclude": ["**/*_test.ts", "**/fixtures"], "include": [ "README.md", "LICENSE", "deno.jsonc", "src/**" ] }, "fmt": { "include": ["src", "tests", "docs", "README.md", "scripts", ".github/maintainers_guide.md", ".github/CONTRIBUTING.md"], "exclude": ["src/schema/slack/functions/_scripts/functions.json"], "semiColons": true, "indentWidth": 2, "lineWidth": 80, "proseWrap": "always", "singleQuote": false, "useTabs": false }, "lint": { "include": ["src", "tests", "scripts"], "exclude": ["src/schema/slack/functions/_scripts/functions.json", "**/*.md"], "rules": { "exclude": ["no-import-prefix", "no-slow-types"] } }, "tasks": { "test": "deno fmt --check && deno lint && deno run --allow-run --allow-env --allow-read scripts/bundle.ts && deno test --allow-read --allow-run --parallel src/ tests/", "coverage": "rm -rf .coverage && deno test --reporter=dot --parallel --allow-read --coverage=.coverage src/ && deno coverage --exclude=fixtures --exclude=_test --lcov --output=lcov.info .coverage", "publish:dry-run": "deno publish --allow-slow-types --dry-run" }, "lock": false } ================================================ FILE: docs/datastores.md ================================================ ## Datastores ### Defining a Datastore Datastores can be defined with the top level `DefineDatastore` export. Below is an example of setting up a Datastore: ```ts import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts"; export const ReversalsDatastore = DefineDatastore({ name: "reversals", attributes: { id: { type: Schema.types.string, }, original: { type: Schema.types.string, }, reversed: { type: Schema.types.string, }, }, primary_key: "id", }); ``` ### Registering a Datastore to the App To register the newly defined Datastore, add it to the array assigned to the `datastores` parameter while defining the [`Manifest`][manifest]. ```ts export default Manifest({ name: "admiring-ox-50", description: "Reverse a string", icon: "assets/icon.png", functions: [ReverseFunction], outgoingDomains: [], datastores: [ReversalsDatastore], botScopes: [ "commands", "chat:write", "chat:write.public", "datastore:read", "datastore:write", ], }); ``` Note: Registering a Datastore will automatically add `datastore:read` and `datastore:write` to the App's defined `botScopes`. ### Using a Datastore in your custom function code Now that you have a Datastore all set up, you can use it in your [`functions`][functions]! Import the [deno-slack-api](https://github.com/slackapi/deno-slack-api) library, instantiate your client, and make an API call to one of the Datastore endpoints! ```ts import { SlackFunction } from "deno_slack_api/mod.ts"; export default SlackFunction(ReverseFunction, async ({ client, inputs }) => { const original = inputs.stringToReverse; const recordId = crypto.randomUUID(); const reversed = inputs.stringToReverse.split("").reverse().join(""); const putResp = await client.apps.datastore.put({ datastore: "reversals", item: { id: recordId, original, reversed, }, }); if (!putResp.ok) { return { error: putResp.error, }; } // ... }); ``` [functions]: ./functions.md [manifest]: ./manifest.md ================================================ FILE: docs/events.md ================================================ ## Events Custom events provide a way for Apps to validate [message metadata](https://api.slack.com/metadata) against a pre-defined schema. ### Defining an event Events can be defined with the top level `DefineEvent` export. Events must be set up as an `object` type or a [`custom Type`][types] of an `object` type. Below is an example of setting up a custom Event that can be used during an incident. ```ts const IncidentEvent = DefineEvent({ name: "incident", title: "Incident", type: Schema.types.object, properties: { id: { type: Schema.types.string }, title: { type: Schema.types.string }, summary: { type: Schema.types.string }, severity: { type: Schema.types.string }, date_created: { type: Schema.types.number }, }, required: ["id", "title", "summary", "severity"], additionalProperties: false, // Setting this to false forces the validation to catch any additional properties }); ``` ### Registering an event with the app To register the newly defined event, add it to the array assigned to the `events` parameter while defining the [`Manifest`][manifest]. Note: All custom events **must** be registered to the [Manifest][manifest] in order for them to be used. There is no automated registration for events. ```ts Manifest({ ... events: [IncidentEvent], }); ``` ### Referencing events There are two places where you can reference your events: 1. Posting a message to Slack 2. Creating a message metadata trigger #### Posting a message to Slack Event validation happens against the App's manifest when an App posts a message to Slack using the [`metadata` parameter](https://api.slack.com/methods/chat.postMessage#arg_metadata). If the `event_type` matches the `name` of a custom Event specified in the App's manifest, it will validate that all required parameters are provided. If it doesn't meet the validation standards, a warning will be returned in the response and the message will still be posted, but the metadata will be dropped from the message. ```ts // At workflow authoring time // This example assumes all required values are passed to the workflow's inputs MyWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: MyWorkflow.inputs.channel_id, message: "We have an incident!", metadata: { event_type: IncidentEvent, event_payload: { id: MyWorkflow.inputs.incident_id, title: MyWorkflow.inputs.incident_title, summary: MyWorkflow.inputs.incident_summary, severity: MyWorkflow.inputs.incident_severity, date_created: MyWorkflow.inputs.incident_date, // Since this isn't required, it doesn't need to exist to pass validation }, }, }); ``` ```ts // At function runtime // This example assumes all required values are passed to the function's inputs await client.chat.postMessage({ channel_id: inputs.channel_id, message: "We have an incident!", metadata: { event_type: IncidentEvent, event_payload: { id: inputs.incident_id, title: inputs.incident_title, summary: inputs.incident_summary, severity: inputs.incident_severity, date_created: inputs.incident_date, // Since this isn't required, it doesn't need to exist to pass validation }, }, }); ``` #### Creating a message metadata trigger Now that the app has a defined schema for the event, a trigger can be created to watch for any message posted with the expected metadata. When the schema is met, the trigger will execute a workflow ```ts // A trigger Definition file for the CLI import { IncidentEvent } from "./manifest.ts"; const trigger: Trigger = { type: "event", name: "Incident Metadata Posted", inputs: { id: "{{data.metadata.event_payload.incident_id}}", title: "{{data.metadata.event_payload.incident_title}}", summary: "{{data.metadata.event_payload.incident_summary}}", severity: "{{data.metadata.event_payload.incident_severity}}", date_created: "{{data.metadata.event_payload.incident_date}}", }, workflow: "#/workflows/start_incident", event: { event_type: "slack#/events/message_metadata_posted", metadata_event_type: IncidentEvent, channel_ids: ["C012354"], // The channel that needs to be watched for message metadata being posted }, }; export default trigger; ``` [manifest]: ./manifest.md [types]: ./types.md ================================================ FILE: docs/functions-action-handlers.md ================================================ ## Block kit action handlers Your application's [functions][functions] can do a wide variety of interesting things: post messages, create channels, or anything available to developers via the [Slack API][api]. One of the more compelling features available to app developers is the ability to use [Block Kit][block-kit] to add richness and depth to messages in Slack. Even better, [Block Kit][block-kit] supports a variety of [interactive components][interactivity]! This document explores the APIs available to app developers building Run-On-Slack applications to leverage these [interactive components][interactivity] and how applications can respond to user interactions with these [interactive components][interactivity]. If you're already familiar with the main concepts underpinning Block Kit Action Handlers, then you may want to skip ahead to the [`addBlockActionsHandler()` method API Reference](#api-reference). - [Block kit action handlers](#block-kit-action-handlers) - [Requirements](#requirements) - [Posting a message with block kit elements](#posting-a-message-with-block-kit-elements) - [Adding block action handlers](#adding-block-action-handlers) - [API reference](#api-reference) - [`addBlockActionsHandler(constraint, handler)`](#addblockactionshandlerconstraint-handler) - [`BlockActionConstraintField`](#blockactionconstraintfield) - [`BlockActionConstraintObject`](#blockactionconstraintobject) ### Requirements Your app needs to have an existing [function][functions] defined, implemented and working before you can add interactivity handlers like Block Kit Action Handlers to them. Make sure you have followed our [functions documentation][functions] and have a function in your app ready that we can expand with a Block Kit Action Handler. As part of exploring how Block Kit Action Handlers work, we'll walk through an approval flow example. A user would trigger our app's function, which would post a message with two buttons: Approve and Deny. Once someone clicks either button, our app will handle these button interactions - these Block Kit Actions - and update the original message with either an "Approved!" or "Denied!" text. For the purposes of walking through this approval flow example, let us assume the following [function][functions] definition (that we will store in a file called `definition.ts` under the `functions/approval/` subdirectory inside your app): ```typescript import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts"; export const ApprovalFunction = DefineFunction({ callback_id: "review_approval", title: "Approval", description: "Get approval for a request", source_file: "functions/approval/mod.ts", // <-- important! Make sure this is where the logic for your function - which we will write in the next section - exists. input_parameters: { properties: { requester_id: { type: Schema.slack.types.user_id, description: "Requester", }, approval_channel_id: { type: Schema.slack.types.channel_id, description: "Approval channel", }, }, required: [ "requester_id", "approval_channel_id", ], }, output_parameters: { properties: { approved: { type: Schema.types.boolean, description: "Approved", }, reviewer: { type: Schema.slack.types.user_id, description: "Reviewer", }, message_ts: { type: Schema.types.string, description: "Request Message TS", }, }, required: ["approved", "reviewer", "message_ts"], }, }); ``` ### Posting a message with block kit elements First, we need a message that has some [interactive components][interactivity] from [Block Kit][block-kit] included! We can modify one of our app's [functions][functions] to post a message that includes some interactive components. Here's an example function (which we will assume exists in a `mod.ts` file under the `functions/approval/` subdirectory in your app) that posts a message with two buttons: an approval button, and a deny button: ```typescript import { SlackFunction } from "deno-slack-sdk/mod.ts"; // ApprovalFunction is the function we defined in the previous section import { ApprovalFunction } from "./definition.ts"; export default SlackFunction(ApprovalFunction, async ({ inputs, client }) => { console.log("Incoming approval!"); await client.chat.postMessage({ channel: inputs.approval_channel_id, blocks: [{ "type": "actions", "block_id": "mah-buttons", "elements": [{ type: "button", text: { type: "plain_text", text: "Approve", }, action_id: "approve_request", style: "primary", }, { type: "button", text: { type: "plain_text", text: "Deny", }, action_id: "deny_request", style: "danger", }], }], }); // Important to set completed: false! We will set the function's complete // status later - in our action handler return { completed: false, }; }); ``` The key bit of information we need to remember before moving on to adding an action handler are the `action_id` and `block_id` properties defined in the `blocks` payload. Using these IDs, we will be able to differentiate between the different button components that users interacted with in this message. ### Adding block action handlers The [Deno Slack SDK][sdk] - which comes bundled in your generated Run-on-Slack application - provides a means for defining a handler to execute every time a user interacts with an interactive Block Kit element created by your function. Continuing with our above example, we can now define a handler that will listen for actions on one of the interactive components we attached to the message our main function posted: either the approve button being clicked or the deny button being clicked. The code to add a Block Kit action handler is "chained" off of your top-level function, and would look like this: ```typescript export default SlackFunction(ApprovalFunction, async ({ inputs, client }) => { // ... the rest of your ApprovalFunction logic here ... }).addBlockActionsHandler( ["approve_request", "deny_request"], // The first argument to addBlockActionsHandler can accept an array of action_id strings, among many other formats! // Check the API reference at the end of this document for the full list of supported options async ({ action, body, client }) => { // The second argument is the handler function itself console.log("Incoming action handler invocation", action); const outputs = { reviewer: body.user.id, // Based on which button was pressed - determined via action_id - we can // determine whether the request was approved or not. approved: action.action_id === "approve_request", message_ts: body.message.ts, }; // Remove the button from the original message using the chat.update API // and replace its contents with the result of the approval. await client.chat.update({ channel: body.function_data.inputs.approval_channel_id, ts: outputs.message_ts, blocks: [{ type: "context", elements: [ { type: "mrkdwn", text: `${ outputs.approved ? " :white_check_mark: Approved" : ":x: Denied" } by <@${outputs.reviewer}>`, }, ], }], }); // And now we can mark the function as 'completed' - which is required as // we explicitly marked it as incomplete in the main function handler. await client.functions.completeSuccess({ function_execution_id: body.function_data.execution_id, outputs, }); }, ); ``` Now when you run your app and trigger your function, you have the basics in place to provide interactivity between your application and users in Slack! ### API reference #### `addBlockActionsHandler(constraint, handler)` ```typescript SlackFunction({ ... }).addBlockActionsHandler({ block_id: "mah-buttons", action_id: "approve_request"}, async (ctx) => { ... }); ``` `addHandler` registers a block action handler based on a `constraint` argument. If any incoming actions match the `constraint`, then the specified `handler` will be invoked with the action. This allows for authoring focussed, single-purpose action handlers and provides a concise but flexible API for registering handlers to specific actions. `constraint` is of type [`BlockActionConstraint`][constraint], which itself can be either a [`BlockActionConstraintField`](#blockactionconstraintfield) or a [`BlockActionConstraintObject`](#blockactionconstraintobject). If a [`BlockActionConstraintField`](#blockactionconstraintfield) is used as the value for `constraint`, then this will be matched against the incoming action's `action_id` property. [`BlockActionConstraintObject`](#blockactionconstraintobject) is a more complex object used to match against actions. It contains nested `block_id` and `action_id` properties - both optional - that are used to match against the incoming action. ##### `BlockActionConstraintField` ```typescript type BlockActionConstraintField = string | string[] | RegExp; ``` - when provided as a `string`, it must match the field exactly. - when provided as an array of `string`s, it must match one of the array values exactly. - when provided as a `RegExp`, the regular expression must match. ###### `BlockActionConstraintObject` ```typescript type BlockActionConstraintObject = { block_id?: BlockActionConstraintField; action_id?: BlockActionConstraintField; }; ``` This object can contain two properties, both optional: `action_id` and/or `block_id`. The type of each property is [`BlockActionConstraintField`](#blockactionconstraintfield). If both `action_id` and `block_id` properties exist on the `constraint`, then both `action_id` and `block_id` properties _must match_ any incoming action. If only one of these properties is provided, then only the provided property must match. [functions]: ./functions.md [api]: https://api.slack.com/methods [block-kit]: https://api.slack.com/block-kit [interactivity]: https://api.slack.com/block-kit/interactivity [sdk]: https://github.com/slackapi/deno-slack-sdk [constraint]: ../src/functions/routers/types.ts#L53-L62 ================================================ FILE: docs/functions-suggestion-handlers.md ================================================ ## Block Kit suggestion handlers Your application's [functions][functions] can do a wide variety of interesting things: post messages, create channels, or anything available to developers via the [Slack API][api]. One of the more compelling features available to app developers is the ability to use [Block Kit][block-kit] to add richness and depth to messages in Slack. Even better, [Block Kit][block-kit] supports a variety of [interactive components][interactivity]! This document explores how to provide dynamic menu options for [external-data-sourced Block Kit drop-down menus](https://api.slack.com/reference/block-kit/block-elements#external_select). If you're already familiar with the main concepts underpinning Block Kit Suggestion Handlers, then you may want to skip ahead to the [`addBlockSuggestionHandler()` method API Reference](#api-reference). Worthwhile noting that while this document covers how to render custom menu options for external-data-sourced Block Kit drop-down menus, it does _not_ cover how to respond to a user selecting one of the custom menu options. Do not fear, though! The same approach discussed in our [Block Kit Action Handlers][action-handlers] document can be used to register an action handler to respond to drop-down menu selections. - [Block Kit suggestion handlers](#block-kit-suggestion-handlers) - [Requirements](#requirements) - [Posting a message with block kit elements](#posting-a-message-with-block-kit-elements) - [Adding Block Suggestion Handlers](#adding-block-suggestion-handlers) - [API Reference](#api-reference) - [`addBlockSuggestionHandler(constraint, handler)`](#addblocksuggestionhandlerconstraint-handler) - [`BlockActionConstraintField`](#blockactionconstraintfield) - [`BlockActionConstraintObject`](#blockactionconstraintobject) ### Requirements Your app needs to have an existing [function][functions] defined, implemented and working before you can add interactivity handlers like Block Kit Suggestion Handlers to them. Make sure you have followed our [functions documentation][functions] and have a function in your app ready that we can expand with interactivity. Familiarity with the [Block Kit Actions Handlers][action-handlers] would be a huge plus as the handling Block Kit Actions and handling Block Kit Suggestions is practically identical. As part of exploring how Block Kit Suggestion Handlers work, we'll walk through an example that posts an inspirational quote. A user would trigger our app's function, which would post a message with a drop down select menu and a button. The options rendered in the select menu will be dynamically loaded from an external API. Finally, when someone has selected a drop-down menu option and clicked the button, our app can post the selection to the channel (note: for the purposes of describing how to respond to the select menu interactions, we won't cover handling the button click or posting the selection in this document). For the purposes of walking through this example, let us assume the following [function][functions] definition (that we will store in a file called `definition.ts` under the `functions/quote/` subdirectory inside your app): ```typescript import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts"; export const QuoteFunction = DefineFunction({ callback_id: "quote", title: "Inspire Me", description: "Get an inspirational quote", source_file: "functions/quote/mod.ts", // <-- important! Make sure this is where the logic for your function - which we will write in the next section - exists. input_parameters: { properties: { requester_id: { type: Schema.slack.types.user_id, description: "Requester", }, channel_id: { type: Schema.slack.types.channel_id, description: "Channel", }, }, required: [ "requester_id", "channel_id", ], }, output_parameters: { properties: { quote: { type: Schema.types.string, description: "Quote", }, }, required: ["quote"], }, }); ``` ### Posting a message with block kit elements First, we need a message that has some [interactive components][interactivity] from [Block Kit][block-kit] included! We can modify one of our app's [functions][functions] to post a message that includes some interactive components - including our external select drop down menu. Here's an example function (which we will assume exists in a `mod.ts` file under the `functions/quote/` subdirectory in your app) that posts a message with a external data select drop down menu: ```typescript import { SlackFunction } from "deno-slack-sdk/mod.ts"; // QuoteFunction is the function we defined in the previous section import { QuoteFunction } from "./definition.ts"; export default SlackFunction(QuoteFunction, async ({ inputs, client }) => { console.log("Incoming quote request!"); await client.chat.postMessage({ channel: inputs.channel_id, blocks: [{ "type": "actions", "block_id": "so-inspired", "elements": [{ type: "external_select", placeholder: { type: "plain_text", text: "Inspire", }, action_id: "ext_select_input", }, { type: "button", text: { type: "plain_text", text: "Post", }, action_id: "post_quote", }], }], }); // Important to set completed: false! We should set the function's complete // status later - in the action handler responding to the button click return { completed: false, }; }); ``` The key bit of information we need to remember before moving on to adding a suggestion handler are the `action_id` and `block_id` properties defined in the `blocks` payload. Using these IDs, we will be able to differentiate between the different Block Kit components that users interacted with in this message. ### Adding Block Suggestion Handlers The [Deno Slack SDK][sdk] - which comes bundled in your generated Run-on-Slack application - provides a means for defining a handler to execute every time a user interacts with an interactive Block Kit element created by your function. Continuing with our above example, we can now define a handler that will listen for interactions with the external data drop down menu. The code to add a Block Kit suggestion handler is "chained" off of your top-level function, and would look like this: ```typescript export default SlackFunction(QuoteFunction, async ({ inputs, client }) => { // ... the rest of your QuoteFunction logic here ... }).addBlockSuggestionHandler( "ext_select_input", // The first argument to addBlockActionsHandler can accept an action_id string, among many other formats! // Check the API reference at the end of this document for the full list of supported options async ({ body, client }) => { // The second argument is the handler function itself console.log("Incoming suggestion handler invocation", body); // Fetch some inspirational quotes const apiResp = await fetch( "https://motivational-quote-api.herokuapp.com/quotes", ); const quotes = await apiResp.json(); console.log("Returning", quotes.length, "quotes"); const opts = { "options": quotes.map((q) => ({ value: `${q.id}`, text: { type: "plain_text", text: q.quote.slice(0, 70) }, })), }; return opts; }, ); ``` ### API Reference #### `addBlockSuggestionHandler(constraint, handler)` ```typescript SlackFunction({ ... }).addBlockSuggestionHandler({ block_id: "mah-buttons", action_id: "approve_request"}, async (ctx) => { ... }); ``` `addBlockSuggestionHandler` registers a block suggestion handler based on a `constraint` argument. If any incoming suggestion events match the `constraint`, then the specified `handler` will be invoked with the suggestion payload. This allows for authoring focussed, single-purpose suggestion handlers and provides a concise but flexible API for registering handlers to specific external-data-sourced drop down menu. `constraint` is of type [`BlockActionConstraint`][constraint], which itself can be either a [`BlockActionConstraintField`](#blockactionconstraintfield) or a [`BlockActionConstraintObject`](#blockactionconstraintobject). If a [`BlockActionConstraintField`](#blockactionconstraintfield) is used as the value for `constraint`, then this will be matched against the incoming action's `action_id` property. [`BlockActionConstraintObject`](#blockactionconstraintobject) is a more complex object used to match against actions. It contains nested `block_id` and `action_id` properties - both optional - that are used to match against the incoming suggestion. ##### `BlockActionConstraintField` ```typescript type BlockActionConstraintField = string | string[] | RegExp; ``` - when provided as a `string`, it must match the field exactly. - when provided as an array of `string`s, it must match one of the array values exactly. - when provided as a `RegExp`, the regular expression must match. ###### `BlockActionConstraintObject` ```typescript type BlockActionConstraintObject = { block_id?: BlockActionConstraintField; action_id?: BlockActionConstraintField; }; ``` This object can contain two properties, both optional: `action_id` and/or `block_id`. The type of each property is [`BlockActionConstraintField`](#blockactionconstraintfield). If both `action_id` and `block_id` properties exist on the `constraint`, then both `action_id` and `block_id` properties _must match_ any incoming suggestion. If only one of these properties is provided, then only the provided property must match. [functions]: ./functions.md [action-handlers]: ./functions-action-handlers.md [api]: https://api.slack.com/methods [block-kit]: https://api.slack.com/block-kit [interactivity]: https://api.slack.com/block-kit/interactivity [sdk]: https://github.com/slackapi/deno-slack-sdk [constraint]: ../src/functions/routers/types.ts#L53-L62 ================================================ FILE: docs/functions-view-handlers.md ================================================ ## View Handlers Your application's [functions][functions] can do a wide variety of interesting things: post messages, create channels, or anything available to developers via the [Slack API][api]. They can even include [interactive components][interactivity] or pop up a [Modal][modals]. [Modals][modals] are composed of up to three [Views][views]. These [Views][views] can contain form inputs or [interactive components][interactivity]. [Views][views] themselves may also [trigger events][view-events]. This document explores the APIs available to app developers building Run-On-Slack applications to create [modals][modals] composed of [views][views] and how applications can respond to the [view submission and closed events][view-events] they can trigger. If you're already familiar with the main concepts underpinning View Handlers, then you may want to skip ahead to the [API Reference](#api-reference). - [View Handlers](#view-handlers) - [Requirements](#requirements) - [Opening a view](#opening-a-view) - [Opening a view from a custom function](#opening-a-view-from-a-custom-function) - [Opening a view from a block action handler](#opening-a-view-from-a-block-action-handler) - [Adding view handlers](#adding-view-handlers) - [API reference](#api-reference) - [`addViewSubmissionHandler(constraint, handler)`](#addviewsubmissionhandlerconstraint-handler) - [`addViewClosedHandler(constraint, handler)`](#addviewclosedhandlerconstraint-handler) ### Requirements This functionality requires at least version 0.2.0 of the [`deno-slack-sdk`][sdk]. Your app needs to have an existing [function][functions] defined, implemented and working before you can add interactivity handlers like View Handlers or [Block Kit Action Handlers][action-handlers] to them. Make sure you have followed our [functions documentation][functions] and have a function in your app ready that we can expand with a View Handler. As part of exploring how View Handlers work, we'll walk through a simple diary flow example. It is nothing more than a contrived example aimed at showing off the APIs. A user would trigger our app's function, which would open a view with a single text input. If the view is submitted with content, the application will send the user a DM with their inputted content. If the view is closed, the application will send the user a DM encouraging them not to give up on their diarying habit. For the purposes of walking through this approval flow example, let us assume the following [function][functions] definition (that we will store in a file called `definition.ts` under the `functions/diary/` subdirectory inside your app): ```typescript import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts"; export const DiaryFunction = DefineFunction({ callback_id: "diary", title: "Diary", description: "Write a diary entry", source_file: "functions/diary/mod.ts", // <-- important! Make sure this is where the logic for your function - which we will write in the next section - exists. input_parameters: { properties: { interactivity: { // <-- important! This gives Slack a hint that your function will create interactive elements like views type: Schema.slack.types.interactivity, }, channel_id: { type: Schema.slack.types.channel_id, }, }, required: ["interactivity"], }, output_parameters: { properties: {}, required: [], }, }); ``` ### Opening a view [Opening a view via the `views.open` API][views-open] and [pushing a new view onto the view stack via the `views.push` API][views-push] both require the use of a [`trigger_id`][trigger-ids]. These are identifiers representing specific user interactions. Slack uses these to prevent applications from haphazardly opening modals in users' faces willy-nilly. Without a `trigger_id`, your application can't create a modal and open a view. FYI `trigger_id`s are also known as `interactivity_pointer`s. As such, there are two ways to open a view from inside a Run-On-Slack application: doing so [from a function directly](#opening-a-view-from-a-function) vs. doing so [from a Block Action Handler](#opening-a-view-from-a-block-action-handler). The sections covering each approach below discuss how to retrieve the `trigger_id` in each scenario. We will explore implementing our contrived example above by opening a view from a function. In a section further below, we will also cover [opening a view from a Block Action Handler](#opening-a-view-from-a-block-action-handler). #### Opening a view from a custom function As mentioned in the previous section, we need to have a `trigger_id` handy in order to open a view. This is why we defined an `interactivity` input in our function definition earlier: this input will magically provide us with a `trigger_id`. The property to use as a `trigger_id` exists on inputs with the type `Schema.slack.types.interactivity` under the `interactivity_pointer` property. Check out the code below for an example: ```typescript import { SlackFunction } from "deno-slack-sdk/mod.ts"; // DiaryFunction is the function we defined in the previous section import { DiaryFunction } from "./definition.ts"; export default SlackFunction(DiaryFunction, async ({ inputs, client }) => { console.log('Someone might want to write a diary entry...'); await client.views.open({ trigger_id: inputs.interactivity.interactivity_pointer, view: { "type": "modal", "title": { "type": "plain_text", "text": "Modal title", }, "blocks": [ { "type": "input", "block_id": "section1", "element": { "type": "plain_text_input", "action_id": "diary_input", "multiline": true, "placeholder": { "type": "plain_text", "text": "What is on your mind today?", }, }, "label": { "type": "plain_text", "text": "Diary Entry", }, "hint": { "type": "plain_text", "text": "Don't worry, no one but you will see this.", }, }, ], "close": { "type": "plain_text", "text": "Cancel", }, "submit": { "type": "plain_text", "text": "Save", }, "callback_id": "view_identifier_12", // <-- remember this ID, we will use it to route events to handlers! "notify_on_close": true, // <-- this must be defined in order to trigger `view_closed` events! }, }); // Important to set completed: false! We will set the function's complete // status later - in our view submission handler return { completed: false, }; }; ``` #### Opening a view from a block action handler If [Block Kit Action Handlers][action-handlers] is a foreign concept to you, we recommend first checking out [its documentation][action-handlers] before venturing deeper into this section. Similarly to opening a view from a function, doing so from a [Block Action Handler][action-handlers] is straightforward though slightly different. It is important to remember that `trigger_id`s represent a unique user interaction with a particular interactive component within Slack's UI. As such, when responding to a Block Kit Action interactive component, we don't want to use your function's `inputs` to retrieve the `interactivity_pointer`, as we did in the previous section, but rather, we want to retrieve a `trigger_id` that is unique to the Block Kit interactive component. Luckily for us, this is provided as a parameter to Block Kit Action Handlers! You can use the value of `body.interactivity.interactivity_pointer` within an action handler to open a view, like so: ```typescript export default SlackFunction(DiaryFunction, async ({ inputs, client }) => { // ... the rest of your DiaryFunction logic here ... }).addBlockActionsHandler( "deny_request", async ({ action, body, client }) => { await client.views.open({ trigger_id: body.interactivity.interactivity_pointer, view: {/* your view object goes here */}, }); }, ); ``` ### Adding view handlers The [Deno Slack SDK][sdk] - which comes bundled in your generated Run-on-Slack application - provides a means for defining handlers to execute every time a user interacts with a view. In this way you can route view-related events to specific handlers inside your application. The key identifier that we'll need to keep handy is the `callback_id` we assigned to any views we created. This ID will be the property that determines which view event handler will respond to incoming view events. Continuing with our above example, we can now define handlers that will listen for view submission and closed events and respond accordingly. The code to add view handlers is "chained" off of your top-level function, and would look like this: ```typescript export default SlackFunction(DiaryFunction, async ({ inputs, client }) => { // ... the rest of your DiaryFunction logic here ... }).addViewSubmissionHandler( /view/, // The first argument to any of the addView*Handler methods can accept a string, array of strings, or RegExp. // This first argument will be used to match the view's `callback_id` // Check the API reference at the end of this document for the full list of supported options async ({ view, body, token }) => { // The second argument is the handler function itself console.log("Incoming view submission handler invocation", body); }, ) .addViewClosedHandler( /view/, async ({ view, body, token }) => { console.log("Incoming view closed handler invocation", body); }, ); ``` Importantly, more complex applications will likely be modifying views as users interact with them: updating the view contents (to e.g. add new form fields), perhaps pushing a new view onto the view stack to introduce a new UI to the user, maybe reporting errors to the user for some manner of faulty interaction, or even clearing the entire view stack altogether. All of these modal interaction responses are [covered in depth on our API documentation site][modifying] - make sure to spend the time to understand the concepts presented there. In particular, modal interactions can be responded to by using the API, or by returning particularly-crafted responses directly from inside the view handlers. On our [API site detailing view modification][modifying], these returned view handler responses are called `response_action`s. As an example, consider the following two code snippets. They yield identical behavior! ```typescript export default SlackFunction(DiaryFunction, async ({ inputs, client }) => { // ... the rest of your DiaryFunction logic here ... }).addViewSubmissionHandler(/view/, async ({ client, body }) => { // A view submission handler that pushes a new view using the API await client.views.push({ trigger_id: body.trigger_id, view: {/* your view object goes here */}, }); }).addSubmissionHandler(/view/, async () => { // A view submission handler that pushes a new view using the `response_action` return { response_action: "push", view: {/* your view object goes here */}, }; }); ``` ### API reference #### `addViewSubmissionHandler(constraint, handler)` ```typescript SlackFunction({ ... }).addViewSubmissionHandler("my_view_callback_id", async (ctx) => { ... }); ``` `addViewSubmissionHandler` registers a view handler based on a `constraint` argument. If any incoming [`view_submission` event][view-events] matches the `constraint`, then the specified `handler` will be invoked with the event payload. This allows for authoring focussed, single-purpose view handlers and provides a concise but flexible API for registering handlers to specific view interactions. `constraint` can be either a string, an array of strings, or a regular expression. - A simple string `constraint` must match a view's `callback_id` exactly. - An array of strings `constraint` must match a view's `callback_id` to any of the strings in the array. - A regular expression `constraint` must match a view's `callback_id`. ##### `addViewClosedHandler(constraint, handler)` ```typescript SlackFunction({ ... }).addViewClosedHandler("my_view_callback_id", async (ctx) => { ... }); ``` ⚠️ IMPORTANT: you must set a view's `notify_on_close` property to `true` for the `view_closed` event to trigger; by default this property is `false`. See the [View reference documentation - in particular the Fields section][view-ref] for more information. `addViewClosedHandler` registers a view handler based on a `constraint` argument. If any incoming [`view_closed` event][view-events] matches the `constraint`, then the specified `handler` will be invoked with the event payload. This allows for authoring focussed, single-purpose view handlers and provides a concise but flexible API for registering handlers to specific view interactions. `constraint` can be either a string, an array of strings, or a regular expression. - A simple string `constraint` must match a view's `callback_id` exactly. - An array of strings `constraint` must match a view's `callback_id` to any of the strings in the array. - A regular expression `constraint` must match a view's `callback_id`. [functions]: ./functions.md [action-handlers]: ./functions-action-handlers.md [api]: https://api.slack.com/methods [block-kit]: https://api.slack.com/block-kit [interactivity]: https://api.slack.com/block-kit/interactivity [sdk]: https://github.com/slackapi/deno-slack-sdk [modals]: https://api.slack.com/surfaces/modals [views]: https://api.slack.com/surfaces/modals/using [modifying]: https://api.slack.com/surfaces/modals/using#modifying [trigger-ids]: https://api.slack.com/interactivity/handling#modal_responses [view-events]: https://api.slack.com/reference/interaction-payloads/views [views-methods]: https://api.slack.com/methods?filter=views [views-open]: https://api.slack.com/methods/views.open [views-update]: https://api.slack.com/methods/views.update [views-push]: https://api.slack.com/methods/views.push [view-ref]: https://api.slack.com/reference/surfaces/views ================================================ FILE: docs/functions.md ================================================ ## Custom functions Functions are the core of your Slack app: they accept one or more input parameters, execute some logic and return one or more output parameters. Functions can optionally define different kinds of Interactivity Handlers. If your function creates messages or opens views, then it may need to define one or more interactivity handlers to respond to user interactions with these interactive components. Run-on-Slack applications support the following interactivity handlers, follow the links to get more information about how each of them work: - [Block Kit Action Handlers][action-handlers]: Handle events from interactive [Block Kit][block-kit] components that you can use in messages like [Buttons, Menus and Date/Time Pickers](https://api.slack.com/block-kit/interactivity) - [View Handlers][view-handlers]: Handle events triggered from [Modals][modals], which are composed of [Views][views]. - [Block Kit Suggestion Handlers][suggest-handlers]: Handle events from [external-data-sourced Block Kit select menus](https://api.slack.com/reference/block-kit/block-elements#external_select) ### Defining a custom function Functions can be defined with the top level `DefineFunction` export. Below is an example function that turns a `name` input parameter into a dinosaur name: ```ts import { DefineFunction, Schema } from "slack-cloud-sdk/mod.ts"; export const DinoFunction = DefineFunction({ callback_id: "dino", title: "Dino", description: "Turns a name into a dinosaur name", source_file: "functions/dino.ts", input_parameters: { name: { type: Schema.types.string, description: "The provided name", }, }, output_parameters: { dinoname: { type: Schema.types.string, description: "The new dinosaur name", }, }, }); ``` Let's go over each of the arguments that must be provided to `DefineFunction`. #### Function definition The passed argument is the `definition` of the function, an object with a few properties that help to describe and define the function in more detail. In particular, the required properties of the object are: - `callback_id`: A unique string identifier representing the function (`"dino"` in the above example). It must be unique in your application; no other functions may be named identically. Changing a function's `callback_id` is not recommended as it means that the function will be removed from the app and created under the new `callback_id`, which will break any workflows referencing the old function. - `title`: A pretty string to nicely identify the function. - `description`: A short-and-sweet string description of your function succinctly summarizing what your function does. - `source_file`: The relative path from the project root to the function `handler` file. - `input_parameters`: Itself an object which describes one or more input parameters that will be available to your function. Each top-level property of this object defines the name of one input parameter which will become available to your function. The value for this property needs to be an object with further sub-properties: - `type`: The type of the input parameter. The supported types are `string`, `integer`, `boolean`, `number`, `object` and `array`. - `description`: A string description of the input parameter. - `output_parameters`: Itself an object which describes one or more output parameters that will be returned by your function. This object follows the exact same pattern as `input_parameters`: top-level properties of the object define output parameter names, with the property values being an object that further describes the `type` and `description` of individual output parameters. ### Adding runtime logic to your custom function Now that you have defined your function's input and output parameters, it's time to define the body of your function. First, create a new file at the location set on the `source_file` parameter of your function definition. Next, let's add code for your function! You will want to `export default` an instance of `SlackFunction`, like so: ```typescript import { SlackFunction } from "deno-slack-sdk/mod.ts"; export default SlackFunction( // Pass along the function definition you created earlier using `DefineFunction` DinoFunction, ({ inputs }) => { // Provide any context properties, like `inputs`, `env`, or `token` // Implement your function const { name } = inputs; const dinoname = `${name}asaurus`; // Don't forget any required output parameters return { outputs: { dinoname } }; }, ); ``` Key points: - The function takes a single argument, referred to as the [function "context"](#function-handler-context). - The function [returns an object](#function-return-object). #### Custom function handler context The single argument to your function is an object composed of several properties that may be useful to leverage during your function's execution: - `env`: represents environment variables available to your function's execution context. - `inputs`: an object containing the input parameters you defined as part of your function definition. In the example above, the `name` input parameter is available on the `inputs` property of our function handler context. - `client`: An API client ready for use in your function. An instance of the `deno-slack-api` library. - `token`: your application's access token. - `team_id`: the encoded team (a.k.a. Slack workspace) ID, i.e. T12345. - `enterprise_id`: the encoded enterprise ID, i.e. E12345. If the Slack workspace the function executes in is not a part of an enterprise grid, then this value will be the empty string (`""`). - `event`: an object containing the full incoming event details. ##### Custom function return object The object returned by your function that supports the following properties: - `error`: a string indicating the error that was encountered. If present, the function will return an error regardless of what is passed to `outputs`. - `outputs`: an object that exactly matches the structure of your function definition's `output_parameters`. This is required unless an `error` is returned. - `completed`: a boolean indicating whether or not the function is completed. This defaults to `true`. ### Adding custom functions to the manifest Once you have defined a function, don't forget to include it in your [`Manifest`][manifest] definition! ```ts import { ReverseString } from "./functions/reverse_definition.ts"; Manifest({ name: "heuristic-tortoise", description: "A demo showing how to use custom functions", icon: "assets/icon.png", botScopes: ["commands", "chat:write", "chat:write.public"], functions: [ReverseString], // <-- don't forget this! }); ``` [manifest]: ./manifest.md [action-handlers]: ./functions-action-handlers.md [view-handlers]: ./functions-view-handlers.md [suggest-handlers]: ./functions-suggestion-handlers.md [block-kit]: https://api.slack.com/block-kit [modals]: https://api.slack.com/surfaces/modals [views]: https://api.slack.com/surfaces/modals/using ================================================ FILE: docs/manifest.md ================================================ ## Manifest A Manifest defines your entire Slack application, from its core properties like its name and description to its behavioural aspects like what [functions][functions] it contains. ### Defining a manifest A Manifest can be defined with the top level `Manifest` export. Below is an example, taken from the template application: ```ts import { Manifest } from "slack-cloud-sdk/mod.ts"; import { ReverseString } from "./functions/reverse_definition.ts"; export default Manifest({ name: "heuristic-tortoise-312", description: "A demo showing how to use Slack functions", icon: "assets/icon.png", botScopes: ["commands", "chat:write", "chat:write.public"], functions: [ReverseString], datastores: [], outgoing_domains: [], }); ``` The object passed into the `Manifest` method is the type [`SlackManifestType`][manifest-type]. Check out [its definition][manifest-type] for the full list of attributes it supports, but the minimum required properties are listed in the table below: | Property | Type | Description | | ------------- | ------------- | ------------------------------------------------------------------------- | | `name` | string | Your Slack application name. | | `description` | string | A short sentence describing your application. | | `icon` | string | A relative path to an image asset to use for the application's icon. | | `botScopes` | Array<string> | A list of [scopes][scopes], or permissions, the bot requires to function. | Furthermore, to set up how your application works, you would create [functions][functions], and register them in the Manifest using the `functions` property of [`SlackManifestType`][manifest-type] argument used when creating a new `Manifest`. [functions]: ./functions.md [manifest-type]: ../src/types.ts#L13 [scopes]: https://api.slack.com/scopes ================================================ FILE: docs/types.md ================================================ ## Types Custom Types provide a way to introduce reusable, sharable types to Apps. ### Defining a type Types can be defined with the top level `DefineType` export. Below is an example of setting up a custom Type used for incident management. ```ts const IncidentType = DefineType({ name: "incident", title: "Incident Ticket", description: "Use this to enter an Incident Ticket", type: Schema.types.object, properties: { id: { type: Schema.types.string, minLength: 3, }, title: { type: Schema.types.string, }, summary: { type: Schema.types.string, }, severity: { type: Schema.types.string, }, date_created: { type: Schema.types.number, }, }, required: [], }); ``` ### Registering a type with the App To register the newly defined type, add it to the array assigned to the `types` parameter while defining the [`Manifest`][manifest]. Note: All Custom Types **must** be registered to the [Manifest][manifest] in order for them to be used, but any types referenced by existing [`functions`][functions], [`workflows`][workflows], [`datastores`][datastores], or other types will be registered automatically. ```ts Manifest({ ... types: [IncidentType], }); ``` ### Referencing types To use a type as a [function][functions] parameter, set the parameter's `type` property to the Type it should reference. ```js input_parameters: { incident: : { title: 'A Special Incident', type: IncidentType } ... } ``` _In the provided example the title from the Custom Type is being overridden_ [functions]: ./functions.md [manifest]: ./manifest.md [datastores]: ./datastores.md [workflows]: ./workflows.md ================================================ FILE: docs/workflows.md ================================================ ## Workflows Workflows can be defined and included in your [manifest][manifest]. A workflow itself has several pieces of metadata, such as a unique `callback_id`, a `title` and a `description`. It can also include `input_parameters` just like a [function][function]. Key to a workflow is a series of steps, each of which are a function that can be passed dynamic data to their inputs through referencing workflow inputs, or outputs from previous steps. Let's take a look at an example. ```ts import { DefineWorkflow, Manifest, Schema } from "slack-cloud-sdk/mod.ts"; const workflow = DefineWorkflow({ callback_id: "my_workflow", title: "My Workflow", description: "A sample workflow", input_parameters: { properties: { a_string: { type: Schema.types.string, }, a_channel: { type: Schema.slack.types.channel_id, } }, required: ["a_string", "a_channel"], }, }); // register your workflow in your manifest export default Manifest({ ..., workflows: [ workflow, ] }); ``` A workflow by itself isn't of much use, and isn't valid, until you add some steps. Let's use the `DinoFunction` we've defined over in the [functions][function] example as one of our steps. The `DinoFunction` has a single `input_parameter` of `name` that we'll need to pass it. We'll use our `a_string` workflow `input_parameter` as the value for this, but you could just as easily pass a hard-coded value to any step input parameter as well. ```ts import { DefineWorkflow } from "slack-cloud-sdk/mod.ts"; import { DinoFunction } from '../functions/dino.ts'; const workflow = DefineWorkflow({...}); const step1 = workflow.addStep(DinoFunction, { name: workflow.inputs.a_string, }); ``` Great, we've got a single step workflow that takes a string, and turns it into a dinosaur name via our `DinoFunction`. It would be nice to see what that looks like, so lets add another step that sends that value as a message somewhere. For this, we can use one of Slack's functions. Notice how we can also use our reference to `step1` to access an output called `dinoname` that the `DinoFunction` produces. ```ts const step1 = workflow.addStep(...); workflow.addStep("slack#/functions/send_message", { channel: workflow.inputs.a_channel, message: `A dinosaur name: ${step1.outputs.dinoname}`, }); ``` You'll notice the first parameter to `addStep()` here is a string, instead of something like our `DinoFunction`. This is because we're referencing a step produced outside of our app, in this case by `slack`. We're using the string reference of `"slack#/functions/send_message"` to identify the function we're adding as a step. In fact, you can do the same thing with your own functions by creating what's called a local reference string to your own app's function. This uses your `callback_id`, and would look like `"#/functions/my_workflow"`. If we added our function as a step that way, it would look like this: ```ts const step1 = workflow.addStep("#/functions/my_workflow", { name: workflow.inputs.a_string, }); ``` The big difference here is you won't get some of the automatic typing of `inputs` and `outputs` for that step, but you can still reference them as long as you follow the definition of that function. ### Auto-Registration of workflow dependencies When a workflow is registered on your `Manifest()` any `functions` it uses as steps, or custom `types` used as `input_parameters` to the workflow or functions it references are automatically registered in your manifest. This can save you from having to register each function and type that a workflow might use, and just register the workflow. [manifest]: ./manifest.md [function]: ./functions.md ================================================ FILE: scripts/build_npm.ts ================================================ // ex. scripts/build_npm.ts import { build, emptyDir } from "jsr:@deno/dnt@0.42.1"; await emptyDir("./npm"); await build({ typeCheck: false, test: false, entryPoints: ["./src/mod.ts"], outDir: "./npm", // ensures that the emitted package is compatible with node v14 later compilerOptions: { lib: ["ES2022.Error"], // fix ErrorOptions not exported in ES2020 target: "ES2020", }, shims: { // see JS docs for overview and more options deno: true, // Shim fetch, File, FormData, Headers, Request, and Response undici: true, }, package: { // package.json properties name: "@slack/deno-slack-sdk", version: Deno.args[0], description: "Official library for using Deno Slack SDK in node Slack apps", license: "MIT", repository: { type: "git", url: "git+https://github.com/slackapi/deno-slack-sdk.git", }, bugs: { url: "https://github.com/slackapi/deno-slack-sdk/issues", }, // sets the minimum engine to node v14 // as of writing, dnt transpilation-generated code // seems to only be able to successfully compile as far back ES2020 engines: { "node": ">=14.20.1", "npm": ">=6.14.15", }, }, }); // post build steps Deno.copyFileSync("README.md", "npm/README.md"); ================================================ FILE: scripts/bundle.ts ================================================ import * as esbuild from "npm:esbuild@0.25.5"; import { denoPlugins } from "jsr:@luca/esbuild-deno-loader@0.11.1"; import { join } from "jsr:@std/path@1.1.0"; async function bundle(options: { target: string; }): Promise<Uint8Array> { const cwd = Deno.cwd(); try { // esbuild configuration options https://esbuild.github.io/api/#overview const result = await esbuild.build({ entryPoints: [join(cwd, "src/mod.ts")], platform: "browser", target: options.target, format: "esm", // esm format stands for "ECMAScript module" bundle: true, // inline any imported dependencies into the file itself absWorkingDir: cwd, // root of this project write: false, // Favor returning the contents outdir: "out", // Nothing is being written to file here plugins: [ ...denoPlugins({ configPath: join(cwd, "deno.jsonc") }), ], }); return result.outputFiles[0].contents; } finally { esbuild.stop(); } } console.log("Building bundle for target: deno1"); const deno1Bundle = await bundle({ target: "deno1" }); const deno1Content = new TextDecoder().decode(deno1Bundle); console.log(`Bundle for deno1 built successfully!`); console.log(deno1Content); console.log("Building bundle for target: deno2"); const deno2Bundle = await bundle({ target: "deno2" }); const deno2Content = new TextDecoder().decode(deno2Bundle); console.log(`Bundle for deno2 built successfully!`); console.log(deno2Content); ================================================ FILE: scripts/imports/update.ts ================================================ import { parseArgs } from "jsr:@std/cli@1.0.17/parse-args"; import { dirname, join, relative } from "jsr:@std/path@1.1.0"; const flags = parseArgs(Deno.args, { string: ["import-file", "sdk"], default: { "import-file": `${Deno.cwd()}/deno.jsonc`, "sdk": "./deno-slack-sdk/", }, }); const importFilePath = await Deno.realPath(flags["import-file"]); const importFileDir = dirname(importFilePath); const sdkDir = await Deno.realPath(flags.sdk); const importFileJsonIn = await Deno.readTextFile(importFilePath); console.log(`content in ${importFilePath}:`, importFileJsonIn); const sdkPackageSpecifier = join( relative(importFileDir, sdkDir), "/src/", ); const importMap = JSON.parse(importFileJsonIn); importMap["imports"]["deno-slack-sdk/"] = sdkPackageSpecifier; const importMapJsonOut = JSON.stringify(importMap); console.log("`import file` out content:", importMapJsonOut); await Deno.writeTextFile(importFilePath, importMapJsonOut); ================================================ FILE: src/README.md ================================================ <h1 align="center"> Deno Slack SDK <br> </h1> <p align="center"> <img alt="deno.land version" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fdeno-visualizer.danopia.net%2Fshields%2Flatest-version%2Fx%2Fdeno_slack_sdk%2Fmod.ts"> <img alt="deno dependencies" src="https://img.shields.io/endpoint?url=https%3A%2F%2Fdeno-visualizer.danopia.net%2Fshields%2Fupdates%2Fx%2Fdeno_slack_sdk%2Fmod.ts"> <img alt="Samples Integration Type-checking" src="https://github.com/slackapi/deno-slack-sdk/workflows/Samples%20Integration%20Type-checking/badge.svg"> </a> </p> A Deno SDK to build Slack apps with the latest platform features. Read the [quickstart guide](https://api.slack.com/automation/quickstart) and look at our [code samples](https://api.slack.com/automation/samples) to learn how to build apps. ## 📢 Important We recommend importing this package from [JSR](https://jsr.io/@slack/sdk/versions), but new releases are currently still published to `deno.land/x` as well. ## Versioning Releases for this repository follow the [SemVer](https://semver.org/) versioning scheme. The SDK's contract is determined by the top-level exports from `src/mod.ts` and `src/types.ts`. Exports not included in these files are deemed internal and any modifications will not be treated as breaking changes. As such, internal exports should be treated as unstable and used at your own risk. ## Setup Make sure you have a development workspace where you have permission to install apps. **Please note that the features in this project require that the workspace be part of [a Slack paid plan](https://slack.com/pricing).** ### Install the Slack CLI You need to install and configure the Slack CLI. Step-by-step instructions can be found on our [install & authorize page](https://api.slack.com/automation/cli/install). ## Creating an app Create a blank project by executing the following command: ```zsh slack create my-app --template slack-samples/deno-blank-template cd my-app/ ``` The `manifest.ts` file contains the app's configuration. This file defines attributes like app name, description and functions. ### Create a [function](https://api.slack.com/automation/functions/custom) ```zsh mkdir ./functions && touch ./functions/hello_world.ts ``` ```ts // Contents of ./functions/hello_world.ts import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; export const HelloWorldFunctionDef = DefineFunction({ callback_id: "hello_world_function", title: "Hello World", source_file: "functions/hello_world.ts", input_parameters: { properties: {}, required: [], }, output_parameters: { properties: { message: { type: Schema.types.string, description: "Hello world message", }, }, required: ["message"], }, }); export default SlackFunction( HelloWorldFunctionDef, () => { return { outputs: { message: "Hello World!" }, }; }, ); ``` `DefineFunction` is used to define a custom function and provide Slack with the information required to use it. `SlackFunction` uses the definition returned by `DefineFunction` and your custom executable code to export a Slack-usable custom function. ### Create a [workflow](https://api.slack.com/automation/workflows) ```zsh mkdir ./workflows && touch ./workflows/hello_world.ts ``` ```ts // Contents of ./workflows/hello_world.ts import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; import { HelloWorldFunctionDef } from "../functions/hello_world.ts"; const HelloWorldWorkflowDef = DefineWorkflow({ callback_id: "hello_world_workflow", title: "Hello World Workflow", input_parameters: { properties: { channel: { type: Schema.slack.types.channel_id, }, }, required: ["channel"], }, }); const helloWorldStep = HelloWorldWorkflowDef.addStep(HelloWorldFunctionDef, {}); HelloWorldWorkflowDef.addStep(Schema.slack.functions.SendMessage, { channel_id: HelloWorldWorkflowDef.inputs.channel, message: helloWorldStep.outputs.message, }); export default HelloWorldWorkflowDef; ``` `DefineWorkflow` is used to define a workflow and provide Slack with the information required to use it. `HelloWorldWorkflow.addStep` is used to add a step to the workflow; here we add the `HelloWorldFunction` and then the `SendMessage` Slack Function that will post the `message` to a Slack channel. ### Update the [manifest](https://api.slack.com/automation/manifest) ```ts // Contents of manifest.ts import { Manifest } from "deno-slack-sdk/mod.ts"; import HelloWorldWorkflow from "./workflows/hello_world.ts"; export default Manifest({ name: "my-app", description: "A Hello World app", icon: "assets/default_new_app_icon.png", workflows: [HelloWorldWorkflow], outgoingDomains: [], botScopes: ["chat:write", "chat:write.public"], }); ``` `Manifest` is used to define your apps [manifest](https://api.slack.com/automation/manifest) and provides Slack with the information required to manage it. ### Create a [trigger](https://api.slack.com/automation/triggers) ```zsh mkdir ./triggers && touch ./triggers/hello_world.ts ``` ```ts // Contents of ./triggers/hello_world.ts import { Trigger } from "deno-slack-sdk/types.ts"; import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts"; import HelloWorldWorkflow from "../workflows/hello_world.ts"; const trigger: Trigger<typeof HelloWorldWorkflow.definition> = { type: TriggerTypes.Shortcut, name: "Reverse a string", description: "Starts the workflow to reverse a string", workflow: `#/workflows/${HelloWorldWorkflow.definition.callback_id}`, inputs: { channel: { value: TriggerContextData.Shortcut.channel_id, }, }, }; export default trigger; ``` The `Trigger` object is used to define a trigger that will invoke the `HelloWorldWorkflow`. The Slack CLI will detect this file and prompt you for its creation. ## Running an app ```zsh slack run ``` When prompted, create the `triggers/hello_world.ts` trigger. This will send your trigger configuration to Slack. Post the `Hello world shortcut trigger` in a slack message and **use it** ## Getting Help [This documentation](https://api.slack.com/automation) has more information on basic and advanced concepts of the SDK. Information on how to get started developing with Deno can be found in [this documentation](https://api.slack.com/automation/deno/develop). If you get stuck, we're here to help. The following are the best ways to get assistance working through your issue: - [Issue Tracker](https://github.com/slackapi/deno-slack-sdk/issues?q=is%3Aissue) for questions, bug reports, feature requests, and general discussion. **Try searching for an existing issue before creating a new one.** - Email our developer support team: `support@slack.com` ## Contributing Contributions are more than welcome. Please look at the [contributing guidelines](https://github.com/slackapi/deno-slack-sdk/blob/main/.github/CONTRIBUTING.md) for more info! ================================================ FILE: src/datastore/datastore_test.ts ================================================ import { assertStrictEquals } from "../dev_deps.ts"; import { DefineDatastore } from "./mod.ts"; import SchemaTypes from "../schema/schema_types.ts"; import { DefineType } from "../types/mod.ts"; const customType = DefineType({ name: "custom_type", type: SchemaTypes.boolean, }); Deno.test("Datastore sets appropriate defaults", () => { const datastore = DefineDatastore({ name: "dinos", primary_key: "attr1", attributes: { attr1: { type: SchemaTypes.string, }, attr2: { type: SchemaTypes.boolean, }, attr3: { type: customType, }, attr4: { type: SchemaTypes.object, properties: { anObjectString: { type: SchemaTypes.string }, }, }, }, }); assertStrictEquals(datastore.definition.name, "dinos"); assertStrictEquals(datastore.definition.primary_key, "attr1"); const exported = datastore.export(); assertStrictEquals(exported.primary_key, "attr1"); assertStrictEquals(exported.attributes.attr1.type, SchemaTypes.string); assertStrictEquals(exported.attributes.attr2.type, SchemaTypes.boolean); assertStrictEquals(exported.attributes.attr3.type, customType); assertStrictEquals(exported.attributes.attr4.type, SchemaTypes.object); assertStrictEquals( exported.attributes.attr4.properties?.anObjectString?.type, SchemaTypes.string, ); }); ================================================ FILE: src/datastore/mod.ts ================================================ import type { SlackManifest } from "../manifest/mod.ts"; import type { ManifestDatastoreSchema } from "../manifest/manifest_schema.ts"; import type { ISlackDatastore, SlackDatastoreAttributes, SlackDatastoreDefinition, } from "./types.ts"; import { isCustomType } from "../types/mod.ts"; /** * Define a datastore and primary key and attributes for use in a Slack application. * @param {SlackDatastoreDefinition<string, SlackDatastoreAttributes, string>} definition Defines information about your datastore. * @returns {SlackDatastore} */ export const DefineDatastore = < Name extends string, Attributes extends SlackDatastoreAttributes, PrimaryKey extends keyof Attributes, TimeToLiveAttribute extends keyof Attributes, >( definition: SlackDatastoreDefinition< Name, Attributes, PrimaryKey, TimeToLiveAttribute >, ) => { return new SlackDatastore(definition); }; export class SlackDatastore< Name extends string, Attributes extends SlackDatastoreAttributes, PrimaryKey extends keyof Attributes, TimeToLiveAttribute extends keyof Attributes, > implements ISlackDatastore { public name: Name; constructor( public definition: SlackDatastoreDefinition< Name, Attributes, PrimaryKey, TimeToLiveAttribute >, ) { this.name = definition.name; } registerAttributeTypes(manifest: SlackManifest) { Object.values(this.definition.attributes).forEach((attribute) => { if (isCustomType(attribute.type)) { manifest.registerType(attribute.type); } }); } export(): ManifestDatastoreSchema { return { primary_key: this.definition.primary_key as string, time_to_live_attribute: this.definition.time_to_live_attribute as string, attributes: this.definition.attributes, }; } } ================================================ FILE: src/datastore/types.ts ================================================ import type { ICustomType } from "../types/types.ts"; import type { ManifestDatastoreSchema } from "../manifest/manifest_schema.ts"; import type { SlackManifest } from "../manifest/mod.ts"; import type { SlackPrimitiveTypes } from "../schema/slack/types/mod.ts"; import type { ValidSchemaTypes } from "../schema/schema_types.ts"; import type { ValidSlackPrimitiveTypes } from "../schema/slack/types/mod.ts"; import type { LooseStringAutocomplete } from "../type_utils.ts"; type InvalidDatastoreTypes = | typeof SlackPrimitiveTypes.blocks | typeof SlackPrimitiveTypes.oauth2; type ValidDatastoreTypes = Exclude< | ValidSchemaTypes | ValidSlackPrimitiveTypes, InvalidDatastoreTypes >; export type SlackDatastoreAttribute = { // supports custom types, primitive types, inline objects and lists type: LooseStringAutocomplete<ValidDatastoreTypes> | ICustomType; }; export type SlackDatastoreAttributes = Record<string, SlackDatastoreAttribute>; export type SlackDatastoreDefinition< Name extends string, Attributes extends SlackDatastoreAttributes, PrimaryKey extends keyof Attributes, TimeToLiveAttribute extends keyof Attributes, > = { name: Name; "primary_key": PrimaryKey; "time_to_live_attribute"?: TimeToLiveAttribute; attributes: Attributes; }; export interface ISlackDatastore { name: string; export: () => ManifestDatastoreSchema; registerAttributeTypes: (manifest: SlackManifest) => void; } export type SlackDatastoreItem<Attributes extends SlackDatastoreAttributes> = { // TODO: In the future, see if we can map the attribute.type to // the TS type map like functions do w/ parameters // deno-lint-ignore no-explicit-any [k in keyof Attributes]: any; }; export type PartialSlackDatastoreItem< Attributes extends SlackDatastoreAttributes, > = OptionalPartial<Attributes>; // deno-lint-ignore no-explicit-any type OptionalPartial<T extends any> = { // deno-lint-ignore no-explicit-any [P in keyof T]?: any; }; ================================================ FILE: src/deps.ts ================================================ export { SlackAPI } from "jsr:@slack/api@2.9.0"; export type { SlackAPIClient, Trigger } from "jsr:@slack/api@2.9.0/types"; ================================================ FILE: src/dev_deps.ts ================================================ export { assertEquals, assertExists, assertInstanceOf, AssertionError, assertMatch, assertNotStrictEquals, assertRejects, assertStrictEquals, assertStringIncludes, fail, } from "jsr:@std/assert@^1.0.0"; export * as mock from "jsr:@std/testing@^1.0.0/mock"; export { assertType as assert } from "jsr:@std/testing@^1.0.0/types"; export type { IsAny, IsExact } from "jsr:@std/testing@^1.0.0/types"; export { toPascalCase } from "jsr:@std/text@^1.0.0"; ================================================ FILE: src/events/events_test.ts ================================================ import { DefineEvent } from "./mod.ts"; import { assertEquals } from "../dev_deps.ts"; import { DefineType, Schema } from "../mod.ts"; import { isCustomType } from "../types/mod.ts"; Deno.test("DefineEvent accepts object types", () => { const TestEvent = DefineEvent({ name: "test", type: Schema.types.object, properties: {}, }); assertEquals(TestEvent.id, TestEvent.definition.name); assertEquals(typeof TestEvent.definition.type, "string"); }); Deno.test("DefineEvent accepts custom types", () => { const TestType = DefineType({ name: "test", type: Schema.types.object, properties: {}, }); const TestEvent = DefineEvent({ title: "Title", description: "Description", name: "test", type: TestType, }); assertEquals(isCustomType(TestEvent.definition.type), true); }); Deno.test("DefineEvent is properly stringified", () => { const TestEvent = DefineEvent({ name: "test", type: Schema.types.object, properties: {}, }); assertEquals(`${TestEvent}`, TestEvent.id); assertEquals(TestEvent.toJSON(), TestEvent.id); assertEquals(TestEvent.toString(), TestEvent.id); }); ================================================ FILE: src/events/mod.ts ================================================ import type { SlackManifest } from "../manifest/mod.ts"; import type { ManifestCustomEventSchema } from "../manifest/manifest_schema.ts"; import type { CustomEventDefinition, DefineEventSignature, ICustomEvent, } from "./types.ts"; import { isCustomType } from "../types/mod.ts"; import { isTypedObject } from "../parameters/mod.ts"; export const DefineEvent: DefineEventSignature = < Def extends CustomEventDefinition, >( definition: Def, ) => { return new CustomEvent(definition); }; export class CustomEvent<Def extends CustomEventDefinition> implements ICustomEvent { public id: string; public title: string | undefined; public description: string | undefined; constructor( public definition: Def, ) { this.id = definition.name; this.definition = definition; this.description = definition.description; this.title = definition.title; } private generateReferenceString() { return this.id; } toString() { return this.generateReferenceString(); } toJSON() { return this.generateReferenceString(); } registerParameterTypes(manifest: SlackManifest) { if (isCustomType(this.definition.type)) { manifest.registerType(this.definition.type); } else if (isTypedObject(this.definition)) { Object.values(this.definition.properties)?.forEach((property) => { if (isCustomType(property.type)) { manifest.registerType(property.type); } }); } } export(): ManifestCustomEventSchema { // remove name from the definition we pass to the manifest const { name: _n, ...definition } = this.definition; // Using JSON.stringify to force any custom types into their string reference return JSON.parse(JSON.stringify(definition)); } } ================================================ FILE: src/events/types.ts ================================================ import type { CustomTypeParameterDefinition, TypedObjectParameter, } from "../parameters/definition_types.ts"; import type { ManifestCustomEventSchema } from "../manifest/manifest_schema.ts"; import type { CustomEvent } from "./mod.ts"; import type { SlackManifest } from "../manifest/mod.ts"; type AcceptedEventTypes = | TypedObjectParameter | CustomTypeParameterDefinition; export type CustomEventDefinition = & { /** * The name of your event * @example my_custom_event */ name: string; } & AcceptedEventTypes; export type DefineEventSignature = { <Def extends CustomEventDefinition>(definition: Def): CustomEvent<Def>; }; export interface ICustomEvent { id: string; definition: CustomEventDefinition; description?: string; registerParameterTypes: (manifest: SlackManifest) => void; export(): ManifestCustomEventSchema; } ================================================ FILE: src/functions/definitions/connector-function.ts ================================================ import type { ManifestFunctionType } from "../../manifest/manifest_schema.ts"; import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { FunctionDefinitionArgs, ISlackFunctionDefinition, } from "../types.ts"; /** * Define a connector and its input and output parameters for use in a Slack application. * @param {FunctionDefinitionArgs<InputParameters, OutputParameters, RequiredInput, RequiredOutput>} definition Defines information about your function (title, description) as well as formalizes the input and output parameters of a connector * @returns {ConnectorFunctionDefinition} */ export const DefineConnector = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( definition: FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { return new ConnectorFunctionDefinition(definition); }; export class ConnectorFunctionDefinition< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, > implements ISlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput > { type: ManifestFunctionType = "API"; id: string; constructor( public definition: FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) { this.id = definition.callback_id; this.definition = definition; } } ================================================ FILE: src/functions/definitions/connector-function_test.ts ================================================ import { assertEquals, assertInstanceOf, assertStrictEquals, } from "../../dev_deps.ts"; import type { PossibleParameterKeys } from "../../parameters/types.ts"; import Schema from "../../schema/mod.ts"; import { ConnectorFunctionDefinition, DefineConnector, } from "./connector-function.ts"; type emptyParameterType = Record<string, never>; Deno.test("DefineConnector returns an instance of `ConnectorFunctionDefinition`", () => { const connector = DefineConnector({ callback_id: "my_connector", title: "My Connector", }); assertInstanceOf( connector, ConnectorFunctionDefinition< emptyParameterType, emptyParameterType, PossibleParameterKeys<emptyParameterType>, PossibleParameterKeys<emptyParameterType> >, ); }); Deno.test("DefineConnector sets appropriate defaults", () => { const Func = DefineConnector({ callback_id: "my_connector", title: "My Connector", }); assertEquals(Func.id, Func.definition.callback_id); assertStrictEquals(Func.type, "API"); assertEquals(Func.definition.title, "My Connector"); assertEquals(Func.definition.input_parameters, undefined); assertEquals(Func.definition.output_parameters, undefined); }); Deno.test("DefineConnector with input parameters but no output parameters", () => { const inputParameters = { properties: { aString: { type: Schema.types.string }, }, required: [], }; const NoOutputParamFunction = DefineConnector({ callback_id: "input_params_only", title: "No Parameter Function", input_parameters: inputParameters, }); assertStrictEquals( NoOutputParamFunction.definition.input_parameters, inputParameters, ); assertEquals( NoOutputParamFunction.definition.output_parameters, undefined, ); }); Deno.test("DefineConnector with output parameters but no input parameters", () => { const outputParameters = { properties: { aString: { type: Schema.types.string }, }, required: [], }; const NoInputParamFunction = DefineConnector({ callback_id: "output_params_only", title: "No Parameter Function", output_parameters: outputParameters, }); assertEquals( NoInputParamFunction.definition.input_parameters, undefined, ); assertStrictEquals( NoInputParamFunction.definition.output_parameters, outputParameters, ); }); ================================================ FILE: src/functions/definitions/mod.ts ================================================ export { DefineFunction, SlackFunctionDefinition } from "./slack-function.ts"; export { ConnectorFunctionDefinition, DefineConnector, } from "./connector-function.ts"; ================================================ FILE: src/functions/definitions/slack-function.ts ================================================ import type { ManifestFunctionSchema, ManifestFunctionType, } from "../../manifest/manifest_schema.ts"; import type { SlackManifest } from "../../mod.ts"; import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { ISlackFunctionDefinition, SlackFunctionDefinitionArgs, } from "../types.ts"; /** * Define a function and its input and output parameters for use in a Slack application. * @param {SlackFunctionDefinitionArgs<InputParameters, OutputParameters, RequiredInput, RequiredOutput>} definition Defines information about your function (title, description) as well as formalizes the input and output parameters of your function * @returns {SlackFunctionDefinition} */ export const DefineFunction = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( definition: SlackFunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { return new SlackFunctionDefinition(definition); }; export class SlackFunctionDefinition< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, > implements ISlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput > { type: ManifestFunctionType = "app"; id: string; constructor( public definition: SlackFunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) { this.id = definition.callback_id; this.definition = definition; } registerParameterTypes(manifest: SlackManifest) { const { input_parameters: inputParams, output_parameters: outputParams } = this.definition; manifest.registerTypes(inputParams?.properties ?? {}); manifest.registerTypes(outputParams?.properties ?? {}); } export(): ManifestFunctionSchema { return { title: this.definition.title, description: this.definition.description, source_file: this.definition.source_file, input_parameters: this.definition.input_parameters ?? { properties: {}, required: [] }, output_parameters: this.definition.output_parameters ?? { properties: {}, required: [] }, }; } } export function isCustomFunctionDefinition< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutputs extends PossibleParameterKeys<OutputParameters>, >( functionDefinition: ISlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutputs >, ): functionDefinition is SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutputs > { if ( functionDefinition.type === "app" && functionDefinition.export && functionDefinition.registerParameterTypes ) { return true; } return false; } ================================================ FILE: src/functions/definitions/slack-function_test.ts ================================================ import { assertEquals, assertInstanceOf, assertStrictEquals, } from "../../dev_deps.ts"; import Schema from "../../schema/mod.ts"; import type { PossibleParameterKeys } from "../../parameters/types.ts"; import { DefineFunction, isCustomFunctionDefinition, SlackFunctionDefinition, } from "./slack-function.ts"; import type { ISlackFunctionDefinition } from "../types.ts"; // TODO: Re-add tests to validate function execution when we've determined how to execute functions locally const emptyParameterObject = Object.freeze({ required: [], properties: {} }); type emptyParameterType = Record<string, never>; Deno.test("DefineFunction returns an instance of `SlackFunctionDefinition`", () => { const func = DefineFunction({ callback_id: "my_function", title: "My function", source_file: "functions/dino.ts", }); assertInstanceOf( func, SlackFunctionDefinition< emptyParameterType, emptyParameterType, PossibleParameterKeys<emptyParameterType>, PossibleParameterKeys<emptyParameterType> >, ); }); Deno.test("DefineFunction sets appropriate defaults", () => { const Func = DefineFunction({ callback_id: "my_function", title: "My function", source_file: "functions/dino.ts", }); assertEquals(Func.id, Func.definition.callback_id); assertEquals(Func.definition.title, "My function"); assertEquals(Func.definition.source_file, "functions/dino.ts"); const exportedFunc = Func.export(); assertStrictEquals(exportedFunc.source_file, "functions/dino.ts"); assertEquals(exportedFunc.input_parameters, emptyParameterObject); assertEquals(exportedFunc.output_parameters, emptyParameterObject); }); Deno.test("DefineFunction with required params", () => { const AllTypesFunction = DefineFunction({ callback_id: "my_function", title: "All Types Function", source_file: "functions/example.ts", input_parameters: { properties: { myString: { type: Schema.types.string, title: "My string", description: "a really neat value", hint: "Ex. my neat value", }, myBoolean: { type: Schema.types.boolean, title: "My boolean", hint: "Ex: true/false", }, myInteger: { type: Schema.types.integer, description: "integer", hint: "0-100", }, myNumber: { type: Schema.types.number, description: "number", }, }, required: ["myString", "myNumber"], }, output_parameters: { properties: { out: { type: Schema.types.string, }, }, required: ["out"], }, }); assertEquals(AllTypesFunction.definition.input_parameters?.required, [ "myString", "myNumber", ]); assertEquals(AllTypesFunction.definition.output_parameters?.required, [ "out", ]); assertEquals( AllTypesFunction.definition.input_parameters?.properties.myString.hint, "Ex. my neat value", ); assertEquals( AllTypesFunction.definition.input_parameters?.properties.myBoolean.hint, "Ex: true/false", ); }); Deno.test("DefineFunction without input and output parameters", () => { const NoParamFunction = DefineFunction({ callback_id: "no_params", title: "No Parameter Function", source_file: "functions/no_params.ts", }); assertEquals(emptyParameterObject, NoParamFunction.export().input_parameters); assertEquals( emptyParameterObject, NoParamFunction.export().output_parameters, ); }); Deno.test("DefineFunction with input parameters but no output parameters", () => { const inputParameters = { properties: { aString: { type: Schema.types.string }, }, required: [], }; const NoOutputParamFunction = DefineFunction({ callback_id: "input_params_only", title: "No Parameter Function", source_file: "functions/input_params_only.ts", input_parameters: inputParameters, }); NoOutputParamFunction.export(); assertStrictEquals( inputParameters, NoOutputParamFunction.definition.input_parameters, ); assertEquals( emptyParameterObject, NoOutputParamFunction.export().output_parameters, ); }); Deno.test("DefineFunction with output parameters but no input parameters", () => { const outputParameters = { properties: { aString: { type: Schema.types.string }, }, required: [], }; const NoInputParamFunction = DefineFunction({ callback_id: "output_params_only", title: "No Parameter Function", source_file: "functions/output_params_only.ts", output_parameters: outputParameters, }); assertEquals( emptyParameterObject, NoInputParamFunction.export().input_parameters, ); assertStrictEquals( outputParameters, NoInputParamFunction.definition.output_parameters, ); }); Deno.test("DefineFunction using an OAuth2 property requests a provider key", () => { /** * The `oauth2_provider_key` is not currently required because `type` supports any string * But eventually we'd like to support static errors for OAuth2 properties without the provider key */ const OAuth2Function = DefineFunction({ callback_id: "oauth", title: "OAuth Function", source_file: "functions/oauth.ts", input_parameters: { properties: { googleAccessTokenId: { type: Schema.slack.types.oauth2, oauth2_provider_key: "test", }, }, required: [], }, }); /** * TODO: Support the following test for static error // ts-expect-error `oauth2_provider_key` must be set const _IncompleteOAuth2Function = DefineFunction({ callback_id: "oauth", title: "OAuth Function", source_file: "functions/oauth.ts", input_parameters: { properties: { googleAccessTokenId: { type: Schema.slack.types.oauth2, }, }, required: [], }, }); */ assertEquals( { googleAccessTokenId: { oauth2_provider_key: "test", type: Schema.slack.types.oauth2, }, }, OAuth2Function.export().input_parameters.properties, ); }); Deno.test("DefineFunction using an OAuth2 property allows require_end_user_auth", () => { const OAuth2Function = DefineFunction({ callback_id: "oauth", title: "OAuth Function", source_file: "functions/oauth.ts", input_parameters: { properties: { googleAccessTokenId: { type: Schema.slack.types.oauth2, oauth2_provider_key: "test", require_end_user_auth: true, }, }, required: [], }, }); assertEquals( { googleAccessTokenId: { oauth2_provider_key: "test", type: Schema.slack.types.oauth2, require_end_user_auth: true, }, }, OAuth2Function.export().input_parameters.properties, ); }); Deno.test("isCustomFunctionDefinition should return true when SlackFunctionDefinition is passed", () => { const NoParamFunction = DefineFunction({ callback_id: "no_params", title: "No Parameter Function", source_file: "functions/no_params.ts", }); assertInstanceOf(NoParamFunction, SlackFunctionDefinition); assertEquals(true, isCustomFunctionDefinition(NoParamFunction)); }); Deno.test("isCustomFunctionDefinition should return false when a non custom function is passed", () => { const notCustomFunction: ISlackFunctionDefinition< emptyParameterType, emptyParameterType, PossibleParameterKeys<emptyParameterType>, PossibleParameterKeys<emptyParameterType> > = { type: "API", id: "not_custom", definition: { callback_id: "not_custom", title: "Not a custom Function", description: undefined, input_parameters: undefined, output_parameters: undefined, }, }; assertEquals(false, isCustomFunctionDefinition(notCustomFunction)); }); ================================================ FILE: src/functions/enrich-context.ts ================================================ import { SlackAPI } from "../deps.ts"; import type { BaseRuntimeFunctionContext, FunctionContextEnrichment, } from "./types.ts"; export const enrichContext = ( // deno-lint-ignore no-explicit-any context: BaseRuntimeFunctionContext<any>, ): typeof context & FunctionContextEnrichment => { const token = context.token; const slackApiUrl = (context.env || {})["SLACK_API_URL"]; const client = SlackAPI(token, { slackApiUrl: slackApiUrl ? slackApiUrl : undefined, }); return { ...context, client, }; }; ================================================ FILE: src/functions/enrich-context_test.ts ================================================ import { assertExists } from "../dev_deps.ts"; import { enrichContext } from "./enrich-context.ts"; import type { BaseRuntimeFunctionContext } from "./types.ts"; Deno.test("enrichContext with no env.SLACK_API_URL", () => { // deno-lint-ignore no-explicit-any const ctx: BaseRuntimeFunctionContext<any> = { env: {}, inputs: {}, team_id: "team", enterprise_id: "", token: "token", }; const newContext = enrichContext(ctx); assertExists(newContext.client); }); Deno.test("enrichContext with env.SLACK_API_URL", () => { // deno-lint-ignore no-explicit-any const ctx: BaseRuntimeFunctionContext<any> = { env: { "SLACK_API_URL": "https://something.slack.com/api", }, inputs: {}, team_id: "team", enterprise_id: "", token: "token", }; const newContext = enrichContext(ctx); assertExists(newContext.client); }); ================================================ FILE: src/functions/interactivity/block_actions_router.ts ================================================ import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { SlackFunctionDefinition } from "../definitions/mod.ts"; import { UnhandledEventError } from "../unhandled-event-error.ts"; import { enrichContext } from "../enrich-context.ts"; import type { FunctionDefinitionArgs, FunctionRuntimeParameters, } from "../types.ts"; import type { ActionContext, BlockActionConstraint, BlockActionHandler, RuntimeActionContext, } from "./types.ts"; import type { BlockAction } from "./block_kit_types.ts"; import { matchBasicConstraintField, normalizeConstraintToArray, } from "./matchers.ts"; /** * Define an actions "router" and its input and output parameters for use in a Slack application. The ActionsRouter will route incoming action events to action-specific handlers. * @param {SlackFunctionDefinition<InputParameters, OutputParameters, RequiredInput, RequiredOutput>} func Reference to your previously-created SlackFunction, defined via DefineFunction * @returns {ActionsRouter} */ export const BlockActionsRouter = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { const router = new ActionsRouter(func); // deno-lint-ignore no-explicit-any const exportedHandler: any = router.export(); // deno-lint-ignore no-explicit-any exportedHandler.addHandler = ((...args: any) => { router.addHandler.apply(router, args); return exportedHandler; }) as typeof router.addHandler; return exportedHandler as & ReturnType<typeof router.export> & Pick<typeof router, "addHandler">; }; export class ActionsRouter< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, > { private routes: Array< [BlockActionConstraint, BlockActionHandler<typeof this.func.definition>] >; constructor( private func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) { this.func = func; this.routes = []; } /** * Add an action handler for something that can match an action event. * @param {BlockActionConstraint} actionConstraint A BlockActionConstraintField(i.e. a string, array of strings or regular expression) or more complex BlockActionConstraintObject to match incoming block action events. A BlockActionConstraintField parameter are matched with a block action event's `action_id` property. A BlockActionConstraintObject parameter allows to match with other block action event properties like `block_id` as well as `action_id`. If multiple properties are specified using BlockActionConstraintObject, then the event must match ALL provided BlockActionConstraintObject properties. * @returns {ActionsRouter} */ addHandler( actionConstraint: BlockActionConstraint, handler: BlockActionHandler< FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput > >, ): ActionsRouter< InputParameters, OutputParameters, RequiredInput, RequiredOutput > { this.routes.push([actionConstraint, handler]); return this; } /** * Returns a method handling routing of action payloads to the appropriate action handler. * The output of export() should be attached to the `blockActions` export of your function. */ export() { return async ( context: RuntimeActionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >, ) => { const action: BlockAction = context.action; const handler = this.matchHandler(action); if (handler === null) { throw new UnhandledEventError( `Received block action payload with action=${ JSON.stringify(action) } but this app has no action handler defined to handle it!`, ); } const enrichedContext = enrichContext(context) as ActionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >; return await handler(enrichedContext); }; } /** * Return the first registered ActionHandler that matches the action ID string provided. */ matchHandler( action: BlockAction, ): | BlockActionHandler< FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput > > | null { for (let i = 0; i < this.routes.length; i++) { const route = this.routes[i]; let [constraint, handler] = route; // Handle different constraint types below if ( constraint instanceof RegExp || constraint instanceof Array || typeof constraint === "string" ) { // Normalize simple string constraints to be an array of strings for consistency in handling inside this method. constraint = normalizeConstraintToArray(constraint); // Handle the case where the constraint is either a regex or an array of strings to match against action_id if (matchBasicConstraintField(constraint, "action_id", action)) { return handler; } } else { // Assumes an object as a constraint (type BlockActionConstraintObject) // Return first match *within* any of the defined fields on the constaint object, but ensure there is a match on *all* defined fields // Effectively a logical AND across the action_id and block_id field(s) // If either of the constraint fields are not defined, pre-set them to have matched so we can effectively // ignore them when determining if we have a match by &&'ing them let actionIDMatched = constraint.action_id ? false : true; let blockIDMatched = constraint.block_id ? false : true; if (constraint.action_id) { actionIDMatched = matchBasicConstraintField( normalizeConstraintToArray(constraint.action_id), "action_id", action, ); } if (constraint.block_id) { blockIDMatched = matchBasicConstraintField( normalizeConstraintToArray(constraint.block_id), "block_id", action, ); } if (blockIDMatched && actionIDMatched) { return handler; } } } return null; } } ================================================ FILE: src/functions/interactivity/block_actions_router_test.ts ================================================ import { SlackAPI } from "../../deps.ts"; import { assertEquals, assertExists, assertRejects, mock, } from "../../dev_deps.ts"; import { BlockActionsRouter } from "./block_actions_router.ts"; import type { ActionContext } from "./types.ts"; import type { BlockAction } from "./block_kit_types.ts"; import type { FunctionParameters, FunctionRuntimeParameters, } from "../types.ts"; import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { SlackFunctionDefinition } from "../definitions/mod.ts"; import { DefineFunction, Schema } from "../../mod.ts"; // Helper test types // TODO: maybe we want to export this for userland usage at some point? // Very much a direct copy from the existing main function tester types and utilties in src/functions/tester type SlackActionHandlerTesterArgs<InputParameters extends FunctionParameters> = & Partial< ActionContext<InputParameters> > & { inputs: InputParameters; }; type CreateActionHandlerContext< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { ( args: SlackActionHandlerTesterArgs< FunctionRuntimeParameters<InputParameters, RequiredInput> >, ): ActionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >; }; type SlackActionHandlerTesterResponse< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { createContext: CreateActionHandlerContext<InputParameters, RequiredInput>; }; type SlackActionHandlerTesterSignature = { < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ): SlackActionHandlerTesterResponse< InputParameters, RequiredInput >; }; // Helper test fixtures and utilities const DEFAULT_ACTION: BlockAction = { type: "button", block_id: "block_id", action_ts: `${new Date().getTime()}`, action_id: "action_id", text: { type: "plain_text", text: "duncare", emoji: false }, style: "danger", }; const SlackActionHandlerTester: SlackActionHandlerTesterSignature = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( _func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { const createContext: CreateActionHandlerContext< InputParameters, RequiredInput > = ( args, ) => { const inputs = (args.inputs || {}) as FunctionRuntimeParameters< InputParameters, RequiredInput >; const DEFAULT_BODY = { type: "block_actions", actions: [DEFAULT_ACTION], function_data: { execution_id: "123", function: { callback_id: "456" }, inputs, }, interactivity: { interactor: { secret: "shhhh", id: "123", }, interactivity_pointer: "123.asdf", }, user: { id: "123", name: "asdf", team_id: "123", }, team: { id: "123", domain: "asdf", }, enterprise: null, is_enterprise_install: false, api_app_id: "123", token: "123", trigger_id: "123", response_url: "asdf", }; const token = args.token || "slack-function-test-token"; return { inputs, env: args.env || {}, token, client: SlackAPI(token), team_id: args.team_id || "test-team-id", enterprise_id: "", action: args.action || DEFAULT_ACTION, body: args.body || DEFAULT_BODY, }; }; return { createContext }; }; // a basic function definition and associated block action router to test const func = DefineFunction({ callback_id: "id", title: "test", source_file: "whatever", input_parameters: { properties: { garbage: { type: Schema.types.string }, }, required: ["garbage"], }, }); const { createContext } = SlackActionHandlerTester(func); const inputs = { garbage: "in, garbage out" }; const getRouter = () => { return BlockActionsRouter(func); }; Deno.test("ActionsRouter", async (t) => { await t.step( "export method returns result of handler when matching action comes in and baseline handler context parameters are present and exist", async () => { const router = getRouter(); let handlerCalled = false; router.addHandler(DEFAULT_ACTION.action_id, (ctx) => { assertExists(ctx.inputs); assertEquals<string>(ctx.inputs.garbage, inputs.garbage); assertExists(ctx.token); assertExists(ctx.action); assertExists(ctx.env); assertExists(ctx.client); handlerCalled = true; }); await router(createContext({ inputs })); assertEquals(handlerCalled, true, "action handler not called!"); }, ); }); Deno.test("ActionsRouter action matching happy path", async (t) => { await t.step("simple string matching to action_id", async () => { const router = getRouter(); let handlerCalled = false; router.addHandler(DEFAULT_ACTION.action_id, (ctx) => { assertExists(ctx.inputs); assertExists(ctx.token); assertExists(ctx.action); assertExists(ctx.env); assertExists(ctx.client); handlerCalled = true; }); await router(createContext({ inputs })); assertEquals(handlerCalled, true, "action handler not called!"); }); await t.step("array of strings matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler(["nope", DEFAULT_ACTION.action_id], handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("regex matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler(/action/, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{action_id:string} matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: DEFAULT_ACTION.action_id }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{action_id:[string]} matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler( { action_id: ["hahtrickedyou", DEFAULT_ACTION.action_id] }, handler, ); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{action_id:regex} matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: /action/ }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{block_id:string} matching to block_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: DEFAULT_ACTION.block_id }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{block_id:[string]} matching to block_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler( { block_id: ["lol", DEFAULT_ACTION.block_id] }, handler, ); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{block_id:regex} matching to block_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: /block/ }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step( "{block_id:string, action_id:string} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: DEFAULT_ACTION.block_id, action_id: DEFAULT_ACTION.action_id, }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:string, action_id:[string]} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: DEFAULT_ACTION.block_id, action_id: ["notthisoneeither", DEFAULT_ACTION.action_id], }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:string, action_id:regex} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler( { block_id: DEFAULT_ACTION.block_id, action_id: /action/ }, handler, ); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:[string], action_id:string} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: ["notthistime", DEFAULT_ACTION.block_id], action_id: DEFAULT_ACTION.action_id, }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:[string], action_id:[string]} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: ["notthistime", DEFAULT_ACTION.block_id], action_id: ["gotyougood", DEFAULT_ACTION.action_id], }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:[string], action_id:regex} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: ["notthistime", DEFAULT_ACTION.block_id], action_id: /action/, }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:regex, action_id:string} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler( { block_id: /block/, action_id: DEFAULT_ACTION.action_id }, handler, ); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:regex, action_id:[string]} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: /block/, action_id: ["hahanope", DEFAULT_ACTION.action_id], }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:regex, action_id:regex} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: /block/, action_id: /action/ }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); }); Deno.test("ActionsRouter action matching sad path", async (t) => { await t.step("unhandled action should throw", async () => { const router = getRouter(); await assertRejects(() => router(createContext({ inputs }))); }); await t.step("no false positives", async (t) => { await t.step( "not matching action_id: string", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler("nope", handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); }); await t.step( "not matching action_id: string[]", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler(["nope", "nuh uh"], handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching action_id: regex", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler(/regex/, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string}", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: "nope" }, () => handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[]}", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: ["nope", "nuh uh"] }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex}", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: /regex/ }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {block_id: string}", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: "nope" }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {block_id: string[]}", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: ["nope", "nuh uh"] }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {block_id: regex}", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ block_id: /regex/ }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: regex}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: "not good enough", block_id: /block/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: regex}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: "action_id", block_id: /noway/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: string[]}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: "not good enough", block_id: ["notthisonebut", "block_id"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: "action_id", block_id: ["this", "wont", "work"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: string}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: "not good enough", block_id: "block_id", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: "action_id", block_id: "nicetry", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: regex}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: ["not", "good", "enough"], block_id: /block/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: regex}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: ["decoy", "action_id"], block_id: /noway/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: string[]}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: ["not", "good", "enough"], block_id: ["notthisonebut", "block_id"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: ["decoy", "action_id"], block_id: ["this", "wont", "work"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: string}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: ["not", "good", "enough"], block_id: "block_id", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: ["decoy", "action_id"], block_id: "nicetry", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: regex}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: /heh/, block_id: /block/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: regex}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: /action/, block_id: /noway/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: string[]}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: /hah/, block_id: ["notthisonebut", "block_id"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: /action/, block_id: ["this", "wont", "work"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: string}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: /huh/, block_id: "block_id", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(); router.addHandler({ action_id: /action/, block_id: "nicetry", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); }); ================================================ FILE: src/functions/interactivity/block_actions_types.ts ================================================ /* This is currently a pretty light type of a block_actions payload that we can fill out once we have a way to share some of these payload types across our different libraries better Some other references that may be useful to fill this out: - https://api.slack.com/changelog/2020-09-full-state-on-view-submisson-and-block-actions - https://api.slack.com/reference/interaction-payloads/block-actions#fields - https://github.com/slackapi/bolt-js/blob/main/src/types/actions/block-action.ts - https://api.slack.com/changelog/2018-05-file-threads-soon-tread */ import type { BlockAction, BlockElement } from "./block_kit_types.ts"; /** * @description Block Actions-specific type for the `body` property of a `block_actions` event */ export type BlockActionsBody = { actions: BlockAction[]; /** * @description The encoded application ID the event was dispatched to, e.g. A123456. */ api_app_id: string; /** * @description If applicable, the channel the Block Action interaction originated from. */ channel?: { /** * @description Encoded channel ID, e.g. C123456. */ id: string; /** * @description Channel name, without the "#" prefix. */ name: string; }; /** * @description If applicable, the enterprise associated with the workspace the Block Action interaction originated from. If not applicable, will be null. */ enterprise: { /** * @description Encoded enterprise ID, e.g. E123456. */ id: string; /** * @description Enterprise name. */ name: string; } | null; /** * @description Whether this event originated from a workspace that is part of an enterprise installation. */ is_enterprise_install: boolean; /** * @description Information about the source message this Block Action interaction originated from. * This may be optional in the case that the Block Action interaction originated from a view rather than a message. */ message?: { /** * @description The encoded application ID the event was dispatched to, e.g. A123456. */ app_id: string; /** * @description The {@link https://api.slack.com/block-kit Block Kit} elements included in the message. */ blocks: BlockElement[]; /** * @description Whether the {@link https://api.slack.com/messaging/managing#threading thread} has been locked. */ is_locked: boolean; /** * @description If the {@link https://api.slack.com/messaging/managing#threading thread} has at least one reply, points to the most recent reply's `ts` value. */ latest_reply?: string; /** * @description {@link https://api.slack.com/metadata Message metadata}, if any was attached to the message. */ metadata?: { event_type: string; event_payload: { // deno-lint-ignore no-explicit-any [key: string]: any; }; }; /** * @description Total number of replies in the {@link https://api.slack.com/messaging/managing#threading thread}. */ reply_count: number; /** * @description Array of up to 5 encoded user IDs (i.e. U12345) that replied in the {@link https://api.slack.com/messaging/managing#threading thread}. */ reply_users: string[]; /** * @description Total number of users that replied in the {@link https://api.slack.com/messaging/managing#threading thread}. */ reply_users_count: number; /** * @description Encoded team ID, e.g. T123456. */ team: string; /** * @description The text in the message. If the message was composed of Block Kit elements, this property would * contain the fallback text to display in constrained UIs (like notifications) or in screenreaders. See * {@link https://api.slack.com/methods/chat.postMessage#blocks_and_attachments the API documentation for use of text, blocks and attachments in messages}. */ text: string; /** * @description Timestamp of the parent message. If the message is already a parent message, then this value will equal the `ts` value. Use this value if you want to post a message as a {@link https://api.slack.com/messaging/managing#threading threaded reply} to a particular message. */ thread_ts: string; /** * @description Timestamp of the message. */ ts: string; /** * @description The type of message. This is always "message." */ type: "message"; /** * @description Encoded user ID of the user that posted the message, e.g. U123456. */ user: string; }; /** * @description The workspace, or team, details the Block Kit interaction originated from. */ team: { /** * @description The subdomain of the team, e.g. domain.slack.com */ domain: string; /** * @description Encoded team ID, e.g. T123456. */ id: string; }; token: string; /** * @description A one-time use ID for opening modals or triggering other UI changes based on user interactions. */ trigger_id: string; /** * @description Details for the user that initiated the Block Kit action. */ user: { /** * @description Encoded user ID, e.g. U123456. */ id: string; /** * @description User's handle as seen in the Slack client when e.g. at-notifying the user. */ name: string; /** * @description The encoded team ID for the workspace, or team, where the Block Kit action originated from. */ team_id: string; }; // deno-lint-ignore no-explicit-any [key: string]: any; /* TODO: Other properties seen on this type that should be added at some point: * container: {}; // should be a reference to message or view, depending on the container for the block kit interactive componenet * message container looks like: * "container": { "type": "message", "message_ts": "1663103912.870299", "channel_id": "C03DS3P5ED6", "is_ephemeral": false }, view container looks like: container: { type: "view", view_id: "V041UDW806B" } * view: {}; // if the block kit interactive component was part of a view, details for the view are here * state: { values: {}}; // seen but usually empty? */ }; ================================================ FILE: src/functions/interactivity/block_kit_types.ts ================================================ /** * @description A single Block Kit interactive component interaction. */ export type BlockAction = & { /** * @description Identifies the Block Kit interactive component that was interacted with. */ action_id: string; } & BlockElement; /** * @description A single Block element */ export type BlockElement = { /** * @description Identifies the block within a surface. */ block_id: string; type: string; // deno-lint-ignore no-explicit-any [key: string]: any; }; ================================================ FILE: src/functions/interactivity/block_suggestion_router.ts ================================================ import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { SlackFunctionDefinition } from "../definitions/mod.ts"; import { UnhandledEventError } from "../unhandled-event-error.ts"; import { enrichContext } from "../enrich-context.ts"; import type { FunctionDefinitionArgs, FunctionRuntimeParameters, } from "../types.ts"; import type { BlockActionConstraint, BlockSuggestionHandler, RuntimeSuggestionContext, SuggestionContext, } from "./types.ts"; import type { BlockAction } from "./block_kit_types.ts"; import { matchBasicConstraintField, normalizeConstraintToArray, } from "./matchers.ts"; /** * Define a suggestion "router" and its input and output parameters for use in a Slack application. The SuggestionRouter will route incoming action events to action-specific handlers. * @param {SlackFunctionDefinition<InputParameters, OutputParameters, RequiredInput, RequiredOutput>} func Reference to your previously-created SlackFunction, defined via DefineFunction * @returns {SuggestionRouter} */ export const BlockSuggestionRouter = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { const router = new SuggestionRouter(func); // deno-lint-ignore no-explicit-any const exportedHandler: any = router.export(); // deno-lint-ignore no-explicit-any exportedHandler.addHandler = ((...args: any) => { router.addHandler.apply(router, args); return exportedHandler; }) as typeof router.addHandler; return exportedHandler as & ReturnType<typeof router.export> & Pick<typeof router, "addHandler">; }; export class SuggestionRouter< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, > { private routes: Array< [BlockActionConstraint, BlockSuggestionHandler<typeof this.func.definition>] >; constructor( private func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) { this.func = func; this.routes = []; } /** * Add a suggestion handler for something that can match an action event. * @param {BlockActionConstraint} actionConstraint A BlockActionConstraintField(i.e. a string, array of strings or regular expression) or more complex BlockActionConstraintObject to match incoming block suggestion events. A BlockActionConstraintField parameter are matched with a block suggestion event's `action_id` property. A BlockActionConstraintObject parameter allows to match with other block suggestion event properties like `block_id` as well as `action_id`. If multiple properties are specified using BlockActionConstraintObject, then the event must match ALL provided BlockActionConstraintObject properties. * @returns {SuggestionRouter} */ addHandler( actionConstraint: BlockActionConstraint, handler: BlockSuggestionHandler< FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput > >, ): SuggestionRouter< InputParameters, OutputParameters, RequiredInput, RequiredOutput > { this.routes.push([actionConstraint, handler]); return this; } /** * Returns a method handling routing of suggestion payloads to the appropriate suggestion handler. * The output of export() should be attached to the `blockSuggestion` export of your function. */ export() { return async ( context: RuntimeSuggestionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >, ) => { const suggestion = context.body; const handler = this.matchHandler(suggestion); if (handler === null) { throw new UnhandledEventError( `Received block suggestion payload with suggestion=${ JSON.stringify(suggestion) } but this app has no suggestion handler defined to handle it!`, ); } const enrichedContext = enrichContext(context) as SuggestionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >; return await handler(enrichedContext); }; } /** * Return the first registered SuggestionHandler that matches the action and/or block ID string(s) provided. */ matchHandler( action: BlockAction, // TODO: this type name is a bit misleading; BlockAction just has both action_id and block_id props on it, so it applies to both block_suggestion and block_action payloads ): | BlockSuggestionHandler< FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput > > | null { for (let i = 0; i < this.routes.length; i++) { const route = this.routes[i]; let [constraint, handler] = route; // Handle different constraint types below if ( constraint instanceof RegExp || constraint instanceof Array || typeof constraint === "string" ) { // Normalize simple string constraints to be an array of strings for consistency in handling inside this method. constraint = normalizeConstraintToArray(constraint); // Handle the case where the constraint is either a regex or an array of strings to match against action_id if (matchBasicConstraintField(constraint, "action_id", action)) { return handler; } } else { // Assumes an object as a constraint (type BlockActionConstraintObject) // Return first match *within* any of the defined fields on the constaint object, but ensure there is a match on *all* defined fields // Effectively a logical AND across the action_id and block_id field(s) // If either of the constraint fields are not defined, pre-set them to have matched so we can effectively // ignore them when determining if we have a match by &&'ing them let actionIDMatched = constraint.action_id ? false : true; let blockIDMatched = constraint.block_id ? false : true; if (constraint.action_id) { actionIDMatched = matchBasicConstraintField( normalizeConstraintToArray(constraint.action_id), "action_id", action, ); } if (constraint.block_id) { blockIDMatched = matchBasicConstraintField( normalizeConstraintToArray(constraint.block_id), "block_id", action, ); } if (blockIDMatched && actionIDMatched) { return handler; } } } return null; } } ================================================ FILE: src/functions/interactivity/block_suggestion_router_test.ts ================================================ import { SlackAPI } from "../../deps.ts"; import { assertEquals, assertExists, assertRejects, mock, } from "../../dev_deps.ts"; import { BlockSuggestionRouter } from "./block_suggestion_router.ts"; import type { SuggestionContext } from "./types.ts"; import type { FunctionParameters, FunctionRuntimeParameters, } from "../types.ts"; import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { SlackFunctionDefinition } from "../definitions/mod.ts"; import { DefineFunction, Schema } from "../../mod.ts"; // Helper test types // TODO: maybe we want to export this for userland usage at some point? // Very much a direct copy from the existing main function tester types and utilties in src/functions/tester type SlackSuggestionHandlerTesterArgs< InputParameters extends FunctionParameters, > = & Partial< SuggestionContext<InputParameters> > & { inputs: InputParameters; }; type CreateSuggestionHandlerContext< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { ( args: SlackSuggestionHandlerTesterArgs< FunctionRuntimeParameters<InputParameters, RequiredInput> >, ): SuggestionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >; }; type SlackSuggestionHandlerTesterResponse< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { createContext: CreateSuggestionHandlerContext<InputParameters, RequiredInput>; }; type SlackSuggestionHandlerTesterSignature = { < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ): SlackSuggestionHandlerTesterResponse< InputParameters, RequiredInput >; }; const DEFAULT_ACTION_ID = "action_id"; const DEFAULT_BLOCK_ID = "block_id"; // deno-lint-ignore no-explicit-any const generateSuggestion = (inputs: any) => ({ block_id: DEFAULT_BLOCK_ID, action_id: DEFAULT_ACTION_ID, value: "ohai", type: "block_suggestion", function_data: { execution_id: "123", function: { callback_id: "456" }, inputs, }, interactivity: { interactor: { secret: "shhhh", id: "123", }, interactivity_pointer: "123.asdf", }, user: { id: "123", name: "asdf", team_id: "123", }, team: { id: "123", domain: "asdf", }, enterprise: null, api_app_id: "123", }); const SlackSuggestionHandlerTester: SlackSuggestionHandlerTesterSignature = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( _func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { const createContext: CreateSuggestionHandlerContext< InputParameters, RequiredInput > = ( args, ) => { const inputs = (args.inputs || {}) as FunctionRuntimeParameters< InputParameters, RequiredInput >; const token = args.token || "slack-function-test-token"; return { inputs, env: args.env || {}, token, client: SlackAPI(token), team_id: args.team_id || "test-team-id", enterprise_id: "", body: args.body || generateSuggestion(inputs), }; }; return { createContext }; }; // a basic function definition and associated block action router to test const func = DefineFunction({ callback_id: "id", title: "test", source_file: "whatever", input_parameters: { properties: { garbage: { type: Schema.types.string }, }, required: ["garbage"], }, }); const { createContext } = SlackSuggestionHandlerTester(func); const inputs = { garbage: "in, garbage out" }; const getRouter = () => { return BlockSuggestionRouter(func); }; Deno.test("SuggestionRouter", async (t) => { await t.step( "export method returns result of handler when matching suggestion comes in and baseline handler context parameters are present and exist", async () => { const router = getRouter(); let handlerCalled = false; router.addHandler(DEFAULT_ACTION_ID, (ctx) => { assertExists(ctx.inputs); assertEquals<string>(ctx.inputs.garbage, inputs.garbage); assertExists(ctx.token); assertExists(ctx.body); assertExists(ctx.env); assertExists(ctx.client); handlerCalled = true; return { options: [] }; }); await router(createContext({ inputs })); assertEquals(handlerCalled, true, "suggestion handler not called!"); }, ); }); Deno.test("SuggestionRouter matching happy path", async (t) => { await t.step("simple string matching to action_id", async () => { const router = getRouter(); let handlerCalled = false; router.addHandler(DEFAULT_ACTION_ID, (ctx) => { assertExists(ctx.inputs); assertExists(ctx.token); assertExists(ctx.body); assertExists(ctx.env); assertExists(ctx.client); handlerCalled = true; return { options: [] }; }); await router(createContext({ inputs })); assertEquals(handlerCalled, true, "suggestion handler not called!"); }); await t.step("array of strings matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler(["nope", DEFAULT_ACTION_ID], handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("regex matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler(/action/, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{action_id:string} matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: DEFAULT_ACTION_ID }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{action_id:[string]} matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler( { action_id: ["hahtrickedyou", DEFAULT_ACTION_ID] }, handler, ); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{action_id:regex} matching to action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: /action/ }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{block_id:string} matching to block_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: DEFAULT_BLOCK_ID }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{block_id:[string]} matching to block_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler( { block_id: ["lol", DEFAULT_BLOCK_ID] }, handler, ); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("{block_id:regex} matching to block_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: /block/ }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step( "{block_id:string, action_id:string} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: DEFAULT_BLOCK_ID, action_id: DEFAULT_ACTION_ID, }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:string, action_id:[string]} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: DEFAULT_BLOCK_ID, action_id: ["notthisoneeither", DEFAULT_ACTION_ID], }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:string, action_id:regex} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler( { block_id: DEFAULT_BLOCK_ID, action_id: /action/ }, handler, ); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:[string], action_id:string} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: ["notthistime", DEFAULT_BLOCK_ID], action_id: DEFAULT_ACTION_ID, }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:[string], action_id:[string]} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: ["notthistime", DEFAULT_BLOCK_ID], action_id: ["gotyougood", DEFAULT_ACTION_ID], }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:[string], action_id:regex} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: ["notthistime", DEFAULT_BLOCK_ID], action_id: /action/, }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:regex, action_id:string} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler( { block_id: /block/, action_id: DEFAULT_ACTION_ID }, handler, ); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:regex, action_id:[string]} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: /block/, action_id: ["hahanope", DEFAULT_ACTION_ID], }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); await t.step( "{block_id:regex, action_id:regex} matching to both block_id and action_id", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: /block/, action_id: /action/ }, handler); await router(createContext({ inputs })); mock.assertSpyCalls(handler, 1); }, ); }); Deno.test("SuggestionRouter matching sad path", async (t) => { await t.step("unhandled suggestion should throw", async () => { const router = getRouter(); await assertRejects(() => router(createContext({ inputs }))); }); await t.step("no false positives", async (t) => { await t.step( "not matching action_id: string", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler("nope", handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); }); await t.step( "not matching action_id: string[]", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler(["nope", "nuh uh"], handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching action_id: regex", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler(/regex/, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string}", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: "nope" }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[]}", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: ["nope", "nuh uh"] }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex}", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: /regex/ }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {block_id: string}", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: "nope" }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {block_id: string[]}", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: ["nope", "nuh uh"] }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {block_id: regex}", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ block_id: /regex/ }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: regex}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: "not good enough", block_id: /block/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: regex}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: DEFAULT_ACTION_ID, block_id: /noway/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: string[]}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: "not good enough", block_id: ["notthisonebut", DEFAULT_BLOCK_ID], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: DEFAULT_ACTION_ID, block_id: ["this", "wont", "work"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: string}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: "not good enough", block_id: DEFAULT_BLOCK_ID, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string, block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: DEFAULT_ACTION_ID, block_id: "nicetry", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: regex}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: ["not", "good", "enough"], block_id: /block/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: regex}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: ["decoy", DEFAULT_ACTION_ID], block_id: /noway/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: string[]}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: ["not", "good", "enough"], block_id: ["notthisonebut", DEFAULT_BLOCK_ID], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: ["decoy", DEFAULT_ACTION_ID], block_id: ["this", "wont", "work"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: string}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: ["not", "good", "enough"], block_id: DEFAULT_BLOCK_ID, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: string[], block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: ["decoy", DEFAULT_ACTION_ID], block_id: "nicetry", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: regex}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: /heh/, block_id: /block/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: regex}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: /action/, block_id: /noway/, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: string[]}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: /hah/, block_id: ["notthisonebut", DEFAULT_BLOCK_ID], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: /action/, block_id: ["this", "wont", "work"], }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: string}, action_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: /huh/, block_id: DEFAULT_BLOCK_ID, }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching {action_id: regex, block_id: string}, block_id does not match", async () => { const router = getRouter(); const handler = mock.spy(() => ({ options: [] })); router.addHandler({ action_id: /action/, block_id: "nicetry", }, handler); await assertRejects(() => router(createContext({ inputs }))); mock.assertSpyCalls(handler, 0); }, ); }); ================================================ FILE: src/functions/interactivity/block_suggestion_types.ts ================================================ import type { BlockAction, BlockElement } from "./block_kit_types.ts"; // TODO: lots of duplication here with block_actions_types.ts /** * @description Block Suggestion-specific type for the `body` property of a `block_suggestion` event */ export type BlockSuggestionBody = & BlockAction // adds block_id and action_id properties & { /** * @description The encoded application ID the event was dispatched to, e.g. A123456. */ api_app_id: string; /** * @description If applicable, the channel the Block Suggestion interaction originated from. */ channel?: { /** * @description Encoded channel ID, e.g. C123456. */ id: string; /** * @description Channel name, without the "#" prefix. */ name: string; }; /** * @description If applicable, the enterprise associated with the workspace the Block Suggestion interaction originated from. If not applicable, will be null. */ enterprise: { /** * @description Encoded enterprise ID, e.g. E123456. */ id: string; /** * @description Enterprise name. */ name: string; } | null; /** * @description Information about the source message this Block Suggestion interaction originated from. * This may be optional in the case that the Block Suggestion interaction originated from a view rather than a message. */ message?: { /** * @description The encoded application ID the event was dispatched to, e.g. A123456. */ app_id: string; /** * @description The {@link https://api.slack.com/block-kit Block Kit} elements included in the message. */ blocks: BlockElement[]; /** * @description Whether the {@link https://api.slack.com/messaging/managing#threading thread} has been locked. */ is_locked: boolean; /** * @description If the {@link https://api.slack.com/messaging/managing#threading thread} has at least one reply, points to the most recent reply's `ts` value. */ latest_reply?: string; /** * @description {@link https://api.slack.com/metadata Message metadata}, if any was attached to the message. */ metadata?: { event_type: string; event_payload: { // deno-lint-ignore no-explicit-any [key: string]: any; }; }; /** * @description Total number of replies in the {@link https://api.slack.com/messaging/managing#threading thread}. */ reply_count: number; /** * @description Array of up to 5 encoded user IDs (i.e. U12345) that replied in the {@link https://api.slack.com/messaging/managing#threading thread}. */ reply_users: string[]; /** * @description Total number of users that replied in the {@link https://api.slack.com/messaging/managing#threading thread}. */ reply_users_count: number; /** * @description Encoded team ID, e.g. T123456. */ team: string; /** * @description The text in the message. If the message was composed of Block Kit elements, this property would * contain the fallback text to display in constrained UIs (like notifications) or in screenreaders. See * {@link https://api.slack.com/methods/chat.postMessage#blocks_and_attachments the API documentation for use of text, blocks and attachments in messages}. */ text: string; /** * @description Timestamp of the parent message. If the message is already a parent message, then this value will equal the `ts` value. Use this value if you want to post a message as a {@link https://api.slack.com/messaging/managing#threading threaded reply} to a particular message. */ thread_ts: string; /** * @description Timestamp of the message. */ ts: string; /** * @description The type of message. This is always "message." */ type: "message"; /** * @description Encoded user ID of the user that posted the message, e.g. U123456. */ user: string; }; /** * @description The workspace, or team, details the Block Kit interaction originated from. */ team: { /** * @description The subdomain of the team, e.g. domain.slack.com */ domain: string; /** * @description Encoded team ID, e.g. T123456. */ id: string; }; /** * @description Details for the user that initiated the Block Suggestion interaction. */ user: { /** * @description Encoded user ID, e.g. U123456. */ id: string; /** * @description User's handle as seen in the Slack client when e.g. at-notifying the user. */ name: string; /** * @description The encoded team ID for the workspace, or team, where the Block Suggestion originated from. */ team_id: string; }; /** * @description The value the user entered into the select menu. */ value: string; // deno-lint-ignore no-explicit-any [key: string]: any; /* TODO: Other properties seen on this type that should be added at some point: * container: {}; // should be a reference to message or view, depending on the container for the block kit interactive componenet * message container looks like: * "container": { "type": "message", "message_ts": "1663103912.870299", "channel_id": "C03DS3P5ED6", "is_ephemeral": false }, view container looks like: container: { type: "view", view_id: "V041UDW806B" } * view: {}; // if the block kit interactive component was part of a view, details for the view are here */ }; ================================================ FILE: src/functions/interactivity/matchers.ts ================================================ import type { BasicConstraintField, BlockActionConstraintObject, } from "./types.ts"; import type { BlockAction } from "./block_kit_types.ts"; import type { View } from "./view_types.ts"; export function normalizeConstraintToArray(constraint: BasicConstraintField) { if (typeof constraint === "string") { constraint = [constraint]; } return constraint; } export function matchBasicConstraintField( constraint: BasicConstraintField, field: keyof BlockActionConstraintObject | "callback_id", payload: BlockAction | View, ) { if (constraint instanceof RegExp) { if (payload[field].match(constraint)) { return true; } } else if (constraint instanceof Array) { for (let j = 0; j < constraint.length; j++) { const c = constraint[j]; if (payload[field] === c) { return true; } } } return false; } ================================================ FILE: src/functions/interactivity/mod.ts ================================================ export { BlockActionsRouter } from "./block_actions_router.ts"; export { ViewsRouter } from "./view_router.ts"; export { BlockSuggestionRouter } from "./block_suggestion_router.ts"; ================================================ FILE: src/functions/interactivity/types.ts ================================================ import type { BaseRuntimeFunctionContext, FunctionContextEnrichment, FunctionDefinitionArgs, FunctionParameters, FunctionRuntimeParameters, } from "../types.ts"; import type { BlockActionsBody } from "./block_actions_types.ts"; import type { BlockSuggestionBody } from "./block_suggestion_types.ts"; import type { BlockAction } from "./block_kit_types.ts"; import type { View, ViewClosedBody, ViewEvents, ViewSubmissionBody, } from "./view_types.ts"; export type BlockActionHandler<Definition> = Definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? { ( context: ActionContext<FunctionRuntimeParameters<I, RI>>, // deno-lint-ignore no-explicit-any ): Promise<any> | any; } : never; export type BlockSuggestionHandler<Definition> = Definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? { ( context: SuggestionContext<FunctionRuntimeParameters<I, RI>>, ): Promise<BlockSuggestionHandlerResponse> | BlockSuggestionHandlerResponse; } : never; type BlockSuggestionHandlerResponse = | BlockSuggestionHandlerOptionsResponse | BlockSuggestionHandlerOptionGroupsResponse; type BlockSuggestionHandlerOptionsResponse = { options: MenuOption[]; }; type BlockSuggestionHandlerOptionGroupsResponse = { option_groups: MenuOptionGroup[]; }; type MenuOptionGroup = { /** * @description A {@link PlainTextObject} that defines the label shown above this group of options. Maximum length for the `text` property inside this field is 75 characters. */ label: PlainTextObject; /** * @description An array of {@link MenuOption} objects that belong to this specific group. Maximum of 100 items. */ options: MenuOption[]; }; type MenuOption = { /** * @description A {@link PlainTextObject} that defines the text shown in the option on the menu. Maximum length for the `text` property inside this field is 75 characters. */ text: PlainTextObject; /** * @description A unique string value that will be passed to your app when this option is chosen. Maximum length for this filed is 75 characters. */ value: string; }; type PlainTextObject = { type: "plain_text"; /** * @description The text for the object. */ text: string; /** * @description Indicates whether emojis in the text field shoul be escaped into a colon emoji format. */ emoji?: boolean; }; export type ViewSubmissionHandler<Definition> = Definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? { ( context: ViewSubmissionContext<FunctionRuntimeParameters<I, RI>>, // deno-lint-ignore no-explicit-any ): Promise<any> | any; } : never; export type ViewClosedHandler<Definition> = Definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? { ( context: ViewClosedContext<FunctionRuntimeParameters<I, RI>>, // deno-lint-ignore no-explicit-any ): Promise<any> | any; } : never; export type UnhandledEventHandler<Definition> = Definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? { ( context: UnhandledEventContext<FunctionRuntimeParameters<I, RI>>, // deno-lint-ignore no-explicit-any ): Promise<any> | any; } : never; export type BaseInteractivityContext< InputParameters extends FunctionParameters, > = & BaseRuntimeFunctionContext<InputParameters> & FunctionContextEnrichment; export type ActionContext<InputParameters extends FunctionParameters> = & BaseInteractivityContext<InputParameters> & ActionSpecificContext<InputParameters>; export type SuggestionContext<InputParameters extends FunctionParameters> = & BaseInteractivityContext<InputParameters> & SuggestionSpecificContext<InputParameters>; export type ViewSubmissionContext<InputParameters extends FunctionParameters> = & BaseInteractivityContext<InputParameters> & ViewSubmissionSpecificContext<InputParameters>; export type ViewClosedContext<InputParameters extends FunctionParameters> = & BaseInteractivityContext<InputParameters> & ViewClosedSpecificContext<InputParameters>; export type UnhandledEventContext<InputParameters extends FunctionParameters> = & BaseInteractivityContext< InputParameters > & UnhandledEventSpecificContext<InputParameters>; type ActionSpecificContext<InputParameters extends FunctionParameters> = { body: BlockActionInvocationBody<InputParameters>; action: BlockAction; }; type SuggestionSpecificContext<InputParameters extends FunctionParameters> = { body: BlockSuggestionInvocationBody<InputParameters>; }; type ViewSubmissionSpecificContext<InputParameters extends FunctionParameters> = { body: ViewSubmissionInvocationBody<InputParameters>; view: View; }; type ViewClosedSpecificContext<InputParameters extends FunctionParameters> = { body: ViewClosedInvocationBody<InputParameters>; view: View; }; type UnhandledEventSpecificContext<InputParameters extends FunctionParameters> = { // unhandled events will contain at least function_data, but the rest is unknown body: & Pick<FunctionInteractivity<InputParameters>, "function_data"> & { // deno-lint-ignore no-explicit-any [key: string]: any; }; }; export type BlockActionInvocationBody< InputParameters extends FunctionParameters, > = & BlockActionsBody & FunctionInteractivity<InputParameters>; export type BlockSuggestionInvocationBody< InputParameters extends FunctionParameters, > = & BlockSuggestionBody & FunctionInteractivity<InputParameters>; export type ViewSubmissionInvocationBody< InputParameters extends FunctionParameters, > = & ViewSubmissionBody & FunctionInteractivity<InputParameters>; export type ViewClosedInvocationBody< InputParameters extends FunctionParameters, > = & ViewClosedBody & FunctionInteractivity<InputParameters>; type FunctionInteractivity<InputParameters extends FunctionParameters> = { function_data: FunctionData<InputParameters>; interactivity: Interactivity; }; type FunctionData<InputParameters extends FunctionParameters> = { function: { callback_id: string; }; execution_id: string; inputs: InputParameters; }; type Interactivity = { interactor: UserContext; interactivity_pointer: string; }; type UserContext = { secret: string; id: string; }; // TODO: with the arrival of block_suggestion payloads, the naming here is not // fully accurate: these constraint fields can apply to both block_actions and block_suggestion // payloads. Perhaps worth renaming to BlockConstraint? /** * @description An {@link BlockActionConstraintObject} or {@link BasicConstraintField} constraining which Block Kit interaction payloads get handled by particular interactivity handlers. */ export type BlockActionConstraint = | BasicConstraintField | BlockActionConstraintObject; /** * @description An object constraining which Block Kit interaction payloads get handled by particular interactivity handlers. * If both `block_id` and `action_id` properties are specified, then both properties must have their constraint satisfied in order for there to be a match. */ export type BlockActionConstraintObject = { /** * @description A {@link BasicConstraintField} to match against the `block_id` property of a Block Kit interactivity event. */ block_id?: BasicConstraintField; /** * @description A {@link BasicConstraintField} to match against the `action_id` property of a Block Kit interactivity event. */ action_id?: BasicConstraintField; }; export type ViewConstraintObject = { type: ViewEvents; callback_id: BasicConstraintField; }; export type BasicConstraintField = string | string[] | RegExp; // -- These types represent the deno-slack-runtime function handler interfaces export type RuntimeSuggestionContext< InputParameters extends FunctionParameters, > = & BaseRuntimeFunctionContext<InputParameters> & SuggestionSpecificContext<InputParameters>; export type RuntimeActionContext<InputParameters extends FunctionParameters> = & BaseRuntimeFunctionContext<InputParameters> & ActionSpecificContext<InputParameters>; export type RuntimeViewSubmissionContext< InputParameters extends FunctionParameters, > = & BaseRuntimeFunctionContext<InputParameters> & ViewSubmissionSpecificContext<InputParameters>; export type RuntimeViewClosedContext< InputParameters extends FunctionParameters, > = & BaseRuntimeFunctionContext<InputParameters> & ViewClosedSpecificContext<InputParameters>; ================================================ FILE: src/functions/interactivity/view_router.ts ================================================ import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { SlackFunctionDefinition } from "../definitions/mod.ts"; import { UnhandledEventError } from "../unhandled-event-error.ts"; import { enrichContext } from "../enrich-context.ts"; import type { FunctionDefinitionArgs, FunctionRuntimeParameters, } from "../types.ts"; import { matchBasicConstraintField, normalizeConstraintToArray, } from "./matchers.ts"; import type { BasicConstraintField, RuntimeViewClosedContext, RuntimeViewSubmissionContext, ViewClosedHandler, ViewConstraintObject, ViewSubmissionHandler, } from "./types.ts"; import type { View, ViewEvents } from "./view_types.ts"; export const ViewsRouter = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { return new ViewRouter(func); }; class ViewRouter< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, > { private closedRoutes: Array< [ ViewConstraintObject, ViewClosedHandler<typeof this.func.definition>, ] >; private submissionRoutes: Array< [ ViewConstraintObject, ViewSubmissionHandler<typeof this.func.definition>, ] >; constructor( private func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) { this.func = func; this.submissionRoutes = []; this.closedRoutes = []; // Bind these two handler functions as they're meant to exported directly by the user-defined function module this.viewClosed = this.viewClosed.bind(this); this.viewSubmission = this.viewSubmission.bind(this); } /** * Add a handler for view_closed events * @param {BasicConstraintField} viewConstraint A view constraing (i.e. a string, array of strings or regular expression) that matches against view_closed event's `callback_id` property. * @param handler A handler function for the matched view_closed event * @returns {ViewRouter} */ addClosedHandler( viewConstraint: BasicConstraintField, handler: ViewClosedHandler< FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput > >, ): ViewRouter< InputParameters, OutputParameters, RequiredInput, RequiredOutput > { const constraint: ViewConstraintObject = { type: "view_closed", callback_id: viewConstraint, }; this.closedRoutes.push([constraint, handler]); return this; } /** * Add a handler for view_submission events * @param {BasicConstraintField} viewConstraint A view constraing (i.e. a string, array of strings or regular expression) that matches against view_submission event's `callback_id` property. * @param handler A handler function for the matched view_submission event * @returns {ViewRouter} */ addSubmissionHandler( viewConstraint: BasicConstraintField, handler: ViewSubmissionHandler< FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput > >, ): ViewRouter< InputParameters, OutputParameters, RequiredInput, RequiredOutput > { const constraint: ViewConstraintObject = { type: "view_submission", callback_id: viewConstraint, }; this.submissionRoutes.push([constraint, handler]); return this; } /** * Method for handling view_closed events. This should be the `viewClosed` export of your function module. */ async viewClosed( context: RuntimeViewClosedContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >, ) { const handler = this.matchHandler(context.body.type, context.view); if (handler === null) { throw new UnhandledEventError( `Received ${context.body.type} payload ${ JSON.stringify(context.view) } but this app has no view handler defined to handle it!`, ); } const enrichedContext = enrichContext(context); return await handler(enrichedContext); } /** * Method for handling view_submission events. This should be the `viewSubmission` export of your function module. */ async viewSubmission( context: RuntimeViewSubmissionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >, ) { const handler = this.matchHandler(context.body.type, context.view); if (handler === null) { throw new UnhandledEventError( `Received ${context.body.type} payload ${ JSON.stringify(context.view) } but this app has no view handler defined to handle it!`, ); } const enrichedContext = enrichContext(context); return await handler(enrichedContext); } private matchHandler( type: ViewEvents, view: View, // deno-lint-ignore no-explicit-any ): any { let routes; let _handler: typeof type extends "view_closed" ? ViewClosedHandler< FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput > > : ViewSubmissionHandler< FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutput > >; if (type === "view_closed") { routes = this.closedRoutes; } else { routes = this.submissionRoutes; } for (let i = 0; i < routes.length; i++) { const route = routes[i]; const [constraint, _handler] = route; // Check that the view event type (submission vs. closed) matches if (constraint.type !== type) continue; // Normalize simple string constraints to be an array of strings for consistency in handling inside this method. const constraintArray = normalizeConstraintToArray( constraint.callback_id, ); // Handle the case where the constraint is either a regex or an array of strings to match against action_id if (matchBasicConstraintField(constraintArray, "callback_id", view)) { return _handler; } } return null; } } ================================================ FILE: src/functions/interactivity/view_router_test.ts ================================================ import { SlackAPI } from "../../deps.ts"; import { assertEquals, assertExists, assertRejects, mock, } from "../../dev_deps.ts"; import { ViewsRouter } from "./view_router.ts"; import type { ViewClosedContext, ViewClosedInvocationBody, ViewSubmissionContext, ViewSubmissionInvocationBody, } from "./types.ts"; import type { View } from "./view_types.ts"; import type { FunctionDefinitionArgs, FunctionParameters, FunctionRuntimeParameters, } from "../types.ts"; import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { SlackFunctionDefinition } from "../definitions/mod.ts"; import { DefineFunction, Schema } from "../../mod.ts"; // Helper test types // TODO: maybe we want to export this for userland usage at some point? // Very much a direct copy from the existing main function tester types and utilties in src/functions/tester type SlackViewSubmissionHandlerTesterArgs< InputParameters extends FunctionParameters, > = & Partial< ViewSubmissionContext<InputParameters> > & { inputs: InputParameters; }; type SlackViewClosedHandlerTesterArgs< InputParameters extends FunctionParameters, > = & Partial< ViewClosedContext<InputParameters> > & { inputs: InputParameters; }; type CreateViewSubmissionHandlerContext< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { ( args: SlackViewSubmissionHandlerTesterArgs< FunctionRuntimeParameters<InputParameters, RequiredInput> >, ): ViewSubmissionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >; }; type CreateViewClosedHandlerContext< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { ( args: SlackViewClosedHandlerTesterArgs< FunctionRuntimeParameters<InputParameters, RequiredInput> >, ): ViewClosedContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >; }; type SlackViewSubmissionHandlerTesterResponse< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { createContext: CreateViewSubmissionHandlerContext< InputParameters, RequiredInput >; }; type SlackViewClosedHandlerTesterResponse< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { createContext: CreateViewClosedHandlerContext< InputParameters, RequiredInput >; }; type SlackViewSubmissionHandlerTesterSignature = { < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ): SlackViewSubmissionHandlerTesterResponse< InputParameters, RequiredInput >; }; type SlackViewClosedHandlerTesterSignature = { < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ): SlackViewClosedHandlerTesterResponse< InputParameters, RequiredInput >; }; // Helper test fixtures and utilities const DEFAULT_VIEW: View = { type: "modal", team_id: "T123456", state: { values: {} }, notify_on_close: false, hash: "pipe", clear_on_close: false, callback_id: "123", blocks: [], app_installed_team_id: "T123456", app_id: "A123456", }; const SlackViewSubmissionHandlerTester: SlackViewSubmissionHandlerTesterSignature = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( _func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { const createContext: CreateViewSubmissionHandlerContext< InputParameters, RequiredInput > = ( args, ) => { const inputs = (args.inputs || {}) as FunctionRuntimeParameters< InputParameters, RequiredInput >; const DEFAULT_BODY = { type: "view_submission" as const, view: DEFAULT_VIEW, function_data: { execution_id: "123", function: { callback_id: "456" }, inputs, }, interactivity: { interactor: { secret: "shhhh", id: "123", }, interactivity_pointer: "123.asdf", }, user: { id: "123", name: "asdf", team_id: DEFAULT_VIEW.team_id, }, team: { id: DEFAULT_VIEW.team_id, domain: "asdf", }, enterprise: null, is_enterprise_install: false, api_app_id: DEFAULT_VIEW.app_id, app_id: DEFAULT_VIEW.app_id, token: "123", response_urls: [], trigger_id: "12345", }; const token = args.token || "slack-function-test-token"; return { inputs, env: args.env || {}, token, client: SlackAPI(token), view: args.view || DEFAULT_VIEW, body: args.body || DEFAULT_BODY, team_id: DEFAULT_VIEW.team_id, enterprise_id: "", }; }; return { createContext }; }; const SlackViewClosedHandlerTester: SlackViewClosedHandlerTesterSignature = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( _func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { const createContext: CreateViewClosedHandlerContext< InputParameters, RequiredInput > = ( args, ) => { const inputs = (args.inputs || {}) as FunctionRuntimeParameters< InputParameters, RequiredInput >; const DEFAULT_BODY = { type: "view_closed" as const, view: DEFAULT_VIEW, function_data: { execution_id: "123", function: { callback_id: "456" }, inputs, }, interactivity: { interactor: { secret: "shhhh", id: "123", }, interactivity_pointer: "123.asdf", }, user: { id: "123", name: "asdf", team_id: DEFAULT_VIEW.team_id, }, team: { id: DEFAULT_VIEW.team_id, domain: "asdf", }, enterprise: null, is_enterprise_install: false, api_app_id: DEFAULT_VIEW.app_id, app_id: DEFAULT_VIEW.app_id, token: "123", is_cleared: false, }; const token = args.token || "slack-function-test-token"; return { inputs, env: args.env || {}, token, client: SlackAPI(token), view: args.view || DEFAULT_VIEW, body: args.body || DEFAULT_BODY, team_id: DEFAULT_VIEW.team_id, enterprise_id: "", }; }; return { createContext }; }; // a basic function definition and associated block action router to test const func = DefineFunction({ callback_id: "id", title: "test", source_file: "whatever", input_parameters: { properties: { garbage: { type: Schema.types.string }, }, required: ["garbage"], }, }); const { createContext: createSubmissionContext } = SlackViewSubmissionHandlerTester(func); const { createContext: createClosedContext } = SlackViewClosedHandlerTester( func, ); // Dummy object to be able to programmatically reference the identifiers const inputs = { garbage: "in, garbage out" }; const getRouter = () => ViewsRouter(func); Deno.test("ViewRouter viewSubmission", async (t) => { await t.step( "export method returns result of submissionHandler when matching view comes in and baseline handler context parameters are present and exist", async () => { const router = getRouter(); let handlerCalled = false; router.addSubmissionHandler(DEFAULT_VIEW.callback_id, (ctx) => { assertExists(ctx.inputs); assertEquals<string>(ctx.inputs.garbage, inputs.garbage); assertExists(ctx.token); assertExists(ctx.client); assertExists<View>(ctx.view); assertExists< ViewSubmissionInvocationBody< typeof func.definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? FunctionRuntimeParameters<I, RI> : never > >(ctx.body); assertExists(ctx.env); handlerCalled = true; }); await router.viewSubmission(createSubmissionContext({ inputs })); assertEquals(handlerCalled, true, "view handler not called!"); }, ); }); Deno.test("ViewRouter viewSubmission happy path", async (t) => { await t.step("simple string matching to callback_id", async () => { const router = getRouter(); let handlerCalled = false; router.addSubmissionHandler(DEFAULT_VIEW.callback_id, (ctx) => { assertExists(ctx.inputs); assertExists<string>(ctx.token); assertExists<View>(ctx.view); assertExists(ctx.env); assertExists(ctx.client); handlerCalled = true; }); await router.viewSubmission(createSubmissionContext({ inputs })); assertEquals(handlerCalled, true, "view handler not called!"); }); await t.step("array of strings matching to callback_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addSubmissionHandler( ["nope", DEFAULT_VIEW.callback_id], handler, ); await router.viewSubmission(createSubmissionContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("regex matching to callback_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addSubmissionHandler(/12/, handler); await router.viewSubmission(createSubmissionContext({ inputs })); mock.assertSpyCalls(handler, 1); }); }); Deno.test("ViewRouter viewSubmission sad path", async (t) => { await t.step( "unhandled view_submission throw", async () => { const router = getRouter(); await assertRejects( () => router.viewSubmission(createSubmissionContext({ inputs })), "no view handler defined", ); }, ); await t.step("no false positives", async (t) => { await t.step( "not matching callback_id: string", async () => { const router = getRouter(); const handler = mock.spy(); router.addSubmissionHandler("nope", handler); await assertRejects(() => router.viewSubmission(createSubmissionContext({ inputs })) ); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching callback_id: string[]", async () => { const router = getRouter(); const handler = mock.spy(); router.addSubmissionHandler(["nope", "nuh uh"], handler); await assertRejects(() => router.viewSubmission(createSubmissionContext({ inputs })) ); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching callback_id: regex", async () => { const router = getRouter(); const handler = mock.spy(); router.addSubmissionHandler(/regex/, handler); await assertRejects(() => router.viewSubmission(createSubmissionContext({ inputs })) ); mock.assertSpyCalls(handler, 0); }, ); }); }); Deno.test("ViewRouter viewClosed", async (t) => { await t.step( "export method returns result of closedHandler when matching view comes in and baseline handler context parameters are present and exist", async () => { const router = getRouter(); let handlerCalled = false; router.addClosedHandler(DEFAULT_VIEW.callback_id, (ctx) => { assertExists(ctx.inputs); assertEquals<string>(ctx.inputs.garbage, inputs.garbage); assertExists(ctx.token); assertExists(ctx.client); assertExists<View>(ctx.view); assertExists< ViewClosedInvocationBody< typeof func.definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? FunctionRuntimeParameters<I, RI> : never > >(ctx.body); assertExists(ctx.env); handlerCalled = true; }); await router.viewClosed(createClosedContext({ inputs })); assertEquals(handlerCalled, true, "view handler not called!"); }, ); }); Deno.test("ViewRouter viewClosed happy path", async (t) => { await t.step("simple string matching to callback_id", async () => { const router = getRouter(); let handlerCalled = false; router.addClosedHandler(DEFAULT_VIEW.callback_id, (ctx) => { assertExists(ctx.inputs); assertExists<string>(ctx.token); assertExists(ctx.client); assertExists<View>(ctx.view); assertExists(ctx.env); handlerCalled = true; }); await router.viewClosed(createClosedContext({ inputs })); assertEquals(handlerCalled, true, "view handler not called!"); }); await t.step("array of strings matching to callback_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addClosedHandler( ["nope", DEFAULT_VIEW.callback_id], handler, ); await router.viewClosed(createClosedContext({ inputs })); mock.assertSpyCalls(handler, 1); }); await t.step("regex matching to callback_id", async () => { const router = getRouter(); const handler = mock.spy(); router.addClosedHandler(/12/, handler); await router.viewClosed(createClosedContext({ inputs })); mock.assertSpyCalls(handler, 1); }); }); Deno.test("ViewRouter viewClosed sad path", async (t) => { await t.step( "unhandled view_submission should throw", async () => { const router = getRouter(); await assertRejects( () => router.viewClosed(createClosedContext({ inputs })), "no view handler defined", ); }, ); await t.step("no false positives", async (t) => { await t.step( "not matching callback_id: string", async () => { const router = getRouter(); const handler = mock.spy(); router.addClosedHandler("nope", handler); await assertRejects(() => router.viewClosed(createClosedContext({ inputs })) ); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching callback_id: string[]", async () => { const router = getRouter(); const handler = mock.spy(); router.addClosedHandler(["nope", "nuh uh"], handler); await assertRejects(() => router.viewClosed(createClosedContext({ inputs })) ); mock.assertSpyCalls(handler, 0); }, ); await t.step( "not matching callback_id: regex", async () => { const router = getRouter(); const handler = mock.spy(); router.addClosedHandler(/regex/, handler); await assertRejects(() => router.viewClosed(createClosedContext({ inputs })) ); mock.assertSpyCalls(handler, 0); }, ); }); }); ================================================ FILE: src/functions/interactivity/view_types.ts ================================================ /* Possibly helpful links: - https://github.com/slackapi/bolt-js/blob/main/src/types/view/index.ts - https://api.slack.com/reference/interaction-payloads/views - https://api.slack.com/reference/surfaces/views */ import type { BlockElement } from "./block_kit_types.ts"; export type ViewEvents = "view_submission" | "view_closed"; /** * @description Common `body` type for both `view_submission` and `view_closed` events. */ export type BaseViewBody = { /** * @description The encoded application ID the view event was dispatched to, e.g. A123456. */ api_app_id: string; /** * @description If applicable, the enterprise associated with the workspace the Block Action interaction originated from. If not applicable, will be null. */ enterprise: { /** * @description Encoded enterprise ID, e.g. E123456. */ id: string; /** * @description Enterprise name. */ name: string; } | null; /** * @description Whether this event originated from a workspace that is part of an enterprise installation. */ is_enterprise_install: boolean; /** * @description The workspace, or team, details the Block Kit interaction originated from. */ team: { /** * @description The subdomain of the team, e.g. domain.slack.com */ domain: string; /** * @description Encoded team ID, e.g. T123456. */ id: string; }; token: string; type: string; /** * @description Details for the user that initiated the Block Kit action. */ user: { /** * @description Encoded user ID, e.g. U123456. */ id: string; /** * @description User's handle as seen in the Slack client when e.g. at-notifying the user. name: string; /** * @description The encoded team ID for the workspace, or team, where the Block Kit action originated from. */ team_id: string; }; /** * @description The source view of the modal interacted with. */ view: View; // deno-lint-ignore no-explicit-any [key: string]: any; }; /** * @description `body` type for `view_submission` event. */ export type ViewSubmissionBody = & BaseViewBody & { /** * @description Used to open a modal by passing it to e.g. `view.open` or `view.push` APIs. Represents a particular user interaction with an interactive component. Short-lived token (expires fast!) that may only be used once. */ trigger_id: string; type: "view_submission"; }; /** * @description `body` type for `view_closed` event. */ export type ViewClosedBody = & BaseViewBody & { /** * @description Whether or not an entire view stack was cleared. */ is_cleared: boolean; type: "view_closed"; }; /** * @description The source view of the modal interacted with. */ export type View = { /** * @description The encoded application ID the view event was dispatched to, e.g. A123456. */ app_id: string; /** * @description The encoded team ID for the workspace, or team, of the View. */ app_installed_team_id: string; /** * @description The Block elements composing the view. */ blocks: BlockElement[]; // bot_id: string; /** * @description An identifier to recognize interactions and submissions of this particular view. Don't use this to store sensitive information (use `private_metadata` instead). Max length of 255 characters. */ callback_id: string; /** * @description Whether clicking on the close button clears all views in this modal and closes it.. */ clear_on_close: boolean; // close: any; // external_id: string; /** * @description A unique value which is optionally accepted in `views.update` and `views.publish` API calls. When provided to those APIs, the `hash` is validated such that only the most recent view can be updated. This should be used to ensure the correct view is being updated when updates are happening asynchronously. */ hash: string; // id: string; /** * @description Indicates whether Slack sends your function a `view_closed` event when a user clicks the close button in this view. */ notify_on_close: boolean; // previous_view_id: any; /** * @description An optional string that will be sent to your app in `view_submission` and `view_closed` events. Max length of 3000 characters. */ private_metadata?: string; // root_view_id: string; /** * @description Object representing view state. */ state: { /** * @description A dictionary of objects. Each object represents a block in the view that contained stateful, interactive components. Objects are keyed by the `block_id` of those blocks. These objects each contain a child object. The child object is keyed by the `action_id` of the interactive element in the block. This final child object will contain the type and submitted value of the input block element. */ values: { // deno-lint-ignore no-explicit-any [key: string]: any; }; }; // submit?: any; /** * @description The encoded team ID for the workspace, or team, of the View. */ team_id: string; // title: any; type: "modal"; // deno-lint-ignore no-explicit-any [key: string]: any; }; ================================================ FILE: src/functions/mod.ts ================================================ export { DefineConnector, DefineFunction } from "./definitions/mod.ts"; ================================================ FILE: src/functions/slack-function.ts ================================================ import type { ParameterSetDefinition, PossibleParameterKeys, } from "../parameters/types.ts"; import type { EnrichedSlackFunctionHandler, RuntimeFunctionContext, RuntimeUnhandledEventContext, SlackFunctionType, } from "./types.ts"; import type { SlackFunctionDefinition } from "./definitions/mod.ts"; import { enrichContext } from "./enrich-context.ts"; import { BlockActionsRouter } from "./interactivity/block_actions_router.ts"; import { BlockSuggestionRouter } from "./interactivity/block_suggestion_router.ts"; import { ViewsRouter } from "./interactivity/view_router.ts"; export const SlackFunction = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( func: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, functionHandler: EnrichedSlackFunctionHandler<typeof func.definition>, ) => { // Start with the provided fn handler, and we'll wrap it up so we can append some additional functions to it // Wrap the provided handler's call so we can add additional context // deno-lint-ignore no-explicit-any const handlerModule: any = ( ctx: RuntimeFunctionContext<InputParameters>, // deno-lint-ignore no-explicit-any ...args: any ) => { // enrich the context w/ additional properties const newContext = enrichContext(ctx); //@ts-expect-error - intentionally specifying the provided functionHandler as the `this` arg for the handler's call return functionHandler.apply(functionHandler, [newContext, ...args]); }; // Unhandled events are sent to a single handler, which is not set by default handlerModule.unhandledEvent = undefined; // Create routers for block/view actions // TODO: we could probably lazily create these when corresponding add* functions are called const blockActionsRouter = BlockActionsRouter(func); const blockSuggestionRouter = BlockSuggestionRouter(func); const viewsRouter = ViewsRouter(func); // Add fns for additional function handlers // deno-lint-ignore no-explicit-any handlerModule.addBlockActionsHandler = (...args: any) => { blockActionsRouter.addHandler.apply(blockActionsRouter, args); return handlerModule; }; // deno-lint-ignore no-explicit-any handlerModule.addBlockSuggestionHandler = (...args: any) => { blockSuggestionRouter.addHandler.apply(blockSuggestionRouter, args); return handlerModule; }; // deno-lint-ignore no-explicit-any handlerModule.addViewClosedHandler = (...args: any) => { viewsRouter.addClosedHandler.apply(viewsRouter, args); return handlerModule; }; // deno-lint-ignore no-explicit-any handlerModule.addViewSubmissionHandler = (...args: any) => { viewsRouter.addSubmissionHandler.apply(viewsRouter, args); return handlerModule; }; // deno-lint-ignore no-explicit-any handlerModule.addUnhandledEventHandler = (handler: any) => { // Set the unhandledEvent property directly handlerModule.unhandledEvent = ( ctx: RuntimeUnhandledEventContext<InputParameters>, // deno-lint-ignore no-explicit-any ...args: any ) => { const newContext = enrichContext(ctx); return handler.apply(handler, [newContext, ...args]); }; return handlerModule; }; // Expose named handlers that the deno-slack-runtime will invoke handlerModule.blockActions = blockActionsRouter; handlerModule.blockSuggestion = blockSuggestionRouter; handlerModule.viewClosed = viewsRouter.viewClosed; handlerModule.viewSubmission = viewsRouter.viewSubmission; return handlerModule as SlackFunctionType<typeof func.definition>; }; ================================================ FILE: src/functions/slack-function_test.ts ================================================ import { assertEquals, assertExists, mock } from "../dev_deps.ts"; import { DefineFunction, SlackFunction } from "../mod.ts"; const TestFunction = DefineFunction({ title: "Test", callback_id: "test", source_file: "test.js", }); Deno.test("SlackFunction returns the expected interface", () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); assertEquals(typeof handlers.addBlockActionsHandler, "function"); assertEquals(typeof handlers.addViewClosedHandler, "function"); assertEquals(typeof handlers.addViewSubmissionHandler, "function"); assertEquals(typeof handlers.addUnhandledEventHandler, "function"); }); Deno.test("SlackFunction returns a proper function module definition", () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); assertEquals(typeof handlers, "function"); assertEquals(typeof typedHandlers.blockActions, "function"); assertEquals(typeof typedHandlers.viewSubmission, "function"); assertEquals(typeof typedHandlers.viewClosed, "function"); }); Deno.test("SlackFunction unhandledEvent is undefined by default", () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); assertEquals(typedHandlers.unhandledEvent, undefined); }); Deno.test("SlackFunction unhandledEvent is defined after calling addUnhandledEventHandler", () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); const handlerSpy = mock.spy(); typedHandlers.addUnhandledEventHandler(handlerSpy); assertEquals(typeof typedHandlers.unhandledEvent, "function"); }); Deno.test("Main handler should pass arguments through", () => { const args = { test: "arguments" }; // deno-lint-ignore no-explicit-any const mainFnHandler = mock.spy((ctx: any) => { assertEquals(ctx.test, args.test); assertExists(ctx.client); return { outputs: {} }; }); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); typedHandlers(args); mock.assertSpyCalls(mainFnHandler, 1); }); Deno.test("Main handler should have a client instance", () => { const args = { test: "arguments" }; // deno-lint-ignore no-explicit-any const mainFnHandler = mock.spy((ctx: any) => { assertExists(ctx.client); return { outputs: {} }; }); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); typedHandlers(args); mock.assertSpyCalls(mainFnHandler, 1); }); Deno.test("addBlockActionsHandler", async () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); const actionId = "whatever"; const actionSpy = mock.spy(); typedHandlers.addBlockActionsHandler(actionId, actionSpy); await typedHandlers.blockActions({ action: { action_id: actionId } }); mock.assertSpyCalls(actionSpy, 1); }); Deno.test("addBlockSuggestionHandler", async () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); const actionId = "whatever"; const suggestionSpy = mock.spy(() => ({ options: [] })); typedHandlers.addBlockSuggestionHandler(actionId, suggestionSpy); await typedHandlers.blockSuggestion({ body: { action_id: actionId } }); mock.assertSpyCalls(suggestionSpy, 1); }); Deno.test("addViewClosedHandler", async () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); const callbackId = "lolwutwut"; const closedHandler = mock.spy(); typedHandlers.addViewClosedHandler(callbackId, closedHandler); await typedHandlers.viewClosed({ body: { type: "view_closed" }, view: { callback_id: callbackId }, }); mock.assertSpyCalls(closedHandler, 1); }); Deno.test("addViewSubmissionHandler", async () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); const callbackId = "lolwutwut"; const submissionSpy = mock.spy(); typedHandlers.addViewSubmissionHandler(callbackId, submissionSpy); await typedHandlers.viewSubmission({ body: { type: "view_submission" }, view: { callback_id: callbackId }, }); mock.assertSpyCalls(submissionSpy, 1); }); Deno.test("addUnhandledEventHandler", async () => { const mainFnHandler = mock.spy(() => ({ outputs: {} })); const handlers = SlackFunction(TestFunction, mainFnHandler); const typedHandlers = typeHandlersForTesting(handlers); // deno-lint-ignore no-explicit-any const handlerSpy = mock.spy((ctx: any) => { assertEquals(ctx.some, args.some); assertExists(ctx.client); return { outputs: {} }; }); typedHandlers.addUnhandledEventHandler(handlerSpy); const args = { some: "arguments" }; await typedHandlers.unhandledEvent(args); }); const typeHandlersForTesting = <HandlerType>(handlers: HandlerType) => { return handlers as HandlerType & { (...args: unknown[]): unknown; blockActions: (...args: unknown[]) => void; blockSuggestion: (...args: unknown[]) => void; viewSubmission: (...args: unknown[]) => void; viewClosed: (...args: unknown[]) => void; unhandledEvent: (...args: unknown[]) => void; }; }; ================================================ FILE: src/functions/tester/function_tester_test.ts ================================================ import { assertEquals, mock } from "../../dev_deps.ts"; import { DEFAULT_FUNCTION_TESTER_TITLE, SlackFunctionTester } from "./mod.ts"; import { DefineFunction } from "../mod.ts"; import { Schema, SlackFunction } from "../../mod.ts"; Deno.test("SlackFunctionTester.createContext using a string for callback_id", () => { const callbackId = "my_callback_id"; const { createContext } = SlackFunctionTester(callbackId); const inputs = { myValue: "some value", }; const ctxWithInputs = createContext({ inputs, }); const ctxWithoutInputs = createContext({ inputs: {} }); assertEquals(ctxWithInputs.inputs, inputs); assertEquals(ctxWithInputs.env, {}); assertEquals(typeof ctxWithInputs.token, "string"); assertEquals(ctxWithInputs.event.type, "function_executed"); assertEquals(ctxWithInputs.event.function.callback_id, callbackId); assertEquals( ctxWithInputs.event.function.title, DEFAULT_FUNCTION_TESTER_TITLE, ); assertEquals(ctxWithoutInputs.inputs, {}); assertEquals(ctxWithoutInputs.event.function.callback_id, callbackId); }); Deno.test("SlackFunctionTester.createContext using function definitions", () => { const callbackId = "my_callback_id"; const TestFunction = DefineFunction({ callback_id: callbackId, source_file: "test", title: "Test", input_parameters: { properties: { myValue: { type: Schema.types.string }, myOptionalValue: { type: Schema.types.boolean }, }, required: ["myValue"], }, }); const { createContext } = SlackFunctionTester(TestFunction); const requiredCtx = createContext({ inputs: { myValue: "some value" }, }); const optionalCtx = createContext({ inputs: { myValue: "some value", myOptionalValue: true }, }); assertEquals(requiredCtx.inputs, { myValue: "some value" }); assertEquals(requiredCtx.env, {}); assertEquals(typeof requiredCtx.token, "string"); assertEquals(requiredCtx.event.type, "function_executed"); assertEquals(requiredCtx.event.function.callback_id, callbackId); assertEquals(requiredCtx.event.function.title, TestFunction.definition.title); assertEquals(optionalCtx.inputs, { myValue: "some value", myOptionalValue: true, }); }); Deno.test("SlackFunctionTester.createContext with empty inputs", () => { const callbackId = "my_callback_id"; const { createContext } = SlackFunctionTester(callbackId); const ctx = createContext({ inputs: {} }); assertEquals(ctx.inputs, {}); assertEquals(ctx.env, {}); assertEquals(typeof ctx.token, "string"); assertEquals(ctx.event.type, "function_executed"); assertEquals(ctx.event.function.callback_id, callbackId); }); Deno.test("SlackFunctionTester with a SlackFunction()", async () => { const callbackId = "my_callback_id"; const outputValue = "an-output-value"; const { createContext } = SlackFunctionTester(callbackId); const TestFunction = DefineFunction({ callback_id: callbackId, source_file: "test", title: "Test", input_parameters: { properties: { myValue: { type: Schema.types.string }, myOptionalValue: { type: Schema.types.boolean }, }, required: ["myValue"], }, output_parameters: { properties: { myOutput: { type: Schema.types.string }, }, required: ["myOutput"], }, }); const handlerSpy = mock.spy(() => { return { outputs: { myOutput: outputValue } }; }); const handler = SlackFunction(TestFunction, handlerSpy); const ctx = createContext({ inputs: { myValue: "test" } }); const resp = await handler(ctx); assertEquals(resp.outputs?.myOutput, outputValue); }); ================================================ FILE: src/functions/tester/mod.ts ================================================ import { SlackAPI } from "../../deps.ts"; import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { SlackFunctionDefinition } from "../definitions/mod.ts"; import type { FunctionRuntimeParameters } from "../types.ts"; import type { CreateFunctionContext, SlackFunctionTesterSignature, } from "./types.ts"; export const DEFAULT_FUNCTION_TESTER_TITLE = "Function Test Title"; export const SlackFunctionTester: SlackFunctionTesterSignature = < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( funcOrCallbackId: | string | SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ) => { const now = new Date(); const testFnID = `fn${now.getTime()}`; let testFnCallbackID: string; let testFnTitle: string; if (typeof funcOrCallbackId === "string") { testFnCallbackID = funcOrCallbackId; testFnTitle = DEFAULT_FUNCTION_TESTER_TITLE; } else { testFnCallbackID = funcOrCallbackId.definition.callback_id; testFnTitle = funcOrCallbackId.definition.title; } const createContext: CreateFunctionContext<InputParameters, RequiredInput> = ( args, ) => { const ts = new Date(); const token = args.token || "slack-function-test-token"; // TODO: can we reuse some of our existing types for modeling payloads to ensure this structure doesnt become out of date? return { inputs: (args.inputs || {}) as FunctionRuntimeParameters< InputParameters, RequiredInput >, env: args.env || {}, token, client: SlackAPI(token), team_id: args.team_id || "test-team-id", enterprise_id: args.enterprise_id || "test-enterprise-id", event: args.event || { type: "function_executed", event_ts: `${ts.getTime()}`, function_execution_id: `fx${ts.getTime()}`, inputs: args.inputs as Record<string, unknown>, function: { id: testFnID, callback_id: testFnCallbackID, title: testFnTitle, }, }, }; }; return { createContext }; }; ================================================ FILE: src/functions/tester/types.ts ================================================ import type { ParameterSetDefinition, PossibleParameterKeys, } from "../../parameters/types.ts"; import type { SlackFunctionDefinition } from "../definitions/mod.ts"; import type { FunctionContext, FunctionParameters, FunctionRuntimeParameters, } from "../types.ts"; export type SlackFunctionTesterArgs< InputParameters extends FunctionParameters, > = & Partial< FunctionContext<InputParameters> > & { inputs: InputParameters; }; export type CreateFunctionContext< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { ( args: SlackFunctionTesterArgs< FunctionRuntimeParameters<InputParameters, RequiredInput> | undefined >, ): FunctionContext< FunctionRuntimeParameters<InputParameters, RequiredInput> >; }; export type SlackFunctionTesterResponse< InputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, > = { createContext: CreateFunctionContext<InputParameters, RequiredInput>; }; // This type is overloaded to accept either a string or a SlackFunction export type SlackFunctionTesterSignature = { < InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutput extends PossibleParameterKeys<OutputParameters>, >( funcOrCallbackId: SlackFunctionDefinition< InputParameters, OutputParameters, RequiredInput, RequiredOutput >, ): SlackFunctionTesterResponse< InputParameters, RequiredInput >; // Accept a string (funcOrCallbackId: string): { createContext: { <I extends FunctionParameters>( args: SlackFunctionTesterArgs<I>, ): FunctionContext<I>; }; }; }; ================================================ FILE: src/functions/types.ts ================================================ import type { SlackAPIClient } from "../deps.ts"; import type { Env } from "../types.ts"; import type { ManifestFunctionSchema, ManifestFunctionType, } from "../manifest/manifest_schema.ts"; import type { ParameterPropertiesDefinition, ParameterSetDefinition, PossibleParameterKeys, } from "../parameters/types.ts"; import type { CustomTypeParameterDefinition, ParameterDefinition, TypedArrayParameterDefinition, TypedObjectParameterDefinition, TypedObjectProperties, TypedObjectRequiredProperties, } from "../parameters/definition_types.ts"; import type SchemaTypes from "../schema/schema_types.ts"; import type SlackSchemaTypes from "../schema/slack/schema_types.ts"; import type { SlackManifest } from "../manifest/mod.ts"; import type { BasicConstraintField, BlockActionConstraint, BlockActionHandler, BlockSuggestionHandler, UnhandledEventHandler, ViewClosedHandler, ViewSubmissionHandler, } from "./interactivity/types.ts"; import type { ICustomType } from "../types/types.ts"; import type { IncreaseDepth, MaxRecursionDepth, RecursionDepthLevel, } from "../type_utils.ts"; export type { BlockActionHandler } from "./interactivity/types.ts"; export type FunctionInvocationBody = { "team_id": string; "api_app_id": string; type: "event_callback"; "event_id": string; event: { type: "function_executed"; function: { id: string; "callback_id": string; title: string; description?: string; }; "function_execution_id": string; inputs?: Record<string, unknown>; "event_ts": string; }; }; /** * @description Maps a ParameterDefinition into a runtime type, i.e. "string" === string. */ type FunctionInputRuntimeType< Param extends ParameterDefinition, CurrentDepth extends RecursionDepthLevel = 0, > = // Recurse through Custom Types, stop when we hit our max depth CurrentDepth extends MaxRecursionDepth ? UnknownRuntimeType : Param["type"] extends ICustomType ? Param extends CustomTypeParameterDefinition ? FunctionInputRuntimeType< Param["type"]["definition"], IncreaseDepth<CurrentDepth> > : UnknownRuntimeType : Param["type"] extends | typeof SchemaTypes.string | typeof SlackSchemaTypes.user_id | typeof SlackSchemaTypes.usergroup_id | typeof SlackSchemaTypes.channel_id | typeof SlackSchemaTypes.canvas_id | typeof SlackSchemaTypes.canvas_template_id | typeof SlackSchemaTypes.date | typeof SlackSchemaTypes.salesforce_record_id | typeof SlackSchemaTypes.message_ts ? string : Param["type"] extends | typeof SchemaTypes.integer | typeof SchemaTypes.number | typeof SlackSchemaTypes.timestamp ? number : Param["type"] extends typeof SchemaTypes.boolean ? boolean : Param["type"] extends typeof SchemaTypes.array ? Param extends TypedArrayParameterDefinition ? TypedArrayFunctionInputRuntimeType<Param> : UnknownRuntimeType[] : Param["type"] extends typeof SchemaTypes.object ? Param extends TypedObjectParameterDefinition< infer P, infer RP > ? TypedObjectFunctionInputRuntimeType<P, RP, Param> : UnknownRuntimeType : Param["type"] extends | typeof SlackSchemaTypes.rich_text | typeof SlackSchemaTypes.expanded_rich_text ? UnknownRuntimeType : UnknownRuntimeType; // deno-lint-ignore no-explicit-any type UnknownRuntimeType = any; type TypedObjectFunctionInputRuntimeType< Props extends TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props>, Param extends TypedObjectParameterDefinition<Props, RequiredProps>, > = & { [prop in keyof Props]?: FunctionInputRuntimeType< Props[prop] >; } & (RequiredProps extends Array<keyof Props> ? { [prop in RequiredProps[number]]: FunctionInputRuntimeType< Props[prop] >; } : Record<never, never>) & (Param["additionalProperties"] extends false ? Record<never, never> : { [key: string]: UnknownRuntimeType; }); type TypedArrayFunctionInputRuntimeType< Param extends TypedArrayParameterDefinition, > = FunctionInputRuntimeType<Param["items"]>[]; /** * @description Converts a ParameterSetDefinition, and list of required params into an object type used for runtime inputs and outputs */ export type FunctionRuntimeParameters< Params extends ParameterSetDefinition, RequiredParams extends PossibleParameterKeys<Params>, > = & { [k in RequiredParams[number]]: FunctionInputRuntimeType< Params[k] >; } & { [k in keyof Params]?: FunctionInputRuntimeType< Params[k] >; }; type AsyncFunctionHandler< InputParameters extends FunctionParameters, OutputParameters extends FunctionParameters, Context extends BaseRuntimeFunctionContext<InputParameters>, > = { ( context: Context, ): Promise<FunctionHandlerReturnArgs<OutputParameters>>; }; type SyncFunctionHandler< InputParameters extends FunctionParameters, OutputParameters extends FunctionParameters, Context extends BaseRuntimeFunctionContext<InputParameters>, > = { ( context: Context, ): FunctionHandlerReturnArgs<OutputParameters>; }; /** * @description Custom function handler from a function definition */ export type RuntimeSlackFunctionHandler<Definition> = Definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? BaseRuntimeSlackFunctionHandler< FunctionRuntimeParameters<I, RI>, FunctionRuntimeParameters<O, RO> > : never; /** * @description Custom function handler from input and output types directly */ export type BaseRuntimeSlackFunctionHandler< InputParameters extends FunctionParameters, OutputParameters extends FunctionParameters, > = | AsyncFunctionHandler< InputParameters, OutputParameters, RuntimeFunctionContext<InputParameters> > | SyncFunctionHandler< InputParameters, OutputParameters, RuntimeFunctionContext<InputParameters> >; /** * @description Custom function handler from a function definition */ export type EnrichedSlackFunctionHandler<Definition> = Definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? (BaseEnrichedSlackFunctionHandler< FunctionRuntimeParameters<I, RI>, FunctionRuntimeParameters<O, RO> >) : never; /** * @description Custom function handler from input and output types directly */ type BaseEnrichedSlackFunctionHandler< InputParameters extends FunctionParameters, OutputParameters extends FunctionParameters, > = | AsyncFunctionHandler< InputParameters, OutputParameters, FunctionContext<InputParameters> > | SyncFunctionHandler< InputParameters, OutputParameters, FunctionContext<InputParameters> >; // export type SlackFunctionHandler<Definition> = EnrichedSlackFunctionHandler< // Definition // >; type SuccessfulFunctionReturnArgs< OutputParameters extends FunctionParameters, > = { completed?: true; // Allow function to return an empty object if no outputs are defined outputs: OutputParameters extends undefined ? (Record<never, never>) : OutputParameters; error?: string; }; type ErroredFunctionReturnArgs<OutputParameters extends FunctionParameters> = & Partial<SuccessfulFunctionReturnArgs<OutputParameters>> & Required<Pick<SuccessfulFunctionReturnArgs<OutputParameters>, "error">>; type PendingFunctionReturnArgs = { completed: false; outputs?: never; error?: never; }; export type FunctionHandlerReturnArgs< OutputParameters extends FunctionParameters, > = | SuccessfulFunctionReturnArgs<OutputParameters> | ErroredFunctionReturnArgs<OutputParameters> | PendingFunctionReturnArgs; // This describes the base-version of context objects deno-slack-runtime passes into different function handlers (i.e. main fn handler, blockActions, etc). // Each function handler type extends this with it's own specific additions. export type BaseRuntimeFunctionContext< InputParameters extends FunctionParameters, > = { /** * @description A map of string keys to string values containing any environment variables available and provided to your function handler's execution context. */ env: Env; /** * @description The inputs to the function as defined by your function definition. If no inputs are specified, an empty object is provided at runtime. */ inputs: InputParameters; /** * @description API token that can be used with the deno-slack-api API client. */ token: string; /** * @description A unique encoded ID representing the Slack team associated with the workspace where the function execution takes place. */ team_id: string; /** * @description A unique encoded ID representing the Slack enterprise associated with the workspace where the function execution takes place. In a non-enterprise workspace, this value will be the empty string. */ enterprise_id: string; }; // SDK function handlers receive these additional properties on the function context object export type FunctionContextEnrichment = { client: SlackAPIClient; }; // This is the context deno-slack-runtime passes to the main function handler export type RuntimeFunctionContext<InputParameters extends FunctionParameters> = & BaseRuntimeFunctionContext<InputParameters> & { event: FunctionInvocationBody["event"]; }; // This is the enriched context object passed into the main function handler setup with SlackFunction() export type FunctionContext< InputParameters extends FunctionParameters, > = RuntimeFunctionContext<InputParameters> & FunctionContextEnrichment; // Allow undefined here for functions that have no inputs and/or outputs export type FunctionParameters = { // deno-lint-ignore no-explicit-any [key: string]: any; } | undefined; export interface ISlackFunctionDefinition< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInput extends PossibleParameterKeys<InputParameters>, RequiredOutputs extends PossibleParameterKeys<OutputParameters>, > { type: ManifestFunctionType; id: string; definition: FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInput, RequiredOutputs >; export?: (() => ManifestFunctionSchema) | undefined; registerParameterTypes?: ((manifest: SlackManifest) => void) | undefined; } export type SlackFunctionDefinitionArgs< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInputs extends PossibleParameterKeys<InputParameters>, RequiredOutputs extends PossibleParameterKeys<OutputParameters>, > = & FunctionDefinitionArgs< InputParameters, OutputParameters, RequiredInputs, RequiredOutputs > & { source_file: string }; export type FunctionDefinitionArgs< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInputs extends PossibleParameterKeys<InputParameters>, RequiredOutputs extends PossibleParameterKeys<OutputParameters>, > = { callback_id: string; /** A title for your function. */ title: string; /** An optional description for your function. */ description?: string; /** An optional map of input parameter names containing information about their type, title, description, required and (additional) properties. */ "input_parameters"?: ParameterPropertiesDefinition< InputParameters, RequiredInputs >; /** An optional map of output parameter names containing information about their type, title, description, required and (additional) properties. */ "output_parameters"?: ParameterPropertiesDefinition< OutputParameters, RequiredOutputs >; }; export type SlackFunctionType<Definition> = Definition extends FunctionDefinitionArgs<infer I, infer O, infer RI, infer RO> ? ( & EnrichedSlackFunctionHandler<Definition> & { /** * @description Add an interactivity handler responding to specific {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` events}. * @param {BlockActionConstraint} actionConstraint - A {@link BlockActionConstraint} filter; only `block_actions` payloads that satisfy the constraints provided in this parameter will be routed to the provided handler. * @param {BlockActionHandler} handler - A {@link BlockActionHandler} function handler that will be invoked when a matching {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload} is dispatched to your application. */ addBlockActionsHandler( actionConstraint: BlockActionConstraint, handler: BlockActionHandler< FunctionDefinitionArgs<I, O, RI, RO> >, ): SlackFunctionType<Definition>; /** * @description Add an interactivity handler responding to specific `block_suggestion` events. * @param {BlockActionConstraint} actionConstraint - A {@link BlockActionConstraint} filter; only `block_suggestion` payloads that satisfy the constraints provided in this parameter will be routed to the provided handler. * @param {BlockSuggestionHandler} handler - A {@link BlockSuggestionHandler} function handler that will be invoked when a matching `block_suggestion` payload is dispatched to your application. */ addBlockSuggestionHandler( actionConstraint: BlockActionConstraint, handler: BlockSuggestionHandler< FunctionDefinitionArgs<I, O, RI, RO> >, ): SlackFunctionType<Definition>; addViewClosedHandler( viewConstraint: BasicConstraintField, handler: ViewClosedHandler< FunctionDefinitionArgs<I, O, RI, RO> >, ): SlackFunctionType<Definition>; addViewSubmissionHandler( viewConstraint: BasicConstraintField, handler: ViewSubmissionHandler< FunctionDefinitionArgs<I, O, RI, RO> >, ): SlackFunctionType<Definition>; addUnhandledEventHandler( handler: UnhandledEventHandler< FunctionDefinitionArgs<I, O, RI, RO> >, ): SlackFunctionType<Definition>; } ) : never; // This is the context deno-slack-runtime passes to the unhandledEvent handler export type RuntimeUnhandledEventContext< InputParameters extends FunctionParameters, > = & BaseRuntimeFunctionContext<InputParameters> & { // deno-lint-ignore no-explicit-any body: any; }; ================================================ FILE: src/functions/types_base_runtime_function_handler_test.ts ================================================ import { assertEquals } from "../dev_deps.ts"; import { SlackFunctionTester } from "./tester/mod.ts"; import type { BaseRuntimeSlackFunctionHandler } from "./types.ts"; // These tests are to ensure our Function Handler types are supporting the use cases we want to // Any "failures" here will most likely be reflected in Type errors Deno.test("BaseRuntimeSlackFunctionHandler types", () => { type Inputs = { in: string; }; type Outputs = { out: string; }; const handler: BaseRuntimeSlackFunctionHandler<Inputs, Outputs> = ( { inputs }, ) => { return { outputs: { out: inputs.in, }, }; }; const { createContext } = SlackFunctionTester("test"); const inputs = { in: "test" }; const result = handler(createContext({ inputs })); assertEquals(result.outputs?.out, inputs.in); }); Deno.test("BaseRuntimeSlackFunctionHandler with empty inputs and empty outputs", () => { type Inputs = Record<never, never>; type Outputs = Record<never, never>; const handler: BaseRuntimeSlackFunctionHandler<Inputs, Outputs> = () => { return { outputs: {}, }; }; const { createContext } = SlackFunctionTester("test"); const result = handler(createContext({ inputs: {} })); assertEquals(result.outputs, {}); }); Deno.test("BaseRuntimeSlackFunctionHandler with undefined inputs and outputs", () => { type Inputs = undefined; type Outputs = undefined; const handler: BaseRuntimeSlackFunctionHandler<Inputs, Outputs> = () => { return { outputs: {}, }; }; const { createContext } = SlackFunctionTester("test"); const result = handler(createContext({ inputs: undefined })); assertEquals(result.outputs, {}); }); Deno.test("BaseRuntimeSlackFunctionHandler with inputs and empty outputs", () => { type Inputs = { in: string; }; type Outputs = Record<never, never>; const handler: BaseRuntimeSlackFunctionHandler<Inputs, Outputs> = ( { inputs }, ) => { const _test = inputs.in; return { outputs: {}, }; }; const { createContext } = SlackFunctionTester("test"); const inputs = { in: "test" }; const result = handler(createContext({ inputs })); assertEquals(result.outputs, {}); }); Deno.test("BaseRuntimeSlackFunctionHandler with empty inputs and outputs", () => { type Inputs = Record<never, never>; type Outputs = { out: string; }; const handler: BaseRuntimeSlackFunctionHandler<Inputs, Outputs> = () => { return { outputs: { out: "test", }, }; }; const { createContext } = SlackFunctionTester("test"); const result = handler(createContext({ inputs: {} })); assertEquals(result.outputs?.out, "test"); }); Deno.test("BaseRuntimeSlackFunctionHandler with any inputs and any outputs", () => { // deno-lint-ignore no-explicit-any const handler: BaseRuntimeSlackFunctionHandler<any, any> = ({ inputs }) => { return { outputs: { out: inputs.in, }, }; }; const { createContext } = SlackFunctionTester("test"); const inputs = { in: "test" }; const result = handler(createContext({ inputs })); assertEquals(result.outputs?.out, inputs.in); }); Deno.test("BaseRuntimeSlackFunctionHandler with no inputs and error output", () => { // deno-lint-ignore no-explicit-any const handler: BaseRuntimeSlackFunctionHandler<any, { example: string }> = () => { return { error: "error", }; }; const { createContext } = SlackFunctionTester("test"); const result = handler(createContext({ inputs: {} })); assertEquals(result.error, "error"); }); Deno.test("BaseRuntimeSlackFunctionHandler with no inputs and completed false output", () => { // deno-lint-ignore no-explicit-any const handler: BaseRuntimeSlackFunctionHandler<any, { example: boolean }> = () => { return { completed: false, }; }; const { createContext } = SlackFunctionTester("test"); const result = handler(createContext({ inputs: {} })); assertEquals(result.completed, false); }); Deno.test("BaseRuntimeSlackFunctionHandler with set inputs and any outputs", () => { type Inputs = { in: string; }; // deno-lint-ignore no-explicit-any const handler: BaseRuntimeSlackFunctionHandler<Inputs, any> = ( { inputs }, ) => { return { outputs: { out: inputs.in, }, }; }; const { createContext } = SlackFunctionTester("test"); const inputs = { in: "test" }; const result = handler(createContext({ inputs })); assertEquals(result.outputs?.out, inputs.in); }); Deno.test("BaseRuntimeSlackFunctionHandler with input and output objects", () => { type Inputs = { anObject: { in: string; }; }; type Outputs = { anObject: { out: string; }; }; const handler: BaseRuntimeSlackFunctionHandler<Inputs, Outputs> = ( { inputs }, ) => { return { outputs: { anObject: { out: inputs.anObject.in }, }, }; }; const { createContext } = SlackFunctionTester("test"); const inputs = { anObject: { in: "test" } }; const result = handler(createContext({ inputs })); assertEquals(result.outputs?.anObject?.out, inputs.anObject.in); }); ================================================ FILE: src/functions/types_runtime_slack_function_handler_test.ts ================================================ import { assertExists } from "../dev_deps.ts"; import { assertEqualsTypedValues } from "../test_utils.ts"; import { SlackFunctionTester } from "./tester/mod.ts"; import { DefineFunction } from "./mod.ts"; import type { RuntimeSlackFunctionHandler } from "./types.ts"; Deno.test("RuntimeSlackFunctionHandler type should not include a client property", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { example: { type: "boolean", }, }, required: ["example"], }, }); const handler: RuntimeSlackFunctionHandler<typeof TestFn.definition> = ( ctx, ) => { // @ts-expect-error ctx.client shouldn't be typed by RuntimeSlackFunctionHandler type - but SlackFunctionTester should inject it at runtime assertExists(ctx.client); return { completed: false, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = handler(createContext({ inputs: {} })); assertEqualsTypedValues(result.completed, false); }); ================================================ FILE: src/functions/unhandled-event-error.ts ================================================ // This error conforms with what the deno-slack-runtime expects for an unhandled events export class UnhandledEventError extends Error { constructor(message: string) { super(message); // The name here is important, as it's what deno-slack-runtime keys off of this.name = "UnhandledEventError"; } } ================================================ FILE: src/manifest/errors.ts ================================================ export class DuplicateCallbackIdError extends Error { constructor(callbackId: string, readableType: "Function" | "Workflow") { super(`Duplicate callback_id: "${callbackId}" found in ${readableType}.`); } } export class DuplicateNameError extends Error { constructor( name: string, readableType: "CustomType" | "Datastore" | "CustomEvent", ) { super(`Duplicate name: "${name}" found in ${readableType}.`); } } export class DuplicateProviderKeyError extends Error { constructor(provider_key: string, readableType: "OAuth2Provider") { super( `Duplicate provider_key: "${provider_key}" found in ${readableType}.`, ); } } ================================================ FILE: src/manifest/errors_test.ts ================================================ import { DuplicateCallbackIdError, DuplicateNameError, DuplicateProviderKeyError, } from "./errors.ts"; import { assertStringIncludes } from "../dev_deps.ts"; Deno.test(`${DuplicateCallbackIdError.name} returns proper error message`, () => { const actual = new DuplicateCallbackIdError("test", "Function"); assertStringIncludes(actual.message, `callback_id: "test`); assertStringIncludes(actual.message, "Function"); }); Deno.test(`${DuplicateNameError.name} returns proper error message`, () => { const actual = new DuplicateNameError("test", "CustomType"); assertStringIncludes(actual.message, `name: "test`); assertStringIncludes(actual.message, "CustomType"); }); Deno.test(`${DuplicateProviderKeyError.name} returns proper error message`, () => { const actual = new DuplicateProviderKeyError("test", "OAuth2Provider"); assertStringIncludes(actual.message, `provider_key: "test`); assertStringIncludes(actual.message, "OAuth2Provider"); }); ================================================ FILE: src/manifest/manifest_schema.ts ================================================ import type { ISlackDatastore } from "../datastore/types.ts"; import type { ISlackFunctionDefinition } from "../functions/types.ts"; import type { ParameterSetDefinition } from "../parameters/types.ts"; import type { ParameterDefinition } from "../parameters/definition_types.ts"; import type { OAuth2ProviderTypeValues } from "../schema/providers/oauth2/types.ts"; import type { ICustomType } from "../types/types.ts"; import type { ISlackWorkflow } from "../workflows/types.ts"; import type { OAuth2ProviderOptions } from "../providers/oauth2/types.ts"; // ---------------------------------------------------------------------------- // Manifest Schema Types // These types should map directly to internal types, basically a pass through // ---------------------------------------------------------------------------- export type ManifestSchema = { _metadata?: ManifestMetadataSchema; settings: ManifestSettingsSchema; app_directory?: ManifestAppDirectorySchema; display_information: ManifestDisplayInformationSchema; icon: string; oauth_config: ManifestOauthConfigSchema; features: ManifestFeaturesSchema; functions?: ManifestFunctionsSchema; workflows?: ManifestWorkflowsSchema; outgoing_domains?: string[]; types?: ManifestCustomTypesSchema; datastores?: ManifestDataStoresSchema; events?: ManifestCustomEventsSchema; external_auth_providers?: ManifestExternalAuthProviders; }; // --------------------------------------------------------------------------- // Manifest: _metadata // --------------------------------------------------------------------------- export type ManifestMetadataSchema = { major_version?: number; minor_version?: number; }; // --------------------------------------------------------------------------- // Manifest: settings // --------------------------------------------------------------------------- export type ManifestSettingsSchema = { allowed_ip_address_ranges?: [string, ...string[]]; event_subscriptions?: ManifestEventSubscriptionsSchema; incoming_webhooks?: ManifestIncomingWebhooks; interactivity?: ManifestInteractivitySchema; org_deploy_enabled?: boolean; socket_mode_enabled?: boolean; token_rotation_enabled?: boolean; siws_links?: ManifestSiwsLinksSchema; function_runtime?: ManifestFunctionRuntime; }; // Settings: event subscriptions export type ManifestEventSubscriptionsSchema = { request_url?: string; user_events?: string[]; bot_events?: string[]; metadata_subscriptions?: [ { app_id: string; event_type: string; }, ...{ app_id: string; event_type: string; }[], ]; }; // Settings: incoming webhooks export type ManifestIncomingWebhooks = { incoming_webhooks_enabled?: boolean; }; // Settings interactivity export type ManifestInteractivitySchema = { is_enabled: boolean; request_url?: string; message_menu_options_url?: string; }; // Settings: SIWS export type ManifestSiwsLinksSchema = { initiate_uri?: string; }; // Settings: function runtime export type ManifestFunctionRuntime = "slack" | "remote" | "local"; // --------------------------------------------------------------------------- // Manifest: app directory // --------------------------------------------------------------------------- export type ManifestAppDirectorySchema = { app_directory_categories?: string[]; use_direct_install?: boolean; direct_install_url?: string; installation_landing_page: string; privacy_policy_url: string; support_url: string; support_email: string; supported_languages: [string, ...string[]]; pricing: string; }; // --------------------------------------------------------------------------- // Manifest: display information // --------------------------------------------------------------------------- export type ManifestDisplayInformationSchema = { name: string; description?: string; background_color?: string; long_description?: string; }; // --------------------------------------------------------------------------- // Manifest: OAuth config // --------------------------------------------------------------------------- export type ManifestOauthConfigSchema = { scopes: { bot?: string[]; user?: string[]; }; redirect_urls?: string[]; token_management_enabled?: boolean; }; // --------------------------------------------------------------------------- // Manifest: features // --------------------------------------------------------------------------- export interface ManifestFeaturesSchema { bot_user?: ManifestBotUserSchema; app_home?: ManifestAppHomeSchema; shortcuts?: ManifestShortcutsSchema; slash_commands?: ManifestSlashCommandsSchema; unfurl_domains?: ManifestUnfurlDomainsSchema; workflow_steps?: ManifestWorkflowStepsSchemaLegacy; } // Features: bot user export type ManifestBotUserSchema = { display_name: string; always_online?: boolean; }; // Features: app home export type ManifestAppHomeSchema = ManifestAppHomeMessagesTabSchema & { home_tab_enabled?: boolean; }; export type ManifestAppHomeMessagesTabSchema = { /** @default true */ messages_tab_enabled?: true; /** @default true */ messages_tab_read_only_enabled?: boolean; } | { /** @default true */ messages_tab_enabled: false; /** @default true */ messages_tab_read_only_enabled: false; }; // Features: shortcuts export type ManifestShortcutSchema = { name: string; type: "message" | "global"; callback_id: string; description: string; }; export type ManifestShortcutsSchema = PopulatedArray<ManifestShortcutSchema>; // Features: slash commands export type ManifestSlashCommandsSchema = PopulatedArray< ManifestSlashCommandSchema >; export type ManifestSlashCommandSchema = { command: string; url?: string; description: string; usage_hint?: string; should_escape?: boolean; }; // Features: legacy workflow step (To be deprecated) // Not to be confused with next generation ManifestWorkflowStepSchema export type ManifestWorkflowStepLegacy = { name: string; callback_id: string; }; export type ManifestWorkflowStepsSchemaLegacy = PopulatedArray< ManifestWorkflowStepLegacy >; // Features: unfurl domains export type ManifestUnfurlDomainsSchema = [string, ...string[]]; // --------------------------------------------------------------------------- // Manifest: custom functions // --------------------------------------------------------------------------- // This is typed liberally at this level but more specifically down further // This is to work around an issue TS has with resolving the generics across the hierarchy // deno-lint-ignore no-explicit-any export type ManifestFunction = ISlackFunctionDefinition<any, any, any, any>; export type ManifestFunctionsSchema = { [key: string]: ManifestFunctionSchema }; export type ManifestFunctionType = "API" | "app" | undefined; export type ManifestFunctionSchema = { type?: ManifestFunctionType; title?: string; description?: string; source_file: string; input_parameters: ManifestFunctionParameters; output_parameters: ManifestFunctionParameters; }; export type ManifestFunctionParameters = { required?: RequiredParameters; properties: ParameterSetDefinition; }; export type RequiredParameters = { [index: number]: string | number | symbol; }; // --------------------------------------------------------------------------- // Manifest: workflows // Not to be confused with ManifestWorkflowStepsSchemaLegacy // --------------------------------------------------------------------------- export type ManifestWorkflow = ISlackWorkflow; export type ManifestWorkflowsSchema = { [key: string]: ManifestWorkflowSchema }; export type ManifestWorkflowSchema = { title?: string; description?: string; input_parameters?: ManifestFunctionParameters; steps: ManifestWorkflowStepSchema[]; }; export type ManifestWorkflowStepSchema = { id: string; function_id: string; inputs: { [name: string]: unknown; }; }; // --------------------------------------------------------------------------- // Manifest: custom events // --------------------------------------------------------------------------- export type ManifestCustomEventSchema = ParameterDefinition; export type ManifestCustomEventsSchema = { [key: string]: ManifestCustomEventSchema; }; // --------------------------------------------------------------------------- // Manifest: custom types // --------------------------------------------------------------------------- export type ManifestCustomTypeSchema = ParameterDefinition; export type ManifestCustomTypesSchema = { [key: string]: ManifestCustomTypeSchema; }; // --------------------------------------------------------------------------- // Manifest: datastores // --------------------------------------------------------------------------- export type ManifestDatastore = ISlackDatastore; export type ManifestDatastoreSchema = { primary_key: string; time_to_live_attribute?: string; attributes: { [key: string]: { type: string | ICustomType; items?: ManifestCustomTypeSchema; properties?: { [key: string]: ManifestCustomTypeSchema; }; }; }; }; export type ManifestDataStoresSchema = { [key: string]: ManifestDatastoreSchema; }; // ------------------------------------------------------------------------- // Manifest: OAuth2 provider // ------------------------------------------------------------------------- export type ManifestOAuth2Schema = { [key: string]: ManifestOAuth2ProviderSchema; }; export type ManifestOAuth2ProviderSchema = { provider_type: OAuth2ProviderTypeValues; options: OAuth2ProviderOptions; }; export interface ManifestExternalAuthProviders { oauth2?: ManifestOAuth2Schema; } // ------------------------------------------------------------------------- // Utilities // ------------------------------------------------------------------------- // Utility type for the array types which requires minumum one subtype in it. export type PopulatedArray<T> = [T, ...T[]]; ================================================ FILE: src/manifest/manifest_test.ts ================================================ import type { ISlackManifestRemote, ISlackManifestRunOnSlack, SlackManifestType, } from "./types.ts"; import { Manifest, SlackManifest } from "./mod.ts"; import { DefineDatastore, DefineEvent, DefineFunction, DefineType, DefineWorkflow, Schema, } from "../mod.ts"; import { assert, assertEquals, assertInstanceOf, AssertionError, assertStrictEquals, assertStringIncludes, fail, type IsExact, mock, } from "../dev_deps.ts"; import { DefineConnector } from "../functions/mod.ts"; import { InternalSlackTypes } from "../schema/slack/types/custom/mod.ts"; import { DuplicateCallbackIdError, DuplicateNameError } from "./errors.ts"; Deno.test("SlackManifestType correctly resolves to a Hosted App when runOnSlack = true", () => { const definition: SlackManifestType = { runOnSlack: true, name: "test", description: "description", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "displayName", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], }; assert<IsExact<typeof definition, ISlackManifestRunOnSlack>>(true); assert<IsExact<typeof definition, ISlackManifestRemote>>(false); }); Deno.test("SlackManifestType correctly resolves to a Remote App when runOnSlack = false", () => { const definition: SlackManifestType = { runOnSlack: false, name: "test", description: "description", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "displayName", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], }; assert<IsExact<typeof definition, ISlackManifestRunOnSlack>>(false); assert<IsExact<typeof definition, ISlackManifestRemote>>(true); }); Deno.test("Manifest() sets function_runtime = slack when runOnSlack = true", () => { const definition: SlackManifestType = { runOnSlack: true, name: "test", description: "description", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "displayName", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], }; const manifest = Manifest(definition); assertEquals(manifest.settings.function_runtime, "slack"); }); Deno.test("Manifest() sets function_runtime = remote when runOnSlack = false", () => { const definition: SlackManifestType = { runOnSlack: false, name: "test", description: "description", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "displayName", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], }; const manifest = Manifest(definition); assertEquals(manifest.settings.function_runtime, "remote"); }); Deno.test("Manifest() property mappings", () => { const definition: SlackManifestType = { runOnSlack: true, name: "fear and loathing in las vegas", description: "fear and loathing in las vegas: a savage journey to the heart of the american dream", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "fear and loathing", icon: "icon.png", botScopes: [], }; let manifest = Manifest(definition); assertEquals(manifest.display_information, { name: definition.name, background_color: definition.backgroundColor, long_description: definition.longDescription, description: definition.description, }); assertStrictEquals(manifest.icon, definition.icon); assertStrictEquals( manifest.features.bot_user?.display_name, definition.displayName, ); assertEquals(manifest.settings.function_runtime, "slack"); // If display_name is not defined on definition, should fall back to name delete definition.displayName; manifest = Manifest(definition); assertStrictEquals( manifest.features.bot_user?.display_name, definition.name, ); }); // TODO: Re-add test to catch dup datastore names // TODO: Re-add test for datastore columns Deno.test("Manifest() automatically registers types used by function input and output parameters", () => { const inputTypeId = "test_input_type"; const outputTypeId = "test_output_type"; const stringTypeId = "test_string_type"; const CustomStringType = DefineType({ name: stringTypeId, type: Schema.types.string, }); const CustomInputType = DefineType({ name: inputTypeId, type: CustomStringType, }); const CustomOutputType = DefineType({ name: outputTypeId, type: Schema.types.boolean, }); const Function = DefineFunction( { callback_id: "test_function", title: "Function title", source_file: "functions/test_function.ts", input_parameters: { properties: { aType: { type: CustomInputType }, }, required: [], }, output_parameters: { properties: { aType: { type: CustomOutputType }, }, required: [], }, }, ); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: [], functions: [Function], }; const manifest = Manifest(definition); assertEquals(definition.types, [ CustomInputType, CustomOutputType, CustomStringType, ]); assertEquals(manifest.types, { [inputTypeId]: CustomInputType.export(), [stringTypeId]: CustomStringType.export(), [outputTypeId]: CustomOutputType.export(), }); }); Deno.test("Manifest() automatically registers functions used by workflows", () => { const Function = DefineFunction( { callback_id: "test_function", title: "Function title", source_file: "functions/test_function.ts", input_parameters: { properties: { aString: { type: Schema.types.string } }, required: [], }, output_parameters: { properties: { aType: { type: Schema.types.string } }, required: [], }, }, ); const Workflow = DefineWorkflow({ title: "test workflow", callback_id: "test_workflow", }); Workflow.addStep(Function, { aString: "test", }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: [], workflows: [Workflow], }; const manifest = Manifest(definition); assertEquals(manifest.workflows, { [Workflow.id]: Workflow.export(), }); assertEquals(manifest.functions, { [Function.id]: Function.export(), }); }); Deno.test("Manifest() properly converts name to proper key", () => { const UsingName = DefineType({ name: "Using Name", type: Schema.types.boolean, }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: [], types: [UsingName], }; const manifest = Manifest(definition); assertEquals(manifest.types, { "Using Name": { type: "boolean" } }); }); Deno.test("Manifest() always sets token_management_enabled to false for runOnSlack: true apps", () => { // When runOnSlack is explicitly specified as true, token_management_enabled must be set to false const definition: SlackManifestType = { runOnSlack: true, name: "", description: "", backgroundColor: "#FFF", longDescription: "", displayName: "", icon: "", botScopes: [], }; const manifest = Manifest(definition); assertEquals(manifest.oauth_config.token_management_enabled, false); }); Deno.test("Manifest() always sets token_management_enabled to false for function_runtime: slack apps", () => { // SlackManifestType will default to function_runtime == slack when runOnSlack property omitted // AND when no remote-only features are specified const definition: SlackManifestType = { name: "", description: "", backgroundColor: "#FFF", longDescription: "", displayName: "", icon: "", botScopes: [], }; const manifest = Manifest(definition); assertEquals(manifest.oauth_config.token_management_enabled, false); }); Deno.test("Manifest() sets token_management_enabled to true by default for runOnSlack: false apps", () => { const definition: SlackManifestType = { runOnSlack: false, name: "", description: "", backgroundColor: "", longDescription: "", displayName: "", icon: "", botScopes: [], }; const manifest = Manifest(definition); assertEquals(manifest.oauth_config.token_management_enabled, true); }); Deno.test("Manifest() automatically registers types referenced by datastores", () => { const stringTypeId = "test_string_type"; const objectTypeId = "test_object_type"; const StringType = DefineType({ name: stringTypeId, type: Schema.types.string, }); const ObjectType = DefineType({ name: objectTypeId, type: Schema.types.object, properties: { aString: { type: StringType }, }, }); const Store = DefineDatastore({ name: "Test store", attributes: { aString: { type: "string" }, aType: { type: ObjectType }, }, primary_key: "aString", }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], datastores: [Store], }; const manifest = Manifest(definition); assertEquals(definition.types, [ObjectType, StringType]); assertEquals(manifest.types, { [stringTypeId]: StringType.export(), [objectTypeId]: ObjectType.export(), }); }); Deno.test("Manifest() automatically registers types referenced by events", () => { const objectEventTypeId = "test_object_event_type"; const objectTypeId = "test_object_type"; const objectEventId = "test_object_event"; const stringTypeId = "test_string_type"; const booleanTypeId = "test_boolean_type"; const arrayTypeId = "test_array_type"; const BooleanType = DefineType({ name: booleanTypeId, type: Schema.types.boolean, }); const StringType = DefineType({ name: stringTypeId, type: Schema.types.string, }); const ArrayType = DefineType({ name: arrayTypeId, type: Schema.types.array, items: { type: StringType, }, }); const ObjectType = DefineType({ name: objectTypeId, type: Schema.types.object, properties: { aBoolean: { type: BooleanType }, }, }); const ObjectEvent = DefineEvent({ name: objectEventId, type: Schema.types.object, properties: { aBoolean: { type: BooleanType }, anArray: { type: ArrayType }, }, }); const ObjectTypeEvent = DefineEvent({ name: objectEventTypeId, type: ObjectType, }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: [], events: [ObjectTypeEvent, ObjectEvent], }; const manifest = Manifest(definition); assertEquals(definition.events, [ObjectTypeEvent, ObjectEvent]); assertEquals(manifest.events, { [objectEventTypeId]: ObjectTypeEvent.export(), [objectEventId]: ObjectEvent.export(), }); assertEquals(definition.types, [ ObjectType, BooleanType, ArrayType, StringType, ]); assertEquals(manifest.types, { [objectTypeId]: ObjectType.export(), [booleanTypeId]: BooleanType.export(), [arrayTypeId]: ArrayType.export(), [stringTypeId]: StringType.export(), }); }); Deno.test("Manifest() automatically registers types referenced by other types", () => { const objectTypeId = "test_object_type"; const stringTypeId = "test_string_type"; const booleanTypeId = "test_boolean_type"; const arrayTypeId = "test_array_type"; const BooleanType = DefineType({ name: booleanTypeId, type: Schema.types.boolean, }); const StringType = DefineType({ name: stringTypeId, type: Schema.types.string, }); const ObjectType = DefineType({ name: objectTypeId, type: Schema.types.object, properties: { aBoolean: { type: BooleanType, }, }, }); const ArrayType = DefineType({ name: arrayTypeId, type: Schema.types.array, items: { type: StringType, }, }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: [], types: [ArrayType, ObjectType], }; const manifest = Manifest(definition); assertEquals(definition.types, [ ArrayType, ObjectType, StringType, BooleanType, ]); assertEquals(manifest.types, { [arrayTypeId]: ArrayType.export(), [objectTypeId]: ObjectType.export(), [stringTypeId]: StringType.export(), [booleanTypeId]: BooleanType.export(), }); }); Deno.test("Manifest() correctly assigns display_information properties ", () => { const definition: SlackManifestType = { runOnSlack: false, name: "fear and loathing in las vegas", description: "fear and loathing in las vegas: a savage journey to the heart of the american dream", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "fear and loathing", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], }; const manifest = Manifest(definition); assertEquals(manifest.display_information, { name: definition.name, background_color: definition.backgroundColor, long_description: definition.longDescription, description: definition.description, }); assertStrictEquals(manifest.icon, definition.icon); assertStrictEquals( manifest.features.bot_user?.display_name, definition.displayName, ); }); Deno.test("Manifest() correctly assigns app_directory properties", () => { const definition: SlackManifestType = { runOnSlack: false, name: "fear and loathing in las vegas", description: "fear and loathing in las vegas: a savage journey to the heart of the american dream", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "fear and loathing", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], appDirectory: { app_directory_categories: ["app-directory-test"], use_direct_install: true, direct_install_url: "https://api.slack.com/", installation_landing_page: "https://api.slack.com/", privacy_policy_url: "https://api.slack.com/", support_url: "https://api.slack.com/", support_email: "example@salesfroce.com", supported_languages: ["eng", "fr"], pricing: "free", }, }; const manifest = Manifest(definition); // app directory assertStrictEquals( manifest.app_directory, definition.appDirectory, ); }); Deno.test("Manifest() correctly assigns remote app settings properties", () => { const definition: SlackManifestType = { runOnSlack: false, name: "fear and loathing in las vegas", description: "fear and loathing in las vegas: a savage journey to the heart of the american dream", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "fear and loathing", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], settings: { "allowed_ip_address_ranges": ["123.89.34.56"], "incoming_webhooks": { "incoming_webhooks_enabled": true }, interactivity: { is_enabled: true, request_url: "https://app.slack.com/test", message_menu_options_url: "https://app.slack.com", }, "org_deploy_enabled": true, "siws_links": { initiate_uri: "https://app.slack.com" }, }, eventSubscriptions: { request_url: "string", user_events: ["app_home_opened"], bot_events: ["app_home_opened"], metadata_subscriptions: [ { app_id: "metadata-test", event_type: "customer_created", }, ], }, socketModeEnabled: true, tokenRotationEnabled: false, }; const manifest = Manifest(definition); assertStrictEquals( manifest.settings.socket_mode_enabled, definition.socketModeEnabled, ); assertStrictEquals( manifest.settings.token_rotation_enabled, definition.tokenRotationEnabled, ); assertStrictEquals( manifest.settings.event_subscriptions, definition.eventSubscriptions, ); assertStrictEquals( manifest.settings.allowed_ip_address_ranges, definition.settings?.allowed_ip_address_ranges, ); assertStrictEquals( manifest.settings.incoming_webhooks, definition.settings?.incoming_webhooks, ); assertStrictEquals( manifest.settings.org_deploy_enabled, definition.settings?.org_deploy_enabled, ); assertStrictEquals( manifest.settings.siws_links, definition.settings?.siws_links, ); assertStrictEquals(manifest.settings.function_runtime, "remote"); // When org_deploy_enabled not supplied, remote app settings default org deploy to true const definition2: SlackManifestType = { runOnSlack: false, name: "", description: "", backgroundColor: "", longDescription: "", displayName: "", icon: "", botScopes: [], settings: {}, }; const manifest2 = Manifest(definition2); assertEquals(manifest2.settings.org_deploy_enabled, true); }); Deno.test("Manifest() correctly assigns run on slack app settings properties", () => { const definition: SlackManifestType = { name: "", description: "", backgroundColor: "", longDescription: "", displayName: "", icon: "", botScopes: [], }; const manifest = Manifest(definition); assertEquals(manifest.settings.org_deploy_enabled, true); }); Deno.test("Manifest() correctly assigns oauth properties", () => { const definition: SlackManifestType = { runOnSlack: false, name: "fear and loathing in las vegas", description: "fear and loathing in las vegas: a savage journey to the heart of the american dream", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "fear and loathing", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], userScopes: ["admin", "calls:read"], redirectUrls: ["https://api.slack.com/", "https://app.slack.com/"], }; const manifest = Manifest(definition); //oauth assertStrictEquals( manifest.oauth_config.scopes.user, definition.userScopes, ); assertStrictEquals( manifest.oauth_config.redirect_urls, definition.redirectUrls, ); assertStrictEquals( manifest.oauth_config.token_management_enabled, true, ); }); Deno.test("Manifest() correctly assigns other app features", () => { const definition: SlackManifestType = { runOnSlack: false, name: "fear and loathing in las vegas", description: "fear and loathing in las vegas: a savage journey to the heart of the american dream", backgroundColor: "#FFF", longDescription: "The book is a roman à clef, rooted in autobiographical incidents. The story follows its protagonist, Raoul Duke, and his attorney, Dr. Gonzo, as they descend on Las Vegas to chase the American Dream...", displayName: "fear and loathing", icon: "icon.png", botScopes: ["channels:history", "chat:write", "commands"], features: { botUser: { always_online: false }, shortcuts: [{ name: "test-shortcut", type: "message", callback_id: "callback_id", description: "shortcut", }], appHome: { homeTabEnabled: true, messagesTabEnabled: false, messagesTabReadOnlyEnabled: false, }, slashCommands: [{ command: "sample-command", url: "https://app.slack.com", description: "test command", usage_hint: "testing", should_escape: true, }, { command: "sample-command2", url: "https://app.slack.com", description: "test command 2", usage_hint: "testing 2", should_escape: true, }], unfurlDomains: ["https://app.slack.com"], workflowSteps: [{ name: "workflow step test", callback_id: "workflow-step-test", }], }, }; const manifest = Manifest(definition); //features assertStrictEquals( manifest.features.bot_user?.always_online, definition.features?.botUser?.always_online, ); assertStrictEquals( manifest.features.shortcuts, definition.features?.shortcuts, ); assertStrictEquals( manifest.features.slash_commands, definition.features?.slashCommands, ); assertEquals( manifest.features.app_home?.home_tab_enabled, true, ); assertEquals( manifest.features.app_home?.messages_tab_enabled, false, ); assertEquals( manifest.features.app_home?.messages_tab_read_only_enabled, false, ); assertStrictEquals( manifest.features.unfurl_domains, definition.features?.unfurlDomains, ); assertStrictEquals( manifest.features.workflow_steps, definition.features?.workflowSteps, ); }); Deno.test("SlackManifest() registration functions don't allow duplicates", () => { const functionId = "test_function"; const arrayTypeId = "test_array_type"; const objectTypeId = "test_object_type"; const stringTypeId = "test_string_type"; const CustomStringType = DefineType({ name: stringTypeId, type: Schema.types.string, }); const CustomObjectType = DefineType({ name: objectTypeId, type: Schema.types.object, properties: { aString: { type: CustomStringType, }, }, required: ["aString"], }); const CustomArrayType = DefineType({ name: arrayTypeId, type: Schema.types.array, items: { type: CustomStringType, }, }); const Func = DefineFunction({ callback_id: functionId, title: "Function title", source_file: `functions/${functionId}.ts`, }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: [], functions: [Func], types: [CustomArrayType, CustomObjectType], }; const Manifest = new SlackManifest(definition); Manifest.registerFunction(Func); Manifest.registerFunction(Func); Manifest.registerType(CustomObjectType); Manifest.registerType(CustomObjectType); Manifest.registerType(CustomArrayType); Manifest.registerType(CustomStringType); Manifest.registerType(InternalSlackTypes.form_input_object); const exportedManifest = Manifest.export(); assertEquals(definition.functions, [Func]); assertEquals(exportedManifest.functions, { [functionId]: Func.export() }); assertEquals(definition.types, [ CustomArrayType, CustomObjectType, CustomStringType, ]); assertEquals(exportedManifest.types, { [arrayTypeId]: CustomArrayType.export(), [objectTypeId]: CustomObjectType.export(), [stringTypeId]: CustomStringType.export(), }); }); Deno.test("SlackManifest.export() warns of missing datastore scopes if they are not present and app includes a datastore", () => { const Store = DefineDatastore({ name: "test store", attributes: { attr: { type: Schema.types.string, }, }, primary_key: "attr", }); const definition: SlackManifestType = { runOnSlack: true, name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: [], datastores: [Store], }; const Manifest = new SlackManifest(definition); const warnStub = mock.stub(console, "warn"); Manifest.export(); assertStringIncludes( warnStub.calls[0].args[0], "does not specify the following datastore-related scopes", ); assertStringIncludes(warnStub.calls[0].args[0], "datastore:read"); assertStringIncludes(warnStub.calls[0].args[0], "datastore:write"); warnStub.restore(); }); Deno.test("SlackManifest.export() does not warn of missing datastore scopes if they're already present and app includes at least one datastore", () => { const Store = DefineDatastore({ name: "test store", attributes: { attr: { type: Schema.types.string, }, }, primary_key: "attr", }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: ["datastore:read", "datastore:write"], datastores: [Store], }; const Manifest = new SlackManifest(definition); const warnStub = mock.stub(console, "warn"); const exportedManifest = Manifest.export(); const botScopes = exportedManifest.oauth_config.scopes.bot; assertStrictEquals( botScopes?.filter((scope: string) => scope === "datastore:read").length, 1, ); assertStrictEquals( botScopes?.filter((scope: string) => scope === "datastore:write").length, 1, ); mock.assertSpyCalls(warnStub, 0); warnStub.restore(); }); Deno.test("SlackManifest.export() defaults to enabling the read only messages tab", () => { const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], }; const Manifest = new SlackManifest(definition); const exportedManifest = Manifest.export(); exportedManifest.features.app_home?.messages_tab_enabled; exportedManifest.features.app_home?.messages_tab_read_only_enabled; assertStrictEquals( exportedManifest.features.app_home?.messages_tab_enabled, true, ); assertStrictEquals( exportedManifest.features.app_home?.messages_tab_read_only_enabled, true, ); }); Deno.test("SlackManifest.export() allows overriding app home features", () => { const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], features: { appHome: { messagesTabEnabled: false, messagesTabReadOnlyEnabled: false, }, }, }; const Manifest = new SlackManifest(definition); const exportedManifest = Manifest.export(); exportedManifest.features.app_home?.messages_tab_enabled; exportedManifest.features.app_home?.messages_tab_read_only_enabled; assertStrictEquals( exportedManifest.features.app_home?.messages_tab_enabled, false, ); assertStrictEquals( exportedManifest.features.app_home?.messages_tab_read_only_enabled, false, ); }); Deno.test("Manifest supports multiple workflows with parameters", () => { const workflow1 = DefineWorkflow({ callback_id: "test", title: "test", input_parameters: { properties: { one: { type: Schema.types.string, }, }, required: ["one"], }, }); const workflow2 = DefineWorkflow({ callback_id: "test2", title: "test", input_parameters: { properties: { one: { type: Schema.types.string, }, }, required: ["one"], }, }); const manifest = Manifest({ name: "Name", description: "Description", botScopes: [], icon: "icon.png", workflows: [workflow1, workflow2], }); assertEquals(Object.keys(manifest.workflows || {}).length, 2); }); Deno.test("Manifest() does not register connectors used by workflows", () => { const Function = DefineConnector( { callback_id: "test_connector", title: "Connector title", input_parameters: { properties: { aString: { type: Schema.types.string } }, required: [], }, output_parameters: { properties: { aType: { type: Schema.types.string } }, required: [], }, }, ); const Workflow = DefineWorkflow({ title: "test workflow", callback_id: "test_workflow", }); Workflow.addStep(Function, { aString: "test", }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", longDescription: "LongDescription", botScopes: [], workflows: [Workflow], }; const manifest = Manifest(definition); assertEquals(manifest.workflows, { [Workflow.id]: Workflow.export(), }); assertEquals(manifest.functions, {}); }); Deno.test("Manifest throws error when workflows with duplicate callback_id are added", () => { const workflow1 = DefineWorkflow({ callback_id: "test", title: "workflow1", input_parameters: { properties: {}, required: [], }, }); const workflow2 = DefineWorkflow({ callback_id: "test", title: "workflow2", input_parameters: { properties: {}, required: [], }, }); try { Manifest({ name: "Name", description: "Description", botScopes: [], icon: "icon.png", workflows: [workflow1, workflow2], }); fail("Manifest() should have thrown an error"); } catch (error) { if (error instanceof AssertionError) throw error; assertInstanceOf(error, DuplicateCallbackIdError); assertStringIncludes(error.message, "Workflow"); } }); Deno.test("Manifest throws error when functions with duplicate callback_id are added", () => { const function1 = DefineFunction({ callback_id: "test", title: "function1", source_file: `functions/test.ts`, }); const function2 = DefineFunction({ callback_id: "test", title: "function2", source_file: `functions/test.ts`, }); try { Manifest({ name: "Name", description: "Description", botScopes: [], icon: "icon.png", functions: [function1, function2], }); fail("Manifest() should have thrown an error"); } catch (error) { if (error instanceof AssertionError) throw error; assertInstanceOf(error, DuplicateCallbackIdError); assertStringIncludes(error.message, "Function"); } }); Deno.test("Manifest throws error when customType with duplicate name are added", () => { const customType1 = DefineType({ name: "customType", type: Schema.types.string, }); const customType2 = DefineType({ name: "customType", type: Schema.types.string, }); try { Manifest({ name: "Name", description: "Description", botScopes: [], icon: "icon.png", types: [customType1, customType2], }); fail("Manifest() should have thrown an error"); } catch (error) { if (error instanceof AssertionError) throw error; assertInstanceOf(error, DuplicateNameError); assertStringIncludes(error.message, "CustomType"); } }); Deno.test("Manifest throws error when Datastores with duplicate name are added", () => { const datastore1 = DefineDatastore({ name: "Test store", attributes: { datastore1: { type: "string" }, }, primary_key: "datastore1", }); const datastore2 = DefineDatastore({ name: "Test store", attributes: { datastore2: { type: "string" }, }, primary_key: "datastore2", }); try { Manifest({ name: "Name", description: "Description", botScopes: [], icon: "icon.png", datastores: [datastore1, datastore2], }); fail("Manifest() should have thrown an error"); } catch (error) { if (error instanceof AssertionError) throw error; assertInstanceOf(error, DuplicateNameError); assertStringIncludes(error.message, "Datastore"); } }); Deno.test("Manifest throws error when CustomEvents with duplicate name are added", () => { const customEvent1 = DefineEvent({ name: "test", title: "customEvent1", type: Schema.types.object, properties: {}, }); const customEvent2 = DefineEvent({ name: "test", title: "customEvent2", type: Schema.types.object, properties: {}, }); try { Manifest({ name: "Name", description: "Description", botScopes: [], icon: "icon.png", events: [customEvent1, customEvent2], }); fail("Manifest() should have thrown an error"); } catch (error) { if (error instanceof AssertionError) throw error; assertInstanceOf(error, DuplicateNameError); assertStringIncludes(error.message, "CustomEvent"); } }); ================================================ FILE: src/manifest/mod.ts ================================================ import type { ISlackManifestRemote, ISlackManifestRunOnSlack, SlackManifestType, } from "./types.ts"; import type { ICustomType } from "../types/types.ts"; import type { ParameterSetDefinition } from "../parameters/types.ts"; import type { ManifestAppHomeMessagesTabSchema, ManifestAppHomeSchema, ManifestCustomEventsSchema, ManifestCustomTypesSchema, ManifestDataStoresSchema, ManifestFunction, ManifestFunctionRuntime, ManifestFunctionsSchema, ManifestSchema, ManifestWorkflowsSchema, } from "./manifest_schema.ts"; import { isCustomType } from "../types/mod.ts"; import { isCustomFunctionDefinition } from "../functions/definitions/slack-function.ts"; import { DuplicateCallbackIdError, DuplicateNameError, DuplicateProviderKeyError, } from "./errors.ts"; export const Manifest = ( definition: Omit<ISlackManifestRunOnSlack, "runOnSlack">, ) => { const manifest = new SlackManifest(definition); return manifest.export(); }; export class SlackManifest { constructor(private definition: SlackManifestType) { this.registerFeatures(); } export() { const def = this.definition; const manifest: ManifestSchema = { _metadata: { // todo: is there a more idiomatic way of defining this? constant file? major_version: 2, }, display_information: { background_color: def.backgroundColor, name: def.name, long_description: def.longDescription, description: def.description, }, icon: def.icon, oauth_config: { scopes: { bot: this.ensureBotScopes(), }, }, features: { bot_user: { display_name: def.displayName || def.name, }, }, settings: { function_runtime: this.getFunctionRuntime() }, }; // Assign other shared properties if (def.functions) { manifest.functions = def.functions.reduce<ManifestFunctionsSchema>( (acc = {}, fn) => { if (isCustomFunctionDefinition(fn)) { if (fn.id in acc) { throw new DuplicateCallbackIdError(fn.id, "Function"); } acc[fn.id] = fn.export(); } return acc; }, {}, ); } if (def.workflows) { manifest.workflows = def.workflows.reduce<ManifestWorkflowsSchema>( (acc = {}, workflow) => { if (workflow.id in acc) { throw new DuplicateCallbackIdError(workflow.id, "Workflow"); } acc[workflow.id] = workflow.export(); return acc; }, {}, ); } if (def.types) { manifest.types = def.types.reduce<ManifestCustomTypesSchema>( (acc = {}, customType) => { if (customType.id in acc) { throw new DuplicateNameError(customType.id, "CustomType"); } acc[customType.id] = customType.export(); return acc; }, {}, ); } if (def.datastores) { manifest.datastores = def.datastores.reduce<ManifestDataStoresSchema>( (acc = {}, datastore) => { if (datastore.name in acc) { throw new DuplicateNameError(datastore.name, "Datastore"); } acc[datastore.name] = datastore.export(); return acc; }, {}, ); } if (def.events) { manifest.events = def.events.reduce<ManifestCustomEventsSchema>( (acc = {}, event) => { if (event.id in acc) { throw new DuplicateNameError(event.id, "CustomEvent"); } acc[event.id] = event.export(); return acc; }, {}, ); } manifest.outgoing_domains = def.outgoingDomains || []; // Assign remote hosted app properties if (manifest.settings.function_runtime === "slack") { this.assignRunOnSlackManifestProperties(manifest); } else if (manifest.settings.function_runtime === "remote") { this.assignRemoteSlackManifestProperties(manifest); } return manifest; } private registerFeatures() { this.definition.workflows?.forEach((workflow) => { workflow.registerStepFunctions(this); workflow.registerParameterTypes(this); }); // Loop through functions to automatically register any referenced types this.definition.functions?.forEach((func) => { if (isCustomFunctionDefinition(func)) { func.registerParameterTypes(this); } }); // Loop through datastores to automatically register any referenced types this.definition.datastores?.forEach((datastore) => { datastore.registerAttributeTypes(this); }); // Loop through events to automatically register any referenced types this.definition.events?.forEach((event) => { event.registerParameterTypes(this); }); // Loop through types to automatically register any referenced sub-types const registeredTypes = this.definition.types || []; for (let i = 0; i < registeredTypes.length; i++) { this.definition.types?.[i].registerParameterTypes(this); } } registerFunction(func: ManifestFunction) { if (!this.definition.functions) this.definition.functions = []; // Check to make sure function doesn't already exist on manifest else if (this.definition.functions.some((f) => func.id === f.id)) return; // Add function to manifest this.definition.functions.push(func); } // Loop through a ParameterSetDefinition to register each individual type registerTypes(parameterSet: ParameterSetDefinition) { Object.values(parameterSet).forEach((param) => { if (isCustomType(param.type)) { this.registerType(param.type); } }); } registerType(customType: ICustomType) { if (!this.definition.types) this.definition.types = []; // Don't register Slack types if (customType.id.startsWith("slack#/")) { return; } // Check to make sure type doesn't already exist on manifest if (this.definition.types.some((type) => type.id === customType.id)) { return; } // Add type to manifest this.definition.types.push(customType); } /** * Verifies scopes defined in the app passes baseline validation. * @returns {string[]} The user-defined manifest scopes from `definition.botScopes` */ private ensureBotScopes(): string[] { // Warn about missing datastore scopes if app includes datastores if (Object.keys(this.definition.datastores ?? {}).length > 0) { const missingScopes: string[] = []; const datastoreScopes = ["datastore:read", "datastore:write"]; datastoreScopes.forEach((scope) => { if (!this.definition.botScopes.includes(scope)) { missingScopes.push(scope); } }); if (missingScopes.length > 0) { console.warn( `Warning! Application manifest includes at least one datastore, but does not specify the following datastore-related scopes in its 'botScopes': ${ missingScopes.join(", ") }`, ); } } return this.definition.botScopes; } // Maps the top level runOnSlack boolean property to corresponding underlying ManifestSchema function_runtime property required by Slack API. // If no runOnSlack property supplied, then functionRuntime defaults to "slack". private getFunctionRuntime(): ManifestFunctionRuntime { return this.definition.runOnSlack === false ? "remote" : "slack"; } // Assigns the remote app properties private assignRemoteSlackManifestProperties(manifest: ManifestSchema) { const def = this.definition as ISlackManifestRemote; //Settings manifest.settings = { ...manifest.settings, ...def.settings, }; manifest.settings.event_subscriptions = def.eventSubscriptions; manifest.settings.socket_mode_enabled = def.socketModeEnabled; manifest.settings.token_rotation_enabled = def.tokenRotationEnabled; // Set app home features if (def.features?.appHome) { const { homeTabEnabled, messagesTabEnabled, messagesTabReadOnlyEnabled, } = def.features.appHome; manifest.features.app_home = { home_tab_enabled: homeTabEnabled, messages_tab_enabled: messagesTabEnabled, messages_tab_read_only_enabled: messagesTabReadOnlyEnabled, } as ManifestAppHomeSchema; } // Set org deploy enabled to true unless specified by dev // Org deploy enabled is required to use remote functions manifest.settings.org_deploy_enabled = (def.settings?.org_deploy_enabled !== undefined) ? def.settings?.org_deploy_enabled : true; //AppDirectory manifest.app_directory = def.appDirectory; //OauthConfig manifest.oauth_config.scopes.user = def.userScopes; manifest.oauth_config.redirect_urls = def.redirectUrls; // Remote-hosted Slack apps manage their own tokens manifest.oauth_config.token_management_enabled = true; // Remote Features manifest.features.bot_user!.always_online = def.features?.botUser ?.always_online; manifest.features.shortcuts = def.features?.shortcuts; manifest.features.slash_commands = def.features?.slashCommands; manifest.features.unfurl_domains = def.features?.unfurlDomains; manifest.features.workflow_steps = def.features?.workflowSteps; } private assignRunOnSlackManifestProperties(manifest: ManifestSchema) { const def = this.definition as ISlackManifestRunOnSlack; // Run on Slack Apps do not manage access tokens // This is set by default as false manifest.oauth_config.token_management_enabled = false; // Required App Settings for run on slack apps manifest.settings.org_deploy_enabled = true; // App Home // Default to messages enabled manifest.features.app_home = { messages_tab_enabled: true, messages_tab_read_only_enabled: true, } as ManifestAppHomeMessagesTabSchema; // Allow App Home override values if provided in apphome if (def.features?.appHome) { const { messagesTabEnabled, messagesTabReadOnlyEnabled, } = def.features.appHome; manifest.features.app_home.messages_tab_enabled = messagesTabEnabled; manifest.features.app_home.messages_tab_read_only_enabled = messagesTabReadOnlyEnabled; } // External Auth providers if (def.externalAuthProviders?.length) { manifest.external_auth_providers = def.externalAuthProviders.reduce( (acc, provider) => { acc["oauth2"] = acc["oauth2"] ?? {}; if (provider.id in acc["oauth2"]) { throw new DuplicateProviderKeyError(provider.id, "OAuth2Provider"); } acc["oauth2"][provider.id] = provider.export(); return acc; }, {} as NonNullable<ManifestSchema["external_auth_providers"]>, ); } } } ================================================ FILE: src/manifest/types.ts ================================================ import type { ManifestAppDirectorySchema, ManifestAppHomeMessagesTabSchema, ManifestAppHomeSchema, ManifestBotUserSchema, ManifestDatastore, ManifestEventSubscriptionsSchema, ManifestFunction, ManifestSettingsSchema, ManifestShortcutsSchema, ManifestSlashCommandsSchema, ManifestUnfurlDomainsSchema, ManifestWorkflow, ManifestWorkflowStepsSchemaLegacy, } from "./manifest_schema.ts"; import type { OAuth2Provider } from "../providers/oauth2/mod.ts"; import type { ICustomType } from "../types/types.ts"; import type { CamelCasedPropertiesDeep } from "./types_util.ts"; import type { ICustomEvent } from "../events/types.ts"; /** Manifest definition. * * SlackManifestType contains affordances for better user experience (e.g runOnSlack property) * The lower level ManifestSchema aligns with Slack API * * A discriminated union where the discriminant property runOnSlack * maps to function_runtime in the underlying ManifestSchema. */ export type SlackManifestType = | ISlackManifestRunOnSlack | ISlackManifestRemote; /** Slack-hosted app manifest * * When runOnSlack = true * Corresponds to function_runtime = slack in ManifestSchema. */ export interface ISlackManifestRunOnSlack extends ISlackManifestShared { runOnSlack?: true; // maps to function_runtime = "slack" in ManifestSchema, optional since the apps are slack hosted by default features?: ISlackManifestRunOnSlackFeaturesSchema; externalAuthProviders?: (OAuth2Provider /*|OAuth1Provider*/)[]; } /** Non-Slack hosted app manifest * * When runOnSlack = false. * Corresponds to function_runtime = remote in ManifestSchema. */ export interface ISlackManifestRemote extends ISlackManifestShared { runOnSlack: false; // maps to function_runtime = "remote" in ManifestSchema settings?: Omit< ManifestSettingsSchema, | "event_subscriptions" | "socket_mode_enabled" | "token_rotation_enabled" | "function_runtime" >; // lifting omitted properties to top level eventSubscriptions?: ManifestEventSubscriptionsSchema; socketModeEnabled?: boolean; tokenRotationEnabled?: boolean; appDirectory?: ManifestAppDirectorySchema; userScopes?: Array<string>; redirectUrls?: Array<string>; features?: ISlackManifestRemoteFeaturesSchema; } /* Shared app manifest properties */ interface ISlackManifestShared { name: string; backgroundColor?: string; description: string; displayName?: string; icon: string; longDescription?: string; botScopes: Array<string>; functions?: ManifestFunction[]; workflows?: ManifestWorkflow[]; outgoingDomains?: Array<string>; events?: ICustomEvent[]; types?: ICustomType[]; datastores?: ManifestDatastore[]; } interface ISlackManifestRunOnSlackFeaturesSchema { // currently home_tab_enabled is not supported for RunOnSlack apps appHome?: CamelCasedPropertiesDeep<ManifestAppHomeMessagesTabSchema>; } interface ISlackManifestRemoteFeaturesSchema { appHome?: CamelCasedPropertiesDeep<ManifestAppHomeSchema>; botUser?: Omit<ManifestBotUserSchema, "display_name">; shortcuts?: ManifestShortcutsSchema; slashCommands?: ManifestSlashCommandsSchema; unfurlDomains?: ManifestUnfurlDomainsSchema; workflowSteps?: ManifestWorkflowStepsSchemaLegacy; } ================================================ FILE: src/manifest/types_util.ts ================================================ /** Utility types to enable conversion of type properties to camelCase * * This is a shameless lift from sindresorhus's awesome type-fest project * * https://github.com/sindresorhus/type-fest * * Imported simply because it hasn't been distributed to deno.land yet * Please support this project! */ type Split< S extends string, Delimiter extends string, > = S extends `${infer Head}${Delimiter}${infer Tail}` ? [Head, ...Split<Tail, Delimiter>] : S extends Delimiter ? [] : [S]; //deno-fmt-ignore type CamelCase<K> = K extends string ? CamelCaseStringArray< Split<K extends Uppercase<K> ? Lowercase<K> : K, "-" | "_" | " "> > : K; //deno-fmt-ignore type CamelCaseStringArray<Parts extends readonly string[]> = Parts extends [`${infer FirstPart}`, ...infer RemainingParts] ? Uncapitalize< `${FirstPart}${InnerCamelCaseStringArray<RemainingParts, FirstPart>}` > : never; // deno-lint-ignore no-explicit-any type InnerCamelCaseStringArray<Parts extends readonly any[], PreviousPart> = Parts extends [`${infer FirstPart}`, ...infer RemainingParts] ? FirstPart extends undefined ? "" : FirstPart extends "" ? InnerCamelCaseStringArray<RemainingParts, PreviousPart> : `${PreviousPart extends "" ? FirstPart : Capitalize<FirstPart>}${InnerCamelCaseStringArray< RemainingParts, FirstPart >}` : ""; // deno-lint-ignore ban-types export type CamelCasedPropertiesDeep<Value> = Value extends Function ? Value : Value extends Array<infer U> ? Array<CamelCasedPropertiesDeep<U>> : Value extends Set<infer U> ? Set<CamelCasedPropertiesDeep<U>> : { [K in keyof Value as CamelCase<K>]: CamelCasedPropertiesDeep<Value[K]>; }; ================================================ FILE: src/mod.ts ================================================ export { Manifest, SlackManifest } from "./manifest/mod.ts"; export type { ISlackManifestRemote, ISlackManifestRunOnSlack, SlackManifestType, } from "./manifest/types.ts"; export type { ManifestSchema } from "./manifest/manifest_schema.ts"; export { DefineFunction } from "./functions/mod.ts"; export { SlackFunction } from "./functions/slack-function.ts"; export { BlockActionsRouter, ViewsRouter, } from "./functions/interactivity/mod.ts"; export type { ViewEvents } from "./functions/interactivity/view_types.ts"; export { DefineWorkflow } from "./workflows/mod.ts"; export { DefineEvent } from "./events/mod.ts"; export { DefineType } from "./types/mod.ts"; export { DefineOAuth2Provider } from "./providers/oauth2/mod.ts"; export { default as Schema } from "./schema/mod.ts"; export { DefineDatastore } from "./datastore/mod.ts"; export { SlackFunctionTester } from "./functions/tester/mod.ts"; export { SlackAPI } from "./deps.ts"; export { DefineProperty } from "./parameters/define_property.ts"; ================================================ FILE: src/mod_test.ts ================================================ import { assertExists } from "./dev_deps.ts"; import * as mod from "./mod.ts"; Deno.test("Include all content of mod.ts in code coverage", () => { assertExists(mod); }); ================================================ FILE: src/parameters/define_property.ts ================================================ import type { TypedObjectParameterDefinition, TypedObjectProperties, TypedObjectRequiredProperties, } from "../parameters/definition_types.ts"; export const DefineProperty = < Props extends TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props>, Def extends TypedObjectParameterDefinition<Props, RequiredProps>, >( definition: Def, ) => { return definition; }; ================================================ FILE: src/parameters/define_property_test.ts ================================================ import { DefineProperty } from "./define_property.ts"; import SchemaTypes from "../schema/schema_types.ts"; import { assert, type IsExact } from "../dev_deps.ts"; Deno.test("DefineProperty should allow for object property names to be specified in the required field", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, anOptionalString: { type: SchemaTypes.string, }, }, required: ["aString"], additionalProperties: true, }); assert<IsExact<typeof obj.required, "aString"[]>>(true); }); /* TODO: DefineProperty fails to constrain the required field to property names :( Deno.test("DefineProperty should prevent non-object property names to be specified in the required field", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, anOptionalString: { type: SchemaTypes.string, }, }, // @ts-expect-error should not allow for bogus property names required: ["bogus"], additionalProperties: true, }); }); */ ================================================ FILE: src/parameters/definition_types.ts ================================================ import type SchemaTypes from "../schema/schema_types.ts"; import type { ValidSchemaTypes } from "../schema/schema_types.ts"; import type { SlackPrimitiveTypes, ValidSlackPrimitiveTypes, } from "../schema/slack/types/mod.ts"; import type { LooseStringAutocomplete } from "../type_utils.ts"; import type { ICustomType } from "../types/types.ts"; export type ParameterDefinition = TypedParameterDefinition; export type PrimitiveParameterDefinition = | BooleanParameterDefinition | StringParameterDefinition | NumberParameterDefinition | IntegerParameterDefinition | BaseParameterDefinition<AllValues> // | UntypedArrayParameterDefinition | TypedArrayParameterDefinition; export type TypedParameterDefinition = | CustomTypeParameterDefinition | TypedObjectParameter | UntypedObjectParameterDefinition | PrimitiveParameterDefinition | OAuth2ParameterDefinition; export interface CustomTypeParameterDefinition extends Omit<BaseParameterDefinition<AllValues>, "type"> { type: ICustomType; } interface BaseParameterDefinition<T> { /** Defines the parameter type. */ type: LooseStringAutocomplete<ValidSchemaTypes | ValidSlackPrimitiveTypes>; /** An optional parameter title. */ title?: string; /** An optional parameter description. */ description?: string; /** An optional parameter hint. */ hint?: string; /** An optional parameter default value. */ default?: T; /** An option list of examples; intended for future use in a possible app type schemas page. */ examples?: T[]; } /** * Only used for defining Custom Types via `DefineType` * The below type is explicitly different from the above ParameterDefinition type in that: * - It replaces the generic-less ComplexParameterDefinition so that... * - It can lift the generic-ful TypeddObjectParamaterDefinition's generics to ParameterDefinitionWithgenerics so that... * - The props/required props generic pair, which rely on each other, can be exposed in DefineType so that... * - .. the dependency between props/required props can be raised to the dev when authoring function runtime logic, and e.g. not returning a required property in a function output */ export type ParameterDefinitionWithGenerics< Props extends TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props>, > = | Exclude<ParameterDefinition, TypedObjectParameter> | TypedObjectParameterDefinition<Props, RequiredProps>; export interface UntypedObjectParameterDefinition extends BaseParameterDefinition<ObjectValue> { type: typeof SchemaTypes.object; } export type TypedObjectProperties = { [key: string]: | PrimitiveParameterDefinition | CustomTypeParameterDefinition; }; export type TypedObjectRequiredProperties<Props extends TypedObjectProperties> = | (Exclude<keyof Props, symbol>)[] | undefined; /** * Models the shape of a Typed Object parameter, and using the two generics, * models the dependent relationship between the properties of an object and which of the properties are required vs. optional */ export interface TypedObjectParameterDefinition< Props extends TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props>, > extends UntypedObjectParameterDefinition // TODO: the second type parameter would not accurately reflect what typed objects would look like - would limit to flat objects only. { /** * Whether the parameter can accept objects with additional keys beyond those defined via `properties` * @default "true" */ additionalProperties?: boolean; /** Object defining what properties are allowed on the parameter. */ properties: Props; /** A list of required property names (must reference names defined on the `properties` property). Only for use with Object types. */ required?: RequiredProps; } /** * Models _only_ the shape of a Typed Object parameter * Unlike TypedObjectParameterDefinition above, does _not_ constrain the elements * of the `required` array to the keys of the `properties` object. */ export type TypedObjectParameter = TypedObjectParameterDefinition< TypedObjectProperties, TypedObjectRequiredProperties<TypedObjectProperties> >; interface BooleanParameterDefinition extends BaseParameterDefinition<boolean> { type: typeof SchemaTypes.boolean; } interface StringParameterDefinition extends BaseParameterDefinition<string> { type: typeof SchemaTypes.string; /** Minimum number of characters comprising the string */ minLength?: number; /** Maximum number of characters comprising the string */ maxLength?: number; /** Constrain the available string options to just the list of strings denoted in the `enum` property. Usage of `enum` also instructs any UI that collects a value for this parameter to render a dropdown select input rather than a free-form text input. */ enum?: string[]; /** Defines labels that correspond to the `enum` values. */ choices?: EnumChoice<string>[]; /** Define accepted format of the string */ format?: "url" | "email"; } interface IntegerParameterDefinition extends BaseParameterDefinition<number> { type: typeof SchemaTypes.integer; /** Absolute minimum acceptable value for the integer */ minimum?: number; /** Absolute maximum acceptable value for the integer */ maximum?: number; /** Constrain the available integer options to just the list of integers denoted in the `enum` property. Usage of `enum` also instructs any UI that collects a value for this parameter to render a dropdown select input rather than a free-form text input. */ enum?: number[]; /** Defines labels that correspond to the `enum` values. */ choices?: EnumChoice<number>[]; } interface NumberParameterDefinition extends BaseParameterDefinition<number> { type: typeof SchemaTypes.number; /** Absolute minimum acceptable value for the number */ minimum?: number; /** Absolute maximum acceptable value for the number */ maximum?: number; /** Constrain the available number options to just the list of numbers denoted in the `enum` property. Usage of `enum` also instructs any UI that collects a value for this parameter to render a dropdown select input rather than a free-form text input. */ enum?: number[]; /** Defines labels that correspond to the `enum` values. */ choices?: EnumChoice<number>[]; } interface OAuth2ParameterDefinition extends BaseParameterDefinition<string> { type: typeof SlackPrimitiveTypes.oauth2; /** Specifies the oauth2 provider this input is associated to */ oauth2_provider_key: string; /** Dictates whether only the auth of the user running the workflow should be used. Defaults to `false`. */ require_end_user_auth?: boolean; } type EnumChoice<T> = { /** The `enum` value this {@link EnumChoice} corresponds to. */ value: T; /** The label to display for this {@link EnumChoice}. */ title: string; /** An optional description for this {@link EnumChoice}. Intended for potential future use in a possible app type schemas page. */ description?: string; }; interface UntypedArrayParameterDefinition extends BaseParameterDefinition<ArrayValue> { type: typeof SchemaTypes.array; /** Minimum number of items comprising the array */ minItems?: number; /** Maximum number of items comprising the array */ maxItems?: number; } export interface TypedArrayParameterDefinition extends UntypedArrayParameterDefinition { /** Defines the type of the items contained within the array parameter. */ items: ParameterDefinition; } type AllValues = AllPrimitiveValues | ObjectValue | ArrayValue; type AllPrimitiveValues = string | number | boolean; type ObjectValue = { [key: string]: AllPrimitiveValues | AllPrimitiveValues[]; }; type ArrayValue = AllPrimitiveValues[]; ================================================ FILE: src/parameters/mod.ts ================================================ // import SchemaTypes from "../schema/schema_types.ts"; import type { ObjectParameterVariableType, ParameterVariableType, SingleParameterVariable, UntypedObjectParameterVariableType, } from "./types.ts"; import type { ParameterDefinition, TypedArrayParameterDefinition, TypedObjectParameter, TypedObjectParameterDefinition, TypedObjectProperties, TypedObjectRequiredProperties, } from "./definition_types.ts"; import { ParamReference } from "./param.ts"; import { WithUntypedObjectProxy } from "./with-untyped-object-proxy.ts"; import SchemaTypes from "../schema/schema_types.ts"; import { isCustomType } from "../types/mod.ts"; // Helpers that use type predicate for narrowing down to a Typed Object or Array export const isTypedObject = ( def: ParameterDefinition, ): def is TypedObjectParameter => ("properties" in def); export const isTypedArray = ( def: ParameterDefinition, ): def is TypedArrayParameterDefinition => ("items" in def); export const ParameterVariable = <P extends ParameterDefinition>( namespace: string, paramName: string, definition: P, ): ParameterVariableType<P> => { let param: ParameterVariableType<P> | null = null; if (isCustomType(definition.type)) { return ParameterVariable( namespace, paramName, definition.type.definition, ); } else if (definition.type === SchemaTypes.object) { if (isTypedObject(definition)) { param = CreateTypedObjectParameterVariable( namespace, paramName, definition, ) as ParameterVariableType<P>; } else { param = CreateUntypedObjectParameterVariable(namespace, paramName); } } else { param = CreateSingleParameterVariable( namespace, paramName, ) as ParameterVariableType<P>; } return param as ParameterVariableType<P>; }; const CreateTypedObjectParameterVariable = < Props extends TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props>, P extends TypedObjectParameterDefinition< Props, RequiredProps >, >( namespace: string, paramName: string, definition: P, ): ObjectParameterVariableType<P> => { const ns = namespace ? `${namespace}.` : ""; const pathReference = `${ns}${paramName}`; const param = ParamReference(pathReference); for ( const [propName, propDefinition] of Object.entries( definition.properties || {}, ) ) { param[propName as string] = ParameterVariable( pathReference, propName, propDefinition, ); } // We wrap the typed object parameter w/ an untyped proxy to allow indexing into additional properties return WithUntypedObjectProxy( param, namespace, paramName, ) as ObjectParameterVariableType<P>; }; export const CreateUntypedObjectParameterVariable = ( namespace: string, paramName: string, ): UntypedObjectParameterVariableType => { return WithUntypedObjectProxy( {}, namespace, paramName, ) as UntypedObjectParameterVariableType; }; const CreateSingleParameterVariable = ( namespace: string, paramName: string, ): SingleParameterVariable => { return ParamReference(namespace, paramName) as SingleParameterVariable; }; ================================================ FILE: src/parameters/param.ts ================================================ // deno-lint-ignore no-explicit-any export const ParamReference = (...path: (string | undefined)[]): any => { const fullPath = path.filter(Boolean).join("."); return { toString: () => `{{${fullPath}}}`, toJSON: () => `{{${fullPath}}}`, }; }; ================================================ FILE: src/parameters/param_test.ts ================================================ import { assertEquals } from "../dev_deps.ts"; import { ParamReference } from "./param.ts"; Deno.test(ParamReference.name, async (t) => { await t.step("should return a . separated string reference value", () => { const actual = ParamReference("hello", "world"); assertEquals(actual.toString(), "{{hello.world}}"); assertEquals(actual.toJSON(), "{{hello.world}}"); }); await t.step( "should filter undefined values from string reference", () => { const actual = ParamReference("hello", undefined, "world"); assertEquals(actual.toString(), "{{hello.world}}"); assertEquals(actual.toJSON(), "{{hello.world}}"); }, ); }); ================================================ FILE: src/parameters/parameter-variable_test.ts ================================================ import SchemaTypes from "../schema/schema_types.ts"; import { ParameterVariable } from "./mod.ts"; import type { SingleParameterVariable } from "./types.ts"; import { assert, assertStrictEquals, type IsAny, type IsExact, } from "../dev_deps.ts"; import type { CannotBeUndefined } from "../test_utils.ts"; /** * ParameterVariable-wrapped parameters should yield particular types */ Deno.test("ParameterVariable of type string yields a SingleParameterVariable type that coerces into a string containing the provided parameter name", () => { const param = ParameterVariable("", "incident_name", { type: SchemaTypes.string, }); assertStrictEquals(`${param}`, "{{incident_name}}"); }); Deno.test("ParameterVariable untyped object should yield a parameter of type any", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, }); assert<IsAny<typeof param>>(true); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.name.foo.bar}`, "{{incident.name.foo.bar}}"); }); Deno.test("ParameterVariable array should yield SingleParameterVariable type", () => { const param = ParameterVariable("", "myArray", { type: SchemaTypes.array, items: { type: SchemaTypes.string, }, }); assert<IsExact<typeof param, SingleParameterVariable>>(true); assertStrictEquals(`${param}`, "{{myArray}}"); assertStrictEquals(`${param}`, "{{myArray}}"); }); Deno.test("ParameterVariable unwrapped typed object with all optional properties should never yield object with potentially undefined properties", () => { const obj = { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: [], }; const param = ParameterVariable("", "incident", obj); assert<CannotBeUndefined<typeof param.id>>(true); assert<CannotBeUndefined<typeof param.name>>(true); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); }); Deno.test("ParameterVariable unwrapped typed object with all required properties should yield object with properties that cannot be undefined", () => { const obj = { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id", "name"], }; const param = ParameterVariable("", "incident", obj); assert<CannotBeUndefined<typeof param.id>>(true); assert<CannotBeUndefined<typeof param.name>>(true); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); }); Deno.test("ParameterVariable unwrapped typed object with mix of optional and required properties should yield object with no undefined properties", () => { const obj = { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id"], }; const param = ParameterVariable("", "incident", obj); assert<CannotBeUndefined<typeof param.id>>(true); assert<CannotBeUndefined<typeof param.name>>(true); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); }); Deno.test("ParameterVariable unwrapped typed object with all optional properties and undefined additionalProperties allows access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: [], }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable unwrapped typed object with all required properties and undefined additionalProperties allows access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id", "name"], }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable unwrapped typed object with mix of required and optional properties and undefined additionalProperties allows access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id"], }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable unwrapped typed object with all optional properties and additionalProperties=true allows access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: [], additionalProperties: true, }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable unwrapped typed object with all required properties and additionalProperties=true allows access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id", "name"], additionalProperties: true, }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable unwrapped typed object with mix of required and optional properties and additionalProperties=true allows access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id"], additionalProperties: true, }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable unwrapped typed object with all optional properties and additionalProperties=false prevents access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: [], additionalProperties: false, }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable unwrapped typed object with all required properties and additionalProperties=false prevents access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id", "name"], additionalProperties: false, }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable unwrapped typed object with mix of required and optional properties and additionalProperties=false prevents access to additional properties", () => { const param = ParameterVariable("", "incident", { type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id"], additionalProperties: false, }); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); ================================================ FILE: src/parameters/types.ts ================================================ import type { IncreaseDepth, MaxRecursionDepth, RecursionDepthLevel, } from "../type_utils.ts"; import type { CustomTypeParameterDefinition, ParameterDefinition, TypedObjectParameter, TypedObjectProperties, UntypedObjectParameterDefinition, } from "./definition_types.ts"; // Used for defining a set of input or output parameters export type ParameterSetDefinition = { [key: string]: ParameterDefinition; }; export type PossibleParameterKeys< ParameterSetInternal extends ParameterSetDefinition, > = (keyof ParameterSetInternal)[]; export type ParameterPropertiesDefinition< Params extends ParameterSetDefinition, Required extends PossibleParameterKeys<Params>, > = { properties: Params; required: Required; }; export type ParameterVariableType< Def extends ParameterDefinition, CurrentDepth extends RecursionDepthLevel = 0, > = CurrentDepth extends MaxRecursionDepth ? UntypedObjectParameterVariableType // i.e. any : Def extends CustomTypeParameterDefinition // If the ParameterVariable is a Custom type, use it's definition instead ? ParameterVariableType< Def["type"]["definition"], IncreaseDepth<CurrentDepth> > // If the ParameterVariable is of type object, allow access to the object's properties : Def extends TypedObjectParameter ? ObjectParameterVariableType<Def> : Def extends UntypedObjectParameterDefinition ? UntypedObjectParameterVariableType : SingleParameterVariable; // deno-lint-ignore ban-types export type SingleParameterVariable = {}; // deno-lint-ignore no-explicit-any export type UntypedObjectParameterVariableType = any; export type ObjectParameterPropertyTypes< Props extends TypedObjectProperties, > = { [name in keyof Props]: ParameterVariableType< Props[name] >; }; // If additionalProperties is set to true, allow access to any key. // Otherwise, only allow keys provided through use of properties export type ObjectParameterVariableType< Def extends TypedObjectParameter, > = & ObjectParameterPropertyTypes<Def["properties"]> & (Def["additionalProperties"] extends false ? Record<never, never> : { // deno-lint-ignore no-explicit-any [key: string]: any; }); ================================================ FILE: src/parameters/with-untyped-object-proxy.ts ================================================ import { ParamReference } from "./param.ts"; export const WithUntypedObjectProxy = ( // deno-lint-ignore no-explicit-any rootObject: Record<string, any>, ...path: (string | undefined)[] // deno-lint-ignore no-explicit-any ): any => { const parameterizedObject = { ...rootObject, ...ParamReference(...path), }; const proxy = new Proxy(parameterizedObject, { get: function (obj, prop) { // If it's a property that exists, just access it directly if (prop in obj) { // deno-lint-ignore no-explicit-any return Reflect.get.apply(obj, arguments as any); } // We're attempting to access a property that doesn't exist, so create a new nested proxy if (typeof prop === "string") { return WithUntypedObjectProxy(obj, ...path, prop); } // Fallback to trying to access it directly even if it's not in this objects props // deno-lint-ignore no-explicit-any return Reflect.get.apply(obj, arguments as any); }, }); return proxy; }; ================================================ FILE: src/parameters/with-untyped-object-proxy_test.ts ================================================ import { WithUntypedObjectProxy } from "./with-untyped-object-proxy.ts"; import { assertStrictEquals } from "../dev_deps.ts"; Deno.test("WithUntypedObjectProxy", () => { const ctx = WithUntypedObjectProxy({}); assertStrictEquals(`${ctx.foo}`, "{{foo}}"); assertStrictEquals(`${ctx.foo.baz}`, "{{foo.baz}}"); assertStrictEquals( `${ctx.foo.baz.biz.buzz.wut.wut.hi.bye}`, "{{foo.baz.biz.buzz.wut.wut.hi.bye}}", ); assertStrictEquals(`Some text ${ctx.variable}`, "Some text {{variable}}"); }); Deno.test("WithUntypedObjectProxy with namespace", () => { const ctx = WithUntypedObjectProxy({}, "metadata"); assertStrictEquals(`${ctx.foo}`, "{{metadata.foo}}"); assertStrictEquals(`${ctx.foo.baz}`, "{{metadata.foo.baz}}"); assertStrictEquals( `${ctx.foo.baz.biz.buzz.wut.wut.hi.bye}`, "{{metadata.foo.baz.biz.buzz.wut.wut.hi.bye}}", ); assertStrictEquals( `Some text ${ctx.variable}`, "Some text {{metadata.variable}}", ); }); ================================================ FILE: src/providers/oauth2/mod.ts ================================================ import type { OAuth2ProviderDefinitionArgs, OAuth2ProviderOptions, } from "./types.ts"; import type { OAuth2ProviderTypeValues } from "../../schema/providers/oauth2/types.ts"; import type { ManifestOAuth2ProviderSchema } from "../../manifest/manifest_schema.ts"; export const DefineOAuth2Provider = ( definition: OAuth2ProviderDefinitionArgs, ): OAuth2Provider => { return new OAuth2Provider(definition); }; export class OAuth2Provider { public id: string; private provider_type: OAuth2ProviderTypeValues; private options: OAuth2ProviderOptions; constructor( public definition: OAuth2ProviderDefinitionArgs, ) { this.id = definition.provider_key; this.provider_type = definition.provider_type; this.options = definition.options; } export(): ManifestOAuth2ProviderSchema { return { provider_type: this.provider_type, options: this.options, }; } } ================================================ FILE: src/providers/oauth2/oauth2_test.ts ================================================ import type { SlackManifestType } from "../../manifest/types.ts"; import { Manifest, SlackManifest } from "../../manifest/mod.ts"; import { DefineOAuth2Provider, Schema } from "../../mod.ts"; import { assertEquals, assertInstanceOf, AssertionError, assertStrictEquals, assertStringIncludes, fail, } from "../../dev_deps.ts"; import { DuplicateProviderKeyError } from "../../manifest/errors.ts"; Deno.test("SlackManifest() oauth2 throws error when Providers with duplicate provider_keys are added", () => { const provider1 = DefineOAuth2Provider({ provider_key: "test", provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a"], }, }); const provider2 = DefineOAuth2Provider({ provider_key: "test", provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a"], }, }); try { Manifest({ name: "Name", description: "Description", botScopes: [], icon: "icon.png", externalAuthProviders: [provider1, provider2], }); fail("Manifest() should have thrown an error"); } catch (error) { if (error instanceof AssertionError) throw error; assertInstanceOf(error, DuplicateProviderKeyError); assertStringIncludes(error.message, "OAuth2Provider"); } }); Deno.test("SlackManifest() oauth2 providers get set properly", () => { const providerKey = "test_provider"; const Provider = DefineOAuth2Provider({ provider_key: providerKey, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], }, }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], externalAuthProviders: [Provider], }; const Manifest = new SlackManifest(definition); const exportedManifest = Manifest.export(); assertEquals(definition.externalAuthProviders, [Provider]); assertEquals(exportedManifest.external_auth_providers, { "oauth2": { "test_provider": Provider.export() }, }); }); Deno.test("SlackManifest() oauth2 providers get set properly with use_pkce", () => { const providerKey1 = "test_provider_with_with_pkce_true"; const providerKey2 = "test_provider_with_with_pkce_false"; const providerKey3 = "test_provider_with_with_pkce_unset"; const Provider1 = DefineOAuth2Provider({ provider_key: providerKey1, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], "use_pkce": true, }, }); const Provider2 = DefineOAuth2Provider({ provider_key: providerKey2, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], "use_pkce": false, }, }); const Provider3 = DefineOAuth2Provider({ provider_key: providerKey3, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], }, }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], externalAuthProviders: [Provider1, Provider2, Provider3], }; assertEquals(definition.externalAuthProviders, [ Provider1, Provider2, Provider3, ]); const Manifest = new SlackManifest(definition); const exportedManifest = Manifest.export(); assertEquals(exportedManifest.external_auth_providers, { "oauth2": { "test_provider_with_with_pkce_true": Provider1.export(), "test_provider_with_with_pkce_false": Provider2.export(), "test_provider_with_with_pkce_unset": Provider3.export(), }, }); assertStrictEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_with_pkce_true?.options?.use_pkce, true, ); assertStrictEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_with_pkce_false?.options?.use_pkce, false, ); assertStrictEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_with_pkce_unset?.options?.use_pkce, undefined, ); }); Deno.test("SlackManifest() oauth2 providers are undefined when not configured", () => { const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], }; const Manifest = new SlackManifest(definition); const exportedManifest = Manifest.export(); assertEquals(definition.externalAuthProviders, undefined); assertEquals(exportedManifest.external_auth_providers, undefined); }); Deno.test("SlackManifest() oauth2 providers are undefined when set to the empty array", () => { const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], externalAuthProviders: [], }; const Manifest = new SlackManifest(definition); const exportedManifest = Manifest.export(); assertEquals(definition.externalAuthProviders, []); assertEquals(exportedManifest.external_auth_providers, undefined); }); Deno.test("SlackManifest() oauth2 providers get set properly with token_url_config", () => { // test with token_url_config unset const providerKey1 = "test_provider_with_token_url_config_unset"; const Provider1 = DefineOAuth2Provider({ provider_key: providerKey1, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], "token_url_config": {}, }, }); // test with use_basic_auth_scheme false const providerKey2 = "test_provider_with_use_basic_auth_scheme_false"; const Provider2 = DefineOAuth2Provider({ provider_key: providerKey2, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], "token_url_config": { "use_basic_auth_scheme": false, }, }, }); // test with use_basic_auth_scheme true const providerKey3 = "test_provider_with_use_basic_auth_scheme_true"; const Provider3 = DefineOAuth2Provider({ provider_key: providerKey3, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], "token_url_config": { "use_basic_auth_scheme": true, }, }, }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], externalAuthProviders: [Provider1, Provider2, Provider3], }; assertEquals(definition.externalAuthProviders, [ Provider1, Provider2, Provider3, ]); const Manifest = new SlackManifest(definition); const exportedManifest = Manifest.export(); assertEquals(exportedManifest.external_auth_providers, { "oauth2": { "test_provider_with_token_url_config_unset": Provider1.export(), "test_provider_with_use_basic_auth_scheme_false": Provider2 .export(), "test_provider_with_use_basic_auth_scheme_true": Provider3 .export(), }, }); // test with use_basic_auth_scheme unset assertStrictEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_token_url_config_unset?.options ?.token_url_config?.use_basic_auth_scheme, undefined, ); // test with use_basic_auth_scheme false assertStrictEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_use_basic_auth_scheme_false?.options ?.token_url_config?.use_basic_auth_scheme, false, ); // test with use_basic_auth_scheme true assertStrictEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_use_basic_auth_scheme_true?.options ?.token_url_config?.use_basic_auth_scheme, true, ); }); Deno.test("SlackManifest() oauth2 providers get set properly with identity_config", () => { //test with identity_config containing required fields const providerKey1 = "test_provider_with_identity_config_required_fields_set1"; const Provider1 = DefineOAuth2Provider({ provider_key: providerKey1, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], "identity_config": { "url": "https://example.com", "account_identifier": "account_identifier_string", }, }, }); //test with identity_config containing all fields with POST method const providerKey2 = "test_provider_with_identity_config_with_all_fields_set2"; const Provider2 = DefineOAuth2Provider({ provider_key: providerKey2, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], "identity_config": { "url": "https://example.com", "account_identifier": "account_identifier_string", "headers": { "key1": "header_1", "key2": "header_2" }, "body": { "param1": "body_1", "param2": "body_2" }, "http_method_type": "POST", }, }, }); // test with identity_config containing all fields with GET method const providerKey3 = "test_provider_with_identity_config_with_all_fields_set3"; const Provider3 = DefineOAuth2Provider({ provider_key: providerKey3, provider_type: Schema.providers.oauth2.CUSTOM, options: { "client_id": "123.456", "scope": ["scope_a", "scope_b"], "identity_config": { "url": "https://example.com", "account_identifier": "account_identifier_string", "headers": { "key1": "header_1", "key2": "header_2" }, "body": { "param1": "body_1", "param2": "body_2" }, "http_method_type": "GET", }, }, }); const definition: SlackManifestType = { name: "Name", description: "Description", icon: "icon.png", botScopes: [], externalAuthProviders: [Provider1, Provider2, Provider3], }; assertEquals(definition.externalAuthProviders, [ Provider1, Provider2, Provider3, ]); const Manifest = new SlackManifest(definition); const exportedManifest = Manifest.export(); assertEquals(exportedManifest.external_auth_providers, { "oauth2": { "test_provider_with_identity_config_required_fields_set1": Provider1 .export(), "test_provider_with_identity_config_with_all_fields_set2": Provider2 .export(), "test_provider_with_identity_config_with_all_fields_set3": Provider3 .export(), }, }); //test with identity_config containing required fields assertEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_identity_config_required_fields_set1?.options ?.identity_config, { "url": "https://example.com", "account_identifier": "account_identifier_string", }, ); //test with identity_config containing all fields with POST method assertEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_identity_config_with_all_fields_set2?.options ?.identity_config, { "url": "https://example.com", "account_identifier": "account_identifier_string", "headers": { "key1": "header_1", "key2": "header_2" }, "body": { "param1": "body_1", "param2": "body_2" }, "http_method_type": "POST", }, ); //test with identity_config containing all fields with GET metthod assertEquals( exportedManifest.external_auth_providers?.oauth2 ?.test_provider_with_identity_config_with_all_fields_set3?.options ?.identity_config, { "url": "https://example.com", "account_identifier": "account_identifier_string", "headers": { "key1": "header_1", "key2": "header_2" }, "body": { "param1": "body_1", "param2": "body_2" }, "http_method_type": "GET", }, ); }); ================================================ FILE: src/providers/oauth2/types.ts ================================================ import type { OAuth2ProviderTypeValues, } from "../../schema/providers/oauth2/types.ts"; /** Http Method types that are currently supported by identity config */ export type IdentityUrlHttpMethodTypes = "GET" | "POST"; export type OAuth2ProviderIdentitySchema = { /** url that is used to identify the authed user */ "url": string; /** A field name that is returned in response to invoking the identity config url that * can be used as the account identifier of the authed user */ "account_identifier": string; /** Extra headers that the identity url might expect. * Note: It adds `Authorization` header automatically so no need to specify this. */ "headers"?: { [key: string]: string; }; /** static body parameters that the identity url expects. This is nullable since only POST methods use this */ "body"?: { [key: string]: string; }; /** method type of the identity url configured above. By default it is considered GET. */ "http_method_type"?: IdentityUrlHttpMethodTypes; }; export type tokenUrlConfigSchema = { /** Default value is false */ "use_basic_auth_scheme"?: boolean; }; export type OAuth2ProviderOptions = { /** Client id for your provider */ "client_id": string; /** Scopes for your provider */ "scope": string[]; /** Display name for your provider. Required for CUSTOM provider types. */ "provider_name"?: string; /** Authorization url for your provider. Required for CUSTOM provider types. */ "authorization_url"?: string; /** Token url for your provider. Required for CUSTOM provider types. */ "token_url"?: string; /** Optional configs for token url. Required for CUSTOM provider types. */ "token_url_config"?: tokenUrlConfigSchema; /** Identity configuration for your provider. Required for CUSTOM provider types. * If token_url_config is not present, use_basic_auth_scheme value is false by default. */ "identity_config"?: OAuth2ProviderIdentitySchema; /** Optional extras dict for authorization url for your provider. Required for CUSTOM provider types. */ "authorization_url_extras"?: { [key: string]: string }; /** Optional boolean flag to specify if the provider uses PKCE. by default it is considered false. Required for CUSTOM provider types. */ "use_pkce"?: boolean; }; export type OAuth2ProviderDefinitionArgs = { /** A unique name for your provider */ provider_key: string; /** Type of your provider */ provider_type: OAuth2ProviderTypeValues; /** OAuth2 Configuration options for your provider */ options: OAuth2ProviderOptions; }; ================================================ FILE: src/schema/mod.ts ================================================ import SchemaTypes from "./schema_types.ts"; import SlackSchema from "./slack/mod.ts"; import Providers from "./providers/mod.ts"; const Schema = { // Contains primitive types types: SchemaTypes, // Contains slack-specific schema types slack: SlackSchema, providers: Providers, } as const; export default Schema; ================================================ FILE: src/schema/providers/mod.ts ================================================ import OAuth2Types from "./oauth2/mod.ts"; const Schema = { oauth2: OAuth2Types, } as const; export default Schema; ================================================ FILE: src/schema/providers/oauth2/mod.ts ================================================ const ProviderTypes = { CUSTOM: "CUSTOM", } as const; export default ProviderTypes; ================================================ FILE: src/schema/providers/oauth2/types.ts ================================================ import type OAuth2ProviderTypes from "./mod.ts"; export type OAuth2ProviderTypeValues = typeof OAuth2ProviderTypes[keyof typeof OAuth2ProviderTypes]; ================================================ FILE: src/schema/schema_types.ts ================================================ const SchemaTypes = { string: "string", boolean: "boolean", integer: "integer", number: "number", object: "object", array: "array", } as const; export type ValidSchemaTypes = typeof SchemaTypes[keyof typeof SchemaTypes]; export default SchemaTypes; ================================================ FILE: src/schema/slack/functions/_scripts/.gitignore ================================================ functions.json ================================================ FILE: src/schema/slack/functions/_scripts/README.md ================================================ # Generating Slack function source files This script will generate the necessary function TypeScript files along with their tests in the `schema/slack/functions` directory, i.e. `schema/slack/functions/send_message.ts` and `schema/slack/functions/send_message_test.ts`. It will also update the `schema/slack/functions/mod.ts` file to import/export all of the defined functions. It will also remove outdated function TypeScript files but not their corresponding test, the tests must be removed manually. ## Instructions 1. First, you'll need to grab the response from `functions.list` API method tester: - Choose a session token from a public production enterprise grid workspace that is NOT enrolled in any beta toggles. Recommend using the Slack DevRel production enterprise grid token. - Use `builtins` as the value for the `function_type` parameter to this API. - Copy the response into a `functions.json` file in this directory. 2. With this `_scripts` directory as your working directory, run the generate script: ```sh > ./generate ``` If it completes without any linter errors, you should be good to go, with new, formatted and linted TypeScript files for all of the Slack functions included in your `functions.json` payload. If there are any unexpected linting issues, you may need to go into those files and manually resolve any problems. ================================================ FILE: src/schema/slack/functions/_scripts/generate ================================================ #!/bin/bash set -euo pipefail cd "$(dirname "$0")" # Clean parent directory of all files ending in .ts echo "Cleaning folder directory" ls -1 -d $PWD/../* | { grep "\.ts$" || :; } | while read -r filename ; do rm "$filename" done # Temporaraly generate a mod.ts file to prevent circular imports cat > $PWD/../mod.ts <<EOL const SlackFunctions = {}; export default SlackFunctions; EOL # Writes the function & test files based on a functions.json file existing alongside this script deno run --quiet --allow-read --allow-write ./src/write_function_files.ts echo "Formatting Slack function files..." deno fmt --quiet ../*.ts echo "Linting Slack function files..." deno lint --quiet ../*.ts echo "Type-checking Slack function files..." deno check --quiet ../*.ts ================================================ FILE: src/schema/slack/functions/_scripts/src/templates/mod.ts ================================================ export { SlackFunctionTemplate } from "./template_function.ts"; export { SlackTestFunctionTemplate } from "./test_template.ts"; export { SlackFunctionModTemplate } from "./template_mod.ts"; ================================================ FILE: src/schema/slack/functions/_scripts/src/templates/template_function.ts ================================================ import { isCustomType } from "../../../../../../types/mod.ts"; import SchemaTypes from "../../../../../schema_types.ts"; import SlackSchemaTypes from "../../../../schema_types.ts"; import { InternalSlackTypes } from "../../../../types/custom/mod.ts"; import type { FunctionParameter, FunctionProperties, FunctionProperty, FunctionRecord, } from "../types.ts"; import { isArrayFunctionProperty, isObjectFunctionProperty } from "../utils.ts"; import { autogeneratedComment, getSlackCallbackId, renderTypeImports, sanitize, } from "./utils.ts"; import type { AllowedTypeValue, AllowedTypeValueObject } from "./types.ts"; type AllowedHiddenParamsMap = Record< string, Record<"input" | "output", string[]> >; // Oops we accidentally exposed hidden parameters. That's ok, we'll keep them public for now. export const allowedHiddenParams: AllowedHiddenParamsMap = { "open_form": { input: [], output: ["interactivity"], }, "reply_in_thread": { input: ["files"], output: ["action", "interactivity"], }, "send_dm": { input: ["files"], output: [ "action", "interactivity", "timestamp_started", "timestamp_completed", ], }, "send_message": { input: ["files"], output: [ "action", "interactivity", "timestamp_started", "timestamp_completed", ], }, }; const typeMap: Record<string, AllowedTypeValueObject> = { SchemaTypes, SlackTypes: SlackSchemaTypes, InternalSlackTypes, }; const schemaTypeMap = Object.entries(typeMap).reduce<Record<string, string>>( (acc, [schemaKey, schemaTypes]) => { for (const schemaType in schemaTypes) { const value: AllowedTypeValue = schemaTypes[schemaType]; const type: string = isCustomType(value) ? value.id : value; acc[type] = `${schemaKey}.${schemaType}`; } return acc; }, {}, ); const propertyToTypeScript = ( property: FunctionProperty, ): string => { const typescript = []; const sdkType = schemaTypeMap[property.type]; if (!sdkType) { throw new Error( `Unrecognized type "${property.type}"! Maybe a new automation platform type was recently introduced? If so, add it to one of the type files under src/schema.`, ); } typescript.push( `type: ${sdkType}`, ); if (property.description) { typescript.push(`description: ${sanitize(property.description)}`); } if (property.title) { typescript.push(`title: ${sanitize(property.title)}`); } if (isArrayFunctionProperty(property)) { typescript.push(`items: ${propertyToTypeScript(property.items)}`); } if (isObjectFunctionProperty(property)) { typescript.push( `properties: ${propertiesToTypeScript(property.properties)}`, ); typescript.push( `additionalProperties: ${property.additionalProperties ?? true}`, ); typescript.push(`required: ${JSON.stringify(property.required ?? [])}`); } return `{${typescript.join(",\n")}}`; }; const propertiesToTypeScript = ( properties: FunctionProperties, ) => { const typescript: string[] = []; Object.entries(properties).forEach(([propertyKey, property]) => { typescript.push( `${propertyKey}: ${propertyToTypeScript(property)}`, ); }); return `{${typescript.join(",\n")}}`; }; const manifestParametersToTypeScript = ( allowedHiddenParams: string[], functionParameters: FunctionParameter[], ) => { const typescript: string[] = []; typescript.push( `properties: {${ functionParameters.filter((p) => allowedHiddenParams.includes(p.name) || !p.is_hidden ).map((parameter) => `${parameter.name}: ${propertyToTypeScript(parameter)}` ).join(",\n") }}`, ); typescript.push(`required: ${ JSON.stringify( functionParameters.filter((p) => p.is_required).map( (p) => p.name, ), ) }`); return `{${typescript.join(",\n")}}`; }; export function manifestFunctionFieldsToTypeScript( allowedParamsMap: AllowedHiddenParamsMap, functionRecord: FunctionRecord, ) { const typescript: string[] = []; typescript.push(`source_file: ""`); if (functionRecord.title) { typescript.push( `title: ${sanitize(functionRecord.title)}`, ); } if (functionRecord.description) { typescript.push( `description: ${sanitize(functionRecord.description)}`, ); } const allowedHiddenParams = allowedParamsMap[functionRecord.callback_id] || { input: [], output: [] }; typescript.push( `input_parameters: ${ manifestParametersToTypeScript( allowedHiddenParams.input, functionRecord.input_parameters, ) }`, ); typescript.push( `output_parameters: ${ manifestParametersToTypeScript( allowedHiddenParams.output, functionRecord.output_parameters, ) }`, ); return typescript.join(",\n"); } const defineFunctionInputToTypeScript = ( functionRecord: FunctionRecord, ) => { const typescript: string[] = []; typescript.push( `callback_id: ${sanitize(getSlackCallbackId(functionRecord))}`, ); typescript.push( manifestFunctionFieldsToTypeScript(allowedHiddenParams, functionRecord), ); return `{${typescript.join(",\n")}}`; }; export function SlackFunctionTemplate( functionRecord: FunctionRecord, ): string { const typescript: string[] = []; typescript.push(autogeneratedComment()); typescript.push( `import { DefineFunction } from "../../../functions/mod.ts";`, ); typescript.push(renderTypeImports(functionRecord)); typescript.push(""); typescript.push( `export default DefineFunction(${ defineFunctionInputToTypeScript(functionRecord) });`, ); return typescript.join("\n"); } export default SlackFunctionTemplate; ================================================ FILE: src/schema/slack/functions/_scripts/src/templates/template_mod.ts ================================================ import { autogeneratedComment, getFunctionName, renderFunctionImport, } from "./utils.ts"; import type { FunctionRecord } from "../types.ts"; export const SlackFunctionModTemplate = ( functionRecords: FunctionRecord[], ) => { const callbackIds = functionRecords.map((dfi) => dfi.callback_id); const typescript: string[] = []; typescript.push(autogeneratedComment(true)); callbackIds.forEach((callbackId) => { typescript.push(renderFunctionImport(callbackId)); }); typescript.push(""); typescript.push( `const SlackFunctions = {${ functionRecords.map((dfi) => `${getFunctionName(dfi.callback_id)}`) .join(",") }} as const;`, ); typescript.push(""); typescript.push(`export default SlackFunctions;`); return typescript.join("\n"); }; export default SlackFunctionModTemplate; ================================================ FILE: src/schema/slack/functions/_scripts/src/templates/test_template.ts ================================================ import { autogeneratedComment, getFunctionName, getSlackCallbackId, renderFunctionImport, renderTypeImports, } from "./utils.ts"; import type { FunctionParameter, FunctionRecord } from "../types.ts"; import { allowedHiddenParams, manifestFunctionFieldsToTypeScript, } from "./template_function.ts"; export const manifestFunctionToTypeScript = ( functionRecord: FunctionRecord, ) => { return `{${ manifestFunctionFieldsToTypeScript(allowedHiddenParams, functionRecord) }}`; }; const renderFunctionManifestTest = ( functionRecord: FunctionRecord, ) => { const functionName = getFunctionName(functionRecord.callback_id); const typescript: string[] = []; typescript.push( `assertEquals(${functionName}.definition.callback_id, "${ getSlackCallbackId(functionRecord) }");`, ); typescript.push( `const expected: ManifestFunctionSchema = ${ manifestFunctionToTypeScript( functionRecord, ) };`, ); typescript.push(`const actual = ${functionName}.export();`); typescript.push(""); typescript.push(`assertNotStrictEquals(actual, expected);`); return `() => {${typescript.join("\n")}}`; }; const workflowToTypeScript = (functionName: string) => { const typescript: string[] = []; typescript.push(`callback_id: "test_${functionName}_slack_function"`); typescript.push(`title: "Test ${functionName}"`); typescript.push( `description: "This is a generated test to test ${functionName}"`, ); return `{${typescript.join(", \n")}}`; }; const requiredParametersToTypeScript = ( parameters: FunctionParameter[], ) => { const typescript: string[] = []; parameters.forEach((parameter: FunctionParameter) => { if (parameter.is_required) { typescript.push(`${parameter.name}: "test"`); } }); return `{${typescript.join(",\n")}}`; }; const renderWorkflowStepTest = (functionRecord: FunctionRecord) => { const functionName = getFunctionName(functionRecord.callback_id); const inputParameters = requiredParametersToTypeScript( functionRecord.input_parameters, ); const typescript: string[] = []; typescript.push( `const testWorkflow = DefineWorkflow(${ workflowToTypeScript(functionName) });`, ); typescript.push( `testWorkflow.addStep(${functionName}, ${inputParameters});`, ); typescript.push(`const actual = testWorkflow.steps[0].export();`); typescript.push(""); typescript.push( `assertEquals(actual.function_id, "${ getSlackCallbackId(functionRecord) }");`, ); typescript.push(`assertEquals(actual.inputs, ${inputParameters});`); return `() => {${typescript.join("\n")}}`; }; const renderOutputExistenceTest = (functionRecord: FunctionRecord) => { const functionName = getFunctionName(functionRecord.callback_id); const typescript: string[] = []; typescript.push( `const testWorkflow = DefineWorkflow(${ workflowToTypeScript(functionName) });`, ); typescript.push( `const step = testWorkflow.addStep(${functionName}, ${ requiredParametersToTypeScript( functionRecord.input_parameters, ) });`, ); for (const parameter of functionRecord.output_parameters) { typescript.push( `assertExists(step.outputs.${parameter.name});`, ); } return `() => {${typescript.join("\n")}}`; }; export function SlackTestFunctionTemplate( functionRecord: FunctionRecord, ): string { const functionName = getFunctionName(functionRecord.callback_id); const typescript: string[] = []; typescript.push(autogeneratedComment()); typescript.push( `import { assertEquals, assertNotStrictEquals } from "../../../dev_deps.ts";`, ); typescript.push( `import { DefineWorkflow } from "../../../workflows/mod.ts";`, ); typescript.push( `import { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts";`, ); typescript.push(renderTypeImports(functionRecord)); typescript.push(renderFunctionImport(functionRecord.callback_id)); typescript.push(""); typescript.push( `Deno.test("${functionName} generates valid FunctionManifest", ${ renderFunctionManifestTest(functionRecord) });`, ); typescript.push(""); typescript.push( `Deno.test("${functionName} can be used as a Slack function in a workflow step", ${ renderWorkflowStepTest(functionRecord) });`, ); if (!functionRecord.output_parameters.length) { return typescript.join("\n"); } typescript[1] = `import { assertEquals, assertNotStrictEquals, assertExists } from "../../../dev_deps.ts";`; typescript.push(""); typescript.push( `Deno.test("All outputs of Slack function ${functionName} should exist", ${ renderOutputExistenceTest(functionRecord) });`, ); return typescript.join("\n"); } export default SlackTestFunctionTemplate; ================================================ FILE: src/schema/slack/functions/_scripts/src/templates/types.ts ================================================ import type { ICustomType } from "../../../../../../types/types.ts"; export type AllowedTypeValue = ICustomType | string; export type AllowedTypeValueObject = Record<string, AllowedTypeValue>; ================================================ FILE: src/schema/slack/functions/_scripts/src/templates/utils.ts ================================================ import { toPascalCase } from "../../../../../../dev_deps.ts"; import type { FunctionProperty, FunctionRecord } from "../types.ts"; import SchemaTypes from "../../../../../schema_types.ts"; import SlackTypes from "../../../../schema_types.ts"; import { InternalSlackTypes } from "../../../../types/custom/mod.ts"; import type { AllowedTypeValue, AllowedTypeValueObject } from "./types.ts"; import { isCustomType } from "../../../../../../types/mod.ts"; import { isArrayFunctionProperty, isObjectFunctionProperty } from "../utils.ts"; export function autogeneratedComment(includeDate?: boolean): string { const dateString = includeDate ? ` on ${new Date().toDateString()}` : ""; return `/** This file was autogenerated${dateString}. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/`; } export function renderFunctionImport(callbackId: string): string { return `import ${getFunctionName(callbackId)} from "./${callbackId}.ts";`; } export function getFunctionName(callbackId: string): string { return toPascalCase(callbackId); } export function getSlackCallbackId( functionRecord: FunctionRecord, ): string { return `slack#/functions/${functionRecord.callback_id}`; } export function getParameterType(type: AllowedTypeValue): string { return isCustomType(type) ? type.id : type; } const getParameterList = ( functionRecord: FunctionRecord, ): FunctionProperty[] => [ ...functionRecord.input_parameters, ...functionRecord.output_parameters, ]; const hasTypeObject = ( types: string[], typeObject: AllowedTypeValueObject, ): boolean => types.some((t) => Object.values(typeObject).map((val) => getParameterType(val)).includes(t) ); const extractTypes = (properties: FunctionProperty[]): string[] => { let types: Set<string> = new Set(); properties.forEach((property) => { types.add(property.type); if (isArrayFunctionProperty(property)) { types = new Set([ ...types, ...extractTypes([property.items]), ]); } if (isObjectFunctionProperty(property)) { types = new Set([ ...types, ...extractTypes(Object.values(property.properties)), ]); } }); return Array.from(types); }; export function renderTypeImports(functionRecord: FunctionRecord) { const typescript: string[] = []; const functionRecordTypes = extractTypes(getParameterList(functionRecord)); if (hasTypeObject(functionRecordTypes, SchemaTypes)) { typescript.push('import SchemaTypes from "../../schema_types.ts";'); } if (hasTypeObject(functionRecordTypes, SlackTypes)) { typescript.push('import SlackTypes from "../schema_types.ts";'); } if (hasTypeObject(functionRecordTypes, InternalSlackTypes)) { typescript.push( 'import { InternalSlackTypes } from "../types/custom/mod.ts";', ); } return typescript.join("\n"); } export function sanitize(value: string): string { return JSON.stringify(value); } ================================================ FILE: src/schema/slack/functions/_scripts/src/templates/utils_test.ts ================================================ import { autogeneratedComment, getFunctionName, getSlackCallbackId, renderFunctionImport, renderTypeImports, sanitize, } from "./utils.ts"; import { assertEquals, assertStringIncludes, } from "../../../../../../dev_deps.ts"; import type { FunctionRecord } from "../types.ts"; import SchemaTypes from "../../../../../schema_types.ts"; import SlackTypes from "../../../../schema_types.ts"; import { InternalSlackTypes } from "../../../../types/custom/mod.ts"; const DESCRIPTION = "Test the Slack function template"; const TITLE = "test function"; const CALLBACK_ID = "test_function"; const SLACK_FUNCTION_TYPE = "builtin"; Deno.test("Autogenerated comment should contain readme location", () => { const actual = autogeneratedComment(); assertStringIncludes(actual, "src/schema/slack/functions/_scripts/README.md"); }); Deno.test("Autogenerated comment shouldn't mention the date by default", () => { const actual = autogeneratedComment(); assertStringIncludes(actual, "autogenerated."); }); Deno.test("Autogenerated comment can include the date", () => { const actual = autogeneratedComment(true); assertStringIncludes(actual, "autogenerated on "); }); Deno.test("Function name should be pascal case", () => { const actual = getFunctionName(CALLBACK_ID); assertEquals(actual, "TestFunction"); }); Deno.test("Function import should contain file path", () => { const actual = renderFunctionImport(CALLBACK_ID); assertStringIncludes(actual, `./${CALLBACK_ID}.ts`); }); Deno.test("getSlackCallbackId should generate the valid slack callback_id", () => { const actual = `slack#/functions/${CALLBACK_ID}`; const expected = getSlackCallbackId({ callback_id: CALLBACK_ID, title: TITLE, description: DESCRIPTION, type: SLACK_FUNCTION_TYPE, input_parameters: [], output_parameters: [], }); assertStringIncludes(actual, expected); }); Deno.test("renderTypeImports should render all imports provided with slack and primitive types", () => { const dfi: FunctionRecord = { callback_id: CALLBACK_ID, title: TITLE, description: DESCRIPTION, type: SLACK_FUNCTION_TYPE, input_parameters: [ { type: SlackTypes.channel_id, name: "channel_id", title: "Select a channel", is_required: true, description: "Search all channels", }, { type: InternalSlackTypes.form_input_object.id, name: "fields", title: "fields", is_required: true, description: "Input fields to be shown on the form", }, ], output_parameters: [ { type: SchemaTypes.string, name: "message_ts", title: "Message time stamp", description: "Message time stamp", }, ], }; const actual = renderTypeImports(dfi); assertStringIncludes(actual, "SchemaTypes"); assertStringIncludes(actual, "SlackTypes"); assertStringIncludes(actual, "InternalSlackTypes"); }); Deno.test("renderTypeImports should render imports required for array type", () => { const dfi: FunctionRecord = { callback_id: CALLBACK_ID, title: TITLE, description: DESCRIPTION, type: SLACK_FUNCTION_TYPE, input_parameters: [], output_parameters: [ { type: SchemaTypes.array, name: "user_ids", title: "User Ids", description: "User Ids", items: { type: SlackTypes.channel_id, }, }, ], }; const actual = renderTypeImports(dfi); assertStringIncludes(actual, "SchemaTypes"); assertStringIncludes(actual, "SlackTypes"); }); Deno.test("renderTypeImports should render imports required for object type", () => { const dfi: FunctionRecord = { callback_id: CALLBACK_ID, title: TITLE, description: DESCRIPTION, type: SLACK_FUNCTION_TYPE, input_parameters: [], output_parameters: [ { type: SchemaTypes.object, name: "user_ids", title: "User Ids", description: "User Ids", properties: { my_param: { type: SlackTypes.channel_id, }, }, }, ], }; const actual = renderTypeImports(dfi); assertStringIncludes(actual, "SchemaTypes"); assertStringIncludes(actual, "SlackTypes"); }); Deno.test("renderTypeImports should render imports required for a nested complex object type", () => { const dfi: FunctionRecord = { callback_id: CALLBACK_ID, title: TITLE, description: DESCRIPTION, type: SLACK_FUNCTION_TYPE, input_parameters: [], output_parameters: [ { type: SchemaTypes.array, items: { type: SchemaTypes.object, properties: { my_slack_type: { type: InternalSlackTypes.form_input_object.id, }, my_primitive_type: { type: SlackTypes.channel_id, }, }, }, name: "user_ids", }, ], }; const actual = renderTypeImports(dfi); assertStringIncludes(actual, "InternalSlackTypes"); assertStringIncludes(actual, "SchemaTypes"); assertStringIncludes(actual, "SlackTypes"); }); Deno.test("renderTypeImports should render imports required for primitive & complex types", () => { const dfi: FunctionRecord = { callback_id: CALLBACK_ID, title: TITLE, description: DESCRIPTION, type: SLACK_FUNCTION_TYPE, input_parameters: [], output_parameters: [ { type: SchemaTypes.array, items: { type: SchemaTypes.string, }, name: "user_ids", }, { type: SchemaTypes.object, name: "my_object", properties: { my_param: { type: SchemaTypes.string, }, }, }, { type: SchemaTypes.string, name: "my_primitive", }, ], }; const actual = renderTypeImports(dfi); assertStringIncludes(actual, "SchemaTypes"); }); Deno.test(`${sanitize.name} should properly escape \" characters`, () => { const testText = 'Send an "only visible to you" message'; const actual = sanitize(testText); assertEquals(actual, '"Send an \\"only visible to you\\" message"'); }); ================================================ FILE: src/schema/slack/functions/_scripts/src/test/data/function.json ================================================ { "ok": true, "functions": [ { "id": "Fn0102", "callback_id": "send_message", "title": "Send a message to channel", "description": "Send a message to channel", "type": "builtin", "input_parameters": [ { "type": "slack#/types/channel_id", "name": "channel_id", "title": "Select a channel", "is_required": true, "description": "Search all channels" }, { "type": "slack#/types/rich_text", "name": "message", "title": "Add a message", "is_required": true, "description": "Add a message" }, { "type": "slack#/types/message_ts", "name": "thread_ts", "title": "Another message's timestamp value", "description": "Provide another message's ts value to make this message a reply" }, { "type": "object", "name": "object", "title": "Object", "description": "Object type", "required": ["event_type", "event_payload"], "additionalProperties": true, "properties": { "event_type": { "type": "string" }, "event_payload": { "type": "object" } } }, { "type": "array", "name": "test_array", "title": "test array", "description": "test an array", "items": { "type": "string" } }, { "type": "slack#/types/blocks", "name": "interactive_blocks", "title": "Button(s) to send with the message", "description": "Button(s) to send with the message" } ], "output_parameters": [ { "type": "slack#/types/message_ts", "name": "message_ts", "title": "Message time stamp", "is_required": true, "description": "Message time stamp" }, { "type": "slack#/types/interactivity", "name": "interactivity", "title": "interactivity", "description": "Interactivity context", "is_hidden": true, "hello big": true }, { "type": "slack#/types/message_context", "name": "message_context", "title": "Reference to the message sent", "description": "Reference to the message sent", "is_required": true } ] }, { "id": "Fn03R943D2UW", "callback_id": "create_out_of_office_event", "title": "Create an out-of-office event", "description": "Create an all day out-of-office event in Google Calendar", "type": "app", "input_parameters": [ { "type": "slack#/types/user_context", "name": "user", "description": "Id of slack user connected to calendar", "title": "Id of slack user connected to calendar", "is_required": true }, { "type": "slack#/types/date", "name": "end_date", "description": "End date of the event ( inclusive. yyyy-mm-dd )", "title": "End date", "is_required": true } ], "output_parameters": [ { "type": "string", "name": "event_id", "description": "Event id returned by Google calendar app", "title": "Event id returned by Google calendar app", "is_required": false } ], "app_id": "ADZ494LHY", "date_created": 1659034796, "date_updated": 1670840200, "date_deleted": 0 } ] } ================================================ FILE: src/schema/slack/functions/_scripts/src/types.ts ================================================ type BaseFunctionProperty = { type: string; description?: string; title?: string; }; export type ObjectFunctionProperty = BaseFunctionProperty & { properties: FunctionProperties; required?: string[]; additionalProperties?: boolean; }; export type ArrayFunctionProperty = BaseFunctionProperty & { items: FunctionProperty; }; export type FunctionProperty = | BaseFunctionProperty | ObjectFunctionProperty | ArrayFunctionProperty; export type FunctionProperties = { [key: string]: FunctionProperty; }; export type FunctionParameter = FunctionProperty & { name: string; is_required?: boolean; is_hidden?: boolean; }; export type FunctionRecord = { callback_id: string; title: string; description: string; app_id?: string; input_parameters: FunctionParameter[]; output_parameters: FunctionParameter[]; type?: string; }; export type FunctionsPayload = { ok: boolean; functions: FunctionRecord[]; }; ================================================ FILE: src/schema/slack/functions/_scripts/src/utils.ts ================================================ import type { ArrayFunctionProperty, FunctionProperty, FunctionRecord, FunctionsPayload, ObjectFunctionProperty, } from "./types.ts"; const FUNCTIONS_JSON_PATH = "functions.json"; const green = "\x1b[92m"; const yellow = "\x1b[38;5;214m"; const red = "\x1b[91m"; const reset = "\x1b[0m"; export const greenText = (text: string) => green + text + reset; export const yellowText = (text: string) => yellow + text + reset; export const redText = (text: string) => red + text + reset; // TODO: once List steps work in code, bring this back const FUNCTIONS_TO_IGNORE = [ "update_list_record", "share_list_users", "lists_activity_feed", "list_add_record", "delete_list_record", "copy_list", ]; export async function getSlackFunctions( functionsPayloadPath: string = FUNCTIONS_JSON_PATH, ): Promise<FunctionRecord[]> { const functionsPayload: FunctionsPayload = await Deno.readTextFile( functionsPayloadPath, ).then(JSON.parse); return functionsPayload.functions.filter((fn) => fn.type == "builtin" && !FUNCTIONS_TO_IGNORE.includes(fn.callback_id) ); } export function isObjectFunctionProperty( property: FunctionProperty, ): property is ObjectFunctionProperty { return "properties" in property; } export function isArrayFunctionProperty( property: FunctionProperty, ): property is ArrayFunctionProperty { return "items" in property; } ================================================ FILE: src/schema/slack/functions/_scripts/src/utils_test.ts ================================================ import { getSlackFunctions, greenText, isArrayFunctionProperty, isObjectFunctionProperty, redText, yellowText, } from "./utils.ts"; import { assert, assertEquals, type IsExact } from "../../../../../dev_deps.ts"; import type { ArrayFunctionProperty, FunctionProperty, ObjectFunctionProperty, } from "./types.ts"; Deno.test("colored text remain consistent", () => { assertEquals("\x1b[92mtest\x1b[0m", greenText("test")); assertEquals("\x1b[91mtest\x1b[0m", redText("test")); assertEquals("\x1b[38;5;214mtest\x1b[0m", yellowText("test")); }); Deno.test("Non Slack functions should be filtered", async () => { const actual = await getSlackFunctions( "src/schema/slack/functions/_scripts/src/test/data/function.json", ); assertEquals(actual.length, 1); }); Deno.test("isObjectFunctionProperty distinguishes ObjectFunctionProperty from FunctionProperty", () => { const property: FunctionProperty = { type: "object", properties: { myString: { type: "string", description: "test description", title: "String property", }, }, required: [], additionalProperties: true, }; assert<IsExact<ObjectFunctionProperty, typeof property>>(true); assertEquals(true, isObjectFunctionProperty(property)); }); Deno.test("isArrayFunctionProperty distinguishes ArrayFunctionProperty from FunctionProperty", () => { const property: FunctionProperty = { type: "array", description: "test description", title: "ArrayFunctionProperty", items: { type: "string", }, }; assert<IsExact<ArrayFunctionProperty, typeof property>>(true); assertEquals(true, isArrayFunctionProperty(property)); }); ================================================ FILE: src/schema/slack/functions/_scripts/src/write_function_files.ts ================================================ import { SlackFunctionModTemplate, SlackFunctionTemplate, SlackTestFunctionTemplate, } from "./templates/mod.ts"; import { getSlackFunctions, greenText, redText } from "./utils.ts"; import type { FunctionRecord } from "./types.ts"; const VALID_FILENAME_REGEX = /^[0-9a-zA-Z_\-]+$/; async function main() { const slackFunctions: FunctionRecord[] = await _internals.getSlackFunctions(); // Sorting alphabetically cause only a monster would generate these in a random order slackFunctions.sort((a, b) => a.callback_id.localeCompare(b.callback_id)); await Promise.all( slackFunctions.map(async (functionRecord: FunctionRecord) => { console.log( `Generating code & tests for Slack function: ${ greenText(functionRecord.callback_id) }`, ); if (!VALID_FILENAME_REGEX.test(functionRecord.callback_id)) { console.log( `${redText("FAILURE:")} Invalid characters in callback_id: ${ redText(functionRecord.callback_id) }`, ); return; } const filename = `../${functionRecord.callback_id}.ts`; const testFilename = `../${functionRecord.callback_id}_test.ts`; const templateString = _internals.SlackFunctionTemplate(functionRecord); const templateTestString = _internals.SlackTestFunctionTemplate( functionRecord, ); await _internals.writeTextFile(filename, templateString); await _internals.writeTextFile(testFilename, templateTestString); }), ); console.log( `Generated ${slackFunctions.length} Slack functions with their unit tests`, ); const modString = _internals.SlackFunctionModTemplate(slackFunctions); await _internals.writeTextFile("../mod.ts", modString); console.log("Updated functions module export"); } export const _internals = { main, getSlackFunctions, SlackFunctionModTemplate, SlackFunctionTemplate, SlackTestFunctionTemplate, writeTextFile: ( path: string, data: string, options?: Deno.WriteFileOptions, ): ReturnType<typeof Deno.writeTextFile> => { return Deno.writeTextFile(path, data, options); }, }; if (import.meta.main) { _internals.main(); } ================================================ FILE: src/schema/slack/functions/add_bookmark.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/add_bookmark", source_file: "", title: "Add a bookmark", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, name: { type: SchemaTypes.string, description: "Enter the bookmark name", title: "Bookmark name", }, link: { type: SchemaTypes.string, description: "https://docs.acme.com", title: "Bookmark Link", }, }, required: ["channel_id", "name", "link"], }, output_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Channel", title: "Channel", }, bookmark_name: { type: SchemaTypes.string, description: "Bookmark name", title: "Bookmark name", }, bookmark_link: { type: SchemaTypes.string, description: "Bookmark link", title: "Bookmark link", }, }, required: ["channel_id", "bookmark_name", "bookmark_link"], }, }); ================================================ FILE: src/schema/slack/functions/add_bookmark_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import AddBookmark from "./add_bookmark.ts"; Deno.test("AddBookmark generates valid FunctionManifest", () => { assertEquals( AddBookmark.definition.callback_id, "slack#/functions/add_bookmark", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Add a bookmark", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, name: { type: SchemaTypes.string, description: "Enter the bookmark name", title: "Bookmark name", }, link: { type: SchemaTypes.string, description: "https://docs.acme.com", title: "Bookmark Link", }, }, required: ["channel_id", "name", "link"], }, output_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Channel", title: "Channel", }, bookmark_name: { type: SchemaTypes.string, description: "Bookmark name", title: "Bookmark name", }, bookmark_link: { type: SchemaTypes.string, description: "Bookmark link", title: "Bookmark link", }, }, required: ["channel_id", "bookmark_name", "bookmark_link"], }, }; const actual = AddBookmark.export(); assertNotStrictEquals(actual, expected); }); Deno.test("AddBookmark can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_AddBookmark_slack_function", title: "Test AddBookmark", description: "This is a generated test to test AddBookmark", }); testWorkflow.addStep(AddBookmark, { channel_id: "test", name: "test", link: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/add_bookmark"); assertEquals(actual.inputs, { channel_id: "test", name: "test", link: "test", }); }); Deno.test("All outputs of Slack function AddBookmark should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_AddBookmark_slack_function", title: "Test AddBookmark", description: "This is a generated test to test AddBookmark", }); const step = testWorkflow.addStep(AddBookmark, { channel_id: "test", name: "test", link: "test", }); assertExists(step.outputs.channel_id); assertExists(step.outputs.bookmark_name); assertExists(step.outputs.bookmark_link); }); ================================================ FILE: src/schema/slack/functions/add_pin.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/add_pin", source_file: "", title: "Pin a message", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, message: { type: SchemaTypes.string, description: "Enter a message link or message timestamp", title: "Message link or message timestamp", }, }, required: ["channel_id", "message"], }, output_parameters: { properties: {}, required: [] }, }); ================================================ FILE: src/schema/slack/functions/add_pin_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertNotStrictEquals } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import AddPin from "./add_pin.ts"; Deno.test("AddPin generates valid FunctionManifest", () => { assertEquals(AddPin.definition.callback_id, "slack#/functions/add_pin"); const expected: ManifestFunctionSchema = { source_file: "", title: "Pin a message", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, message: { type: SchemaTypes.string, description: "Enter a message link or message timestamp", title: "Message link or message timestamp", }, }, required: ["channel_id", "message"], }, output_parameters: { properties: {}, required: [] }, }; const actual = AddPin.export(); assertNotStrictEquals(actual, expected); }); Deno.test("AddPin can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_AddPin_slack_function", title: "Test AddPin", description: "This is a generated test to test AddPin", }); testWorkflow.addStep(AddPin, { channel_id: "test", message: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/add_pin"); assertEquals(actual.inputs, { channel_id: "test", message: "test" }); }); ================================================ FILE: src/schema/slack/functions/add_reaction.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/add_reaction", source_file: "", title: "Add a reaction to a message", input_parameters: { properties: { message_context: { type: SlackTypes.message_context, description: "Select a message to react to", title: "Select a message to react to", }, emoji: { type: SchemaTypes.string, description: "Reaction (emoji) name", title: "Emoji", }, }, required: ["message_context", "emoji"], }, output_parameters: { properties: {}, required: [] }, }); ================================================ FILE: src/schema/slack/functions/add_reaction_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertNotStrictEquals } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import AddReaction from "./add_reaction.ts"; Deno.test("AddReaction generates valid FunctionManifest", () => { assertEquals( AddReaction.definition.callback_id, "slack#/functions/add_reaction", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Add a reaction to a message", input_parameters: { properties: { message_context: { type: SlackTypes.message_context, description: "Select a message to react to", title: "Select a message to react to", }, emoji: { type: SchemaTypes.string, description: "Reaction (emoji) name", title: "Emoji", }, }, required: ["message_context", "emoji"], }, output_parameters: { properties: {}, required: [] }, }; const actual = AddReaction.export(); assertNotStrictEquals(actual, expected); }); Deno.test("AddReaction can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_AddReaction_slack_function", title: "Test AddReaction", description: "This is a generated test to test AddReaction", }); testWorkflow.addStep(AddReaction, { message_context: "test", emoji: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/add_reaction"); assertEquals(actual.inputs, { message_context: "test", emoji: "test" }); }); ================================================ FILE: src/schema/slack/functions/add_user_to_usergroup.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/add_user_to_usergroup", source_file: "", title: "Add people to a user group", description: "Additional permissions might be required", input_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "Search all user groups", title: "Select a user group", }, user_ids: { type: SchemaTypes.array, description: "Search all people", title: "Select member(s)", items: { type: SlackTypes.user_id }, }, }, required: ["usergroup_id", "user_ids"], }, output_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "User group", title: "User group", }, }, required: ["usergroup_id"], }, }); ================================================ FILE: src/schema/slack/functions/add_user_to_usergroup_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import AddUserToUsergroup from "./add_user_to_usergroup.ts"; Deno.test("AddUserToUsergroup generates valid FunctionManifest", () => { assertEquals( AddUserToUsergroup.definition.callback_id, "slack#/functions/add_user_to_usergroup", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Add people to a user group", description: "Additional permissions might be required", input_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "Search all user groups", title: "Select a user group", }, user_ids: { type: SchemaTypes.array, description: "Search all people", title: "Select member(s)", items: { type: SlackTypes.user_id }, }, }, required: ["usergroup_id", "user_ids"], }, output_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "User group", title: "User group", }, }, required: ["usergroup_id"], }, }; const actual = AddUserToUsergroup.export(); assertNotStrictEquals(actual, expected); }); Deno.test("AddUserToUsergroup can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_AddUserToUsergroup_slack_function", title: "Test AddUserToUsergroup", description: "This is a generated test to test AddUserToUsergroup", }); testWorkflow.addStep(AddUserToUsergroup, { usergroup_id: "test", user_ids: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/add_user_to_usergroup"); assertEquals(actual.inputs, { usergroup_id: "test", user_ids: "test" }); }); Deno.test("All outputs of Slack function AddUserToUsergroup should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_AddUserToUsergroup_slack_function", title: "Test AddUserToUsergroup", description: "This is a generated test to test AddUserToUsergroup", }); const step = testWorkflow.addStep(AddUserToUsergroup, { usergroup_id: "test", user_ids: "test", }); assertExists(step.outputs.usergroup_id); }); ================================================ FILE: src/schema/slack/functions/archive_channel.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/archive_channel", source_file: "", title: "Archive a channel", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, }, required: ["channel_id"], }, output_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Channel name", title: "Channel name", }, }, required: ["channel_id"], }, }); ================================================ FILE: src/schema/slack/functions/archive_channel_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SlackTypes from "../schema_types.ts"; import ArchiveChannel from "./archive_channel.ts"; Deno.test("ArchiveChannel generates valid FunctionManifest", () => { assertEquals( ArchiveChannel.definition.callback_id, "slack#/functions/archive_channel", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Archive a channel", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, }, required: ["channel_id"], }, output_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Channel name", title: "Channel name", }, }, required: ["channel_id"], }, }; const actual = ArchiveChannel.export(); assertNotStrictEquals(actual, expected); }); Deno.test("ArchiveChannel can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ArchiveChannel_slack_function", title: "Test ArchiveChannel", description: "This is a generated test to test ArchiveChannel", }); testWorkflow.addStep(ArchiveChannel, { channel_id: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/archive_channel"); assertEquals(actual.inputs, { channel_id: "test" }); }); Deno.test("All outputs of Slack function ArchiveChannel should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ArchiveChannel_slack_function", title: "Test ArchiveChannel", description: "This is a generated test to test ArchiveChannel", }); const step = testWorkflow.addStep(ArchiveChannel, { channel_id: "test" }); assertExists(step.outputs.channel_id); }); ================================================ FILE: src/schema/slack/functions/canvas_copy.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/canvas_copy", source_file: "", title: "Copy a canvas", input_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Search all canvases", title: "Select a canvas", }, title: { type: SchemaTypes.string, description: "Enter a canvas name", title: "Canvas name", }, owner_id: { type: SlackTypes.user_id, description: "Person", title: "Canvas owner", }, placeholder_values: { type: SchemaTypes.object, description: "Variables", title: "Variables", }, }, required: ["canvas_id", "title", "owner_id"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: ["canvas_id"], }, }); ================================================ FILE: src/schema/slack/functions/canvas_copy_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import CanvasCopy from "./canvas_copy.ts"; Deno.test("CanvasCopy generates valid FunctionManifest", () => { assertEquals( CanvasCopy.definition.callback_id, "slack#/functions/canvas_copy", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Copy a canvas", input_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Search all canvases", title: "Select a canvas", }, title: { type: SchemaTypes.string, description: "Enter a canvas name", title: "Canvas name", }, owner_id: { type: SlackTypes.user_id, description: "Person", title: "Canvas owner", }, placeholder_values: { type: SchemaTypes.object, description: "Variables", title: "Variables", }, }, required: ["canvas_id", "title", "owner_id"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: ["canvas_id"], }, }; const actual = CanvasCopy.export(); assertNotStrictEquals(actual, expected); }); Deno.test("CanvasCopy can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CanvasCopy_slack_function", title: "Test CanvasCopy", description: "This is a generated test to test CanvasCopy", }); testWorkflow.addStep(CanvasCopy, { canvas_id: "test", title: "test", owner_id: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/canvas_copy"); assertEquals(actual.inputs, { canvas_id: "test", title: "test", owner_id: "test", }); }); Deno.test("All outputs of Slack function CanvasCopy should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CanvasCopy_slack_function", title: "Test CanvasCopy", description: "This is a generated test to test CanvasCopy", }); const step = testWorkflow.addStep(CanvasCopy, { canvas_id: "test", title: "test", owner_id: "test", }); assertExists(step.outputs.canvas_id); }); ================================================ FILE: src/schema/slack/functions/canvas_create.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/canvas_create", source_file: "", title: "Create a canvas", input_parameters: { properties: { title: { type: SchemaTypes.string, description: "Enter a canvas name", title: "Canvas name", }, canvas_create_type: { type: SchemaTypes.string, description: "Type of creation", title: "Type of creation", }, canvas_template_id: { type: SlackTypes.canvas_template_id, description: "Select an option", title: "Select a canvas template", }, owner_id: { type: SlackTypes.user_id, description: "Person", title: "Canvas owner", }, content: { type: SlackTypes.expanded_rich_text, description: "Add content to the canvas", title: "Add content", }, placeholder_values: { type: SchemaTypes.object, description: "Variables", title: "Variables", }, }, required: ["title", "owner_id"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: ["canvas_id"], }, }); ================================================ FILE: src/schema/slack/functions/canvas_create_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import CanvasCreate from "./canvas_create.ts"; Deno.test("CanvasCreate generates valid FunctionManifest", () => { assertEquals( CanvasCreate.definition.callback_id, "slack#/functions/canvas_create", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Create a canvas", input_parameters: { properties: { title: { type: SchemaTypes.string, description: "Enter a canvas name", title: "Canvas name", }, canvas_create_type: { type: SchemaTypes.string, description: "Type of creation", title: "Type of creation", }, canvas_template_id: { type: SlackTypes.canvas_template_id, description: "Select an option", title: "Select a canvas template", }, owner_id: { type: SlackTypes.user_id, description: "Person", title: "Canvas owner", }, content: { type: SlackTypes.expanded_rich_text, description: "Add content to the canvas", title: "Add content", }, placeholder_values: { type: SchemaTypes.object, description: "Variables", title: "Variables", }, }, required: ["title", "owner_id"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: ["canvas_id"], }, }; const actual = CanvasCreate.export(); assertNotStrictEquals(actual, expected); }); Deno.test("CanvasCreate can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CanvasCreate_slack_function", title: "Test CanvasCreate", description: "This is a generated test to test CanvasCreate", }); testWorkflow.addStep(CanvasCreate, { title: "test", owner_id: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/canvas_create"); assertEquals(actual.inputs, { title: "test", owner_id: "test" }); }); Deno.test("All outputs of Slack function CanvasCreate should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CanvasCreate_slack_function", title: "Test CanvasCreate", description: "This is a generated test to test CanvasCreate", }); const step = testWorkflow.addStep(CanvasCreate, { title: "test", owner_id: "test", }); assertExists(step.outputs.canvas_id); }); ================================================ FILE: src/schema/slack/functions/canvas_update_content.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/canvas_update_content", source_file: "", title: "Update a canvas", input_parameters: { properties: { canvas_update_type: { type: SchemaTypes.string, description: "Type of update", title: "Type of update", }, channel_id: { type: SlackTypes.channel_id, description: "Channel name", title: "Select a channel", }, canvas_id: { type: SlackTypes.canvas_id, description: "Search standalone canvases", title: "Select a canvas", }, section_id: { type: SchemaTypes.string, description: "Select an option", title: "Choose which section to update", }, action: { type: SchemaTypes.string, description: "Select an option", title: "How do you want to update?", }, content: { type: SlackTypes.expanded_rich_text, description: "Add content to the canvas", title: "Content", }, }, required: ["action", "content"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: ["canvas_id"], }, }); ================================================ FILE: src/schema/slack/functions/canvas_update_content_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import CanvasUpdateContent from "./canvas_update_content.ts"; Deno.test("CanvasUpdateContent generates valid FunctionManifest", () => { assertEquals( CanvasUpdateContent.definition.callback_id, "slack#/functions/canvas_update_content", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Update a canvas", input_parameters: { properties: { canvas_update_type: { type: SchemaTypes.string, description: "Type of update", title: "Type of update", }, channel_id: { type: SlackTypes.channel_id, description: "Channel name", title: "Select a channel", }, canvas_id: { type: SlackTypes.canvas_id, description: "Search standalone canvases", title: "Select a canvas", }, section_id: { type: SchemaTypes.string, description: "Select an option", title: "Choose which section to update", }, action: { type: SchemaTypes.string, description: "Select an option", title: "How do you want to update?", }, content: { type: SlackTypes.expanded_rich_text, description: "Add content to the canvas", title: "Content", }, }, required: ["action", "content"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: ["canvas_id"], }, }; const actual = CanvasUpdateContent.export(); assertNotStrictEquals(actual, expected); }); Deno.test("CanvasUpdateContent can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CanvasUpdateContent_slack_function", title: "Test CanvasUpdateContent", description: "This is a generated test to test CanvasUpdateContent", }); testWorkflow.addStep(CanvasUpdateContent, { action: "test", content: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/canvas_update_content"); assertEquals(actual.inputs, { action: "test", content: "test" }); }); Deno.test("All outputs of Slack function CanvasUpdateContent should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CanvasUpdateContent_slack_function", title: "Test CanvasUpdateContent", description: "This is a generated test to test CanvasUpdateContent", }); const step = testWorkflow.addStep(CanvasUpdateContent, { action: "test", content: "test", }); assertExists(step.outputs.canvas_id); }); ================================================ FILE: src/schema/slack/functions/channel_canvas_create.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/channel_canvas_create", source_file: "", title: "Create channel canvas", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Channel name", title: "Select a channel", }, canvas_create_type: { type: SchemaTypes.string, description: "Type of creation", title: "Type of creation", }, canvas_template_id: { type: SlackTypes.canvas_template_id, description: "Select an option", title: "Select a canvas template", }, content: { type: SlackTypes.expanded_rich_text, description: "Add content to the canvas", title: "Add content", }, placeholder_values: { type: SchemaTypes.object, description: "Variables", title: "Variables", }, }, required: ["channel_id"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: ["canvas_id"], }, }); ================================================ FILE: src/schema/slack/functions/channel_canvas_create_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import ChannelCanvasCreate from "./channel_canvas_create.ts"; Deno.test("ChannelCanvasCreate generates valid FunctionManifest", () => { assertEquals( ChannelCanvasCreate.definition.callback_id, "slack#/functions/channel_canvas_create", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Create channel canvas", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Channel name", title: "Select a channel", }, canvas_create_type: { type: SchemaTypes.string, description: "Type of creation", title: "Type of creation", }, canvas_template_id: { type: SlackTypes.canvas_template_id, description: "Select an option", title: "Select a canvas template", }, content: { type: SlackTypes.expanded_rich_text, description: "Add content to the canvas", title: "Add content", }, placeholder_values: { type: SchemaTypes.object, description: "Variables", title: "Variables", }, }, required: ["channel_id"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: ["canvas_id"], }, }; const actual = ChannelCanvasCreate.export(); assertNotStrictEquals(actual, expected); }); Deno.test("ChannelCanvasCreate can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ChannelCanvasCreate_slack_function", title: "Test ChannelCanvasCreate", description: "This is a generated test to test ChannelCanvasCreate", }); testWorkflow.addStep(ChannelCanvasCreate, { channel_id: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/channel_canvas_create"); assertEquals(actual.inputs, { channel_id: "test" }); }); Deno.test("All outputs of Slack function ChannelCanvasCreate should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ChannelCanvasCreate_slack_function", title: "Test ChannelCanvasCreate", description: "This is a generated test to test ChannelCanvasCreate", }); const step = testWorkflow.addStep(ChannelCanvasCreate, { channel_id: "test", }); assertExists(step.outputs.canvas_id); }); ================================================ FILE: src/schema/slack/functions/create_channel.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/create_channel", source_file: "", title: "Create a channel", input_parameters: { properties: { team_id: { type: SlackTypes.team_id, description: "Search all workspaces", title: "Select a workspace", }, channel_name: { type: SchemaTypes.string, description: "Enter a channel name", title: "Channel name", }, manager_ids: { type: SchemaTypes.array, description: "Search all people", title: "Select Channel Manager(s)", items: { type: SlackTypes.user_id }, }, is_private: { type: SchemaTypes.boolean, description: "Make this channel private", title: "Make channel private", }, }, required: ["channel_name"], }, output_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Channel name", title: "Channel name", }, manager_ids: { type: SchemaTypes.array, description: "Person(s) who were made channel manager", title: "Person(s) who were made channel manager", items: { type: SlackTypes.user_id }, }, }, required: ["channel_id"], }, }); ================================================ FILE: src/schema/slack/functions/create_channel_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import CreateChannel from "./create_channel.ts"; Deno.test("CreateChannel generates valid FunctionManifest", () => { assertEquals( CreateChannel.definition.callback_id, "slack#/functions/create_channel", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Create a channel", input_parameters: { properties: { team_id: { type: SlackTypes.team_id, description: "Search all workspaces", title: "Select a workspace", }, channel_name: { type: SchemaTypes.string, description: "Enter a channel name", title: "Channel name", }, manager_ids: { type: SchemaTypes.array, description: "Search all people", title: "Select Channel Manager(s)", items: { type: SlackTypes.user_id }, }, is_private: { type: SchemaTypes.boolean, description: "Make this channel private", title: "Make channel private", }, }, required: ["channel_name"], }, output_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Channel name", title: "Channel name", }, manager_ids: { type: SchemaTypes.array, description: "Person(s) who were made channel manager", title: "Person(s) who were made channel manager", items: { type: SlackTypes.user_id }, }, }, required: ["channel_id"], }, }; const actual = CreateChannel.export(); assertNotStrictEquals(actual, expected); }); Deno.test("CreateChannel can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CreateChannel_slack_function", title: "Test CreateChannel", description: "This is a generated test to test CreateChannel", }); testWorkflow.addStep(CreateChannel, { channel_name: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/create_channel"); assertEquals(actual.inputs, { channel_name: "test" }); }); Deno.test("All outputs of Slack function CreateChannel should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CreateChannel_slack_function", title: "Test CreateChannel", description: "This is a generated test to test CreateChannel", }); const step = testWorkflow.addStep(CreateChannel, { channel_name: "test" }); assertExists(step.outputs.channel_id); assertExists(step.outputs.manager_ids); }); ================================================ FILE: src/schema/slack/functions/create_usergroup.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/create_usergroup", source_file: "", title: "Create a user group", description: "Additional permissions might be required", input_parameters: { properties: { team_id: { type: SlackTypes.team_id, description: "Search all teams", title: "Select a team", }, usergroup_handle: { type: SchemaTypes.string, description: "Ex: accounts-team", title: "Handle", }, usergroup_name: { type: SchemaTypes.string, description: "Ex: Accounts Team", title: "Display name", }, }, required: ["usergroup_handle", "usergroup_name"], }, output_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "User group", title: "User group", }, }, required: ["usergroup_id"], }, }); ================================================ FILE: src/schema/slack/functions/create_usergroup_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import CreateUsergroup from "./create_usergroup.ts"; Deno.test("CreateUsergroup generates valid FunctionManifest", () => { assertEquals( CreateUsergroup.definition.callback_id, "slack#/functions/create_usergroup", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Create a user group", description: "Additional permissions might be required", input_parameters: { properties: { team_id: { type: SlackTypes.team_id, description: "Search all teams", title: "Select a team", }, usergroup_handle: { type: SchemaTypes.string, description: "Ex: accounts-team", title: "Handle", }, usergroup_name: { type: SchemaTypes.string, description: "Ex: Accounts Team", title: "Display name", }, }, required: ["usergroup_handle", "usergroup_name"], }, output_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "User group", title: "User group", }, }, required: ["usergroup_id"], }, }; const actual = CreateUsergroup.export(); assertNotStrictEquals(actual, expected); }); Deno.test("CreateUsergroup can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CreateUsergroup_slack_function", title: "Test CreateUsergroup", description: "This is a generated test to test CreateUsergroup", }); testWorkflow.addStep(CreateUsergroup, { usergroup_handle: "test", usergroup_name: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/create_usergroup"); assertEquals(actual.inputs, { usergroup_handle: "test", usergroup_name: "test", }); }); Deno.test("All outputs of Slack function CreateUsergroup should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_CreateUsergroup_slack_function", title: "Test CreateUsergroup", description: "This is a generated test to test CreateUsergroup", }); const step = testWorkflow.addStep(CreateUsergroup, { usergroup_handle: "test", usergroup_name: "test", }); assertExists(step.outputs.usergroup_id); }); ================================================ FILE: src/schema/slack/functions/delay.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/delay", source_file: "", title: "Delay this workflow", description: "Pauses the workflow at this step", input_parameters: { properties: { minutes_to_delay: { type: SchemaTypes.integer, description: "Enter number of minutes", title: "Delay", }, }, required: ["minutes_to_delay"], }, output_parameters: { properties: {}, required: [] }, }); ================================================ FILE: src/schema/slack/functions/delay_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertNotStrictEquals } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import Delay from "./delay.ts"; Deno.test("Delay generates valid FunctionManifest", () => { assertEquals(Delay.definition.callback_id, "slack#/functions/delay"); const expected: ManifestFunctionSchema = { source_file: "", title: "Delay this workflow", description: "Pauses the workflow at this step", input_parameters: { properties: { minutes_to_delay: { type: SchemaTypes.integer, description: "Enter number of minutes", title: "Delay", }, }, required: ["minutes_to_delay"], }, output_parameters: { properties: {}, required: [] }, }; const actual = Delay.export(); assertNotStrictEquals(actual, expected); }); Deno.test("Delay can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_Delay_slack_function", title: "Test Delay", description: "This is a generated test to test Delay", }); testWorkflow.addStep(Delay, { minutes_to_delay: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/delay"); assertEquals(actual.inputs, { minutes_to_delay: "test" }); }); ================================================ FILE: src/schema/slack/functions/invite_user_to_channel.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/invite_user_to_channel", source_file: "", title: "Add people to a channel", description: "You or the people using the workflow must have permission to invite others to the channel", input_parameters: { properties: { channel_ids: { type: SchemaTypes.array, description: "Search all channels", title: "Select channels", items: { type: SlackTypes.channel_id }, }, user_ids: { type: SchemaTypes.array, description: "Search all people", title: "Select member(s)", items: { type: SlackTypes.user_id }, }, usergroup_ids: { type: SchemaTypes.array, description: "Search all user groups", title: "Select user groups", items: { type: SlackTypes.usergroup_id }, }, }, required: ["channel_ids"], }, output_parameters: { properties: { user_ids: { type: SchemaTypes.array, description: "Person(s) who were invited", title: "Person(s) who were invited", items: { type: SlackTypes.user_id }, }, usergroup_ids: { type: SchemaTypes.array, description: "Usergroup(s) who were invited", title: "Usergroup(s) who were invited", items: { type: SlackTypes.usergroup_id }, }, }, required: [], }, }); ================================================ FILE: src/schema/slack/functions/invite_user_to_channel_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import InviteUserToChannel from "./invite_user_to_channel.ts"; Deno.test("InviteUserToChannel generates valid FunctionManifest", () => { assertEquals( InviteUserToChannel.definition.callback_id, "slack#/functions/invite_user_to_channel", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Add people to a channel", description: "You or the people using the workflow must have permission to invite others to the channel", input_parameters: { properties: { channel_ids: { type: SchemaTypes.array, description: "Search all channels", title: "Select channels", items: { type: SlackTypes.channel_id }, }, user_ids: { type: SchemaTypes.array, description: "Search all people", title: "Select member(s)", items: { type: SlackTypes.user_id }, }, usergroup_ids: { type: SchemaTypes.array, description: "Search all user groups", title: "Select user groups", items: { type: SlackTypes.usergroup_id }, }, }, required: ["channel_ids"], }, output_parameters: { properties: { user_ids: { type: SchemaTypes.array, description: "Person(s) who were invited", title: "Person(s) who were invited", items: { type: SlackTypes.user_id }, }, usergroup_ids: { type: SchemaTypes.array, description: "Usergroup(s) who were invited", title: "Usergroup(s) who were invited", items: { type: SlackTypes.usergroup_id }, }, }, required: [], }, }; const actual = InviteUserToChannel.export(); assertNotStrictEquals(actual, expected); }); Deno.test("InviteUserToChannel can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_InviteUserToChannel_slack_function", title: "Test InviteUserToChannel", description: "This is a generated test to test InviteUserToChannel", }); testWorkflow.addStep(InviteUserToChannel, { channel_ids: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/invite_user_to_channel"); assertEquals(actual.inputs, { channel_ids: "test" }); }); Deno.test("All outputs of Slack function InviteUserToChannel should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_InviteUserToChannel_slack_function", title: "Test InviteUserToChannel", description: "This is a generated test to test InviteUserToChannel", }); const step = testWorkflow.addStep(InviteUserToChannel, { channel_ids: "test", }); assertExists(step.outputs.user_ids); assertExists(step.outputs.usergroup_ids); }); ================================================ FILE: src/schema/slack/functions/mod.ts ================================================ /** This file was autogenerated on Fri Sep 06 2024. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import AddBookmark from "./add_bookmark.ts"; import AddPin from "./add_pin.ts"; import AddReaction from "./add_reaction.ts"; import AddUserToUsergroup from "./add_user_to_usergroup.ts"; import ArchiveChannel from "./archive_channel.ts"; import CanvasCopy from "./canvas_copy.ts"; import CanvasCreate from "./canvas_create.ts"; import CanvasUpdateContent from "./canvas_update_content.ts"; import ChannelCanvasCreate from "./channel_canvas_create.ts"; import CreateChannel from "./create_channel.ts"; import CreateUsergroup from "./create_usergroup.ts"; import Delay from "./delay.ts"; import InviteUserToChannel from "./invite_user_to_channel.ts"; import OpenForm from "./open_form.ts"; import RemoveReaction from "./remove_reaction.ts"; import RemoveUserFromUsergroup from "./remove_user_from_usergroup.ts"; import ReplyInThread from "./reply_in_thread.ts"; import SendDm from "./send_dm.ts"; import SendEphemeralMessage from "./send_ephemeral_message.ts"; import SendMessage from "./send_message.ts"; import ShareCanvas from "./share_canvas.ts"; import ShareCanvasInThread from "./share_canvas_in_thread.ts"; import UpdateChannelTopic from "./update_channel_topic.ts"; const SlackFunctions = { AddBookmark, AddPin, AddReaction, AddUserToUsergroup, ArchiveChannel, CanvasCopy, CanvasCreate, CanvasUpdateContent, ChannelCanvasCreate, CreateChannel, CreateUsergroup, Delay, InviteUserToChannel, OpenForm, RemoveReaction, RemoveUserFromUsergroup, ReplyInThread, SendDm, SendEphemeralMessage, SendMessage, ShareCanvas, ShareCanvasInThread, UpdateChannelTopic, } as const; export default SlackFunctions; ================================================ FILE: src/schema/slack/functions/open_form.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import { InternalSlackTypes } from "../types/custom/mod.ts"; export default DefineFunction({ callback_id: "slack#/functions/open_form", source_file: "", title: "Collect info in a form", description: "Uses a form to collect information", input_parameters: { properties: { title: { type: SchemaTypes.string, description: "Title of the form", title: "title", }, description: { type: SchemaTypes.string, description: "Description of the form", title: "description", }, submit_label: { type: SchemaTypes.string, description: "Submit Label of the form", title: "submit_label", }, fields: { type: InternalSlackTypes.form_input_object, description: "Input fields to be shown on the form", title: "fields", }, interactivity: { type: SlackTypes.interactivity, description: "Context about the interactive event that led to opening of the form", title: "interactivity", }, }, required: ["title", "fields", "interactivity"], }, output_parameters: { properties: { fields: { type: SchemaTypes.object, description: "fields", title: "fields", }, interactivity: { type: SlackTypes.interactivity, description: "Context about the form submit action interactive event", title: "interactivity", }, submit_user: { type: SlackTypes.user_id, description: "Person who submitted the form", title: "Person who submitted the form", }, timestamp_started: { type: SlackTypes.timestamp, description: "Time when step started", title: "Time when step started", }, timestamp_completed: { type: SlackTypes.timestamp, description: "Time when step ended", title: "Time when step ended", }, }, required: [ "fields", "interactivity", "submit_user", "timestamp_started", "timestamp_completed", ], }, }); ================================================ FILE: src/schema/slack/functions/open_form_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import { InternalSlackTypes } from "../types/custom/mod.ts"; import OpenForm from "./open_form.ts"; Deno.test("OpenForm generates valid FunctionManifest", () => { assertEquals(OpenForm.definition.callback_id, "slack#/functions/open_form"); const expected: ManifestFunctionSchema = { source_file: "", title: "Collect info in a form", description: "Uses a form to collect information", input_parameters: { properties: { title: { type: SchemaTypes.string, description: "Title of the form", title: "title", }, description: { type: SchemaTypes.string, description: "Description of the form", title: "description", }, submit_label: { type: SchemaTypes.string, description: "Submit Label of the form", title: "submit_label", }, fields: { type: InternalSlackTypes.form_input_object, description: "Input fields to be shown on the form", title: "fields", }, interactivity: { type: SlackTypes.interactivity, description: "Context about the interactive event that led to opening of the form", title: "interactivity", }, }, required: ["title", "fields", "interactivity"], }, output_parameters: { properties: { fields: { type: SchemaTypes.object, description: "fields", title: "fields", }, interactivity: { type: SlackTypes.interactivity, description: "Context about the form submit action interactive event", title: "interactivity", }, submit_user: { type: SlackTypes.user_id, description: "Person who submitted the form", title: "Person who submitted the form", }, timestamp_started: { type: SlackTypes.timestamp, description: "Time when step started", title: "Time when step started", }, timestamp_completed: { type: SlackTypes.timestamp, description: "Time when step ended", title: "Time when step ended", }, }, required: [ "fields", "interactivity", "submit_user", "timestamp_started", "timestamp_completed", ], }, }; const actual = OpenForm.export(); assertNotStrictEquals(actual, expected); }); Deno.test("OpenForm can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_OpenForm_slack_function", title: "Test OpenForm", description: "This is a generated test to test OpenForm", }); testWorkflow.addStep(OpenForm, { title: "test", fields: "test", interactivity: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/open_form"); assertEquals(actual.inputs, { title: "test", fields: "test", interactivity: "test", }); }); Deno.test("All outputs of Slack function OpenForm should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_OpenForm_slack_function", title: "Test OpenForm", description: "This is a generated test to test OpenForm", }); const step = testWorkflow.addStep(OpenForm, { title: "test", fields: "test", interactivity: "test", }); assertExists(step.outputs.fields); assertExists(step.outputs.interactivity); assertExists(step.outputs.submit_user); assertExists(step.outputs.timestamp_started); assertExists(step.outputs.timestamp_completed); }); ================================================ FILE: src/schema/slack/functions/remove_reaction.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/remove_reaction", source_file: "", title: "Remove a reaction from a message", input_parameters: { properties: { message_context: { type: SlackTypes.message_context, description: "Select a message to unreact from", title: "Select a message to unreact from", }, emoji: { type: SchemaTypes.string, description: "Reaction (emoji) name", title: "Emoji", }, }, required: ["message_context", "emoji"], }, output_parameters: { properties: {}, required: [] }, }); ================================================ FILE: src/schema/slack/functions/remove_reaction_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertNotStrictEquals } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import RemoveReaction from "./remove_reaction.ts"; Deno.test("RemoveReaction generates valid FunctionManifest", () => { assertEquals( RemoveReaction.definition.callback_id, "slack#/functions/remove_reaction", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Remove a reaction from a message", input_parameters: { properties: { message_context: { type: SlackTypes.message_context, description: "Select a message to unreact from", title: "Select a message to unreact from", }, emoji: { type: SchemaTypes.string, description: "Reaction (emoji) name", title: "Emoji", }, }, required: ["message_context", "emoji"], }, output_parameters: { properties: {}, required: [] }, }; const actual = RemoveReaction.export(); assertNotStrictEquals(actual, expected); }); Deno.test("RemoveReaction can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_RemoveReaction_slack_function", title: "Test RemoveReaction", description: "This is a generated test to test RemoveReaction", }); testWorkflow.addStep(RemoveReaction, { message_context: "test", emoji: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/remove_reaction"); assertEquals(actual.inputs, { message_context: "test", emoji: "test" }); }); ================================================ FILE: src/schema/slack/functions/remove_user_from_usergroup.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/remove_user_from_usergroup", source_file: "", title: "Remove someone from a user group", description: "Additional permissions might be required", input_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "Search all user groups", title: "Select a user group", }, user_ids: { type: SchemaTypes.array, description: "Search all people", title: "Select member(s)", items: { type: SlackTypes.user_id }, }, }, required: ["usergroup_id", "user_ids"], }, output_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "User group", title: "User group", }, }, required: [], }, }); ================================================ FILE: src/schema/slack/functions/remove_user_from_usergroup_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import RemoveUserFromUsergroup from "./remove_user_from_usergroup.ts"; Deno.test("RemoveUserFromUsergroup generates valid FunctionManifest", () => { assertEquals( RemoveUserFromUsergroup.definition.callback_id, "slack#/functions/remove_user_from_usergroup", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Remove someone from a user group", description: "Additional permissions might be required", input_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "Search all user groups", title: "Select a user group", }, user_ids: { type: SchemaTypes.array, description: "Search all people", title: "Select member(s)", items: { type: SlackTypes.user_id }, }, }, required: ["usergroup_id", "user_ids"], }, output_parameters: { properties: { usergroup_id: { type: SlackTypes.usergroup_id, description: "User group", title: "User group", }, }, required: [], }, }; const actual = RemoveUserFromUsergroup.export(); assertNotStrictEquals(actual, expected); }); Deno.test("RemoveUserFromUsergroup can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_RemoveUserFromUsergroup_slack_function", title: "Test RemoveUserFromUsergroup", description: "This is a generated test to test RemoveUserFromUsergroup", }); testWorkflow.addStep(RemoveUserFromUsergroup, { usergroup_id: "test", user_ids: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals( actual.function_id, "slack#/functions/remove_user_from_usergroup", ); assertEquals(actual.inputs, { usergroup_id: "test", user_ids: "test" }); }); Deno.test("All outputs of Slack function RemoveUserFromUsergroup should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_RemoveUserFromUsergroup_slack_function", title: "Test RemoveUserFromUsergroup", description: "This is a generated test to test RemoveUserFromUsergroup", }); const step = testWorkflow.addStep(RemoveUserFromUsergroup, { usergroup_id: "test", user_ids: "test", }); assertExists(step.outputs.usergroup_id); }); ================================================ FILE: src/schema/slack/functions/reply_in_thread.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/reply_in_thread", source_file: "", title: "Reply to a message in thread", input_parameters: { properties: { message_context: { type: SlackTypes.message_context, description: "Select a message to reply to", title: "Select a message to reply to", }, message: { type: SlackTypes.rich_text, description: "Add a reply", title: "Add a reply", }, reply_broadcast: { type: SchemaTypes.boolean, description: "Also send to conversation", title: "Also send to conversation", }, metadata: { type: SchemaTypes.object, description: "Metadata you post to Slack is accessible to any app or user who is a member of that workspace", title: "Message metadata", properties: { event_type: { type: SchemaTypes.string }, event_payload: { type: SchemaTypes.object }, }, additionalProperties: true, required: ["event_type", "event_payload"], }, interactive_blocks: { type: SlackTypes.blocks, description: "Button(s) to send with the message", title: "Button(s) to send with the message", }, files: { type: SchemaTypes.array, description: "File(s) to attach to the message", title: "File(s) to attach to the message", items: { type: SlackTypes.file_id }, }, }, required: ["message_context", "message"], }, output_parameters: { properties: { message_context: { type: SlackTypes.message_context, description: "Reference to the message sent", title: "Reference to the message sent", }, message_link: { type: SchemaTypes.string, description: "Message link", title: "Message link", }, action: { type: SchemaTypes.object, description: "Button interactivity data", title: "Button interactivity data", }, interactivity: { type: SlackTypes.interactivity, description: "Interactivity context", title: "interactivity", }, timestamp_started: { type: SlackTypes.timestamp, description: "Time when step started", title: "Time when step started", }, timestamp_completed: { type: SlackTypes.timestamp, description: "Time when step ended", title: "Time when step ended", }, }, required: [ "message_context", "message_link", "timestamp_started", "timestamp_completed", ], }, }); ================================================ FILE: src/schema/slack/functions/reply_in_thread_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import ReplyInThread from "./reply_in_thread.ts"; Deno.test("ReplyInThread generates valid FunctionManifest", () => { assertEquals( ReplyInThread.definition.callback_id, "slack#/functions/reply_in_thread", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Reply to a message in thread", input_parameters: { properties: { message_context: { type: SlackTypes.message_context, description: "Select a message to reply to", title: "Select a message to reply to", }, message: { type: SlackTypes.rich_text, description: "Add a reply", title: "Add a reply", }, reply_broadcast: { type: SchemaTypes.boolean, description: "Also send to conversation", title: "Also send to conversation", }, metadata: { type: SchemaTypes.object, description: "Metadata you post to Slack is accessible to any app or user who is a member of that workspace", title: "Message metadata", properties: { event_type: { type: SchemaTypes.string }, event_payload: { type: SchemaTypes.object }, }, additionalProperties: true, required: ["event_type", "event_payload"], }, interactive_blocks: { type: SlackTypes.blocks, description: "Button(s) to send with the message", title: "Button(s) to send with the message", }, files: { type: SchemaTypes.array, description: "File(s) to attach to the message", title: "File(s) to attach to the message", items: { type: SlackTypes.file_id }, }, }, required: ["message_context", "message"], }, output_parameters: { properties: { message_context: { type: SlackTypes.message_context, description: "Reference to the message sent", title: "Reference to the message sent", }, message_link: { type: SchemaTypes.string, description: "Message link", title: "Message link", }, action: { type: SchemaTypes.object, description: "Button interactivity data", title: "Button interactivity data", }, interactivity: { type: SlackTypes.interactivity, description: "Interactivity context", title: "interactivity", }, timestamp_started: { type: SlackTypes.timestamp, description: "Time when step started", title: "Time when step started", }, timestamp_completed: { type: SlackTypes.timestamp, description: "Time when step ended", title: "Time when step ended", }, }, required: [ "message_context", "message_link", "timestamp_started", "timestamp_completed", ], }, }; const actual = ReplyInThread.export(); assertNotStrictEquals(actual, expected); }); Deno.test("ReplyInThread can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ReplyInThread_slack_function", title: "Test ReplyInThread", description: "This is a generated test to test ReplyInThread", }); testWorkflow.addStep(ReplyInThread, { message_context: "test", message: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/reply_in_thread"); assertEquals(actual.inputs, { message_context: "test", message: "test" }); }); Deno.test("All outputs of Slack function ReplyInThread should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ReplyInThread_slack_function", title: "Test ReplyInThread", description: "This is a generated test to test ReplyInThread", }); const step = testWorkflow.addStep(ReplyInThread, { message_context: "test", message: "test", }); assertExists(step.outputs.message_context); assertExists(step.outputs.message_link); assertExists(step.outputs.action); assertExists(step.outputs.interactivity); assertExists(step.outputs.timestamp_started); assertExists(step.outputs.timestamp_completed); }); ================================================ FILE: src/schema/slack/functions/send_dm.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/send_dm", source_file: "", title: "Send a message to a person", input_parameters: { properties: { user_id: { type: SlackTypes.user_id, description: "Search all people", title: "Select a member", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, interactive_blocks: { type: SlackTypes.blocks, description: "Button(s) to send with the message", title: "Button(s) to send with the message", }, files: { type: SchemaTypes.array, description: "File(s) to attach to the message", title: "File(s) to attach to the message", items: { type: SlackTypes.file_id }, }, }, required: ["user_id", "message"], }, output_parameters: { properties: { message_timestamp: { type: SlackTypes.timestamp, description: "Message timestamp", title: "Message timestamp", }, message_link: { type: SchemaTypes.string, description: "Message link", title: "Message link", }, action: { type: SchemaTypes.object, description: "Button interactivity data", title: "Button interactivity data", }, interactivity: { type: SlackTypes.interactivity, description: "Interactivity context", title: "interactivity", }, message_context: { type: SlackTypes.message_context, description: "Reference to the message sent", title: "Reference to the message sent", }, timestamp_started: { type: SlackTypes.timestamp, description: "Time when step started", title: "Time when step started", }, timestamp_completed: { type: SlackTypes.timestamp, description: "Time when step ended", title: "Time when step ended", }, }, required: [ "message_timestamp", "message_link", "message_context", "timestamp_started", "timestamp_completed", ], }, }); ================================================ FILE: src/schema/slack/functions/send_dm_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import SendDm from "./send_dm.ts"; Deno.test("SendDm generates valid FunctionManifest", () => { assertEquals(SendDm.definition.callback_id, "slack#/functions/send_dm"); const expected: ManifestFunctionSchema = { source_file: "", title: "Send a message to a person", input_parameters: { properties: { user_id: { type: SlackTypes.user_id, description: "Search all people", title: "Select a member", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, interactive_blocks: { type: SlackTypes.blocks, description: "Button(s) to send with the message", title: "Button(s) to send with the message", }, files: { type: SchemaTypes.array, description: "File(s) to attach to the message", title: "File(s) to attach to the message", items: { type: SlackTypes.file_id }, }, }, required: ["user_id", "message"], }, output_parameters: { properties: { message_timestamp: { type: SlackTypes.timestamp, description: "Message timestamp", title: "Message timestamp", }, message_link: { type: SchemaTypes.string, description: "Message link", title: "Message link", }, action: { type: SchemaTypes.object, description: "Button interactivity data", title: "Button interactivity data", }, interactivity: { type: SlackTypes.interactivity, description: "Interactivity context", title: "interactivity", }, message_context: { type: SlackTypes.message_context, description: "Reference to the message sent", title: "Reference to the message sent", }, timestamp_started: { type: SlackTypes.timestamp, description: "Time when step started", title: "Time when step started", }, timestamp_completed: { type: SlackTypes.timestamp, description: "Time when step ended", title: "Time when step ended", }, }, required: [ "message_timestamp", "message_link", "message_context", "timestamp_started", "timestamp_completed", ], }, }; const actual = SendDm.export(); assertNotStrictEquals(actual, expected); }); Deno.test("SendDm can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_SendDm_slack_function", title: "Test SendDm", description: "This is a generated test to test SendDm", }); testWorkflow.addStep(SendDm, { user_id: "test", message: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/send_dm"); assertEquals(actual.inputs, { user_id: "test", message: "test" }); }); Deno.test("All outputs of Slack function SendDm should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_SendDm_slack_function", title: "Test SendDm", description: "This is a generated test to test SendDm", }); const step = testWorkflow.addStep(SendDm, { user_id: "test", message: "test", }); assertExists(step.outputs.message_timestamp); assertExists(step.outputs.message_link); assertExists(step.outputs.action); assertExists(step.outputs.interactivity); assertExists(step.outputs.message_context); assertExists(step.outputs.timestamp_started); assertExists(step.outputs.timestamp_completed); }); ================================================ FILE: src/schema/slack/functions/send_ephemeral_message.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/send_ephemeral_message", source_file: "", title: 'Send an "only visible to you" message', description: "Send a temporary message to someone in a channel that only they can see", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, user_id: { type: SlackTypes.user_id, description: "Search all people", title: "Select a member of the channel", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, thread_ts: { type: SchemaTypes.string, description: "Provide another message's ts value to make this message a reply", title: "Another message's timestamp value", }, }, required: ["channel_id", "user_id", "message"], }, output_parameters: { properties: { message_ts: { type: SlackTypes.message_ts, description: "Message timestamp", title: "Message timestamp", }, }, required: ["message_ts"], }, }); ================================================ FILE: src/schema/slack/functions/send_ephemeral_message_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import SendEphemeralMessage from "./send_ephemeral_message.ts"; Deno.test("SendEphemeralMessage generates valid FunctionManifest", () => { assertEquals( SendEphemeralMessage.definition.callback_id, "slack#/functions/send_ephemeral_message", ); const expected: ManifestFunctionSchema = { source_file: "", title: 'Send an "only visible to you" message', description: "Send a temporary message to someone in a channel that only they can see", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, user_id: { type: SlackTypes.user_id, description: "Search all people", title: "Select a member of the channel", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, thread_ts: { type: SchemaTypes.string, description: "Provide another message's ts value to make this message a reply", title: "Another message's timestamp value", }, }, required: ["channel_id", "user_id", "message"], }, output_parameters: { properties: { message_ts: { type: SlackTypes.message_ts, description: "Message timestamp", title: "Message timestamp", }, }, required: ["message_ts"], }, }; const actual = SendEphemeralMessage.export(); assertNotStrictEquals(actual, expected); }); Deno.test("SendEphemeralMessage can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_SendEphemeralMessage_slack_function", title: "Test SendEphemeralMessage", description: "This is a generated test to test SendEphemeralMessage", }); testWorkflow.addStep(SendEphemeralMessage, { channel_id: "test", user_id: "test", message: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/send_ephemeral_message"); assertEquals(actual.inputs, { channel_id: "test", user_id: "test", message: "test", }); }); Deno.test("All outputs of Slack function SendEphemeralMessage should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_SendEphemeralMessage_slack_function", title: "Test SendEphemeralMessage", description: "This is a generated test to test SendEphemeralMessage", }); const step = testWorkflow.addStep(SendEphemeralMessage, { channel_id: "test", user_id: "test", message: "test", }); assertExists(step.outputs.message_ts); }); ================================================ FILE: src/schema/slack/functions/send_message.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/send_message", source_file: "", title: "Send a message to a channel", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, metadata: { type: SchemaTypes.object, description: "Metadata you post to Slack is accessible to any app or user who is a member of that workspace", title: "Message metadata", properties: { event_type: { type: SchemaTypes.string }, event_payload: { type: SchemaTypes.object }, }, additionalProperties: true, required: ["event_type", "event_payload"], }, interactive_blocks: { type: SlackTypes.blocks, description: "Button(s) to send with the message", title: "Button(s) to send with the message", }, files: { type: SchemaTypes.array, description: "File(s) to attach to the message", title: "File(s) to attach to the message", items: { type: SlackTypes.file_id }, }, }, required: ["channel_id", "message"], }, output_parameters: { properties: { message_timestamp: { type: SlackTypes.timestamp, description: "Message timestamp", title: "Message timestamp", }, message_link: { type: SchemaTypes.string, description: "Message link", title: "Message link", }, action: { type: SchemaTypes.object, description: "Button interactivity data", title: "Button interactivity data", }, interactivity: { type: SlackTypes.interactivity, description: "Interactivity context", title: "interactivity", }, message_context: { type: SlackTypes.message_context, description: "Reference to the message sent", title: "Reference to the message sent", }, timestamp_started: { type: SlackTypes.timestamp, description: "Time when step started", title: "Time when step started", }, timestamp_completed: { type: SlackTypes.timestamp, description: "Time when step ended", title: "Time when step ended", }, }, required: [ "message_timestamp", "message_link", "message_context", "timestamp_started", "timestamp_completed", ], }, }); ================================================ FILE: src/schema/slack/functions/send_message_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import SendMessage from "./send_message.ts"; Deno.test("SendMessage generates valid FunctionManifest", () => { assertEquals( SendMessage.definition.callback_id, "slack#/functions/send_message", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Send a message to a channel", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, metadata: { type: SchemaTypes.object, description: "Metadata you post to Slack is accessible to any app or user who is a member of that workspace", title: "Message metadata", properties: { event_type: { type: SchemaTypes.string }, event_payload: { type: SchemaTypes.object }, }, additionalProperties: true, required: ["event_type", "event_payload"], }, interactive_blocks: { type: SlackTypes.blocks, description: "Button(s) to send with the message", title: "Button(s) to send with the message", }, files: { type: SchemaTypes.array, description: "File(s) to attach to the message", title: "File(s) to attach to the message", items: { type: SlackTypes.file_id }, }, }, required: ["channel_id", "message"], }, output_parameters: { properties: { message_timestamp: { type: SlackTypes.timestamp, description: "Message timestamp", title: "Message timestamp", }, message_link: { type: SchemaTypes.string, description: "Message link", title: "Message link", }, action: { type: SchemaTypes.object, description: "Button interactivity data", title: "Button interactivity data", }, interactivity: { type: SlackTypes.interactivity, description: "Interactivity context", title: "interactivity", }, message_context: { type: SlackTypes.message_context, description: "Reference to the message sent", title: "Reference to the message sent", }, timestamp_started: { type: SlackTypes.timestamp, description: "Time when step started", title: "Time when step started", }, timestamp_completed: { type: SlackTypes.timestamp, description: "Time when step ended", title: "Time when step ended", }, }, required: [ "message_timestamp", "message_link", "message_context", "timestamp_started", "timestamp_completed", ], }, }; const actual = SendMessage.export(); assertNotStrictEquals(actual, expected); }); Deno.test("SendMessage can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_SendMessage_slack_function", title: "Test SendMessage", description: "This is a generated test to test SendMessage", }); testWorkflow.addStep(SendMessage, { channel_id: "test", message: "test" }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/send_message"); assertEquals(actual.inputs, { channel_id: "test", message: "test" }); }); Deno.test("All outputs of Slack function SendMessage should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_SendMessage_slack_function", title: "Test SendMessage", description: "This is a generated test to test SendMessage", }); const step = testWorkflow.addStep(SendMessage, { channel_id: "test", message: "test", }); assertExists(step.outputs.message_timestamp); assertExists(step.outputs.message_link); assertExists(step.outputs.action); assertExists(step.outputs.interactivity); assertExists(step.outputs.message_context); assertExists(step.outputs.timestamp_started); assertExists(step.outputs.timestamp_completed); }); ================================================ FILE: src/schema/slack/functions/share_canvas.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/share_canvas", source_file: "", title: "Share a canvas", input_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Search all canvases", title: "Select a canvas", }, channel_ids: { type: SchemaTypes.array, description: "Select channels", title: "Select channels", items: { type: SlackTypes.channel_id }, }, user_ids: { type: SchemaTypes.array, description: "Search users", title: "Select people", items: { type: SlackTypes.user_id }, }, access_level: { type: SchemaTypes.string, description: "Select an option", title: "Select access level", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, }, required: ["canvas_id", "access_level"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: [], }, }); ================================================ FILE: src/schema/slack/functions/share_canvas_in_thread.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/share_canvas_in_thread", source_file: "", title: "Share canvas in thread", input_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Search all canvases", title: "Select a canvas", }, message_context: { type: SlackTypes.message_context, description: "Select a message to reply to", title: "Select a message to reply to", }, access_level: { type: SchemaTypes.string, description: "Select an option", title: "Select access level", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, reply_broadcast: { type: SchemaTypes.boolean, description: "Also send to conversation", title: "Also send to conversation", }, }, required: ["canvas_id", "message_context", "access_level"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, message_context: { type: SlackTypes.message_context, description: "Reference to the message sent", title: "Reference to the message sent", }, }, required: [], }, }); ================================================ FILE: src/schema/slack/functions/share_canvas_in_thread_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import ShareCanvasInThread from "./share_canvas_in_thread.ts"; Deno.test("ShareCanvasInThread generates valid FunctionManifest", () => { assertEquals( ShareCanvasInThread.definition.callback_id, "slack#/functions/share_canvas_in_thread", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Share canvas in thread", input_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Search all canvases", title: "Select a canvas", }, message_context: { type: SlackTypes.message_context, description: "Select a message to reply to", title: "Select a message to reply to", }, access_level: { type: SchemaTypes.string, description: "Select an option", title: "Select access level", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, reply_broadcast: { type: SchemaTypes.boolean, description: "Also send to conversation", title: "Also send to conversation", }, }, required: ["canvas_id", "message_context", "access_level"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, message_context: { type: SlackTypes.message_context, description: "Reference to the message sent", title: "Reference to the message sent", }, }, required: [], }, }; const actual = ShareCanvasInThread.export(); assertNotStrictEquals(actual, expected); }); Deno.test("ShareCanvasInThread can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ShareCanvasInThread_slack_function", title: "Test ShareCanvasInThread", description: "This is a generated test to test ShareCanvasInThread", }); testWorkflow.addStep(ShareCanvasInThread, { canvas_id: "test", message_context: "test", access_level: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/share_canvas_in_thread"); assertEquals(actual.inputs, { canvas_id: "test", message_context: "test", access_level: "test", }); }); Deno.test("All outputs of Slack function ShareCanvasInThread should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ShareCanvasInThread_slack_function", title: "Test ShareCanvasInThread", description: "This is a generated test to test ShareCanvasInThread", }); const step = testWorkflow.addStep(ShareCanvasInThread, { canvas_id: "test", message_context: "test", access_level: "test", }); assertExists(step.outputs.canvas_id); assertExists(step.outputs.message_context); }); ================================================ FILE: src/schema/slack/functions/share_canvas_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import ShareCanvas from "./share_canvas.ts"; Deno.test("ShareCanvas generates valid FunctionManifest", () => { assertEquals( ShareCanvas.definition.callback_id, "slack#/functions/share_canvas", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Share a canvas", input_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Search all canvases", title: "Select a canvas", }, channel_ids: { type: SchemaTypes.array, description: "Select channels", title: "Select channels", items: { type: SlackTypes.channel_id }, }, user_ids: { type: SchemaTypes.array, description: "Search users", title: "Select people", items: { type: SlackTypes.user_id }, }, access_level: { type: SchemaTypes.string, description: "Select an option", title: "Select access level", }, message: { type: SlackTypes.rich_text, description: "Add a message", title: "Add a message", }, }, required: ["canvas_id", "access_level"], }, output_parameters: { properties: { canvas_id: { type: SlackTypes.canvas_id, description: "Canvas link", title: "Canvas link", }, }, required: [], }, }; const actual = ShareCanvas.export(); assertNotStrictEquals(actual, expected); }); Deno.test("ShareCanvas can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ShareCanvas_slack_function", title: "Test ShareCanvas", description: "This is a generated test to test ShareCanvas", }); testWorkflow.addStep(ShareCanvas, { canvas_id: "test", access_level: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/share_canvas"); assertEquals(actual.inputs, { canvas_id: "test", access_level: "test" }); }); Deno.test("All outputs of Slack function ShareCanvas should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_ShareCanvas_slack_function", title: "Test ShareCanvas", description: "This is a generated test to test ShareCanvas", }); const step = testWorkflow.addStep(ShareCanvas, { canvas_id: "test", access_level: "test", }); assertExists(step.outputs.canvas_id); }); ================================================ FILE: src/schema/slack/functions/update_channel_topic.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { DefineFunction } from "../../../functions/mod.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; export default DefineFunction({ callback_id: "slack#/functions/update_channel_topic", source_file: "", title: "Update the channel topic", description: "You or the people using the workflow must be members of the channel", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, topic: { type: SchemaTypes.string, description: "Enter a topic", title: "Add a topic", }, }, required: ["channel_id", "topic"], }, output_parameters: { properties: { topic: { type: SchemaTypes.string, description: "Channel topic", title: "Channel topic", }, }, required: ["topic"], }, }); ================================================ FILE: src/schema/slack/functions/update_channel_topic_test.ts ================================================ /** This file was autogenerated. Follow the steps in src/schema/slack/functions/_scripts/README.md to rebuild **/ import { assertEquals, assertExists, assertNotStrictEquals, } from "../../../dev_deps.ts"; import { DefineWorkflow } from "../../../workflows/mod.ts"; import type { ManifestFunctionSchema } from "../../../manifest/manifest_schema.ts"; import SchemaTypes from "../../schema_types.ts"; import SlackTypes from "../schema_types.ts"; import UpdateChannelTopic from "./update_channel_topic.ts"; Deno.test("UpdateChannelTopic generates valid FunctionManifest", () => { assertEquals( UpdateChannelTopic.definition.callback_id, "slack#/functions/update_channel_topic", ); const expected: ManifestFunctionSchema = { source_file: "", title: "Update the channel topic", description: "You or the people using the workflow must be members of the channel", input_parameters: { properties: { channel_id: { type: SlackTypes.channel_id, description: "Search all channels", title: "Select a channel", }, topic: { type: SchemaTypes.string, description: "Enter a topic", title: "Add a topic", }, }, required: ["channel_id", "topic"], }, output_parameters: { properties: { topic: { type: SchemaTypes.string, description: "Channel topic", title: "Channel topic", }, }, required: ["topic"], }, }; const actual = UpdateChannelTopic.export(); assertNotStrictEquals(actual, expected); }); Deno.test("UpdateChannelTopic can be used as a Slack function in a workflow step", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_UpdateChannelTopic_slack_function", title: "Test UpdateChannelTopic", description: "This is a generated test to test UpdateChannelTopic", }); testWorkflow.addStep(UpdateChannelTopic, { channel_id: "test", topic: "test", }); const actual = testWorkflow.steps[0].export(); assertEquals(actual.function_id, "slack#/functions/update_channel_topic"); assertEquals(actual.inputs, { channel_id: "test", topic: "test" }); }); Deno.test("All outputs of Slack function UpdateChannelTopic should exist", () => { const testWorkflow = DefineWorkflow({ callback_id: "test_UpdateChannelTopic_slack_function", title: "Test UpdateChannelTopic", description: "This is a generated test to test UpdateChannelTopic", }); const step = testWorkflow.addStep(UpdateChannelTopic, { channel_id: "test", topic: "test", }); assertExists(step.outputs.topic); }); ================================================ FILE: src/schema/slack/mod.ts ================================================ import SlackTypes from "./schema_types.ts"; import SlackFunctions from "./functions/mod.ts"; const SlackSchema = { types: SlackTypes, functions: SlackFunctions, } as const; export default SlackSchema; ================================================ FILE: src/schema/slack/schema_types.ts ================================================ import { SlackPrimitiveTypes } from "./types/mod.ts"; import { CustomSlackTypes } from "./types/custom/mod.ts"; export default { ...SlackPrimitiveTypes, ...CustomSlackTypes }; ================================================ FILE: src/schema/slack/types/custom/custom_slack_types_test.ts ================================================ import { assertEquals } from "../../../../dev_deps.ts"; import { CustomSlackTypes, InternalSlackTypes } from "./mod.ts"; Deno.test("Custom Slack Types are stringified correctly", () => { const { interactivity, user_context, message_context } = CustomSlackTypes; const { form_input_object } = InternalSlackTypes; assertEquals(`${interactivity}`, interactivity.id); assertEquals(`${user_context}`, user_context.id); assertEquals(`${message_context}`, message_context.id); assertEquals(`${form_input_object}`, form_input_object.id); }); ================================================ FILE: src/schema/slack/types/custom/form_input.ts ================================================ import SchemaTypes from "../../../schema_types.ts"; import { DefineType } from "../../../../types/mod.ts"; const FormInput = DefineType({ name: "slack#/types/form_input_object", description: "Input fields to be shown on the form", type: SchemaTypes.object, properties: { required: { type: SchemaTypes.array, items: { type: SchemaTypes.string, }, }, elements: { type: SchemaTypes.array, items: { type: SchemaTypes.object, }, }, }, required: ["elements"], }); export { FormInput }; ================================================ FILE: src/schema/slack/types/custom/interactivity.ts ================================================ import SchemaTypes from "../../../schema_types.ts"; import { DefineType } from "../../../../types/mod.ts"; import { UserContextType } from "./user_context.ts"; const InteractivityType = DefineType({ name: "slack#/types/interactivity", description: "Context about a user interaction", type: SchemaTypes.object, properties: { interactivity_pointer: { type: SchemaTypes.string, }, interactor: { type: UserContextType, }, }, required: ["interactivity_pointer", "interactor"], }); export { InteractivityType }; ================================================ FILE: src/schema/slack/types/custom/message_context.ts ================================================ import SchemaTypes from "../../../schema_types.ts"; import { SlackPrimitiveTypes } from "../../types/mod.ts"; import { DefineType } from "../../../../types/mod.ts"; const MessageContextType = DefineType({ name: "slack#/types/message_context", type: SchemaTypes.object, properties: { message_ts: { type: SlackPrimitiveTypes.message_ts, }, user_id: { type: SlackPrimitiveTypes.user_id, }, channel_id: { type: SlackPrimitiveTypes.channel_id, }, }, required: ["message_ts"], }); export { MessageContextType }; ================================================ FILE: src/schema/slack/types/custom/mod.ts ================================================ import { InteractivityType } from "./interactivity.ts"; import { UserContextType } from "./user_context.ts"; import { FormInput } from "./form_input.ts"; import { MessageContextType } from "./message_context.ts"; export const CustomSlackTypes = { interactivity: InteractivityType, user_context: UserContextType, message_context: MessageContextType, }; export const InternalSlackTypes = { form_input_object: FormInput, }; ================================================ FILE: src/schema/slack/types/custom/user_context.ts ================================================ import SchemaTypes from "../../../schema_types.ts"; import { SlackPrimitiveTypes } from "../../types/mod.ts"; import { DefineType } from "../../../../types/mod.ts"; const UserContextType = DefineType({ name: "slack#/types/user_context", type: SchemaTypes.object, properties: { id: { type: SlackPrimitiveTypes.user_id, }, secret: { type: SchemaTypes.string, }, }, required: ["id", "secret"], }); export { UserContextType }; ================================================ FILE: src/schema/slack/types/mod.ts ================================================ const SlackPrimitiveTypes = { blocks: "slack#/types/blocks", canvas_id: "slack#/types/canvas_id", canvas_template_id: "slack#/types/canvas_template_id", channel_id: "slack#/types/channel_id", date: "slack#/types/date", expanded_rich_text: "slack#/types/expanded_rich_text", file_id: "slack#/types/file_id", // TODO: once List steps work in code, bring this back // list_id: "slack#/types/list_id", message_ts: "slack#/types/message_ts", oauth2: "slack#/types/credential/oauth2", rich_text: "slack#/types/rich_text", salesforce_record_id: "slack#/types/salesforce_record_id", team_id: "slack#/types/team_id", timestamp: "slack#/types/timestamp", user_id: "slack#/types/user_id", usergroup_id: "slack#/types/usergroup_id", } as const; export type ValidSlackPrimitiveTypes = typeof SlackPrimitiveTypes[keyof typeof SlackPrimitiveTypes]; export { SlackPrimitiveTypes }; ================================================ FILE: src/schema/types.ts ================================================ type BaseSchemaType = { types?: { [key: string]: string; }; }; // Allow for sub-schema, i.e. schema.slack.types... export type SchemaType = BaseSchemaType & { [key: string]: BaseSchemaType; }; ================================================ FILE: src/test_utils.ts ================================================ import { assertEquals } from "./dev_deps.ts"; // deno-lint-ignore ban-types type IsAny<T> = unknown extends T ? T extends {} ? T : never : never; type NotAny<T> = T extends IsAny<T> ? never : T; /** @description Prevents a value of type `any` from being passed into `assertEquals` */ export const assertEqualsTypedValues = <T>( actual: NotAny<T>, expected: NotAny<T>, msg?: string, ): void => assertEquals<T>(actual, expected, msg); /** * Checks whether T includes U. */ export type CanBe<T, U> = Extract<T, U> extends never ? false : true; /** * Checks whether T can never include U. */ export type CannotBe<T, U> = Extract<T, U> extends never ? true : false; /** * Checks whether the provided type parameter allows to be undefined. * Useful for checking optionality. */ export type CanBeUndefined<T> = CanBe<T, undefined> extends true ? true : false; export type CannotBeUndefined<T> = CannotBe<T, undefined> extends true ? true : false; ================================================ FILE: src/test_utils_test.ts ================================================ import { assertExists } from "https://deno.land/std@0.152.0/testing/asserts.ts"; import { assert, fail } from "./dev_deps.ts"; import { assertEqualsTypedValues, type CanBe, type CanBeUndefined, type CannotBe, type CannotBeUndefined, } from "./test_utils.ts"; Deno.test("CanBe", async (t) => { const T = { test: "", notIncluded: "", }; const U = { test: "", }; await t.step( `should be true when ${Object(T).name} includes ${Object(U).name}`, () => { assert<CanBe<typeof T, typeof U>>(true); }, ); await t.step( `should be false when ${Object(U).name} does not include ${Object(T).name}`, () => { assert<CanBe<typeof U, typeof T>>(false); }, ); }); Deno.test("CannotBe", async (t) => { const T = { test: "", notIncluded: "", }; const U = { test: "", }; await t.step( `should be true when ${Object(U).name} can never include ${Object(T).name}`, () => { assert<CannotBe<typeof U, typeof T>>(true); }, ); await t.step( `should be false when ${Object(U).name} does not include ${Object(T).name}`, () => { assert<CannotBe<typeof T, typeof U>>(false); }, ); }); Deno.test("CanBeUndefined", async (t) => { await t.step("should be true when variable can be undefined", () => { const canBeUndefined: string | undefined = undefined; assert<CanBeUndefined<typeof canBeUndefined>>(true); }); await t.step("should be false when variable cannot be undefined", () => { const cannotBeUndefined = ""; assert<CanBeUndefined<typeof cannotBeUndefined>>(false); }); }); Deno.test("CannotBeUndefined", async (t) => { await t.step("should be true when variable can not be undefined", () => { const cannotBeUndefined = ""; assert<CannotBeUndefined<typeof cannotBeUndefined>>(true); }); await t.step("should be false when variable cannot be undefined", () => { const canBeUndefined: string | undefined = undefined; assert<CannotBeUndefined<typeof canBeUndefined>>(false); }); }); Deno.test(assertEqualsTypedValues.name, async (t) => { type T = { output?: { out: boolean; }; }; await t.step( "should assert true when object follow the same type and value", () => { const typedValue: T = { output: { out: true, }, }; assertEqualsTypedValues(typedValue, { output: { out: true, }, }); }, ); await t.step( "should raise error when objects do not follow the same type and value", () => { const typedValue: T = { output: { out: true, }, }; try { assertEqualsTypedValues(typedValue, { output: { out: false, }, }); fail(`${assertEqualsTypedValues} should have raised an exception`); } catch (error) { assertExists(error); } }, ); }); ================================================ FILE: src/type_utils.ts ================================================ /** @description Defines accepted recursion depth values */ export type RecursionDepthLevel = 0 | 1 | 2 | 3 | 4 | 5; /** @description Defines the maximum number of times we allow for recursion */ export type MaxRecursionDepth = 5; /** @description Increases the recursion depth value one at a time */ export type IncreaseDepth<Depth extends RecursionDepthLevel = 0> = Depth extends 0 ? 1 : Depth extends 1 ? 2 : Depth extends 2 ? 3 : Depth extends 3 ? 4 : Depth extends 4 ? 5 : Depth extends 5 ? MaxRecursionDepth : MaxRecursionDepth; /** @description Provides typeahead for passed strict string values while allowing any other string to be passed as well */ // deno-lint-ignore ban-types export type LooseStringAutocomplete<T> = T | (string & {}); ================================================ FILE: src/types/mod.ts ================================================ import type { SlackManifest } from "../manifest/mod.ts"; import type { ManifestCustomTypeSchema } from "../manifest/manifest_schema.ts"; import type { CustomTypeDefinition, ICustomType } from "./types.ts"; import { isTypedArray, isTypedObject } from "../parameters/mod.ts"; import type { TypedObjectProperties, TypedObjectRequiredProperties, } from "../parameters/definition_types.ts"; // Helper that uses a type predicate for narrowing down to a Custom Type export const isCustomType = (type: string | ICustomType): type is ICustomType => type instanceof CustomType; export function DefineType< Props extends TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props>, Def extends CustomTypeDefinition<Props, RequiredProps>, >( definition: Def, ): CustomType<Props, RequiredProps, Def> { return new CustomType(definition); } export class CustomType< Props extends TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props>, Def extends CustomTypeDefinition<Props, RequiredProps>, > implements ICustomType { public id: string; public title: string | undefined; public description: string | undefined; constructor( public definition: Def, ) { this.id = definition.name; this.definition = definition; this.description = definition.description; this.title = definition.title; } private generateReferenceString() { return this.id.includes("#/") ? this.id : `#/types/${this.id}`; } toString() { return this.generateReferenceString(); } toJSON() { return this.generateReferenceString(); } registerParameterTypes(manifest: SlackManifest) { if (isCustomType(this.definition.type)) { manifest.registerType(this.definition.type); } else if (isTypedArray(this.definition)) { if (isCustomType(this.definition.items.type)) { manifest.registerType(this.definition.items.type); } } else if (isTypedObject(this.definition)) { Object.values(this.definition.properties)?.forEach((property) => { if (isCustomType(property.type)) { manifest.registerType(property.type); } }); } } export(): ManifestCustomTypeSchema { // remove name from the definition we pass to the manifest const { name: _n, ...definition } = this.definition; // Using JSON.stringify to force any custom types into their string reference return JSON.parse(JSON.stringify(definition)); } } ================================================ FILE: src/types/types.ts ================================================ import type { ParameterDefinitionWithGenerics, TypedObjectProperties, TypedObjectRequiredProperties, } from "../parameters/definition_types.ts"; import type { SlackManifest } from "../manifest/mod.ts"; import type { ManifestCustomTypeSchema } from "../manifest/manifest_schema.ts"; export type CustomTypeDefinition< Props extends TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props>, > = & { name: string } & ParameterDefinitionWithGenerics<Props, RequiredProps>; export interface ICustomType< Props extends TypedObjectProperties = TypedObjectProperties, RequiredProps extends TypedObjectRequiredProperties<Props> = TypedObjectRequiredProperties<Props>, > { id: string; definition: CustomTypeDefinition<Props, RequiredProps>; description?: string; registerParameterTypes: (manifest: SlackManifest) => void; export(): ManifestCustomTypeSchema; } ================================================ FILE: src/types/types_test.ts ================================================ import { DefineType } from "./mod.ts"; import { assertEquals } from "../dev_deps.ts"; Deno.test("DefineType test against id using the name parameter", () => { const Type = DefineType({ title: "Title", description: "Description", name: "Name", type: "string", }); assertEquals(Type.id, "Name"); }); Deno.test("DefineType test toString using the name parameter", () => { const Type = DefineType({ title: "Title", description: "Description", name: "Name", type: "string", }); const typeJson = Type.toJSON(); assertEquals(typeJson, "#/types/Name"); }); Deno.test("DefineType test export using the name parameter", () => { const Type = DefineType({ title: "Title", description: "Description", name: "Name", type: "string", }); const exportType = Type.export(); assertEquals(exportType, { title: "Title", description: "Description", type: "string", }); }); ================================================ FILE: src/types.ts ================================================ // ---------------------------------------------------------------------------- // Invocation // ---------------------------------------------------------------------------- // This is the schema received from the runtime // TODO: flush this out as we add support for other payloads export type InvocationPayload<Body> = { // TODO: type this out to handle multiple body types body: Body; context: { bot_access_token: string; variables: Record<string, string>; }; }; // ---------------------------------------------------------------------------- // Env // ---------------------------------------------------------------------------- export type Env = Record<string, string>; export type { SlackAPIClient, Trigger } from "./deps.ts"; ================================================ FILE: src/workflows/mod.ts ================================================ import type { SlackManifest } from "../manifest/mod.ts"; import type { ManifestWorkflowSchema } from "../manifest/manifest_schema.ts"; import type { ISlackFunctionDefinition } from "../functions/types.ts"; import type { ParameterSetDefinition, ParameterVariableType, PossibleParameterKeys, } from "../parameters/types.ts"; import { ParameterVariable } from "../parameters/mod.ts"; import { TypedWorkflowStepDefinition, UntypedWorkflowStepDefinition, type WorkflowStepDefinition, } from "./workflow-step.ts"; import type { ISlackWorkflow, SlackWorkflowDefinitionArgs, WorkflowInputs, WorkflowOutputs, WorkflowStepInputs, } from "./types.ts"; export const DefineWorkflow = < Inputs extends ParameterSetDefinition, Outputs extends ParameterSetDefinition, RequiredInputs extends PossibleParameterKeys<Inputs>, RequiredOutputs extends PossibleParameterKeys<Outputs>, CallbackID extends string, >( definition: SlackWorkflowDefinitionArgs< Inputs, Outputs, RequiredInputs, RequiredOutputs, CallbackID >, ) => { return new WorkflowDefinition(definition); }; export class WorkflowDefinition< Inputs extends ParameterSetDefinition, Outputs extends ParameterSetDefinition, RequiredInputs extends PossibleParameterKeys<Inputs>, RequiredOutputs extends PossibleParameterKeys<Outputs>, CallbackID extends string, > implements ISlackWorkflow { public id: string; public definition: SlackWorkflowDefinitionArgs< Inputs, Outputs, RequiredInputs, RequiredOutputs, CallbackID >; public inputs: WorkflowInputs< Inputs, RequiredInputs >; public outputs: WorkflowOutputs< Outputs, RequiredOutputs >; steps: WorkflowStepDefinition[] = []; constructor( definition: SlackWorkflowDefinitionArgs< Inputs, Outputs, RequiredInputs, RequiredOutputs, CallbackID >, ) { this.id = definition.callback_id; this.definition = definition; this.inputs = {} as WorkflowInputs< Inputs, RequiredInputs >; this.outputs = {} as WorkflowOutputs< Outputs, RequiredOutputs >; for ( const [inputName, inputDefinition] of Object.entries( definition.input_parameters?.properties ? definition.input_parameters.properties : {}, ) ) { // deno-lint-ignore ban-ts-comment //@ts-ignore this.inputs[ inputName as keyof Inputs ] = ParameterVariable( "inputs", inputName, inputDefinition, ) as ParameterVariableType< Inputs[typeof inputName] >; } } // Supports adding a typed step where an ISlackFunctionDefinition reference is used, which produces typed inputs and outputs // and the functionReference string can be rerived from that ISlackFunctionDefinition reference // Important that this overload is 1st, as it's the more specific match, and preffered type if it matches addStep< StepInputs extends ParameterSetDefinition, StepOutputs extends ParameterSetDefinition, RequiredStepInputs extends PossibleParameterKeys<StepInputs>, RequiredStepOutputs extends PossibleParameterKeys<StepOutputs>, >( slackFunction: ISlackFunctionDefinition< StepInputs, StepOutputs, RequiredStepInputs, RequiredStepOutputs >, inputs: WorkflowStepInputs<StepInputs, RequiredStepInputs>, ): TypedWorkflowStepDefinition< StepInputs, StepOutputs, RequiredStepInputs, RequiredStepOutputs >; // Supports adding an untyped step by using a plain function reference string and input configuration // This won't support any typed inputs or outputs on the step, but can be useful when adding a step w/o the type definition available addStep( functionReference: string, // This is essentially an untyped step input configuration inputs: WorkflowStepInputs< ParameterSetDefinition, PossibleParameterKeys<ParameterSetDefinition> >, ): UntypedWorkflowStepDefinition; // The runtime implementation of addStep handles both signatures (straight function-reference & config, or ISlackFunctionDefinition) addStep< StepInputs extends ParameterSetDefinition, StepOutputs extends ParameterSetDefinition, RequiredStepInputs extends PossibleParameterKeys<StepInputs>, RequiredStepOutputs extends PossibleParameterKeys<StepOutputs>, >( functionOrReference: | string | ISlackFunctionDefinition< StepInputs, StepOutputs, RequiredStepInputs, RequiredStepOutputs >, inputs: WorkflowStepInputs<StepInputs, RequiredStepInputs>, ): WorkflowStepDefinition { const stepId = `${this.steps.length}`; if (typeof functionOrReference === "string") { const newStep = new UntypedWorkflowStepDefinition( stepId, functionOrReference, inputs, ); this.steps.push(newStep); return newStep; } const slackFunction = functionOrReference as ISlackFunctionDefinition< StepInputs, StepOutputs, RequiredStepInputs, RequiredStepOutputs >; const newStep = new TypedWorkflowStepDefinition( stepId, slackFunction, inputs, ); this.steps.push(newStep); return newStep; } export(): ManifestWorkflowSchema { return { title: this.definition.title, description: this.definition.description, input_parameters: this.definition.input_parameters, steps: this.steps.map((s) => s.export()), }; } registerStepFunctions(manifest: SlackManifest) { this.steps.forEach((s) => s.registerFunction(manifest)); } registerParameterTypes(manifest: SlackManifest) { const { input_parameters: inputParams, output_parameters: outputParams } = this.definition; manifest.registerTypes(inputParams?.properties ?? {}); manifest.registerTypes(outputParams?.properties ?? {}); } toJSON() { return this.export(); } } ================================================ FILE: src/workflows/types.ts ================================================ import type { SlackManifest } from "../manifest/mod.ts"; import type { ManifestWorkflowSchema } from "../manifest/manifest_schema.ts"; import type { ParameterPropertiesDefinition, ParameterSetDefinition, ParameterVariableType, PossibleParameterKeys, } from "../parameters/types.ts"; export interface ISlackWorkflow { id: string; export: () => ManifestWorkflowSchema; registerStepFunctions: (manifest: SlackManifest) => void; registerParameterTypes: (manfest: SlackManifest) => void; } export type SlackWorkflowDefinition<Definition> = Definition extends SlackWorkflowDefinitionArgs<infer I, infer O, infer RI, infer RO, infer CB> ? SlackWorkflowDefinitionArgs<I, O, RI, RO, CB> : never; export type SlackWorkflowDefinitionArgs< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInputs extends PossibleParameterKeys<InputParameters>, RequiredOutputs extends PossibleParameterKeys<OutputParameters>, CallbackID extends string, > = { callback_id: CallbackID; title: string; description?: string; "input_parameters"?: ParameterPropertiesDefinition< InputParameters, RequiredInputs >; "output_parameters"?: ParameterPropertiesDefinition< OutputParameters, RequiredOutputs >; }; export type WorkflowInputs< Params extends ParameterSetDefinition, RequiredParams extends PossibleParameterKeys<Params>, > = WorkflowParameterReferences<Params, RequiredParams>; export type WorkflowOutputs< Params extends ParameterSetDefinition, RequiredParams extends PossibleParameterKeys<Params>, > = WorkflowParameterReferences<Params, RequiredParams>; export type WorkflowStepOutputs< Params extends ParameterSetDefinition, RequiredParams extends PossibleParameterKeys<Params>, > = WorkflowParameterReferences<Params, RequiredParams>; type WorkflowParameterReferences< Params extends ParameterSetDefinition, Required extends PossibleParameterKeys<Params>, > = & { [name in Required[number]]: ParameterVariableType<Params[name]>; } & { [name in keyof Params]?: ParameterVariableType<Params[name]>; }; // Workflow Step inputs are different than workflow inputs/outputs or workflow step outputs. // They are purely the config values for the step, and not definitions that can be referenced // as variables like you can with workflow inputs and workflow step outputs export type WorkflowStepInputs< InputParameters extends ParameterSetDefinition, RequiredInputs extends PossibleParameterKeys<InputParameters>, > = & { // deno-lint-ignore no-explicit-any [k in RequiredInputs[number]]: any; } & { // deno-lint-ignore no-explicit-any [k in keyof InputParameters]?: any; }; ================================================ FILE: src/workflows/workflow-step.ts ================================================ import type { SlackManifest } from "../manifest/mod.ts"; import type { ManifestFunction, ManifestWorkflowStepSchema, } from "../manifest/manifest_schema.ts"; import type { ISlackFunctionDefinition } from "../functions/types.ts"; import { CreateUntypedObjectParameterVariable, ParameterVariable, } from "../parameters/mod.ts"; import type { ParameterSetDefinition, ParameterVariableType, PossibleParameterKeys, } from "../parameters/types.ts"; import type { WorkflowStepInputs, WorkflowStepOutputs } from "./types.ts"; const localFnPrefix = "#/functions/"; export type WorkflowStepDefinition = // deno-lint-ignore no-explicit-any | TypedWorkflowStepDefinition<any, any, any, any> | UntypedWorkflowStepDefinition; abstract class BaseWorkflowStepDefinition { protected stepId: string; protected functionReference: string; protected inputs: Record<string, unknown>; constructor( stepId: string, functionReference: string, inputs: Record<string, unknown>, ) { this.stepId = stepId; // ensures the function reference is a full path - local functions will only be passing in the function callback id this.functionReference = functionReference.includes("#/") ? functionReference : `${localFnPrefix}${functionReference}`; this.inputs = inputs; } templatizeInputs() { const templatizedInputs: ManifestWorkflowStepSchema["inputs"] = {} as ManifestWorkflowStepSchema["inputs"]; for (const [inputName, inputValue] of Object.entries(this.inputs)) { try { templatizedInputs[inputName] = JSON.parse(JSON.stringify(inputValue)); } catch { templatizedInputs[inputName] = undefined; } } return templatizedInputs; } export(): ManifestWorkflowStepSchema { return { id: this.stepId, function_id: this.functionReference, inputs: this.templatizeInputs(), }; } toJSON() { return this.export(); } registerFunction(_manifest: SlackManifest) { // default is a noop, only steps using a function definition will register themselves on the manifest } protected isLocalFunctionReference(): boolean { return this.functionReference.startsWith(localFnPrefix); } } export class TypedWorkflowStepDefinition< InputParameters extends ParameterSetDefinition, OutputParameters extends ParameterSetDefinition, RequiredInputs extends PossibleParameterKeys<InputParameters>, RequiredOutputs extends PossibleParameterKeys<OutputParameters>, > extends BaseWorkflowStepDefinition { public definition: ISlackFunctionDefinition< InputParameters, OutputParameters, RequiredInputs, RequiredOutputs >; public outputs: WorkflowStepOutputs< OutputParameters, RequiredOutputs >; constructor( stepId: string, slackFunction: ISlackFunctionDefinition< InputParameters, OutputParameters, RequiredInputs, RequiredOutputs >, inputs: WorkflowStepInputs<InputParameters, RequiredInputs>, ) { super(stepId, slackFunction.id, inputs); this.definition = slackFunction; this.outputs = {} as WorkflowStepOutputs< OutputParameters, RequiredOutputs >; // Setup step outputs for use in input template expressions for ( const [outputName, outputDefinition] of Object.entries( slackFunction?.definition?.output_parameters?.properties ?? {}, ) ) { // deno-lint-ignore ban-ts-comment //@ts-ignore this.outputs[ outputName as keyof OutputParameters ] = ParameterVariable( `steps.${this.stepId}`, outputName, outputDefinition, ) as ParameterVariableType< OutputParameters[typeof outputName] >; } } override registerFunction(manifest: SlackManifest) { if (this.isLocalFunctionReference()) { manifest.registerFunction(this.definition as ManifestFunction); } } } export class UntypedWorkflowStepDefinition extends BaseWorkflowStepDefinition { public outputs: ReturnType<typeof CreateUntypedObjectParameterVariable>; constructor( stepId: string, functionReference: string, // deno-lint-ignore no-explicit-any inputs: WorkflowStepInputs<any, any>, ) { super(stepId, functionReference, inputs); this.outputs = CreateUntypedObjectParameterVariable( `steps.${stepId}`, "", ); } } ================================================ FILE: tests/integration/functions/runtime_context/array_parameters_test.ts ================================================ import { assert, assertEquals, type IsAny } from "../../../../src/dev_deps.ts"; import type { CanBeUndefined, CannotBeUndefined, } from "../../../../src/test_utils.ts"; import { DefineFunction, DefineType, Schema } from "../../../../src/mod.ts"; import { DefineProperty } from "../../../../src/parameters/define_property.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; import { assertEqualsTypedValues } from "../../../../src/test_utils.ts"; /** * Custom function handler tests, exercising Array inputs/outputs */ Deno.test("Custom function using an input of Typed Arrays of Custom Types of DefineProperty-wrapped typed objects should honor required and optional properties and allow for referencing additional properties", () => { const obj = DefineProperty({ type: Schema.types.object, properties: { aString: { type: Schema.types.string, }, anOptionalString: { type: Schema.types.string, }, }, required: ["aString"], additionalProperties: true, }); const customType = DefineType({ name: "customType", ...obj, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { arr: { type: Schema.types.array, items: { type: customType, }, }, }, required: ["arr"], }, }); const sharedInputs = { arr: [{ aString: "hi" }, { aString: "hello", anOptionalString: "goodbye" }], }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { arr } = inputs; const first = arr[0]; const second = arr[1]; assert<CannotBeUndefined<typeof first.aString>>(true); assert<CanBeUndefined<typeof first.anOptionalString>>(true); assert<CannotBeUndefined<typeof second.aString>>(true); assert<CanBeUndefined<typeof second.anOptionalString>>(true); assert<IsAny<typeof first.somethingRandom>>(true); assert<IsAny<typeof second.andNowForSomethingCompletelyDifferent>>(true); assertEqualsTypedValues( first.aString, sharedInputs.arr[0].aString, ); assertEqualsTypedValues( first.anOptionalString, undefined, ); assertEqualsTypedValues( second.aString, sharedInputs.arr[1].aString, ); assertEqualsTypedValues( second.anOptionalString, sharedInputs.arr[1].anOptionalString, ); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); handler(createContext({ inputs: sharedInputs })); }); Deno.test("Custom function using an input of Typed Arrays of Custom Types of DefineProperty-wrapped typed objects should honor additionalProperties=false", () => { const obj = DefineProperty({ type: Schema.types.object, properties: { aString: { type: Schema.types.string, }, anOptionalString: { type: Schema.types.string, }, }, required: ["aString"], additionalProperties: false, }); const customType = DefineType({ name: "customType", ...obj, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { arr: { type: Schema.types.array, items: { type: customType, }, }, }, required: ["arr"], }, }); const sharedInputs = { arr: [{ aString: "hi" }, { aString: "hello", anOptionalString: "goodbye" }], }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { arr } = inputs; const first = arr[0]; const second = arr[1]; assert<CannotBeUndefined<typeof first.aString>>(true); assert<CanBeUndefined<typeof first.anOptionalString>>(true); assert<CannotBeUndefined<typeof second.aString>>(true); assert<CanBeUndefined<typeof second.anOptionalString>>(true); assertEqualsTypedValues( first.aString, sharedInputs.arr[0].aString, ); assertEqualsTypedValues( first.anOptionalString, undefined, ); assertEqualsTypedValues( second.aString, sharedInputs.arr[1].aString, ); assertEqualsTypedValues( second.anOptionalString, sharedInputs.arr[1].anOptionalString, ); // @ts-expect-error batman cannot exist assertEquals(first.batman, undefined); // @ts-expect-error robin cannot exist assertEquals(second.robin, undefined); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); handler(createContext({ inputs: sharedInputs })); }); Deno.test("Custom function using an input of Typed Arrays of DefineProperty-wrapped typed objects should honor required and optional properties and allow for referencing additional properties", () => { const obj = DefineProperty({ type: Schema.types.object, properties: { aString: { type: Schema.types.string, }, anOptionalString: { type: Schema.types.string, }, }, required: ["aString"], additionalProperties: true, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { arr: { type: Schema.types.array, items: obj, }, }, required: ["arr"], }, }); const sharedInputs = { arr: [{ aString: "hi" }, { aString: "hello", anOptionalString: "goodbye" }], }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { arr } = inputs; const first = arr[0]; const second = arr[1]; assert<CannotBeUndefined<typeof first.aString>>(true); assert<CanBeUndefined<typeof first.anOptionalString>>(true); assert<CannotBeUndefined<typeof second.aString>>(true); assert<CanBeUndefined<typeof second.anOptionalString>>(true); assert<IsAny<typeof first.somethingRandom>>(true); assert<IsAny<typeof second.andNowForSomethingCompletelyDifferent>>(true); assertEqualsTypedValues( first.aString, sharedInputs.arr[0].aString, ); assertEqualsTypedValues( first.anOptionalString, undefined, ); assertEqualsTypedValues( second.aString, sharedInputs.arr[1].aString, ); assertEqualsTypedValues( second.anOptionalString, sharedInputs.arr[1].anOptionalString, ); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); handler(createContext({ inputs: sharedInputs })); }); Deno.test("Custom function using an input of Typed Arrays of DefineProperty-wrapped typed objects should honor additionalProperties=false", () => { const obj = DefineProperty({ type: Schema.types.object, properties: { aString: { type: Schema.types.string, }, anOptionalString: { type: Schema.types.string, }, }, required: ["aString"], additionalProperties: false, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { arr: { type: Schema.types.array, items: obj, }, }, required: ["arr"], }, }); const sharedInputs = { arr: [{ aString: "hi" }, { aString: "hello", anOptionalString: "goodbye" }], }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { arr } = inputs; const first = arr[0]; const second = arr[1]; assert<CannotBeUndefined<typeof first.aString>>(true); assert<CanBeUndefined<typeof first.anOptionalString>>(true); assert<CannotBeUndefined<typeof second.aString>>(true); assert<CanBeUndefined<typeof second.anOptionalString>>(true); assertEqualsTypedValues( first.aString, sharedInputs.arr[0].aString, ); assertEqualsTypedValues( first.anOptionalString, undefined, ); assertEqualsTypedValues( second.aString, sharedInputs.arr[1].aString, ); assertEqualsTypedValues( second.anOptionalString, sharedInputs.arr[1].anOptionalString, ); // @ts-expect-error batman cannot exist assertEquals(first.batman, undefined); // @ts-expect-error robin cannot exist assertEquals(second.robin, undefined); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); handler(createContext({ inputs: sharedInputs })); }); Deno.test("Custom function using untyped Arrays and typed arrays of strings", () => { const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { anUntypedArray: { type: Schema.types.array, }, aTypedArray: { type: Schema.types.array, items: { type: "string", }, }, }, required: ["aTypedArray", "anUntypedArray"], }, output_parameters: { properties: { anUntypedArray: { type: Schema.types.array, }, aTypedArray: { type: Schema.types.array, items: { type: "string", }, }, }, required: ["aTypedArray", "anUntypedArray"], }, }); const sharedInputs = { aTypedArray: ["hello"], anUntypedArray: [1, "goodbye"], }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { aTypedArray, anUntypedArray } = inputs; assert<IsAny<typeof anUntypedArray[0]>>(true); assert<IsAny<typeof aTypedArray[0]>>(false); assert<CannotBeUndefined<typeof aTypedArray>>(true); assert<CannotBeUndefined<typeof anUntypedArray>>(true); // These tests are a little weird, could technically be undefined if these arrays are empty assert<CannotBeUndefined<typeof aTypedArray[0]>>(true); assert<CannotBeUndefined<typeof aTypedArray[0]>>(true); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = handler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); }); /** * TODO: the next two particular tests lead to the array items being typed as `any` Deno.test("Custom function using Typed Arrays of Custom Types of unwrapped typed objects should honor required and optional properties", () => { const obj = { type: Schema.types.object, properties: { aString: { type: Schema.types.string, }, anOptionalString: { type: Schema.types.string, }, }, required: ["aString"], additionalProperties: true, }; const customType = DefineType({ name: "customType", ...obj, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { arr: { type: Schema.types.array, items: { type: customType, }, }, }, required: ["arr"], }, }); const sharedInputs = { arr: [{ aString: "hi" }, { aString: "hello", anOptionalString: "goodbye" }], }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { arr } = inputs; const first = arr[0]; const second = arr[1]; assert<CannotBeUndefined<typeof first.aString>>(true); assert<CanBeUndefined<typeof first.anOptionalString>>(true); assert<CannotBeUndefined<typeof second.aString>>(true); assert<CanBeUndefined<typeof second.anOptionalString>>(true); assertEqualsTypedValues( first.aString, sharedInputs.arr[0].aString, ); assertEqualsTypedValues( first.anOptionalString, undefined, ); assertEqualsTypedValues( second.aString, sharedInputs.arr[1].aString, ); assertEqualsTypedValues( second.anOptionalString, sharedInputs.arr[1].anOptionalString, ); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); handler(createContext({ inputs: sharedInputs })); }); Deno.test("Custom function using Typed Arrays of unwrapped typed objects should honor required and optional properties", () => { const obj = { type: Schema.types.object, properties: { aString: { type: Schema.types.string, }, anOptionalString: { type: Schema.types.string, }, }, required: ["aString"], additionalProperties: true, }; const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { arr: { type: Schema.types.array, items: obj, }, }, required: ["arr"], }, }); const sharedInputs = { arr: [{ aString: "hi" }, { aString: "hello", anOptionalString: "goodbye" }], }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { arr } = inputs; const first = arr[0]; const second = arr[1]; assert<CannotBeUndefined<typeof first.aString>>(true); assert<CanBeUndefined<typeof first.anOptionalString>>(true); assert<CannotBeUndefined<typeof second.aString>>(true); assert<CanBeUndefined<typeof second.anOptionalString>>(true); assertEqualsTypedValues( first.aString, sharedInputs.arr[0].aString, ); assertEqualsTypedValues( first.anOptionalString, undefined, ); assertEqualsTypedValues( second.aString, sharedInputs.arr[1].aString, ); assertEqualsTypedValues( second.anOptionalString, sharedInputs.arr[1].anOptionalString, ); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); handler(createContext({ inputs: sharedInputs })); }); */ ================================================ FILE: tests/integration/functions/runtime_context/custom_type_parameters_test.ts ================================================ import { assert, type IsAny, type IsExact } from "../../../../src/dev_deps.ts"; import type { CanBe, CanBeUndefined, CannotBeUndefined, } from "../../../../src/test_utils.ts"; import { DefineFunction, DefineType, Schema } from "../../../../src/mod.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; import { assertEqualsTypedValues } from "../../../../src/test_utils.ts"; import { InternalSlackTypes } from "../../../../src/schema/slack/types/custom/mod.ts"; /** * Custom function handler tests, exercising Custom Type inputs/outputs, including Slack/internal custom Slack types */ Deno.test("Custom function using Slack's FormInput internal Custom Type input should provide correct typedobject typing in a function handler context", () => { const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { formInput: { type: InternalSlackTypes.form_input_object, }, }, required: ["formInput"], }, output_parameters: { properties: { formInput: { type: InternalSlackTypes.form_input_object, }, }, required: ["formInput"], }, }); const sharedInputs = { formInput: { required: [], elements: [], }, }; const validHandler: EnrichedSlackFunctionHandler< typeof TestFunction.definition > = ( { inputs }, ) => { const { formInput } = inputs; assert<CanBeUndefined<typeof formInput.required>>( true, ); assert<CanBe<typeof formInput.required, string[]>>(true); assert<CannotBeUndefined<typeof formInput.elements>>( true, ); // deno-lint-ignore no-explicit-any assert<CanBe<typeof formInput.elements, any[]>>(true); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); validHandler(createContext({ inputs: sharedInputs })); }); Deno.test("Custom function using Slack's message-context Custom Type input should provide correct typedobject typing in a function handler context", () => { const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { msgContext: { type: Schema.slack.types.message_context, }, }, required: ["msgContext"], }, output_parameters: { properties: { msgContext: { type: Schema.slack.types.message_context, }, }, required: ["msgContext"], }, }); const sharedInputs = { msgContext: { message_ts: "1234.567", channel_id: "C12345", }, }; const validHandler: EnrichedSlackFunctionHandler< typeof TestFunction.definition > = ( { inputs }, ) => { const { msgContext } = inputs; // channel_id sub-property assert<CanBeUndefined<typeof msgContext.channel_id>>( true, ); assert<CanBe<typeof msgContext.channel_id, string>>(true); // user_id sub-property assert<CanBeUndefined<typeof msgContext.user_id>>( true, ); assert<CanBe<typeof msgContext.user_id, string>>(true); // message_ts sub-property assert<CannotBeUndefined<typeof msgContext.message_ts>>( true, ); assert<IsExact<typeof msgContext.message_ts, string>>(true); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); validHandler(createContext({ inputs: sharedInputs })); }); Deno.test("Custom function using a Custom Type input for an unwrapped typedobject with mixed required/optional properties should provide correct typedobject typing in a function handler context", () => { const myType = DefineType({ name: "custom", type: Schema.types.object, properties: { required_property: { type: "string" }, optional_property: { type: "string" }, }, required: ["required_property"], }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, output_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, }); const sharedInputs = { custom_type: { required_property: "i am a necessity", }, }; const validHandler: EnrichedSlackFunctionHandler< typeof TestFunction.definition > = ( { inputs }, ) => { const { custom_type } = inputs; assert<IsAny<typeof custom_type>>(false); assert<IsExact<typeof custom_type.required_property, string>>(true); assert<CanBeUndefined<typeof custom_type.optional_property>>(true); assert<CanBe<typeof custom_type.optional_property, string>>(true); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = validHandler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); }); Deno.test("Custom function using a Custom Type input for an unwrapped typedobject with mixed required/optional properties should complain if required output not provided", () => { const myType = DefineType({ name: "custom", type: Schema.types.object, properties: { required_property: { type: "string" }, optional_property: { type: "string" }, }, required: ["required_property"], }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, output_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, }); // @ts-expect-error Type error if required property isn't returned const _invalidHandler: EnrichedSlackFunctionHandler< typeof TestFunction.definition > = () => { return { outputs: { custom_type: { optional_property: "im useless", }, }, }; }; }); Deno.test("Custom function using a Custom Type input for an unwrapped typedobject with additionalProperties=undefined should allow for referencing additional properties", () => { const myType = DefineType({ name: "custom", type: Schema.types.object, properties: { required_property: { type: "string" }, optional_property: { type: "string" }, }, required: ["required_property"], }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, output_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, }); const sharedInputs = { custom_type: { required_property: "i am a necessity", }, }; const validHandler: EnrichedSlackFunctionHandler< typeof TestFunction.definition > = ( { inputs }, ) => { const { custom_type } = inputs; assert<IsAny<typeof custom_type>>(false); assert<IsAny<typeof custom_type.something>>(true); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = validHandler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); }); Deno.test("Custom function using a Custom Type input for an unwrapped typedobject with additionalProperties=true should allow for referencing additional properties", () => { const myType = DefineType({ name: "custom", type: Schema.types.object, properties: { required_property: { type: "string" }, optional_property: { type: "string" }, }, required: ["required_property"], additionalProperties: true, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, output_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, }); const sharedInputs = { custom_type: { required_property: "i am a necessity", }, }; const validHandler: EnrichedSlackFunctionHandler< typeof TestFunction.definition > = ( { inputs }, ) => { const { custom_type } = inputs; assert<IsAny<typeof custom_type>>(false); assert<IsAny<typeof custom_type.something>>(true); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = validHandler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); }); Deno.test("Custom function using a Custom Type input for an unwrapped typedobject with additionalProperties=false should prevent referencing additional properties", () => { const myType = DefineType({ name: "custom", type: Schema.types.object, properties: { required_property: { type: "string" }, optional_property: { type: "string" }, }, required: ["required_property"], additionalProperties: false, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, output_parameters: { properties: { custom_type: { type: myType, }, }, required: ["custom_type"], }, }); const sharedInputs = { custom_type: { required_property: "i am a necessity", }, }; const validHandler: EnrichedSlackFunctionHandler< typeof TestFunction.definition > = ( { inputs }, ) => { const { custom_type } = inputs; assert<IsAny<typeof custom_type>>(false); // @ts-expect-error somethingElse cant exist custom_type.somethingElse; return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = validHandler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); }); ================================================ FILE: tests/integration/functions/runtime_context/empty_undefined_parameters_test.ts ================================================ import { DefineFunction } from "../../../../src/mod.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; import { assertEqualsTypedValues } from "../../../../src/test_utils.ts"; /** * Custom function handler tests exercising empty/undefined/ inputs/outputs */ Deno.test("Custom function with no inputs or outputs", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: {}, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = handler(createContext({ inputs: {} })); assertEqualsTypedValues(result.outputs, {}); }); Deno.test("Custom function with undefined inputs and outputs", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: undefined, output_parameters: undefined, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: {}, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = handler(createContext({ inputs: {} })); assertEqualsTypedValues(result.outputs, {}); }); Deno.test("Custom function with empty inputs and outputs", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: {}, required: [] }, output_parameters: { properties: {}, required: [] }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: {}, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = handler(createContext({ inputs: {} })); assertEqualsTypedValues(result.outputs, {}); }); ================================================ FILE: tests/integration/functions/runtime_context/incomplete_error_status_test.ts ================================================ import { DefineFunction } from "../../../../src/mod.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; import { assertEqualsTypedValues } from "../../../../src/test_utils.ts"; /** * Custom function handler tests exercising error or incomplete function return values */ Deno.test("Custom function that returns completed=false", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { example: { type: "boolean", }, }, required: ["example"], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { completed: false, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = handler(createContext({ inputs: {} })); assertEqualsTypedValues(result.completed, false); }); Deno.test("Custom function that returns error", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { example: { type: "string", }, }, required: ["example"], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { error: "error", }; }; const { createContext } = SlackFunctionTester(TestFn); const result = handler(createContext({ inputs: {} })); assertEqualsTypedValues(result.error, "error"); }); ================================================ FILE: tests/integration/functions/runtime_context/input_parameter_optionality_test.ts ================================================ import { assert } from "../../../../src/dev_deps.ts"; import type { CanBe, CanBeUndefined } from "../../../../src/test_utils.ts"; import { DefineFunction, Schema } from "../../../../src/mod.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; /** * Custom function handler tests exercising optionality of inputs for primitive types */ Deno.test("Custom function with an optional string input provide the string/undefined input in a function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { in: { type: Schema.types.string, }, }, required: [], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<CanBe<typeof inputs.in, string>>(true); assert<CanBeUndefined<typeof inputs.in>>(true); return { outputs: { out: inputs.in || "default", }, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = {}; handler(createContext({ inputs })); }); Deno.test("Custom function with an optional boolean input provide the boolean/undefined input in a function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { in: { type: Schema.types.boolean, }, }, required: [], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<CanBe<typeof inputs.in, boolean>>(true); assert<CanBeUndefined<typeof inputs.in>>(true); return { outputs: { out: inputs.in || "default", }, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = {}; handler(createContext({ inputs })); }); Deno.test("Custom function with an optional integer input provide the number/undefined input in a function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { in: { type: Schema.types.integer, }, }, required: [], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<CanBe<typeof inputs.in, number>>(true); assert<CanBeUndefined<typeof inputs.in>>(true); return { outputs: { out: inputs.in || "default", }, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = {}; handler(createContext({ inputs })); }); Deno.test("Custom function with an optional number input provide the number/undefined input in a function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { in: { type: Schema.types.number, }, }, required: [], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<CanBe<typeof inputs.in, number>>(true); assert<CanBeUndefined<typeof inputs.in>>(true); return { outputs: { out: inputs.in || "default", }, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = {}; handler(createContext({ inputs })); }); ================================================ FILE: tests/integration/functions/runtime_context/input_parameters_test.ts ================================================ import { assert, type IsExact } from "../../../../src/dev_deps.ts"; import { DefineFunction, Schema } from "../../../../src/mod.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; import { assertEqualsTypedValues } from "../../../../src/test_utils.ts"; /** * Custom function handler tests exercising inputs of various primitive types */ Deno.test("Custom function with a string input should provide a string input in the function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { in: { type: Schema.types.string, }, }, required: ["in"], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<IsExact<typeof inputs.in, string>>(true); return { outputs: {}, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = { in: "test" }; const result = handler(createContext({ inputs })); assertEqualsTypedValues(result.outputs, {}); }); Deno.test("Custom function with a boolean input should provide a boolean input in the function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { in: { type: Schema.types.boolean, }, }, required: ["in"], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<IsExact<typeof inputs.in, boolean>>(true); return { outputs: {}, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = { in: false }; const result = handler(createContext({ inputs })); assertEqualsTypedValues(result.outputs, {}); }); Deno.test("Custom function with an integer input should provide a number input in the function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { in: { type: Schema.types.integer, }, }, required: ["in"], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<IsExact<typeof inputs.in, number>>(true); return { outputs: {}, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = { in: 21 }; const result = handler(createContext({ inputs })); assertEqualsTypedValues(result.outputs, {}); }); Deno.test("Custom function with a number input should provide a number input in the function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { in: { type: Schema.types.number, }, }, required: ["in"], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<IsExact<typeof inputs.in, number>>(true); return { outputs: {}, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = { in: 21.5 }; const result = handler(createContext({ inputs })); assertEqualsTypedValues(result.outputs, {}); }); ================================================ FILE: tests/integration/functions/runtime_context/output_parameter_optionality_test.ts ================================================ import { assert } from "../../../../src/dev_deps.ts"; import type { CanBe, CanBeUndefined } from "../../../../src/test_utils.ts"; import { DefineFunction, Schema } from "../../../../src/mod.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; /** * Custom function handler tests exercising optionality of outputs for primitive types */ Deno.test("Custom function with an optional string output returns an output that can be either undefined or a string", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { out: { type: Schema.types.string, }, }, required: [], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: { out: Math.random() > 0.5 ? "default" : undefined, }, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = {}; const result = handler(createContext({ inputs })); const output = result.outputs?.out; assert<CanBeUndefined<typeof output>>(true); assert<CanBe<typeof output, string>>(true); }); Deno.test("Custom function with an optional boolean output returns an output that can be either undefined or a boolean", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { out: { type: Schema.types.boolean, }, }, required: [], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: { out: Math.random() > 0.5 ? true : undefined, }, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = {}; const result = handler(createContext({ inputs })); const output = result.outputs?.out; assert<CanBeUndefined<typeof output>>(true); assert<CanBe<typeof output, boolean>>(true); }); Deno.test("Custom function with an optional integer output returns an output that can be either undefined or a number", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { out: { type: Schema.types.integer, }, }, required: [], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: { out: Math.random() > 0.5 ? 1337 : undefined, }, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = {}; const result = handler(createContext({ inputs })); const output = result.outputs?.out; assert<CanBeUndefined<typeof output>>(true); assert<CanBe<typeof output, number>>(true); }); Deno.test("Custom function with an optional number output returns an output that can be either undefined or a number", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { out: { type: Schema.types.number, }, }, required: [], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: { out: Math.random() > 0.5 ? 9.5 : undefined, }, }; }; const { createContext } = SlackFunctionTester(TestFn); const inputs = {}; const result = handler(createContext({ inputs })); const output = result.outputs?.out; assert<CanBeUndefined<typeof output>>(true); assert<CanBe<typeof output, number>>(true); }); ================================================ FILE: tests/integration/functions/runtime_context/output_parameters_test.ts ================================================ import { DefineFunction, Schema } from "../../../../src/mod.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; import { assertEqualsTypedValues } from "../../../../src/test_utils.ts"; /** * Custom function handler tests exercising outputs of various primitive types */ Deno.test("Custom function with only a string output defined must return a string output", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { out: { type: Schema.types.string, }, }, required: ["out"], }, }); const validHandler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: { out: "test", }, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = validHandler(createContext({ inputs: {} })); assertEqualsTypedValues(result.outputs?.out, "test"); // @ts-expect-error `out` output property must be a string const _invalidHandler: EnrichedSlackFunctionHandler< typeof TestFn.definition > = () => { return { outputs: { out: 1, }, }; }; }); Deno.test("Custom function with only a boolean output defined must return a boolean output", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { out: { type: Schema.types.boolean, }, }, required: ["out"], }, }); const validHandler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: { out: true, }, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = validHandler(createContext({ inputs: {} })); assertEqualsTypedValues(result.outputs?.out, true); // @ts-expect-error `out` output property must be a boolean const _invalidHandler: EnrichedSlackFunctionHandler< typeof TestFn.definition > = () => { return { outputs: { out: "haha", }, }; }; }); Deno.test("Custom function with only an integer output defined must return a number output", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { out: { type: Schema.types.integer, }, }, required: ["out"], }, }); const validHandler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: { out: 14, }, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = validHandler(createContext({ inputs: {} })); assertEqualsTypedValues(result.outputs?.out, 14); // @ts-expect-error `out` output property must be an integer const _invalidHandler: EnrichedSlackFunctionHandler< typeof TestFn.definition > = () => { return { outputs: { out: "haha", }, }; }; }); Deno.test("Custom function with only a number output defined must return a number output", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { out: { type: Schema.types.number, }, }, required: ["out"], }, }); const validHandler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = () => { return { outputs: { out: 14.2, }, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = validHandler(createContext({ inputs: {} })); assertEqualsTypedValues(result.outputs?.out, 14.2); // @ts-expect-error `out` output property must be an integer const _invalidHandler: EnrichedSlackFunctionHandler< typeof TestFn.definition > = () => { return { outputs: { out: "haha", }, }; }; }); ================================================ FILE: tests/integration/functions/runtime_context/typed_object_property_test.ts ================================================ import { assert, assertEquals, assertExists, type IsAny, type IsExact, } from "../../../../src/dev_deps.ts"; import type { CanBe, CanBeUndefined, CannotBeUndefined, } from "../../../../src/test_utils.ts"; import { DefineFunction, Schema } from "../../../../src/mod.ts"; import { DefineProperty } from "../../../../src/parameters/define_property.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; import { assertEqualsTypedValues } from "../../../../src/test_utils.ts"; /** * Custom function handler tests, exercising Typed Object inputs/outputs */ Deno.test("Custom function with a required DefineProperty-wrapped typedobject input with a required string property and a required DefineProperty-wrapped typedobject output should provide correct typing in a function handler context and complain if required output not provided", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { anObject: DefineProperty({ type: Schema.types.object, properties: { in: { type: "string" } }, required: ["in"], }), }, required: ["anObject"], }, output_parameters: { properties: { anObject: DefineProperty({ type: Schema.types.object, properties: { out: { type: "string" } }, required: ["out"], }), }, required: ["anObject"], }, }); const validHandler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<CannotBeUndefined<typeof inputs.anObject.in>>(true); assert<IsExact<typeof inputs.anObject.in, string>>(true); return { outputs: { anObject: { out: inputs.anObject.in, }, }, }; }; // @ts-expect-error Type error if required property isn't returned const _invalidHandler: EnrichedSlackFunctionHandler< typeof TestFn.definition > = (_arg) => { return { outputs: { anObject: {}, }, }; }; const { createContext } = SlackFunctionTester(TestFn); const result = validHandler( createContext({ inputs: { anObject: { in: "test" } } }), ); assertEqualsTypedValues(result.outputs?.anObject.out, "test"); }); Deno.test("Custom function with a required DefineProperty-wrapped typedobject input with an optional string property should provide correct typing in a function handler context", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", input_parameters: { properties: { anObject: DefineProperty({ type: Schema.types.object, properties: { in: { type: "string" } }, required: [], }), }, required: ["anObject"], }, }); const handler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( { inputs }, ) => { assert<CanBeUndefined<typeof inputs.anObject.in>>(true); assert<CanBe<typeof inputs.anObject.in, string>>(true); return { outputs: {}, }; }; const { createContext } = SlackFunctionTester(TestFn); const _result = handler( createContext({ inputs: { anObject: { in: "test" } } }), ); }); Deno.test("Custom function with a required output DefineProperty-wrapped typedobject with mixed required/optional property requirements should complain if required object properties are not returned by function", () => { const TestFn = DefineFunction({ callback_id: "test", title: "test fn", source_file: "test.ts", output_parameters: { properties: { anObject: DefineProperty({ type: Schema.types.object, properties: { req: { type: "string" }, opt: { type: "string" } }, required: ["req"], }), }, required: ["anObject"], }, }); // Ensure no error raised if object required property provided const _reqHandler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( _arg, ) => { return { outputs: { anObject: { req: "i'm here" }, }, }; }; //@ts-expect-error anObject.req is a required property and must be defined const _optHandler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( _arg, ) => { return { outputs: { anObject: { opt: "i'm here" }, }, }; }; // Ensure no error raised if object required property provided const _mixedHandler: EnrichedSlackFunctionHandler<typeof TestFn.definition> = ( _arg, ) => { return { outputs: { anObject: { req: "i'm here", opt: "i'm here", }, }, }; }; }); Deno.test("Custom function with an input of DefineProperty-wrapped Typed Object with additional properties allows referencing into additional properties in a function handler context", () => { const obj = DefineProperty({ type: Schema.types.object, properties: { aString: { type: Schema.types.string }, }, required: [], additionalProperties: true, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { addlPropertiesObj: obj, }, required: ["addlPropertiesObj"], }, output_parameters: { properties: { addlPropertiesObj: obj, }, required: ["addlPropertiesObj"], }, }); const sharedInputs = { addlPropertiesObj: { aString: "hi", somethingElse: "ello" }, }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { addlPropertiesObj } = inputs; assertEqualsTypedValues( addlPropertiesObj, sharedInputs.addlPropertiesObj, ); assertEqualsTypedValues( addlPropertiesObj.aString, sharedInputs.addlPropertiesObj.aString, ); assert<IsAny<typeof addlPropertiesObj.somethingElse>>(true); assert<IsAny<typeof addlPropertiesObj.anythingElse>>(true); assertEquals(addlPropertiesObj.somethingElse, "ello"); assertEquals(addlPropertiesObj.anythingElse, undefined); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = handler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); assertExists(result.outputs?.addlPropertiesObj); assertExists(result.outputs?.addlPropertiesObj.aString); assertEquals(result.outputs?.addlPropertiesObj.somethingElse, "ello"); assertEquals(result.outputs?.addlPropertiesObj.anythingElse, undefined); if (result.outputs) { assert<IsAny<typeof result.outputs.addlPropertiesObj.anythingElse>>(true); } result.outputs.addlPropertiesObj.anothaOne; }); Deno.test("Custom function with an input of DefineProperty-wrapped Typed Object without additional properties prevents referencing into additional properties in a function handler context", () => { const obj = DefineProperty({ type: Schema.types.object, properties: { aString: { type: Schema.types.string }, }, required: [], additionalProperties: false, }); const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { noAddlPropertiesObj: obj, }, required: ["noAddlPropertiesObj"], }, output_parameters: { properties: { noAddlPropertiesObj: obj, }, required: ["noAddlPropertiesObj"], }, }); const sharedInputs = { noAddlPropertiesObj: { aString: "hi" }, }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { noAddlPropertiesObj } = inputs; assertEqualsTypedValues( noAddlPropertiesObj, sharedInputs.noAddlPropertiesObj, ); assertEqualsTypedValues( noAddlPropertiesObj.aString, sharedInputs.noAddlPropertiesObj.aString, ); // @ts-expect-error anythingElse cant exist assertEquals(noAddlPropertiesObj.anythingElse, undefined); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = handler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); assertExists(result.outputs?.noAddlPropertiesObj); assertExists(result.outputs?.noAddlPropertiesObj.aString); // @ts-expect-error anythingElse cant exist assertEquals(result.outputs?.noAddlPropertiesObj.anythingElse, undefined); }); Deno.test("Custom function using an unwrapped Typed Object input with additionalProperties=undefined should allow referencing additional properties in a function handler context", () => { const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { addlPropertiesObj: { type: Schema.types.object, properties: { aString: { type: Schema.types.string }, }, required: [], }, }, required: ["addlPropertiesObj"], }, output_parameters: { properties: { addlPropertiesObj: { type: Schema.types.object, properties: { aString: { type: Schema.types.string }, }, required: [], }, }, required: ["addlPropertiesObj"], }, }); const sharedInputs = { addlPropertiesObj: { aString: "hi" }, }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { addlPropertiesObj } = inputs; assertEqualsTypedValues( addlPropertiesObj, sharedInputs.addlPropertiesObj, ); assertEqualsTypedValues( addlPropertiesObj.aString, sharedInputs.addlPropertiesObj.aString, ); assert<IsAny<typeof addlPropertiesObj.anythingElse>>(true); assertEquals(addlPropertiesObj.anythingElse, undefined); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = handler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); assertExists(result.outputs?.addlPropertiesObj); assertExists(result.outputs?.addlPropertiesObj.aString); assertEquals(result.outputs?.addlPropertiesObj.anythingElse, undefined); }); Deno.test("Custom function using an unwrapped Typed Object input with additionalProperties=false should prevent referencing additional properties in a function handler context", () => { const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { noAddlPropertiesObj: { type: Schema.types.object, properties: { aString: { type: Schema.types.string }, }, required: [], additionalProperties: false, }, }, required: ["noAddlPropertiesObj"], }, output_parameters: { properties: { noAddlPropertiesObj: { type: Schema.types.object, properties: { aString: { type: Schema.types.string }, }, required: [], additionalProperties: false, }, }, required: ["noAddlPropertiesObj"], }, }); const sharedInputs = { noAddlPropertiesObj: { aString: "hi" }, }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { noAddlPropertiesObj } = inputs; assertEqualsTypedValues( noAddlPropertiesObj, sharedInputs.noAddlPropertiesObj, ); assertEqualsTypedValues( noAddlPropertiesObj.aString, sharedInputs.noAddlPropertiesObj.aString, ); // @ts-expect-error anythingElse cant exist assertEquals(noAddlPropertiesObj.anythingElse, undefined); return { outputs: inputs, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = handler(createContext({ inputs: sharedInputs })); assertEqualsTypedValues(sharedInputs, result.outputs); assertExists(result.outputs?.noAddlPropertiesObj); assertExists(result.outputs?.noAddlPropertiesObj.aString); // @ts-expect-error anythingElse cant exist assertEquals(result.outputs?.noAddlPropertiesObj.anythingElse, undefined); }); ================================================ FILE: tests/integration/functions/runtime_context/untyped_object_property_test.ts ================================================ import { assert, assertExists, type IsAny } from "../../../../src/dev_deps.ts"; import { DefineFunction, Schema } from "../../../../src/mod.ts"; import type { EnrichedSlackFunctionHandler, } from "../../../../src/functions/types.ts"; import { SlackFunctionTester } from "../../../../src/functions/tester/mod.ts"; /** * Custom function handler tests, exercising Untyped Object inputs/outputs */ Deno.test("Custom function using untyped Objects should allow for referencing any property in a function handler context", () => { const TestFunction = DefineFunction({ callback_id: "my_callback_id", source_file: "test", title: "Test", input_parameters: { properties: { untypedObj: { type: Schema.types.object, }, }, required: ["untypedObj"], }, output_parameters: { properties: { untypedObj: { type: Schema.types.object, }, }, required: ["untypedObj"], }, }); const sharedInputs = { untypedObj: { aString: "hi" }, }; const handler: EnrichedSlackFunctionHandler<typeof TestFunction.definition> = ( { inputs }, ) => { const { untypedObj } = inputs; assert<IsAny<typeof untypedObj>>(true); assert<IsAny<typeof untypedObj.aString>>(true); return { outputs: { untypedObj: { literallyAnything: "ok" }, }, }; }; const { createContext } = SlackFunctionTester(TestFunction); const result = handler(createContext({ inputs: sharedInputs })); assertExists(result.outputs?.untypedObj); if (result.outputs?.untypedObj) { assert<IsAny<typeof result.outputs.untypedObj>>(true); } }); ================================================ FILE: tests/integration/parameters/parameter_variable_test.ts ================================================ import { DefineProperty } from "../../../src/parameters/define_property.ts"; import { ParameterVariable } from "../../../src/parameters/mod.ts"; import type { SingleParameterVariable } from "../../../src/parameters/types.ts"; import { DefineType } from "../../../src/types/mod.ts"; import SchemaTypes from "../../../src/schema/schema_types.ts"; import { assert, assertStrictEquals, type IsAny, type IsExact, } from "../../../src/dev_deps.ts"; import type { CannotBeUndefined } from "../../../src/test_utils.ts"; /** * Typed Object required/optional property definitions should never yield undefined ParameterVariable properties */ Deno.test("ParameterVariable DefineProperty-wrapped typed object with all optional properties should never yield object with potentially undefined properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: [], }); const param = ParameterVariable("", "incident", obj); assert<CannotBeUndefined<typeof param.id>>(true); assert<CannotBeUndefined<typeof param.name>>(true); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with all required properties should never yield object with potentially undefined properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id", "name"], }); const param = ParameterVariable("", "incident", obj); assert<CannotBeUndefined<typeof param.id>>(true); assert<CannotBeUndefined<typeof param.name>>(true); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with mix of optional and required properties should never yield object with potentially undefined properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id"], }); const param = ParameterVariable("", "incident", obj); assert<CannotBeUndefined<typeof param.id>>(true); assert<CannotBeUndefined<typeof param.name>>(true); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); }); Deno.test("ParameterVariable using Custom Type with DefineProperty-wrapped typed object with optional property should never yield object with potentially undefined property", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: [], }); const customType = DefineType({ name: "customType", ...obj, }); const param = ParameterVariable("", "myCustomType", { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, "{{myCustomType}}"); assertStrictEquals(`${param.aString}`, "{{myCustomType.aString}}"); }); Deno.test("ParameterVariable using Custom Type with DefineProperty-wrapped typed object with required property should never yield object with potentially undefined property", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: ["aString"], }); const customType = DefineType({ name: "customType", ...obj, }); const param = ParameterVariable("", "myCustomType", { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, "{{myCustomType}}"); assertStrictEquals(`${param.aString}`, "{{myCustomType.aString}}"); }); /** * Typed Object additionalProperties controls whether ParameterVariable allows access to additional properties */ Deno.test("ParameterVariable DefineProperty-wrapped typed object with all optional properties and undefined additionalProperties allows access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: [], }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with all required properties and undefined additionalProperties allows access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id", "name"], }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with mix of required and optional properties and undefined additionalProperties allows access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id"], }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with all optional properties and additionalProperties=true allows access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: [], additionalProperties: true, }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with all required properties and additionalProperties=true allows access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id", "name"], additionalProperties: true, }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with mix of required and optional properties and additionalProperties=true allows access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id"], additionalProperties: true, }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with all optional properties and additionalProperties=false prevents access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: [], additionalProperties: false, }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with all required properties and additionalProperties=false prevents access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id", "name"], additionalProperties: false, }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable DefineProperty-wrapped typed object with mix of required and optional properties and additionalProperties=false prevents access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { id: { type: SchemaTypes.integer, }, name: { type: SchemaTypes.string, }, }, required: ["id"], additionalProperties: false, }); const param = ParameterVariable("", "incident", obj); assertStrictEquals(`${param}`, "{{incident}}"); assertStrictEquals(`${param.id}`, "{{incident.id}}"); assertStrictEquals(`${param.name}`, "{{incident.name}}"); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable using Custom Type with DefineProperty-wrapped typed object with optional property and additionalProperties=true yields object that allows access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: [], additionalProperties: true, }); const customType = DefineType({ name: "customType", ...obj, }); const paramName = "myCustomType"; const param = ParameterVariable("", paramName, { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, `{{${paramName}}}`); assertStrictEquals(`${param.aString}`, `{{${paramName}.aString}}`); assertStrictEquals(`${param.foo.bar}`, `{{${paramName}.foo.bar}}`); }); Deno.test("ParameterVariable using Custom Type with DefineProperty-wrapped typed object with required property and additionalProperties=true yields object that allows access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: ["aString"], additionalProperties: true, }); const customType = DefineType({ name: "customType", ...obj, }); const paramName = "myCustomType"; const param = ParameterVariable("", paramName, { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, `{{${paramName}}}`); assertStrictEquals(`${param.aString}`, `{{${paramName}.aString}}`); assertStrictEquals(`${param.foo.bar}`, `{{${paramName}.foo.bar}}`); }); Deno.test("ParameterVariable using Custom Type with DefineProperty-wrapped typed object with optional property and additionalProperties=false yields object that prevents access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: [], additionalProperties: false, }); const customType = DefineType({ name: "customType", ...obj, }); const paramName = "myCustomType"; const param = ParameterVariable("", paramName, { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, `{{${paramName}}}`); assertStrictEquals(`${param.aString}`, `{{${paramName}.aString}}`); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, `{{${paramName}.foo.bar}}`); }); Deno.test("ParameterVariable using Custom Type with DefineProperty-wrapped typed object with required property and additionalProperties=false yields object that prevents access to additional properties", () => { const obj = DefineProperty({ type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: ["aString"], additionalProperties: false, }); const customType = DefineType({ name: "customType", ...obj, }); const paramName = "myCustomType"; const param = ParameterVariable("", paramName, { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, `{{${paramName}}}`); assertStrictEquals(`${param.aString}`, `{{${paramName}.aString}}`); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, `{{${paramName}.foo.bar}}`); }); /** * ParameterVariable wrapped parameters result in specific types returned */ Deno.test("ParameterVariable using Custom Type string should yield SingleParameterVariable type", () => { const customType = DefineType({ name: "customTypeString", type: SchemaTypes.string, }); const param = ParameterVariable("", "myCustomTypeString", { type: customType, }); assert<IsExact<typeof param, SingleParameterVariable>>(true); assertStrictEquals(`${param}`, "{{myCustomTypeString}}"); }); Deno.test("ParameterVariable using Custom Type with untypedarray should yield SingleParameterVariable type", () => { const customType = DefineType({ name: "customTypeArray", type: SchemaTypes.array, }); const param = ParameterVariable("", "myCustomTypeArray", { type: customType, }); assert<IsExact<typeof param, SingleParameterVariable>>(true); assertStrictEquals(`${param}`, "{{myCustomTypeArray}}"); }); Deno.test("ParameterVariable using Custom Type untyped object should yield any type", () => { const customType = DefineType({ name: "customTypeObject", type: SchemaTypes.object, }); const param = ParameterVariable("", "myCustomTypeObject", { type: customType, }); assert<IsAny<typeof param>>(true); assertStrictEquals(`${param}`, "{{myCustomTypeObject}}"); assertStrictEquals(`${param.foo}`, "{{myCustomTypeObject.foo}}"); assertStrictEquals(`${param.foo.bar}`, "{{myCustomTypeObject.foo.bar}}"); assertStrictEquals( `${param.foo.bar.baz}`, "{{myCustomTypeObject.foo.bar.baz}}", ); }); ================================================ FILE: tests/integration/parameters/parameter_variable_unwrapped_test.ts ================================================ import { DefineType } from "../../../src/types/mod.ts"; import SchemaTypes from "../../../src/schema/schema_types.ts"; import { ParameterVariable } from "../../../src/parameters/mod.ts"; import { assert, assertStrictEquals, type IsAny, } from "../../../src/dev_deps.ts"; import type { CannotBeUndefined } from "../../../src/test_utils.ts"; Deno.test("ParameterVariable with unwrapped typed object with an optional Custom Type property should yield an object with a definite value for its sub-properties", () => { const StringType = DefineType({ name: "stringType", type: SchemaTypes.string, minLength: 2, }); const param = ParameterVariable("", "myObjectParam", { type: SchemaTypes.object, properties: { aString: { type: StringType, }, }, required: [], }); assertStrictEquals(`${param}`, "{{myObjectParam}}"); assertStrictEquals(`${param.aString}`, "{{myObjectParam.aString}}"); }); Deno.test("ParameterVariable using Custom Type with unwrapped object referencing another Custom Type as property should yield an object with a definite value for its sub-properties", () => { const StringType = DefineType({ name: "stringType", type: SchemaTypes.string, minLength: 2, }); const customType = DefineType({ name: "customTypeWithCustomType", type: SchemaTypes.object, properties: { customType: { type: StringType, }, }, required: [], }); const param = ParameterVariable("", "myNestedCustomType", { type: customType, }); assertStrictEquals(`${param}`, "{{myNestedCustomType}}"); assertStrictEquals( `${param.customType}`, "{{myNestedCustomType.customType}}", ); }); Deno.test("ParameterVariable using Custom Type with unwrapped typed object with optional property yields object with no undefined property", () => { const customType = DefineType({ name: "customType", type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: [], }); const param = ParameterVariable("", "myCustomType", { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, "{{myCustomType}}"); assertStrictEquals(`${param.aString}`, "{{myCustomType.aString}}"); }); Deno.test("ParameterVariable using Custom Type with unwrapped typed object with optional property and additionalProperties=true yields object that allows access to additional properties", () => { const customType = DefineType({ name: "customType", type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: [], additionalProperties: true, }); const param = ParameterVariable("", "myCustomType", { type: customType, }); assert<IsAny<typeof param.additionalProp>>(true); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, "{{myCustomType}}"); assertStrictEquals(`${param.aString}`, "{{myCustomType.aString}}"); assertStrictEquals(`${param.foo.bar}`, "{{myCustomType.foo.bar}}"); }); Deno.test("ParameterVariable using Custom Type with unwrapped typed object with optional property and additionalProperties=false yields object that prevents access to additional properties", () => { const customType = DefineType({ name: "customType", type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: [], additionalProperties: false, }); const param = ParameterVariable("", "myCustomType", { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, "{{myCustomType}}"); assertStrictEquals(`${param.aString}`, "{{myCustomType.aString}}"); //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, "{{myCustomType.foo.bar}}"); }); /** * TODO: below tests fail because unwrapped typed object yields a SingleParameterVariable, which is incorrect. Only happens when required properties are set. Deno.test("ParameterVariable using Custom Type with unwrapped typed object with required property yields object with no undefined property", () => { const customType = DefineType({ name: "customType", type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: ["aString"], }); const param = ParameterVariable("", "myCustomType", { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, "{{myCustomType}}"); assertStrictEquals(`${param.aString}`, "{{myCustomType.aString}}"); }); Deno.test("ParameterVariable using Custom Type with unwrapped typed object with required property and additionalProperties=true yields object that allows access to additional properties", () => { const customType = DefineType({ name: "customType", type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: ["aString"], additionalProperties: true, }); const param = ParameterVariable("", "myCustomType", { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, "{{myCustomType}}"); assertStrictEquals(`${param.aString}`, "{{myCustomType.aString}}"); assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); Deno.test("ParameterVariable using Custom Type with unwrapped typed object with required property and additionalProperties=false yields object that prevents access to additional properties", () => { const customType = DefineType({ name: "customType", type: SchemaTypes.object, properties: { aString: { type: SchemaTypes.string, }, }, required: ["aString"], additionalProperties: false, }); const param = ParameterVariable("", "myCustomType", { type: customType, }); assert<CannotBeUndefined<typeof param.aString>>(true); assertStrictEquals(`${param}`, "{{myCustomType}}"); assertStrictEquals(`${param.aString}`, "{{myCustomType.aString}}"); // TODO: current failure mode of this test actually causes this next line to incorrectly pass //@ts-expect-error foo doesn't exist assertStrictEquals(`${param.foo.bar}`, "{{incident.foo.bar}}"); }); */ ================================================ FILE: tests/integration/schema/slack/functions/_scripts/write_function_files_test.ts ================================================ import { assertEquals, assertExists, mock, } from "../../../../../../src/dev_deps.ts"; import { _internals } from "../../../../../../src/schema/slack/functions/_scripts/src/write_function_files.ts"; import { getSlackFunctions } from "../../../../../../src/schema/slack/functions/_scripts/src/utils.ts"; Deno.test(`write_function_files.ts ${_internals.main.name} function generates valid typescript`, async () => { const getSlackFunctionsStub = mock.stub( _internals, "getSlackFunctions", ( functionsPayloadPath = "src/schema/slack/functions/_scripts/src/test/data/function.json", ) => { return getSlackFunctions( functionsPayloadPath, ); }, ); const outputs: Record<string, string> = {}; const writeTextFileStub = mock.stub( _internals, "writeTextFile", // deno-lint-ignore require-await async (path, data) => { outputs[path] = data; }, ); try { await _internals.main(); mock.assertSpyCalls(writeTextFileStub, 3); mock.assertSpyCalls(getSlackFunctionsStub, 1); const generatedContent = outputs["../send_message.ts"]; assertExists(generatedContent); const command = new Deno.Command(Deno.execPath(), { cwd: `${Deno.cwd()}/src/schema/slack/functions/`, args: [ "eval", generatedContent, ], }); const { code, stdout, stderr } = await command.output(); const textDecoder = new TextDecoder(); assertEquals( textDecoder.decode(stderr), "", "The generated TypeScript content is not valid", ); assertEquals( textDecoder.decode(stdout), "", "The generated TypeScript should not print to the console", ); assertEquals(code, 0); } finally { writeTextFileStub.restore(); getSlackFunctionsStub.restore(); } }); ================================================ FILE: tests/integration/workflows/workflows_test.ts ================================================ import { assertEquals } from "../../../src/dev_deps.ts"; import { DefineWorkflow } from "../../../src/workflows/mod.ts"; import { DefineFunction } from "../../../src/mod.ts"; import SlackTypes from "../../../src/schema/slack/schema_types.ts"; Deno.test("Multi-step Workflow should export correct double-brace-wrapped step input values", () => { const TestFunction = DefineFunction({ callback_id: "no_params", title: "Test function", source_file: "", input_parameters: { properties: { email: { type: "string", }, name: { type: "string", }, manager: { type: "object", properties: { email: { type: "string" }, name: { type: "string" }, }, required: [], }, }, required: ["email"], }, output_parameters: { properties: { url: { type: "string", }, manager: { type: "object", properties: { email: { type: "string" }, name: { type: "string" }, }, required: [], }, }, required: ["url"], }, }); const workflow = DefineWorkflow({ callback_id: "test_wf", title: "test", input_parameters: { properties: { email: { type: "string", }, name: { type: "string", }, manager: { type: "object", properties: { email: { type: "string" }, name: { type: "string" }, }, required: ["name"], }, }, required: ["email", "manager"], }, }); assertEquals(workflow.id, workflow.definition.callback_id); assertEquals(workflow.definition.title, "test"); assertEquals(workflow.definition.description, undefined); // Add a DefineFunction result as a step const step1 = workflow.addStep(TestFunction, { email: workflow.inputs.email, name: `A name: ${workflow.inputs.name}`, manager: { name: workflow.inputs.manager.name, email: workflow.inputs.manager.email, }, }); // add a manually configured step const step2 = workflow.addStep("slack#/functions/create_channel", { channel_name: `test-channel-${workflow.inputs.name}-${step1.outputs.url}`, }); // another manually configured step, reyling on outputs of another manually configured step workflow.addStep("slack#/functions/send_message", { channel_id: "C123123", message: `Channel Created <#${step2.outputs.channel_id}>`, }); const exportedWorkflow = workflow.export(); const step1Inputs = exportedWorkflow.steps[0].inputs; const step2Inputs = exportedWorkflow.steps[1].inputs; const step3Inputs = exportedWorkflow.steps[2].inputs; assertEquals(exportedWorkflow.steps.length, 3); assertEquals(exportedWorkflow.title, "test"); assertEquals(exportedWorkflow?.input_parameters?.properties.email, { type: "string", }); assertEquals(`${step1Inputs.email}`, "{{inputs.email}}"); assertEquals(`${step1Inputs.name}`, "A name: {{inputs.name}}"); assertEquals(`${step1.outputs.url}`, "{{steps.0.url}}"); assertEquals(`${step1.outputs.manager?.email}`, "{{steps.0.manager.email}}"); assertEquals(`${step1.outputs.manager?.name}`, "{{steps.0.manager.name}}"); assertEquals( `${step2Inputs.channel_name}`, "test-channel-{{inputs.name}}-{{steps.0.url}}", ); assertEquals(`${step3Inputs.channel_id}`, "C123123"); assertEquals( `${step3Inputs.message}`, "Channel Created <#{{steps.1.channel_id}}>", ); }); Deno.test("Workflow addStep returns appropriate output types and properties for interactivity and user context types", () => { const TestFunction = DefineFunction({ source_file: "./test.ts", callback_id: "test", title: "Test", output_parameters: { properties: { interactivity: { type: SlackTypes.interactivity, }, user: { type: SlackTypes.user_context, }, }, required: ["interactivity"], }, }); const TestWorkflow = DefineWorkflow({ callback_id: "test", title: "Test", }); const step1 = TestWorkflow.addStep(TestFunction, {}); assertEquals( `${step1.outputs.interactivity}`, `{{steps.0.interactivity}}`, ); assertEquals( `${step1.outputs.interactivity.interactivity_pointer}`, `{{steps.0.interactivity.interactivity_pointer}}`, ); assertEquals( `${step1.outputs.interactivity.interactor.id}`, `{{steps.0.interactivity.interactor.id}}`, ); assertEquals( `${step1.outputs.interactivity.interactor.secret}`, `{{steps.0.interactivity.interactor.secret}}`, ); assertEquals( `${step1.outputs.user}`, `{{steps.0.user}}`, ); assertEquals( `${step1.outputs.user?.id}`, `{{steps.0.user.id}}`, ); assertEquals( `${step1.outputs.user?.secret}`, `{{steps.0.user.secret}}`, ); }); // Deno will exit on uncaught exceptions. // JSON.parse will raise an exception when given undefined. // By setting undefined manually to reflect the case of an // undefined inputValue, which will allow Deno to continue // and pass the undefined values up to the validation API // -- which will then communicate back to the CLI the specific // validation errors it ran in to. Deno.test("Malformed workflow step inputs should be undefined", () => { const TestFunction = DefineFunction({ callback_id: "test_undefined", title: "Test function", source_file: "", input_parameters: { properties: { message: { type: "string", }, }, required: ["message"], }, }); const workflow = DefineWorkflow({ callback_id: "test_wf", title: "test", }); const malformedFunctionStep = workflow.addStep(TestFunction, { message: undefined, }); workflow.addStep("slack#/functions/send_message", { channel_id: "C12345", message: malformedFunctionStep.outputs.message, }); const exportedWorkflow = workflow.export(); assertEquals(exportedWorkflow.steps[0].inputs.message, undefined); });