Compare commits

...

343 Commits

Author SHA1 Message Date
Eric Allam 63b1c2237a Changeset: release new version 2023-02-27 14:03:29 +00:00
Eric Allam c72120eab0 Removed accidental log statement 2023-02-27 14:03:15 +00:00
Eric Allam 313b7163e8 Change from npm create trigger to npx create-trigger 2023-02-27 13:49:04 +00:00
Eric Allam b0e64937a0 Changeset: new version 2023-02-27 13:04:15 +00:00
Eric Allam 3a2cf0ddb7 Fixed the missing error message when logging invalid API key and improved the error message 2023-02-27 13:03:14 +00:00
Eric Allam 1579cdbfb2 Changeset: release new version 2023-02-27 12:11:58 +00:00
James Ritchie f1e81137b7 Improved the copy for how to trigger your workflow 2023-02-27 12:10:50 +00:00
Eric Allam 45b2bea39f Changeset: version next.3 2023-02-27 11:35:08 +00:00
Eric Allam 4f47d03114 Added changeset 2023-02-27 11:35:08 +00:00
Eric Allam 618cca3304 Validate the API key (to better work with the templates and the CLI) 2023-02-27 11:35:08 +00:00
James Ritchie 5706c3ab52 Improved the API information link 2023-02-27 11:14:46 +00:00
James Ritchie e8c81cafb8 Improved some of the Template’s title copy 2023-02-27 11:14:46 +00:00
Eric Allam a0d062c977 Changeset: version next.2 2023-02-27 11:10:06 +00:00
Eric Allam 87a3bbee8e Added a more helpful message when adding an API key 2023-02-27 11:09:38 +00:00
Eric Allam a5ff50f737 Implement new API key sidebar and a couple other various fixes 2023-02-26 22:15:49 +00:00
James Ritchie 763dd8b55a Improved the environment panel in the side menu to be clearer 2023-02-26 19:10:06 +00:00
James Ritchie 9e5c687174 Improvements to the Org page api keys 2023-02-26 18:04:10 +00:00
James Ritchie dcbb0f5287 small padding fix 2023-02-26 17:54:52 +00:00
James Ritchie d64cfe5add Widened the pages displaying the Template grid 2023-02-26 17:40:21 +00:00
James Ritchie 1fa52f1c8c Added padding back to the template cards 2023-02-26 17:40:00 +00:00
James Ritchie ea28f3dd47 Added installation instructions to the npm copy button for a template 2023-02-26 17:29:00 +00:00
James Ritchie 2c563ce998 Improvements to the Container component for mobile 2023-02-26 17:28:31 +00:00
James Ritchie 7dd0102579 Fixed the public template page on mobile 2023-02-26 17:08:29 +00:00
James Ritchie 01bc7df3e2 Template Overview page sticky left column now works 2023-02-26 16:51:37 +00:00
James Ritchie 6d4f71d22c Improvements to the template grid 2023-02-26 16:46:01 +00:00
James Ritchie cc002ef707 Increased the width of the template overview side column so you can see more package info 2023-02-26 16:06:38 +00:00
James Ritchie f552af517b Removed redundant class 2023-02-26 15:58:57 +00:00
James Ritchie 8aff958bd3 Copy fix 2023-02-26 15:57:06 +00:00
James Ritchie ad189b1f4b Improved the scroll bars on modals 2023-02-26 15:54:48 +00:00
James Ritchie e54ba7cbed Improved the Create workflow page and moved some info into a modal 2023-02-26 15:39:10 +00:00
James Ritchie 1d056b5328 Updated the org page styles 2023-02-26 12:26:31 +00:00
Eric Allam cf7c413c87 Combine the workflow onboarding into one component and obfuscate the api key 2023-02-24 22:16:53 +00:00
Eric Allam ac87a634a3 Add in the apiKeys in the orgs list page, obfuscated, and obfuscate them in the SideMenu as well 2023-02-24 22:09:30 +00:00
James Ritchie c9e411be9b Simplified the info on the template cards 2023-02-24 18:51:38 +00:00
James Ritchie ee4e181fc3 Added ability to copy env vars from the Org page 2023-02-24 18:51:38 +00:00
James Ritchie f51f8f3ece Added more styles to the copy text panel 2023-02-24 18:51:38 +00:00
D-K-P 12f4f3bc68 Changed blank starter and hello world images 2023-02-24 18:02:21 +00:00
Eric Allam 894a5dcb5d Pass through command flags 2023-02-24 17:58:05 +00:00
Eric Allam 3af9b63a9c Changeset: release next.1 version 2023-02-24 17:34:09 +00:00
Eric Allam 0932ae7d89 Log out when a run first starts, as well as completes 2023-02-24 17:32:13 +00:00
Eric Allam 12056d888b Update the docs for the new create-trigger cli 2023-02-24 16:36:50 +00:00
Eric Allam 63e004594c No workflows in the blank starter 2023-02-24 16:20:25 +00:00
James Ritchie ec3e98ca9f Updated the Orgs with more distinct icons 2023-02-24 16:12:48 +00:00
Eric Allam f3ffffd50b implement the New Workflow page and add the api key to the command to copy for templates and the main one 2023-02-24 15:59:34 +00:00
Eric Allam e6550d156e Convert basic starter to hello world and added isLive to templates to filter out the existing basic starter 2023-02-24 15:29:27 +00:00
James Ritchie ae550df37d Updated the Workflows Overview page if you have no workflows 2023-02-24 15:25:45 +00:00
James Ritchie 26ac1fa57c CopyText ignores default and propagation clicks 2023-02-24 15:25:45 +00:00
James Ritchie 14031a8c6e Title component now accepts className 2023-02-24 15:25:45 +00:00
James Ritchie 719ef2a3a9 Improved the grid template styles 2023-02-24 15:25:45 +00:00
Eric Allam aa295134ff Changeset: prerelease the next version 2023-02-24 13:29:36 +00:00
Eric Allam b2f14cbea3 Make the SDK a patch 2023-02-24 13:29:08 +00:00
Eric Allam 26e69cb659 Added cli changeset 2023-02-24 13:15:49 +00:00
Eric Allam 51f9bc9d72 Added handy links to the SDK when logging information about the workflows 2023-02-24 13:15:49 +00:00
Eric Allam 74a6a21c35 Adding the create-trigger CLI package 2023-02-24 13:15:49 +00:00
James Ritchie b69058bfcb Style tweaks 2023-02-24 12:17:00 +00:00
James Ritchie 1c5d8a4c0b Updated the template page to include the npm copy panel, link to repo and docs 2023-02-24 11:53:48 +00:00
James Ritchie 15ab35c816 Added a new copy npm install values to the template cards 2023-02-24 11:31:51 +00:00
James Ritchie 1218320492 Added a new copy panel 2023-02-24 11:30:41 +00:00
James Ritchie 80373c6874 Removed file for old steps 1, 2, 3 2023-02-24 09:54:22 +00:00
James Ritchie 590dd688a9 Fixed max width 2023-02-24 09:50:19 +00:00
James Ritchie f739f49209 First workflow onboarding step is now Choose a Template 2023-02-24 09:49:43 +00:00
James Ritchie dabf26c2c9 Added a message to Test page to make it clearer how to run tests 2023-02-24 09:37:30 +00:00
Eric Allam 173f88dcad Fixed the revalidator in the useEffect deps array causing tons of requests 2023-02-23 18:23:37 +00:00
Eric Allam ee20f9212f Make the schema an optional param for customEvent and webhookEvent 2023-02-23 12:33:51 +00:00
James Ritchie 566fb54615 Fixed generic webhook panel not showing when it was connected 2023-02-22 17:57:47 +00:00
Eric Allam 100edb8abd Don’t require dotenv, instead use it in the dev command 2023-02-22 16:21:04 +00:00
Eric Allam 8cb08b63d2 Recreate the original changelog for generated packages 2023-02-22 12:47:15 +00:00
James Ritchie 4f21aaef97 Removed Intercom 2023-02-22 11:30:52 +00:00
James Ritchie 8272363069 Added absolute path for the integrations link 2023-02-22 11:03:08 +00:00
James Ritchie 3e32802fdf Warp example infisical cmd fixed 2023-02-22 10:50:47 +00:00
James Ritchie 3e1c1138e5 Fix for integrations server env variables not being loaded on local 2023-02-22 10:50:31 +00:00
Matt Aitken 7d1e64cb01 WIP on API.fetch support 2023-02-22 09:47:21 +00:00
Matt Aitken 08abf25fd9 Added base url to services, for use by fetch 2023-02-22 09:46:49 +00:00
Matt Aitken 9c9a4caa20 Merge pull request #66 from triggerdotdev/features/new-overview-page
Improved overview page
2023-02-21 22:26:13 +00:00
Matt Aitken 352b7f75ce Installed packages 2023-02-21 22:22:04 +00:00
Matt Aitken f0378fa874 Fix for incorrect types (after merge) 2023-02-21 22:21:51 +00:00
Matt Aitken 8de37d47bf Renamed new example package to @examples/generic-webhook 2023-02-21 22:16:37 +00:00
Matt Aitken 23376457e6 Merge remote-tracking branch 'origin/dev' into features/new-overview-page
# Conflicts:
#	pnpm-lock.yaml
2023-02-21 22:13:42 +00:00
James Ritchie 75589d3bae Reduced prominence of connect api message 2023-02-21 21:35:07 +00:00
James Ritchie 96c4f319e4 Label positioned better 2023-02-21 21:30:14 +00:00
James Ritchie 8587841ee4 Made the test button less prominent 2023-02-21 21:27:30 +00:00
James Ritchie 058afb40ce Copy improvement 2023-02-21 21:18:41 +00:00
Eric Allam dc058c8835 Remove webapp dep on integrations server 2023-02-21 21:15:20 +00:00
James Ritchie f80253ed1d typography improvements 2023-02-21 21:13:19 +00:00
James Ritchie 3e384ff18a Show better copy in the How to trigger your workflow panel 2023-02-21 21:05:12 +00:00
James Ritchie db9b03ca05 Link to the org integrations page 2023-02-21 21:04:17 +00:00
James Ritchie cc6adecfb4 Only show the Create workflow run panel if a workflow or non-test runs have happened 2023-02-21 20:43:46 +00:00
James Ritchie 841421bf76 Made the concertina panels open when relevant 2023-02-21 20:36:02 +00:00
Matt Aitken f5ebde8ddb Changeset: release version 2023-02-21 16:27:10 +00:00
Matt Aitken da9e357b6d Added env vars to prod webapp 2023-02-21 16:23:18 +00:00
Matt Aitken cde0b7fcfe Increased the p-integrations resources 2023-02-21 16:21:50 +00:00
Matt Aitken 43f913e796 Prod flightcontrol for integrations 2023-02-21 16:14:54 +00:00
Matt Aitken 26ae66ad2a Add Airtable client_id for prod 2023-02-21 16:14:11 +00:00
Matt Aitken 88fe24ad67 Fix so integration requests can use api_key auth type 2023-02-21 15:53:25 +00:00
Matt Aitken da0ee481b8 Make staging webapp depend on integrations 2023-02-21 15:53:25 +00:00
Matt Aitken ff57a70a53 Added airtable to staging client ids 2023-02-21 15:53:25 +00:00
Matt Aitken 22ae2131a5 The internal CLI can now push API keys to Pizzly from our new integrations 2023-02-21 15:53:25 +00:00
Matt Aitken 5880d966ac serviceFetch basics working. Share some code with requestEndpoint 2023-02-21 15:53:25 +00:00
Eric Allam 9eeacee7c2 Fix: pass in the id from sendEvent through to the API call 2023-02-21 14:12:14 +00:00
D-K-P 6ced99fb02 Updated the healthcheck image 2023-02-21 13:12:14 +00:00
James Ritchie 28fb5d066f Updated the integrations link 2023-02-21 12:49:15 +00:00
James Ritchie 0d72b8e0c4 Added a custom webhook connection concertina 2023-02-21 12:47:51 +00:00
James Ritchie 547aab667d Added a custom plug icon 2023-02-21 12:47:20 +00:00
James Ritchie d744efdb26 Added a generic webhook example workflow 2023-02-21 12:47:07 +00:00
Matt Aitken 727a492e45 Don’t restrict access to /healthcheck 2023-02-21 12:12:28 +00:00
Matt Aitken 384752d36c Added generate script to integrations 2023-02-21 11:58:41 +00:00
Matt Aitken d699d38981 Changeset version – next: New integrations service compatibility 2023-02-21 11:51:13 +00:00
Matt Aitken 6b53aeb8c2 Changeset added: New integrations service compatibility 2023-02-21 11:49:55 +00:00
Matt Aitken b7ee8fd432 Revert "Changeset for new integrations service"
This reverts commit 5df5e30a7d.
2023-02-21 11:48:34 +00:00
Matt Aitken 8836aeae81 Latest pnpm lockfile 2023-02-21 11:46:24 +00:00
Matt Aitken 2748d9e9a6 Revert "Changeset version: created a pre-release 0.3.x"
This reverts commit 9e739a50b2.

# Conflicts:
#	apps/integrations/package.json
2023-02-21 11:45:17 +00:00
Matt Aitken f2a0f304af Removed @trigger.dev/sdk dependency from the integrations package.json 2023-02-21 11:43:01 +00:00
Matt Aitken 7659835541 Integrations docker frozen-lockfile again 2023-02-21 11:25:21 +00:00
James Ritchie 8862b563dd Added a “How to run your workflow” concertina 2023-02-21 11:22:35 +00:00
James Ritchie ed1c2da660 Added a permenant message about api connectiions 2023-02-21 11:22:13 +00:00
Matt Aitken 9e739a50b2 Changeset version: created a pre-release 0.3.x 2023-02-21 11:21:04 +00:00
Matt Aitken e4efed07e5 Improved changeset pre-release instructions and fixed a config issue 2023-02-21 11:20:47 +00:00
Matt Aitken 55644577ea no-frozen-lockfile 2023-02-21 11:17:01 +00:00
Matt Aitken 5df5e30a7d Changeset for new integrations service 2023-02-21 11:16:18 +00:00
Matt Aitken 43ceb44db9 Fixed Sendgrid version number 2023-02-21 11:16:03 +00:00
Matt Aitken 154294f86e Added more ignores to changeset config 2023-02-21 11:12:40 +00:00
Matt Aitken e20f7ba16a Installed packages 2023-02-21 11:12:35 +00:00
Matt Aitken be6954abf2 changeset config for generated-integrations 2023-02-21 11:00:17 +00:00
Matt Aitken 810996f26f Fix for type error with optional outpyt JSONSchema 2023-02-21 11:00:04 +00:00
Matt Aitken a7044511bb Doesn’t change the generated package.json file if it exists 2023-02-21 10:59:48 +00:00
Matt Aitken 0efbbece9e Changed version number for Airtable 2023-02-21 10:59:29 +00:00
Matt Aitken a38945ebcd Delete not-exported slack integration 2023-02-21 10:58:34 +00:00
James Ritchie a63ebd73fb Added a connection icon and fixed some concertina behaviour issues 2023-02-21 10:44:05 +00:00
Matt Aitken b34018eb17 Created integrations Dockerfile 2023-02-21 10:25:36 +00:00
Matt Aitken a6f9e1e35d Setup flightcontrol for integrations 2023-02-21 10:25:28 +00:00
Matt Aitken c7c5d407ef Added healthcheck 2023-02-21 10:25:00 +00:00
James Ritchie 0c6d98d353 Added margin between the sections 2023-02-21 10:19:16 +00:00
Matt Aitken f6ed5517c2 Merge branch 'features/new-integrations' into dev 2023-02-21 10:10:15 +00:00
Matt Aitken 4aae45ede2 WIP on integration.fetch 2023-02-21 10:09:56 +00:00
James Ritchie f969f03197 Now exporting the connection data for useConnectionSlots 2023-02-21 10:01:19 +00:00
James Ritchie cd8e85f85f Show hide the concertina panel for connecting the source 2023-02-21 09:29:50 +00:00
James Ritchie 3c27b3c40b Passed classnames props 2023-02-21 09:19:54 +00:00
Eric Allam 0a3b12d135 Make the private repo checkbox flipped to be public, and now it’s default private 2023-02-20 21:17:48 +00:00
Eric Allam aca332fffc Remove the testing instructions from the run local markdown 2023-02-20 21:09:27 +00:00
James Ritchie 350a60590d WIP adding a concertina menu to the main workflow panel 2023-02-20 18:24:06 +00:00
James Ritchie ad8a2f170a Improved the border style of the disconnected apis 2023-02-20 18:23:06 +00:00
D-K-P 67f6971601 Added github issues seed / md files 2023-02-20 17:49:59 +00:00
Eric Allam 695e4ca3f0 Split pulsar and the rest of the local docker services 2023-02-20 17:23:42 +00:00
D-K-P 4243b10683 Updated the template grid so it would preview on the signed in version better, and replaced the resend image 2023-02-20 17:10:52 +00:00
James Ritchie c9bd1812f4 Swapped over the workflow panel with the API panel 2023-02-20 17:10:18 +00:00
James Ritchie a117a3b534 APIs not connected get a red border treatment to make them stand out 2023-02-20 17:07:33 +00:00
James Ritchie 9e64f9d570 Copy changes to titles 2023-02-20 16:53:01 +00:00
D-K-P 823da44fb7 Added schedule healthcheck info 2023-02-20 16:21:32 +00:00
Matt Aitken a5a5dcbccb Generated SendGrid SDK 2023-02-20 15:35:09 +00:00
Matt Aitken e2693eb539 Removed $ref from SendGrid marketingContacts endpoint schema 2023-02-20 15:33:07 +00:00
Matt Aitken 036ffa7e5f Created marketingContacts endpoint 2023-02-20 15:23:52 +00:00
Matt Aitken 679ac99fad Added scopes to api_key authentication in the web app 2023-02-20 15:23:34 +00:00
Matt Aitken 9f6886c55b Added infisical config to the root 2023-02-20 13:45:28 +00:00
Eric Allam ad9089da57 Make sure only started runs are considered created 2023-02-20 13:43:58 +00:00
Eric Allam 9313adee12 Add the github access docs link and tweak copy 2023-02-20 13:29:07 +00:00
Matt Aitken 9bb8363643 Fix for typecheck issue 2023-02-20 13:27:11 +00:00
Matt Aitken 4e2fe3dcac Moved all presenters into the presenters folder 2023-02-20 13:25:46 +00:00
D-K-P 092b139d71 Added a second line to the github button on the template deploy page 2023-02-20 12:34:02 +00:00
Matt Aitken ab92e9515b Installed packages 2023-02-20 12:28:34 +00:00
Matt Aitken d4939c4e11 More fixes from mege conflict 2023-02-20 12:28:30 +00:00
James Ritchie ef3f075042 Added a missing noreferrer property 2023-02-20 12:26:01 +00:00
Matt Aitken c76d930f08 Fixing issues with service metadata 2023-02-20 12:22:32 +00:00
Matt Aitken 06f2fc3aeb Fix for bad import of WorkflowsPresenter 2023-02-20 12:12:13 +00:00
Matt Aitken 3cd4f2fb9f Merge remote-tracking branch 'origin/dev' into features/new-integrations
# Conflicts:
#	apps/webapp/app/env.server.ts
#	apps/webapp/app/models/integrations.server.ts
#	apps/webapp/app/models/organizationTemplatePresenter.server.ts
#	apps/webapp/app/models/templateListPresenter.server.ts
#	apps/webapp/app/models/templatePresenter.server.ts
#	apps/webapp/app/models/workflowListPresenter.server.ts
#	apps/webapp/app/models/workflowStartPresenter.server.ts
#	apps/webapp/app/models/workflowsPresenter.server.ts
#	apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/index.tsx
#	apps/webapp/app/routes/__app/orgs/$organizationSlug/__org/workflows.new.tsx
#	pnpm-lock.yaml
2023-02-20 12:10:50 +00:00
D-K-P 8f011a5da3 Updated step 2 text so it’s clearer which route includes the repo templates 2023-02-20 11:59:50 +00:00
Eric Allam 067dbfc0cf Improved the template docs 2023-02-20 11:57:36 +00:00
Matt Aitken 8dfdfdb2fa Regenerated docs with default open on params 2023-02-20 11:52:08 +00:00
Matt Aitken 3b76574ef8 Make the params default open 2023-02-20 11:52:00 +00:00
Matt Aitken 3a0a6e491c Update SendGrid’s documentation 2023-02-20 11:48:48 +00:00
Matt Aitken 24203d1b8d Sanitize text for mdx (so we can display HTML tags on the page) 2023-02-20 11:48:37 +00:00
Matt Aitken 8da0f9ac01 Generated SendGrid SDK 2023-02-19 19:30:01 +00:00
Matt Aitken 011264865b Fixes for an optional body in the response 2023-02-19 19:29:52 +00:00
Matt Aitken 889d9910e2 SendGrid mailSend endpoint implemented 2023-02-19 19:18:23 +00:00
Matt Aitken 679239d9fc Added scopes to api_key authentication method 2023-02-19 19:18:08 +00:00
Matt Aitken 20d8493c3e Response body is optional 2023-02-19 19:17:56 +00:00
Matt Aitken fe598ff713 Add support for api_key authentication 2023-02-19 19:17:41 +00:00
Matt Aitken ac1354b71a Deal with validating when a schema is undefined or data is undefined 2023-02-19 19:17:27 +00:00
Matt Aitken 55f04d5d12 Fix for generateTypes test now we don’t format 2023-02-19 19:17:01 +00:00
Matt Aitken 3796ba5b07 Generated docs for Slack example and Airtable 2023-02-19 17:49:34 +00:00
Matt Aitken 86e1e7143f Generate docs with good support for oneOf and anyOf 2023-02-19 17:49:25 +00:00
Matt Aitken d32aa49020 output the response types 2023-02-19 17:27:38 +00:00
Matt Aitken 08338d15a5 Generate much nicer params 2023-02-19 17:09:46 +00:00
Matt Aitken 4b3c9b7230 Much better generated docs 2023-02-19 16:36:56 +00:00
Matt Aitken e84e43e4e1 Decent param field generation 2023-02-19 16:02:10 +00:00
Matt Aitken 584b12451d Started generating docs, with metadata first 2023-02-19 15:15:28 +00:00
Matt Aitken 0913a34174 Prepared code generation for docs 2023-02-19 14:58:41 +00:00
Matt Aitken e6d6bfced4 Newly generated SDKs using the new generator 2023-02-19 14:37:37 +00:00
Matt Aitken c7e926a8a9 Ignore missing patterns when linting 2023-02-19 14:36:09 +00:00
Matt Aitken 3ad8a0a5de Separate schema titles and descriptions so we get nice TypeNames and jsdocs 2023-02-19 14:35:58 +00:00
Matt Aitken 0d918e7bf6 Generate types by creating a combined schema 2023-02-19 14:35:27 +00:00
Matt Aitken 444996533f Deduplicated type definitions, but still has multiple interfaces 2023-02-19 13:48:51 +00:00
Matt Aitken 1b53ccbbac WIP on de-duplicating type definitions from generated SDK 2023-02-19 13:42:48 +00:00
Matt Aitken dbd4e9a68c Don’t format the types when creating them from schemas 2023-02-19 13:42:32 +00:00
Matt Aitken 0c36c3e16f When making schemas don’t add properties with undefined values, it messes up the Typescript generator 2023-02-19 12:53:27 +00:00
Matt Aitken 05af0ba2b5 Make multiple actions at once 2023-02-19 12:48:25 +00:00
Matt Aitken f2e341e753 Delete records and record 2023-02-18 17:26:27 +00:00
Matt Aitken 6e337eb144 Create mutiple and single records 2023-02-18 17:06:05 +00:00
Matt Aitken fd90e3c3d8 Update single record 2023-02-18 16:56:47 +00:00
Matt Aitken b969653f7c Added update/upsert records 2023-02-18 16:49:54 +00:00
Matt Aitken b5128abee0 listRecords is working 2023-02-18 16:26:34 +00:00
Matt Aitken 1f5053bcdb Reworked how nock works to allow updating 2023-02-18 16:07:57 +00:00
Matt Aitken 232b58daf1 Added listRecords endpoints with improvements to generating schemas 2023-02-18 15:17:50 +00:00
Matt Aitken d07a54f09b Airtable Field schema 2023-02-18 14:06:16 +00:00
Matt Aitken 2adea4cd9b Created an AirTable field schema 2023-02-18 12:30:18 +00:00
Matt Aitken 27e1d134f7 WIP Airtable schemas 2023-02-17 19:07:20 +00:00
Matt Aitken 92ca01f3c7 An easier way to write JSON Schemas 2023-02-17 19:05:48 +00:00
Matt Aitken c75181f10d Renamed the code that used open api specs 2023-02-17 19:05:25 +00:00
James Ritchie a26002c525 Improved the layout of the TemplateOverview 2023-02-17 18:45:22 +00:00
James Ritchie 5a3fdc470f The logo had a broken URL link in the marketing header 2023-02-17 17:06:00 +00:00
Matt Aitken 781a208c07 Merge pull request #60 from mv-turtle/main
Added infisical as one of the command in package.json
2023-02-17 16:12:26 +00:00
Matt Aitken 67eb592ecc Merge branch 'dev' into main 2023-02-17 16:11:20 +00:00
James Ritchie efc35c6c79 Added a Todo comment to make it clear the id needs to be unique 2023-02-17 16:03:36 +00:00
Eric Allam 65649c1fc7 Merge branch 'main' into dev 2023-02-17 14:35:49 +00:00
Matt Aitken 0fa59d9387 More code comments in the examples 2023-02-17 14:25:27 +00:00
Matt Aitken aa8cd86041 Improved GitHub star example with comments 2023-02-17 14:15:06 +00:00
Matt Aitken 165bd68d2d Added nice code comments to GitHub issues example 2023-02-17 14:15:06 +00:00
Eric Allam 9b664a1af2 Added the org template repo to the workflow overview page 2023-02-17 14:10:58 +00:00
Matt Aitken 3d174f5aeb Added code comments to the custom event 2023-02-17 13:52:05 +00:00
Matt Aitken b341e7bb71 Added code comments to the webhook example 2023-02-17 13:51:42 +00:00
Matt Aitken 1dc11a39ea Improved the WhatsApp example text 2023-02-17 13:36:48 +00:00
James Ritchie 970cfcb094 Improved the copy on step 6 of the existing repo journey 2023-02-17 13:35:55 +00:00
James Ritchie c33e228b26 Small improvement to the connected workflow message 2023-02-17 13:35:55 +00:00
Matt Aitken 6ba6e20026 Improved the Shopify example 2023-02-17 13:16:52 +00:00
Matt Aitken 81b2742577 Removed new user (Resend) example as it involves too much config for now 2023-02-17 12:25:40 +00:00
Matt Aitken ccadf08bd2 Set the from email to say Trigger.dev 2023-02-17 12:11:59 +00:00
D-K-P cea1291496 Removed title dupe 2023-02-17 12:05:15 +00:00
Matt Aitken 3e6b0c747b Workflow not connected to environment: Show a message on the overview page with basic help 2023-02-17 12:01:02 +00:00
Matt Aitken affcabc36f Workflow not connected to environment: hide test and runs pages 2023-02-17 12:01:02 +00:00
D-K-P 6732015b5c Updated the welcome email 2023-02-17 11:44:19 +00:00
Eric Allam cbb50a05ad Fixed issue with expiring user-to-server access tokens 2023-02-17 11:34:49 +00:00
Eric Allam 142fa85b14 Removed unnecessary log 2023-02-17 11:34:49 +00:00
James Ritchie 857ecf1de7 Gave the connected workflow more space 2023-02-17 11:24:24 +00:00
James Ritchie df6fe919c2 Added a Waiting to connect state to the Deploy step 2023-02-17 11:19:23 +00:00
James Ritchie 579e98c7af Copy updates to the new repo journey 2023-02-17 10:48:46 +00:00
James Ritchie 4724ef00fc Fixed Safari bug where template grid cards were really long and scrolling 2023-02-17 10:40:53 +00:00
James Ritchie 77e3769dbd Removed full stop 2023-02-17 10:40:53 +00:00
James Ritchie 70a547a5c5 Button arrow now uses a heroicon 2023-02-17 10:40:53 +00:00
Matt Aitken 3c8a3c421b Make it clear a request error comes from the API being called 2023-02-17 10:31:08 +00:00
Eric Allam 842ab0cdcc Ping the connection every second to try and keep it open through CloudFront 2023-02-17 09:11:13 +00:00
James Ritchie 64e0445acb Added a github icon to the external repo link 2023-02-16 20:50:25 +00:00
Eric Allam 213d86cb1f Improve run local instructions 2023-02-16 20:49:57 +00:00
Eric Allam 66805f67fa Call examples examples, not templates 2023-02-16 20:39:21 +00:00
Eric Allam 3defbcc0ed Fill in the correct API key in the run local docs 2023-02-16 20:23:12 +00:00
Eric Allam 942e695782 Fill in the ready to deploy api keys 2023-02-16 20:21:49 +00:00
James Ritchie 4a8b977a99 Remade the Deploy step layout 2023-02-16 20:03:31 +00:00
Eric Allam 2e9875c4c6 Add loading UI state when submitting add template form 2023-02-16 19:59:13 +00:00
Eric Allam ed775706a0 Use the installation_repositories.added webhook because repository.created wasn’t reliably being delivered 2023-02-16 19:42:36 +00:00
Matt Aitken f659d9aec6 Don’t delete the nock snapshot when a test suite fails 2023-02-16 15:34:04 +00:00
Matt Aitken 6c50ce078a Improved nock scripts, and setup for Airtable 2023-02-16 12:46:23 +00:00
Matt Aitken 9dc2ec4dca WIP on AirTable failing test 2023-02-16 12:10:05 +00:00
Matt Aitken 694df6ee38 Nock records and saves a successful run’s recording from tests 2023-02-16 12:08:35 +00:00
Matt Aitken c27987c191 Created a system to record successful test suite responses and load them on the next run 2023-02-16 11:50:35 +00:00
Matt Aitken 25cf0245a4 Tidied up the mad nock reply 2023-02-15 17:48:45 +00:00
Matt Aitken 98cac166b3 Use nock to return from the API 2023-02-15 17:42:14 +00:00
Matt Aitken c2e9accade Moved the logger below express json 2023-02-15 17:41:55 +00:00
Matt Aitken 1410688499 Added morgan logging to the integrations server 2023-02-15 14:57:38 +00:00
Matt Aitken a94c8e4576 Airtable example working 2023-02-15 14:51:14 +00:00
Matt Aitken 5fc1236d2e Added nock 2023-02-15 14:50:58 +00:00
Matt Aitken 3b67a5ce10 Remove unused integrations index file 2023-02-15 14:50:52 +00:00
Matt Aitken 2acd79fb36 Fix so the integration actually responds 2023-02-15 14:08:59 +00:00
Matt Aitken 8d15e9a2b9 Created Airtable example 2023-02-15 14:01:29 +00:00
Matt Aitken d99446a1b2 Added descriptions to the response fields 2023-02-15 14:01:19 +00:00
Matt Aitken 7b24248b80 Deleted slackv2 example 2023-02-15 14:01:07 +00:00
Matt Aitken 192a1d2119 Made the Airtable fields a loose schema for now 2023-02-15 13:53:46 +00:00
Matt Aitken 2261a769e4 Deleted pointless generateService test 2023-02-15 13:49:59 +00:00
Matt Aitken 7e92e8cc75 Added Airtable to the catalog 2023-02-15 13:41:25 +00:00
Matt Aitken 5a312dec55 Fix generate test to point at slackv2 2023-02-15 13:41:06 +00:00
Matt Aitken 15316a7504 Started work on Airtable 2023-02-15 13:29:15 +00:00
Matt Aitken fa80ca07b7 Make the slack example not live 2023-02-15 13:22:20 +00:00
Matt Aitken b142c55fc5 The DisplayProperties are now returned properly from the API 2023-02-15 11:55:06 +00:00
Matt Aitken 454bfcc58e Add the version info to a created IntegrationRequest 2023-02-15 11:54:37 +00:00
Matt Aitken eb98f50fca Get the display properties when creating an integration request 2023-02-15 11:54:06 +00:00
Matt Aitken 81ddf597ed Debugging the webapp should use infisical 2023-02-15 11:53:45 +00:00
Matt Aitken 75d6471c9e Created an example that uses the slackv2 integration 2023-02-15 11:53:30 +00:00
Matt Aitken 68544359f1 Generate the SDK with the correct endpoint name 2023-02-15 11:53:15 +00:00
Matt Aitken 3ce5d5bf1a Include generated-integrations in the workspace 2023-02-15 11:18:38 +00:00
Matt Aitken f8ac05500a Renamed the new slack integration slackv2 2023-02-15 11:17:59 +00:00
Matt Aitken 23dbeff1c6 Added .template to the end of the template files so turbo doesn’t pick up on them 2023-02-15 11:10:07 +00:00
Matt Aitken 9db77b6478 Reverted workflowRunStep.server.ts 2023-02-15 10:37:05 +00:00
Matt Aitken 08ecd288e1 DisplayProperties stored on a run step and first read from there 2023-02-15 10:33:03 +00:00
Matt Aitken 83201312b2 The API won’t respond with services that aren’t live 2023-02-15 10:08:52 +00:00
Matt Aitken bfa096dc4a Services metadata being pulled from old and new systems 2023-02-15 09:54:34 +00:00
Matt Aitken 558b705ace Added version to the Display properties are working for v1 integrations 2023-02-15 09:49:02 +00:00
Matt Aitken 4874288db0 Added icon to the services API json 2023-02-15 09:26:50 +00:00
Matt Aitken 30c0984623 Make the api use v2 (as v1 was the old integrations system) 2023-02-15 09:23:14 +00:00
Vladyslav Matsiiako 1b675968f6 Added infisical as one of the command in package.json 2023-02-14 19:46:36 -08:00
Matt Aitken a96bfd3528 The display properties endpoint is working 2023-02-14 20:20:55 +00:00
Matt Aitken c359962b3a Removed logging in the auth middleware 2023-02-14 18:45:35 +00:00
Matt Aitken 7bc11fe41f Added test for action displayProperties and made a fix 2023-02-14 18:44:24 +00:00
Matt Aitken 108bb6281e Added displayProperties functionality to the endpoints/actions 2023-02-14 18:39:29 +00:00
Matt Aitken 140d02f462 WIP getting the new integrations working with the old ones 2023-02-14 16:54:13 +00:00
Matt Aitken 6a3ffa6395 Added authentication middleware to the integrations service 2023-02-14 16:53:49 +00:00
Matt Aitken d807421b40 Migrated the metadata format 2023-02-14 15:39:59 +00:00
Matt Aitken b2bd170a2c Downgraded vitest in the webapp because it caused typecheck issues 2023-02-14 15:39:43 +00:00
Matt Aitken e013c6faa9 Renamed slug to service 2023-02-14 15:09:46 +00:00
Matt Aitken d60e9ae939 Renamed IntegrationMetadata to ServiceMetadata 2023-02-14 15:09:00 +00:00
Matt Aitken 8039f326a9 Created an IntegrationsClient that’s used to communicate with the integrations service 2023-02-14 15:04:45 +00:00
Matt Aitken 259e28e13a Removed old todo 2023-02-14 15:04:27 +00:00
Matt Aitken 974033d5d2 Don’t export the service metadata type 2023-02-14 14:52:51 +00:00
Matt Aitken 84bd5a63a2 Added API key auth type 2023-02-14 14:52:35 +00:00
Matt Aitken dfab30c901 Endpoint that returns information about the services 2023-02-14 14:33:58 +00:00
Matt Aitken 8f5474072f Added live flag to service 2023-02-14 14:33:29 +00:00
Matt Aitken fb6953bc1d Actually we’re going to cache using connection + service 2023-02-14 13:58:54 +00:00
Matt Aitken 8a6467f4c8 The namespace for the cache includs connection id and workflow id
Connections are used across workflows, so there was a chance the same key could have been fighting
2023-02-14 13:57:14 +00:00
Matt Aitken 581068362f The integration service now returns the response in the format the webapp is expecting 2023-02-14 13:44:03 +00:00
Matt Aitken 011d7d5611 Added retryableStatusCodes to a service 2023-02-14 13:43:24 +00:00
Matt Aitken caac573a64 Added version and made response schema optional for SDK requests 2023-02-14 12:35:24 +00:00
Matt Aitken 39d02e6d00 Added the integrations service to the example Warp config 2023-02-14 12:33:17 +00:00
Matt Aitken 1e1da62b32 The API for integrations is now versioned, accepts params and build credentials 2023-02-14 12:32:21 +00:00
Matt Aitken a5190693c8 Use __trigger so we don’t have to change the webhooks and introduce incompatibility 2023-02-13 23:08:13 +00:00
Matt Aitken de5b2ca3d8 Add metadata to slack post message 2023-02-13 23:02:29 +00:00
Matt Aitken 5c8fc8188a Passing the cache through to requests, it’s working. Made the expected request metadata specific 2023-02-13 22:52:01 +00:00
Matt Aitken 5949c98748 Created Postgres schema with cache table and PostgresCacheService 2023-02-13 22:40:44 +00:00
Matt Aitken 4f7a8f6ae0 Created a VS Code workspace 2023-02-13 22:05:57 +00:00
Matt Aitken c01bc15622 Allow metadata to be passed to the integrations API 2023-02-13 22:05:39 +00:00
Matt Aitken 027f0210dc The input is validated and results are coming back! 2023-02-13 21:44:18 +00:00
Matt Aitken 38f32f405f Progress on action api endpoint 2023-02-13 18:24:36 +00:00
Matt Aitken 76a2c82d9f Fixes for CommonJS in express and tests failing because of access token 2023-02-13 17:59:17 +00:00
Matt Aitken 4d6963f0e7 Added target node16 to tsup 2023-02-13 16:37:41 +00:00
Matt Aitken 73fb0f3c0d Added explicit returns types because tsup was complaining 2023-02-13 16:37:26 +00:00
Matt Aitken 9751531cc1 Watch mode working properly with the action endpoint 2023-02-13 16:14:43 +00:00
Matt Aitken 313a8c59cf First commit of WIP generated Slack SDK 2023-02-13 16:14:31 +00:00
Matt Aitken 76c660f167 eslint ignore build folder 2023-02-13 16:10:04 +00:00
Matt Aitken 3d5bbf899e Got integrations express server up and running 2023-02-13 15:54:36 +00:00
Matt Aitken 62ab7e098d Generate integrations in the correct folder 2023-02-13 15:03:02 +00:00
Matt Aitken 63486d21c4 Renamed command to generate-integrations 2023-02-13 14:57:09 +00:00
Matt Aitken fbdc3c799d Created the generated-integrations folder with a Readme 2023-02-13 14:56:55 +00:00
Matt Aitken fc1dd44f39 Added the new integrations system into the mono-repo 2023-02-13 14:48:12 +00:00
Matt Aitken dee2db231a Merge pull request #57 from triggerdotdev/dev
Workflow run created event now has the `environmentType` property
2023-02-10 14:20:04 +00:00
Matt Aitken 8a73e559e8 Merge pull request #56 from triggerdotdev/dev
Fixed all linting issues, almost all unused imports or type imports
2023-02-10 10:25:11 +00:00
Matt Aitken 2a20546e7b Merge pull request #55 from triggerdotdev/dev
Added WebHook examples for first workflow tests
2023-02-10 10:22:45 +00:00
Matt Aitken 565aba4021 Merge pull request #54 from triggerdotdev/dev
Analytics updates (orgs, workflows and runs)
2023-02-09 17:50:50 +00:00
James Ritchie 05289d0cb1 Merge pull request #52 from triggerdotdev/dev
The side menu now only shows the workflows tab if you have 0 workflows
2023-02-09 15:06:22 +00:00
Eric Allam 5f3d985b64 Merge pull request #51 from triggerdotdev/dev
Very simple admin area to assist with user support
2023-02-08 10:18:28 +00:00
Eric Allam 29b4ba4c78 Merge pull request #50 from triggerdotdev/dev
Feb 7 2023 - Safer SDK and added log feedback
2023-02-07 15:17:42 +00:00
Eric Allam ccd89d08d9 Merge pull request #49 from triggerdotdev/dev
Feb 6 2023 Release
2023-02-06 15:45:07 +00:00
Dan bf57430b66 Merge pull request #41 from triggerdotdev/onboarding
Fixed button
2023-02-02 22:00:12 +00:00
Eric Allam ad70dbc5dd Read from the latest trigger messages when creating workflow trigger subscription 2023-02-02 15:13:03 +00:00
Eric Allam a5d8290663 New emoji and fixed the runs image 2023-02-01 14:11:33 +00:00
Eric Allam 4fa967b2b1 Merge pull request #37 from triggerdotdev/dev
Fix builds by locking turbo dep to 1.7.0
2023-02-01 13:48:29 +00:00
Eric Allam cd11246d9a Merge pull request #36 from triggerdotdev/dev
README and mobile updates
2023-02-01 11:37:09 +00:00
Eric Allam f377aba208 Merge pull request #35 from triggerdotdev/dev
Move to dual license
2023-01-31 14:33:33 +00:00
Eric Allam 55a9031526 Merge pull request #34 from triggerdotdev/dev
Create separate packages for each integration
2023-01-31 14:11:23 +00:00
328 changed files with 49301 additions and 1775 deletions
+8 -1
View File
@@ -7,5 +7,12 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["wss", "webapp"]
"ignore": [
"wss",
"webapp",
"integrations",
"emails",
"internal-cli",
"integration-catalog"
]
}
+1
View File
@@ -0,0 +1 @@
{"workspaceId":"63e5e42daf9a537ba8d9503c"}
+19 -1
View File
@@ -5,18 +5,36 @@
"version": "0.2.0",
"configurations": [
{
"command": "pnpm run dev --filter webapp",
"command": "pnpm run idev --filter webapp",
"name": "Run webapp",
"request": "launch",
"type": "node-terminal",
"cwd": "${workspaceFolder}"
},
{
"command": "pnpm run dev --filter integrations",
"name": "Run integrations",
"request": "launch",
"type": "node-terminal",
"cwd": "${workspaceFolder}"
},
{
"type": "chrome",
"request": "launch",
"name": "Chrome webapp",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/apps/webapp/app"
},
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal"
}
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"folders": [
{
"name": "trigger.dev",
"path": "../"
},
{
"name": "Webapp",
"path": "../apps/webapp"
},
{
"name": "Integrations",
"path": "../apps/integrations"
}
],
"settings": {
"vitest.commandLine": "pnpm exec vitest"
}
}
+10 -1
View File
@@ -31,8 +31,17 @@ windows:
panes:
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
commands:
- exec: pnpm run dev --filter webapp
- exec: pnpm run build --filter webapp
- exec: pnpm run i-dev --filter webapp
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/webapp
- title: integrations
layout:
split_direction: vertical
panes:
- cwd: /Users/eric/code/triggerdotdev/trigger.dev
commands:
- exec: pnpm run dev --filter integrations
- cwd: /Users/eric/code/triggerdotdev/trigger.dev/apps/integrations
- title: ngrok
layout:
split_direction: horizontal
+5 -2
View File
@@ -22,5 +22,8 @@ Based on the instructions [here](https://github.com/changesets/changesets/blob/m
## Pre-release instructions
- Switch into pre-release mode by running `pnpm run changeset:next`.
- Switch back into normal mode by running `pnpm run changeset:normal`.
1. Add changesets as usual `pnpm run changeset:add`
2. Switch to pre-release mode by running `pnpm run changeset:next`
3. Create version `pnpm run changeset:version`
4. Release `pnpm run changeset:release`
5. Switch back to normal mode by running `pnpm run changeset:normal`
+5 -3
View File
@@ -68,10 +68,11 @@ export PULSAR_CPP_DIR=/opt/homebrew/Cellar/libpulsar/3.1.0
If you are proxying the webapp according to step 2 then in `webapp/.env`, set the `APP_ORIGIN` to the `NGROK_SUBDOMAIN` provided to the `./scripts/proxy-webapp.sh` command, e.g. `APP_ORIGIN=https://dan-trigger-dev.eu.ngrok.io`
4. Start postgresql, pulsar, and pizzly server
4. Start postgresql, redis, pizzly server, followed by pulsar standalone
```bash
pnpm run docker:db
pnpm run docker:services
pnpm run docker:pulsar
```
> **Note:** The npm script will complete while Docker sets up the container in the background. Ensure that Docker has finished and your container is running before proceeding.
@@ -173,7 +174,8 @@ pnpm install
1. Ensure the docker containers are running
```bash
pnpm run docker:db
pnpm run docker:services
pnpm run docker:pulsar
```
2. Run the webapp
+1 -2
View File
@@ -9,7 +9,7 @@
</div>
# **⚡️ Trigger.dev**
# ** Trigger.dev**
### **The developer-first open source Zapier alternative.**
@@ -174,7 +174,6 @@ One of the most powerful features of Trigger.dev is the [runs page](https://docs
![Viewing runs](https://imagedelivery.net/3TbraffuDZ4aEf8KWOmI_w/7343f3b2-2342-493b-5007-78f60ba5b700/public)
&nbsp;
# **🏠 Running Trigger.dev locally:**
+21
View File
@@ -0,0 +1,21 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"env": {
"node": true,
"commonjs": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"root": true,
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"no-console": "off"
},
"ignorePatterns": ["**/dist/*.js"]
}
+105
View File
@@ -0,0 +1,105 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
.DS_Store
+1
View File
@@ -0,0 +1 @@
{ "workspaceId": "63ea6cff29733e58db1e6b80" }
+4
View File
@@ -0,0 +1,4 @@
node_modules
/build
.env
+55
View File
@@ -0,0 +1,55 @@
FROM node:16-bullseye AS pruner
RUN apt-get update && apt-get install openssl -y
WORKDIR /app
RUN npm install turbo@1.7.0 -g
COPY . .
RUN turbo prune --scope=integrations --docker
RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
# Base strategy to have layer caching
FROM node:16-bullseye AS base
RUN apt-get update && apt-get install openssl -y
WORKDIR /app
COPY .gitignore .gitignore
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
FROM base AS production-deps
WORKDIR /app
RUN npm install turbo@1.7.0 -g
RUN corepack enable
ENV NODE_ENV production
COPY --from=pruner /app/out/full/apps/integrations/prisma/schema.prisma /app/apps/integrations/prisma/schema.prisma
RUN pnpm install --prod --frozen-lockfile
RUN pnpx prisma generate --schema /app/apps/integrations/prisma/schema.prisma
FROM base AS builder
WORKDIR /app
RUN npm install turbo@1.7.0 -g
COPY turbo.json turbo.json
RUN corepack enable
COPY --from=pruner /app/out/full/ .
ENV NODE_ENV development
RUN pnpm install --ignore-scripts --frozen-lockfile
ENV NODE_ENV production
RUN pnpm run generate
RUN pnpm run build --filter=integrations...
RUN pnpx prisma migrate deploy --schema apps/integrations/prisma/schema.prisma
# Runner
FROM node:16-bullseye AS runner
RUN apt-get update && apt-get install openssl -y
WORKDIR /app
RUN corepack enable
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 expressjs
RUN chown -R expressjs:nodejs /app
USER expressjs
COPY --from=pruner --chown=expressjs:nodejs /app/out/full/ .
COPY --from=production-deps --chown=expressjs:nodejs /app .
COPY --from=builder --chown=expressjs:nodejs /app/apps/integrations/dist/index.js ./apps/integrations/dist/index.js
ENTRYPOINT ["pnpm", "--filter", "integrations", "run", "start"]
+2
View File
@@ -0,0 +1,2 @@
# trigger-integrations
You can create API integrations using this data and system
+63
View File
@@ -0,0 +1,63 @@
{
"private": true,
"name": "integrations",
"version": "1.0.0",
"description": "API integrations in a format that can be used to generate clients",
"main": "./dist/index.js",
"scripts": {
"build": "tsup",
"start": "node dist/index.js",
"dev": "concurrently \"tsup --watch\" \"nodemon -r dotenv/config -q dist/index.js\"",
"test": "vitest",
"lint": "eslint --no-error-on-unmatched-pattern src/**",
"lint-fix": "eslint --no-error-on-unmatched-pattern --fix src/**",
"generate-integrations": "tsx src/trigger/sdk/generate.ts",
"generate": "prisma generate",
"db:migrate:deploy": "prisma migrate deploy",
"db:migrate:dev": "prisma migrate dev"
},
"dependencies": {
"@cfworker/json-schema": "^1.12.5",
"@prisma/client": "^4.3.0",
"express": "^4.18.1",
"express-async-errors": "^3.1.1",
"json-pointer": "^0.6.2",
"json-schema-deref-sync": "^0.14.0",
"json-schema-to-typescript": "^11.0.3",
"loglevel": "^1.8.1",
"morgan": "^1.10.0",
"node-fetch": "^3.3.0",
"zod": "^3.20.2"
},
"devDependencies": {
"@types/eslint": "^8.4.6",
"@types/express": "^4.17.13",
"@types/json-pointer": "^1.0.31",
"@types/morgan": "^1.9.3",
"@types/node": "^18.13.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"concurrently": "^7.6.0",
"dotenv": "^16.0.3",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard-with-typescript": "^34.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.6.1",
"eslint-plugin-promise": "^6.1.1",
"nock": "^13.3.0",
"nodemon": "^2.0.19",
"prisma": "^4.3.0",
"rimraf": "^4.1.2",
"tiny-invariant": "^1.2.0",
"ts-morph": "^17.0.1",
"tsup": "^6.6.2",
"tsx": "^3.12.3",
"typescript": "^4.9.5",
"vite": "^4.1.1",
"vite-tsconfig-paths": "^4.0.5",
"vitest": "^0.28.4"
},
"engines": {
"node": ">=16.0.0"
}
}
@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "Cache" (
"id" TEXT NOT NULL,
"namespace" TEXT NOT NULL,
"key" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Cache_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Cache_key_key" ON "Cache"("key");
-- CreateIndex
CREATE INDEX "Cache_namespace_key_idx" ON "Cache"("namespace", "key");
-- CreateIndex
CREATE UNIQUE INDEX "Cache_namespace_key_key" ON "Cache"("namespace", "key");
@@ -0,0 +1,5 @@
-- DropIndex
DROP INDEX "Cache_key_key";
-- DropIndex
DROP INDEX "Cache_namespace_key_idx";
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Cache" ALTER COLUMN "expiresAt" DROP NOT NULL;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
+22
View File
@@ -0,0 +1,22 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
output = "../node_modules/.prisma/client"
binaryTargets = ["native", "debian-openssl-1.1.x"]
previewFeatures = ["orderByNulls"]
}
model Cache {
id String @id @default(cuid())
namespace String
key String
value String
expiresAt DateTime?
createdAt DateTime @default(now())
@@unique([namespace, key])
}
@@ -0,0 +1,35 @@
import { InputSpec } from "core/action/types";
export function createParametersBody(
inputSpec: InputSpec,
params?: Record<string | number, any> | undefined
) {
//separate the parameters and body by looking at the spec and pulling properties out
let parameters: Record<string, any> | undefined = undefined;
let body: any = undefined;
if (params) {
inputSpec.parameters?.forEach((p) => {
if (!parameters) {
parameters = {};
}
parameters[p.name] = params[p.name];
});
const bodyProperties = inputSpec.body?.properties;
if (bodyProperties) {
body = {};
Object.keys(bodyProperties).forEach((name) => {
const value = params?.[name];
if (value !== undefined) {
body[name] = value;
}
});
}
}
return {
parameters,
body,
};
}
@@ -0,0 +1,50 @@
import { DisplayProperties } from "core/action/types";
import { Request, Response } from "express";
import { catalog } from "integrations/catalog";
import { z } from "zod";
import { createParametersBody } from "./createParametersBody";
import { getServiceAction } from "./validation";
const requestBodySchema = z.object({
params: z.record(z.string().or(z.number()), z.any()).optional(),
});
export async function handleActionDisplay(req: Request, res: Response) {
const serviceActionResult = getServiceAction(req.params);
if (!serviceActionResult.success) {
res
.status(404)
.send(
JSON.stringify({ success: false, error: serviceActionResult.error })
);
return;
}
const { service, action } = serviceActionResult;
const parsedRequestBody = requestBodySchema.safeParse(req.body);
if (!parsedRequestBody.success) {
res.status(400).send(
JSON.stringify({
success: false,
error: {
type: "invalid_body",
message: "Action not found",
service,
action,
issues: parsedRequestBody.error.issues,
},
})
);
return;
}
const requestData = createParametersBody(
action.spec.input,
parsedRequestBody.data.params
);
const displayProperties = await action.displayProperties(requestData);
res.send(JSON.stringify({ success: true, properties: displayProperties }));
}
@@ -0,0 +1,148 @@
import { PostgresCacheService } from "cache/postgresCache";
import { AuthCredentials } from "core/authentication/types";
import { Service } from "core/service/types";
import { validateInputs } from "core/validation/inputs";
import { Request, Response } from "express";
import { z } from "zod";
import { error, ReturnResponse } from "../requestUtilities";
import { createParametersBody } from "./createParametersBody";
import { getServiceAction } from "./validation";
const requestBodySchema = z.object({
credentials: z.object({ accessToken: z.string() }).optional(),
params: z.record(z.string().or(z.number()), z.any()).optional(),
metadata: z.object({
requestId: z.string(),
workflowId: z.string(),
connectionId: z.string(),
}),
});
export async function handleAction(req: Request, res: Response) {
const serviceActionResult = getServiceAction(req.params);
if (!serviceActionResult.success) {
res
.status(404)
.send(JSON.stringify(error(404, false, serviceActionResult.error)));
return;
}
const { service, action } = serviceActionResult;
const parsedRequestBody = requestBodySchema.safeParse(req.body);
if (!parsedRequestBody.success) {
res.status(400).send(
JSON.stringify(
error(400, false, {
type: "invalid_body",
message: "Action not found",
service,
action,
issues: parsedRequestBody.error.issues,
})
)
);
return;
}
//for v1 of this API we're building the credentials from the action
//this is fine for now but we'll want to use the connection in future to cover complex cases
let credentials: AuthCredentials | undefined = undefined;
if (parsedRequestBody.data.credentials && action.spec.input.security) {
const firstSecurityMethod = Object.entries(action.spec.input.security)[0];
if (firstSecurityMethod) {
const [name, scopes] = firstSecurityMethod;
//get the full info from the service
const securityMethod = service.authentication[name];
switch (securityMethod.type) {
case "oauth2":
credentials = {
type: "oauth2",
name,
accessToken: parsedRequestBody.data.credentials.accessToken,
scopes,
};
break;
case "api_key":
credentials = {
type: "api_key",
name,
api_key: parsedRequestBody.data.credentials.accessToken,
scopes,
};
break;
default:
throw new Error(
`Not implemented credentials for: ${JSON.stringify(securityMethod)}`
);
}
}
}
const { parameters, body } = createParametersBody(
action.spec.input,
parsedRequestBody.data.params
);
const inputValidationResult = await validateInputs(action.spec.input, {
parameters,
body,
credentials,
});
if (!inputValidationResult.success) {
res
.status(400)
.send(JSON.stringify(error(400, false, inputValidationResult.error)));
return;
}
const { metadata } = parsedRequestBody.data;
const cache = new PostgresCacheService(`${metadata.connectionId}-${service}`);
try {
const data = await action.action(
{ credentials, parameters, body },
cache,
metadata
);
//convert into the format for the webapp
const response: ReturnResponse = {
ok: true,
isRetryable: isRetryable(service, data.status),
response: {
output: data.body ?? {},
context: {
statusCode: data.status,
headers: data.headers,
},
},
};
res.send(JSON.stringify(response));
} catch (e: any) {
console.error(e);
if (e instanceof Error) {
res
.status(500)
.send(JSON.stringify(error(500, false, { error: JSON.stringify(e) })));
return;
}
if ("error" in e) {
res.status(500).send(JSON.stringify(error(500, false, e.error)));
return;
}
res
.status(500)
.send(JSON.stringify(error(500, false, { error: JSON.stringify(e) })));
}
}
function isRetryable(service: Service, status: number): boolean {
return service.retryableStatusCodes.includes(status);
}
@@ -0,0 +1,50 @@
import { Action } from "core/action/types";
import { Service } from "core/service/types";
import { Request } from "express";
import { catalog } from "integrations/catalog";
type ServiceActionResult =
| {
success: true;
service: Service;
action: Action;
}
| {
success: false;
error: Record<string, any>;
};
export function getServiceAction(
params: Request["params"]
): ServiceActionResult {
const { service, action } = params;
const matchingService = Object.values(catalog.services).find(
(s) => s.service === service
);
if (!matchingService) {
return {
success: false,
error: { type: "missing_service", message: "Service not found", service },
};
}
const matchingAction = Object.values(matchingService.actions).find(
(a) => a.name === action
);
if (!matchingAction) {
return {
success: false,
error: {
type: "missing_action",
message: "Action not found",
service,
action,
},
};
}
return { success: true, service: matchingService, action: matchingAction };
}
+116
View File
@@ -0,0 +1,116 @@
import { AuthCredentials } from "core/authentication/types";
import { HTTPMethodSchema } from "core/endpoint/types";
import { serviceFetch } from "core/fetch/serviceFetch";
import { Request, Response } from "express";
import { catalog } from "integrations/catalog";
import { z } from "zod";
import { error } from "./requestUtilities";
const requestBodySchema = z.object({
credentials: z.object({ accessToken: z.string() }).optional(),
path: z.string(),
method: HTTPMethodSchema,
headers: z.record(z.string(), z.string()).optional(),
body: z.any(),
});
type ReturnResponse = {
response: NormalizedResponse;
isRetryable: boolean;
ok: boolean;
};
type NormalizedResponse = {
output: NonNullable<any>;
context: any;
};
export async function handleAction(req: Request, res: Response) {
const { service } = req.params;
const matchingService = Object.values(catalog.services).find(
(s) => s.service === service
);
if (!matchingService) {
return {
success: false,
error: { type: "missing_service", message: "Service not found", service },
};
}
const parsedRequestBody = requestBodySchema.safeParse(req.body);
if (!parsedRequestBody.success) {
res.status(400).send(
JSON.stringify(
error(400, false, {
type: "invalid_body",
message: "Invalid input",
issues: parsedRequestBody.error.issues,
})
)
);
return;
}
//v1 build credentials from the service authentication info
let credentials: AuthCredentials | undefined = undefined;
if (parsedRequestBody.data.credentials) {
//just get the first security method
const [name, securityMethod] = Object.entries(
matchingService.authentication
)[0];
switch (securityMethod.type) {
case "oauth2":
credentials = {
type: "oauth2",
name,
accessToken: parsedRequestBody.data.credentials.accessToken,
scopes: Object.keys(securityMethod.scopes),
};
break;
case "api_key":
credentials = {
type: "api_key",
name,
api_key: parsedRequestBody.data.credentials.accessToken,
scopes: Object.keys(securityMethod.scopes),
};
break;
default:
throw new Error(
`Not implemented credentials for: ${JSON.stringify(securityMethod)}`
);
}
}
const url = matchingService.baseUrl + parsedRequestBody.data.path;
const data = await serviceFetch({
url,
method: parsedRequestBody.data.method,
headers: parsedRequestBody.data.headers,
body: parsedRequestBody.data.body,
authentication: matchingService.authentication,
credentials,
});
const response: ReturnResponse = {
ok: true,
isRetryable: isRetryable(data.status),
response: {
output: data.body ?? {},
context: {
statusCode: data.status,
headers: data.headers,
},
},
};
res.send(JSON.stringify(response));
}
function isRetryable(statusCode: number) {
return [408, 429, 500, 502, 503, 504].includes(statusCode);
}
@@ -0,0 +1,29 @@
export type ReturnResponse = {
response: NormalizedResponse;
isRetryable: boolean;
ok: boolean;
};
export type NormalizedResponse = {
output: NonNullable<any>;
context: any;
};
export function error(
status: number,
isRetryable: boolean,
error: Record<string, any>
): ReturnResponse {
const response: ReturnResponse = {
ok: false,
isRetryable,
response: {
output: error,
context: {
statusCode: status,
headers: {},
},
},
};
return response;
}
+36
View File
@@ -0,0 +1,36 @@
import { IntegrationAuthentication } from "core/authentication/types";
import { Service } from "core/service/types";
import { Request, Response } from "express";
import { catalog } from "integrations/catalog";
type ServiceMetadata = {
name: string;
service: string;
version: string;
icon: string;
live: boolean;
authentication: IntegrationAuthentication;
};
export async function handleServices(req: Request, res: Response) {
const servicesMetadata: Record<string, ServiceMetadata> = {};
Object.entries(catalog.services).forEach(([key, service]) => {
if (service.live === false) return;
const metadata = omitExtraInfo(service);
servicesMetadata[key] = {
...metadata,
icon: `/integrations/${metadata.service}.png`,
};
});
res.send(
JSON.stringify({
services: servicesMetadata,
})
);
}
function omitExtraInfo(service: Service): ServiceMetadata {
const { actions, retryableStatusCodes, ...rest } = service;
return rest;
}
+58
View File
@@ -0,0 +1,58 @@
import { PrismaClient, Prisma } from ".prisma/client";
import invariant from "tiny-invariant";
export { Prisma };
let prisma: PrismaClient;
declare global {
// eslint-disable-next-line no-var
var __db__: PrismaClient;
}
// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
// in production we'll have a single connection to the DB.
if (process.env.NODE_ENV === "production") {
prisma = getClient();
} else {
if (!global.__db__) {
global.__db__ = getClient();
}
prisma = global.__db__;
}
function getClient() {
const { DATABASE_URL } = process.env;
invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set");
// Remove the username:password in the url and print that to the console
const urlWithoutCredentials = new URL(DATABASE_URL);
urlWithoutCredentials.password = "";
console.log(
`1. 🔌 setting up prisma client to ${urlWithoutCredentials.toString()}`
);
const client = new PrismaClient({
datasources: {
db: {
url: DATABASE_URL,
},
},
log: ["warn", "error"],
});
console.log(`2.0 🔌 prisma client connecting`);
// connect eagerly
client.$connect();
console.log(`3.0 🔌 prisma client connected`);
return client;
}
export { prisma };
export type { PrismaClient } from ".prisma/client";
+53
View File
@@ -0,0 +1,53 @@
import type { Prisma, Cache } from ".prisma/client";
import { prisma } from "./db.server";
import { CacheService } from "core/cache/types";
export class PostgresCacheService implements CacheService {
constructor(private readonly namespace: string) {}
async get(key: string) {
const cachedRow = await prisma.cache.findFirst({
where: {
namespace: this.namespace,
key,
OR: [
{ expiresAt: null },
{
expiresAt: {
gt: new Date(),
},
},
],
},
});
if (cachedRow) {
return cachedRow.value;
}
return null;
}
async set(key: string, value: string, ttl?: number): Promise<void> {
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
await prisma.cache.upsert({
where: {
namespace_key: {
namespace: this.namespace,
key,
},
},
update: {
value,
expiresAt,
},
create: {
namespace: this.namespace,
key,
value,
expiresAt,
},
});
}
}
@@ -0,0 +1,27 @@
import { EndpointSpecMetadata } from "core/endpoint/types";
import { RequestData } from "core/request/types";
export async function getDisplayProperties(
data: RequestData,
displayProperties: EndpointSpecMetadata["displayProperties"]
) {
return {
title: interpolateString(displayProperties.title, {
parameters: data.parameters,
body: data.body,
}),
};
}
function interpolateString(
template: string,
data: Record<string | number, any>
) {
return template.replace(/\${([^}]+)}/g, (_, key) => {
return key
.split(".")
.reduce((obj: Record<string | number, any>, prop: string | number) => {
return obj[prop];
}, data);
});
}
@@ -0,0 +1,74 @@
import { CacheService } from "core/cache/types";
import { Endpoint } from "core/endpoint/types";
import { RequestData, RequestResponse } from "core/request/types";
import { getDisplayProperties } from "./getDisplayProperties";
import { Action, Metadata } from "./types";
import { makeInputSpec, makeOutputSpec } from "./utilities";
/** Create an action where you specify the spec and action */
export const makeAdvancedAction = ({
endpoint,
spec,
action,
}: {
endpoint: Endpoint;
spec: Action["spec"];
action: (
data: RequestData,
cache?: CacheService,
metadata?: Metadata
) => Promise<RequestResponse>;
}) => {
const displayProperties = async (data: RequestData) => {
return getDisplayProperties(
data,
endpoint.spec.endpointSpec.metadata.displayProperties
);
};
return {
name: endpoint.spec.endpointSpec.metadata.name,
description: endpoint.spec.endpointSpec.metadata.description,
path: endpoint.spec.endpointSpec.path,
method: endpoint.spec.endpointSpec.method,
spec,
action,
displayProperties,
};
};
/** Creates an action that calls an endpoint */
export const makeSimpleAction = (endpoint: Endpoint) => {
const action = async (
data: RequestData,
cache?: CacheService,
metadata?: Metadata
) => {
//a simple request doesn't use the cache or metadata
return await endpoint.request(data);
};
return makeAdvancedAction({
endpoint,
spec: {
input: makeInputSpec(endpoint),
output: makeOutputSpec(endpoint),
},
action,
});
};
export const makeSimpleActions = <
TEndpoints extends Record<string, Endpoint>,
K extends keyof TEndpoints
>(
endpoints: TEndpoints
): Record<K, Action> => {
const actions: any = {};
Object.entries(endpoints).forEach(([name, endpoint]) => {
actions[name as K] = makeSimpleAction(endpoint);
});
return actions;
};
@@ -0,0 +1,42 @@
import { CacheService } from "core/cache/types";
import { EndpointSpec, HTTPMethod } from "core/endpoint/types";
import { RequestData, RequestResponse } from "core/request/types";
export type InputSpec = {
security?: EndpointSpec["security"];
parameters?: EndpointSpec["parameters"];
body?: NonNullable<EndpointSpec["request"]["body"]>["schema"];
};
export type OutputSpec = {
responses: EndpointSpec["responses"];
};
export type Metadata = Record<string, any>;
export type DisplayProperties = {
title: string;
};
export type Action = {
name: string;
description: string;
path: string;
method: HTTPMethod;
spec: {
input: InputSpec;
output: OutputSpec;
};
action: (
/** The data to be sent to the endpoint */
data: RequestData,
/** The cache service to use for caching */
cache?: CacheService,
/** Additional metadata that can be used to modify the request */
metadata?: Metadata
) => Promise<RequestResponse>;
displayProperties: (
/** The data to be sent to the endpoint */
data: RequestData
) => Promise<DisplayProperties>;
};
@@ -0,0 +1,49 @@
import { Endpoint } from "core/endpoint/types";
import { InputSpec, OutputSpec } from "./types";
export function makeInputSpec(endpoint: Endpoint): InputSpec {
return {
security: endpoint.spec.endpointSpec.security,
parameters: endpoint.spec.endpointSpec.parameters,
body: endpoint.spec.endpointSpec.request.body?.schema,
};
}
export function makeOutputSpec(endpoint: Endpoint): OutputSpec {
return {
responses: endpoint.spec.endpointSpec.responses,
};
}
export function combineSecurityScopes(
securities: (Record<string, string[]> | undefined)[]
): Record<string, string[]> | undefined {
const securityA = securities[0];
const securityB = securities[1];
//where a key already exists, concatenate the scope arrays together
//where a key does not exist, add it to the object
let combined: Record<string, string[]> = {};
if (securityA) {
combined = {
...securityA,
};
}
if (securityB) {
for (const key in securityB) {
if (combined[key]) {
combined[key] = [...combined[key], ...securityB[key]];
} else {
combined[key] = securityB[key];
}
}
}
if (securities.length > 2) {
return combineSecurityScopes([combined, ...securities.slice(2)]);
}
return combined;
}
@@ -0,0 +1,82 @@
import { EndpointSpec } from "core/endpoint/types";
import { FetchConfig } from "core/request/types";
import { expect, test } from "vitest";
import { checkRequiredScopes, applyCredentials } from "./credentials";
import { AuthCredentials, IntegrationAuthentication } from "./types";
test("Required scopes present", async () => {
const credentials: AuthCredentials = {
type: "oauth2",
name: "authName",
accessToken: "token",
scopes: ["scope1", "scope2"],
};
const requiredScopes = ["scope1", "scope2"];
const result = checkRequiredScopes(requiredScopes, credentials);
expect(result.success).toEqual(true);
});
test("Required scopes missing", async () => {
const credentials: AuthCredentials = {
type: "oauth2",
name: "authName",
accessToken: "token",
scopes: ["scope1", "scope2"],
};
const requiredScopes = ["scope1", "scope2", "scope3"];
const result = checkRequiredScopes(requiredScopes, credentials);
expect(result.success).toEqual(false);
if (result.success) throw new Error("Should not be success");
expect(result.missingScopes).toEqual(["scope3"]);
});
test("Applied credentials", async () => {
const credentials: AuthCredentials = {
type: "oauth2",
name: "authName",
accessToken: "123456",
scopes: ["scope1", "scope2"],
};
const endpointSecurity: EndpointSpec["security"] = {
authName: ["scope1", "scope2"],
};
const integrationAuthentication: IntegrationAuthentication = {
authName: {
type: "oauth2",
placement: {
in: "header",
type: "bearer",
key: "Authorization",
},
authorizationUrl: "https://example.com",
tokenUrl: "https://example.com",
flow: "accessCode",
scopes: {
scope1: "scope1",
scope2: "scope2",
},
},
};
const existingFetch: FetchConfig = {
url: "https://example.com",
method: "GET",
headers: {
"Content-Type": "application/json",
},
};
const fetchConfig = applyCredentials(existingFetch, {
endpointSecurity,
authentication: integrationAuthentication,
credentials,
});
expect(fetchConfig.headers.Authorization).toEqual("Bearer 123456");
expect(fetchConfig.headers["Content-Type"]).toEqual("application/json");
});
@@ -0,0 +1,107 @@
import { EndpointSpec } from "core/endpoint/types";
import { InsufficientScopesError } from "core/request/errors";
import { FetchConfig } from "core/request/types";
import { IntegrationAuthentication, AuthCredentials } from "./types";
/** Apply the given credentials to the given fetch config */
export function applyCredentials(
fetch: FetchConfig,
{
endpointSecurity,
authentication,
credentials,
}: {
endpointSecurity: EndpointSpec["security"];
authentication: IntegrationAuthentication;
credentials: AuthCredentials;
}
): FetchConfig {
if (endpointSecurity === undefined) return fetch;
// check if the credentials have the required scopes
const requiredScopes = endpointSecurity[credentials.name] ?? [];
const scopesCheckResult = checkRequiredScopes(requiredScopes, credentials);
if (!scopesCheckResult.success) {
const error: InsufficientScopesError = {
type: "insufficient_scopes",
missingScopes: scopesCheckResult.missingScopes,
};
throw error;
}
return addCredentialsToConfig(fetch, { authentication, credentials });
}
export function addCredentialsToConfig(
fetch: FetchConfig,
{
authentication,
credentials,
}: {
authentication: IntegrationAuthentication;
credentials: AuthCredentials;
}
) {
// apply the credentials
switch (credentials.type) {
case "oauth2": {
const authConfig = authentication[credentials.name];
switch (authConfig.placement.in) {
case "header": {
fetch.headers[
authConfig.placement.key
] = `Bearer ${credentials.accessToken}`;
return fetch;
}
}
break;
}
case "api_key": {
const authConfig = authentication[credentials.name];
if (authConfig.placement.in === "header") {
let value = "";
switch (authConfig.placement.type) {
case "basic":
value = `Basic ${credentials.api_key}`;
break;
case "bearer":
value = `Bearer ${credentials.api_key}`;
break;
}
fetch.headers[authConfig.placement.key] = value;
}
return fetch;
}
}
throw new Error("Invalid credentials");
}
type ScopesCheckResult =
| {
success: true;
}
| {
success: false;
missingScopes: string[];
};
/** Check if the given credentials have the required scopes */
export function checkRequiredScopes(
requiredScopes: string[],
credentials: AuthCredentials
): ScopesCheckResult {
const missingScopes = requiredScopes.filter(
(scope) => !credentials.scopes.includes(scope)
);
if (missingScopes.length > 0) {
return {
success: false,
missingScopes,
};
}
return {
success: true,
};
}
@@ -0,0 +1,61 @@
import { z } from "zod";
export type IntegrationAuthentication = Record<
string,
AuthenticationDefinition
>;
type AuthenticationDefinition = OAuth2Authentication | APIKeyAuthentication;
type OAuth2Authentication = {
type: "oauth2";
placement: AuthenticationPlacement;
authorizationUrl: string;
tokenUrl: string;
flow: "accessCode" | "implicit" | "password" | "application";
scopes: Record<string, string>;
};
type APIKeyAuthentication = {
type: "api_key";
placement: AuthenticationPlacement;
documentation: string;
scopes: Record<string, string>;
additionalFields?: {
key: string;
fieldType: "text";
name: string;
placeholder?: string;
description: string;
}[];
};
type AuthenticationPlacement = HeaderAuthentication;
interface HeaderAuthentication {
in: "header";
type: "basic" | "bearer";
key: string;
}
const OAuth2CredentialsSchema = z.object({
type: z.literal("oauth2"),
name: z.string(),
accessToken: z.string(),
scopes: z.array(z.string()),
});
const APIKeyCredentialsSchema = z.object({
type: z.literal("api_key"),
name: z.string(),
api_key: z.string(),
additionalFields: z.record(z.string(), z.string()).optional(),
scopes: z.array(z.string()),
});
export const AuthCredentialsSchema = z.discriminatedUnion("type", [
OAuth2CredentialsSchema,
APIKeyCredentialsSchema,
]);
export type AuthCredentials = z.infer<typeof AuthCredentialsSchema>;
+4
View File
@@ -0,0 +1,4 @@
export interface CacheService {
get: (key: string) => Promise<string | null>;
set: (key: string, value: string, ttl?: number) => Promise<void>;
}
+5
View File
@@ -0,0 +1,5 @@
import { Service } from "./service/types";
export type Catalog = {
services: Record<string, Service>;
};
@@ -0,0 +1,36 @@
import { IntegrationAuthentication } from "core/authentication/types";
import { requestEndpoint } from "core/request/requestEndpoint";
import { RequestData, RequestResponse, RequestSpec } from "core/request/types";
import { Endpoint, EndpointSpec } from "./types";
export const makeEndpoint = (spec: RequestSpec) => {
const request = async (data: RequestData) => {
return await requestEndpoint(spec, data);
};
return {
spec,
request,
};
};
export const makeEndpoints = <
TSpecs extends Record<string, EndpointSpec>,
K extends keyof TSpecs
>(
baseUrl: string,
authentication: IntegrationAuthentication,
specs: TSpecs
): Record<K, Endpoint> => {
const endpoints: any = {};
Object.entries(specs).forEach(([name, spec]) => {
endpoints[name as K] = makeEndpoint({
baseUrl,
endpointSpec: spec,
authentication,
});
});
return endpoints;
};
@@ -0,0 +1,71 @@
import { RequestData, RequestResponse, RequestSpec } from "core/request/types";
import { JSONSchema } from "core/schemas/types";
import { z } from "zod";
export type Endpoint = {
spec: RequestSpec;
request: (data: RequestData) => Promise<RequestResponse>;
};
export const HTTPMethodSchema = z.union([
z.literal("GET"),
z.literal("POST"),
z.literal("PUT"),
z.literal("PATCH"),
z.literal("DELETE"),
z.literal("HEAD"),
z.literal("OPTIONS"),
z.literal("TRACE"),
]);
export type HTTPMethod = z.infer<typeof HTTPMethodSchema>;
export interface EndpointSpec {
path: string;
method: HTTPMethod;
metadata: EndpointSpecMetadata;
parameters?: EndpointSpecParameter[];
security?: Record<string, string[]>;
request: EndpointSpecRequest;
responses: { default: EndpointSpecResponse[] } & Record<
string,
EndpointSpecResponse[]
>;
}
export interface EndpointSpecParameter {
name: string;
description: string;
in: "query" | "path" | "header";
required?: boolean;
schema: JSONSchema;
}
interface EndpointSpecRequest {
headers?: Record<string, string>;
body?: {
schema: JSONSchema;
};
}
export interface EndpointSpecResponse {
success: boolean;
name: string;
description?: string;
schema?: JSONSchema;
}
export interface EndpointSpecMetadata {
name: string;
description: string;
displayProperties: {
title: string;
};
externalDocs?: ExternalDocs;
tags: string[];
}
interface ExternalDocs {
description: string;
url: string;
}
@@ -0,0 +1,43 @@
import { RequestResponse } from "core/request/types";
import { type Response } from "node-fetch";
export async function getFetch() {
return (await import("node-fetch")).default;
}
export async function safeGetJson(response: Response) {
try {
return await response.json();
} catch (error) {
return undefined;
}
}
export function normalizeHeaders(headers: Headers): Record<string, string> {
const normalizedHeaders: Record<string, string> = {};
headers.forEach((value, key) => {
normalizedHeaders[key.toLowerCase()] = value;
});
return normalizedHeaders;
}
export function responseFromCaughtError(error: any): RequestResponse {
if (error instanceof Error) {
return {
success: false,
status: 400,
headers: {},
body: { error: error.message },
};
}
//convert the error into JSON
return {
success: false,
status: 400,
headers: {},
body: { error: JSON.stringify(error) },
};
}
@@ -0,0 +1,29 @@
import { authentication } from "integrations/airtable/authentication";
import { startNock, stopNock } from "testing/nock";
import { describe, expect, test } from "vitest";
import { serviceFetch } from "./serviceFetch";
const authToken = () => process.env.AIRTABLE_TOKEN ?? "";
describe("serviceFetch", async () => {
test("simple GET", async () => {
const accessToken = authToken();
const nockDone = await startNock("serviceFetch.simple.get");
const response = await serviceFetch({
url: "https://api.airtable.com/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m/recHcnB1MbBr9Rd2P",
method: "GET",
authentication: authentication,
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:read"],
},
});
expect(response.status).toEqual(200);
expect(response.success).toEqual(true);
expect(response.body).not.toBeNull();
stopNock(nockDone);
});
});
@@ -0,0 +1,73 @@
import { addCredentialsToConfig } from "core/authentication/credentials";
import {
AuthCredentials,
IntegrationAuthentication,
} from "core/authentication/types";
import { HTTPMethod } from "core/endpoint/types";
import { FetchConfig, RequestResponse } from "core/request/types";
import {
getFetch,
normalizeHeaders,
responseFromCaughtError,
safeGetJson,
} from "./fetchUtilities";
export type ServiceFetchOptions = {
url: string;
method: HTTPMethod;
headers?: Record<string, string>;
body?: any;
authentication: IntegrationAuthentication;
credentials?: AuthCredentials;
};
export async function serviceFetch({
url,
method = "GET",
headers,
body,
credentials,
authentication,
}: ServiceFetchOptions): Promise<RequestResponse> {
let fetchConfig: FetchConfig = {
url,
method,
headers: {
...headers,
},
body: JSON.stringify(body),
};
if (credentials == null) {
throw {
type: "missing_credentials",
};
}
fetchConfig = addCredentialsToConfig(fetchConfig, {
authentication,
credentials,
});
const fetchObject = {
method: fetchConfig.method,
headers: fetchConfig.headers,
body: fetchConfig.body,
};
try {
const fetch = await getFetch();
const response = await fetch(url, fetchObject);
const json = await safeGetJson(response);
return {
success: response.ok,
status: response.status,
headers: normalizeHeaders(response.headers),
body: json,
};
} catch (error: any) {
return responseFromCaughtError(error);
}
}
@@ -0,0 +1,66 @@
import { JSONSchemaError } from "core/schemas/types";
export type RequestError =
| RequestBodyInvalid
| ParameterMissing
| ParametersInvalid
| ExtraParametersError
| InsufficientScopesError
| MissingResponseSpec
| ResponseBodyInvalid
| BodyMissing
| MissingCredentialsError;
export interface RequestBodyInvalid {
type: "request_body_invalid";
errors: any[];
}
export interface BodyMissing {
type: "missing_body";
}
export interface ParameterMissing {
type: "missing_parameter";
parameter: {
name: string;
};
}
export interface ParametersInvalid {
type: "parameter_invalid";
parameter: {
name: string;
value: any;
};
errors: JSONSchemaError[];
}
export interface ExtraParametersError {
type: "extra_parameters";
parameters: Array<{
name: string;
value: any;
}>;
}
export interface MissingCredentialsError {
type: "missing_credentials";
}
export interface InsufficientScopesError {
type: "insufficient_scopes";
missingScopes: string[];
}
export interface MissingResponseSpec {
type: "no_response_spec";
status: number;
}
export interface ResponseBodyInvalid {
type: "response_invalid";
errors: Array<{ name: string; errors: JSONSchemaError[] }>;
status: number;
body?: any;
}
@@ -0,0 +1,209 @@
import { applyCredentials } from "core/authentication/credentials";
import { EndpointSpec, EndpointSpecResponse } from "core/endpoint/types";
import {
getFetch,
normalizeHeaders,
responseFromCaughtError,
safeGetJson,
} from "core/fetch/fetchUtilities";
import { JSONSchemaError } from "core/schemas/types";
import { validate } from "core/schemas/validate";
import {
FetchConfig,
RequestData,
RequestResponse,
RequestSpec,
} from "./types";
export async function requestEndpoint(
{ baseUrl, endpointSpec, authentication }: RequestSpec,
{ parameters, body, credentials }: RequestData
): Promise<RequestResponse> {
const { method, security, request, responses } = endpointSpec;
let path = endpointSpec.path;
// validate the request body
if (body == null && request.body?.schema != null) {
throw {
type: "missing_body",
};
}
const requestValid = await validate(body, request.body?.schema);
if (!requestValid.success) {
throw {
type: "request_body_invalid",
errors: requestValid.errors,
};
}
let headers: Record<string, string> = {};
// validate and add the parameters
if (endpointSpec.parameters != null) {
for (const parameter of endpointSpec.parameters) {
const { name, in: location, required } = parameter;
// if the parameter is missing
if (parameters === undefined || parameters[name] == null) {
if (required) {
throw {
type: "missing_parameter",
parameter: {
name,
},
};
} else {
continue;
}
}
const element = parameters[name];
//validate the parameter against the schema
const valid = await validate(element, parameter.schema);
if (!valid.success) {
throw {
type: "parameter_invalid",
parameter: {
name,
value: element,
},
errors: valid.errors,
};
}
//add the parameter
switch (location) {
case "path":
path = path.replace(`{${name}}`, element as string);
break;
case "query": {
if (parameter.schema?.type === "array") {
const array = element as Array<string>;
for (const item of array) {
path = `${path}${path.includes("?") ? "&" : "?"}${name}=${item}`;
}
break;
}
path = `${path}${path.includes("?") ? "&" : "?"}${name}=${element}`;
break;
}
case "header":
headers = {
...headers,
[name]: `${element}`,
};
break;
}
}
}
// add headers from the config
for (const name in request.headers) {
if (Object.prototype.hasOwnProperty.call(request.headers, name)) {
const element = request.headers[name];
headers = {
...headers,
[name]: element,
};
}
}
// build the fetch config
const url = `${baseUrl}${path}`;
let fetchConfig: FetchConfig = {
url,
method,
headers: {
...headers,
},
body: JSON.stringify(body),
};
// apply credentials
if (security != null) {
if (credentials == null) {
throw {
type: "missing_credentials",
};
}
fetchConfig = applyCredentials(fetchConfig, {
endpointSecurity: security,
authentication,
credentials,
});
}
// do the fetch and try get the JSON
const fetchObject = {
method: fetchConfig.method,
headers: fetchConfig.headers,
body: fetchConfig.body,
};
try {
const fetch = await getFetch();
const response = await fetch(fetchConfig.url, fetchObject);
const json = await safeGetJson(response);
// validate the response against the specs
const responseSpecs = getResponseSpecsForStatusCode(
response.status,
responses
);
if (!responseSpecs) {
throw {
type: "no_response_spec",
status: response.status,
};
}
// start with the first spec and loop through them, if one succeeds then return that
const specErrors: Array<{ name: string; errors: JSONSchemaError[] }> = [];
for (const spec of responseSpecs) {
const responseValid = await validate(json, spec.schema);
if (responseValid.success) {
return {
success: spec.success,
status: response.status,
headers: normalizeHeaders(response.headers),
body: json,
};
} else {
if (responseValid.errors != null) {
specErrors.push({ name: spec.name, errors: responseValid.errors });
}
}
}
throw {
type: "response_invalid",
status: response.status,
body: json,
errors: specErrors,
};
} catch (error: any) {
return responseFromCaughtError(error);
}
}
/** Get the appropriate endpoint response object based on the status code. It supports wild cards like 20x and 2xx */
function getResponseSpecsForStatusCode(
statusCode: number,
endpointResponseSpecs: EndpointSpec["responses"]
): EndpointSpecResponse[] {
let specs = endpointResponseSpecs[statusCode.toString()];
if (specs) return specs;
specs =
endpointResponseSpecs[
`${statusCode.toString().charAt(0)}${statusCode.toString().charAt(1)}x`
];
if (specs) return specs;
specs = endpointResponseSpecs[`${statusCode.toString().charAt(0)}xx`];
if (specs) return specs;
return endpointResponseSpecs.default;
}
@@ -0,0 +1,30 @@
import {
IntegrationAuthentication,
AuthCredentials,
} from "core/authentication/types";
import { HTTPMethod, EndpointSpec } from "core/endpoint/types";
export interface FetchConfig {
url: string;
method: HTTPMethod;
headers: Record<string, string>;
body?: any;
}
export interface RequestSpec {
baseUrl: string;
endpointSpec: EndpointSpec;
authentication: IntegrationAuthentication;
}
export interface RequestData {
parameters?: Record<string, any>;
body?: any;
credentials?: AuthCredentials;
}
export interface RequestResponse {
success: boolean;
status: number;
headers?: Record<string, string>;
body?: any;
}
@@ -0,0 +1 @@
declare module "json-schema-deref-sync";
@@ -0,0 +1,141 @@
import { JSONSchema } from "./types";
export function makeStringSchema(
title: string,
description?: string,
options?: {
defaultValue?: string;
enum?: string[];
}
): JSONSchema {
const schema: JSONSchema = {
type: "string",
title,
};
if (description) {
schema.description = description;
}
if (options?.defaultValue) {
schema.default = options.defaultValue;
}
if (options?.enum) {
schema.enum = options.enum;
}
return schema;
}
export function makeNumberSchema(
title: string,
description?: string,
options?: {
defaultValue: number;
}
): JSONSchema {
const schema: JSONSchema = {
type: "number",
title,
};
if (description) {
schema.description = description;
}
if (options?.defaultValue) {
schema.default = options.defaultValue;
}
return schema;
}
export function makeBooleanSchema(
title: string,
description?: string,
options?: {
defaultValue?: boolean;
enum?: boolean;
}
): JSONSchema {
const schema: JSONSchema = {
title,
type: "boolean",
};
if (description) {
schema.description = description;
}
if (options?.defaultValue) {
schema.default = options.defaultValue;
}
if (options?.enum) {
schema.enum = [options.enum];
}
return schema;
}
export function makeArraySchema(title: string, items: JSONSchema): JSONSchema {
return {
type: "array",
title,
items,
};
}
export function makeObjectSchema(
title: string,
options: {
optionalProperties?: Record<string, JSONSchema>;
requiredProperties?: Record<string, JSONSchema>;
additionalProperties?: boolean | JSONSchema;
}
): JSONSchema {
let properties: Record<string, JSONSchema> | undefined = undefined;
if (options.optionalProperties || options.requiredProperties) {
properties = {};
}
if (options.optionalProperties) {
properties = {
...properties,
...options.optionalProperties,
};
}
if (options.requiredProperties) {
properties = {
...properties,
...options.requiredProperties,
};
}
return {
type: "object",
title,
properties,
required: options.requiredProperties
? Object.keys(options.requiredProperties)
: undefined,
additionalProperties: options.additionalProperties,
};
}
export function makeOneOf(title: string, schemas: JSONSchema[]): JSONSchema {
return {
title,
oneOf: schemas,
};
}
export function makeAnyOf(title: string, schemas: JSONSchema[]): JSONSchema {
return {
title,
anyOf: schemas,
};
}
@@ -0,0 +1,13 @@
import { expect, test } from "vitest";
import spec from "./test-openapi-spec-v2.json";
import { dereferenceSpec, schemaFromOpenApiSpecV2 } from "./openApi";
test("Returns the correct schema", async () => {
const dereferenced = dereferenceSpec(spec);
const schema = schemaFromOpenApiSpecV2(
dereferenced,
"/paths//conversations.list/get/responses/200/schema"
);
expect(schema.type).toEqual("object");
expect(schema.additionalProperties).toEqual(false);
});
@@ -0,0 +1,15 @@
import deref from "json-schema-deref-sync";
import pointer from "json-pointer";
import { type JSONSchema } from "./types";
export function dereferenceSpec(spec: any): any {
return deref(spec);
}
// todo validate that it's a valid json schema
export function schemaFromOpenApiSpecV2(spec: any, path: string): JSONSchema {
// we need to escape the path because it contains ~ and / characters
path = path.replace(/~/g, "~0").replace(/\/\//g, "/~1");
const schema = pointer.get(spec as pointer.JsonObject, path);
return schema;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,4 @@
import { Schema, OutputUnit } from "@cfworker/json-schema";
export type JSONSchema = Schema;
export type JSONSchemaError = OutputUnit;
@@ -0,0 +1,57 @@
import { JSONSchema } from "./types";
export async function validate(data: any, schema?: JSONSchema) {
try {
if (schema === undefined) {
return {
success: true as const,
};
}
if (data === undefined) {
return {
success: false as const,
errors: [
{
keyword: "undefined",
keywordLocation: "undefined",
instanceLocation: "undefined",
error: "data is undefined",
},
],
};
}
const Validator = await getValidator();
const validator = new Validator(schema);
const result = validator.validate(data);
if (!result.valid) {
return {
success: false as const,
errors: result.errors,
};
}
return {
success: true as const,
};
} catch (e: any) {
console.error(e);
return {
success: false as const,
errors: [
{
keyword: "undefined",
keywordLocation: "undefined",
instanceLocation: "undefined",
error: e.toString(),
},
],
};
}
}
async function getValidator() {
const tool = await import("@cfworker/json-schema");
return tool.Validator;
}
@@ -0,0 +1,13 @@
import { Action } from "core/action/types";
import { IntegrationAuthentication } from "core/authentication/types";
export type Service = {
name: string;
service: string;
version: string;
baseUrl: string;
live: boolean;
authentication: IntegrationAuthentication;
actions: Record<string, Action>;
retryableStatusCodes: number[];
};
@@ -0,0 +1,109 @@
import { InputSpec } from "core/action/types";
import { checkRequiredScopes } from "core/authentication/credentials";
import { RequestError } from "core/request/errors";
import { RequestData, RequestSpec } from "core/request/types";
import { validate } from "core/schemas/validate";
type ValidationResult =
| {
success: true;
}
| {
success: false;
error: RequestError;
};
export async function validateInputs(
inputSpec: InputSpec,
{ parameters, body, credentials }: RequestData
): Promise<ValidationResult> {
if (inputSpec.security) {
if (credentials === undefined) {
return {
success: false,
error: {
type: "missing_credentials",
},
};
}
const requiredScopes = inputSpec.security[credentials.name] ?? [];
const result = checkRequiredScopes(requiredScopes, credentials);
if (!result.success) {
return {
success: false,
error: {
type: "insufficient_scopes",
missingScopes: result.missingScopes,
},
};
}
}
// validate the request body exists if it should
if (body == null && inputSpec.body != null) {
return {
success: false,
error: {
type: "missing_body",
},
};
}
//validate the request body against the schema
const requestValid = await validate(body, inputSpec.body);
if (!requestValid.success) {
return {
success: false,
error: {
type: "request_body_invalid",
errors: requestValid.errors,
},
};
}
//validate the parameters
if (inputSpec.parameters != null) {
for (const parameter of inputSpec.parameters) {
const { name, required } = parameter;
// if the parameter is missing
if (parameters === undefined || parameters[name] == null) {
if (required) {
return {
success: false,
error: {
type: "missing_parameter",
parameter: {
name,
},
},
};
} else {
continue;
}
}
const element = parameters[name];
//validate the parameter against the schema
const valid = await validate(element, parameter.schema);
if (!valid.success) {
return {
success: false,
error: {
type: "parameter_invalid",
parameter: {
name,
value: element,
},
errors: valid.errors,
},
};
}
}
}
return {
success: true,
};
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,75 @@
[
{
"scope": "https://slack.com:443",
"method": "GET",
"path": "/api/conversations.list?limit=3",
"body": "",
"status": 200,
"response": [
"13a10900e45a2e5f5f662b462e26151762c481e859dbffffde6956a4097eb37f02aa77df7d8db575e3fea335752c0a039e145027e3d40b2d00eba4129d035425d78756f60ba204af170b1f86483c07dee72fe4317870e818d6cdc59b7d081878c0287860ab6e19cd3bf225f61be1bc2382ac0003f2e24cce767358bdc5ff27e6efe91e15d4cecfa8a6aaeaa6eeeaf667d1c49ef8fb383a0c655532f02ae7e5e72c8ac21662b52b9fbf2ed41ddc25fcc145a24ddfb1d411d84be8b4ff04dbb1b7b63ebf31fc3bfed0b427f133131f2a9ed4f65f9d87833bea7962df1dcbb40c575534c5b0150ce8e822c38f0726b8c1a31775894ab417e0d9318cb3c3335335424a29fa12934fe2d87ceefa561a8129f271cb7784f29485b4fd55e0491abc5f00ce6df0e046706922da1285233d68fe538d002f132a1be061b31db24c20e0de211ded031dfdc7403381cc3efb633986e2d0cae55b25863e82922a674915308c922049729e62948938a552fde834122d2af230a4ad5404eb6368268695184d5a6b371a5b02166f21265a49a688495cc46831de9149e52d1de762e84a357473940f6c7da403cff8c744e37194d39b870b86ef4f71a40ab47210eafedec73fa66bb99aa36aaa6acc30179aa4d4548102cec5c0a3e46de9484720aa1fd1458a057d8fa70eb30d722b62ad1bfdaf51732b0c120ad5261e0936adc6e03e980bf5b38efe852022fc10052d45ff3cb4e9a73bcca22c88540219b11b9ff60572714c9e4bede4f2e2fa97ef83a8cfa3a06aeb94601a99baa6d96693b436663171fddf9a64af46731d453115ddd0d4d9b7d5af35e352b56742300a313884c29056bed452699f53febf3154c19949cfb0d0f7301142aadafa91c1303d001ec4a72f59f8541fdd1e3d3cbc3c7f0437caf536fcff03"
],
"rawHeaders": [
"date",
"Sat, 18 Feb 2023 16:06:41 GMT",
"server",
"Apache",
"x-powered-by",
"HHVM/4.153.1",
"access-control-allow-origin",
"*",
"referrer-policy",
"no-referrer",
"x-slack-backend",
"r",
"x-slack-unique-id",
"Y_D3kQScXBLeo8CcHpwldgAAECY",
"strict-transport-security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-headers",
"slack-route, x-slack-version-ts, x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags",
"access-control-expose-headers",
"x-slack-req-id, retry-after",
"x-oauth-scopes",
"channels:read,channels:join,channels:manage,chat:write,groups:write,im:write,mpim:write,chat:write.customize,reactions:write",
"x-accepted-oauth-scopes",
"channels:read,groups:read,mpim:read,im:read,read",
"expires",
"Mon, 26 Jul 1997 05:00:00 GMT",
"cache-control",
"private, no-cache, no-store, must-revalidate",
"pragma",
"no-cache",
"x-xss-protection",
"0",
"x-content-type-options",
"nosniff",
"x-slack-req-id",
"10d2571cfa8e50b57c91d537221aff81",
"vary",
"Accept-Encoding",
"content-encoding",
"br",
"content-length",
"654",
"content-type",
"application/json; charset=utf-8",
"x-envoy-upstream-service-time",
"118",
"x-backend",
"main_normal main_canary_with_overflow main_control_with_overflow",
"x-server",
"slack-www-hhvm-main-iad-cxtr",
"x-slack-shared-secret-outcome",
"no-match",
"via",
"envoy-www-iad-scsk, envoy-edge-lhr-csok",
"x-edge-backend",
"envoy-www",
"x-slack-edge-shared-secret-outcome",
"no-match",
"connection",
"close"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,53 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "POST",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m",
"body": {
"fields": {
"Employee": "Employee single create"
}
},
"status": 200,
"response": {
"id": "recY9ZLK96wNBblmo",
"createdTime": "2023-02-18T17:05:53.000Z",
"fields": {
"Employee": "Employee single create"
}
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 17:05:53 GMT",
"Content-Type",
"120",
"Connection",
"close",
"Set-Cookie",
"AWSALB=uxQV2kzjELpFGUscnluykIVz7XOSrD1HxCYqoKdrnnPDt2Djy+EBfTKTDx4p0fAh1qD5c3/jRM2bCy9rhptozXyMsWOCDL7v2H3E6VRso6pGZm+kmcdzFjjoIO36; Expires=Sat, 25 Feb 2023 17:05:53 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=uxQV2kzjELpFGUscnluykIVz7XOSrD1HxCYqoKdrnnPDt2Djy+EBfTKTDx4p0fAh1qD5c3/jRM2bCy9rhptozXyMsWOCDL7v2H3E6VRso6pGZm+kmcdzFjjoIO36; Expires=Sat, 25 Feb 2023 17:05:53 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwL5hqTq5iBqCg9Q; path=/; expires=Sun, 18 Feb 2024 17:05:53 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"114"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,73 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "POST",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m",
"body": {
"records": [
{
"fields": {
"Employee": "Employee #1"
}
},
{
"fields": {
"Employee": "Employee #2"
}
}
]
},
"status": 200,
"response": {
"records": [
{
"id": "recGy1FsSzQnQ1Q5G",
"createdTime": "2023-02-18T17:05:09.000Z",
"fields": {
"Employee": "Employee #1"
}
},
{
"id": "recfrk4NonMC37k3q",
"createdTime": "2023-02-18T17:05:09.000Z",
"fields": {
"Employee": "Employee #2"
}
}
]
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 17:05:09 GMT",
"Content-Type",
"145",
"Connection",
"close",
"Set-Cookie",
"AWSALB=S3w+xLzGuJ4YZa7uQpSGEZYmk2IXTVlsJ9yVl+E2aUbEW5BtGoPl4lNa9pwXEpNvI29HEMTQgpuOXAnvfTFKzm+ot6y2DAQJwqa1nDNgQVRZ2z6lNn602nx0ebhI; Expires=Sat, 25 Feb 2023 17:05:08 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=S3w+xLzGuJ4YZa7uQpSGEZYmk2IXTVlsJ9yVl+E2aUbEW5BtGoPl4lNa9pwXEpNvI29HEMTQgpuOXAnvfTFKzm+ot6y2DAQJwqa1nDNgQVRZ2z6lNn602nx0ebhI; Expires=Sat, 25 Feb 2023 17:05:08 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brw7GK0C3hS9Fnih9; path=/; expires=Sun, 18 Feb 2024 17:05:08 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"221"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,97 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "POST",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m",
"body": {
"fields": {
"Employee": "Delete now #3"
}
},
"status": 200,
"response": {
"id": "recyubpGhZWle9GOS",
"createdTime": "2023-02-18T17:25:39.000Z",
"fields": {
"Employee": "Delete now #3"
}
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 17:25:38 GMT",
"Content-Type",
"121",
"Connection",
"close",
"Set-Cookie",
"AWSALB=jt7yHA1t0HaG6Wp2xE4aJWZdQ59coYaZrRwqp8gwZmizt1A1N1H+xEqqycwyHEJtuK/6Rvc5FGcWdsJZQD2kbg5MSS0aropskjuOcOWICRp4+IHNWdYH5Osmcfre; Expires=Sat, 25 Feb 2023 17:25:38 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=jt7yHA1t0HaG6Wp2xE4aJWZdQ59coYaZrRwqp8gwZmizt1A1N1H+xEqqycwyHEJtuK/6Rvc5FGcWdsJZQD2kbg5MSS0aropskjuOcOWICRp4+IHNWdYH5Osmcfre; Expires=Sat, 25 Feb 2023 17:25:38 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwoFcsjVo3nFDHPT; path=/; expires=Sun, 18 Feb 2024 17:25:38 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"105"
],
"responseIsBinary": false
},
{
"scope": "https://api.airtable.com:443",
"method": "DELETE",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m/recyubpGhZWle9GOS",
"body": "",
"status": 200,
"response": {
"deleted": true,
"id": "recyubpGhZWle9GOS"
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 17:25:39 GMT",
"Content-Type",
"59",
"Connection",
"close",
"Set-Cookie",
"AWSALB=SjG+vAIOeCRwgx9Zhi7bVWiFG0acnh1G6+u2nL+ZUfo+4ON1RPjGdhX41SiaekKoYBmW2oCbSynCYAXKGnQjJYaOmPFAeIkml0VZuXSCv1Jm8P2PsXR+DoKIvZTk; Expires=Sat, 25 Feb 2023 17:25:39 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=SjG+vAIOeCRwgx9Zhi7bVWiFG0acnh1G6+u2nL+ZUfo+4ON1RPjGdhX41SiaekKoYBmW2oCbSynCYAXKGnQjJYaOmPFAeIkml0VZuXSCv1Jm8P2PsXR+DoKIvZTk; Expires=Sat, 25 Feb 2023 17:25:39 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwUynCRjs24wfp6Z; path=/; expires=Sun, 18 Feb 2024 17:25:39 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"41"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,125 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "POST",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m",
"body": {
"records": [
{
"fields": {
"Employee": "Delete now #1"
}
},
{
"fields": {
"Employee": "Delete now #2"
}
}
]
},
"status": 200,
"response": {
"records": [
{
"id": "recTR6YLOshwwyi1O",
"createdTime": "2023-02-18T17:22:12.000Z",
"fields": {
"Employee": "Delete now #1"
}
},
{
"id": "recuQVL2gpUjN64py",
"createdTime": "2023-02-18T17:22:12.000Z",
"fields": {
"Employee": "Delete now #2"
}
}
]
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 17:22:12 GMT",
"Content-Type",
"154",
"Connection",
"close",
"Set-Cookie",
"AWSALB=IAMzoMptPLC7k7rWXTocxJiu8ciJvbHqD17c0EPaM38k1FkzA/zW7S+tRKK6LOzrVFyLL8GUgFtdfDGFELrQ8uO1bADYnEiB4vfOAXjRwSBA2Dc61N3O+MIWMdnM; Expires=Sat, 25 Feb 2023 17:22:12 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=IAMzoMptPLC7k7rWXTocxJiu8ciJvbHqD17c0EPaM38k1FkzA/zW7S+tRKK6LOzrVFyLL8GUgFtdfDGFELrQ8uO1bADYnEiB4vfOAXjRwSBA2Dc61N3O+MIWMdnM; Expires=Sat, 25 Feb 2023 17:22:12 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwFID6JiyKkz2OUb; path=/; expires=Sun, 18 Feb 2024 17:22:12 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"225"
],
"responseIsBinary": false
},
{
"scope": "https://api.airtable.com:443",
"method": "DELETE",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m?records=recTR6YLOshwwyi1O&records=recuQVL2gpUjN64py",
"body": "",
"status": 200,
"response": {
"records": [
{
"deleted": true,
"id": "recTR6YLOshwwyi1O"
},
{
"deleted": true,
"id": "recuQVL2gpUjN64py"
}
]
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 17:22:13 GMT",
"Content-Type",
"91",
"Connection",
"close",
"Set-Cookie",
"AWSALB=psOMBihmNThSHe+ZEkpP1CKmbhnepN53OlaE2Eyi0zBUDOzM7dHGR7ptEm8ebKfpyqDolVl93quNNO44wwIfZqZHuSPVGyZsJWJoM2FsHY4G4HaFCJZoPEkqRFFs; Expires=Sat, 25 Feb 2023 17:22:12 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=psOMBihmNThSHe+ZEkpP1CKmbhnepN53OlaE2Eyi0zBUDOzM7dHGR7ptEm8ebKfpyqDolVl93quNNO44wwIfZqZHuSPVGyZsJWJoM2FsHY4G4HaFCJZoPEkqRFFs; Expires=Sat, 25 Feb 2023 17:22:12 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwq5X9BQFhfFj5lr; path=/; expires=Sun, 18 Feb 2024 17:22:12 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"97"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,79 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "GET",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m/recHcnB1MbBr9Rd2P",
"body": "",
"status": 200,
"response": {
"id": "recHcnB1MbBr9Rd2P",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "Faith Butterworth",
"Phone": "(123) 456-7890",
"Headshot": [
{
"id": "att3ibwyOXULYL15O",
"width": 950,
"height": 700,
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/0Hy62Ommo3C1BR3r_TsIMw/xGdMUGBUycsvzqjCPHlIuKyvUCM_Y6InDa7Bz15pCaGtWXyue5BlHYak_kFJYmgVXbgvWAXkaigRk7xdcUHiZU66F33CrAUP36ihAFT3-eU/-_khPf6Rh8P9uLt3Oc55HupkyS000nORrpvZpY1bKwI",
"filename": "headshot-purple-2 (1).png",
"size": 191272,
"type": "image/png",
"thumbnails": {
"small": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/MlmaPKrIg1yOj_qipSwKhg/fVQ5W7QtoxoAszvbIHT2dksddzWQi2ETkCewMXRSY2NIiX6wD1LWuC6d4VICHwRCz_6sKW4vhwZ1vkScFYpcgQ/Iy8lz5HcTKpTrG8gBq4fQJRyPAdnfYrwn3u05_lovmc",
"width": 49,
"height": 36
},
"large": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/BBIfL5Oqc8BAHkugmNUkXA/JygSzSF9lLJi1U1dP1xCpH1QTSJwxb2FRvJssEdxZubfzVIa9iofsOn5HKsIFLuPl2K3yYCvaxAD3ZsHa7WCig/Rpbg9vWTPa6-oxoPUO4gqkOBFHzadVBHEZhjJoIu6Y8",
"width": 695,
"height": 512
},
"full": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/xlrKts_ACw09qLqsAW16EQ/qrDMlQpI1cVdPCe0uw6ZJw9FeZCcwenmFzZ317qbsO0q8ZY2DY5k-bpPFYBaS4KkqztczIOqdnCBIYiuJ3_wqA/4LU_0QW7hp8Rw828Z_ov3GNcZa86pywziLdQy364XXE",
"width": 3000,
"height": 3000
}
}
}
],
"Employee type": "Contractor"
}
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 16:02:10 GMT",
"Content-Type",
"872",
"Connection",
"close",
"Set-Cookie",
"AWSALB=PhNLtNqLT//gbRBjRTywLiyg0lY3ZGo9Q2LLwOqLnAwsXcrRIaaELYV7A+s2lJlrAsid5UKIKld15Jq1nLyroXxAcFPd3vcbq6ku7tK7LE8HnOQiaMOV9gOoxqoa; Expires=Sat, 25 Feb 2023 16:02:10 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=PhNLtNqLT//gbRBjRTywLiyg0lY3ZGo9Q2LLwOqLnAwsXcrRIaaELYV7A+s2lJlrAsid5UKIKld15Jq1nLyroXxAcFPd3vcbq6ku7tK7LE8HnOQiaMOV9gOoxqoa; Expires=Sat, 25 Feb 2023 16:02:10 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwuEVkvPW1vATlmC; path=/; expires=Sun, 18 Feb 2024 16:02:10 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"1320"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,112 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "POST",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m/listRecords",
"body": {
"timeZone": "America/Los_Angeles",
"userLocale": "en",
"sort": [
{
"field": "Employee",
"direction": "asc"
}
],
"fields": [
"Employee"
]
},
"status": 200,
"response": {
"records": [
{
"id": "recWhRejYoDn5CNsB",
"createdTime": "2023-01-23T07:46:46.000Z",
"fields": {}
},
{
"id": "recFeH8y7qLNR6CxB",
"createdTime": "2023-01-22T23:15:09.000Z",
"fields": {
"Employee": "Employee 7"
}
},
{
"id": "recRo4MuSqRcb33pp",
"createdTime": "2023-01-23T00:48:36.000Z",
"fields": {
"Employee": "Employee 9"
}
},
{
"id": "recHcnB1MbBr9Rd2P",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "Faith Butterworth"
}
},
{
"id": "recbKWDa9URtGWIGG",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "Harley Holbrook"
}
},
{
"id": "rec3cPi3z4s6oe9SD",
"createdTime": "2023-01-22T23:51:21.000Z",
"fields": {
"Employee": "Jimmy dean"
}
},
{
"id": "recGPicQa5yIo9dBY",
"createdTime": "2023-01-22T19:22:05.000Z",
"fields": {
"Employee": "Matt Aitken"
}
},
{
"id": "recElL0yiCgIEVcyc",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "Quinn Nguyen"
}
}
]
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 16:24:53 GMT",
"Content-Type",
"338",
"Connection",
"close",
"Set-Cookie",
"AWSALB=4+PtBWlCPjGrIo2V0YEi8J0ogkebmIOEMfx8TOaOhZvHsaXUZ1TQV+5DlPbCxYxrXoXbbP/F0iRXc3qLGC8LVtm5VIkxt0NDhBENoLsdbBAWF1bbmlOs2DM3N+k3; Expires=Sat, 25 Feb 2023 16:24:53 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=4+PtBWlCPjGrIo2V0YEi8J0ogkebmIOEMfx8TOaOhZvHsaXUZ1TQV+5DlPbCxYxrXoXbbP/F0iRXc3qLGC8LVtm5VIkxt0NDhBENoLsdbBAWF1bbmlOs2DM3N+k3; Expires=Sat, 25 Feb 2023 16:24:53 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brw4B3gkaAL97bpKd; path=/; expires=Sun, 18 Feb 2024 16:24:53 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"829"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,218 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "POST",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m/listRecords",
"body": {},
"status": 200,
"response": {
"records": [
{
"id": "rec3cPi3z4s6oe9SD",
"createdTime": "2023-01-22T23:51:21.000Z",
"fields": {
"Employee": "Jimmy dean",
"Phone": "7778",
"Employee type": "Employee",
"Manager": {
"id": "usrpWV6yqSVvzVO0j",
"email": "matt@trigger.dev",
"name": "Matt Aitken"
}
}
},
{
"id": "recElL0yiCgIEVcyc",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "Quinn Nguyen",
"Phone": "(123) 456-7890",
"Headshot": [
{
"id": "attx14Pa1lP0DMgRw",
"width": 950,
"height": 700,
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/5gbGs-cRWoOC46FxBWQZKw/ph_QI1srGvvZArxOz207bnKOs-xlIXF8ZqMlk5ssMr-jbiMuFfXOjmjadjOACGFRyQ9nE59Dpy7540UU_6bpgsQ_tMKtCFaaf2a4ieIPEEU/ptI5mq4oczxmmrz-RRWb3MSegywNfAwWf--9mL5IT34",
"filename": "headshot-pink-1.png",
"size": 215411,
"type": "image/png",
"thumbnails": {
"small": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/3bEGb1n_oJxadHpHMcDg_w/1Mga_f3JNzLtWq_an_E7I98CxejzOpiALmDJHRdI0XcBbVwIv-vzj96mY-XeAL0W32Q_WCQPu4TqWzisnlqLYg/7pJLZzaspSp1339lIgp1o1Nb1durOAoIx-IflDRI8gY",
"width": 49,
"height": 36
},
"large": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/7Xr67It2dJ296uYyumST1g/HH8QTjlaP0frLwscoJy3vJLvOdKwz1zHL_ll8zMtfFkm1uFB9-nRkA09YeSojhemScYjpvIB7OoxJTh3lqU-jQ/8jtF2js5F47hndbxts2_ZIFZaxkuq3G0BxbomG1wRDA",
"width": 695,
"height": 512
},
"full": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/O1xGv0NoeaGTVYYDxpZnuQ/IBXER_7M4O9QqAmv_9pF2evdf5_9YzeVmdzk3Rsi2gFaK6ueREni-Kaw6Xg8VORqFpHNtUQ7JrV_5Ya4zhSuXg/VAGl2IqW98-D0E1RgBretLYJCmNCDqledVmdTrUOEIU",
"width": 3000,
"height": 3000
}
}
}
],
"Employee type": "Employee"
}
},
{
"id": "recFeH8y7qLNR6CxB",
"createdTime": "2023-01-22T23:15:09.000Z",
"fields": {
"Employee": "Employee 7",
"Phone": "342567",
"Employee type": "Contractor",
"Manager": {
"id": "usrpWV6yqSVvzVO0j",
"email": "matt@trigger.dev",
"name": "Matt Aitken"
}
}
},
{
"id": "recGPicQa5yIo9dBY",
"createdTime": "2023-01-22T19:22:05.000Z",
"fields": {
"Employee": "Matt Aitken",
"Phone": "12345678",
"Employee type": "Employee",
"Manager": {
"id": "usrpWV6yqSVvzVO0j",
"email": "matt@trigger.dev",
"name": "Matt Aitken"
}
}
},
{
"id": "recHcnB1MbBr9Rd2P",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "Faith Butterworth",
"Phone": "(123) 456-7890",
"Headshot": [
{
"id": "att3ibwyOXULYL15O",
"width": 950,
"height": 700,
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/xV5UysTMWXnI9p8E8KMTlA/Ov21BA--8-n8JC_sH7-sx_Iub_aXA1PGiVAohUAhE5SuEtqSMGhN39pRHI3tmu3LqEh8iDz8pTZ_TdWHVV8riCTBPEm8NvvD3o6BtJTRXcE/LJjYfS0Wh_KMcmkgL-82HOLXyhhDHnMsFvrqL1OCtXQ",
"filename": "headshot-purple-2 (1).png",
"size": 191272,
"type": "image/png",
"thumbnails": {
"small": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/L9WFjSk92HlOH9RDdrP7MA/7AgbUc3M9KuDz5plvUFL5dsRwt55PiqcnPvoDIawWfk0w_N4GDZPTPDiiqxFOsUMNOk4CB1RuBZFFLNEfu0bRA/RYcp3x_oGgLtvOi7CtUplGM_rmU-EdbBQ5zdcr7L-Yo",
"width": 49,
"height": 36
},
"large": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/Zl-Thn_JCnnb_-j9CpJ0lg/9hIAzhles7CH7n0EgRzrg-_qVCz1G6oHXX8M7A70KVUBy3Ttnc1NwoSKLMj7JxmYkqc8hYCRU4qRA_S22sS1cw/BhQLDmDmMAUd4sUaxVfTbHnxPGJD9q0_NPB3l7rYz4o",
"width": 695,
"height": 512
},
"full": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/4E18KjffN_36eSJ65hCEFg/1_ts6X0NwupOAEyHnehE_hy5ROdSOt1jODcXv5Cny3Wn4O5r3Ewl3vgsg3Lebkwv40iJo7tbtblZEdtZMecpJA/GBgtTGmld0ZH7_4FygTqipHkEpQVn4dt3CmGBnOo6Wc",
"width": 3000,
"height": 3000
}
}
}
],
"Employee type": "Contractor"
}
},
{
"id": "recRo4MuSqRcb33pp",
"createdTime": "2023-01-23T00:48:36.000Z",
"fields": {
"Employee": "Employee 9",
"Phone": "12345678",
"Employee type": "Employee",
"Manager": {
"id": "usrpWV6yqSVvzVO0j",
"email": "matt@trigger.dev",
"name": "Matt Aitken"
}
}
},
{
"id": "recWhRejYoDn5CNsB",
"createdTime": "2023-01-23T07:46:46.000Z",
"fields": {}
},
{
"id": "recbKWDa9URtGWIGG",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "Harley Holbrook",
"Phone": "(123) 456-7890",
"Headshot": [
{
"id": "attIMHgGEnN0YutyM",
"width": 950,
"height": 700,
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/Fz00EH3JBLCN7V_swur5cg/p3tx8jWcy5175n6bvK9aCabwdIfMur6wDrly_X6A8cL6YnO5JBKK0YJW1EjrEzjCtkj0uJ0PAS4lbwD2hMsKlKUZjopUmDegHLnms_ucOxE/6HP7Qw3GHTGiAmHUM21jHC5fhLcyfTrlD9qrs35a9-I",
"filename": "headshot-yellow-2.png",
"size": 349926,
"type": "image/png",
"thumbnails": {
"small": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/Cyg5kaW38cKzs03TbM-X3g/8omtIQVimV1MrD9PPtj2SaAlOA3CxuCdqHWoVPxi6UELMV2w7EhFv9Ko3WexIF5bcQyd9-N4RLFGvd5__VgP6A/suCIwJkTLliZrHLwioXIUqnrkJ7gIOxtNrdQtkDrBjk",
"width": 49,
"height": 36
},
"large": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/g3EV5PxrbBETRQ9Qzn5WYg/rloz8wROtoNA6Ab9HEykJEmHtnmqUzxnnksoNJU7Y1NjeoV6dQQQZxLTjQBYJukBQfil4EXI8b_ejtARnkANhw/RwNwembhBCNpNJPThTWE_Fae3KtNvBQf-nDboeTP06c",
"width": 695,
"height": 512
},
"full": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676743200000/hFdCcDELa9Y1qNU8ynA-gA/rY9pKGsa3U-i95eAYUT9qPkXhakhXNpWaWBOKsKSSvmc0MFERzMvBTQ9X6hfBBOWdJNPm2ewSI5qaMk1OqdDYg/IVswNEnsz2QWGQtpfHLGHHaAZEWVam08gd7vSXPVMOI",
"width": 3000,
"height": 3000
}
}
}
],
"Employee type": "On leave"
}
}
]
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 15:58:57 GMT",
"Content-Type",
"2247",
"Connection",
"close",
"Set-Cookie",
"AWSALB=eKX9sqNvujzmAJZfcbDUrm7NlI8Xe9niXH7mrVhWbrGQZxHi9a8M9cKLI067MCp0mv5UfHVCEeUvkQbnHW8lB/LQwpch1qdox/a0c+J5kcVIGXWLH8zOB6pv2t42; Expires=Sat, 25 Feb 2023 15:58:57 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=eKX9sqNvujzmAJZfcbDUrm7NlI8Xe9niXH7mrVhWbrGQZxHi9a8M9cKLI067MCp0mv5UfHVCEeUvkQbnHW8lB/LQwpch1qdox/a0c+J5kcVIGXWLH8zOB6pv2t42; Expires=Sat, 25 Feb 2023 15:58:57 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwJiIPgBOUtNPjri; path=/; expires=Sun, 18 Feb 2024 15:58:57 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"4968"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,83 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "PATCH",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m/recHcnB1MbBr9Rd2P",
"body": {
"fields": {
"Employee": "John Doe II"
}
},
"status": 200,
"response": {
"id": "recHcnB1MbBr9Rd2P",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "John Doe II",
"Phone": "(123) 456-7890",
"Headshot": [
{
"id": "att3ibwyOXULYL15O",
"width": 950,
"height": 700,
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/0Hy62Ommo3C1BR3r_TsIMw/xGdMUGBUycsvzqjCPHlIuKyvUCM_Y6InDa7Bz15pCaGtWXyue5BlHYak_kFJYmgVXbgvWAXkaigRk7xdcUHiZU66F33CrAUP36ihAFT3-eU/-_khPf6Rh8P9uLt3Oc55HupkyS000nORrpvZpY1bKwI",
"filename": "headshot-purple-2 (1).png",
"size": 191272,
"type": "image/png",
"thumbnails": {
"small": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/MlmaPKrIg1yOj_qipSwKhg/fVQ5W7QtoxoAszvbIHT2dksddzWQi2ETkCewMXRSY2NIiX6wD1LWuC6d4VICHwRCz_6sKW4vhwZ1vkScFYpcgQ/Iy8lz5HcTKpTrG8gBq4fQJRyPAdnfYrwn3u05_lovmc",
"width": 49,
"height": 36
},
"large": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/BBIfL5Oqc8BAHkugmNUkXA/JygSzSF9lLJi1U1dP1xCpH1QTSJwxb2FRvJssEdxZubfzVIa9iofsOn5HKsIFLuPl2K3yYCvaxAD3ZsHa7WCig/Rpbg9vWTPa6-oxoPUO4gqkOBFHzadVBHEZhjJoIu6Y8",
"width": 695,
"height": 512
},
"full": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/xlrKts_ACw09qLqsAW16EQ/qrDMlQpI1cVdPCe0uw6ZJw9FeZCcwenmFzZ317qbsO0q8ZY2DY5k-bpPFYBaS4KkqztczIOqdnCBIYiuJ3_wqA/4LU_0QW7hp8Rw828Z_ov3GNcZa86pywziLdQy364XXE",
"width": 3000,
"height": 3000
}
}
}
],
"Employee type": "Contractor"
}
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 16:56:09 GMT",
"Content-Type",
"868",
"Connection",
"close",
"Set-Cookie",
"AWSALB=2CpHsb1CL3MMw3hBwb9rVw+mR1QJ7rNbVnCS1fkDNEEi3FM0b4jSEGQ0FhjkIkdkQfLhOMrCXK95XbgEImuhWJuv4FKNdOIk6hNaJawWjFq6kh/mOMlFi7HU8r8h; Expires=Sat, 25 Feb 2023 16:56:09 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=2CpHsb1CL3MMw3hBwb9rVw+mR1QJ7rNbVnCS1fkDNEEi3FM0b4jSEGQ0FhjkIkdkQfLhOMrCXK95XbgEImuhWJuv4FKNdOIk6hNaJawWjFq6kh/mOMlFi7HU8r8h; Expires=Sat, 25 Feb 2023 16:56:09 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwP6XoEdwHyGdxxZ; path=/; expires=Sun, 18 Feb 2024 16:56:09 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"1314"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,92 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "PATCH",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m",
"body": {
"records": [
{
"id": "recHcnB1MbBr9Rd2P",
"fields": {
"Employee": "John Doe"
}
}
]
},
"status": 200,
"response": {
"records": [
{
"id": "recHcnB1MbBr9Rd2P",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "John Doe",
"Phone": "(123) 456-7890",
"Headshot": [
{
"id": "att3ibwyOXULYL15O",
"width": 950,
"height": 700,
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/0Hy62Ommo3C1BR3r_TsIMw/xGdMUGBUycsvzqjCPHlIuKyvUCM_Y6InDa7Bz15pCaGtWXyue5BlHYak_kFJYmgVXbgvWAXkaigRk7xdcUHiZU66F33CrAUP36ihAFT3-eU/-_khPf6Rh8P9uLt3Oc55HupkyS000nORrpvZpY1bKwI",
"filename": "headshot-purple-2 (1).png",
"size": 191272,
"type": "image/png",
"thumbnails": {
"small": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/MlmaPKrIg1yOj_qipSwKhg/fVQ5W7QtoxoAszvbIHT2dksddzWQi2ETkCewMXRSY2NIiX6wD1LWuC6d4VICHwRCz_6sKW4vhwZ1vkScFYpcgQ/Iy8lz5HcTKpTrG8gBq4fQJRyPAdnfYrwn3u05_lovmc",
"width": 49,
"height": 36
},
"large": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/BBIfL5Oqc8BAHkugmNUkXA/JygSzSF9lLJi1U1dP1xCpH1QTSJwxb2FRvJssEdxZubfzVIa9iofsOn5HKsIFLuPl2K3yYCvaxAD3ZsHa7WCig/Rpbg9vWTPa6-oxoPUO4gqkOBFHzadVBHEZhjJoIu6Y8",
"width": 695,
"height": 512
},
"full": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/xlrKts_ACw09qLqsAW16EQ/qrDMlQpI1cVdPCe0uw6ZJw9FeZCcwenmFzZ317qbsO0q8ZY2DY5k-bpPFYBaS4KkqztczIOqdnCBIYiuJ3_wqA/4LU_0QW7hp8Rw828Z_ov3GNcZa86pywziLdQy364XXE",
"width": 3000,
"height": 3000
}
}
}
],
"Employee type": "Contractor"
}
}
]
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 16:46:03 GMT",
"Content-Type",
"870",
"Connection",
"close",
"Set-Cookie",
"AWSALB=aRijGYsG1c7HW7/3PfA+iqX9yfDtsVgoPBVm4dkHcigHjt0gpNbzI/GN4nVRaRPorF1fOAE0jRrIREfxlnx9YcqQP097XI6IpF109sT5W5dRnY6rzb2gyGS93Vhn; Expires=Sat, 25 Feb 2023 16:46:03 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=aRijGYsG1c7HW7/3PfA+iqX9yfDtsVgoPBVm4dkHcigHjt0gpNbzI/GN4nVRaRPorF1fOAE0jRrIREfxlnx9YcqQP097XI6IpF109sT5W5dRnY6rzb2gyGS93Vhn; Expires=Sat, 25 Feb 2023 16:46:03 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwkAO755Iu5BkpXR; path=/; expires=Sun, 18 Feb 2024 16:46:03 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"1325"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,114 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "PATCH",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m",
"body": {
"performUpsert": {
"fieldsToMergeOn": [
"Employee"
]
},
"records": [
{
"fields": {
"Employee": "John Doe"
}
},
{
"fields": {
"Employee": "Jane Doe"
}
}
]
},
"status": 200,
"response": {
"records": [
{
"id": "recHcnB1MbBr9Rd2P",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "John Doe",
"Phone": "(123) 456-7890",
"Headshot": [
{
"id": "att3ibwyOXULYL15O",
"width": 950,
"height": 700,
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/0Hy62Ommo3C1BR3r_TsIMw/xGdMUGBUycsvzqjCPHlIuKyvUCM_Y6InDa7Bz15pCaGtWXyue5BlHYak_kFJYmgVXbgvWAXkaigRk7xdcUHiZU66F33CrAUP36ihAFT3-eU/-_khPf6Rh8P9uLt3Oc55HupkyS000nORrpvZpY1bKwI",
"filename": "headshot-purple-2 (1).png",
"size": 191272,
"type": "image/png",
"thumbnails": {
"small": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/MlmaPKrIg1yOj_qipSwKhg/fVQ5W7QtoxoAszvbIHT2dksddzWQi2ETkCewMXRSY2NIiX6wD1LWuC6d4VICHwRCz_6sKW4vhwZ1vkScFYpcgQ/Iy8lz5HcTKpTrG8gBq4fQJRyPAdnfYrwn3u05_lovmc",
"width": 49,
"height": 36
},
"large": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/BBIfL5Oqc8BAHkugmNUkXA/JygSzSF9lLJi1U1dP1xCpH1QTSJwxb2FRvJssEdxZubfzVIa9iofsOn5HKsIFLuPl2K3yYCvaxAD3ZsHa7WCig/Rpbg9vWTPa6-oxoPUO4gqkOBFHzadVBHEZhjJoIu6Y8",
"width": 695,
"height": 512
},
"full": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676750400000/xlrKts_ACw09qLqsAW16EQ/qrDMlQpI1cVdPCe0uw6ZJw9FeZCcwenmFzZ317qbsO0q8ZY2DY5k-bpPFYBaS4KkqztczIOqdnCBIYiuJ3_wqA/4LU_0QW7hp8Rw828Z_ov3GNcZa86pywziLdQy364XXE",
"width": 3000,
"height": 3000
}
}
}
],
"Employee type": "Contractor"
}
},
{
"id": "recpyQHEcTiiwzAAe",
"createdTime": "2023-02-18T16:49:12.000Z",
"fields": {
"Employee": "Jane Doe"
}
}
],
"updatedRecords": [
"recHcnB1MbBr9Rd2P"
],
"createdRecords": [
"recpyQHEcTiiwzAAe"
]
},
"rawHeaders": [
"Date",
"Sat, 18 Feb 2023 16:49:12 GMT",
"Content-Type",
"931",
"Connection",
"close",
"Set-Cookie",
"AWSALB=/B2FI/4JzHLz69XGh0fnXEzIHT58xav2z9CRaJPk/mxxK+IPKjzX/Kd13DU/uJsnD5QcWRgFZjepxA7s8HoojMSUw3+Vkav3Db4mPcly8wQXp88r5BEUjDvFso9o; Expires=Sat, 25 Feb 2023 16:49:12 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=/B2FI/4JzHLz69XGh0fnXEzIHT58xav2z9CRaJPk/mxxK+IPKjzX/Kd13DU/uJsnD5QcWRgFZjepxA7s8HoojMSUw3+Vkav3Db4mPcly8wQXp88r5BEUjDvFso9o; Expires=Sat, 25 Feb 2023 16:49:12 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwsqrG8CnBxgniFg; path=/; expires=Sun, 18 Feb 2024 16:49:12 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"1504"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,78 @@
[
{
"scope": "https://slack.com:443",
"method": "POST",
"path": "/api/chat.postMessage",
"body": {
"channel": "C00AAAAAAAA",
"text": "This channel doesn't exist, so message won't send"
},
"status": 200,
"response": [
"13270000045439f20ace64f1259fdc9ec3444e00b2e6817cef2beb4e3f1ab2011262c998a76bb93c"
],
"rawHeaders": [
"date",
"Sat, 18 Feb 2023 16:06:42 GMT",
"server",
"Apache",
"x-powered-by",
"HHVM/4.153.1",
"access-control-allow-origin",
"*",
"referrer-policy",
"no-referrer",
"x-slack-backend",
"r",
"x-slack-unique-id",
"Y_D3kqn7oMaK4u0tyzhR0AAAEDY",
"strict-transport-security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-headers",
"slack-route, x-slack-version-ts, x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags",
"access-control-expose-headers",
"x-slack-req-id, retry-after",
"x-oauth-scopes",
"channels:read,channels:join,channels:manage,chat:write,groups:write,im:write,mpim:write,chat:write.customize,reactions:write",
"x-accepted-oauth-scopes",
"chat:write",
"expires",
"Mon, 26 Jul 1997 05:00:00 GMT",
"cache-control",
"private, no-cache, no-store, must-revalidate",
"pragma",
"no-cache",
"x-xss-protection",
"0",
"x-content-type-options",
"nosniff",
"x-slack-req-id",
"ebbefac5397dbab5385db0aae08cf6b3",
"vary",
"Accept-Encoding",
"content-encoding",
"br",
"content-length",
"40",
"content-type",
"application/json; charset=utf-8",
"x-envoy-upstream-service-time",
"93",
"x-backend",
"main_normal main_canary_with_overflow main_control_with_overflow",
"x-server",
"slack-www-hhvm-main-iad-dihx",
"x-slack-shared-secret-outcome",
"no-match",
"via",
"envoy-www-iad-uqca, envoy-edge-lhr-ozbu",
"x-edge-backend",
"envoy-www",
"x-slack-edge-shared-secret-outcome",
"no-match",
"connection",
"close"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,78 @@
[
{
"scope": "https://slack.com:443",
"method": "POST",
"path": "/api/chat.postMessage",
"body": {
"channel": "C04GWUTDC3W",
"text": "This the Trigger.dev integrations test"
},
"status": 200,
"response": [
"132a0300c4f7dfcd9f56ce9d9f9342c8d6f68b484575e2661a76992518653ed6029993da6eeb1ffccd5ee7345e3bb70b6a2dc047cc6a474c8790110b4b6112e38710141d5d7f7f7e5c14e36f28ac9610f8244dd23821e74d1688288302d2d790231ad355b5df82a0e0e8fa972e9eeeae73eef97ed686181b0ad73ce882605eae7aede863d1ef76db0bd36a6fa27fe9e317b9002e2360a92b8c47fc31049f8e6edce7e7ebcbcd1ba9a03e9b3101f2b01b4fd93d33e1da644bc8ff7161faf2553ea405e1dab9e6672888b8ed2456c3b4c796c43df1a8dab9fcc53ca1fa78c294ffe6b7c20bc170baaa060a937a4be63de7f627f531276273af355d3dd88e517b0185cce397a058db09ab71d2bca74b29d992ad9b657572babef0e99649c9b2cb3197da8159b25dbd5ab2f5d9ac64496a7192be18348e881e3192189d34d057f0f9d534a00667852a365b685402cac16b4132aa117b171387829075f67ae7f319"
],
"rawHeaders": [
"date",
"Sat, 18 Feb 2023 16:06:41 GMT",
"server",
"Apache",
"x-powered-by",
"HHVM/4.153.1",
"access-control-allow-origin",
"*",
"referrer-policy",
"no-referrer",
"x-slack-backend",
"r",
"x-slack-unique-id",
"Y_D3kaZTOTsosPP4CQqdxwAAAD8",
"strict-transport-security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-headers",
"slack-route, x-slack-version-ts, x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags",
"access-control-expose-headers",
"x-slack-req-id, retry-after",
"x-oauth-scopes",
"channels:read,channels:join,channels:manage,chat:write,groups:write,im:write,mpim:write,chat:write.customize,reactions:write",
"x-accepted-oauth-scopes",
"chat:write",
"expires",
"Mon, 26 Jul 1997 05:00:00 GMT",
"cache-control",
"private, no-cache, no-store, must-revalidate",
"pragma",
"no-cache",
"x-xss-protection",
"0",
"x-content-type-options",
"nosniff",
"x-slack-req-id",
"bd4f8b61920e9df75d67e4cfeb504ab9",
"vary",
"Accept-Encoding",
"content-encoding",
"br",
"content-length",
"351",
"content-type",
"application/json; charset=utf-8",
"x-envoy-upstream-service-time",
"141",
"x-backend",
"main_normal main_canary_with_overflow main_control_with_overflow",
"x-server",
"slack-www-hhvm-main-iad-mhht",
"x-slack-shared-secret-outcome",
"no-match",
"via",
"envoy-www-iad-qbeb, envoy-edge-lhr-vvct",
"x-edge-backend",
"envoy-www",
"x-slack-edge-shared-secret-outcome",
"no-match",
"connection",
"close"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,75 @@
[
{
"scope": "https://slack.com:443",
"method": "GET",
"path": "/api/conversations.list?limit=1",
"body": "",
"status": 200,
"response": [
"13b70300c4c8b67c7d337bad87225e710b16c4026732712e9d6da4139c5a54d3ce1324f223350ae881931e68f7fe6f2e24241b5ae4fce77333b3308328b0bdc4c2a6b0c47fd8fe99218bb08d13c7b0eeaf3fed1350d47ec5b10d59b12bca7aac133d4690f51821096e8017ee6455da533537753e31a7cf1f8a4134bc504d55d54dddd5edb6948833517f6a3b0cb0cc0dea32ab8b76a2c2b0c8a4d6b369799577425dd5010f896e52233616b8cbf8b8df824e74d5d5cf1fc5e9f8fd719fd94f603a045e61471fcef3f123a9795cbe1f966919aeaa688a612b14de3162df8f279060601b5ea44a20d17d8337c7302e4f2e4d558826a6c8229a3e6ec7f23bfa639a72cc9ae6e29f85125e05bc3b2a819318db3338ce776ce35ed4d248740b126c7a503e5423e15e3c991b886c598e5622e0ce7dc37abc6fe8684101b30acd3947bb1c13d3a154d39724844e2171995571e95794c47e1c677542492aa2844b4dee39115d926741c0bb52eec747642206ab11f9b5d631d92d819adefc4a7429f12192908486c57ab7442a76f9301383deac46a939aa07554cf5b06d2cfe028d6b812d9122d694e12ac0b13c8f7d9c6302b6115dbca7c16b73fa70fa64dc9d450f6f85d8c36201"
],
"rawHeaders": [
"date",
"Sat, 18 Feb 2023 16:06:42 GMT",
"server",
"Apache",
"x-powered-by",
"HHVM/4.153.1",
"access-control-allow-origin",
"*",
"referrer-policy",
"no-referrer",
"x-slack-backend",
"r",
"x-slack-unique-id",
"Y_D3khlLQ9vqek922QY0uQAAECM",
"strict-transport-security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-headers",
"slack-route, x-slack-version-ts, x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags",
"access-control-expose-headers",
"x-slack-req-id, retry-after",
"x-oauth-scopes",
"channels:read,channels:join,channels:manage,chat:write,groups:write,im:write,mpim:write,chat:write.customize,reactions:write",
"x-accepted-oauth-scopes",
"channels:read,groups:read,mpim:read,im:read,read",
"expires",
"Mon, 26 Jul 1997 05:00:00 GMT",
"cache-control",
"private, no-cache, no-store, must-revalidate",
"pragma",
"no-cache",
"x-xss-protection",
"0",
"x-content-type-options",
"nosniff",
"x-slack-req-id",
"c9ac7f0c1a73910e91a87a6cd3925462",
"vary",
"Accept-Encoding",
"content-encoding",
"br",
"content-length",
"452",
"content-type",
"application/json; charset=utf-8",
"x-envoy-upstream-service-time",
"115",
"x-backend",
"main_normal main_canary_with_overflow main_control_with_overflow",
"x-server",
"slack-www-hhvm-main-iad-bmkm",
"x-slack-shared-secret-outcome",
"no-match",
"via",
"envoy-www-iad-vpph, envoy-edge-lhr-tspo",
"x-edge-backend",
"envoy-www",
"x-slack-edge-shared-secret-outcome",
"no-match",
"connection",
"close"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,56 @@
[
{
"scope": "https://api.sendgrid.com:443",
"method": "POST",
"path": "/v3/mail/send",
"body": {
"from": {
"email": "matt@email.trigger.dev"
},
"subject": "Hello, World!",
"content": [
{
"type": "text/plain",
"value": "Email body here"
}
],
"personalizations": [
{
"to": [
{
"email": "matt@trigger.dev"
}
],
"subject": "Hello, World!"
}
]
},
"status": 202,
"response": "",
"rawHeaders": [
"Server",
"nginx",
"Date",
"Sun, 19 Feb 2023 19:16:30 GMT",
"Content-Length",
"0",
"Connection",
"close",
"X-Message-Id",
"D9FX-x6ETvKlMlzEi0Qlpg",
"Access-Control-Allow-Origin",
"https://sendgrid.api-docs.io",
"Access-Control-Allow-Methods",
"POST",
"Access-Control-Allow-Headers",
"Authorization, Content-Type, On-behalf-of, x-sg-elas-acl",
"Access-Control-Max-Age",
"600",
"X-No-CORS-Reason",
"https://sendgrid.com/docs/Classroom/Basics/API/cors.html",
"Strict-Transport-Security",
"max-age=600; includeSubDomains"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,59 @@
[
{
"scope": "https://api.sendgrid.com:443",
"method": "PUT",
"path": "/v3/marketing/contacts",
"body": {
"contacts": [
{
"email": "matt+1@mattaitken.com",
"first_name": "Matt",
"last_name": "Aitken"
}
]
},
"status": 202,
"response": {
"job_id": "1befbf98-bfdc-4565-ba5a-c7e8b91f2678"
},
"rawHeaders": [
"Server",
"nginx",
"Date",
"Mon, 20 Feb 2023 15:21:43 GMT",
"Content-Type",
"application/json",
"Content-Length",
"50",
"Connection",
"close",
"x-amzn-requestid",
"6fcf9591-e37d-4fb9-b175-59d434ba45c6",
"access-control-allow-origin",
"*",
"access-control-allow-headers",
"AUTHORIZATION, Content-Type, On-behalf-of, x-sg-elas-acl, X-Recaptcha, X-Request-Source",
"x-amz-apigw-id",
"ApNxLHMAvHcFjkA=",
"access-control-allow-methods",
"PUT,OPTIONS,DELETE,OPTIONS",
"access-control-expose-headers",
"Link, Location",
"x-amzn-trace-id",
"Root=1-63f39007-0392a34047f268f366e7d4ce;Sampled=0",
"x-envoy-upstream-service-time",
"231",
"referrer-policy",
"strict-origin-when-cross-origin",
"x-content-type-options",
"nosniff",
"x-ratelimit-limit",
"200",
"x-ratelimit-remaining",
"197",
"x-ratelimit-reset",
"17"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,79 @@
[
{
"scope": "https://api.airtable.com:443",
"method": "GET",
"path": "/v0/appBlf3KsalIQeMUo/tblvXn2TOeVPC9c6m/recHcnB1MbBr9Rd2P",
"body": "",
"status": 200,
"response": {
"id": "recHcnB1MbBr9Rd2P",
"createdTime": "2023-01-03T12:05:53.000Z",
"fields": {
"Employee": "John Doe II",
"Phone": "(123) 456-7890",
"Headshot": [
{
"id": "att3ibwyOXULYL15O",
"width": 950,
"height": 700,
"url": "https://v5.airtableusercontent.com/v1/15/15/1676995200000/D_htRl39BrNhGxsiGlNpcQ/tYWJWmeq3WBDaQCZIeDn-IAKEczimZduMBB30Xq48IYOla6DEq2h59YjRRAQZr4xH-YCc7n3cf4GAXlxPXYAYGfo2e0K230BdUzQXk43vrc/Y61oM8Pgbyu_5-jhuGhea9r2mpjjm5lxIGOJPP5BaO4",
"filename": "headshot-purple-2 (1).png",
"size": 191272,
"type": "image/png",
"thumbnails": {
"small": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676995200000/1Ev7NzTXpXw2FnaLtcYrzg/NZwW2F5QNr7qa_SXXlDx9vhjD_oxlwvSCboJj2T_S1WC0J0WWYYOWr6pfn_s_Z3aOU34l-v7ScjvQdL9vDtczQ/U0GygRlbr35RvoD2S-yyeJ3IWrbOqEjFe1D_-8KKpNY",
"width": 49,
"height": 36
},
"large": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676995200000/lgoKQYxOC2eUe51PTO-_yg/dKv1Cx9ws40PNDnn2Y8bKlv4fbCfyGDshlFno1NOrEkceHJW-cCWG5E97cBs4B389t0Rf8SK3dMt8RkUOyueCg/3WxWv9_VvEua1zls82Z4Dca3yjWD9Cp2e6ldqrhzTKg",
"width": 695,
"height": 512
},
"full": {
"url": "https://v5.airtableusercontent.com/v1/15/15/1676995200000/Rihc4R_4ndQBrAHA49_j7Q/3V3Js1Xsx4AxXmxVkurk09ZpYY0kmOJHJlhDqybO5Zws1tP1oZZC4mCaRRnMLpx3XLu41c5aALWuo7Jl7UC2yQ/1Kej1MQjyAGYqcSVqo4_1GHLMOoF9puGps0NxQCCHpQ",
"width": 3000,
"height": 3000
}
}
}
],
"Employee type": "Contractor"
}
},
"rawHeaders": [
"Date",
"Tue, 21 Feb 2023 12:47:49 GMT",
"Content-Type",
"867",
"Connection",
"close",
"Set-Cookie",
"AWSALB=P04kqBtI/0ytyUg9Fjvlqdj/79QEnNQZOJrk8expPLgAkwq3exJsTzPeYwbkOAkiuGDomuXqutUq6flBHmrEFz1du9r03QrsztyUe1mYaFVAoAPR4BacSk6CNE/8; Expires=Tue, 28 Feb 2023 12:47:49 GMT; Path=/",
"Set-Cookie",
"AWSALBCORS=P04kqBtI/0ytyUg9Fjvlqdj/79QEnNQZOJrk8expPLgAkwq3exJsTzPeYwbkOAkiuGDomuXqutUq6flBHmrEFz1du9r03QrsztyUe1mYaFVAoAPR4BacSk6CNE/8; Expires=Tue, 28 Feb 2023 12:47:49 GMT; Path=/; SameSite=None; Secure",
"Server",
"Tengine",
"Set-Cookie",
"brw=brwJ69m62Pda820BR; path=/; expires=Wed, 21 Feb 2024 12:47:49 GMT; domain=.airtable.com; samesite=none; secure",
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload",
"access-control-allow-origin",
"*",
"access-control-allow-methods",
"DELETE,GET,OPTIONS,PATCH,POST,PUT",
"access-control-allow-headers",
"authorization,content-length,content-type,user-agent,x-airtable-application-id,x-airtable-user-agent,x-api-version,x-requested-with",
"X-Frame-Options",
"DENY",
"X-Content-Type-Options",
"nosniff",
"Vary",
"Accept-Encoding",
"airtable-uncompressed-content-length",
"1314"
],
"responseIsBinary": false
}
]
@@ -0,0 +1,106 @@
import { EndpointSpec } from "core/endpoint/types";
import { JSONSchema } from "core/schemas/types";
import { expect, test } from "vitest";
import { createInputSchema } from "./combineSchemas";
test("create input schema when only a body schema", async () => {
try {
const schema: JSONSchema = {
type: "object",
required: ["channel"],
properties: {
as_user: {
type: "string",
description:
"Pass true to post the message as the authed user, instead of as a bot. Defaults to false. See [authorship](#authorship) below.",
},
attachments: {
type: "string",
description:
"A JSON-based array of structured attachments, presented as a URL-encoded string.",
},
channel: {
type: "string",
description:
"Channel, private group, or IM channel to send message to. Can be an encoded ID, or a name. See [below](#channels) for more details.",
},
},
};
const inputSchema = createInputSchema({ body: schema });
expect(inputSchema).toEqual(schema);
} catch (e: any) {
console.error(JSON.stringify(e.errors, null, 2));
expect(e).toEqual(null);
}
});
test("combine input body and parameters into a schema", async () => {
try {
const bodySchema: JSONSchema = {
type: "object",
required: ["channel"],
properties: {
as_user: {
type: "string",
description:
"Pass true to post the message as the authed user, instead of as a bot. Defaults to false. See [authorship](#authorship) below.",
},
attachments: {
type: "string",
description:
"A JSON-based array of structured attachments, presented as a URL-encoded string.",
},
channel: {
type: "string",
description:
"Channel, private group, or IM channel to send message to. Can be an encoded ID, or a name. See [below](#channels) for more details.",
},
},
};
const parameters: EndpointSpec["parameters"] = [
{
name: "limit",
description: "The maximum number of items to return.",
in: "path",
required: true,
schema: {
type: "integer",
description: "The maximum number of items to return.",
},
},
];
const inputSchema = createInputSchema({ body: bodySchema, parameters });
expect(inputSchema).toMatchInlineSnapshot(`
{
"properties": {
"as_user": {
"description": "Pass true to post the message as the authed user, instead of as a bot. Defaults to false. See [authorship](#authorship) below.",
"type": "string",
},
"attachments": {
"description": "A JSON-based array of structured attachments, presented as a URL-encoded string.",
"type": "string",
},
"channel": {
"description": "Channel, private group, or IM channel to send message to. Can be an encoded ID, or a name. See [below](#channels) for more details.",
"type": "string",
},
"limit": {
"description": "The maximum number of items to return.",
"type": "integer",
},
},
"required": [
"channel",
"limit",
],
"type": "object",
}
`);
} catch (e: any) {
console.error(JSON.stringify(e.errors, null, 2));
expect(e).toEqual(null);
}
});
@@ -0,0 +1,79 @@
import { Action } from "core/action/types";
import { JSONSchema } from "core/schemas/types";
export function generateInputOutputSchemas(
spec: Action["spec"],
name: string
): {
input: JSONSchema | undefined;
output: JSONSchema | undefined;
} {
const inputSchema = createInputSchema(spec.input);
if (inputSchema) inputSchema.title = `${name}Input`;
const outputSchema = createSuccessfulOutputSchema(spec.output);
if (outputSchema) outputSchema.title = `${name}Output`;
return {
input: inputSchema,
output: outputSchema,
};
}
export function createInputSchema(
spec: Action["spec"]["input"]
): JSONSchema | undefined {
let inputSchema: JSONSchema | undefined = spec.body;
if (spec.parameters && spec.parameters.length > 0) {
if (!inputSchema) {
inputSchema = {
type: "object",
properties: {},
};
}
inputSchema = {
type: "object",
properties: {
...inputSchema.properties,
...Object.fromEntries(
spec.parameters.map((p) => [
p.name,
{ ...p.schema, description: p.description },
])
),
},
required: [
...(inputSchema.required ?? []),
...spec.parameters.filter((p) => p.required).map((p) => p.name),
],
};
}
return inputSchema;
}
export function createSuccessfulOutputSchema(
spec: Action["spec"]["output"]
): JSONSchema | undefined {
if (spec === undefined) return undefined;
//combine all "success" output schemas into a union
const outputSuccessSchemas = Object.values(spec.responses).flatMap((s) =>
s.flatMap((r) => (r.success ? r.schema : []))
);
return outputSuccessSchemas.length === 1
? outputSuccessSchemas[0]
: createDiscriminatedUnionSchema(`Output`, outputSuccessSchemas);
}
function createDiscriminatedUnionSchema(
name: string,
schemas: JSONSchema[]
): JSONSchema {
return {
$id: name,
oneOf: schemas,
};
}
@@ -0,0 +1,119 @@
import { expect, test } from "vitest";
import { getTypesFromSchema as generateTypesFromSchema } from "./generateTypes";
test("simple schema type generation", async () => {
try {
const schema = {
type: "object",
properties: {
channel: {
type: "string",
description: "ID of conversation to join",
},
},
required: ["channel"],
};
const data = await generateTypesFromSchema(schema, "Input");
expect(data).toMatchInlineSnapshot(`
"export interface Input {
/**
* ID of conversation to join
*/
channel: string
}
"
`);
} catch (e: any) {
console.error(JSON.stringify(e.errors, null, 2));
expect(e).toEqual(null);
}
});
test("advanced schema type generation", async () => {
try {
const schema = {
type: "object",
properties: {
channel: {
type: "string",
},
message: {
type: "object",
properties: {
attachments: {
type: "array",
items: {
type: "object",
properties: {
fallback: {
type: "string",
},
id: {
type: "number",
},
text: {
type: "string",
},
},
required: ["fallback", "id", "text"],
},
},
bot_id: {
type: "string",
},
subtype: {
type: "string",
},
text: {
type: "string",
},
ts: {
type: "string",
},
type: {
type: "string",
},
user: {
type: "string",
},
},
required: ["bot_id", "text", "ts", "type"],
},
ok: {
type: "boolean",
},
ts: {
type: "string",
},
},
required: ["channel", "message", "ok", "ts"],
};
const data = await generateTypesFromSchema(schema, "Input");
expect(data).toMatchInlineSnapshot(`
"export interface Input {
channel: string
message: {
attachments?: {
fallback: string
id: number
text: string
}[]
bot_id: string
subtype?: string
text: string
ts: string
type: string
user?: string
}
ok: boolean
ts: string
}
"
`);
} catch (e: any) {
console.error(JSON.stringify(e.errors, null, 2));
expect(e).toEqual(null);
}
});
@@ -0,0 +1,10 @@
import { compile } from "json-schema-to-typescript";
export async function getTypesFromSchema(schema: any, name: string) {
const ts = await compile(schema, name, {
additionalProperties: false,
bannerComment: "",
format: false,
});
return ts;
}
+62
View File
@@ -0,0 +1,62 @@
import { handleAction } from "api/v2/action";
import { handleActionDisplay } from "api/v2/action/display";
import { handleServices } from "api/v2/services";
import dotenv from "dotenv";
import express, { Express, NextFunction, Request, Response } from "express";
import morgan from "morgan";
dotenv.config();
const app: Express = express();
const port = process.env.PORT ?? 3006;
app.use(express.json());
app.use(morgan("combined"));
const checkAuthentication = function (
req: Request,
res: Response,
next: NextFunction
) {
//if the path is /healthcheck, skip authentication
if (req.path === "/healthcheck") {
next();
return;
}
const authHeader = req.headers.authorization;
if (!authHeader) {
res.status(401).send("Unauthorized");
return;
}
const auth = authHeader.split(" ");
if (auth.length !== 2 || auth[0] !== "Bearer") {
res.status(401).send("Unauthorized");
return;
}
const token = auth[1];
if (token !== process.env.API_TOKEN) {
res.status(401).send("Unauthorized");
return;
}
next();
};
app.use(checkAuthentication);
app.get("/", (req: Request, res: Response) => {
res.send("Trigger.dev integrations service");
});
app.get("/healthcheck", (req: Request, res: Response) => {
res.send("OK");
});
app.get("/api/v2/services", handleServices);
app.post("/api/v2/:service/action/:action/display", handleActionDisplay);
app.post("/api/v2/:service/action/:action", handleAction);
app.listen(port, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
});
@@ -0,0 +1,5 @@
import { makeSimpleActions } from "core/action/makeAction";
import endpoints from "../endpoints/endpoints";
const actions = makeSimpleActions(endpoints);
export default actions;
@@ -0,0 +1,24 @@
import { IntegrationAuthentication } from "core/authentication/types";
export const authentication: IntegrationAuthentication = {
oauth: {
type: "oauth2",
placement: {
in: "header",
type: "bearer",
key: "Authorization",
},
authorizationUrl: "https://airtable.com/oauth2/v1/authorize",
tokenUrl: "https://airtable.com/oauth2/v1/token",
flow: "accessCode",
scopes: {
"data.records:read": "data.records:read",
"data.records:write": "data.records:write",
"data.recordComments:read": "data.recordComments:read",
"data.recordComments:write": "data.recordComments:write",
"schema.bases:read": "schema.bases:read",
"schema.bases:write": "schema.bases:write",
"webhook:manage": "webhook:manage",
},
},
};
@@ -0,0 +1,532 @@
import { EndpointSpecParameter } from "core/endpoint/types";
import {
makeArraySchema,
makeBooleanSchema,
makeNumberSchema,
makeObjectSchema,
makeOneOf,
makeStringSchema,
} from "core/schemas/makeSchema";
import { JSONSchema } from "core/schemas/types";
export const CollaboratorSchema = makeObjectSchema("A Collaborator", {
requiredProperties: {
id: makeStringSchema("Collaborator ID"),
email: makeStringSchema("Collaborator Email"),
name: makeStringSchema("Collaborator Name"),
},
additionalProperties: true,
});
export const ThumbnailSchema = makeObjectSchema("A Thumbnail", {
requiredProperties: {
url: makeStringSchema("Thumbnail URL"),
width: makeNumberSchema("Thumbnail Width"),
height: makeNumberSchema("Thumbnail Height"),
},
additionalProperties: true,
});
export const AttachmentSchema = makeObjectSchema("An Attachment", {
requiredProperties: {
id: makeStringSchema("Attachment ID"),
url: makeStringSchema("Attachment URL"),
filename: makeStringSchema("Attachment Filename"),
size: makeNumberSchema("Attachment Size"),
type: makeStringSchema("Attachment Type"),
},
optionalProperties: {
height: makeNumberSchema("Attachment Height"),
width: makeNumberSchema("Attachment Width"),
thumbnails: makeObjectSchema("Thumbnails", {
requiredProperties: {
small: ThumbnailSchema,
large: ThumbnailSchema,
full: ThumbnailSchema,
},
additionalProperties: true,
}),
},
additionalProperties: true,
});
export const FieldSchema = makeOneOf("FieldValue", [
makeStringSchema("StringValue"),
makeNumberSchema("NumberValue"),
makeBooleanSchema("BooleanValue"),
CollaboratorSchema,
makeArraySchema("Collaborators", CollaboratorSchema),
makeArraySchema("StringValues", makeStringSchema("StringValue")),
makeArraySchema("Attachments", AttachmentSchema),
]);
export const BaseIdParam: EndpointSpecParameter = {
name: "baseId",
in: "path",
description: "The ID of the base",
schema: {
type: "string",
},
required: true,
};
export const TableIdOrNameParam: EndpointSpecParameter = {
name: "tableIdOrName",
in: "path",
description: "The name or id of the table",
schema: {
type: "string",
},
required: true,
};
export const RecordIdParam: EndpointSpecParameter = {
name: "recordId",
in: "path",
description: "The ID of the record",
schema: {
type: "string",
},
required: true,
};
export const TimeZoneSchema: JSONSchema = {
title: "Timezone",
description:
"The time zone that should be used to format dates when using string as the cellFormat. This parameter is required when using string as the cellFormat",
type: "string",
enum: [
"utc",
"client",
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmara",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Argentina/Catamarca",
"America/Argentina/Cordoba",
"America/Argentina/Jujuy",
"America/Argentina/La_Rioja",
"America/Argentina/Mendoza",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Atikokan",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Costa_Rica",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Fort_Nelson",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Indianapolis",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Juneau",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nipigon",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Nuuk",
"America/Ojinaga",
"America/Panama",
"America/Pangnirtung",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rainy_River",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Thunder_Bay",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"America/Yellowknife",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Colombo",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Hebron",
"Asia/Ho_Chi_Minh",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Istanbul",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Kathmandu",
"Asia/Khandyga",
"Asia/Kolkata",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ulaanbaatar",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yangon",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faroe",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Currie",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/Perth",
"Australia/Sydney",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Nicosia",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Ulyanovsk",
"Europe/Uzhgorod",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zaporozhye",
"Europe/Zurich",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Chuuk",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Kanton",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Pohnpei",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Wake",
"Pacific/Wallis",
],
};
@@ -0,0 +1,8 @@
import { makeEndpoints } from "core/endpoint/endpoint";
import { authentication } from "../authentication";
import * as specs from "./specs";
const baseUrl = "https://api.airtable.com/v0";
const endpoints = makeEndpoints(baseUrl, authentication, specs);
export default endpoints;
@@ -0,0 +1,606 @@
import { EndpointSpec, EndpointSpecResponse } from "core/endpoint/types";
import {
makeAnyOf,
makeArraySchema,
makeBooleanSchema,
makeNumberSchema,
makeObjectSchema,
makeStringSchema,
} from "core/schemas/makeSchema";
import {
BaseIdParam,
FieldSchema,
RecordIdParam,
TableIdOrNameParam,
TimeZoneSchema,
} from "../common/schemas";
const errorResponse: EndpointSpecResponse = {
success: false,
name: "Error",
description: "Error response",
schema: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
oneOf: [
{
type: "object",
properties: {
error: {
type: "string",
},
},
required: ["error"],
},
{
type: "object",
properties: {
error: {
type: "object",
properties: {
type: {
type: "string",
},
},
required: ["type"],
},
},
required: ["error"],
},
],
},
};
export const listRecords: EndpointSpec = {
path: "/{baseId}/{tableIdOrName}/listRecords",
method: "POST",
metadata: {
name: "listRecords",
description: `List records in a table. Note that table names and table ids can be used interchangeably. We recommend using table IDs so you don't need to modify your API request when your table name changes.\n
The server returns one page of records at a time. Each page will contain pageSize records, which is 100 by default. If there are more records, the response will contain an offset. To fetch the next page of records, include offset in the next request's parameters. Pagination will stop when you've reached the end of your table. If the maxRecords parameter is passed, pagination will stop once you've reached this maximum.\n
Returned records do not include any fields with "empty" values, e.g. "", [], or false.`,
displayProperties: {
title: "List records from table ${parameters.tableIdOrName}",
},
externalDocs: {
description: "API method documentation",
url: "https://airtable.com/developers/web/api/list-records",
},
tags: ["records"],
},
security: {
oauth: ["data.records:read"],
},
parameters: [BaseIdParam, TableIdOrNameParam],
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: {
schema: makeObjectSchema("List records body", {
optionalProperties: {
timeZone: TimeZoneSchema,
userLocal: makeStringSchema(
"userLocal",
"The user locale that should be used to format dates when using string as the cellFormat. This parameter is required when using string as the cellFormat."
),
pageSize: makeNumberSchema(
"pageSize",
"The number of records returned in each request. Must be less than or equal to 100. Default is 100."
),
maxRecords: makeNumberSchema(
"maxRecords",
"The maximum total number of records that will be returned in your requests. If this value is larger than pageSize (which is 100 by default), you may have to load multiple pages to reach this total."
),
offset: makeStringSchema(
"offset",
"To fetch the next page of records, include offset from the previous request in the next request's parameters."
),
view: makeStringSchema(
"view",
"The name or ID of a view in the table. If set, only the records in that view will be returned. The records will be sorted according to the order of the view unless the sort parameter is included, which overrides that order. Fields hidden in this view will be returned in the results. To only return a subset of fields, use the fields parameter."
),
sort: makeArraySchema(
"Sort",
makeObjectSchema("Sort field", {
requiredProperties: {
field: makeStringSchema("Field name"),
},
optionalProperties: {
direction: makeStringSchema("Direction", "Direction", {
enum: ["asc", "desc"],
}),
},
})
),
filterByFormula: makeStringSchema(
"filterByFormula",
`A formula used to filter records. The formula will be evaluated for each record, and if the result is not 0, false, "", NaN, [], or #Error! the record will be included in the response. If combined with the view parameter, only records in that view which satisfy the formula will be returned. For example, to only include records where the column named "Category" equals "Programming", pass in: filterByFormula={Category}="Programming"`
),
fields: makeArraySchema(
"Only data for fields whose names or IDs are in this list will be included in the result. If you don't need every field, you can use this parameter to reduce the amount of data transferred.",
makeStringSchema("Field name")
),
},
}),
},
},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: makeObjectSchema("List records success body", {
optionalProperties: {
offset: makeStringSchema(
"offset",
"To fetch the next page of records, include offset from the previous request in the next request's parameters."
),
},
requiredProperties: {
records: makeArraySchema(
"Records",
makeObjectSchema("Record", {
requiredProperties: {
id: makeStringSchema("Record ID"),
createdTime: makeStringSchema(
"createdTime",
`A date timestamp in the ISO format, eg:"2018-01-01T00:00:00.000Z"`
),
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
})
),
},
}),
},
],
default: [errorResponse],
},
};
export const getRecord: EndpointSpec = {
path: "/{baseId}/{tableIdOrName}/{recordId}",
method: "GET",
metadata: {
name: "getRecord",
description:
'Retrieve a single record. Any "empty" fields (e.g. "", [], or false) in the record will not be returned.',
displayProperties: {
title:
"Get record ${parameters.recordId} from table ${parameters.tableIdOrName}",
},
externalDocs: {
description: "API method documentation",
url: "https://airtable.com/developers/web/api/get-record",
},
tags: ["records"],
},
security: {
oauth: ["data.records:read"],
},
parameters: [BaseIdParam, TableIdOrNameParam, RecordIdParam],
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: makeObjectSchema("Successful response", {
requiredProperties: {
createdTime: makeStringSchema(
"createdTime",
"When the record was created"
),
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
id: makeStringSchema("id", "The record id"),
},
}),
},
],
default: [errorResponse],
},
};
export const updateRecords: EndpointSpec = {
path: "/{baseId}/{tableIdOrName}",
method: "PATCH",
metadata: {
name: "updateRecords",
description: `Updates up to 10 records, or upserts them when performUpsert is set.`,
displayProperties: {
title: "Update records for table ${parameters.tableIdOrName}",
},
externalDocs: {
description: "API method documentation",
url: "https://airtable.com/developers/web/api/update-multiple-records",
},
tags: ["records"],
},
security: {
oauth: ["data.records:write"],
},
parameters: [BaseIdParam, TableIdOrNameParam],
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: {
schema: makeObjectSchema("Update records body", {
optionalProperties: {
performUpsert: makeObjectSchema("Perform upsert", {
requiredProperties: {
fieldsToMergeOn: makeArraySchema(
"Fields to merge on",
makeStringSchema("Field name")
),
},
}),
typecast: makeBooleanSchema(
"typecast",
"If set to true, Airtable will try to convert string values into the appropriate cell value. This conversion is only performed on a best-effort basis. To ensure your data's integrity, this should only be used when necessary. Defaults to false when unset."
),
},
requiredProperties: {
records: makeArraySchema(
"Records to update/upsert",
makeObjectSchema("Record", {
requiredProperties: {
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
optionalProperties: {
id: makeStringSchema(
"id",
"Record ID. Required when performUpsert is not set."
),
},
})
),
},
}),
},
},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: makeAnyOf("Update/upsert records success body", [
makeObjectSchema("Update response", {
requiredProperties: {
records: makeArraySchema(
"Records",
makeObjectSchema("Record", {
requiredProperties: {
id: makeStringSchema("Record ID"),
createdTime: makeStringSchema(
"createdTime",
`A date timestamp in the ISO format, eg:"2018-01-01T00:00:00.000Z"`
),
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
})
),
},
}),
makeObjectSchema("Upsert response", {
requiredProperties: {
createdRecords: makeArraySchema(
"Created records",
makeStringSchema("Record ID")
),
updatedRecords: makeArraySchema(
"Updated records",
makeStringSchema("Record ID")
),
records: makeArraySchema(
"Records",
makeObjectSchema("Record", {
requiredProperties: {
id: makeStringSchema("Record ID"),
createdTime: makeStringSchema(
"createdTime",
`A date timestamp in the ISO format, eg:"2018-01-01T00:00:00.000Z"`
),
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
})
),
},
}),
]),
},
],
default: [errorResponse],
},
};
export const updateRecord: EndpointSpec = {
path: "/{baseId}/{tableIdOrName}/{recordId}",
method: "PATCH",
metadata: {
name: "updateRecord",
description: `Updates a single record.`,
displayProperties: {
title:
"Update record ${parameters.recordId} for table ${parameters.tableIdOrName}",
},
externalDocs: {
description: "API method documentation",
url: "https://airtable.com/developers/web/api/update-record",
},
tags: ["records"],
},
security: {
oauth: ["data.records:write"],
},
parameters: [BaseIdParam, TableIdOrNameParam, RecordIdParam],
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: {
schema: makeObjectSchema("Update record body", {
optionalProperties: {
typecast: makeBooleanSchema(
"typecast",
"If set to true, Airtable will try to convert string values into the appropriate cell value. This conversion is only performed on a best-effort basis. To ensure your data's integrity, this should only be used when necessary. Defaults to false when unset."
),
},
requiredProperties: {
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
}),
},
},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: makeObjectSchema("Update record success body", {
requiredProperties: {
id: makeStringSchema("Record ID"),
createdTime: makeStringSchema(
"createdTime",
`A date timestamp in the ISO format, eg:"2018-01-01T00:00:00.000Z"`
),
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
}),
},
],
default: [errorResponse],
},
};
export const createRecords: EndpointSpec = {
path: "/{baseId}/{tableIdOrName}",
method: "POST",
metadata: {
name: "createRecords",
description: `Create up to 10 records`,
displayProperties: {
title: "Create records for table ${parameters.tableIdOrName}",
},
externalDocs: {
description: "API method documentation",
url: "https://airtable.com/developers/web/api/create-records",
},
tags: ["records"],
},
security: {
oauth: ["data.records:write"],
},
parameters: [BaseIdParam, TableIdOrNameParam],
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: {
schema: makeObjectSchema("Create records body", {
optionalProperties: {
typecast: makeBooleanSchema(
"typecast",
"If set to true, Airtable will try to convert string values into the appropriate cell value. This conversion is only performed on a best-effort basis. To ensure your data's integrity, this should only be used when necessary. Defaults to false when unset."
),
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
records: makeArraySchema(
"Records to update/upsert",
makeObjectSchema("Record", {
requiredProperties: {
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
})
),
},
}),
},
},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: makeAnyOf("Create records success body", [
makeObjectSchema("Multiple records created response", {
requiredProperties: {
records: makeArraySchema(
"Records",
makeObjectSchema("Record", {
requiredProperties: {
id: makeStringSchema("Record ID"),
createdTime: makeStringSchema(
"createdTime",
`A date timestamp in the ISO format, eg:"2018-01-01T00:00:00.000Z"`
),
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
})
),
},
}),
makeObjectSchema("Single record created response", {
requiredProperties: {
id: makeStringSchema("Record ID"),
createdTime: makeStringSchema(
"createdTime",
`A date timestamp in the ISO format, eg:"2018-01-01T00:00:00.000Z"`
),
fields: makeObjectSchema("Fields", {
additionalProperties: FieldSchema,
}),
},
}),
]),
},
],
default: [errorResponse],
},
};
export const deleteRecords: EndpointSpec = {
path: "/{baseId}/{tableIdOrName}",
method: "DELETE",
metadata: {
name: "deleteRecords",
description: `Delete more than one records with the given record IDs. Note you can't delete a single record with this.`,
displayProperties: {
title: "Delete records for table ${parameters.tableIdOrName}",
},
externalDocs: {
description: "API method documentation",
url: "https://airtable.com/developers/web/api/delete-multiple-records",
},
tags: ["records"],
},
security: {
oauth: ["data.records:write"],
},
parameters: [
BaseIdParam,
TableIdOrNameParam,
{
name: "records",
in: "query",
description: "An array of record IDs to delete",
schema: makeArraySchema(
"Records to delete",
makeStringSchema("Record ID")
),
required: true,
},
],
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: makeObjectSchema("Delete records response", {
requiredProperties: {
records: makeArraySchema(
"Records",
makeObjectSchema("Deleted record", {
requiredProperties: {
id: makeStringSchema("Record ID"),
deleted: makeBooleanSchema(
"deleted",
"Whether the record was deleted",
{
enum: true,
}
),
},
})
),
},
}),
},
],
default: [errorResponse],
},
};
export const deleteRecord: EndpointSpec = {
path: "/{baseId}/{tableIdOrName}/{recordId}",
method: "DELETE",
metadata: {
name: "deleteRecord",
description: `Delete a single record.`,
displayProperties: {
title:
"Delete record ${parameters.recordId} for table ${parameters.tableIdOrName}",
},
externalDocs: {
description: "API method documentation",
url: "https://airtable.com/developers/web/api/delete-record",
},
tags: ["records"],
},
security: {
oauth: ["data.records:write"],
},
parameters: [BaseIdParam, TableIdOrNameParam, RecordIdParam],
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: makeObjectSchema("Delete records response", {
requiredProperties: {
id: makeStringSchema("Record ID"),
deleted: makeBooleanSchema(
"deleted",
"Whether the record was deleted",
{
enum: true,
}
),
},
}),
},
],
default: [errorResponse],
},
};
@@ -0,0 +1,14 @@
import { Service } from "core/service/types";
import { authentication } from "./authentication";
import actions from "./actions/actions";
export const airtable: Service = {
name: "Airtable",
service: "airtable",
version: "0.1.21",
baseUrl: "https://api.airtable.com/v0",
live: true,
authentication,
actions,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
};
@@ -0,0 +1,356 @@
import { startNock, stopNock } from "testing/nock";
import { describe, expect, test } from "vitest";
import endpoints from "../endpoints/endpoints";
const authToken = () => process.env.AIRTABLE_TOKEN ?? "";
describe("airtable.endpoints", async () => {
test("listRecords simple", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.listRecords.simple");
const data = await endpoints.listRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
},
body: {},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:read"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("listRecords advanced", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.listRecords.advanced");
const data = await endpoints.listRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
},
body: {
timeZone: "America/Los_Angeles",
userLocale: "en",
sort: [
{
field: "Employee",
direction: "asc",
},
],
fields: ["Employee"],
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:read"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("getRecord", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.getRecord");
const data = await endpoints.getRecord.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
recordId: "recHcnB1MbBr9Rd2P",
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:read"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("updateRecords", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.updateRecords");
const data = await endpoints.updateRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
},
body: {
records: [
{
id: "recHcnB1MbBr9Rd2P",
fields: {
Employee: "John Doe",
},
},
],
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("upsertRecords", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.upsertRecords");
const data = await endpoints.updateRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
},
body: {
performUpsert: {
fieldsToMergeOn: ["Employee"],
},
records: [
{
fields: {
Employee: "John Doe",
},
},
{
fields: {
Employee: "Jane Doe",
},
},
],
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("updateRecord", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.updateRecord");
const data = await endpoints.updateRecord.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
recordId: "recHcnB1MbBr9Rd2P",
},
body: {
fields: {
Employee: "John Doe II",
},
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("createRecords", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.createRecords");
const data = await endpoints.createRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
},
body: {
records: [
{
fields: {
Employee: "Employee #1",
},
},
{
fields: {
Employee: "Employee #2",
},
},
],
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("createRecord", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.createRecord");
const data = await endpoints.createRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
},
body: {
fields: {
Employee: "Employee single create",
},
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("deleteRecords", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.deleteRecords");
const data = await endpoints.createRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
},
body: {
records: [
{
fields: {
Employee: "Delete now #1",
},
},
{
fields: {
Employee: "Delete now #2",
},
},
],
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
const deletedData = await endpoints.deleteRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
records: data.body.records.map((record: any) => record.id),
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(deletedData.status).toEqual(200);
expect(deletedData.success).toEqual(true);
expect(deletedData.body).not.toBeNull();
stopNock(nockDone);
});
test("deleteRecord", async () => {
const accessToken = authToken();
const nockDone = await startNock("airtable.deleteRecord");
const data = await endpoints.createRecords.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
},
body: {
fields: {
Employee: "Delete now #3",
},
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(data.status).toEqual(200);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
const deletedData = await endpoints.deleteRecord.request({
parameters: {
baseId: "appBlf3KsalIQeMUo",
tableIdOrName: "tblvXn2TOeVPC9c6m",
recordId: data.body.id,
},
credentials: {
type: "oauth2",
name: "oauth",
accessToken,
scopes: ["data.records:write"],
},
});
expect(deletedData.status).toEqual(200);
expect(deletedData.success).toEqual(true);
expect(deletedData.body).not.toBeNull();
stopNock(nockDone);
});
});
@@ -0,0 +1,10 @@
import { Catalog } from "core/catalog";
import { airtable } from "./airtable";
import { sendgrid } from "./sendgrid";
export const catalog: Catalog = {
services: {
airtable,
sendgrid,
},
};
@@ -0,0 +1,5 @@
import { makeSimpleActions } from "core/action/makeAction";
import endpoints from "../endpoints/endpoints";
const actions = makeSimpleActions(endpoints);
export default actions;
@@ -0,0 +1,215 @@
import { IntegrationAuthentication } from "core/authentication/types";
export const authentication: IntegrationAuthentication = {
api_key: {
type: "api_key",
placement: {
in: "header",
type: "bearer",
key: "Authorization",
},
documentation:
"1. [Create an API key](https://app.sendgrid.com/settings/api_keys)\n2. Copy the API key\n3. Paste the API key into the field below",
scopes: {
"alerts.create": "alerts.create",
"alerts.delete": "alerts.delete",
"alerts.read": "alerts.read",
"alerts.update": "alerts.update",
"api_keys.create": "api_keys.create",
"api_keys.delete": "api_keys.delete",
"api_keys.read": "api_keys.read",
"api_keys.update": "api_keys.update",
"asm.groups.create": "asm.groups.create",
"asm.groups.delete": "asm.groups.delete",
"asm.groups.read": "asm.groups.read",
"asm.groups.update": "asm.groups.update",
"categories.create": "categories.create",
"categories.delete": "categories.delete",
"categories.read": "categories.read",
"categories.update": "categories.update",
"categories.stats.read": "categories.stats.read",
"categories.stats.sums.read": "categories.stats.sums.read",
"email_activity.read": "email_activity.read",
"stats.read": "stats.read",
"stats.global.read": "stats.global.read",
"browsers.stats.read": "browsers.stats.read",
"devices.stats.read": "devices.stats.read",
"geo.stats.read": "geo.stats.read",
"mailbox_providers.stats.read": "mailbox_providers.stats.read",
"clients.desktop.stats.read": "clients.desktop.stats.read",
"clients.phone.stats.read": "clients.phone.stats.read",
"clients.stats.read": "clients.stats.read",
"clients.tablet.stats.read": "clients.tablet.stats.read",
"clients.webmail.stats.read": "clients.webmail.stats.read",
"ips.assigned.read": "ips.assigned.read",
"ips.read": "ips.read",
"ips.pools.create": "ips.pools.create",
"ips.pools.delete": "ips.pools.delete",
"ips.pools.read": "ips.pools.read",
"ips.pools.update": "ips.pools.update",
"ips.pools.ips.create": "ips.pools.ips.create",
"ips.pools.ips.delete": "ips.pools.ips.delete",
"ips.pools.ips.read": "ips.pools.ips.read",
"ips.pools.ips.update": "ips.pools.ips.update",
"ips.warmup.create": "ips.warmup.create",
"ips.warmup.delete": "ips.warmup.delete",
"ips.warmup.read": "ips.warmup.read",
"ips.warmup.update": "ips.warmup.update",
"mail_settings.address_whitelist.read":
"mail_settings.address_whitelist.read",
"mail_settings.address_whitelist.update":
"mail_settings.address_whitelist.update",
"mail_settings.bounce_purge.read": "mail_settings.bounce_purge.read",
"mail_settings.bounce_purge.update": "mail_settings.bounce_purge.update",
"mail_settings.footer.read": "mail_settings.footer.read",
"mail_settings.footer.update": "mail_settings.footer.update",
"mail_settings.forward_bounce.read": "mail_settings.forward_bounce.read",
"mail_settings.forward_bounce.update":
"mail_settings.forward_bounce.update",
"mail_settings.forward_spam.read": "mail_settings.forward_spam.read",
"mail_settings.forward_spam.update": "mail_settings.forward_spam.update",
"mail_settings.template.read": "mail_settings.template.read",
"mail_settings.template.update": "mail_settings.template.update",
"mail.batch.create": "mail.batch.create",
"mail.batch.delete": "mail.batch.delete",
"mail.batch.read": "mail.batch.read",
"mail.batch.update": "mail.batch.update",
"mail.send": "mail.send",
"marketing_campaigns.create": "marketing_campaigns.create",
"marketing_campaigns.delete": "marketing_campaigns.delete",
"marketing_campaigns.read": "marketing_campaigns.read",
"marketing_campaigns.update": "marketing_campaigns.update",
"partner_settings.new_relic.read": "partner_settings.new_relic.read",
"partner_settings.new_relic.update": "partner_settings.new_relic.update",
"partner_settings.read": "partner_settings.read",
"user.scheduled_sends.create": "user.scheduled_sends.create",
"user.scheduled_sends.delete": "user.scheduled_sends.delete",
"user.scheduled_sends.read": "user.scheduled_sends.read",
"user.scheduled_sends.update": "user.scheduled_sends.update",
"subusers.create": "subusers.create",
"subusers.delete": "subusers.delete",
"subusers.read": "subusers.read",
"subusers.update": "subusers.update",
"subusers.credits.create": "subusers.credits.create",
"subusers.credits.delete": "subusers.credits.delete",
"subusers.credits.read": "subusers.credits.read",
"subusers.credits.update": "subusers.credits.update",
"subusers.credits.remaining.create": "subusers.credits.remaining.create",
"subusers.credits.remaining.delete": "subusers.credits.remaining.delete",
"subusers.credits.remaining.read": "subusers.credits.remaining.read",
"subusers.credits.remaining.update": "subusers.credits.remaining.update",
"subusers.monitor.create": "subusers.monitor.create",
"subusers.monitor.delete": "subusers.monitor.delete",
"subusers.monitor.read": "subusers.monitor.read",
"subusers.monitor.update": "subusers.monitor.update",
"subusers.reputations.read": "subusers.reputations.read",
"subusers.stats.read": "subusers.stats.read",
"subusers.stats.monthly.read": "subusers.stats.monthly.read",
"subusers.stats.sums.read": "subusers.stats.sums.read",
"subusers.summary.read": "subusers.summary.read",
"suppression.create": "suppression.create",
"suppression.delete": "suppression.delete",
"suppression.read": "suppression.read",
"suppression.update": "suppression.update",
"suppression.bounces.create": "suppression.bounces.create",
"suppression.bounces.read": "suppression.bounces.read",
"suppression.bounces.update": "suppression.bounces.update",
"suppression.bounces.delete": "suppression.bounces.delete",
"suppression.blocks.create": "suppression.blocks.create",
"suppression.blocks.read": "suppression.blocks.read",
"suppression.blocks.update": "suppression.blocks.update",
"suppression.blocks.delete": "suppression.blocks.delete",
"suppression.invalid_emails.create": "suppression.invalid_emails.create",
"suppression.invalid_emails.read": "suppression.invalid_emails.read",
"suppression.invalid_emails.update": "suppression.invalid_emails.update",
"suppression.invalid_emails.delete": "suppression.invalid_emails.delete",
"suppression.spam_reports.create": "suppression.spam_reports.create",
"suppression.spam_reports.read": "suppression.spam_reports.read",
"suppression.spam_reports.update": "suppression.spam_reports.update",
"suppression.spam_reports.delete": "suppression.spam_reports.delete",
"suppression.unsubscribes.create": "suppression.unsubscribes.create",
"suppression.unsubscribes.read": "suppression.unsubscribes.read",
"suppression.unsubscribes.update": "suppression.unsubscribes.update",
"suppression.unsubscribes.delete": "suppression.unsubscribes.delete",
"teammates.create": "teammates.create",
"teammates.read": "teammates.read",
"teammates.update": "teammates.update",
"teammates.delete": "teammates.delete",
"templates.create": "templates.create",
"templates.delete": "templates.delete",
"templates.read": "templates.read",
"templates.update": "templates.update",
"templates.versions.activate.create":
"templates.versions.activate.create",
"templates.versions.activate.delete":
"templates.versions.activate.delete",
"templates.versions.activate.read": "templates.versions.activate.read",
"templates.versions.activate.update":
"templates.versions.activate.update",
"templates.versions.create": "templates.versions.create",
"templates.versions.delete": "templates.versions.delete",
"templates.versions.read": "templates.versions.read",
"templates.versions.update": "templates.versions.update",
"tracking_settings.click.read": "tracking_settings.click.read",
"tracking_settings.click.update": "tracking_settings.click.update",
"tracking_settings.google_analytics.read":
"tracking_settings.google_analytics.read",
"tracking_settings.google_analytics.update":
"tracking_settings.google_analytics.update",
"tracking_settings.open.read": "tracking_settings.open.read",
"tracking_settings.open.update": "tracking_settings.open.update",
"tracking_settings.read": "tracking_settings.read",
"tracking_settings.subscription.read":
"tracking_settings.subscription.read",
"tracking_settings.subscription.update":
"tracking_settings.subscription.update",
"user.account.read": "user.account.read",
"user.credits.read": "user.credits.read",
"user.email.create": "user.email.create",
"user.email.delete": "user.email.delete",
"user.email.read": "user.email.read",
"user.email.update": "user.email.update",
"user.multifactor_authentication.create":
"user.multifactor_authentication.create",
"user.multifactor_authentication.delete":
"user.multifactor_authentication.delete",
"user.multifactor_authentication.read":
"user.multifactor_authentication.read",
"user.multifactor_authentication.update":
"user.multifactor_authentication.update",
"user.password.read": "user.password.read",
"user.password.update": "user.password.update",
"user.profile.read": "user.profile.read",
"user.profile.update": "user.profile.update",
"user.settings.enforced_tls.read": "user.settings.enforced_tls.read",
"user.settings.enforced_tls.update": "user.settings.enforced_tls.update",
"user.timezone.read": "user.timezone.read",
"user.timezone.update": "user.timezone.update",
"user.username.read": "user.username.read",
"user.username.update": "user.username.update",
"user.webhooks.event.settings.read": "user.webhooks.event.settings.read",
"user.webhooks.event.settings.update":
"user.webhooks.event.settings.update",
"user.webhooks.event.test.create": "user.webhooks.event.test.create",
"user.webhooks.event.test.read": "user.webhooks.event.test.read",
"user.webhooks.event.test.update": "user.webhooks.event.test.update",
"user.webhooks.parse.settings.create":
"user.webhooks.parse.settings.create",
"user.webhooks.parse.settings.delete":
"user.webhooks.parse.settings.delete",
"user.webhooks.parse.settings.read": "user.webhooks.parse.settings.read",
"user.webhooks.parse.settings.update":
"user.webhooks.parse.settings.update",
"user.webhooks.parse.stats.read": "user.webhooks.parse.stats.read",
"whitelabel.create": "whitelabel.create",
"whitelabel.delete": "whitelabel.delete",
"whitelabel.read": "whitelabel.read",
"whitelabel.update": "whitelabel.update",
"access_settings.activity.read": "access_settings.activity.read",
"access_settings.whitelist.create": "access_settings.whitelist.create",
"access_settings.whitelist.delete": "access_settings.whitelist.delete",
"access_settings.whitelist.read": "access_settings.whitelist.read",
"access_settings.whitelist.update": "access_settings.whitelist.update",
},
},
};
@@ -0,0 +1,191 @@
import {
makeArraySchema,
makeObjectSchema,
makeStringSchema,
} from "core/schemas/makeSchema";
import { JSONSchema } from "core/schemas/types";
export const ErrorSchema = makeObjectSchema("Error", {
requiredProperties: {
errors: makeArraySchema(
"Errors",
makeObjectSchema("Error", {
requiredProperties: {
message: makeStringSchema("Message"),
},
optionalProperties: {
field: makeStringSchema("Field"),
help: makeStringSchema("Help"),
error_id: makeStringSchema("Error ID"),
parameter: makeStringSchema("Parameter"),
},
})
),
},
});
export const fromEmailObjectSchema: JSONSchema = {
title: "From Email Object",
type: "object",
properties: {
email: {
type: "string",
format: "email",
description:
"The 'From' email address used to deliver the message. This address should be a verified sender in your Twilio SendGrid account.",
},
name: {
type: "string",
description: "A name or title associated with the sending email address.",
},
},
required: ["email"],
example: {
email: "jane_doe@example.com",
name: "Jane Doe",
},
};
export const ToEmailArraySchema: JSONSchema = {
title: "To Email Array",
type: "array",
items: {
type: "object",
properties: {
email: {
type: "string",
format: "email",
description: "The intended recipient's email address.",
},
name: {
type: "string",
description: "The intended recipient's name.",
},
},
required: ["email"],
},
example: [
{
email: "john_doe@example.com",
name: "John Doe",
},
],
};
export const CCBCCEmailObjectSchema: JSONSchema = {
title: "CC BCC Email Object",
type: "object",
properties: {
email: {
type: "string",
format: "email",
description: "The intended recipient's email address.",
},
name: {
type: "string",
description: "The intended recipient's name.",
},
},
required: ["email"],
example: {
email: "jane_doe@example.com",
name: "Jane Doe",
},
};
export const ReplyToEmailObjectSchema: JSONSchema = {
title: "Reply_to Email Object",
type: "object",
properties: {
email: {
type: "string",
format: "email",
description:
"The email address where any replies or bounces will be returned.",
},
name: {
type: "string",
description:
"A name or title associated with the `reply_to` email address.",
},
},
required: ["email"],
example: {
email: "jane_doe@example.com",
name: "Jane Doe",
},
};
export const ContactRequestSchema: JSONSchema = {
title: "contact-request",
type: "object",
properties: {
address_line_1: {
type: "string",
description: "The first line of the address.",
maxLength: 100,
},
address_line_2: {
type: "string",
description: "An optional second line for the address.",
maxLength: 100,
},
alternate_emails: {
type: "array",
description: "Additional emails associated with the contact.",
minItems: 0,
maxItems: 5,
items: {
type: "string",
maxLength: 254,
},
},
city: {
type: "string",
description: "The contact's city.",
maxLength: 60,
},
country: {
type: "string",
description:
"The contact's country. Can be a full name or an abbreviation.",
maxLength: 50,
},
email: {
type: "string",
description:
"The contact's primary email. This is required to be a valid email.",
maxLength: 254,
},
first_name: {
type: "string",
description: "The contact's personal name.",
maxLength: 50,
},
last_name: {
type: "string",
description: "The contact's family name.",
maxLength: 50,
},
postal_code: {
type: "string",
description: "The contact's ZIP code or other postal code.",
},
state_province_region: {
type: "string",
description: "The contact's state, province, or region.",
maxLength: 50,
},
custom_fields: {
title: "custom-fields-by-id",
type: "object",
additionalProperties: true,
example: {
w1: "2002-10-02T15:00:00Z",
w33: 9.5,
e2: "Coffee is a beverage that puts one to sleep when not drank.",
},
},
},
required: ["email"],
};
@@ -0,0 +1,8 @@
import { makeEndpoints } from "core/endpoint/endpoint";
import { authentication } from "../authentication";
import * as specs from "./specs";
const baseUrl = "https://api.sendgrid.com/v3";
const endpoints = makeEndpoints(baseUrl, authentication, specs);
export default endpoints;
@@ -0,0 +1,540 @@
import { EndpointSpec } from "core/endpoint/types";
import {
CCBCCEmailObjectSchema,
ContactRequestSchema,
ErrorSchema,
fromEmailObjectSchema,
ReplyToEmailObjectSchema,
ToEmailArraySchema,
} from "../common/schemas";
const defaultResponses = [
{
success: false,
name: "Error",
description: "error response",
schema: ErrorSchema,
},
{
success: false,
name: "Error",
description: "No body error response",
schema: {},
},
];
export const mailSend: EndpointSpec = {
path: "/mail/send",
method: "POST",
metadata: {
name: "mailSend",
description: "Send email to one or more recipients with personalization",
displayProperties: {
title: "Send mail",
},
externalDocs: {
description: "API method documentation",
url: "https://docs.sendgrid.com/api-reference/mail-send/mail-send",
},
tags: ["send"],
},
security: {
api_key: ["mail.send"],
},
request: {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: {
schema: {
type: "object",
properties: {
personalizations: {
type: "array",
description:
"An array of messages and their metadata. Each object within personalizations can be thought of as an envelope - it defines who should receive an individual message and how that message should be handled. See our [Personalizations documentation](https://sendgrid.com/docs/for-developers/sending-email/personalizations/) for examples.",
uniqueItems: false,
maxItems: 1000,
items: {
type: "object",
properties: {
from: fromEmailObjectSchema,
to: ToEmailArraySchema,
cc: {
type: "array",
description:
"An array of recipients who will receive a copy of your email. Each object in this array must contain the recipient's email address. Each object in the array may optionally contain the recipient's name.",
maxItems: 1000,
items: CCBCCEmailObjectSchema,
},
bcc: {
type: "array",
description:
"An array of recipients who will receive a blind carbon copy of your email. Each object in this array must contain the recipient's email address. Each object in the array may optionally contain the recipient's name.",
maxItems: 1000,
items: CCBCCEmailObjectSchema,
},
subject: {
type: "string",
description:
"The subject of your email. See character length requirements according to [RFC 2822](http://stackoverflow.com/questions/1592291/what-is-the-email-subject-length-limit#answer-1592310).",
minLength: 1,
},
headers: {
type: "object",
description:
"A collection of JSON key/value pairs allowing you to specify handling instructions for your email. You may not overwrite the following headers: `x-sg-id`, `x-sg-eid`, `received`, `dkim-signature`, `Content-Type`, `Content-Transfer-Encoding`, `To`, `From`, `Subject`, `Reply-To`, `CC`, `BCC`",
},
substitutions: {
type: "object",
description:
'Substitutions allow you to insert data without using Dynamic Transactional Templates. This field should **not** be used in combination with a Dynamic Transactional Template, which can be identified by a `template_id` starting with `d-`. This field is a collection of key/value pairs following the pattern "substitution_tag":"value to substitute". The key/value pairs must be strings. These substitutions will apply to the text and html content of the body of your email, in addition to the `subject` and `reply-to` parameters. The total collective size of your substitutions may not exceed 10,000 bytes per personalization object.',
maxProperties: 10000,
},
dynamic_template_data: {
type: "object",
description:
'Dynamic template data is available using Handlebars syntax in Dynamic Transactional Templates. This field should be used in combination with a Dynamic Transactional Template, which can be identified by a `template_id` starting with `d-`. This field is a collection of key/value pairs following the pattern "variable_name":"value to insert".',
},
custom_args: {
type: "object",
description:
"Values that are specific to this personalization that will be carried along with the email and its activity data. Substitutions will not be made on custom arguments, so any string that is entered into this parameter will be assumed to be the custom argument that you would like to be used. This field may not exceed 10,000 bytes.",
maxProperties: 10000,
},
send_at: {
type: "integer",
description:
"A unix timestamp allowing you to specify when your email should be delivered. Scheduling delivery more than 72 hours in advance is forbidden.",
},
},
required: ["to"],
},
},
from: fromEmailObjectSchema,
reply_to: ReplyToEmailObjectSchema,
reply_to_list: {
type: "array",
description:
"An array of recipients who will receive replies and/or bounces. Each object in this array must contain the recipient's email address. Each object in the array may optionally contain the recipient's name. You can either choose to use “reply_to” field or “reply_to_list” but not both.",
uniqueItems: true,
maxItems: 1000,
items: {
type: "object",
properties: {
email: {
type: "string",
description:
"The email address where any replies or bounces will be returned.",
format: "email",
},
name: {
type: "string",
description:
"A name or title associated with the `reply_to_list` email address.",
},
},
required: ["email"],
},
},
subject: {
type: "string",
description:
"The global or 'message level' subject of your email. This may be overridden by subject lines set in personalizations.",
minLength: 1,
},
content: {
type: "array",
description:
"An array where you can specify the content of your email. You can include multiple [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) of content, but you must specify at least one MIME type. To include more than one MIME type, add another object to the array containing the `type` and `value` parameters.",
items: {
type: "object",
properties: {
type: {
type: "string",
description:
"The MIME type of the content you are including in your email (e.g., `“text/plain”` or `“text/html”`).",
minLength: 1,
},
value: {
type: "string",
description:
"The actual content of the specified MIME type that you are including in your email.",
minLength: 1,
},
},
required: ["type", "value"],
},
},
attachments: {
type: "array",
description:
"An array of objects where you can specify any attachments you want to include.",
items: {
type: "object",
properties: {
content: {
type: "string",
description: "The Base64 encoded content of the attachment.",
minLength: 1,
},
type: {
type: "string",
description:
"The MIME type of the content you are attaching (e.g., `“text/plain”` or `“text/html”`).",
minLength: 1,
},
filename: {
type: "string",
description: "The attachment's filename.",
},
disposition: {
type: "string",
default: "attachment",
description:
"The attachment's content-disposition, specifying how you would like the attachment to be displayed. For example, `“inline”` results in the attached file are displayed automatically within the message while `“attachment”` results in the attached file require some action to be taken before it is displayed, such as opening or downloading the file.",
enum: ["inline", "attachment"],
},
content_id: {
type: "string",
description:
"The attachment's content ID. This is used when the disposition is set to `“inline”` and the attachment is an image, allowing the file to be displayed within the body of your email.",
},
},
required: ["content", "filename"],
},
},
template_id: {
type: "string",
description:
"An email template ID. A template that contains a subject and content — either text or html — will override any subject and content values specified at the personalizations or message level.",
},
headers: {
description:
"An object containing key/value pairs of header names and the value to substitute for them. The key/value pairs must be strings. You must ensure these are properly encoded if they contain unicode characters. These headers cannot be one of the reserved headers.",
type: "object",
},
categories: {
type: "array",
description:
"An array of category names for this message. Each category name may not exceed 255 characters. ",
uniqueItems: true,
maxItems: 10,
items: {
type: "string",
maxLength: 255,
},
},
custom_args: {
description:
"Values that are specific to the entire send that will be carried along with the email and its activity data. Key/value pairs must be strings. Substitutions will not be made on custom arguments, so any string that is entered into this parameter will be assumed to be the custom argument that you would like to be used. This parameter is overridden by `custom_args` set at the personalizations level. Total `custom_args` size may not exceed 10,000 bytes.",
type: "string",
},
send_at: {
type: "integer",
description:
"A unix timestamp allowing you to specify when you want your email to be delivered. This may be overridden by the `send_at` parameter set at the personalizations level. Delivery cannot be scheduled more than 72 hours in advance. If you have the flexibility, it's better to schedule mail for off-peak times. Most emails are scheduled and sent at the top of the hour or half hour. Scheduling email to avoid peak times — for example, scheduling at 10:53 — can result in lower deferral rates due to the reduced traffic during off-peak times.",
},
batch_id: {
type: "string",
description:
"An ID representing a batch of emails to be sent at the same time. Including a `batch_id` in your request allows you include this email in that batch. It also enables you to cancel or pause the delivery of that batch. For more information, see the [Cancel Scheduled Sends API](https://sendgrid.com/docs/api-reference/).",
},
asm: {
type: "object",
description:
"An object allowing you to specify how to handle unsubscribes.",
properties: {
group_id: {
type: "integer",
description:
"The unsubscribe group to associate with this email.",
},
groups_to_display: {
type: "array",
description:
"An array containing the unsubscribe groups that you would like to be displayed on the unsubscribe preferences page.",
maxItems: 25,
items: {
type: "integer",
},
},
},
required: ["group_id"],
},
ip_pool_name: {
type: "string",
description:
"The IP Pool that you would like to send this email from.",
minLength: 2,
maxLength: 64,
},
mail_settings: {
type: "object",
description:
"A collection of different mail settings that you can use to specify how you would like this email to be handled.",
properties: {
bypass_list_management: {
type: "object",
description:
"Allows you to bypass all unsubscribe groups and suppressions to ensure that the email is delivered to every single recipient. This should only be used in emergencies when it is absolutely necessary that every recipient receives your email. This filter cannot be combined with any other bypass filters. See our [documentation](https://sendgrid.com/docs/ui/sending-email/index-suppressions/#bypass-suppressions) for more about bypass filters.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
},
},
bypass_spam_management: {
type: "object",
description:
"Allows you to bypass the spam report list to ensure that the email is delivered to recipients. Bounce and unsubscribe lists will still be checked; addresses on these other lists will not receive the message. This filter cannot be combined with the `bypass_list_management` filter. See our [documentation](https://sendgrid.com/docs/ui/sending-email/index-suppressions/#bypass-suppressions) for more about bypass filters.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
},
},
bypass_bounce_management: {
type: "object",
description:
"Allows you to bypass the bounce list to ensure that the email is delivered to recipients. Spam report and unsubscribe lists will still be checked; addresses on these other lists will not receive the message. This filter cannot be combined with the `bypass_list_management` filter. See our [documentation](https://sendgrid.com/docs/ui/sending-email/index-suppressions/#bypass-suppressions) for more about bypass filters.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
},
},
bypass_unsubscribe_management: {
type: "object",
description:
"Allows you to bypass the global unsubscribe list to ensure that the email is delivered to recipients. Bounce and spam report lists will still be checked; addresses on these other lists will not receive the message. This filter applies only to global unsubscribes and will not bypass group unsubscribes. This filter cannot be combined with the `bypass_list_management` filter. See our [documentation](https://sendgrid.com/docs/ui/sending-email/index-suppressions/#bypass-suppressions) for more about bypass filters.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
},
},
footer: {
type: "object",
description:
"The default footer that you would like included on every email.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
text: {
type: "string",
description: "The plain text content of your footer.",
},
html: {
type: "string",
description: "The HTML content of your footer.",
},
},
},
sandbox_mode: {
type: "object",
description:
"Sandbox Mode allows you to send a test email to ensure that your request body is valid and formatted correctly.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
},
},
},
},
tracking_settings: {
type: "object",
description:
"Settings to determine how you would like to track the metrics of how your recipients interact with your email.",
properties: {
click_tracking: {
type: "object",
description:
"Allows you to track if a recipient clicked a link in your email.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
enable_text: {
type: "boolean",
description:
"Indicates if this setting should be included in the `text/plain` portion of your email.",
},
},
},
open_tracking: {
type: "object",
description:
"Allows you to track if the email was opened by including a single pixel image in the body of the content. When the pixel is loaded, Twilio SendGrid can log that the email was opened.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
substitution_tag: {
type: "string",
description:
"Allows you to specify a substitution tag that you can insert in the body of your email at a location that you desire. This tag will be replaced by the open tracking pixel.",
},
},
},
subscription_tracking: {
type: "object",
description:
"Allows you to insert a subscription management link at the bottom of the text and HTML bodies of your email. If you would like to specify the location of the link within your email, you may use the `substitution_tag`.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
text: {
type: "string",
description:
"Text to be appended to the email with the subscription tracking link. You may control where the link is by using the tag <% %>",
},
html: {
type: "string",
description:
"HTML to be appended to the email with the subscription tracking link. You may control where the link is by using the tag <% %>",
},
substitution_tag: {
type: "string",
description:
"A tag that will be replaced with the unsubscribe URL. for example: `[unsubscribe_url]`. If this parameter is used, it will override both the `text` and `html` parameters. The URL of the link will be placed at the substitution tags location with no additional formatting.",
},
},
},
ganalytics: {
type: "object",
description:
"Allows you to enable tracking provided by Google Analytics.",
properties: {
enable: {
type: "boolean",
description: "Indicates if this setting is enabled.",
},
utm_source: {
type: "string",
description:
"Name of the referrer source. (e.g. Google, SomeDomain.com, or Marketing Email)",
},
utm_medium: {
type: "string",
description: "Name of the marketing medium. (e.g. Email)",
},
utm_term: {
type: "string",
description: "Used to identify any paid keywords.",
},
utm_content: {
type: "string",
description:
"Used to differentiate your campaign from advertisements.",
},
utm_campaign: {
type: "string",
description: "The name of the campaign.",
},
},
},
},
},
},
required: ["personalizations", "from", "subject", "content"],
},
},
},
responses: {
"20x": [
{
success: true,
name: "Success",
description: "Successful response",
schema: undefined,
},
],
default: defaultResponses,
},
};
export const marketingContacts: EndpointSpec = {
path: "/marketing/contacts",
method: "PUT",
metadata: {
name: "marketingContacts",
description:
"Add or update (up to 30k) contacts. Contacts are queued and aren't created immediately.",
displayProperties: {
title: "Add/update contacts",
},
externalDocs: {
description: "API method documentation",
url: "https://docs.sendgrid.com/api-reference/contacts/add-or-update-a-contact",
},
tags: ["contacts"],
},
security: {
api_key: [],
},
request: {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: {
schema: {
type: "object",
properties: {
list_ids: {
type: "array",
description:
"An array of List ID strings that this contact will be added to.",
items: {
type: "string",
format: "uuid",
},
},
contacts: {
type: "array",
description:
"One or more contacts objects that you intend to upsert. The available fields for a contact, including the required `email` field are described below.",
minItems: 1,
maxItems: 30000,
items: ContactRequestSchema,
},
},
required: ["contacts"],
},
},
},
responses: {
"20x": [
{
success: true,
name: "Success",
description: "Successful response",
schema: {
type: "object",
properties: {
job_id: {
type: "string",
description:
'Indicates that the contacts are queued for processing. Check the job status with the "Import Contacts Status" endpoint.',
},
},
},
},
],
default: defaultResponses,
},
};
@@ -0,0 +1,14 @@
import { Service } from "core/service/types";
import { authentication } from "./authentication";
import actions from "./actions/actions";
export const sendgrid: Service = {
name: "SendGrid",
service: "sendgrid",
version: "0.1.21",
baseUrl: "https://api.sendgrid.com",
live: true,
authentication,
actions,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
};
@@ -0,0 +1,77 @@
import { startNock, stopNock } from "testing/nock";
import { describe, expect, test } from "vitest";
import endpoints from "../endpoints/endpoints";
const authToken = () => process.env.SENDGRID_API_KEY ?? "";
describe("sendgrid.endpoints", async () => {
test("mailSend simple", async () => {
const api_key = authToken();
const nockDone = await startNock("sendgrid.mailSend");
const data = await endpoints.mailSend.request({
body: {
from: {
email: "matt@email.trigger.dev",
},
subject: "Hello, World!",
content: [
{
type: "text/plain",
value: "Email body here",
},
],
personalizations: [
{
to: [
{
email: "matt@trigger.dev",
},
],
subject: "Hello, World!",
},
],
},
credentials: {
type: "api_key",
name: "api_key",
api_key,
scopes: ["mail.send"],
},
});
expect(data.status).toBeGreaterThanOrEqual(200);
expect(data.status).toBeLessThan(300);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
test("marketingContacts", async () => {
const api_key = authToken();
const nockDone = await startNock("sendgrid.marketingContacts");
const data = await endpoints.marketingContacts.request({
body: {
contacts: [
{
email: "matt+1@mattaitken.com",
first_name: "Matt",
last_name: "Aitken",
},
],
},
credentials: {
type: "api_key",
name: "api_key",
api_key,
scopes: ["mail.send"],
},
});
expect(data.status).toBeGreaterThanOrEqual(200);
expect(data.status).toBeLessThan(300);
expect(data.success).toEqual(true);
expect(data.body).not.toBeNull();
stopNock(nockDone);
});
});
@@ -0,0 +1,165 @@
import { makeAdvancedAction, makeSimpleAction } from "core/action/makeAction";
import { Action } from "core/action/types";
import {
combineSecurityScopes,
makeInputSpec,
makeOutputSpec,
} from "core/action/utilities";
import { CacheService } from "core/cache/types";
import { RequestData } from "core/request/types";
import endpoints from "../endpoints/endpoints";
export const conversationsList: Action = makeSimpleAction(
endpoints.conversationsList
);
export const chatPostMessage: Action = makeAdvancedAction({
endpoint: endpoints.chatPostMessage,
spec: {
input: {
...makeInputSpec(endpoints.chatPostMessage),
security: combineSecurityScopes([
endpoints.chatPostMessage.spec.endpointSpec.security,
endpoints.conversationsList.spec.endpointSpec.security,
endpoints.conversationsJoin.spec.endpointSpec.security,
]),
},
output: makeOutputSpec(endpoints.chatPostMessage),
},
action: async (data, cache, metadata) => {
//get channel id
const channelId = await getChannelId({
channel: data.body.channel,
credentials: data.credentials,
cache,
});
if (!channelId) {
return {
success: false,
status: 404,
body: {
ok: false,
error: "channel_not_found",
},
};
}
//we add __trigger metadata so we can associate messages with workflow runs
let bodyMetadata: {
event_payload?: any;
event_type: string;
} = {
event_type: "post_message",
};
if (metadata) {
bodyMetadata = {
...bodyMetadata,
event_payload: {
...(data.body?.metadata ?? {}),
__trigger: metadata,
},
};
} else {
bodyMetadata = {
...bodyMetadata,
event_payload: {
...(data.body?.metadata ?? {}),
},
};
}
const postMessageBody = {
...data.body,
channel: channelId,
metadata: metadata ? bodyMetadata : undefined,
};
const postResponse = await endpoints.chatPostMessage.request({
parameters: data.parameters,
body: postMessageBody,
credentials: data.credentials,
});
//success
if (postResponse.success) return postResponse;
//if the bot isn't a member of the channel we want to invite
if (postResponse.body?.error !== "not_in_channel") return postResponse;
//join the bot to the channel
const joinResponse = await endpoints.conversationsJoin.request({
body: {
channel: channelId,
},
credentials: data.credentials,
});
//if we can't join the channel, return the error
if (!joinResponse.success) {
return {
success: false,
status: 400,
body: {
ok: false,
error: `failed to join channel: ${joinResponse.body?.error}`,
},
};
}
//try again now we've joined the channel
return await endpoints.chatPostMessage.request({
parameters: data.parameters,
body: postMessageBody,
credentials: data.credentials,
});
},
});
async function getChannelId({
channel,
credentials,
cache,
}: {
channel: string;
credentials: RequestData["credentials"];
cache?: CacheService;
}): Promise<string | undefined> {
if (channel.startsWith("#")) {
channel = channel.substring(1);
}
const cachedChannelId = await cache?.get(channel);
if (cachedChannelId) {
return cachedChannelId;
}
try {
const data = await endpoints.conversationsList.request({
parameters: {
limit: 1000,
},
credentials,
});
if (!data.success) {
return undefined;
}
//lookup by name or id
const match = data.body.channels.find(
(channelData: any) =>
channelData.name === channel || channelData.id === channel
);
if (!match) {
return undefined;
}
//cache for 24 hours
await cache?.set(channel, match.id, 60 * 60 * 24);
return match.id;
} catch (e: any) {
console.error(JSON.stringify(e, null, 2));
return undefined;
}
}
@@ -0,0 +1,84 @@
import { IntegrationAuthentication } from "core/authentication/types";
export const authentication: IntegrationAuthentication = {
slackAuth: {
type: "oauth2",
placement: {
in: "header",
type: "bearer",
key: "Authorization",
},
authorizationUrl: "https://slack.com/oauth/authorize",
tokenUrl: "https://slack.com/api/oauth.access",
flow: "accessCode",
scopes: {
admin: "admin",
"admin.apps:read": "admin.apps:read",
"admin.apps:write": "admin.apps:write",
"admin.conversations:read": "admin.conversations:read",
"admin.conversations:write": "admin.conversations:write",
"admin.invites:read": "admin.invites:read",
"admin.invites:write": "admin.invites:write",
"admin.teams:read": "admin.teams:read",
"admin.teams:write": "admin.teams:write",
"admin.usergroups:read": "admin.usergroups:read",
"admin.usergroups:write": "admin.usergroups:write",
"admin.users:read": "admin.users:read",
"admin.users:write": "admin.users:write",
"authorizations:read": "authorizations:read",
bot: "Bot user scope",
"calls:read": "calls:read",
"calls:write": "calls:write",
"channels:history": "channels:history",
"channels:manage": "channels:manage",
"channels:read": "channels:read",
"channels:write": "channels:write",
"chat:write": "chat:write",
"chat:write:bot": "Author messages as a bot",
"chat:write:user": "Author messages as a user",
"conversations:history": "conversations:history",
"conversations:read": "conversations:read",
"conversations:write": "conversations:write",
"dnd:read": "dnd:read",
"dnd:write": "dnd:write",
"emoji:read": "emoji:read",
"files:read": "files:read",
"files:write:user": "files:write:user",
"groups:history": "groups:history",
"groups:read": "groups:read",
"groups:write": "groups:write",
"identity.basic": "identity.basic",
"im:history": "im:history",
"im:read": "im:read",
"im:write": "im:write",
"links:write": "links:write",
"mpim:history": "mpim:history",
"mpim:read": "mpim:read",
"mpim:write": "mpim:write",
none: "No scope required",
"pins:read": "pins:read",
"pins:write": "pins:write",
"reactions:read": "reactions:read",
"reactions:write": "reactions:write",
"reminders:read": "reminders:read",
"reminders:write": "reminders:write",
"remote_files:read": "remote_files:read",
"remote_files:share": "remote_files:share",
"remote_files:write": "remote_files:write",
"rtm:stream": "rtm:stream",
"search:read": "search:read",
"stars:read": "stars:read",
"stars:write": "stars:write",
"team:read": "team:read",
"tokens.basic": "tokens.basic",
"usergroups:read": "usergroups:read",
"usergroups:write": "usergroups:write",
"users.profile:read": "users.profile:read",
"users.profile:write": "users.profile:write",
"users:read": "users:read",
"users:read.email": "users:read.email",
"users:write": "users:write",
"workflow.steps:execute": "workflow.steps:execute",
},
},
};
@@ -0,0 +1,8 @@
import { makeEndpoints } from "core/endpoint/endpoint";
import { authentication } from "../authentication";
import * as specs from "./specs";
const baseUrl = "https://slack.com/api";
const endpoints = makeEndpoints(baseUrl, authentication, specs);
export default endpoints;
@@ -0,0 +1,3 @@
import { dereferenceSpec } from "core/schemas/openApi";
import rawSpec from "./slack_web_openapi_v2.json";
export const spec = dereferenceSpec(rawSpec);
@@ -0,0 +1,359 @@
import { EndpointSpec, EndpointSpecResponse } from "core/endpoint/types";
import { schemaFromOpenApiSpecV2 } from "core/schemas/openApi";
import { spec } from "./schemas/spec";
const errorResponse: EndpointSpecResponse = {
success: false,
name: "Error",
description: "200 error response",
schema: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
ok: {
type: "boolean",
enum: [false],
},
error: {
type: "string",
},
},
required: ["ok", "error"],
},
};
export const chatPostMessage: EndpointSpec = {
path: "/chat.postMessage",
method: "POST",
metadata: {
name: "postMessage",
description: "Post a message to a channel",
displayProperties: {
title: "Post message to ${body.channel}",
},
externalDocs: {
description: "API method documentation",
url: "https://api.slack.com/methods/chat.postMessage",
},
tags: ["chat"],
},
security: {
slackAuth: ["chat:write:user", "chat:write:bot"],
},
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: {
schema: {
type: "object",
required: ["channel"],
properties: {
as_user: {
type: "string",
description:
"Pass true to post the message as the authed user, instead of as a bot. Defaults to false. See [authorship](#authorship) below.",
},
attachments: {
type: "string",
description:
"A JSON-based array of structured attachments, presented as a URL-encoded string.",
},
blocks: {
type: "string",
description:
"A JSON-based array of structured blocks, presented as a URL-encoded string.",
},
channel: {
type: "string",
description:
"Channel, private group, or IM channel to send message to. Can be an encoded ID, or a name. See [below](#channels) for more details.",
},
icon_emoji: {
type: "string",
description:
"Emoji to use as the icon for this message. Overrides `icon_url`. Must be used in conjunction with `as_user` set to `false`, otherwise ignored. See [authorship](#authorship) below.",
},
icon_url: {
type: "string",
description:
"URL to an image to use as the icon for this message. Must be used in conjunction with `as_user` set to false, otherwise ignored. See [authorship](#authorship) below.",
},
link_names: {
type: "boolean",
description: "Find and link channel names and usernames.",
},
mrkdwn: {
type: "boolean",
description:
"Disable Slack markup parsing by setting to `false`. Enabled by default.",
},
parse: {
type: "string",
description:
"Change how messages are treated. Defaults to `none`. See [below](#formatting).",
},
reply_broadcast: {
type: "boolean",
description:
"Used in conjunction with `thread_ts` and indicates whether reply should be made visible to everyone in the channel or conversation. Defaults to `false`.",
},
text: {
type: "string",
description:
"How this field works and whether it is required depends on other fields you use in your API call. [See below](#text_usage) for more detail.",
},
thread_ts: {
type: "string",
description:
"Provide another message's `ts` value to make this message a reply. Avoid using a reply's `ts` value; use its parent instead.",
},
unfurl_links: {
type: "boolean",
description:
"Pass true to enable unfurling of primarily text-based content.",
},
unfurl_media: {
type: "boolean",
description: "Pass false to disable unfurling of media content.",
},
username: {
type: "string",
description:
"Set your bot's user name. Must be used in conjunction with `as_user` set to false, otherwise ignored. See [authorship](#authorship) below.",
},
},
},
},
},
responses: {
200: [
{
success: true,
name: "Success",
description: "Typical success response",
schema: {
type: "object",
properties: {
channel: {
description: "Channel ID where the message was posted",
type: "string",
},
message: {
type: "object",
properties: {
attachments: {
type: "array",
items: {
type: "object",
properties: {
fallback: {
type: "string",
},
id: {
type: "number",
},
text: {
type: "string",
},
},
required: ["fallback", "id", "text"],
},
},
bot_id: {
type: "string",
},
subtype: {
type: "string",
},
text: {
type: "string",
},
ts: {
type: "string",
},
type: {
type: "string",
},
user: {
type: "string",
},
},
required: ["bot_id", "text", "ts", "type"],
},
ok: {
type: "boolean",
},
ts: {
type: "string",
},
},
required: ["channel", "message", "ok", "ts"],
},
},
errorResponse,
],
default: [
{
success: false,
name: "Error",
description:
"Typical error response if too many attachments are included",
schema: {
description: "Schema for error response chat.postMessage method",
type: "object",
properties: {
error: {
type: "string",
},
ok: {
type: "boolean",
},
},
required: ["error", "ok"],
},
},
],
},
};
export const conversationsList: EndpointSpec = {
path: "/conversations.list",
method: "GET",
metadata: {
name: "conversationsList",
description: "Lists all channels in a Slack team.",
displayProperties: {
title: "List channels",
},
externalDocs: {
description: "API method documentation",
url: "https://api.slack.com/methods/conversations.list",
},
tags: ["conversations"],
},
security: {
slackAuth: ["conversations:read"],
},
parameters: [
{
name: "exclude_archived",
in: "query",
description: "Set to `true` to exclude archived channels from the list",
schema: {
type: "boolean",
},
},
{
name: "types",
in: "query",
description:
"Mix and match channel types by providing a comma-separated list of any combination of `public_channel`, `private_channel`, `mpim`, `im`",
schema: {
type: "string",
},
},
{
name: "limit",
in: "query",
description:
"The maximum number of items to return. Fewer than the requested number of items may be returned, even if the end of the list hasn't been reached. Must be an integer no larger than 1000.",
schema: {
type: "number",
},
},
{
name: "cursor",
in: "query",
description:
'Paginate through collections of data by setting the `cursor` parameter to a `next_cursor` attribute returned by a previous request\'s `response_metadata`. Default value fetches the first "page" of the collection. See [pagination](/docs/pagination) for more detail.',
schema: {
type: "string",
},
},
],
request: {},
responses: {
200: [
{
success: true,
name: "Success",
schema: schemaFromOpenApiSpecV2(
spec,
"/paths//conversations.list/get/responses/200/schema"
),
},
errorResponse,
],
default: [
{
success: false,
name: "Error",
schema: schemaFromOpenApiSpecV2(
spec,
"/paths//conversations.list/get/responses/default/schema"
),
},
],
},
};
export const conversationsJoin: EndpointSpec = {
path: "/conversations.join",
method: "POST",
metadata: {
name: "conversationsJoin",
description: "Joins an existing conversation.",
displayProperties: {
title: "Join ${body.channel}",
},
externalDocs: {
description: "API method documentation",
url: "https://api.slack.com/methods/conversations.join",
},
tags: ["conversations"],
},
security: {
slackAuth: ["channels:write"],
},
request: {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: {
schema: {
type: "object",
properties: {
channel: {
type: "string",
description: "ID of conversation to join",
},
},
required: ["channel"],
},
},
},
responses: {
200: [
{
success: true,
name: "Success",
schema: schemaFromOpenApiSpecV2(
spec,
"/paths//conversations.join/post/responses/200/schema"
),
},
errorResponse,
],
default: [
{
success: false,
name: "Error",
schema: schemaFromOpenApiSpecV2(
spec,
"/paths//conversations.join/post/responses/default/schema"
),
},
],
},
};

Some files were not shown because too many files have changed in this diff Show More