Compare commits
1016 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63b1c2237a | |||
| c72120eab0 | |||
| 313b7163e8 | |||
| b0e64937a0 | |||
| 3a2cf0ddb7 | |||
| 1579cdbfb2 | |||
| f1e81137b7 | |||
| 45b2bea39f | |||
| 4f47d03114 | |||
| 618cca3304 | |||
| 5706c3ab52 | |||
| e8c81cafb8 | |||
| a0d062c977 | |||
| 87a3bbee8e | |||
| a5ff50f737 | |||
| 763dd8b55a | |||
| 9e5c687174 | |||
| dcbb0f5287 | |||
| d64cfe5add | |||
| 1fa52f1c8c | |||
| ea28f3dd47 | |||
| 2c563ce998 | |||
| 7dd0102579 | |||
| 01bc7df3e2 | |||
| 6d4f71d22c | |||
| cc002ef707 | |||
| f552af517b | |||
| 8aff958bd3 | |||
| ad189b1f4b | |||
| e54ba7cbed | |||
| 1d056b5328 | |||
| cf7c413c87 | |||
| ac87a634a3 | |||
| c9e411be9b | |||
| ee4e181fc3 | |||
| f51f8f3ece | |||
| 12f4f3bc68 | |||
| 894a5dcb5d | |||
| 3af9b63a9c | |||
| 0932ae7d89 | |||
| 12056d888b | |||
| 63e004594c | |||
| ec3e98ca9f | |||
| f3ffffd50b | |||
| e6550d156e | |||
| ae550df37d | |||
| 26ac1fa57c | |||
| 14031a8c6e | |||
| 719ef2a3a9 | |||
| aa295134ff | |||
| b2f14cbea3 | |||
| 26e69cb659 | |||
| 51f9bc9d72 | |||
| 74a6a21c35 | |||
| b69058bfcb | |||
| 1c5d8a4c0b | |||
| 15ab35c816 | |||
| 1218320492 | |||
| 80373c6874 | |||
| 590dd688a9 | |||
| f739f49209 | |||
| dabf26c2c9 | |||
| 173f88dcad | |||
| ee20f9212f | |||
| 566fb54615 | |||
| 100edb8abd | |||
| 8cb08b63d2 | |||
| 4f21aaef97 | |||
| 8272363069 | |||
| 3e32802fdf | |||
| 3e1c1138e5 | |||
| 7d1e64cb01 | |||
| 08abf25fd9 | |||
| 9c9a4caa20 | |||
| 352b7f75ce | |||
| f0378fa874 | |||
| 8de37d47bf | |||
| 23376457e6 | |||
| 75589d3bae | |||
| 96c4f319e4 | |||
| 8587841ee4 | |||
| 058afb40ce | |||
| dc058c8835 | |||
| f80253ed1d | |||
| 3e384ff18a | |||
| db9b03ca05 | |||
| cc6adecfb4 | |||
| 841421bf76 | |||
| f5ebde8ddb | |||
| da9e357b6d | |||
| cde0b7fcfe | |||
| 43f913e796 | |||
| 26ae66ad2a | |||
| 88fe24ad67 | |||
| da0ee481b8 | |||
| ff57a70a53 | |||
| 22ae2131a5 | |||
| 5880d966ac | |||
| 9eeacee7c2 | |||
| 6ced99fb02 | |||
| 28fb5d066f | |||
| 0d72b8e0c4 | |||
| 547aab667d | |||
| d744efdb26 | |||
| 727a492e45 | |||
| 384752d36c | |||
| d699d38981 | |||
| 6b53aeb8c2 | |||
| b7ee8fd432 | |||
| 8836aeae81 | |||
| 2748d9e9a6 | |||
| f2a0f304af | |||
| 7659835541 | |||
| 8862b563dd | |||
| ed1c2da660 | |||
| 9e739a50b2 | |||
| e4efed07e5 | |||
| 55644577ea | |||
| 5df5e30a7d | |||
| 43ceb44db9 | |||
| 154294f86e | |||
| e20f7ba16a | |||
| be6954abf2 | |||
| 810996f26f | |||
| a7044511bb | |||
| 0efbbece9e | |||
| a38945ebcd | |||
| a63ebd73fb | |||
| b34018eb17 | |||
| a6f9e1e35d | |||
| c7c5d407ef | |||
| 0c6d98d353 | |||
| f6ed5517c2 | |||
| 4aae45ede2 | |||
| f969f03197 | |||
| cd8e85f85f | |||
| 3c27b3c40b | |||
| 0a3b12d135 | |||
| aca332fffc | |||
| 350a60590d | |||
| ad8a2f170a | |||
| 67f6971601 | |||
| 695e4ca3f0 | |||
| 4243b10683 | |||
| c9bd1812f4 | |||
| a117a3b534 | |||
| 9e64f9d570 | |||
| 823da44fb7 | |||
| a5a5dcbccb | |||
| e2693eb539 | |||
| 036ffa7e5f | |||
| 679ac99fad | |||
| 9f6886c55b | |||
| ad9089da57 | |||
| 9313adee12 | |||
| 9bb8363643 | |||
| 4e2fe3dcac | |||
| 092b139d71 | |||
| ab92e9515b | |||
| d4939c4e11 | |||
| ef3f075042 | |||
| c76d930f08 | |||
| 06f2fc3aeb | |||
| 3cd4f2fb9f | |||
| 8f011a5da3 | |||
| 067dbfc0cf | |||
| 8dfdfdb2fa | |||
| 3b76574ef8 | |||
| 3a0a6e491c | |||
| 24203d1b8d | |||
| 8da0f9ac01 | |||
| 011264865b | |||
| 889d9910e2 | |||
| 679239d9fc | |||
| 20d8493c3e | |||
| fe598ff713 | |||
| ac1354b71a | |||
| 55f04d5d12 | |||
| 3796ba5b07 | |||
| 86e1e7143f | |||
| d32aa49020 | |||
| 08338d15a5 | |||
| 4b3c9b7230 | |||
| e84e43e4e1 | |||
| 584b12451d | |||
| 0913a34174 | |||
| e6d6bfced4 | |||
| c7e926a8a9 | |||
| 3ad8a0a5de | |||
| 0d918e7bf6 | |||
| 444996533f | |||
| 1b53ccbbac | |||
| dbd4e9a68c | |||
| 0c36c3e16f | |||
| 05af0ba2b5 | |||
| f2e341e753 | |||
| 6e337eb144 | |||
| fd90e3c3d8 | |||
| b969653f7c | |||
| b5128abee0 | |||
| 1f5053bcdb | |||
| 232b58daf1 | |||
| d07a54f09b | |||
| 2adea4cd9b | |||
| 27e1d134f7 | |||
| 92ca01f3c7 | |||
| c75181f10d | |||
| a26002c525 | |||
| 5a3fdc470f | |||
| 781a208c07 | |||
| 67eb592ecc | |||
| efc35c6c79 | |||
| 65649c1fc7 | |||
| 0fa59d9387 | |||
| aa8cd86041 | |||
| 165bd68d2d | |||
| 9b664a1af2 | |||
| 3d174f5aeb | |||
| b341e7bb71 | |||
| 1dc11a39ea | |||
| 970cfcb094 | |||
| c33e228b26 | |||
| 6ba6e20026 | |||
| 81b2742577 | |||
| ccadf08bd2 | |||
| cea1291496 | |||
| 3e6b0c747b | |||
| affcabc36f | |||
| 6732015b5c | |||
| cbb50a05ad | |||
| 142fa85b14 | |||
| 857ecf1de7 | |||
| df6fe919c2 | |||
| 579e98c7af | |||
| 4724ef00fc | |||
| 77e3769dbd | |||
| 70a547a5c5 | |||
| 3c8a3c421b | |||
| 842ab0cdcc | |||
| 64e0445acb | |||
| 213d86cb1f | |||
| 66805f67fa | |||
| 3defbcc0ed | |||
| 942e695782 | |||
| 4a8b977a99 | |||
| 2e9875c4c6 | |||
| ed775706a0 | |||
| 89abb57e44 | |||
| f264aaa16f | |||
| 2f7caab661 | |||
| c92fec9787 | |||
| 9211cce807 | |||
| 26c4f5d23c | |||
| 4c9891de90 | |||
| 3c706723a2 | |||
| 6e3778d74e | |||
| 8a10431766 | |||
| 825983db77 | |||
| a79ded125e | |||
| e05b3d633d | |||
| f659d9aec6 | |||
| 1772b0664c | |||
| eca961a30f | |||
| c500155d60 | |||
| f8957423e5 | |||
| 9da448a21b | |||
| 8468db0a83 | |||
| f0d3b12d84 | |||
| ce5a42ccd7 | |||
| fc962f50c9 | |||
| 171d16ef41 | |||
| bec76a3704 | |||
| 3c403c4212 | |||
| 97e432462d | |||
| 4a2e0b28f4 | |||
| fb496b1efa | |||
| 6c299072cd | |||
| 25f70c4574 | |||
| 7f0ad4c18a | |||
| 6c50ce078a | |||
| e8d018ef9d | |||
| 9dc2ec4dca | |||
| 694df6ee38 | |||
| c27987c191 | |||
| 19aedf8f73 | |||
| 4c219a8bf8 | |||
| f30037da15 | |||
| df405aa740 | |||
| 7465d31e28 | |||
| 2876946792 | |||
| ae7c1c2cec | |||
| 10b5558dbc | |||
| 570683446c | |||
| 29f3504a23 | |||
| 4bc156ce04 | |||
| c055c97209 | |||
| bc767516b6 | |||
| 7a19bb8381 | |||
| e742486677 | |||
| 821af448f3 | |||
| 9db2e483ef | |||
| 277108d90c | |||
| 49068d8db1 | |||
| 3558a76e27 | |||
| fde3fec37f | |||
| dabb17cf51 | |||
| 7a8ad5f254 | |||
| ab1d283217 | |||
| 7e80c5e7f6 | |||
| c5a5cef9f3 | |||
| f51aa7b792 | |||
| 9175d1e541 | |||
| 38fbf41e79 | |||
| 4bd3cabad4 | |||
| 6c9d31fe1c | |||
| c13c52aa05 | |||
| 260bd5f007 | |||
| 25cf0245a4 | |||
| 98cac166b3 | |||
| c2e9accade | |||
| dc86fc2998 | |||
| 7a6968d0ac | |||
| 2c780c976e | |||
| fbc1b86ea0 | |||
| f20b29519a | |||
| 3c7f6cf9ec | |||
| 1410688499 | |||
| d583e2a8f2 | |||
| a94c8e4576 | |||
| 5fc1236d2e | |||
| 3b67a5ce10 | |||
| 016cf05aba | |||
| 22c8a16758 | |||
| 0955a51333 | |||
| 8dc5bba8ca | |||
| 3538754a76 | |||
| 8ef434eb13 | |||
| 78ecfb60dc | |||
| 2acd79fb36 | |||
| 8d15e9a2b9 | |||
| d99446a1b2 | |||
| 7b24248b80 | |||
| feafa69068 | |||
| 91ed853747 | |||
| 76737caa47 | |||
| b480fa242a | |||
| 192a1d2119 | |||
| 2261a769e4 | |||
| 7e92e8cc75 | |||
| 5a312dec55 | |||
| a1535dff70 | |||
| 4a00abed46 | |||
| 15316a7504 | |||
| 039f652a41 | |||
| fa80ca07b7 | |||
| f2cfa17b62 | |||
| 77124bba78 | |||
| 7530400a38 | |||
| bfd8a4f6b8 | |||
| 4a72aff4ef | |||
| 544b9b94c1 | |||
| 6b05b84342 | |||
| 2ede12c2f3 | |||
| b142c55fc5 | |||
| 454bfcc58e | |||
| eb98f50fca | |||
| 81ddf597ed | |||
| 75d6471c9e | |||
| 68544359f1 | |||
| 187c0279b2 | |||
| 98cf9afd8e | |||
| 3ce5d5bf1a | |||
| f8ac05500a | |||
| 23dbeff1c6 | |||
| ba0a4727d3 | |||
| 54c5c82f22 | |||
| 6a3f26427b | |||
| 9db77b6478 | |||
| 08ecd288e1 | |||
| 10ae5601f8 | |||
| 9386e19620 | |||
| 1df18b386a | |||
| 83201312b2 | |||
| bfa096dc4a | |||
| 81debee7aa | |||
| 02499a0e63 | |||
| 558b705ace | |||
| 803b279133 | |||
| 4874288db0 | |||
| 30c0984623 | |||
| 1b675968f6 | |||
| e58b7ada83 | |||
| a96bfd3528 | |||
| a47a9b4fd6 | |||
| 727ec22fee | |||
| c359962b3a | |||
| 7bc11fe41f | |||
| 7af88c33da | |||
| 108bb6281e | |||
| 88ff526b87 | |||
| b88eaa2c40 | |||
| a46b93e29d | |||
| 1f672d9612 | |||
| d489311371 | |||
| af29669003 | |||
| 8f32446170 | |||
| 7f6e03b776 | |||
| 140d02f462 | |||
| 6a3ffa6395 | |||
| f6363ab5b6 | |||
| 60ac029220 | |||
| 1f3eb459c4 | |||
| 9388b5bbed | |||
| 047b011fdd | |||
| fa470462c4 | |||
| d807421b40 | |||
| b2bd170a2c | |||
| e013c6faa9 | |||
| d60e9ae939 | |||
| 8039f326a9 | |||
| 259e28e13a | |||
| 103f77215c | |||
| 967b8b0053 | |||
| 974033d5d2 | |||
| 84bd5a63a2 | |||
| f0a05ab4e3 | |||
| 805f675264 | |||
| dfab30c901 | |||
| 8f5474072f | |||
| 056bab7233 | |||
| 08c7a04b61 | |||
| f2a96a1535 | |||
| abc113701c | |||
| fb6953bc1d | |||
| 8a6467f4c8 | |||
| 581068362f | |||
| 011d7d5611 | |||
| caac573a64 | |||
| 39d02e6d00 | |||
| 1e1da62b32 | |||
| d67ab54d55 | |||
| 8e5938cd0c | |||
| 16d89c4b79 | |||
| bf9e269ff3 | |||
| a5190693c8 | |||
| de5b2ca3d8 | |||
| 5c8fc8188a | |||
| 5949c98748 | |||
| 4f7a8f6ae0 | |||
| c01bc15622 | |||
| 027f0210dc | |||
| da155d1378 | |||
| 38f32f405f | |||
| 0fee3ceae5 | |||
| 710003ebdf | |||
| 76a2c82d9f | |||
| 1be2af9309 | |||
| cdbae933b2 | |||
| 7f2c9687b7 | |||
| 721a32a379 | |||
| 2424de07ac | |||
| 179afbbcb3 | |||
| 4d6963f0e7 | |||
| 73fb0f3c0d | |||
| 4ed486ef93 | |||
| 9751531cc1 | |||
| 313a8c59cf | |||
| 76c660f167 | |||
| 3d5bbf899e | |||
| 460e8aa4a7 | |||
| 62ab7e098d | |||
| c4a441129b | |||
| 63486d21c4 | |||
| fbdc3c799d | |||
| fc1dd44f39 | |||
| 8a1517bc7f | |||
| fb0ecb8c34 | |||
| c091bc3326 | |||
| c27bdf6308 | |||
| 007dc2d713 | |||
| ba868cbd40 | |||
| 030844d905 | |||
| 36e25a85f8 | |||
| 4b0d03f9cc | |||
| d6c6c71268 | |||
| 6117bcf7eb | |||
| e1fb69fe44 | |||
| bf5621b716 | |||
| 1c9be1ee1d | |||
| a020dfa036 | |||
| 2e28bd09ca | |||
| 6595535b40 | |||
| a7eac27b98 | |||
| b98bae8588 | |||
| 90a582f6ad | |||
| c41c2e8725 | |||
| ed4d6846f7 | |||
| 952e584d68 | |||
| f178629b9d | |||
| eb2741708a | |||
| fce24cc7fa | |||
| 1ca8760868 | |||
| ffcdc763eb | |||
| bacd76c8e5 | |||
| cf04c3902f | |||
| c4ba591df0 | |||
| ef8431b292 | |||
| b2290e3bc0 | |||
| 1bb2ebc7bc | |||
| 0193327f5d | |||
| 431079c894 | |||
| cc6a37bdf6 | |||
| 9539138992 | |||
| dee2db231a | |||
| 5e99b28ea5 | |||
| 038d53c7c5 | |||
| ad1f9346b8 | |||
| ddfce4e3c7 | |||
| 8a73e559e8 | |||
| ad26302925 | |||
| 2a20546e7b | |||
| 137db98176 | |||
| 94154bd96c | |||
| c34a28b2d5 | |||
| 79622b6123 | |||
| d329aca7e5 | |||
| a347e5526f | |||
| 5328ba1556 | |||
| 7d12d3242e | |||
| 706024841a | |||
| 6e5695f873 | |||
| 565aba4021 | |||
| 5a10e53de6 | |||
| c927342da3 | |||
| 862eeb89d5 | |||
| b88ad37b92 | |||
| 5ff4b68bae | |||
| a7107cf1b1 | |||
| 6132cdf8c0 | |||
| d39fb33c70 | |||
| 5e7ff78f23 | |||
| 05289d0cb1 | |||
| 7dbc634388 | |||
| d58add78bf | |||
| 0c8448c65d | |||
| eed039319e | |||
| 3542c0ed87 | |||
| 162c0f5db5 | |||
| 5f649d1b2e | |||
| e261149065 | |||
| 5f3d985b64 | |||
| 283f6d6be6 | |||
| 7d1b4e71a5 | |||
| 29b4ba4c78 | |||
| cab222ac4b | |||
| 667a60a551 | |||
| 710bcc2c47 | |||
| d1d34da9b2 | |||
| 0c476093d7 | |||
| ba2c895e53 | |||
| 764dd21613 | |||
| 78c3e1060d | |||
| 96a180f26e | |||
| 35718033bd | |||
| 5501a927bf | |||
| 17568a0aec | |||
| 563a535b56 | |||
| 2a2652d3aa | |||
| aca9081e64 | |||
| ccd89d08d9 | |||
| d9ad006550 | |||
| eacee88f45 | |||
| 51432feb74 | |||
| 2398f85081 | |||
| b1fb567c28 | |||
| 8c89ed3334 | |||
| 32b9dbcf94 | |||
| 7d84f59c1e | |||
| d22ef06525 | |||
| 595df9ae62 | |||
| ceb862f484 | |||
| 49846df811 | |||
| 5fec008f09 | |||
| 90f8b088ff | |||
| a1d464325d | |||
| a17442b016 | |||
| cb3b068fab | |||
| 26a5913734 | |||
| 078beb1689 | |||
| 05a2f6f8e1 | |||
| 7fc6358e40 | |||
| 70ce15b271 | |||
| 97b0019d99 | |||
| c419101750 | |||
| fdb2944380 | |||
| 13b6d96f8c | |||
| feae0b3c08 | |||
| 64854b128f | |||
| 46fc33ca15 | |||
| f02eeb0af9 | |||
| f95938d2ac | |||
| c3f2f61982 | |||
| 124751e62a | |||
| 81569eb66e | |||
| 66d959282d | |||
| b0a06f8a9d | |||
| 07f0144354 | |||
| 11beadfb7d | |||
| 54faa44e84 | |||
| f121a48466 | |||
| 92436308c0 | |||
| 8ba20d8fa3 | |||
| 92a37ae3a5 | |||
| db9d9d93df | |||
| 506dfe6860 | |||
| 813c3622e9 | |||
| d0208e698b | |||
| 2a51c5a1a3 | |||
| ee9125ff02 | |||
| 6caadf5da5 | |||
| c98b7c4e24 | |||
| 489c40c847 | |||
| 0d2d9a043c | |||
| 9bb9c1cdbe | |||
| 81d6fbfdaa | |||
| b72d63bdd2 | |||
| 1319651079 | |||
| f6fbd5e5b7 | |||
| 9d5e24dac5 | |||
| 4ad2702e4d | |||
| 2a2da8fa7a | |||
| 64ccb0cf11 | |||
| 8b3abd6a06 | |||
| 63b3d82c2f | |||
| 358e9af01d | |||
| f9cab846b9 | |||
| 8a66983f63 | |||
| b45a176c4f | |||
| 86d1a2413a | |||
| 763ee294ba | |||
| 9a7db7bcff | |||
| 2c2de7b236 | |||
| b9b49a8733 | |||
| 32f09d59e6 | |||
| ddb8b131c0 | |||
| ab8e1f0d06 | |||
| 3f25622abf | |||
| 9692ef5ceb | |||
| 2c29687712 | |||
| a392e6a00c | |||
| 59a5c2df51 | |||
| 9872afcf98 | |||
| 55fd404449 | |||
| faa844efa1 | |||
| 1d02ffaf18 | |||
| 859a31e08b | |||
| a5f18cf45a | |||
| c669c61ba7 | |||
| ec1df6d476 | |||
| 50ecc88b99 | |||
| 605e34b6ad | |||
| 780cd7fcef | |||
| f447cb1ca0 | |||
| bf57430b66 | |||
| ee28eca61c | |||
| d629e3cca1 | |||
| 43a320253d | |||
| 5ffababfe5 | |||
| e77bfc77e0 | |||
| 93903bf072 | |||
| 2b4a74f672 | |||
| 8ffb612496 | |||
| 34f86dda3b | |||
| 81d3d324e2 | |||
| 6d8100d200 | |||
| 3e53699b05 | |||
| 8cca0f520b | |||
| 908fff6fe3 | |||
| da7742985f | |||
| 4077d014e4 | |||
| ab0938e91b | |||
| 547f619112 | |||
| 4008e1d7ed | |||
| 910a8d3651 | |||
| 0e4ec8d8e6 | |||
| 2d08718389 | |||
| 96306eb5e0 | |||
| ad70dbc5dd | |||
| 517d50242f | |||
| cf8163ef5c | |||
| d6c7648bf1 | |||
| 05b0bd9fb4 | |||
| 6495b67d1f | |||
| c37edf970d | |||
| b38961205a | |||
| 7e1c04ae71 | |||
| 49a4b0ce11 | |||
| 42c2a87238 | |||
| aefbba1f37 | |||
| b8b032a5fb | |||
| 2eea62c3ce | |||
| 22327886ac | |||
| 3126d890dc | |||
| 60abac1bc0 | |||
| 37d331588d | |||
| 6ddaf067a9 | |||
| e281297da5 | |||
| f46436b903 | |||
| a5d8290663 | |||
| 4fa967b2b1 | |||
| 6442576a38 | |||
| 93f1814fd2 | |||
| d97b82e572 | |||
| 5e15aaadc3 | |||
| 5fce178cc0 | |||
| 2938a11685 | |||
| 3dc8751189 | |||
| fe8b8a7131 | |||
| d7f59db79e | |||
| a98dfecd17 | |||
| 84053d5a20 | |||
| 3365bf26dc | |||
| 3d5c646a1c | |||
| 2d97d20b37 | |||
| 731b6308e0 | |||
| c1742f0d1b | |||
| d57339a7c1 | |||
| cd11246d9a | |||
| 0833d402f2 | |||
| 4054571929 | |||
| afc47d328f | |||
| d6925ff4b6 | |||
| a372a179d1 | |||
| f8c39e3446 | |||
| 868e5f4ed1 | |||
| e6ce7ea859 | |||
| e6d1ddc5d8 | |||
| 8124930841 | |||
| 421bd9f757 | |||
| b3bc70baa3 | |||
| dbfff6b5f4 | |||
| 69004cdcf4 | |||
| c1511f233c | |||
| 56d23efcfa | |||
| 31c6da3be4 | |||
| 6acda311db | |||
| 9a529492d0 | |||
| f05cfc9895 | |||
| 659637c6a5 | |||
| 37bf4dcadd | |||
| 950138bd72 | |||
| c578891947 | |||
| f377aba208 | |||
| b852731fbb | |||
| 55a9031526 | |||
| 5c350e216a | |||
| 8ac7414bba | |||
| 6e8d9b4c9a | |||
| 1dc9e1253a | |||
| 24afe630ec | |||
| 5bd1f12c0d | |||
| 7e3dfcdfb1 | |||
| 24b57e026b | |||
| 3dd0af6157 | |||
| cdaa5dfe3b | |||
| a4a64affd4 | |||
| a20a26e748 | |||
| 4fb9091870 | |||
| 7b29422a57 | |||
| 1ef4243aaf | |||
| 1b696cabc1 | |||
| 3496053e11 | |||
| fd54c86240 | |||
| 064e27f05d | |||
| 89d38c04aa | |||
| 6c027c3c8f | |||
| fad8f44ad1 | |||
| 5f517a5c70 | |||
| 09142cedd1 | |||
| 2bb7bad947 | |||
| d71c61fc56 | |||
| 3f9e7f4bda | |||
| e2261942a0 | |||
| a015e535f0 | |||
| 6c3c99123b | |||
| c96f9f10b4 | |||
| 23fc242dde | |||
| c1ca53f513 | |||
| b8dc408f0f | |||
| b129381cf2 | |||
| 0141643b66 | |||
| 36cdf50c7c | |||
| f660810fb8 | |||
| 8372cf71d4 | |||
| 4adad0041a | |||
| 887f9d1091 | |||
| 91130472a9 | |||
| 7554b48948 | |||
| 91b1edba7b | |||
| be86c3d45d | |||
| 7f2fd9b1b3 | |||
| a069ba14e4 | |||
| 87f755c93e | |||
| 66b6c2f844 | |||
| c97a64bb02 | |||
| f3258ea411 | |||
| 42648e93ee | |||
| acf8bf038f | |||
| 3769d12bd8 | |||
| 3759dfc434 | |||
| c8e871393d | |||
| f9208f45a7 | |||
| 2d0e766b1a | |||
| 71549606ad | |||
| b63769b4e3 | |||
| f325061444 | |||
| 5ad386e306 | |||
| a04e0f5e2d | |||
| 5b65dd0070 | |||
| 2f0104e960 | |||
| c3e7a3e649 | |||
| 1433004292 | |||
| cc1887b065 | |||
| 614489c67d | |||
| 2a4b91da82 | |||
| 0f8014f613 | |||
| ab7c27be53 | |||
| 4c23370735 | |||
| 0d6e843880 | |||
| da948c4f4b | |||
| 898069b5c8 | |||
| c1b9aee818 | |||
| 63276c266d | |||
| a4342437a2 | |||
| 5035e10f11 | |||
| b68ca6e440 | |||
| a832074264 | |||
| 598d1542b4 | |||
| 0f187803ae | |||
| e235e4e472 | |||
| 5423d5464f | |||
| 251b64ccf2 | |||
| 3e9e743751 | |||
| 1fc7020b84 | |||
| 0137dac540 | |||
| 95d594457c | |||
| e21ae74f21 | |||
| 416c46d4cf | |||
| ba617fa7d7 | |||
| 35aedadfe9 | |||
| 888c8a7460 | |||
| d6512b2ba5 | |||
| 92391c48cb | |||
| 7095b45a37 | |||
| c954b3743f | |||
| 49810df735 | |||
| b647277333 | |||
| 26c1c9f4b2 | |||
| 058824cee4 | |||
| ef149bd260 | |||
| d8ef6becec | |||
| 31e7b77fe7 | |||
| 56e5a5c8a4 | |||
| 28cc99bf9f | |||
| b29041002c | |||
| e1b26c863d | |||
| f5b88ce272 | |||
| dd62655bc7 | |||
| 2d72c112f3 | |||
| 96ff7f136c | |||
| 719c2a6091 | |||
| b327d1bb62 | |||
| 65b78f8f20 | |||
| 128f3baa08 | |||
| 75eca2895b | |||
| 617cc221cb | |||
| ffacc2fb6d | |||
| 9f935cdd3b | |||
| b75ef0266e | |||
| 74df0b702b | |||
| e8a29dca6b | |||
| 057a13b52c | |||
| 52d21ac84d | |||
| 897724e883 | |||
| 7e7c8dafd7 | |||
| 86c543c45f | |||
| 543589ae32 | |||
| e5530299da | |||
| a4bd86fefe | |||
| 518472fcb2 | |||
| 1a20d1b2a3 | |||
| 31a8e364de | |||
| a1b16400d5 | |||
| 24cad37264 | |||
| 6bc46a8b90 | |||
| 6909cc8152 | |||
| fbda1d9ec4 | |||
| 1d5b592c8f | |||
| 519293ca03 | |||
| 2ae86d0f0e | |||
| 60a5ca785e | |||
| e8dfa4bea7 | |||
| f100bfc81f | |||
| f447b98c50 | |||
| 63cc3ad5c5 | |||
| fb6d254898 | |||
| cc4f27a3cd | |||
| 46b59f1bf6 | |||
| 7faf4312b1 | |||
| 58a946547c | |||
| 0ed3784d01 | |||
| e37a2001e1 | |||
| 1374c10d99 | |||
| e63d354144 | |||
| 3710f99270 | |||
| 75d65511b3 | |||
| faa35fdbae | |||
| 9e77246ebf | |||
| 8719a4c680 | |||
| ea9a839886 | |||
| fa7e670a56 | |||
| 94f2de81ea | |||
| 9da1f881f0 | |||
| 3d3a88d9ac | |||
| 5ea825f5eb | |||
| 2dc72d68c3 | |||
| 039321f176 | |||
| ed06751a8f | |||
| dc83526a77 | |||
| d10538c3d7 | |||
| 26264ad2ee | |||
| 9435709ad1 | |||
| 00ac720071 | |||
| af63b52e32 | |||
| c3bf58669b | |||
| 88306d6a63 | |||
| 80ebc1b1fa | |||
| 42065e3ffc | |||
| 3d7e154589 | |||
| ff8ea0c80e | |||
| 989dcc1375 | |||
| ada581cc88 | |||
| 7a8911b89a | |||
| 1c893a538d | |||
| 39853e5ce0 | |||
| 22b2e1635f | |||
| a9cca5ba78 | |||
| 33128bc124 | |||
| 2bf2d707ea | |||
| d3da60b871 | |||
| ddf42558aa | |||
| 40164ee136 | |||
| 2fd9e4fa2f | |||
| 764f5c1fbb | |||
| bce1bd1314 | |||
| e607281b1b | |||
| 109d81d9fa | |||
| 972444da16 | |||
| 2ab9b5f97b | |||
| 001cd811c4 | |||
| 51c295f102 | |||
| 1843a2f944 | |||
| f10dadbd9f | |||
| 809ffe2168 | |||
| 12dfc8dbe1 | |||
| 4085ad983c | |||
| 3caa793de2 | |||
| d6b69ab387 | |||
| 1cc397732f | |||
| 8541ec6198 | |||
| f6b7b4ad53 | |||
| 7be5433bda | |||
| c570f069fb | |||
| a8f8125a94 | |||
| 3b6b8f5964 | |||
| 042dd928d8 | |||
| 46521a3aa8 | |||
| 77f3233224 | |||
| 03008054ea | |||
| 36480dd850 | |||
| aceceec317 | |||
| eebe37109e | |||
| d717a909d2 | |||
| 951bf9a92c | |||
| a2d11a2bf5 | |||
| dc11daced1 | |||
| 681955c19d | |||
| 780688ea73 | |||
| 1c373321f5 | |||
| 84bb476907 | |||
| fb11417aac | |||
| c41026d34c | |||
| 08179a086c | |||
| 98fca0ed60 | |||
| 9ecabd41b9 | |||
| 5dac88601f | |||
| 62a5163c2d | |||
| 45e64b9ef0 | |||
| 748494ca87 | |||
| 2020b0afa5 | |||
| a05436014f | |||
| 39b167ed1f | |||
| ada6e43bb7 | |||
| 8d9587b416 | |||
| 5a32765cf8 | |||
| 4d6eabb0fa | |||
| 4c48390b81 | |||
| e9818d2fad | |||
| fc6e78772c | |||
| a97b759628 | |||
| ba547430b2 | |||
| 63776c7702 | |||
| ce8d146ed4 | |||
| b45a5c60f7 | |||
| 1c62f16117 | |||
| 67a0d54f74 |
@@ -4,8 +4,15 @@
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["wss", "webapp", "docs"]
|
||||
"ignore": [
|
||||
"wss",
|
||||
"webapp",
|
||||
"integrations",
|
||||
"emails",
|
||||
"internal-cli",
|
||||
"integration-catalog"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -44,4 +44,5 @@ yarn-error.log*
|
||||
.env
|
||||
.output
|
||||
apps/**/public/build
|
||||
.tests-container-id.txt
|
||||
.tests-container-id.txt
|
||||
.sentryclirc
|
||||
@@ -0,0 +1 @@
|
||||
{"workspaceId":"63e5e42daf9a537ba8d9503c"}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -7,8 +7,11 @@ Trigger.dev uses [changesets](https://github.com/changesets/changesets) to manag
|
||||
To add a changeset, use `pnpm run changeset:add` and follow the instructions [here](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). Please only ever select one of our public packages when adding a changeset, which currently are:
|
||||
|
||||
- `@trigger.dev/sdk`
|
||||
- `@trigger.dev/integrations`
|
||||
- `@trigger.dev/providers`
|
||||
- `@trigger.dev/integration-sdk`
|
||||
- `@trigger.dev/github`
|
||||
- `@trigger.dev/slack`
|
||||
- `@trigger.dev/shopify`
|
||||
- `@trigger.dev/resend`
|
||||
|
||||
## Release instructions
|
||||
|
||||
@@ -16,3 +19,11 @@ Based on the instructions [here](https://github.com/changesets/changesets/blob/m
|
||||
|
||||
1. Run `pnpm run changeset:version`
|
||||
2. Run `pnpm run changeset:release`
|
||||
|
||||
## Pre-release instructions
|
||||
|
||||
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`
|
||||
|
||||
@@ -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
|
||||
@@ -188,16 +190,10 @@ pnpm run dev --filter=webapp
|
||||
pnpm run dev --filter=wss
|
||||
```
|
||||
|
||||
4. Run the @trigger.dev/sdk:dev
|
||||
4. Build all the @trigger.dev/\* packages
|
||||
|
||||
```bash
|
||||
pnpm run dev --filter=@trigger.dev/sdk
|
||||
```
|
||||
|
||||
5. Run the @trigger.dev integrations
|
||||
|
||||
```bash
|
||||
pnpm run dev --filter=@trigger.dev/integrations
|
||||
pnpm run dev --filter="@trigger.dev/*"
|
||||
```
|
||||
|
||||
## Running the smoke test
|
||||
|
||||
@@ -1,201 +1,25 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
Copyright (c) 2022 API Hero Inc.
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
1. Definitions.
|
||||
- All content that resides under any "ee/" directory of this repository, if such directories exists, are licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Trigger.dev Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,384 +1,204 @@
|
||||

|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
[](https://twitter.com/triggerdotdev) [](https://www.youtube.com/@triggerdotdev)
|
||||
[](https://github.com/triggerdotdev/trigger.dev)
|
||||
|
||||
[Website](https://trigger.dev) | [Community](https://discord.gg/JtBAxBr2m3) | [Docs](https://docs.trigger.dev)
|
||||
</div>
|
||||
|
||||
|
||||
### We are now accepting users 😊! Request early access [here](https://bcymafitv0e.typeform.com/tddsignup#source=GitHubReadme).
|
||||
# **✨ Trigger.dev**
|
||||
### **The developer-first open source Zapier alternative.**
|
||||
|
||||
|
||||
Trigger.dev is an open source platform that makes it easy for developers to create event-driven background tasks directly in their code. Build, test and run workflows locally using our SDK. Subscribe to webhooks, schedule jobs, run background jobs and add long delays easily and reliably. In our web app you get full visibility of every run your workflow has ever made making it easier to monitor and debug.
|
||||
|
||||
|
||||
|
||||
---
|
||||
# **⭐️ Features:**
|
||||
|
||||
- 👂 Easily subscribe to [webhooks](https://docs.trigger.dev/triggers/webhooks)
|
||||
— they work locally without tunnelling.
|
||||
- 🔥 Fire your own [custom events](https://docs.trigger.dev/triggers/custom-events)—a single event can trigger multiple workflows.
|
||||
- 📆 [Schedule workflows](https://docs.trigger.dev/triggers/scheduled)—easily repeat tasks or use CRON syntax for advanced cases.
|
||||
- 🚦 Add [long delays](https://docs.trigger.dev/functions/delays) inside workflows (up to a year) and they will pick up where they left off.
|
||||
- 🤝 When your server goes down [it’s not a problem](https://docs.trigger.dev/guides/resumability), workflows will reconnect and continue.
|
||||
- 🪧 [View every step of every run](https://docs.trigger.dev/viewing-runs), with data, previews and errors.
|
||||
- 👋 Connect to and authenticate with APIs using our custom integrations.
|
||||
- 🚗 If you have a custom use case, we support [Fetch for calling any HTTP endpoint](https://docs.trigger.dev/functions/fetch) or [webhooks](https://docs.trigger.dev/triggers/webhooks) for subscribing to events from APIs.
|
||||
- 📡 All API calls are automatically retried with exponential back off.
|
||||
- 😀 TypeScript SDK, so whether you’re using JavaScript or TypeScript you will have a great experience.
|
||||
|
||||
|
||||
|
||||
# **🌱 Documentation:**
|
||||
|
||||

|
||||
|
||||
[](https://twitter.com/intent/follow?screen_name=triggerdotdev)
|
||||
|
||||
# ⚙️ Automate complex workflows with code
|
||||
|
||||
Trigger workflows from APIs, on a schedule, or on demand. API calls are easy with authentication handled for you. Add durable delays that survive server restarts.
|
||||
|
||||

|
||||
|
||||
# 💻 Developer-first features
|
||||
|
||||
## In your codebase
|
||||
|
||||
Trigger.dev is code-first so you can create workflows where they belong: in your codebase. Version control, localhost, test, review, and deploy like you're used to.
|
||||
|
||||
## Secure by design
|
||||
|
||||
Your workflows run on your servers, not ours. We only receive the data you choose to send to us.
|
||||
- [Getting Started with Trigger.dev](https://docs.trigger.dev/getting-started)
|
||||
- Example workflows
|
||||
- [Welcome email drip campaign using Resend and Slack](https://docs.trigger.dev/examples/resend)
|
||||
- [Post to Slack when a GitHub issue is created or modified](https://docs.trigger.dev/examples/slack)
|
||||
- [Create a new product on Shopify](https://docs.trigger.dev/examples/shopify)
|
||||
- [When a GitHub repo is starred, post information about the user to Slack](https://docs.trigger.dev/examples/github)
|
||||
- Triggers:
|
||||
- [Webhooks](https://docs.trigger.dev/triggers/webhooks)
|
||||
- [Custom events](https://docs.trigger.dev/triggers/custom-events)
|
||||
- [Scheduled](https://docs.trigger.dev/triggers/scheduled)
|
||||
- Functions:
|
||||
- [Fetch](https://docs.trigger.dev/functions/fetch)
|
||||
- [Logging](https://docs.trigger.dev/functions/logging)
|
||||
- [Delays](https://docs.trigger.dev/functions/delays)
|
||||
- [Send event](https://docs.trigger.dev/functions/send-event)
|
||||
- [Loops, conditionals, etc](https://docs.trigger.dev/functions/loops-conditionals-etc)
|
||||
|
||||
|
||||
|
||||
# Hundreds of Integrations
|
||||
# 🔬 **Anatomy of a workflow**
|
||||
|
||||
Subscribe to API changes and make requests, we’ll handle authentication for you.
|
||||
* You create workflows in code on your server using our SDK
|
||||
* Each API integration is a separate package, e.g. `@trigger.dev/slack`
|
||||
* Each workflow has an event that triggers it, e.g. `github.events.newStarEvent`, `scheduleEvent`, `customEvent`
|
||||
* Each workflow has a `run` function that is called when the event is triggered
|
||||
* If we don't have an integration for the API you want to use, you can use `fetch` to call any HTTP endpoint and `webhookEvent` to subscribe to webhooks
|
||||
|
||||
|
||||
## **Example workflows**
|
||||
|
||||
# 🚀 Workflow examples
|
||||
<details open><summary> Post to Slack when a GitHub issue is created or modified
|
||||
</summary>
|
||||
|
||||
## 🔄 Sync GitHub issues to Linear
|
||||
_Integrations required: Slack, GitHub_
|
||||
|
||||
 
|
||||
|
||||
Triggered when a GitHub issue is created or updated. Query your database to map GitHub user ids to Linear user ids. Then create or update Linear issues.",
|
||||
|
||||
```Typescript
|
||||
|
||||
import { Trigger, github, linear } from "@trigger.dev/sdk";
|
||||
```ts
|
||||
import { Trigger } from "@trigger.dev/sdk";
|
||||
import * as github from "@trigger.dev/github";
|
||||
import * as slack from "@trigger.dev/slack";
|
||||
|
||||
new Trigger({
|
||||
name: "Sync Github issues to Linear",
|
||||
on: github.issueEvent({
|
||||
repo: "acme/website",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
const { issue, action } = event;
|
||||
|
||||
// Find the user in our local database
|
||||
const assignee = await findUserByGithubId(issue.assignee?.id);
|
||||
|
||||
if (action === "opened") {
|
||||
await linear.issueCreate({
|
||||
id: issue.id,
|
||||
title: issue.title,
|
||||
description: issue.body,
|
||||
assigneeId: assignee?.linearId,
|
||||
teamId: ctx.env.LINEAR_TEAM_ID,
|
||||
id: "new-github-star-to-slack",
|
||||
name: "New GitHub Star: triggerdotdev/trigger.dev",
|
||||
apiKey: "<my_api_key>",
|
||||
on: github.events.newStarEvent({
|
||||
repo: "triggerdotdev/trigger.dev",
|
||||
}),
|
||||
run: async (event) => {
|
||||
await slack.postMessage("github-stars", {
|
||||
channelName: "github-stars",
|
||||
text: `New GitHub star from \n<${event.sender.html_url}|${event.sender.login}>`,
|
||||
});
|
||||
} else {
|
||||
await linear.issueUpdate(issue.id, {
|
||||
assigneeId: assignee?.linearId,
|
||||
stateId:
|
||||
action === "closed"
|
||||
? ctx.env.LINEAR_CLOSED_STATE_ID
|
||||
: ctx.env.LINEAR_OPEN_STATE_ID,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
}).listen();
|
||||
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## ✉️ Send emails to new users
|
||||
<details><summary>Welcome email drip campaign
|
||||
</summary>
|
||||
|
||||
  
|
||||
_Integrations required: Slack, Resend_
|
||||
|
||||
Triggered on demand by your other code when a user is created. We wait for 3 hours then send a follow-up email if the user hasn’t completed onboarding yet.
|
||||
|
||||
```Typescript
|
||||
import { Trigger, customEvent, slack, mailgun } from "@trigger.dev/sdk";
|
||||
```ts
|
||||
import { customEvent, Trigger, sendEvent } from "@trigger.dev/sdk";
|
||||
import * as resend from "@trigger.dev/resend";
|
||||
import * as slack from "@trigger.dev/slack";
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { getUser } from "../db";
|
||||
import { InactiveEmail, TipsEmail, WelcomeEmail } from "./email-templates";
|
||||
|
||||
new Trigger({
|
||||
name: "Send Email to New Users",
|
||||
on: customEvent<UserEvent>({ name: "user.created" }),
|
||||
run: async (event, ctx) => {
|
||||
// Wait for 3 hours before continuing
|
||||
await ctx.waitFor({ hours: 3 });
|
||||
id: "welcome-email-campaign",
|
||||
name: "Welcome email drip campaign",
|
||||
apiKey: "<my_api_key>",
|
||||
on: customEvent({
|
||||
name: "user.created",
|
||||
schema: z.object({
|
||||
userId: z.string(),
|
||||
}),
|
||||
}),
|
||||
async run(event, context) {
|
||||
//get the user data from the database
|
||||
const user = await getUser(event.userId);
|
||||
|
||||
// Lookup user in the database
|
||||
const user = await findUserById(event.id);
|
||||
await slack.postMessage("send-to-slack", {
|
||||
channelName: "new-users",
|
||||
text: `New user signed up: ${user.name} (${user.email})`,
|
||||
});
|
||||
|
||||
// only send email if user has not onboarded
|
||||
if (!user.hasOnboarded) {
|
||||
await mailgun.send({
|
||||
to: user.email,
|
||||
subject: "Welcome to our app!",
|
||||
body: `Welcome to our app ${user.name}!}`,
|
||||
});
|
||||
//Send the first email
|
||||
const welcomeResponse = await resend.sendEmail("welcome-email", {
|
||||
from: "Trigger.dev <james@email.trigger.dev>",
|
||||
replyTo: "James <james@trigger.dev>",
|
||||
to: user.email,
|
||||
subject: "Welcome to Trigger.dev",
|
||||
react: <WelcomeEmail name={user.name} />,
|
||||
});
|
||||
await context.logger.debug(
|
||||
`Sent welcome email to ${welcomeResponse.to} with id ${welcomeResponse.id}`
|
||||
);
|
||||
|
||||
await slack.sendMessage({
|
||||
text: `Welcome email sent to ${user.email}`,
|
||||
//wait 1 day, check if the user has created a workflow and send the appropriate email
|
||||
await context.waitFor("wait-a-while", { days: 1 });
|
||||
const updatedUser = await getUser(event.userId);
|
||||
|
||||
if (updatedUser.hasOnboarded) {
|
||||
await resend.sendEmail("onboarding-complete", {
|
||||
from: "Trigger.dev <james@email.trigger.dev>",
|
||||
replyTo: "James <james@trigger.dev>",
|
||||
to: updatedUser.email,
|
||||
subject: "Pro tips for workflows",
|
||||
react: <TipsEmail name={updatedUser.name} />,
|
||||
});
|
||||
} else {
|
||||
await slack.sendMessage({
|
||||
text: `User ${user.email} has already onboarded`,
|
||||
await resend.sendEmail("onboarding-incomplete", {
|
||||
from: "Trigger.dev <james@email.trigger.dev>",
|
||||
replyTo: "James <james@trigger.dev>",
|
||||
to: updatedUser.email,
|
||||
subject: "Help with your first workflow",
|
||||
react: <InactiveEmail name={updatedUser.name} />,
|
||||
});
|
||||
}
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## 🚨 Escalate critical incidents
|
||||
|
||||
   
|
||||
|
||||
Triggered when an Intercom incident happens. We create a Linear issue, send a Slack message and, if it’s an urgent incident, we alert whoever is on call.
|
||||
|
||||
```Typescript
|
||||
import { Trigger, intercom, linear, slack, pagerduty } from "@trigger.dev/sdk";
|
||||
|
||||
new Trigger({
|
||||
name: "Intercom Incident",
|
||||
on: intercom.newIncident(),
|
||||
run: async (event, ctx) => {
|
||||
// Find the customer in the database
|
||||
const customer = await db.query("SELECT * FROM users WHERE email = $1", [
|
||||
event.email,
|
||||
]);
|
||||
|
||||
// Create linear ticket
|
||||
const ticket = await linear.issueCreate({
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
assigneeId: ctx.env.LINEAR_ASSIGNEE_ID,
|
||||
teamId: ctx.env.LINEAR_TEAM_ID,
|
||||
});
|
||||
|
||||
// notify account manager
|
||||
await slack.sendMessage({
|
||||
text: `New incident for ${customer.name} in Linear: ${ticket.url}`,
|
||||
});
|
||||
|
||||
if (event.severity === "urgent") {
|
||||
// Create a pagerduty incident
|
||||
await pagerduty.createIncident({
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
severity: "critical",
|
||||
serviceId: ctx.env.PAGERDUTY_SERVICE_ID,
|
||||
});
|
||||
}
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
[More examples here](https://docs.trigger.dev/examples/examples)
|
||||
|
||||
|
||||
|
||||
# ⚡️ Trigger happy
|
||||
# 👀 **Viewing runs:**
|
||||
|
||||
Install our SDK and get instant access to an arsenal of triggers you can use in your code:
|
||||
One of the most powerful features of Trigger.dev is the [runs page](https://docs.trigger.dev/viewing-runs). All of the steps in a workflow, including the initial event, can be viewed in detail. See the status / output of each step, the logs, rich previews, errors and much more.
|
||||
|
||||
|
||||
### **Webhooks**
|
||||
|
||||
Subscribe to webhooks without creating API endpoints. Plus they work locally without tunneling.
|
||||
|
||||
```Typescript
|
||||
github.issueEvent({ repo: "acme/website" })
|
||||
```
|
||||
|
||||
|
||||
### **Scheduled (CRON)**
|
||||
|
||||
Easily subscribe to a recurring schedule using human readable code or CRON syntax.
|
||||
|
||||
```Typescript
|
||||
scheduleEvent({ every: { minutes: 30 } })
|
||||
```
|
||||
|
||||
|
||||
### **Custom Events**
|
||||
|
||||
Trigger workflows from any event in your app. Send us your events, and we'll do the rest.
|
||||
|
||||
```Typescript
|
||||
customEvent<YourType>({ name: "your.event" })
|
||||
```
|
||||
|
||||
### **HTTP Endpoint**
|
||||
|
||||
Expose a HTTP endpoint to trigger your workflows with the method and path of your choice.
|
||||
|
||||
```Typescript
|
||||
httpEvent<User>({ method: "POST", path: "/users/:id" })
|
||||
```
|
||||
|
||||
### **Receive Emails**
|
||||
|
||||
Receive emails from your custom domain and trigger a workflow with the email metadata and content.
|
||||
|
||||
```Typescript
|
||||
emailEvent({ address: "support@help.customdomain.io" })
|
||||
```
|
||||
|
||||
### **AWS Event Bridge**
|
||||
|
||||
Integrate with AWS Event Bridge to trigger workflows on your own Event Bridge events.
|
||||
|
||||
```Typescript
|
||||
eventBridge<SupportRequest>({ bus: "customer-support" })
|
||||
```
|
||||

|
||||
|
||||
|
||||
|
||||
# 🔋 Batteries included
|
||||
# **🏠 Running Trigger.dev locally:**
|
||||
|
||||
### **Debugging and visibility**
|
||||
|
||||
We provide a full history of all runs, so you can see exactly what happened.
|
||||
To run Trigger.dev locally, [follow these steps](https://github.com/triggerdotdev/trigger.dev/blob/main/DEVELOPMENT.md).
|
||||
|
||||
|
||||
|
||||
<img src="https://trigger.dev/_next/static/media/runs-diagram.c9ce4213.png" alt="Runs" width="400"/>
|
||||
# **👏 Contributing:**
|
||||
|
||||
We are open source and love contributions!
|
||||
|
||||
- Request a feature in our [Discord community](https://discord.gg/JtBAxBr2m3)
|
||||
- Open a PR
|
||||
|
||||
|
||||
|
||||
---
|
||||
# **🧘♂️ Self-hosting guide:**
|
||||
|
||||
Please subscribe to the [GitHub issue](https://github.com/triggerdotdev/trigger.dev/issues/48) to be notified when it's live.
|
||||
|
||||
|
||||
|
||||
### **Survives downtime**
|
||||
|
||||
Workflows pick up where they left off when your server or external APIs go down.
|
||||
# **📧 Support & contact:**
|
||||
|
||||
|
||||
|
||||
<img src="https://trigger.dev/_next/static/media/retries-diagram.d90a00d9.png
|
||||
" alt="Retries" width="400"/>
|
||||
|
||||
|
||||
|
||||
# Go from idea to production in minutes
|
||||
|
||||
## **1. Code**
|
||||
|
||||
Write workflows by creating triggers directly in your code. These can be 3rd-party integrations, custom events or on a schedule.
|
||||
|
||||
## **2. Connect**
|
||||
|
||||
When your server runs, your workflow will be registered and you can authenticate with any APIs you’re using.
|
||||
|
||||
## **3. Test**
|
||||
|
||||
When your server runs, your workflow will be registered and you can authenticate with any APIs you’re using.
|
||||
|
||||
## **3. Deploy**
|
||||
|
||||
Deploy your new workflow as you would any other code commit and inspect each workflow run in real time.
|
||||
|
||||
|
||||
|
||||
# ✅ We ❤️ Open Source!
|
||||
|
||||
You’ll always be able to host and run Trigger.dev yourself.
|
||||
|
||||
We've also created [JSON Hero](https://github.com/triggerdotdev/jsonhero-web), an open source JSON viewer used by around 55,000 developers per month.
|
||||
|
||||
|
||||
|
||||
# 🙋 FAQs
|
||||
|
||||
<details><summary> Does my data get sent to your servers?</summary><br>
|
||||
|
||||
Only what you choose to send. The main body of your workflow code runs on your infrastructure.
|
||||
For example when you do a database query, that never touches us.
|
||||
|
||||
Data we will receive (and store to display on the Runs page in your dashboard):
|
||||
|
||||
- Any data that triggers the start of a workflow
|
||||
- Any data you pass to one of our API integrations • Any data you choose to log using our logging function
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary> How is this different to Zapier, Pipedream etc?</summary><br>
|
||||
|
||||
Trigger.dev is a code-first workflow tool that lets you create workflows directly in your code, rather than using a UI builder like Zapier. This means you can stay in your own IDE and keep your internal data secure.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>How long does this take to set up?</summary><br>
|
||||
|
||||
Setting up Trigger.dev is simple and takes 2 minutes. Install our SDK to get started and check out the Getting Started documentation to start creating your first workflow.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Can I use version control or roll-backs?</summary><br>
|
||||
|
||||
Yes. You create workflows directly in your own code so it’s version controlled with everything else.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>How long does it take to code up a workflow?</summary><br>
|
||||
|
||||
A simple workflow triggering two events from different services will take about 5 minutes to create.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Do you have all the integrations I need?</summary><br>
|
||||
|
||||
Probably. Trigger.dev includes over 100 integrations including the most popular services. If you need a specific service that’s not available, you can request it, or create it yourself. We’re open source, so create an issue or Pull Request.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Can I build complex workflows?</summary><br>
|
||||
|
||||
Yes. There’s no limit to the complexity on workflows you can create. Workflows are created in code so you can write conditional, looping, branching or time delayed logic.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary> Can I run Trigger.dev locally?</summary><br>
|
||||
|
||||
Yes. Workflows are created in your code locally and, unlike webhooks, you don’t need to use tunneling to receive triggers.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>How does the pricing model work?</summary><br>
|
||||
|
||||
Our hosted product gives you free runs each month, after that you will need to select a paid tier. You can also self-host, view the open source repository for instructions.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Is Trigger.dev open source?</summary><br>
|
||||
|
||||
Yes, Trigger.dev is open source. We are strong supporters of open source software, and our first product, [jsonhero.io](https://jsonhero.io), has a thriving open source community. Trigger.dev follows in that tradition.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Is Trigger.dev a no/low-code tool?</summary><br>
|
||||
|
||||
No. Trigger.dev is designed for developers who want to create workflows directly in code, without using a UI builder like Zapier. This allows developers to stay in their familiar development environment and customise their workflows with code.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>What languages / frameworks do you support?</summary><br>
|
||||
|
||||
Currently there is support for Node.js. More frameworks will be added soon.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Can I use an API which doesn’t have webhooks?</summary><br>
|
||||
|
||||
Yes. You can use a polling trigger to subscribe is no webhook exists.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Can non-coders use this product?</summary><br>
|
||||
|
||||
Developers will need to create workflows. Anyone on the team can monitor running workflows in the Trigger dashboard.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
If you have any other questions about Trigger.dev, [drop us an email](mailto:hello@trigger.dev), and one of the founders will get back to you.
|
||||
- Join our [Discord community](https://discord.gg/JtBAxBr2m3)
|
||||
- If you have any other questions, get in touch at [hello@trigger.dev](mailto:hello@trigger.dev)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Mintlify Starter Kit
|
||||
|
||||
Click on `Use this template` to quickstarter your documentation setup with Mintlify. The starter kit contains examples including
|
||||
|
||||
- Guide pages
|
||||
- Navigation
|
||||
- Customizations
|
||||
- API Reference pages
|
||||
- Use of popular components
|
||||
|
||||
### 👩💻 Development
|
||||
|
||||
Run these from the root of the repository
|
||||
|
||||
```
|
||||
pnpm install --filter docs
|
||||
```
|
||||
|
||||
```
|
||||
pnpm run dev --filter docs
|
||||
```
|
||||
|
||||
Go to http://localhost:3050
|
||||
|
||||
### 😎 Publishing Changes
|
||||
|
||||
Changes will be deployed to production automatically after pushing to the default branch.
|
||||
|
||||
You can also preview changes using PRs, which generates a preview link of the docs.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.
|
||||
- Mintlify dev is updating really slowly - Run `mintlify clear` to clear the cache.
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: "Example workflows"
|
||||
sidebarTitle: "Example workflows"
|
||||
description: "Example workflows."
|
||||
---
|
||||
|
||||
# Example workflows
|
||||
|
||||
<CardGroup>
|
||||
<Card title="GitHub" icon="rectangle-terminal" href="/examples/github"></Card>
|
||||
<Card
|
||||
title="Shopify"
|
||||
icon="rectangle-terminal"
|
||||
href="/examples/shopify"
|
||||
></Card>
|
||||
<Card title="Slack" icon="rectangle-terminal" href="/examples/slack"></Card>
|
||||
</CardGroup>
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
title: "GitHub"
|
||||
sidebarTitle: "GitHub"
|
||||
description: "Here are some example workflows you could create using the GitHub integration."
|
||||
---
|
||||
|
||||
You can explore all the supported GitHub functions [here](/).
|
||||
|
||||
## When a GitHub issue is created or modified, post to Slack
|
||||
|
||||
Integrations required: GitHub, Slack
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { github, slack } from "@trigger.dev/integrations";
|
||||
import { z } from "zod";
|
||||
|
||||
new Trigger({
|
||||
id: "my-workflow-1",
|
||||
name: "Posts to Slack when GitHub Issue created or modified",
|
||||
apiKey: "<my_api_key>",
|
||||
on: github.events.issueEvent({
|
||||
repo: "my-github-org/my-github-repo",
|
||||
}),
|
||||
|
||||
run: async (event, ctx) => {
|
||||
const response = await slack.postMessage("send-to-slack", {
|
||||
channelName: "my-slack-channel-name",
|
||||
text: `A new issue has been created or modified. ${event.action}`,
|
||||
});
|
||||
|
||||
return response.message;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
@@ -1,211 +0,0 @@
|
||||
---
|
||||
title: "Shopify"
|
||||
sidebarTitle: "Shopify"
|
||||
description: "Here are some example workflows you could create using the Shopify integration."
|
||||
---
|
||||
|
||||
You can explore all the supported Shopify functions [here](/).
|
||||
|
||||
## Create a new product
|
||||
|
||||
Integrations required: Shopify
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
import { shopify } from "@trigger.dev/integrations";
|
||||
|
||||
new Trigger({
|
||||
id: "shopify-products",
|
||||
name: "Shopify products",
|
||||
apiKey: "<my_api_key>",
|
||||
logLevel: "debug",
|
||||
on: customEvent({
|
||||
name: "shopify.products",
|
||||
schema: z.object({}),
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
const newProduct = await shopify.createProduct("create-product", {
|
||||
descriptionHtml: "This is my brilliant <i>product description</i>.",
|
||||
title: `Fantastic product ${Math.floor(Math.random() * 1000)}`,
|
||||
productType: "t-shirt",
|
||||
vendor: "Nike",
|
||||
options: ["Color", "Size"],
|
||||
standardizedProductType: {
|
||||
productTaxonomyNodeId: "gid://shopify/ProductTaxonomyNode/352",
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
price: "99.99",
|
||||
sku: "variant-1",
|
||||
inventoryItem: {
|
||||
tracked: true,
|
||||
},
|
||||
inventoryQuantities: [
|
||||
{
|
||||
availableQuantity: 1,
|
||||
locationId: "gid://shopify/Location/76187369773",
|
||||
},
|
||||
],
|
||||
options: ["Maroon", "Tiny"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const newImages = await shopify.appendProductImages("append-images", {
|
||||
id: newProduct.id,
|
||||
images: [
|
||||
{
|
||||
src: "https://via.placeholder.com/600/92c952.png",
|
||||
altText: "Image 1",
|
||||
},
|
||||
{
|
||||
src: "https://via.placeholder.com/600/d32776.png",
|
||||
altText: "Image 2",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return newProduct;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
## Update a product
|
||||
|
||||
Integrations required: Shopify
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
import { shopify } from "@trigger.dev/integrations";
|
||||
|
||||
new Trigger({
|
||||
id: "shopify-update-product",
|
||||
name: "Shopify update product",
|
||||
apiKey: "<my_api_key>",
|
||||
logLevel: "debug",
|
||||
on: customEvent({
|
||||
name: "product.update",
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
const product = await shopify.updateProduct("update-product", {
|
||||
id: event.id,
|
||||
descriptionHtml: `<h1>Awesome product title</h1><p>With incredible description</p>`,
|
||||
});
|
||||
|
||||
return product;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
## Add a product to a collection
|
||||
|
||||
Integrations required: Shopify
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
import { shopify } from "@trigger.dev/integrations";
|
||||
|
||||
new Trigger({
|
||||
id: "shopify-add-product-to-collection",
|
||||
name: "Shopify add product to collection",
|
||||
apiKey: "<my_api_key>",
|
||||
logLevel: "debug",
|
||||
on: customEvent({
|
||||
name: "product.new",
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
const collection = await shopify.addProductsToCollection(
|
||||
"add-products-to-collection",
|
||||
{
|
||||
collectionId: "gid://shopify/Collection/431864578349",
|
||||
productIds: [event.id],
|
||||
}
|
||||
);
|
||||
|
||||
return collection;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
## Create new product variants with images
|
||||
|
||||
Integrations required: Shopify
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
import { shopify } from "@trigger.dev/integrations";
|
||||
|
||||
const sizes = ["Small", "Medium", "Large"];
|
||||
const colors = [
|
||||
"Red",
|
||||
"Orange",
|
||||
"Yellow",
|
||||
"Green",
|
||||
"Blue",
|
||||
"Purple",
|
||||
"Pink",
|
||||
"Brown",
|
||||
"Black",
|
||||
"White",
|
||||
"Gray",
|
||||
"Silver",
|
||||
"Gold",
|
||||
"Beige",
|
||||
];
|
||||
const materials = ["Cotton", "Polyester", "Lycra", "Wool", "Silk", "Leather"];
|
||||
|
||||
function pickRandom(array: string[]): string {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
new Trigger({
|
||||
id: "shopify-product-variants",
|
||||
name: "Shopify product variants",
|
||||
apiKey: "<my_api_key>",
|
||||
logLevel: "debug",
|
||||
on: customEvent({
|
||||
name: "shopify.product-variants",
|
||||
schema: z.object({}),
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.logger.info("Get Shopify products variants");
|
||||
|
||||
const results = await shopify.searchProductVariants(
|
||||
"get-shopify-variants",
|
||||
{
|
||||
filter: {
|
||||
productId: ["gid://shopify/Product/8098295546157"],
|
||||
sku: ["variant-1"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const newVariant = await shopify.createProductVariant("create-variant", {
|
||||
productId: results.productVariants[0].product.id,
|
||||
inventoryQuantities: [
|
||||
{
|
||||
availableQuantity: 10,
|
||||
locationId: "gid://shopify/Location/76187369773",
|
||||
},
|
||||
],
|
||||
price: "12.34",
|
||||
sku: `variant-${Math.floor(Math.random() * 1000)}`,
|
||||
options: [pickRandom(sizes), pickRandom(materials)],
|
||||
});
|
||||
|
||||
await ctx.logger.debug("Debug message");
|
||||
|
||||
return newVariant;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
title: "Slack"
|
||||
sidebarTitle: "Slack"
|
||||
description: "Here are some example workflows you could create using the Slack integration."
|
||||
---
|
||||
|
||||
You can explore all the supported Slack functions [here](/).
|
||||
|
||||
## Post to Slack when a GitHub issue is created or modified
|
||||
|
||||
Integrations required: Slack, GitHub
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { github, slack } from "@trigger.dev/integrations";
|
||||
import { z } from "zod";
|
||||
|
||||
new Trigger({
|
||||
id: "my-workflow-1",
|
||||
name: "Posts to Slack when GitHub Issue created or modified",
|
||||
apiKey: "<my_api_key>",
|
||||
on: github.events.issueEvent({
|
||||
repo: "my-github-org/my-github-repo",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
const response = await slack.postMessage("send-to-slack", {
|
||||
channelName: "my-slack-channel-name",
|
||||
text: `A new issue has been created or modified. ${event.action}`,
|
||||
});
|
||||
|
||||
return response.message;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
title: "Delays"
|
||||
sidebarTitle: "Delays"
|
||||
description: "You can trigger workflow steps to fire before and after a delay. There are 2 types of delay, waitFor and waitUntil."
|
||||
---
|
||||
|
||||
## `waitFor` delay
|
||||
|
||||
`waitFor` takes a unique key and an object that sets the desired time as a key value pair.
|
||||
|
||||
```ts
|
||||
await ctx.waitFor("wait-for-1", { seconds: 30 });
|
||||
```
|
||||
|
||||
<Note>Reminder: Ensure all keys are unique across all triggers.</Note>
|
||||
|
||||
### Example
|
||||
|
||||
This example posts a message to Slack after a 30 second delay:
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { github, slack } from "@trigger.dev/integrations";
|
||||
import { z } from "zod";
|
||||
|
||||
new Trigger({
|
||||
id: "wait-for-example",
|
||||
name: "Post to Slack after delay",
|
||||
apiKey: "<my_api_key>",
|
||||
on: customEvent({
|
||||
name: "waitForExample",
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.waitFor("initial-wait", { seconds: 30 });
|
||||
|
||||
const response = await slack.postMessage("send-to-slack", {
|
||||
channel: "my-slack-channel",
|
||||
text: `This message posted after a 30 second delay.`,
|
||||
});
|
||||
|
||||
return response.message;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
## `waitUntil` delay
|
||||
|
||||
`waitUntil` takes a unique key and a date function to set desired time.
|
||||
|
||||
```ts
|
||||
await ctx.waitUntil("wait-until-1", new Date(Date.now() + 1000 * 60 * 1));
|
||||
```
|
||||
|
||||
<Note>Reminder: Ensure all keys are unique across all triggers.</Note>
|
||||
|
||||
### Example
|
||||
|
||||
This example posts a message to Slack 1 hour after the workflow triggers:
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { github, slack } from "@trigger.dev/integrations";
|
||||
import { z } from "zod";
|
||||
|
||||
new Trigger({
|
||||
id: "wait-until-example",
|
||||
name: "Post to Slack after 1hr delay",
|
||||
apiKey: "<my_api_key>",
|
||||
on: customEvent({
|
||||
name: "waitUntilExample",
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.waitUntil("wait-until", new Date(Date.now() + 1000 * 60 * 1));
|
||||
|
||||
const response = await slack.postMessage("send-to-slack", {
|
||||
channel: "my-slack-channel",
|
||||
text: `This message posted after a 30 second delay.`,
|
||||
});
|
||||
|
||||
return response.message;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
@@ -1,206 +0,0 @@
|
||||
---
|
||||
title: "Generic Fetch"
|
||||
sidebarTitle: "Fetch"
|
||||
description: "A generic fetch function that can be used to call any HTTP endpoint"
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
A `fetch` function is available to use inside a `Trigger.run` function through the `context` argument, and should be familiar for anyone who has used the standard [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API, with a few modifications.
|
||||
|
||||
```ts
|
||||
import { Trigger } from "@trigger.dev/sdk";
|
||||
|
||||
new Trigger({
|
||||
id: "fetch-example",
|
||||
name: "Fetch Example",
|
||||
on: customEvent({
|
||||
name: "example.fetch",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.fetch("Example Key", "http://httpbin.org/get");
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
Notice the first parameter to `fetch` is a `key` that is used to identify this call to fetch to support resumability. Please see the [Resumability](/guides/resumability) guide for more information.
|
||||
|
||||
You can also import `fetch` and use it outside of a `Trigger.run` function:
|
||||
|
||||
```ts httpBin.ts
|
||||
import { fetch } from "@trigger.dev/sdk";
|
||||
|
||||
export function httpBinGet() {
|
||||
return fetch("Example Key", "http://httpbin.org/get");
|
||||
}
|
||||
```
|
||||
|
||||
Now you can use the exported `httpBinGet` function inside your `Trigger.run` function:
|
||||
|
||||
```ts
|
||||
import { Trigger } from "@trigger.dev/sdk";
|
||||
import { httpBinGet } from "./httpBin";
|
||||
|
||||
new Trigger({
|
||||
id: "fetch-example",
|
||||
name: "Fetch Example",
|
||||
on: customEvent({
|
||||
name: "example.fetch",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await httpBinGet();
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
This is useful if you want to wrap `fetch` to provide an SDK like experience inside your workflows.
|
||||
|
||||
<Warning>
|
||||
Calling `fetch` when not inside of a workflow run will result in a thrown
|
||||
Error.
|
||||
</Warning>
|
||||
|
||||
## Response
|
||||
|
||||
The return value of `fetch` is a similar to a normal fetch response, but we will automatically parse the response body as JSON and provide it as `body`, like so:
|
||||
|
||||
```ts
|
||||
const response = await ctx.fetch("Example Key", "http://httpbin.org/get");
|
||||
|
||||
console.log(response.body.url); // http://httpbin.org/get
|
||||
```
|
||||
|
||||
By default, the `body` is typed as `any`, but you can provide a [`Zod`](/guides/zod) schema to validate the response body and get a more specific type:
|
||||
|
||||
```ts
|
||||
import { z } from "zod";
|
||||
|
||||
const response = await ctx.fetch("Get HTTPBin", "http://httpbin.org/get", {
|
||||
responseSchema: z.object({
|
||||
url: z.string(),
|
||||
origin: z.string(),
|
||||
headers: z.record(z.string()),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Now the `body` will be typed as:
|
||||
|
||||
```ts
|
||||
{
|
||||
url: string;
|
||||
origin: string;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
```
|
||||
|
||||
It's okay to not be comprehensive with the schema, as long as the response body matches the schema, it will be valid. Do note that any properties not included in the schema will be excluded, unless you use `.passthrough()`:
|
||||
|
||||
```ts
|
||||
const response = await ctx.fetch("Get HTTPBin", "http://httpbin.org/get", {
|
||||
responseSchema: z.object({
|
||||
url: z.string(),
|
||||
origin: z.string(),
|
||||
headers: z.record(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
console.log(response.body); // Only includes url, origin, and headers
|
||||
|
||||
// Using passthrough():
|
||||
const response = await ctx.fetch("Get HTTPBin", "http://httpbin.org/get", {
|
||||
responseSchema: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
origin: z.string(),
|
||||
headers: z.record(z.string()),
|
||||
})
|
||||
.passthrough(),
|
||||
});
|
||||
|
||||
console.log(response.body); // Includes url, origin, headers, and everything else
|
||||
```
|
||||
|
||||
<Note>
|
||||
The fetch function currently only supports JSON request and response bodies.
|
||||
</Note>
|
||||
|
||||
## Secret values
|
||||
|
||||
If you are using a header with a secret value, you can use our `secureString` [tagged template](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) to ensure that the value is not logged in the trigger.dev logs:
|
||||
|
||||
```ts
|
||||
import { Trigger, secureString } from "@trigger.dev/sdk";
|
||||
|
||||
new Trigger({
|
||||
id: "fetch-example",
|
||||
name: "Fetch Example",
|
||||
on: customEvent({
|
||||
name: "example.fetch",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.fetch("🔑 Secret API", "http://httpbin.org/post", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: secureString`Bearer ${process.env.SECRET_API_KEY}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
Which will show in the trigger.dev app as:
|
||||
|
||||

|
||||
|
||||
## Params
|
||||
|
||||
<ParamField path="key" type="string" required={true}>
|
||||
A unique string. Please see the [Keys and Resumability](/guides/resumability)
|
||||
doc for more info.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="url" type="string | URL" required={true}>
|
||||
The URL to fetch.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="options" type="object" required={false}>
|
||||
<Expandable title="properties">
|
||||
<ParamField path="method" type="string" required={false}>
|
||||
The HTTP method to use. Defaults to `GET`.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="headers" type="record" required={false}>
|
||||
An object of headers to send with the request.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="responseSchema" type="zod Schema" required={false}>
|
||||
A [Zod](/guides/zod) schema to validate the response body.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="body" type="json" required={false}>
|
||||
The body to send with the request. Only valid for `POST`, `PUT`, and `PATCH`. You don't need to `JSON.stringify` before passing it in.
|
||||
</ParamField>
|
||||
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
## Response
|
||||
|
||||
<ParamField path="ok" type="boolean">
|
||||
Whether the response was successful. Will always be true because we throw an
|
||||
error and stop the trigger if the response is not successful.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="body" type="object" required={false}>
|
||||
The response body, parsed with the `responseSchema` if provided.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="status" type="number">
|
||||
The HTTP status code.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="headers" type="object">
|
||||
The response headers.
|
||||
</ParamField>
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "Logging"
|
||||
sidebarTitle: "Logging"
|
||||
description: "There are 4 different types of logs you can add as workflow steps."
|
||||
---
|
||||
|
||||
## Coming soon
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "Loops, Conditionals…"
|
||||
sidebarTitle: "Loops, Conditionals…"
|
||||
description: "Write any type of code."
|
||||
---
|
||||
|
||||
## Coming soon
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
title: "Send event"
|
||||
sidebarTitle: "Send event"
|
||||
description: "Send events to trigger your custom events workflows."
|
||||
---
|
||||
|
||||
## How to send an event
|
||||
|
||||
You can send an event from anywhere in your code, including from inside another workflow.
|
||||
|
||||
```ts
|
||||
import { sendEvent } from "@trigger.dev/sdk";
|
||||
|
||||
//send an event with the name "user.created"
|
||||
//and the payload
|
||||
await sendEvent("start-fire-2", {
|
||||
name: "user.created",
|
||||
payload: { userId: "123456" },
|
||||
});
|
||||
```
|
||||
|
||||
If you are calling this from inside a workflow, ensure that the first parameter is unique inside your workflow.
|
||||
|
||||
## Sending events from other workflows
|
||||
|
||||
It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them.
|
||||
|
||||
## Writing a workflow that is triggered by an event
|
||||
|
||||
You should read [the documentation on custom events](/triggers/custom-events) to learn how to write a workflow that is triggered by an event.
|
||||
@@ -1,223 +0,0 @@
|
||||
---
|
||||
title: "Getting started with Trigger.dev"
|
||||
sidebarTitle: "Quick start"
|
||||
description: "Get your first workflow running in just a few minutes"
|
||||
---
|
||||
|
||||
Trigger.dev workflows are written in your codebase and run in your existing infrastructure. This means:
|
||||
|
||||
- They are version controlled with the rest of your code.
|
||||
- They run locally and don't require you to use tunnels.
|
||||
|
||||
<Note>
|
||||
Currently we require you to have a long-running Node.js server, e.g. Express.
|
||||
Support for serverless is coming soon.
|
||||
</Note>
|
||||
|
||||
## 1. Installing @trigger.dev packages
|
||||
|
||||
In your existing project install the packages:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="npm">
|
||||
|
||||
```bash
|
||||
npm install @trigger.dev/sdk @trigger.dev/integrations zod
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm install @trigger.dev/sdk @trigger.dev/integrations zod
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="yarn">
|
||||
|
||||
```bash
|
||||
yarn add @trigger.dev/sdk @trigger.dev/integrations zod
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
What do the packages do?
|
||||
|
||||
- `@trigger.dev/sdk` is required to use Trigger.dev, it allows you to run workflows.
|
||||
- `@trigger.dev/integrations` allows you to easily subscribe to webhooks and use API calls from popular services.
|
||||
- `zod` is used to define schemas (the expected shape of an object).
|
||||
|
||||
## 2. Sign in to your Trigger.dev dashboard
|
||||
|
||||
Go to [trigger.dev](https://app.trigger.dev) and sign up or login to your account.
|
||||
|
||||
## 3. Get your API keys
|
||||
|
||||
In the bottom-left corner of an Organization page you can find your API keys.
|
||||

|
||||
|
||||
## 4. Creating your first workflow
|
||||
|
||||
Workflows are triggered by events. In this example, we have defined a custom event called `user.created`. We support many different types of events including webhooks, scheduled, and more coming soon.
|
||||
|
||||
```ts first-workflow.ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { slack } from "@trigger.dev/integrations";
|
||||
import { z } from "zod";
|
||||
|
||||
const postMessage = new Trigger({
|
||||
id: "new-user",
|
||||
name: "New user slack message",
|
||||
apiKey: "<your_api_key>",
|
||||
logLevel: "info",
|
||||
on: customEvent({
|
||||
name: "user.created",
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
paidPlan: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.logger.info("This log will appear on the Trigger.dev run page");
|
||||
|
||||
//send a message to the #new-users Slack channel with user details
|
||||
const response = await slack.postMessage("send-to-slack", {
|
||||
channel: "new-users",
|
||||
text: `New user: ${event.name} (${event.email}) signed up. ${
|
||||
event.paidPlan ? "They are paying" : "They are on the free plan"
|
||||
}.`,
|
||||
});
|
||||
|
||||
return response.message;
|
||||
},
|
||||
});
|
||||
|
||||
//this workflow will now connect and start listening for events
|
||||
postMessage.listen();
|
||||
```
|
||||
|
||||
You'll notice that when we subscribe to the custom event we have to say the name of the event and provide a schema. Schemas are created using Zod. In this case events must send an object that has `name`, `email`, and `paidPlan`.
|
||||
|
||||
<Note>
|
||||
If the event is triggered with an object that doesn't match this schema, the
|
||||
workflow won't run.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
Above we set the API key inside the Trigger object. We recommend instead that
|
||||
you add an environment variable called `TRIGGER_API_KEY`. That way you can use
|
||||
your development API key locally and the production one when you deploy.
|
||||
</Note>
|
||||
|
||||
## 6. Run your web server
|
||||
|
||||
Run your server how you normally would, e.g. `npm run dev`. This will connect your workflow to Trigger.dev, so we can start sending you events. You should see some log messages in your server console (tip: you can turn these off by removing the `logLevel: "info"` from the code above).
|
||||
|
||||
## 5. Testing your workflow from the dashboard
|
||||
|
||||
Now that the workflow is connected to Trigger.dev we need to trigger it. You can easily test your workflow from [your Trigger.dev dashboard](https://app.trigger.dev).
|
||||
|
||||
On the organization page you should see that the Workflow has now appeared (you may need to refresh the page from last time).
|
||||
|
||||

|
||||
|
||||
Move to the "New user slack message" and you will see the workflow page. There have been no runs yet.
|
||||
|
||||
Move to the "Test" page and input a valid test event, remember the workflow expects a name, email and paidPlan. You can copy this:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Rick Astley",
|
||||
"email": "nevergonn@giveyou.up",
|
||||
"paidPlan": true
|
||||
}
|
||||
```
|
||||
|
||||
Hit the "Run test" button and it will take us to our first run 🚀!
|
||||
|
||||

|
||||
|
||||
## 6. The run page
|
||||
|
||||
All of the steps in a workflow, including the initial event, can be viewed in detail. You will need to refresh the page if it's running to see it move between steps.
|
||||
|
||||
But there's a problem, we've used Slack in our code and we haven't authenticated.
|
||||
|
||||

|
||||
|
||||
## 7. Authenticating with Slack
|
||||
|
||||
When a workflow step uses an API integration that you haven't already authenticated with, it will pause until you've authenticated.
|
||||
|
||||
Simply click the "Connect to Slack" button and sign-in with your desired Slack workspace. As soon as you do, the workflow will pick up where it left off.
|
||||
|
||||
Test complete!
|
||||
|
||||

|
||||
|
||||
## 8. Triggering this workflow from code
|
||||
|
||||
As this workflow uses a custom event, we need to manually trigger it from our code. Anywhere in your code you can do this:
|
||||
|
||||
```ts
|
||||
import { sendEvent } from "@trigger.dev/sdk";
|
||||
|
||||
/*
|
||||
...your other code
|
||||
*/
|
||||
|
||||
await sendEvent({
|
||||
apiKey: "<my_api_key>",
|
||||
event: {
|
||||
name: "Eleven",
|
||||
email: "jane@hawksmoorhigh.edu",
|
||||
paidPlan: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
When you run your server and this code executes, it will trigger the workflow. You will see this in the run list on your workflow page.
|
||||
|
||||
## Next steps
|
||||
|
||||
There are many things we didn't cover here, including Webhook and Scheduled triggers. Below are a some more features to explore:
|
||||
|
||||
### Triggers
|
||||
|
||||
Triggers are what cause your workflows to run.
|
||||
|
||||
<CardGroup>
|
||||
<Card title="Webhooks" icon="rectangle-terminal" href="/triggers/webhooks">
|
||||
Easily subscribe to the APIs you're using
|
||||
</Card>
|
||||
<Card title="Scheduled" icon="sliders" href="/triggers/scheduled">
|
||||
Trigger your workflows on a repeating schedule
|
||||
</Card>
|
||||
<Card title="Custom events" icon="heading" href="/triggers/custom-events">
|
||||
More details on custom events
|
||||
</Card>
|
||||
<Card title="More coming soon" icon="heading">
|
||||
On received email, HTTP endpoint and AWS Event Bridge
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Functions
|
||||
|
||||
<CardGroup>
|
||||
<Card
|
||||
title="API integrations"
|
||||
icon="rectangle-terminal"
|
||||
href="/integrations/apis"
|
||||
>
|
||||
We are making it easy to use lots of APIs by adding integrations.
|
||||
</Card>
|
||||
<Card title="Delays" icon="sliders" href="/functions/delays">
|
||||
Add delays to your workflows. They're resilient so it doesn't matter if your
|
||||
server goes down.
|
||||
</Card>
|
||||
<Card title="Send event" icon="heading" href="/functions/send-event">
|
||||
Send an event, to trigger a custom event workflow
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "Resumability"
|
||||
sidebarTitle: "Keys and Resumability"
|
||||
description: "Keys and Resumability"
|
||||
---
|
||||
|
||||
## Coming soon
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "Zod"
|
||||
sidebarTitle: "Zod"
|
||||
description: "TypeScript-first schema validation with static type inference"
|
||||
---
|
||||
|
||||
## Coming soon
|
||||
|
Before Width: | Height: | Size: 412 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 372 KiB |
@@ -1,32 +0,0 @@
|
||||
---
|
||||
title: "APIs"
|
||||
sidebarTitle: "APIs"
|
||||
description: "Integrations"
|
||||
---
|
||||
|
||||
## Coming soon
|
||||
|
||||
We will show each API as a subpage. Each API page will break down all the functions available for each API. It will also link to the relevant sample projects.
|
||||
|
||||
<CardGroup>
|
||||
<Card
|
||||
title="GitHub"
|
||||
icon="rectangle-terminal"
|
||||
href="/integrations/apis/github"
|
||||
></Card>
|
||||
<Card
|
||||
title="Shopify"
|
||||
icon="rectangle-terminal"
|
||||
href="/integrations/apis/shopify"
|
||||
></Card>
|
||||
<Card
|
||||
title="Slack"
|
||||
icon="rectangle-terminal"
|
||||
href="/integrations/apis/slack"
|
||||
></Card>
|
||||
</CardGroup>
|
||||
|
||||
### Do you need a specific integration?
|
||||
|
||||
If so, [let us know](mailto:hello@trigger.dev) and we'll build it for
|
||||
you.
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
title: "GitHub"
|
||||
sidebarTitle: "GitHub"
|
||||
description: "Support for the GitHub API"
|
||||
---
|
||||
|
||||
## GitHub API integration
|
||||
|
||||
Full reference of all functions is coming soon.
|
||||
|
||||
Check out some example workflows using the GitHub API [here](/examples/github).
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
title: "GitHub Events"
|
||||
sidebarTitle: "Events"
|
||||
description: "GitHub Triggers"
|
||||
---
|
||||
|
||||
## GitHub API integration
|
||||
|
||||
Full reference of all functions is coming soon.
|
||||
|
||||
Check out some example workflows using the GitHub API [here](/examples/github).
|
||||
|
||||
### Events
|
||||
|
||||
These are the events you can use to trigger a workflow
|
||||
|
||||
### Requests
|
||||
|
||||
These are the requests you can make to the GitHub API.
|
||||
@@ -1,221 +0,0 @@
|
||||
---
|
||||
title: "New Star Event"
|
||||
sidebarTitle: "New Star"
|
||||
description: "Trigger a workflow whenever a user star's the specified GitHub repository."
|
||||
---
|
||||
|
||||
# New Star
|
||||
|
||||
Trigger a workflow whenever a user star's the specified GitHub repository.
|
||||
|
||||
## Params
|
||||
|
||||
<ParamField path="repo" type="string" required={true}>
|
||||
The full path to the repository, including the organization
|
||||
</ParamField>
|
||||
|
||||
## Event
|
||||
|
||||
<ResponseField name="action" type="string" default="created">
|
||||
The star action, which in this case is always `"created"`.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="starred_at" type="Date" required={true}>
|
||||
The date and time the starring occurred.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="repository" type="object" required={true}>
|
||||
A repository on GitHub.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="sender" type="object" required={true}>
|
||||
The GitHub user object who starred the repository.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="organization" type="object">
|
||||
The organization object the repository belongs to.
|
||||
</ResponseField>
|
||||
|
||||
## Example Workflows
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```typescript Basic
|
||||
new Trigger({
|
||||
id: "new-star",
|
||||
name: "On New Star",
|
||||
on: github.events.newStarEvent({
|
||||
repo: "triggerdotdev/trigger.dev",
|
||||
}),
|
||||
run: async (event, ctx) => {},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
```typescript Notify Slack on New Star
|
||||
import { slack } from "@trigger.dev/integrations";
|
||||
|
||||
new Trigger({
|
||||
id: "new-star",
|
||||
name: "On New Star",
|
||||
on: github.events.newStarEvent({
|
||||
repo: "triggerdotdev/trigger.dev",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await slack.postMessage("⭐️ New Star", {
|
||||
channelName: "github-stars",
|
||||
text: `Repo ${event.repository.full_name} got a star from ${event.sender.login}, for a total of ${event.repository.stargazers_count} stars!`,
|
||||
});
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Example Event payload
|
||||
|
||||
```json Example
|
||||
{
|
||||
"action": "created",
|
||||
"starred_at": "2023-01-21T23:47:38Z",
|
||||
"repository": {
|
||||
"id": 516315865,
|
||||
"node_id": "R_kgDOHsZa2Q",
|
||||
"name": "apihero-openapi-generator",
|
||||
"full_name": "triggerdotdev/apihero-openapi-generator",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "triggerdotdev",
|
||||
"id": 95297378,
|
||||
"node_id": "O_kgDOBa4fYg",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/triggerdotdev",
|
||||
"html_url": "https://github.com/triggerdotdev",
|
||||
"followers_url": "https://api.github.com/users/triggerdotdev/followers",
|
||||
"following_url": "https://api.github.com/users/triggerdotdev/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/triggerdotdev/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/triggerdotdev/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/triggerdotdev/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/triggerdotdev/orgs",
|
||||
"repos_url": "https://api.github.com/users/triggerdotdev/repos",
|
||||
"events_url": "https://api.github.com/users/triggerdotdev/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/triggerdotdev/received_events",
|
||||
"type": "Organization",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/triggerdotdev/apihero-openapi-generator",
|
||||
"description": null,
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator",
|
||||
"forks_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/forks",
|
||||
"keys_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/teams",
|
||||
"hooks_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/events",
|
||||
"assignees_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/tags",
|
||||
"blobs_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/subscription",
|
||||
"commits_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/merges",
|
||||
"archive_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/downloads",
|
||||
"issues_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/triggerdotdev/apihero-openapi-generator/deployments",
|
||||
"created_at": "2022-07-21T09:57:53Z",
|
||||
"updated_at": "2023-01-21T23:47:38Z",
|
||||
"pushed_at": "2022-10-20T13:21:09Z",
|
||||
"git_url": "git://github.com/triggerdotdev/apihero-openapi-generator.git",
|
||||
"ssh_url": "git@github.com:triggerdotdev/apihero-openapi-generator.git",
|
||||
"clone_url": "https://github.com/triggerdotdev/apihero-openapi-generator.git",
|
||||
"svn_url": "https://github.com/triggerdotdev/apihero-openapi-generator",
|
||||
"homepage": null,
|
||||
"size": 4962,
|
||||
"stargazers_count": 1,
|
||||
"watchers_count": 1,
|
||||
"language": "TypeScript",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_downloads": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_discussions": false,
|
||||
"forks_count": 0,
|
||||
"mirror_url": null,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 0,
|
||||
"license": {
|
||||
"key": "mit",
|
||||
"name": "MIT License",
|
||||
"spdx_id": "MIT",
|
||||
"url": "https://api.github.com/licenses/mit",
|
||||
"node_id": "MDc6TGljZW5zZTEz"
|
||||
},
|
||||
"allow_forking": true,
|
||||
"is_template": false,
|
||||
"web_commit_signoff_required": false,
|
||||
"topics": [],
|
||||
"visibility": "public",
|
||||
"forks": 0,
|
||||
"open_issues": 0,
|
||||
"watchers": 1,
|
||||
"default_branch": "main"
|
||||
},
|
||||
"organization": {
|
||||
"login": "triggerdotdev",
|
||||
"id": 95297378,
|
||||
"node_id": "O_kgDOBa4fYg",
|
||||
"url": "https://api.github.com/orgs/triggerdotdev",
|
||||
"repos_url": "https://api.github.com/orgs/triggerdotdev/repos",
|
||||
"events_url": "https://api.github.com/orgs/triggerdotdev/events",
|
||||
"hooks_url": "https://api.github.com/orgs/triggerdotdev/hooks",
|
||||
"issues_url": "https://api.github.com/orgs/triggerdotdev/issues",
|
||||
"members_url": "https://api.github.com/orgs/triggerdotdev/members{/member}",
|
||||
"public_members_url": "https://api.github.com/orgs/triggerdotdev/public_members{/member}",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95297378?v=4",
|
||||
"description": ""
|
||||
},
|
||||
"sender": {
|
||||
"login": "ericallam",
|
||||
"id": 534,
|
||||
"node_id": "MDQ6VXNlcjUzNA==",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/534?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/ericallam",
|
||||
"html_url": "https://github.com/ericallam",
|
||||
"followers_url": "https://api.github.com/users/ericallam/followers",
|
||||
"following_url": "https://api.github.com/users/ericallam/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/ericallam/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/ericallam/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/ericallam/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/ericallam/orgs",
|
||||
"repos_url": "https://api.github.com/users/ericallam/repos",
|
||||
"events_url": "https://api.github.com/users/ericallam/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/ericallam/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
title: "Shopify"
|
||||
sidebarTitle: "Shopify"
|
||||
description: "Support for the Shopify API"
|
||||
---
|
||||
|
||||
## Shopify API integration
|
||||
|
||||
Full reference of all functions is coming soon.
|
||||
|
||||
Check out some example workflows using the Shopify API [here](/examples/shopify).
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
title: "Slack"
|
||||
sidebarTitle: "Slack"
|
||||
description: "Support for the Slack API"
|
||||
---
|
||||
|
||||
## Slack API integration
|
||||
|
||||
Full reference of all functions is coming soon.
|
||||
|
||||
Check out some example workflows using the Slack API [here](/examples/slack).
|
||||
@@ -1,109 +0,0 @@
|
||||
---
|
||||
title: "Post Message"
|
||||
sidebarTitle: "Post Message"
|
||||
description: "Post a message to a Slack channel"
|
||||
---
|
||||
|
||||
Publish slack messages to a public or private channel in your Slack Workspace as the Trigger.dev Slack bot. If you need to publish messages to your customer's Slack channels, consider using [Incoming Webhooks](https://api.slack.com/messaging/webhooks) and our [fetch](/functions/fetch) function.
|
||||
|
||||
## Params
|
||||
|
||||
<ParamField path="key" type="string" required={true}>
|
||||
A unique string. Please see the [Keys and Resumability](/guides/resumability)
|
||||
doc for more info.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="message" type="object" required={true}>
|
||||
<Expandable title="properties" defaultOpen={true}>
|
||||
<ParamField path="channelName" type="string" required={false}>
|
||||
The name of the channel, can optionally include the `#`. E.g. `#team`.
|
||||
Alternatively you can use the `channelId` param.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="channelId" type="string" required={false}>
|
||||
The slack ID of the channel, e.g. `C04GWUTDC4W`. Can be used instead of `channelName`. Use `channelId` if the slack channel name could change.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="text" type="string" required={true}>
|
||||
The formatted text of the message to be published, formatted as [mrkdwn](https://api.slack.com/reference/surfaces/formatting#basics).
|
||||
</ParamField>
|
||||
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
|
||||
## Response
|
||||
|
||||
<ResponseField name="ok" type="boolean" default="true">
|
||||
Always true; non-ok responses will halt the workflow run and throw an error.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="channel" type="string">
|
||||
The channel ID of the channel the message was published to.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="ts" type="string">
|
||||
The "timestamp ID" of the message, which can be used to update or delete the
|
||||
message.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="message" type="object">
|
||||
<Expandable title="properties">
|
||||
<ResponseField name="ts" type="string">
|
||||
The "timestamp ID" of the message, which can be used to update or delete the message.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="text" type="string">
|
||||
The text published to the channel.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="user" type="string">
|
||||
The user ID of the user who published the message.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="bot_id" type="string">
|
||||
The bot ID of the bot that published the message.
|
||||
</ResponseField>
|
||||
|
||||
</Expandable>
|
||||
</ResponseField>
|
||||
|
||||
## Example Workflows
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```typescript Notify Slack on New Star
|
||||
import { slack, github } from "@trigger.dev/integrations";
|
||||
|
||||
new Trigger({
|
||||
id: "new-star",
|
||||
name: "On New Star",
|
||||
on: github.events.newStarEvent({
|
||||
repo: "triggerdotdev/trigger.dev",
|
||||
}),
|
||||
run: async (event, ctx) => {
|
||||
await slack.postMessage("⭐️ New Star", {
|
||||
channelName: "github-stars",
|
||||
text: `Repo ${event.repository.full_name} got a star from ${event.sender.login}, for a total of ${event.repository.stargazers_count} stars!`,
|
||||
});
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"ts": "1673618429.084699",
|
||||
"channel": "C04GWUTDC3W",
|
||||
"message": {
|
||||
"ts": "1673618429.084699",
|
||||
"text": "New domain created: trigger.dev by customer 1st-ever-slack-workflow-yippie",
|
||||
"type": "message",
|
||||
"user": "U04JTTQ08SF",
|
||||
"bot_id": "B04JR3DCB4M"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "Authentication"
|
||||
sidebarTitle: "Authentication"
|
||||
description: "Authentication"
|
||||
---
|
||||
|
||||
Coming soon...
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"name": "Trigger.dev",
|
||||
"logo": {
|
||||
"dark": "/logo/light.png",
|
||||
"light": "/logo/light.png",
|
||||
"href": "https://app.trigger.dev"
|
||||
},
|
||||
"favicon": "/images/favicon.png",
|
||||
"colors": {
|
||||
"primary": "#0fd122",
|
||||
"light": "#41FF54",
|
||||
"dark": "#632CCC",
|
||||
"anchors": {
|
||||
"from": "#069613",
|
||||
"to": "#0fd122"
|
||||
}
|
||||
},
|
||||
"modeToggle": {
|
||||
"default": "dark",
|
||||
"isHidden": true
|
||||
},
|
||||
"topbarLinks": [
|
||||
{
|
||||
"name": "GitHub",
|
||||
"url": "https://github.com/triggerdotdev/trigger.dev"
|
||||
}
|
||||
],
|
||||
"topbarCtaButton": {
|
||||
"name": "Trigger.dev",
|
||||
"url": "https://app.trigger.dev"
|
||||
},
|
||||
"navigation": [
|
||||
{
|
||||
"group": "Getting Started",
|
||||
"pages": [
|
||||
"getting-started"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Triggers",
|
||||
"pages": [
|
||||
"triggers/webhooks",
|
||||
"triggers/custom-events",
|
||||
"triggers/scheduled"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": [
|
||||
{
|
||||
"group": "APIs",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Slack",
|
||||
"pages": [
|
||||
"integrations/apis/slack/actions/post-message"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"integrations/authentication"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Functions",
|
||||
"pages": [
|
||||
"functions/fetch",
|
||||
"functions/logging",
|
||||
"functions/delays",
|
||||
"functions/send-event",
|
||||
"functions/loops-conditionals-etc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Guides",
|
||||
"pages": [
|
||||
"guides/resumability",
|
||||
"guides/zod"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Webhook Catalog",
|
||||
"pages": [
|
||||
{
|
||||
"group": "GitHub",
|
||||
"pages": [
|
||||
"integrations/apis/github/events/new-star"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Example workflows",
|
||||
"pages": [
|
||||
"examples/github",
|
||||
"examples/shopify",
|
||||
"examples/slack"
|
||||
]
|
||||
}
|
||||
],
|
||||
"footerSocials": {
|
||||
"website": "https://app.trigger.dev",
|
||||
"twitter": "https://twitter.com/triggerdotdev",
|
||||
"github": "https://github.com/triggerdotdev/trigger.dev"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"version": "1.0.0",
|
||||
"description": "Trigger.dev documentation",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "PORT=3050 mintlify dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mintlify": "^1.3.0"
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
title: "Resources"
|
||||
description: "Learn more about how to build amazing documentation"
|
||||
---
|
||||
|
||||
<CardGroup>
|
||||
<Card title="Development" icon="rectangle-terminal" href="https://mintlify.com/docs/development">
|
||||
Run on your computer to see changes locally
|
||||
</Card>
|
||||
<Card title="Global Settings" icon="sliders" href="https://mintlify.com/docs/settings/global">
|
||||
Customizing the theme, branding, and other global configurations
|
||||
</Card>
|
||||
<Card title="Page Titles and Metadata" icon="heading" href="https://mintlify.com/docs/settings/page">
|
||||
Setting the title and metadata for navigation and SEO
|
||||
</Card>
|
||||
<Card title="Writing Content" icon="pen-nib" href="https://mintlify.com/docs/components/overview">
|
||||
Introduction on how to add and edit content
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
Learn more about how to take your documentation to the next level
|
||||
|
||||
<CardGroup>
|
||||
<Card title="Custom Domains" icon="browser" href="https://mintlify.com/docs/settings/custom-domain/subdomain">
|
||||
Get the documentation hosted at your custom domain
|
||||
</Card>
|
||||
<Card title="Advanced Components" icon="paintbrush-pencil" href="https://mintlify.com/docs/components/advanced/accordion">
|
||||
Advanced components built for specific communication mediums
|
||||
</Card>
|
||||
<Card title="API Components" icon="square-code" href="https://mintlify.com/docs/api-components/overview">
|
||||
Add an API playground into your documentation
|
||||
</Card>
|
||||
<Card title="Analytics" icon="chart-mixed" href="https://mintlify.com/docs/analytics/supported-integrations">
|
||||
Learn how users are interacting with your documentation
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
title: "Custom event triggers"
|
||||
sidebarTitle: "Custom events"
|
||||
description: "Custom event triggers allow you to run workflows from your own code (or your other workflows)"
|
||||
---
|
||||
|
||||
[Send an event](/functions/send-event) and any workflows that subscribe to that Custom event will get triggered.
|
||||
|
||||
## Name and Schemas
|
||||
|
||||
### Name
|
||||
|
||||
Custom event triggers have a `name`. They will only get triggered when a custom even with that name are sent.
|
||||
|
||||
### Schema
|
||||
|
||||
Custom event triggers have a schema. This is used to validate the data that is sent with the event. If the data does not match the schema, the workflow will not run.
|
||||
|
||||
It also means that inside your run function you will have type safety for the data you receive. We use [Zod](https://github.com/colinhacks/zod#installation) for our schemas – it's a fantastic library that allows you to define schemas in a very simple way.
|
||||
|
||||
You can always start out by using `z.any()` as your schema, and then later on you can add more strict validation.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
import { Trigger, customEvent } from "@trigger.dev/sdk";
|
||||
import { slack } from "@trigger.dev/integrations";
|
||||
|
||||
//this workflow will run when a "user.created" event is sent
|
||||
new Trigger({
|
||||
id: "new-user-slack",
|
||||
name: "New user slack message",
|
||||
apiKey: "<your_api_key>",
|
||||
//this is the custom event subscription
|
||||
on: customEvent({
|
||||
name: "user.created",
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
paidPlan: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
//this function is run when the custom event is received
|
||||
run: async (event, ctx) => {
|
||||
//send a message to the #new-users Slack channel with user details
|
||||
const response = await slack.postMessage("send-to-slack", {
|
||||
channel: "new-users",
|
||||
text: `New user: ${event.name} (${event.email}) signed up. ${
|
||||
event.paidPlan ? "They are paying" : "They are on the free plan"
|
||||
}.`,
|
||||
});
|
||||
|
||||
return response.message;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
That workflow will never run, unless an event is sent from your code somewhere – it can be sent from a completely different server.
|
||||
|
||||
## Sending an event
|
||||
|
||||
There is a [full guide on sending events](/functions/send-event) but here is a quick example:
|
||||
|
||||
```ts
|
||||
import { sendEvent } from "@trigger.dev/sdk";
|
||||
|
||||
//send an event with the name "user.created"
|
||||
await sendEvent("fire-user-created-event", {
|
||||
name: "user.created",
|
||||
payload: { name: "John Doe", email: "john@doe.com", paidPlan: true },
|
||||
});
|
||||
```
|
||||
|
||||
If you are calling this from inside a workflow, ensure that the first parameter is unique inside your workflow.
|
||||
|
||||
<Note>
|
||||
The same event can trigger many different workflows, just subscribe to the
|
||||
same event name to achieve this.
|
||||
</Note>
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
title: "Scheduled triggers"
|
||||
sidebarTitle: "Scheduled"
|
||||
description: "Run a workflow on a recurring schedule"
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Every 5 minutes
|
||||
|
||||
This job will run every 5 minutes, starting 5 minutes after this code is first run on your server (that includes running locally).
|
||||
|
||||
```ts
|
||||
import { Trigger, schedule } from "@trigger.dev/sdk";
|
||||
|
||||
new Trigger({
|
||||
id: "scheduled-workflow",
|
||||
name: "Scheduled Workflow",
|
||||
apiKey: "<your_api_key>",
|
||||
on: scheduleEvent({ rateOf: { minutes: 5 } }),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.logger.info("Received the scheduled event", {
|
||||
event,
|
||||
wallTime: new Date(),
|
||||
});
|
||||
|
||||
return { foo: "bar" };
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
|
||||
### Using CRON syntax
|
||||
|
||||
This job will run at 2:30pm every Monday. You can get help with [CRON syntax](https://crontab.guru/).
|
||||
|
||||
```ts
|
||||
import { Trigger, schedule } from "@trigger.dev/sdk";
|
||||
|
||||
new Trigger({
|
||||
id: "cron-scheduled-workflow",
|
||||
name: "Cron Scheduled Workflow",
|
||||
apiKey: "<your_api_key>",
|
||||
on: scheduleEvent({ cron: "30 14 * * 1" }),
|
||||
run: async (event, ctx) => {
|
||||
await ctx.logger.info("Received the cron scheduled event", {
|
||||
event,
|
||||
wallTime: new Date(),
|
||||
});
|
||||
|
||||
return { foo: "bar" };
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: "Webhooks"
|
||||
sidebarTitle: "Webhooks"
|
||||
description: "Webhooks allow you to subscribe to events from APIs you use."
|
||||
---
|
||||
|
||||
If you want your server to be notified when one of your users has subscribed to your product, or someone has made a change to a support ticket, then you should use webhooks.
|
||||
|
||||
## Advantages of using Trigger.dev for webhooks
|
||||
|
||||
Webhooks can be difficult to work with, especially when developing locally. We make them far easier to use with our [integrations](/integrations/apis).
|
||||
|
||||
- You don't need to register/unregister for webhooks, we do it for you
|
||||
- They work locally during development without needing to use tunnels (e.g. Ngrok)
|
||||
- We receive the webhook, then keep trying to send it to you until you receive it. If your server goes down, no problem.
|
||||
|
||||
## Send a Slack message when a GitHub issue is labeled as Critical
|
||||
|
||||
```ts
|
||||
import { Trigger } from "@trigger.dev/sdk";
|
||||
import { github, slack } from "@trigger.dev/integrations";
|
||||
|
||||
new Trigger({
|
||||
id: "escalate-critical-issues",
|
||||
name: "Posts to Slack when GitHub Issue created or modified",
|
||||
apiKey: "<my_api_key>",
|
||||
//this is the webhook subscription
|
||||
on: github.events.issueEvent({
|
||||
repo: "my-github-org/my-github-repo",
|
||||
}),
|
||||
//this function is run when the webhook fires
|
||||
run: async (event, ctx) => {
|
||||
if (event.action === "labeled") {
|
||||
await ctx.logger.info(
|
||||
`The issue ${event.issue.title} was labeled ${event.label.name}`
|
||||
);
|
||||
|
||||
if (event.label.name === "critical") {
|
||||
await slack.postMessage("send-to-slack", {
|
||||
channel: "serious-issues",
|
||||
text: `Critical issue: ${event.issue.title} was labeled ${event.label.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
}).listen();
|
||||
```
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
{ "workspaceId": "63ea6cff29733e58db1e6b80" }
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
|
||||
/build
|
||||
.env
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,2 @@
|
||||
# trigger-integrations
|
||||
You can create API integrations using this data and system
|
||||
@@ -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"
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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>;
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CacheService {
|
||||
get: (key: string) => Promise<string | null>;
|
||||
set: (key: string, value: string, ttl?: number) => Promise<void>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||