diff --git a/bun.lock b/bun.lock index 7b604b1b8e6..72682c024cd 100644 --- a/bun.lock +++ b/bun.lock @@ -909,6 +909,18 @@ "tslib": "2.6.2", }, }, + "packages/pieces/community/avian": { + "name": "@activepieces/piece-avian", + "version": "0.0.1", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "openai": "4.67.1", + "tslib": "2.6.2", + "zod": "4.3.6", + }, + }, "packages/pieces/community/avoma": { "name": "@activepieces/piece-avoma", "version": "0.1.3", @@ -1030,7 +1042,7 @@ }, "packages/pieces/community/baserow": { "name": "@activepieces/piece-baserow", - "version": "0.8.0", + "version": "0.9.0", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4524,7 +4536,7 @@ }, "packages/pieces/community/microsoft-outlook": { "name": "@activepieces/piece-microsoft-outlook", - "version": "0.3.2", + "version": "0.3.3", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4571,7 +4583,7 @@ }, "packages/pieces/community/microsoft-teams": { "name": "@activepieces/piece-microsoft-teams", - "version": "0.5.0", + "version": "0.5.1", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -4638,7 +4650,7 @@ }, "packages/pieces/community/mistral-ai": { "name": "@activepieces/piece-mistral-ai", - "version": "0.1.4", + "version": "0.2.0", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -5572,7 +5584,7 @@ }, "packages/pieces/community/qawafel": { "name": "@activepieces/piece-qawafel", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -6115,7 +6127,7 @@ }, "packages/pieces/community/service-now": { "name": "@activepieces/piece-service-now", - "version": "0.1.3", + "version": "0.2.0", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -8218,7 +8230,7 @@ }, "packages/shared": { "name": "@activepieces/shared", - "version": "0.68.4", + "version": "0.68.6", "dependencies": { "dayjs": "1.11.9", "deepmerge-ts": "7.1.0", @@ -8474,6 +8486,8 @@ "@activepieces/piece-autocalls": ["@activepieces/piece-autocalls@workspace:packages/pieces/community/autocalls"], + "@activepieces/piece-avian": ["@activepieces/piece-avian@workspace:packages/pieces/community/avian"], + "@activepieces/piece-avoma": ["@activepieces/piece-avoma@workspace:packages/pieces/community/avoma"], "@activepieces/piece-azure-ad": ["@activepieces/piece-azure-ad@workspace:packages/pieces/community/azure-ad"], @@ -9832,11 +9846,11 @@ "@apidevtools/swagger-methods": ["@apidevtools/swagger-methods@3.0.2", "", {}, "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="], - "@apify/consts": ["@apify/consts@2.52.2", "", {}, "sha512-82mKh1V/0OIcH3qzn2aaC9MixeKggH2Cbvfm1uQOzei3VtHXCaZrbIYGmGrIQ4UXsKugiFDHlYw9GqSICSbsaA=="], + "@apify/consts": ["@apify/consts@2.53.0", "", {}, "sha512-gYAPOL24Wry6d8dFJ/0qdzx1BgkEmrUTqhHZ70zmMY99/to47D8utpteUQDOMXMsMCrRP8c/z/dNTGDls1wWKA=="], - "@apify/log": ["@apify/log@2.5.37", "", { "dependencies": { "@apify/consts": "^2.52.2", "ansi-colors": "^4.1.1" } }, "sha512-ZeVrq85GKK8Bm5HSU6+Lr/K9voJkPthQkMnf4FReBa+MSEV+k2RG0z0uZMeGNKKRMpmRxhuo8KDAyAIs4u9f9A=="], + "@apify/log": ["@apify/log@2.5.38", "", { "dependencies": { "@apify/consts": "^2.53.0", "ansi-colors": "^4.1.1" } }, "sha512-Lzmrra3Vvb9Fi4JzWFWPOeSGxItAk50ZHpB070TsSPxuYf9mgJqQ1VlLWn3HNalcdbIf0OoPAFOW31BEs5Khsw=="], - "@apify/utilities": ["@apify/utilities@2.29.2", "", { "dependencies": { "@apify/consts": "^2.52.2", "@apify/log": "^2.5.37" } }, "sha512-g8xET78HrKFrIMIFO8Lrzz+no5q7TmkG4XK6aXpUwhNPpohdKf5s6p7VKzYpNRPlaRG/T9fV/DCKiDIFu+RS8g=="], + "@apify/utilities": ["@apify/utilities@2.29.3", "", { "dependencies": { "@apify/consts": "^2.53.0", "@apify/log": "^2.5.38" } }, "sha512-bbMFHOXlAoJwEfp8+vUauJEfX2pxNPFaAH+E5u1RTjawz6Xq2IcuFNRv33DAC2vPVjDh3oNMTnpfaa0qU8vFuw=="], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], @@ -9846,7 +9860,7 @@ "@atlaskit/adf-schema-generator": ["@atlaskit/adf-schema-generator@2.2.0", "", { "dependencies": { "@atlaskit/editor-prosemirror": "^7.3.0", "@babel/runtime": "^7.0.0", "lodash": "^4.17.21" } }, "sha512-6YtAVbMOBv2MncdT61/KgYQOcqaYlDXLAch+DgfoFDkLj+gr/3W/fxquXw9mSwqFuk7Ikn0BhyCLD1y2G0wBtw=="], - "@atlaskit/adf-utils": ["@atlaskit/adf-utils@19.27.46", "", { "dependencies": { "@atlaskit/adf-schema": "^52.6.0", "@atlaskit/platform-feature-flags": "^1.1.0", "@atlaskit/tmp-editor-statsig": "^72.0.0", "@babel/runtime": "^7.0.0" } }, "sha512-/wVc7IDCCZTJtY7H5vQV/EaDCVLc5s9Rap7Wp+SwjlVZgqgoiz6uadHN4rJEtqPf10bSVGVWD+59OFGbQztNiw=="], + "@atlaskit/adf-utils": ["@atlaskit/adf-utils@19.28.0", "", { "dependencies": { "@atlaskit/adf-schema": "^52.7.0", "@atlaskit/platform-feature-flags": "^1.1.0", "@babel/runtime": "^7.0.0" } }, "sha512-YwrP4VaHXQGXMAzu3O2guFAiB8E4SCD1FksEQmBZowtWn370vS6jXkLhGMSr79WtU762Nu9osWzHsWns4Kk17g=="], "@atlaskit/atlassian-context": ["@atlaskit/atlassian-context@0.2.0", "", { "dependencies": { "@babel/runtime": "^7.0.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-msLRSp0qck6eflkShplgyIoOogNKxKRc6QIWGQlSvKGxHQNEbLEkRGcDzdh8PuBxSs1gda7OqYrdtQYQiPbpTQ=="], @@ -9866,7 +9880,7 @@ "@atlaskit/react-ufo": ["@atlaskit/react-ufo@5.17.0", "", { "dependencies": { "@atlaskit/atlassian-context": "^0.8.0", "@atlaskit/browser-apis": "^0.0.1", "@atlaskit/feature-gate-js-client": "^5.5.0", "@atlaskit/interaction-context": "^3.1.0", "@atlaskit/platform-feature-flags": "^1.1.0", "@babel/runtime": "^7.0.0", "@opentelemetry/api": "^1.9.0", "bind-event-listener": "^3.0.0", "bowser-ultralight": "^1.0.6", "scheduler": "0.23.2", "uuid": "^3.1.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-81lZocFvJrOd0xpk2Iup6aXS/0Cz6Qlf79Y4UrvqumGROoU/qFvtFjP/nTkCBCBYBa6ATgAa/n+qkw8CRQg3iA=="], - "@atlaskit/tmp-editor-statsig": ["@atlaskit/tmp-editor-statsig@72.1.1", "", { "dependencies": { "@atlaskit/feature-gate-js-client": "^5.5.0", "@atlaskit/react-ufo": "^5.17.0", "@babel/runtime": "^7.0.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-1EpKJdpc2lFC4O7Oj/kWYZ2rQMIZ9BClGoZhtm0MKUXIqA0C/bRWTX6Ggq2aPXJww0nCPsceK89VUsJkrFtbqQ=="], + "@atlaskit/tmp-editor-statsig": ["@atlaskit/tmp-editor-statsig@73.0.0", "", { "dependencies": { "@atlaskit/feature-gate-js-client": "^5.5.0", "@atlaskit/react-ufo": "^5.17.0", "@babel/runtime": "^7.0.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6bVLebWPS+OaHFocquVGkQSs+m0905CxvQcIWLDWtU+93ftPqek+nqTfwF09uAenCgFlgBnBIXRA4GNLPuYl6A=="], "@atproto/api": ["@atproto/api@0.16.0", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/lexicon": "^0.4.12", "@atproto/syntax": "^0.4.0", "@atproto/xrpc": "^0.7.1", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-PQHeae6mz/L1YirUslfci7bknfg3RrSZjXpYwzLICxIOvqGKIkOi0+qukC2Py238RhXRo8YZ9dCuole9HQBXDw=="], @@ -11728,7 +11742,7 @@ "@types/semver": ["@types/semver@7.5.6", "", {}, "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="], - "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], @@ -12644,7 +12658,7 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.344", "", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="], + "electron-to-chromium": ["electron-to-chromium@1.5.345", "", {}, "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg=="], "elliptic": ["elliptic@6.5.4", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ=="], @@ -13174,7 +13188,7 @@ "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], - "hono": ["hono@4.12.15", "", {}, "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg=="], + "hono": ["hono@4.12.16", "", {}, "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg=="], "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], @@ -15564,7 +15578,7 @@ "@atlaskit/adf-schema-generator/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], - "@atlaskit/adf-utils/@atlaskit/adf-schema": ["@atlaskit/adf-schema@52.6.5", "", { "dependencies": { "@atlaskit/adf-schema-generator": "^2.2.0", "@atlaskit/editor-prosemirror": "^7.3.0", "@atlaskit/platform-feature-flags": "^1.1.0", "@atlaskit/tmp-editor-statsig": "^72.0.0", "@babel/runtime": "^7.0.0", "css-color-names": "0.0.4", "linkify-it": "^3.0.3", "memoize-one": "^6.0.0" } }, "sha512-9s2bX/TI6IIttfiHFgYFz18oUEmai6kGPbz4uQMI25EuMGTbq/JlqN/2nqM7ctwcVPGdjrwmc2MSu3jAF9Hw9A=="], + "@atlaskit/adf-utils/@atlaskit/adf-schema": ["@atlaskit/adf-schema@52.7.0", "", { "dependencies": { "@atlaskit/adf-schema-generator": "^2.2.0", "@atlaskit/editor-prosemirror": "^7.3.0", "@atlaskit/platform-feature-flags": "^1.1.0", "@atlaskit/tmp-editor-statsig": "^73.0.0", "@babel/runtime": "^7.0.0", "css-color-names": "0.0.4", "linkify-it": "^3.0.3", "memoize-one": "^6.0.0" } }, "sha512-R15BZFw3KE1//FqMtqK8eTSEVt/5RWXUIJipLM3Y+iDd51XLTnJuexggQeFXbH55vUWGGgSILBx9ZI+82EiV2A=="], "@atlaskit/atlassian-context/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], @@ -16738,6 +16752,8 @@ "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], + "@readme/better-ajv-errors/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@rollup/pluginutils/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "@rollup/wasm-node/@types/estree": ["@types/estree@1.0.5", "", {}, "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="], @@ -16904,8 +16920,6 @@ "@tinyhttp/accepts/mime": ["mime@4.0.4", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ=="], - "@tinyhttp/accepts/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], - "@tinyhttp/res/mime": ["mime@4.0.4", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ=="], "@tinyhttp/send/mime": ["mime@4.0.4", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ=="], @@ -16928,6 +16942,8 @@ "@types/request/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="], + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.59.1", "", {}, "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A=="], @@ -17434,8 +17450,6 @@ "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "make-fetch-happen/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], - "make-fetch-happen/socks-proxy-agent": ["socks-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ=="], "mammoth/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], diff --git a/packages/pieces/community/avian/.eslintrc.json b/packages/pieces/community/avian/.eslintrc.json new file mode 100644 index 00000000000..610e15b05bf --- /dev/null +++ b/packages/pieces/community/avian/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "../../../../.eslintrc.base.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} diff --git a/packages/pieces/community/avian/README.md b/packages/pieces/community/avian/README.md new file mode 100644 index 00000000000..6509c3ffce3 --- /dev/null +++ b/packages/pieces/community/avian/README.md @@ -0,0 +1,5 @@ +# pieces-avian + +## Building + +Run `turbo run build --filter=@activepieces/piece-avian` to build the library. diff --git a/packages/pieces/community/avian/package.json b/packages/pieces/community/avian/package.json new file mode 100644 index 00000000000..a982db43033 --- /dev/null +++ b/packages/pieces/community/avian/package.json @@ -0,0 +1,18 @@ +{ + "name": "@activepieces/piece-avian", + "version": "0.0.1", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "openai": "4.67.1", + "zod": "4.3.6", + "tslib": "2.6.2" + }, + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" + } +} diff --git a/packages/pieces/community/avian/src/index.ts b/packages/pieces/community/avian/src/index.ts new file mode 100644 index 00000000000..eb4264cbb74 --- /dev/null +++ b/packages/pieces/community/avian/src/index.ts @@ -0,0 +1,16 @@ +import { createPiece } from '@activepieces/pieces-framework'; +import { askAvian } from './lib/actions/ask-avian'; +import { PieceCategory } from '@activepieces/shared'; +import { avianAuth } from './lib/auth'; + +export const avian = createPiece({ + displayName: 'Avian', + description: 'Integrate with Avian to leverage its powerful language models for generating human-like text based on your prompts.', + auth: avianAuth, + categories: [PieceCategory.ARTIFICIAL_INTELLIGENCE], + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/avian.png', + authors: ['avianion'], + actions: [askAvian], + triggers: [], +}); diff --git a/packages/pieces/community/avian/src/lib/actions/ask-avian.ts b/packages/pieces/community/avian/src/lib/actions/ask-avian.ts new file mode 100644 index 00000000000..e19a529f8b3 --- /dev/null +++ b/packages/pieces/community/avian/src/lib/actions/ask-avian.ts @@ -0,0 +1,203 @@ +import { avianAuth } from '../auth'; +import { createAction, Property, StoreScope } from '@activepieces/pieces-framework'; +import OpenAI from 'openai'; +import { baseUrl } from '../common/common'; +import { z } from 'zod'; +import { propsValidation, httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common'; + +export const askAvian = createAction({ + auth: avianAuth, + name: 'ask_avian', + displayName: 'Ask Avian', + description: 'Ask Avian anything you want!', + props: { + model: Property.Dropdown({ + auth: avianAuth, + displayName: 'Model', + required: true, + description: 'The model which will generate the completion.', + refreshers: [], + options: async ({ auth }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Enter your API key first', + options: [], + }; + } + try { + const response = await httpClient.sendRequest<{ + data: Array<{ id: string }>; + }>({ + method: HttpMethod.GET, + url: `${baseUrl}/models`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: auth.secret_text, + }, + }); + return { + disabled: false, + options: response.body.data.map((model) => { + return { + label: model.id, + value: model.id, + }; + }), + }; + } catch (error) { + return { + disabled: true, + options: [], + placeholder: "Couldn't load models, API key is invalid", + }; + } + }, + }), + prompt: Property.LongText({ + displayName: 'Question', + required: true, + }), + frequencyPenalty: Property.Number({ + displayName: 'Frequency penalty', + required: false, + description: + "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.", + defaultValue: 0, + }), + maxTokens: Property.Number({ + displayName: 'Maximum Tokens', + required: true, + description: + 'The maximum number of tokens to generate.', + defaultValue: 4096, + }), + presencePenalty: Property.Number({ + displayName: 'Presence penalty', + required: false, + description: + "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the mode's likelihood to talk about new topics.", + defaultValue: 0, + }), + responseFormat: Property.StaticDropdown({ + displayName: 'Response Format', + description: + 'The format of the response. IMPORTANT: When using JSON Output, you must also instruct the model to produce JSON yourself', + required: true, + defaultValue: 'text', + options: { + options: [ + { + label: 'Text', + value: 'text', + }, + { + label: 'JSON', + value: 'json_object', + }, + ], + }, + }), + temperature: Property.Number({ + displayName: 'Temperature', + required: false, + description: + 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive. Between 0 and 2. We generally recommend altering this or top_p but not both.', + defaultValue: 1, + }), + topP: Property.Number({ + displayName: 'Top P', + required: false, + description: + 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. Values <=1. We generally recommend altering this or temperature but not both.', + defaultValue: 1, + }), + memoryKey: Property.ShortText({ + displayName: 'Memory Key', + description: + 'A memory key that will keep the chat history shared across runs and flows. Keep it empty to leave Avian without memory of previous messages.', + required: false, + }), + roles: Property.Json({ + displayName: 'Roles', + required: false, + description: 'Array of roles to specify more accurate response', + defaultValue: [ + { role: 'system', content: 'You are a helpful assistant.' }, + ], + }), + }, + async run({ auth, propsValue, store }) { + await propsValidation.validateZod(propsValue, { + temperature: z.number().min(0).max(2).optional(), + memoryKey: z.string().max(128).optional(), + }); + const openai = new OpenAI({ + baseURL: baseUrl, + apiKey: auth.secret_text, + }); + const { + model, + temperature, + maxTokens, + topP, + frequencyPenalty, + presencePenalty, + responseFormat, + prompt, + memoryKey, + } = propsValue; + + let messageHistory: any[] | null = []; + if (memoryKey) { + messageHistory = (await store.get(memoryKey, StoreScope.PROJECT)) ?? []; + } + + messageHistory.push({ + role: 'user', + content: prompt, + }); + + const rolesArray = propsValue.roles ? (propsValue.roles as any) : []; + const roles = rolesArray.map((item: any) => { + const rolesEnum = ['system', 'user', 'assistant']; + if (!rolesEnum.includes(item.role)) { + throw new Error( + 'The only available roles are: [system, user, assistant]' + ); + } + + return { + role: item.role, + content: item.content, + }; + }); + + const completion = await openai.chat.completions.create({ + model: model, + messages: [...roles, ...messageHistory], + temperature: temperature, + max_tokens: maxTokens, + top_p: topP, + frequency_penalty: frequencyPenalty, + presence_penalty: presencePenalty, + response_format: responseFormat === 'json_object' ? { type: 'json_object' } : { type: 'text' }, + }); + + messageHistory = [...messageHistory, completion.choices[0].message]; + + if (memoryKey) { + // Prevent unbounded memory growth that would exceed the context window. + // Keep the most recent messages, dropping the oldest ones first. + const MAX_HISTORY_MESSAGES = 50; + if (messageHistory.length > MAX_HISTORY_MESSAGES) { + messageHistory = messageHistory.slice( + messageHistory.length - MAX_HISTORY_MESSAGES + ); + } + await store.put(memoryKey, messageHistory, StoreScope.PROJECT); + } + + return completion.choices[0].message.content; + }, +}); diff --git a/packages/pieces/community/avian/src/lib/auth.ts b/packages/pieces/community/avian/src/lib/auth.ts new file mode 100644 index 00000000000..194a29b7e0d --- /dev/null +++ b/packages/pieces/community/avian/src/lib/auth.ts @@ -0,0 +1,34 @@ +import { PieceAuth } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common'; +import { baseUrl } from './common/common'; + +export const avianAuth = PieceAuth.SecretText({ + description: ` + Follow these instructions to get your Avian API Key: + +1. Visit https://avian.io and sign up for an account. +2. Navigate to the API Keys section of your dashboard. +3. Create a new API key and copy it.`, + displayName: 'API Key', + required: true, + validate: async ({ auth }) => { + try { + await httpClient.sendRequest({ + method: HttpMethod.GET, + url: `${baseUrl}/balance`, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: auth, + }, + }); + return { + valid: true, + }; + } catch (e) { + return { + valid: false, + error: `${e}`, + }; + } + }, +}); diff --git a/packages/pieces/community/avian/src/lib/common/common.ts b/packages/pieces/community/avian/src/lib/common/common.ts new file mode 100644 index 00000000000..b554e6e1534 --- /dev/null +++ b/packages/pieces/community/avian/src/lib/common/common.ts @@ -0,0 +1,4 @@ +export const baseUrl = 'https://api.avian.io/v1'; + +export const unauthorizedMessage = `Error Occurred: 401 \n +Ensure that your API key is valid. \n`; diff --git a/packages/pieces/community/avian/tsconfig.json b/packages/pieces/community/avian/tsconfig.json new file mode 100644 index 00000000000..29c9dd1bfc1 --- /dev/null +++ b/packages/pieces/community/avian/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/pieces/community/avian/tsconfig.lib.json b/packages/pieces/community/avian/tsconfig.lib.json new file mode 100644 index 00000000000..458a988f7db --- /dev/null +++ b/packages/pieces/community/avian/tsconfig.lib.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "rootDir": ".", + "baseUrl": ".", + "paths": {}, + "outDir": "./dist", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/packages/pieces/community/baserow/package.json b/packages/pieces/community/baserow/package.json index fa77fc0a115..a96cb654bc4 100644 --- a/packages/pieces/community/baserow/package.json +++ b/packages/pieces/community/baserow/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-baserow", - "version": "0.8.0", + "version": "0.9.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/baserow/src/i18n/ca.json b/packages/pieces/community/baserow/src/i18n/ca.json index bfbfeb419a9..330aee5960d 100644 --- a/packages/pieces/community/baserow/src/i18n/ca.json +++ b/packages/pieces/community/baserow/src/i18n/ca.json @@ -120,5 +120,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/de.json b/packages/pieces/community/baserow/src/i18n/de.json index 43d59960c20..316d345acc8 100644 --- a/packages/pieces/community/baserow/src/i18n/de.json +++ b/packages/pieces/community/baserow/src/i18n/de.json @@ -114,5 +114,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/es.json b/packages/pieces/community/baserow/src/i18n/es.json index 2154a7a2806..e9a98e3d499 100644 --- a/packages/pieces/community/baserow/src/i18n/es.json +++ b/packages/pieces/community/baserow/src/i18n/es.json @@ -114,5 +114,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/fr.json b/packages/pieces/community/baserow/src/i18n/fr.json index a62c09befe6..581ff60be16 100644 --- a/packages/pieces/community/baserow/src/i18n/fr.json +++ b/packages/pieces/community/baserow/src/i18n/fr.json @@ -114,5 +114,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choisissez comment vous authentifier auprès de Baserow :\n\n**Jeton de base de données** — recommandé. Permissions CRUD par table, compatible avec les comptes 2FA. Les déclencheurs nécessitent une configuration manuelle des webhooks.\n 1. Connectez-vous à votre compte Baserow.\n 2. Cliquez sur votre photo de profil (en haut à gauche) et allez dans **Paramètres → Jetons de base de données**.\n 3. Créez un nouveau jeton, puis cliquez sur **:** à côté de son nom pour le copier.\n 4. Collez-le dans **Jeton de base de données** ci-dessous. Laissez **Courriel** et **Mot de passe** vides.\n\n**E-mail et mot de passe (JWT)** — accès au workspace entier, active l'enregistrement automatique des webhooks pour les déclencheurs. Incompatible avec les comptes ayant la 2FA activée.\n 1. Remplissez **Courriel** et **Mot de passe** avec vos identifiants Baserow. Laissez **Jeton de base de données** vide.\n\nDans les deux modes, renseignez **API URL** avec l'URL de votre instance Baserow (par défaut : `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Le jeton de base de données est recommandé. Utilisez E-mail et mot de passe (JWT) uniquement si vous avez besoin de l'enregistrement automatique des webhooks pour les déclencheurs.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Requis si la méthode d'authentification est **Jeton de base de données**. Laissez vide pour JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Requis si la méthode d'authentification est **E-mail et mot de passe (JWT)**. Laissez vide pour Jeton de base de données." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Requis si la méthode d'authentification est **E-mail et mot de passe (JWT)**. Laissez vide pour Jeton de base de données.", + "Upsert Row": "Créer ou mettre à jour une ligne", + "Creates a new row or updates an existing one by matching a field value.": "Crée une nouvelle ligne ou met à jour une ligne existante en recherchant une valeur dans un champ.", + "Match Field": "Champ de correspondance", + "Select the field to search for an existing row.": "Sélectionnez le champ pour rechercher une ligne existante.", + "Match Value": "Valeur de correspondance", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "La valeur à rechercher (correspondance exacte). Si une ligne avec cette valeur est trouvée, elle sera mise à jour ; sinon une nouvelle ligne sera créée.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "Lorsque activé, les valeurs de sélection unique/multiple qui n'existent pas encore dans le champ seront ajoutées avant la création ou la mise à jour de la ligne. Les options existantes sont préservées.", + "Upload File": "Téléverser un fichier", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Téléverse un fichier dans Baserow depuis une URL. Renvoie l'objet fichier qui peut être utilisé dans les champs de type fichier.", + "File URL": "URL du fichier", + "The public URL of the file to upload to Baserow.": "L'URL publique du fichier à téléverser dans Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/hi.json b/packages/pieces/community/baserow/src/i18n/hi.json index bfbfeb419a9..330aee5960d 100644 --- a/packages/pieces/community/baserow/src/i18n/hi.json +++ b/packages/pieces/community/baserow/src/i18n/hi.json @@ -120,5 +120,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/id.json b/packages/pieces/community/baserow/src/i18n/id.json index bfbfeb419a9..330aee5960d 100644 --- a/packages/pieces/community/baserow/src/i18n/id.json +++ b/packages/pieces/community/baserow/src/i18n/id.json @@ -120,5 +120,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/ja.json b/packages/pieces/community/baserow/src/i18n/ja.json index 69c52fc323c..6df67cec5c5 100644 --- a/packages/pieces/community/baserow/src/i18n/ja.json +++ b/packages/pieces/community/baserow/src/i18n/ja.json @@ -114,5 +114,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/nl.json b/packages/pieces/community/baserow/src/i18n/nl.json index 7f884690968..064fc73092c 100644 --- a/packages/pieces/community/baserow/src/i18n/nl.json +++ b/packages/pieces/community/baserow/src/i18n/nl.json @@ -114,5 +114,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/pt.json b/packages/pieces/community/baserow/src/i18n/pt.json index 856ff382f3c..22b0886d382 100644 --- a/packages/pieces/community/baserow/src/i18n/pt.json +++ b/packages/pieces/community/baserow/src/i18n/pt.json @@ -114,5 +114,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/ru.json b/packages/pieces/community/baserow/src/i18n/ru.json index d868e1dc845..cb727dcf1cf 100644 --- a/packages/pieces/community/baserow/src/i18n/ru.json +++ b/packages/pieces/community/baserow/src/i18n/ru.json @@ -120,5 +120,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/translation.json b/packages/pieces/community/baserow/src/i18n/translation.json index 15971468de7..623d706a402 100644 --- a/packages/pieces/community/baserow/src/i18n/translation.json +++ b/packages/pieces/community/baserow/src/i18n/translation.json @@ -110,18 +110,25 @@ "Triggers when new rows are created in a Baserow table. Returns all rows from the event as a single batch.": "Triggers when new rows are created in a Baserow table. Returns all rows from the event as a single batch.", "Triggers when existing rows are updated in a Baserow table. Returns all rows from the event as a single batch.": "Triggers when existing rows are updated in a Baserow table. Returns all rows from the event as a single batch.", "Triggers when rows are deleted from a Baserow table. Returns all deleted row IDs from the event as a single batch.": "Triggers when rows are deleted from a Baserow table. Returns all deleted row IDs from the event as a single batch.", -<<<<<<< feat/baserow-jwt-restore "Authentication": "Authentication", "Authentication Method": "Authentication Method", "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." -} -======= + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", "Markdown": "Markdown", "\n## Setup Instructions\n\n1. In Baserow, click the **···** menu beside your table and select **Webhooks**.\n2. Click **Create webhook +**.\n3. Set the HTTP method to **POST**.\n4. Paste the following URL into the endpoint field:\n```text\n{{webhookUrl}}\n```\n5. Under events, select **Rows created**.\n6. Click **Save**.\n": "\n## Setup Instructions\n\n1. In Baserow, click the **···** menu beside your table and select **Webhooks**.\n2. Click **Create webhook +**.\n3. Set the HTTP method to **POST**.\n4. Paste the following URL into the endpoint field:\n```text\n{{webhookUrl}}\n```\n5. Under events, select **Rows created**.\n6. Click **Save**.\n", "\n## Setup Instructions\n\n1. In Baserow, click the **···** menu beside your table and select **Webhooks**.\n2. Click **Create webhook +**.\n3. Set the HTTP method to **POST**.\n4. Paste the following URL into the endpoint field:\n```text\n{{webhookUrl}}\n```\n5. Under events, select **Rows updated**.\n6. Click **Save**.\n": "\n## Setup Instructions\n\n1. In Baserow, click the **···** menu beside your table and select **Webhooks**.\n2. Click **Create webhook +**.\n3. Set the HTTP method to **POST**.\n4. Paste the following URL into the endpoint field:\n```text\n{{webhookUrl}}\n```\n5. Under events, select **Rows updated**.\n6. Click **Save**.\n", - "\n## Setup Instructions\n\n1. In Baserow, click the **···** menu beside your table and select **Webhooks**.\n2. Click **Create webhook +**.\n3. Set the HTTP method to **POST**.\n4. Paste the following URL into the endpoint field:\n```text\n{{webhookUrl}}\n```\n5. Under events, select **Rows deleted**.\n6. Click **Save**.\n": "\n## Setup Instructions\n\n1. In Baserow, click the **···** menu beside your table and select **Webhooks**.\n2. Click **Create webhook +**.\n3. Set the HTTP method to **POST**.\n4. Paste the following URL into the endpoint field:\n```text\n{{webhookUrl}}\n```\n5. Under events, select **Rows deleted**.\n6. Click **Save**.\n" + "\n## Setup Instructions\n\n1. In Baserow, click the **···** menu beside your table and select **Webhooks**.\n2. Click **Create webhook +**.\n3. Set the HTTP method to **POST**.\n4. Paste the following URL into the endpoint field:\n```text\n{{webhookUrl}}\n```\n5. Under events, select **Rows deleted**.\n6. Click **Save**.\n": "\n## Setup Instructions\n\n1. In Baserow, click the **···** menu beside your table and select **Webhooks**.\n2. Click **Create webhook +**.\n3. Set the HTTP method to **POST**.\n4. Paste the following URL into the endpoint field:\n```text\n{{webhookUrl}}\n```\n5. Under events, select **Rows deleted**.\n6. Click **Save**.\n", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } ->>>>>>> main diff --git a/packages/pieces/community/baserow/src/i18n/vi.json b/packages/pieces/community/baserow/src/i18n/vi.json index bfbfeb419a9..330aee5960d 100644 --- a/packages/pieces/community/baserow/src/i18n/vi.json +++ b/packages/pieces/community/baserow/src/i18n/vi.json @@ -120,5 +120,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/i18n/zh.json b/packages/pieces/community/baserow/src/i18n/zh.json index 0c6d4e39baf..32e87fecbb9 100644 --- a/packages/pieces/community/baserow/src/i18n/zh.json +++ b/packages/pieces/community/baserow/src/i18n/zh.json @@ -114,5 +114,16 @@ "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).": "Choose how you want to authenticate with Baserow:\n\n**Database Token** — recommended. Per-table CRUD scoping, compatible with 2FA accounts. Triggers require manual webhook setup.\n 1. Log in to your Baserow account.\n 2. Click on your profile picture (top-left) and go to **Settings → Database tokens**.\n 3. Create a new token, then click **:** beside the token name to copy it.\n 4. Paste it into **Database Token** below. Leave **Email** and **Password** empty.\n\n**Email & Password (JWT)** — workspace-wide access, enables automatic webhook registration for triggers. Not compatible with accounts that have 2FA enabled.\n 1. Fill in **Email** and **Password** with your Baserow login credentials. Leave **Database Token** empty.\n\nIn both modes, set **API URL** to your Baserow instance (default: `https://api.baserow.io`).", "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.": "Database Token is recommended. Use Email & Password (JWT) only if you need automatic webhook registration on triggers.", "Required if Authentication Method is **Database Token**. Leave empty for JWT.": "Required if Authentication Method is **Database Token**. Leave empty for JWT.", - "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token." + "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.": "Required if Authentication Method is **Email & Password (JWT)**. Leave empty for Database Token.", + "Upsert Row": "Upsert Row", + "Creates a new row or updates an existing one by matching a field value.": "Creates a new row or updates an existing one by matching a field value.", + "Match Field": "Match Field", + "Select the field to search for an existing row.": "Select the field to search for an existing row.", + "Match Value": "Match Value", + "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.": "The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.", + "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.": "When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.", + "Upload File": "Upload File", + "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.": "Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields.", + "File URL": "File URL", + "The public URL of the file to upload to Baserow.": "The public URL of the file to upload to Baserow." } diff --git a/packages/pieces/community/baserow/src/index.ts b/packages/pieces/community/baserow/src/index.ts index 0e195c29af5..2f51233c3d2 100644 --- a/packages/pieces/community/baserow/src/index.ts +++ b/packages/pieces/community/baserow/src/index.ts @@ -14,6 +14,8 @@ import { aggregateFieldAction } from './lib/actions/aggregate-field'; import { batchCreateRowsAction } from './lib/actions/batch-create-rows'; import { batchUpdateRowsAction } from './lib/actions/batch-update-rows'; import { batchDeleteRowsAction } from './lib/actions/batch-delete-rows'; +import { upsertRowAction } from './lib/actions/upsert-row'; +import { uploadFileAction } from './lib/actions/upload-file'; import { rowCreatedTrigger } from './lib/triggers/row-created'; import { rowUpdatedTrigger } from './lib/triggers/row-updated'; import { rowDeletedTrigger } from './lib/triggers/row-deleted'; @@ -59,6 +61,8 @@ export const baserow = createPiece({ batchCreateRowsAction, batchUpdateRowsAction, batchDeleteRowsAction, + upsertRowAction, + uploadFileAction, createCustomApiCallAction({ baseUrl: (auth) => { if (!auth) { diff --git a/packages/pieces/community/baserow/src/lib/actions/aggregate-field.ts b/packages/pieces/community/baserow/src/lib/actions/aggregate-field.ts index 4fdadba666b..37c6da2d04c 100644 --- a/packages/pieces/community/baserow/src/lib/actions/aggregate-field.ts +++ b/packages/pieces/community/baserow/src/lib/actions/aggregate-field.ts @@ -83,11 +83,7 @@ export const aggregateFieldAction = createAction({ async run(context) { const { view_id, field_id, aggregation_type } = context.propsValue; const client = await makeClient(context.auth); - const raw = (await client.aggregateField( - view_id!, - field_id!, - aggregation_type! - )) as { value: unknown }; + const raw = await client.aggregateField(view_id!, field_id!, aggregation_type!); return { result: raw['value'] }; }, }); diff --git a/packages/pieces/community/baserow/src/lib/actions/batch-create-rows.ts b/packages/pieces/community/baserow/src/lib/actions/batch-create-rows.ts index 3c21d6422c6..0ef788cc82a 100644 --- a/packages/pieces/community/baserow/src/lib/actions/batch-create-rows.ts +++ b/packages/pieces/community/baserow/src/lib/actions/batch-create-rows.ts @@ -25,10 +25,7 @@ export const batchCreateRowsAction = createAction({ throw new Error('Rows must be a JSON array.'); } const client = await makeClient(context.auth); - const response = (await client.batchCreateRows( - table_id, - rows as Record[] - )) as { items: Record[] }; + const response = await client.batchCreateRows(table_id, rows); return { count: response.items.length, rows: response.items }; }, }); diff --git a/packages/pieces/community/baserow/src/lib/actions/batch-update-rows.ts b/packages/pieces/community/baserow/src/lib/actions/batch-update-rows.ts index d5acdf82c4e..511400d81c6 100644 --- a/packages/pieces/community/baserow/src/lib/actions/batch-update-rows.ts +++ b/packages/pieces/community/baserow/src/lib/actions/batch-update-rows.ts @@ -25,10 +25,7 @@ export const batchUpdateRowsAction = createAction({ throw new Error('Rows must be a JSON array.'); } const client = await makeClient(context.auth); - const response = (await client.batchUpdateRows( - table_id, - rows as Record[] - )) as { items: Record[] }; + const response = await client.batchUpdateRows(table_id, rows); return { count: response.items.length, rows: response.items }; }, }); diff --git a/packages/pieces/community/baserow/src/lib/actions/find-row.ts b/packages/pieces/community/baserow/src/lib/actions/find-row.ts index e1ea8010431..6e93dd325b6 100644 --- a/packages/pieces/community/baserow/src/lib/actions/find-row.ts +++ b/packages/pieces/community/baserow/src/lib/actions/find-row.ts @@ -77,7 +77,7 @@ export const findRowAction = createAction({ undefined, undefined, { [`filter__${field_name}__equal`]: field_value } - )) as { results: Record[]; count: number }; + )) if (response.results.length === 0) { return { found: false, count: 0 }; diff --git a/packages/pieces/community/baserow/src/lib/actions/list-rows.ts b/packages/pieces/community/baserow/src/lib/actions/list-rows.ts index 08fae00154b..96cea8a667f 100644 --- a/packages/pieces/community/baserow/src/lib/actions/list-rows.ts +++ b/packages/pieces/community/baserow/src/lib/actions/list-rows.ts @@ -77,7 +77,7 @@ Example: \`{"field": 123, "type": "equal", "value": "Active"}\``, if (filters && filters.length > 0) { const parsedFilters = filters.map((f) => { - const filter = + const filter: Record = typeof f === 'string' ? JSON.parse(f) : (f as Record); return { field: Number(filter['field']), @@ -99,7 +99,7 @@ Example: \`{"field": 123, "type": "equal", "value": "Active"}\``, order_by, undefined, advancedFilters - )) as { count: number; results: Record[] }; + )) return { count: response.count, rows: response.results }; }, diff --git a/packages/pieces/community/baserow/src/lib/actions/upload-file.ts b/packages/pieces/community/baserow/src/lib/actions/upload-file.ts new file mode 100644 index 00000000000..c167921e036 --- /dev/null +++ b/packages/pieces/community/baserow/src/lib/actions/upload-file.ts @@ -0,0 +1,28 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { baserowAuth, baserowAuthHelpers } from '../auth'; +import { makeClient } from '../common'; + +export const uploadFileAction = createAction({ + name: 'baserow_upload_file', + displayName: 'Upload File', + description: + 'Uploads a file to Baserow from a URL. Returns the uploaded file object that can be used in file fields. Requires Email & Password (JWT) authentication — Database Tokens do not have access to the user-files endpoint.', + auth: baserowAuth, + props: { + url: Property.ShortText({ + displayName: 'File URL', + description: 'The public URL of the file to upload to Baserow.', + required: true, + }), + }, + async run(context) { + if (!baserowAuthHelpers.isJwtAuth(context.auth)) { + throw new Error( + 'Upload File requires Email & Password (JWT) authentication. Database Tokens are limited to row CRUD operations and cannot access the user-files endpoint. Please create a new Baserow connection using Email & Password.' + ); + } + const { url } = context.propsValue; + const client = await makeClient(context.auth); + return await client.uploadFileFromUrl({ url }); + }, +}); diff --git a/packages/pieces/community/baserow/src/lib/actions/upsert-row.ts b/packages/pieces/community/baserow/src/lib/actions/upsert-row.ts new file mode 100644 index 00000000000..b8cec5b0182 --- /dev/null +++ b/packages/pieces/community/baserow/src/lib/actions/upsert-row.ts @@ -0,0 +1,174 @@ +import { + Property, + createAction, + DropdownState, +} from '@activepieces/pieces-framework'; +import { baserowAuth } from '../auth'; +import { + baserowCommon, + ensureSelectOptionsExist, + formatFieldValues, + makeClient, +} from '../common'; +import { BaserowFieldType } from '../common/constants'; + +export const upsertRowAction = createAction({ + name: 'baserow_upsert_row', + displayName: 'Upsert Row', + description: + 'Creates a new row or updates an existing one by matching a field value.', + auth: baserowAuth, + props: { + table_id: baserowCommon.tableId(), + match_field: Property.Dropdown({ + displayName: 'Match Field', + description: 'Select the field to search for an existing row.', + required: true, + auth: baserowAuth, + refreshers: ['auth', 'table_id'], + options: async ({ auth, table_id }): Promise> => { + if (!auth || typeof table_id !== 'number') { + return { + disabled: true, + placeholder: 'Select a table first.', + options: [], + }; + } + const client = await makeClient(auth); + const fields = await client.listTableFields(table_id); + const unsupportedTypes: string[] = [ + BaserowFieldType.LINK_TO_TABLE, + BaserowFieldType.MULTI_SELECT, + BaserowFieldType.MULTIPLE_COLLABORATORS, + BaserowFieldType.FILE, + BaserowFieldType.ROLLUP, + BaserowFieldType.LOOKUP, + BaserowFieldType.COUNT, + BaserowFieldType.LAST_MODIFIED_BY, + BaserowFieldType.CREATED_BY, + BaserowFieldType.DATE, + BaserowFieldType.LAST_MODIFIED, + BaserowFieldType.CREATED_ON, + BaserowFieldType.DURATION, + BaserowFieldType.UUID, + BaserowFieldType.AUTO_NUMBER, + ]; + return { + disabled: false, + options: fields + .filter((f) => !f.read_only && !unsupportedTypes.includes(f.type)) + .map((f) => ({ label: f.name, value: `field_${f.id}` })), + }; + }, + }), + match_value: Property.ShortText({ + displayName: 'Match Value', + description: + 'The value to search for (exact match). If a row with this value is found it will be updated; otherwise a new row is created.', + required: true, + }), + table_fields: baserowCommon.tableFields(true), + create_missing_select_options: Property.Checkbox({ + displayName: 'Create missing select options', + description: + 'When enabled, single/multi-select values that do not yet exist in the field will be added before creating or updating the row. Existing options are preserved.', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { table_id, match_field, match_value, table_fields, create_missing_select_options } = + context.propsValue; + + const client = await makeClient(context.auth); + const tableSchema = await client.listTableFields(table_id!); + + const fieldTypeMap: Record = {}; + for (const column of tableSchema) { + fieldTypeMap[column.name] = column.type; + } + + const formattedFields = formatFieldValues(table_fields!, fieldTypeMap, { + skipEmpty: true, + }); + + const matchFieldId = parseInt(match_field!.replace('field_', ''), 10); + const matchFieldDef = tableSchema.find((f) => f.id === matchFieldId); + + const existing = await client.listRows( + table_id!, + undefined, + 1, + undefined, + undefined, + { [`filter__${match_field}__equal`]: match_value! } + ); + + if (existing.results.length > 0) { + // Strip the match field from the update payload so the upsert stays + // idempotent. Use the dedicated Update Row action to rename a row's + // match value. + if (matchFieldDef) { + delete formattedFields[matchFieldDef.name]; + } + if (create_missing_select_options) { + await ensureSelectOptionsExist({ + fields: tableSchema, + payload: formattedFields, + client, + }); + } + const rowId = existing.results[0].id; + const updated = await client.updateRow(table_id!, rowId, formattedFields); + return { action: 'updated', row: updated }; + } + + // Inject the match field into the new row so the upsert is idempotent. + // Done before ensureSelectOptionsExist so a SINGLE_SELECT match value + // gets auto-created when the toggle is enabled. + if (matchFieldDef) { + formattedFields[matchFieldDef.name] = coerceMatchValue({ + value: match_value!, + fieldType: matchFieldDef.type, + }); + } + + if (create_missing_select_options) { + await ensureSelectOptionsExist({ + fields: tableSchema, + payload: formattedFields, + client, + }); + } + + const created = await client.createRow(table_id!, formattedFields); + return { action: 'created', row: created }; + }, +}); + +function coerceMatchValue({ + value, + fieldType, +}: { + value: string; + fieldType: string; +}): unknown { + switch (fieldType) { + case BaserowFieldType.BOOLEAN: { + const normalized = value.trim().toLowerCase(); + return ['true', '1', 'yes'].includes(normalized); + } + case BaserowFieldType.NUMBER: + case BaserowFieldType.RATING: { + const parsed = Number(value); + if (Number.isNaN(parsed)) { + throw new Error( + `Match Value "${value}" is not a valid number for the selected ${fieldType} field.` + ); + } + return parsed; + } + default: + return value; + } +} diff --git a/packages/pieces/community/baserow/src/lib/common/client.ts b/packages/pieces/community/baserow/src/lib/common/client.ts index 18ff199eadd..a25422fd0fa 100644 --- a/packages/pieces/community/baserow/src/lib/common/client.ts +++ b/packages/pieces/community/baserow/src/lib/common/client.ts @@ -25,7 +25,7 @@ export function prepareQuery(request?: Record): QueryParams { Object.keys(request) .filter(emptyValueFilter((k) => request[k])) .forEach((k: string) => { - params[k] = (request as Record)[k]!.toString(); + params[k] = request[k]!.toString(); }); return params; } @@ -135,7 +135,7 @@ export class BaserowClient { order_by?: string, filters?: Record, advancedFilters?: { filter_type: string; filters: { field: number; type: string; value: string }[] } - ) { + ): Promise<{ results: Array<{ id: number } & Record>; count: number }> { const query = prepareQuery({ user_field_names: 'true', page: page, @@ -147,22 +147,22 @@ export class BaserowClient { if (advancedFilters && advancedFilters.filters.length > 0) { query['filters'] = JSON.stringify(advancedFilters); } - return await this.makeRequest( + return await this.makeRequest<{ results: Array<{ id: number } & Record>; count: number }>( HttpMethod.GET, `/database/rows/table/${table_id}/`, query ); } - async batchCreateRows(table_id: number, items: Record[]) { - return await this.makeRequest( + async batchCreateRows(table_id: number, items: unknown[]): Promise<{ items: Record[] }> { + return await this.makeRequest<{ items: Record[] }>( HttpMethod.POST, `/database/rows/table/${table_id}/batch/`, { user_field_names: 'true' }, { items } ); } - async batchUpdateRows(table_id: number, items: Record[]) { - return await this.makeRequest( + async batchUpdateRows(table_id: number, items: unknown[]): Promise<{ items: Record[] }> { + return await this.makeRequest<{ items: Record[] }>( HttpMethod.PATCH, `/database/rows/table/${table_id}/batch/`, { user_field_names: 'true' }, @@ -188,8 +188,8 @@ export class BaserowClient { view_id: number, field_id: number, aggregation_type: string - ) { - return await this.makeRequest( + ): Promise<{ value: unknown }> { + return await this.makeRequest<{ value: unknown }>( HttpMethod.GET, `/database/views/grid/${view_id}/aggregation/${field_id}/`, { type: aggregation_type } @@ -247,4 +247,12 @@ export class BaserowClient { headers: { Authorization: this.authHeader }, }); } + async uploadFileFromUrl({ url }: { url: string }): Promise> { + return await this.makeRequest( + HttpMethod.POST, + `/user-files/upload-via-url/`, + undefined, + { url } + ); + } } diff --git a/packages/pieces/community/baserow/src/lib/common/index.ts b/packages/pieces/community/baserow/src/lib/common/index.ts index 276493ccc60..9af7da7141a 100644 --- a/packages/pieces/community/baserow/src/lib/common/index.ts +++ b/packages/pieces/community/baserow/src/lib/common/index.ts @@ -190,11 +190,7 @@ export const baserowCommon = { }; } const client = await makeClient(auth); - const response = (await client.listRows( - table_id, - undefined, - 200 - )) as { results: Record[] }; + const response = await client.listRows(table_id, undefined, 200); return { disabled: false, options: response.results.map((row) => { @@ -202,10 +198,8 @@ export const baserowCommon = { .filter(([k]) => k !== 'id' && k !== 'order') .map(([, v]) => (typeof v === 'string' && v ? v : null)) .find(Boolean); - const label = primaryValue - ? `#${row['id']} ${primaryValue}` - : `Row #${row['id']}`; - return { label, value: row['id'] as number }; + const label = primaryValue ? `#${row.id} ${primaryValue}` : `Row #${row.id}`; + return { label, value: row.id }; }), }; }, diff --git a/packages/pieces/community/baserow/src/lib/triggers/row-created.ts b/packages/pieces/community/baserow/src/lib/triggers/row-created.ts index d8ba530ea9f..63e354b9670 100644 --- a/packages/pieces/community/baserow/src/lib/triggers/row-created.ts +++ b/packages/pieces/community/baserow/src/lib/triggers/row-created.ts @@ -33,9 +33,7 @@ export const rowCreatedTrigger = createTrigger({ const tableId = context.propsValue.table_id; if (!tableId) return []; const client = await makeClient(context.auth); - const response = (await client.listRows(tableId, 1, 5)) as { - results: Record[]; - }; + const response = await client.listRows(tableId, 1, 5); return response.results; }, }); diff --git a/packages/pieces/community/baserow/src/lib/triggers/row-deleted.ts b/packages/pieces/community/baserow/src/lib/triggers/row-deleted.ts index 17a1a62a233..4da091af2a3 100644 --- a/packages/pieces/community/baserow/src/lib/triggers/row-deleted.ts +++ b/packages/pieces/community/baserow/src/lib/triggers/row-deleted.ts @@ -31,9 +31,7 @@ export const rowDeletedTrigger = createTrigger({ const tableId = context.propsValue.table_id; if (!tableId) return []; const client = await makeClient(context.auth); - const response = (await client.listRows(tableId, 1, 5)) as { - results: { id: number }[]; - }; + const response = await client.listRows(tableId, 1, 5); return response.results.map((row) => ({ id: row.id })); }, }); diff --git a/packages/pieces/community/baserow/src/lib/triggers/row-event.ts b/packages/pieces/community/baserow/src/lib/triggers/row-event.ts index da6a2510b0e..b0fe30e05c2 100644 --- a/packages/pieces/community/baserow/src/lib/triggers/row-event.ts +++ b/packages/pieces/community/baserow/src/lib/triggers/row-event.ts @@ -61,9 +61,7 @@ export const rowEventTrigger = createTrigger({ const tableId = context.propsValue.table_id; if (!tableId) return []; const client = await makeClient(context.auth); - const response = (await client.listRows(tableId, 1, 5)) as { - results: Record[]; - }; + const response = await client.listRows(tableId, 1, 5); return response.results.map((row) => ({ event_type: 'rows.created', row, diff --git a/packages/pieces/community/baserow/src/lib/triggers/row-updated.ts b/packages/pieces/community/baserow/src/lib/triggers/row-updated.ts index 5a821dd0679..0d7cff02652 100644 --- a/packages/pieces/community/baserow/src/lib/triggers/row-updated.ts +++ b/packages/pieces/community/baserow/src/lib/triggers/row-updated.ts @@ -52,9 +52,7 @@ export const rowUpdatedTrigger = createTrigger({ const tableId = context.propsValue.table_id; if (!tableId) return []; const client = await makeClient(context.auth); - const response = (await client.listRows(tableId, 1, 5)) as { - results: Record[]; - }; + const response = await client.listRows(tableId, 1, 5); return response.results.map((row) => ({ row, previous: null })); }, }); diff --git a/packages/pieces/community/baserow/src/lib/triggers/rows-created.ts b/packages/pieces/community/baserow/src/lib/triggers/rows-created.ts index ca10fae86ff..09fc6639e27 100644 --- a/packages/pieces/community/baserow/src/lib/triggers/rows-created.ts +++ b/packages/pieces/community/baserow/src/lib/triggers/rows-created.ts @@ -37,9 +37,7 @@ export const rowsCreatedTrigger = createTrigger({ const tableId = context.propsValue.table_id; if (!tableId) return []; const client = await makeClient(context.auth); - const response = (await client.listRows(tableId, 1, 5)) as { - results: Record[]; - }; + const response = await client.listRows(tableId, 1, 5); return [{ rows: response.results, count: response.results.length }]; }, }); diff --git a/packages/pieces/community/baserow/src/lib/triggers/rows-deleted.ts b/packages/pieces/community/baserow/src/lib/triggers/rows-deleted.ts index 96a8ff179f3..ec970749aa9 100644 --- a/packages/pieces/community/baserow/src/lib/triggers/rows-deleted.ts +++ b/packages/pieces/community/baserow/src/lib/triggers/rows-deleted.ts @@ -34,9 +34,7 @@ export const rowsDeletedTrigger = createTrigger({ const tableId = context.propsValue.table_id; if (!tableId) return []; const client = await makeClient(context.auth); - const response = (await client.listRows(tableId, 1, 5)) as { - results: { id: number }[]; - }; + const response = await client.listRows(tableId, 1, 5); const rows = response.results.map((row) => ({ id: row.id })); return [{ rows, count: rows.length }]; }, diff --git a/packages/pieces/community/baserow/src/lib/triggers/rows-updated.ts b/packages/pieces/community/baserow/src/lib/triggers/rows-updated.ts index 6e86f6cfeb7..d7258b518b3 100644 --- a/packages/pieces/community/baserow/src/lib/triggers/rows-updated.ts +++ b/packages/pieces/community/baserow/src/lib/triggers/rows-updated.ts @@ -45,9 +45,7 @@ export const rowsUpdatedTrigger = createTrigger({ const tableId = context.propsValue.table_id; if (!tableId) return []; const client = await makeClient(context.auth); - const response = (await client.listRows(tableId, 1, 5)) as { - results: Record[]; - }; + const response = await client.listRows(tableId, 1, 5); const rows = response.results.map((row) => ({ row, previous: null })); return [{ rows, count: rows.length }]; }, diff --git a/packages/pieces/community/microsoft-teams/package.json b/packages/pieces/community/microsoft-teams/package.json index 223212f1bdd..36ee44a9fb7 100644 --- a/packages/pieces/community/microsoft-teams/package.json +++ b/packages/pieces/community/microsoft-teams/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-microsoft-teams", - "version": "0.5.0", + "version": "0.5.1", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/microsoft-teams/src/lib/actions/send-channel-message.ts b/packages/pieces/community/microsoft-teams/src/lib/actions/send-channel-message.ts index cf2e5b6d96a..2ed8394947a 100644 --- a/packages/pieces/community/microsoft-teams/src/lib/actions/send-channel-message.ts +++ b/packages/pieces/community/microsoft-teams/src/lib/actions/send-channel-message.ts @@ -3,7 +3,7 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import { PageCollection } from '@microsoft/microsoft-graph-client'; import { ConversationMember } from '@microsoft/microsoft-graph-types'; import { microsoftTeamsCommon } from '../common'; -import { createGraphClient } from '../common/graph'; +import { createGraphClient, withGraphRetry } from '../common/graph'; export const sendChannelMessageAction = createAction({ auth: microsoftTeamsAuth, @@ -73,15 +73,18 @@ export const sendChannelMessageAction = createAction({ ]); const members: ConversationMember[] = []; - let response: PageCollection = await client - .api(`/teams/${teamId}/members`) - .filter(filterClauses.join(' or ')) - .get(); + let response: PageCollection = await withGraphRetry(() => + client + .api(`/teams/${teamId}/members`) + .filter(filterClauses.join(' or ')) + .get(), + ); - while (response.value.length > 0) { + while (response.value && response.value.length > 0) { members.push(...(response.value as ConversationMember[])); - if (response['@odata.nextLink']) { - response = await client.api(response['@odata.nextLink']).get(); + const nextLink = response['@odata.nextLink']; + if (nextLink) { + response = await withGraphRetry(() => client.api(nextLink).get()); } else { break; } diff --git a/packages/pieces/community/microsoft-teams/src/lib/common/graph.ts b/packages/pieces/community/microsoft-teams/src/lib/common/graph.ts index 365b59c47e8..491fa369913 100644 --- a/packages/pieces/community/microsoft-teams/src/lib/common/graph.ts +++ b/packages/pieces/community/microsoft-teams/src/lib/common/graph.ts @@ -1,4 +1,5 @@ -import { Client } from '@microsoft/microsoft-graph-client'; +import { Client, PageCollection } from '@microsoft/microsoft-graph-client'; +import { tryCatch } from '@activepieces/shared'; import { getGraphBaseUrl } from './microsoft-cloud'; type GraphRetryOptions = { @@ -47,8 +48,6 @@ const extractStatusCode = (error: any): number => { const shouldRetry = (statusCode: number, code?: string): boolean => { // Retry on throttling and transient errors if (statusCode === 429 || statusCode === 503 || statusCode === 504) return true; - // Some concurrency conflicts can be retried - if (statusCode === 409) return true; // Optionally retry generic server errors if (statusCode >= 500 && statusCode < 600) return true; // Some SDKs surface codes like 'TooManyRequests' @@ -119,4 +118,40 @@ export const withGraphRetry = async ( throw new Error("Unexpected error occured"); }; +// Walks a paginated Graph list endpoint with retry, an item cap, and graceful failure. +// Returns whatever was collected before an error or the cap; the cap protects the 60s +// dropdown sandbox timeout from runaway pagination on accounts with very large lists. +export const paginateGraphList = async (params: { + client: Client; + initialUrl: string; + // Omit on endpoints that don't support OData $top (e.g. /me/joinedTeams). + pageSize?: number; + maxItems: number; + expand?: string; +}): Promise<{ items: T[]; truncated: boolean; error: Error | null }> => { + const { client, initialUrl, pageSize, maxItems, expand } = params; + const items: T[] = []; + const { error } = await tryCatch(async () => { + let firstRequest = client.api(initialUrl); + if (pageSize !== undefined) firstRequest = firstRequest.top(pageSize); + if (expand) firstRequest = firstRequest.expand(expand); + let response: PageCollection = await withGraphRetry(() => firstRequest.get()); + while (response.value && response.value.length > 0) { + for (const item of response.value as T[]) { + items.push(item); + if (items.length >= maxItems) break; + } + if (items.length >= maxItems) break; + const nextLink = response['@odata.nextLink']; + if (!nextLink) break; + response = await withGraphRetry(() => client.api(nextLink).get()); + } + }); + return { + items, + truncated: items.length >= maxItems, + error, + }; +}; + diff --git a/packages/pieces/community/microsoft-teams/src/lib/common/index.ts b/packages/pieces/community/microsoft-teams/src/lib/common/index.ts index b41a3cbeb08..93e08c0fb0c 100644 --- a/packages/pieces/community/microsoft-teams/src/lib/common/index.ts +++ b/packages/pieces/community/microsoft-teams/src/lib/common/index.ts @@ -3,7 +3,7 @@ import { DropdownOption, OAuth2PropertyValue, PiecePropValueSchema, Property } f import { PageCollection } from '@microsoft/microsoft-graph-client'; import { Team, Channel, Chat, ConversationMember, CallTranscript, CallRecording } from '@microsoft/microsoft-graph-types'; import { microsoftTeamsAuth } from '../auth'; -import { createGraphClient, resolveMeetingId } from './graph'; +import { createGraphClient, paginateGraphList, resolveMeetingId } from './graph'; export const microsoftTeamsCommon = { teamId: Property.Dropdown({ @@ -22,24 +22,26 @@ export const microsoftTeamsCommon = { const authValue = auth as PiecePropValueSchema; const cloud = (auth as OAuth2PropertyValue).props?.['cloud'] as string | undefined; const client = createGraphClient(authValue.access_token, cloud); - const options: DropdownOption[] = []; - // Pagination : https://learn.microsoft.com/en-us/graph/sdks/paging?view=graph-rest-1.0&tabs=typescript#manually-requesting-subsequent-pages - // List Joined Channels : https://learn.microsoft.com/en-us/graph/api/user-list-joinedteams?view=graph-rest-1.0&tabs=http - let response: PageCollection = await client.api('/me/joinedTeams').get(); - while (response.value.length > 0) { - for (const team of response.value as Team[]) { - options.push({ label: team.displayName!, value: team.id! }); - } - if (response['@odata.nextLink']) { - response = await client.api(response['@odata.nextLink']).get(); - } else { - break; - } + // List Joined Teams : https://learn.microsoft.com/en-us/graph/api/user-list-joinedteams?view=graph-rest-1.0&tabs=http + // Note: this endpoint does not support OData $top — pageSize intentionally omitted. + const { items, error } = await paginateGraphList({ + client, + initialUrl: '/me/joinedTeams', + maxItems: DROPDOWN_LIST_MAX, + }); + + if (error && items.length === 0) { + return { + disabled: true, + placeholder: "Couldn't load teams — please retry.", + options: [], + }; } + return { disabled: false, - options: options, + options: items.map((team) => ({ label: team.displayName!, value: team.id! })), }; }, }), @@ -59,24 +61,26 @@ export const microsoftTeamsCommon = { const authValue = auth as PiecePropValueSchema; const cloud = (auth as OAuth2PropertyValue).props?.['cloud'] as string | undefined; const client = createGraphClient(authValue.access_token, cloud); - const options: DropdownOption[] = []; - // Pagination : https://learn.microsoft.com/en-us/graph/sdks/paging?view=graph-rest-1.0&tabs=typescript#manually-requesting-subsequent-pages // List Channels : https://learn.microsoft.com/en-us/graph/api/channel-list?view=graph-rest-1.0&tabs=http - let response: PageCollection = await client.api(`/teams/${teamId}/channels`).get(); - while (response.value.length > 0) { - for (const channel of response.value as Channel[]) { - options.push({ label: channel.displayName!, value: channel.id! }); - } - if (response['@odata.nextLink']) { - response = await client.api(response['@odata.nextLink']).get(); - } else { - break; - } + // Note: this endpoint does not support OData $top — pageSize intentionally omitted. + const { items, error } = await paginateGraphList({ + client, + initialUrl: `/teams/${teamId}/channels`, + maxItems: DROPDOWN_LIST_MAX, + }); + + if (error && items.length === 0) { + return { + disabled: true, + placeholder: "Couldn't load channels — please retry.", + options: [], + }; } + return { disabled: false, - options: options, + options: items.map((channel) => ({ label: channel.displayName!, value: channel.id! })), }; }, }), @@ -96,22 +100,26 @@ export const microsoftTeamsCommon = { const authValue = auth as PiecePropValueSchema; const cloud = (auth as OAuth2PropertyValue).props?.['cloud'] as string | undefined; const client = createGraphClient(authValue.access_token, cloud); - const options: DropdownOption[] = []; - let response: PageCollection = await client.api(`/teams/${teamId}/members`).get(); - while (response.value.length > 0) { - for (const member of response.value as ConversationMember[]) { - options.push({ label: member.displayName!, value: member.id! }); - } - if (response['@odata.nextLink']) { - response = await client.api(response['@odata.nextLink']).get(); - } else { - break; - } + // Default page size on /teams/{id}/members is 100; explicit top() is + // intentionally omitted to stay compatible with this endpoint's OData support. + const { items, error } = await paginateGraphList({ + client, + initialUrl: `/teams/${teamId}/members`, + maxItems: DROPDOWN_LIST_MAX, + }); + + if (error && items.length === 0) { + return { + disabled: true, + placeholder: "Couldn't load members — please retry.", + options: [], + }; } + return { disabled: false, - options: options, + options: items.map((member) => ({ label: member.displayName!, value: member.id! })), }; }, }), @@ -131,22 +139,26 @@ export const microsoftTeamsCommon = { const authValue = auth as PiecePropValueSchema; const cloud = (auth as OAuth2PropertyValue).props?.['cloud'] as string | undefined; const client = createGraphClient(authValue.access_token, cloud); - const options: DropdownOption[] = []; - let response: PageCollection = await client.api(`/teams/${teamId}/members`).get(); - while (response.value.length > 0) { - for (const member of response.value as ConversationMember[]) { - options.push({ label: member.displayName!, value: member.id! }); - } - if (response['@odata.nextLink']) { - response = await client.api(response['@odata.nextLink']).get(); - } else { - break; - } + // Default page size on /teams/{id}/members is 100; explicit top() is + // intentionally omitted to stay compatible with this endpoint's OData support. + const { items, error } = await paginateGraphList({ + client, + initialUrl: `/teams/${teamId}/members`, + maxItems: DROPDOWN_LIST_MAX, + }); + + if (error && items.length === 0) { + return { + disabled: true, + placeholder: "Couldn't load members — please retry.", + options: [], + }; } + return { disabled: false, - options: options, + options: items.map((member) => ({ label: member.displayName!, value: member.id! })), }; }, }), @@ -269,33 +281,39 @@ export const microsoftTeamsCommon = { const authValue = auth as PiecePropValueSchema; const cloud = (auth as OAuth2PropertyValue).props?.['cloud'] as string | undefined; const client = createGraphClient(authValue.access_token, cloud); - const options: DropdownOption[] = []; - // Pagination : https://learn.microsoft.com/en-us/graph/sdks/paging?view=graph-rest-1.0&tabs=typescript#manually-requesting-subsequent-pages // List Chats : https://learn.microsoft.com/en-us/graph/api/chat-list?view=graph-rest-1.0&tabs=http - let response: PageCollection = await client.api('/chats').expand('members').get(); - while (response.value.length > 0) { - for (const chat of response.value as Chat[]) { + // Cap protects the 60s sandbox timeout and Graph throttling on /chats?$expand=members. + const { items, error } = await paginateGraphList({ + client, + initialUrl: '/chats', + expand: 'members', + pageSize: CHATS_PAGE_SIZE, + maxItems: DROPDOWN_LIST_MAX, + }); + + if (error && items.length === 0) { + return { + disabled: true, + placeholder: "Couldn't load chats — please retry.", + options: [], + }; + } + + return { + disabled: false, + options: items.map((chat) => { const chatName = chat.topic ?? chat.members ?.filter((member: ConversationMember) => member.displayName) .map((member: ConversationMember) => member.displayName) .join(','); - options.push({ + return { label: `(${CHAT_TYPE[chat.chatType! as keyof typeof CHAT_TYPE]} Chat) ${chatName || '(no title)'}`, value: chat.id!, - }); - } - if (response['@odata.nextLink']) { - response = await client.api(response['@odata.nextLink']).get(); - } else { - break; - } - } - return { - disabled: false, - options: options, + }; + }), }; }, }), @@ -307,3 +325,6 @@ const CHAT_TYPE = { meeting: 'Meeting', unknownFutureValue: 'Unknown', }; + +const CHATS_PAGE_SIZE = 50; +const DROPDOWN_LIST_MAX = 500; diff --git a/packages/pieces/community/microsoft-teams/src/lib/triggers/new-channel-message.ts b/packages/pieces/community/microsoft-teams/src/lib/triggers/new-channel-message.ts index b1c62f8c52b..338afd548e2 100644 --- a/packages/pieces/community/microsoft-teams/src/lib/triggers/new-channel-message.ts +++ b/packages/pieces/community/microsoft-teams/src/lib/triggers/new-channel-message.ts @@ -6,7 +6,7 @@ import { TriggerStrategy, } from '@activepieces/pieces-framework'; import { microsoftTeamsCommon } from '../common'; -import { createGraphClient } from '../common/graph'; +import { createGraphClient, withGraphRetry } from '../common/graph'; import { PageCollection } from '@microsoft/microsoft-graph-client'; import { ChatMessage } from '@microsoft/microsoft-graph-types'; import dayjs from 'dayjs'; @@ -99,10 +99,9 @@ const polling: Polling + client.api(`/teams/${teamId}/channels/${channelId}/messages`).top(5).get(), + ); if (!isNil(response.value)) { messages.push(...response.value); @@ -115,7 +114,8 @@ const polling: Polling client.api(url).get()); const channelMessages = response.value as ChatMessage[]; if (Array.isArray(channelMessages)) { diff --git a/packages/pieces/community/microsoft-teams/src/lib/triggers/new-channel.ts b/packages/pieces/community/microsoft-teams/src/lib/triggers/new-channel.ts index 0b0d9da328d..433eba3c996 100644 --- a/packages/pieces/community/microsoft-teams/src/lib/triggers/new-channel.ts +++ b/packages/pieces/community/microsoft-teams/src/lib/triggers/new-channel.ts @@ -6,7 +6,7 @@ import { TriggerStrategy, } from '@activepieces/pieces-framework'; import { microsoftTeamsCommon } from '../common'; -import { createGraphClient } from '../common/graph'; +import { createGraphClient, withGraphRetry } from '../common/graph'; import { PageCollection } from '@microsoft/microsoft-graph-client'; import { Channel } from '@microsoft/microsoft-graph-types'; import dayjs from 'dayjs'; @@ -67,7 +67,9 @@ const polling: Polling + client.api(`/teams/${teamId}/channels${filter}`).get(), + ); while (response.value && response.value.length > 0) { for (const channel of response.value as Channel[]) { @@ -76,8 +78,9 @@ const polling: Polling client.api(nextLink).get()); } else { break; } diff --git a/packages/pieces/community/microsoft-teams/src/lib/triggers/new-chat-message.ts b/packages/pieces/community/microsoft-teams/src/lib/triggers/new-chat-message.ts index 7e2b1a1f485..b36a0bb4c64 100644 --- a/packages/pieces/community/microsoft-teams/src/lib/triggers/new-chat-message.ts +++ b/packages/pieces/community/microsoft-teams/src/lib/triggers/new-chat-message.ts @@ -6,7 +6,7 @@ import { TriggerStrategy, } from '@activepieces/pieces-framework'; import { microsoftTeamsCommon } from '../common'; -import { createGraphClient } from '../common/graph'; +import { createGraphClient, withGraphRetry } from '../common/graph'; import { PageCollection } from '@microsoft/microsoft-graph-client'; import { ChatMessage } from '@microsoft/microsoft-graph-types'; import dayjs from 'dayjs'; @@ -94,10 +94,9 @@ const polling: Polling + client.api(`/chats/${chatId}/messages`).top(5).get(), + ); if (!isNil(response.value)) { messages.push(...(response.value as ChatMessage[])); @@ -109,7 +108,8 @@ const polling: Polling client.api(url).get()); const chatMessages = response.value as ChatMessage[]; if (Array.isArray(chatMessages)) { diff --git a/packages/pieces/community/microsoft-teams/src/lib/triggers/new-chat.ts b/packages/pieces/community/microsoft-teams/src/lib/triggers/new-chat.ts index a2bb643c432..a6cb4fe0cb3 100644 --- a/packages/pieces/community/microsoft-teams/src/lib/triggers/new-chat.ts +++ b/packages/pieces/community/microsoft-teams/src/lib/triggers/new-chat.ts @@ -6,7 +6,7 @@ import { TriggerStrategy, } from '@activepieces/pieces-framework'; import { isNil } from '@activepieces/shared'; -import { createGraphClient } from '../common/graph'; +import { createGraphClient, withGraphRetry } from '../common/graph'; import { PageCollection } from '@microsoft/microsoft-graph-client'; import { Chat, ChatType } from '@microsoft/microsoft-graph-types'; import dayjs from 'dayjs'; @@ -65,10 +65,9 @@ const polling: Polling + client.api(`/chats?${filter}`).get(), + ); while (response.value && response.value.length > 0) { for (const channel of response.value as Chat[]) { @@ -77,8 +76,9 @@ const polling: Polling client.api(nextLink).get()); } else { break; } diff --git a/packages/pieces/community/snowflake/package.json b/packages/pieces/community/snowflake/package.json index fbf3be5ce47..2efe3c7ef56 100644 --- a/packages/pieces/community/snowflake/package.json +++ b/packages/pieces/community/snowflake/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-snowflake", - "version": "0.3.1", + "version": "0.3.2", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/snowflake/src/lib/auth.ts b/packages/pieces/community/snowflake/src/lib/auth.ts index fda24a8b40f..f541125f8ff 100644 --- a/packages/pieces/community/snowflake/src/lib/auth.ts +++ b/packages/pieces/community/snowflake/src/lib/auth.ts @@ -54,10 +54,13 @@ CREATE SECURITY INTEGRATION "activepieces" OAUTH_CLIENT_TYPE = 'CONFIDENTIAL' OAUTH_REDIRECT_URI = 'https://cloud.activepieces.com/redirect' ENABLED = TRUE - OAUTH_ISSUE_REFRESH_TOKENS = TRUE; + OAUTH_ISSUE_REFRESH_TOKENS = TRUE + OAUTH_USE_SECONDARY_ROLES = IMPLICIT; \`\`\` > Replace the redirect URI with the one shown on your Activepieces OAuth connection page. +> +> \`OAUTH_USE_SECONDARY_ROLES = IMPLICIT\` lets the access token use any role granted to the user. Without it, the token is bound to the user's default Snowflake role and the **Default Role** field below will be ignored. --- @@ -85,7 +88,7 @@ Copy the **Account Identifier** (e.g. \`xy12345.us-east-1\` or \`orgname-account Enter the **Client ID** and **Client Secret** in the Activepieces OAuth2 settings for this connection, fill in the **Account Identifier** below, then click **Connect**.`, authUrl: 'https://{account}.snowflakecomputing.com/oauth/authorize', tokenUrl: 'https://{account}.snowflakecomputing.com/oauth/token-request', - scope: ['session:role-any', 'refresh_token'], + scope: ['refresh_token'], pkce: false, authorizationMethod: OAuth2AuthorizationMethod.BODY, required: true, diff --git a/packages/server/api/src/app/chat/chat-compaction.ts b/packages/server/api/src/app/chat/chat-compaction.ts new file mode 100644 index 00000000000..3ed1bd4db1d --- /dev/null +++ b/packages/server/api/src/app/chat/chat-compaction.ts @@ -0,0 +1,192 @@ +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { ActivepiecesError, AIProviderName, aiProviderUtils, ErrorCode } from '@activepieces/shared' +import { generateText, LanguageModel, ModelMessage } from 'ai' +import { FastifyBaseLogger } from 'fastify' + +const COMPACTION_THRESHOLD = 0.7 +const RECENT_WINDOW_RATIO = 0.3 +const CHARS_PER_TOKEN_ESTIMATE = 4 +const MIN_MESSAGES_BEFORE_COMPACTION = 6 +const ESTIMATED_TOKENS_PER_MESSAGE = 200 + +const COMPACTION_SYSTEM_PROMPT = readFileSync( + path.resolve('packages/server/api/src/assets/prompts/chat-compaction-prompt.md'), + 'utf8', +) + +function estimateTokenCount({ messages, systemPromptLength }: { + messages: ModelMessage[] + systemPromptLength: number +}): number { + const totalChars = JSON.stringify(messages).length + systemPromptLength + return Math.ceil(totalChars / CHARS_PER_TOKEN_ESTIMATE) +} + +function shouldCompact({ estimatedTokens, provider, messageCount }: { + estimatedTokens: number + provider: AIProviderName + messageCount: number +}): boolean { + if (messageCount < MIN_MESSAGES_BEFORE_COMPACTION) { + return false + } + const maxContext = aiProviderUtils.getMaxContextTokens({ provider }) + return estimatedTokens > maxContext * COMPACTION_THRESHOLD +} + +/** + * Anthropic requires that every tool_result has a preceding tool_use in the + * same context, so the cutoff must not split an assistant→tool pair. + */ +function snapToSafeMessageBoundary({ messages, rawCutoff }: { + messages: ModelMessage[] + rawCutoff: number +}): number { + let idx = Math.max(0, Math.min(rawCutoff, messages.length - 1)) + + while (idx > 0 && messages[idx].role === 'tool') { + idx-- + } + + return idx +} + +async function compactMessages({ messages, existingSummary, summarizedUpToIndex, provider, model, log }: { + messages: ModelMessage[] + existingSummary: string | null + summarizedUpToIndex: number | null + provider: AIProviderName + model: LanguageModel + log: FastifyBaseLogger +}): Promise<{ summary: string, summarizedUpToIndex: number }> { + const maxContext = aiProviderUtils.getMaxContextTokens({ provider }) + const targetRecentTokens = maxContext * RECENT_WINDOW_RATIO + const recentWindowSize = Math.min( + Math.max(2, Math.floor(targetRecentTokens / ESTIMATED_TOKENS_PER_MESSAGE)), + messages.length - 1, + ) + const rawCutoff = messages.length - recentWindowSize + const newCutoffIndex = snapToSafeMessageBoundary({ messages, rawCutoff }) + + const startIndex = summarizedUpToIndex ?? 0 + if (newCutoffIndex <= startIndex) { + return { summary: existingSummary ?? '', summarizedUpToIndex: startIndex } + } + const messagesToSummarize = messages.slice(startIndex, newCutoffIndex) + + let contentToSummarize = '' + if (existingSummary) { + contentToSummarize += `Previous conversation summary:\n${existingSummary}\n\nNew messages since last summary:\n` + } + + for (const msg of messagesToSummarize) { + const content = extractTextContent(msg) + if (content) { + contentToSummarize += `[${msg.role}]: ${content}\n` + } + } + + log.info({ + totalMessages: messages.length, + messagesToSummarize: messagesToSummarize.length, + newCutoffIndex, + recentWindowSize, + hadExistingSummary: !!existingSummary, + }, 'Compacting chat messages') + + const { text: summary } = await generateText({ + model, + system: COMPACTION_SYSTEM_PROMPT, + prompt: contentToSummarize, + }) + + return { summary, summarizedUpToIndex: newCutoffIndex } +} + +function buildCompactedPayload({ messages, summary, summarizedUpToIndex, provider }: { + messages: ModelMessage[] + summary: string | null + summarizedUpToIndex: number | null + provider: AIProviderName +}): ModelMessage[] { + if (!summary || summarizedUpToIndex === null) { + return messages + } + + const recentMessages = messages.slice(summarizedUpToIndex) + const summaryText = `[Previous conversation summary]\n${summary}\n[End of summary — conversation continues below]` + + const maxContext = aiProviderUtils.getMaxContextTokens({ provider }) + const threshold = maxContext * COMPACTION_THRESHOLD + const summaryCharLen = JSON.stringify(summaryText).length + const recentLengths = recentMessages.map((m) => JSON.stringify(m).length) + + let runningCharLen = summaryCharLen + recentLengths.reduce((a, b) => a + b, 0) + let startIdx = 0 + + while ( + startIdx < recentMessages.length - 1 + && (Math.ceil(runningCharLen / CHARS_PER_TOKEN_ESTIMATE) > threshold || recentMessages[startIdx].role === 'tool') + ) { + runningCharLen -= recentLengths[startIdx] + startIdx++ + } + + const trimmedRecent = recentMessages.slice(startIdx) + + // Anthropic rejects consecutive same-role messages, so merge the summary + // into the first message when it is already a 'user' turn. + const finalPayload: ModelMessage[] = trimmedRecent[0]?.role === 'user' + ? [ + { + ...trimmedRecent[0], + content: typeof trimmedRecent[0].content === 'string' + ? `${summaryText}\n\n${trimmedRecent[0].content}` + : [{ type: 'text' as const, text: summaryText }, ...trimmedRecent[0].content], + }, + ...trimmedRecent.slice(1), + ] + : [{ role: 'user', content: summaryText }, ...trimmedRecent] + const finalEstimate = Math.ceil(runningCharLen / CHARS_PER_TOKEN_ESTIMATE) + + if (finalEstimate > maxContext) { + throw new ActivepiecesError({ + code: ErrorCode.CHAT_CONTEXT_LIMIT_EXCEEDED, + params: {}, + }) + } + + return finalPayload +} + +function extractTextContent(message: ModelMessage): string { + if (typeof message.content === 'string') return message.content + if (!Array.isArray(message.content)) return '' + let text = '' + for (const part of message.content) { + if (typeof part === 'string') { + text += part + } + else if (typeof part === 'object' && part !== null && 'type' in part) { + if (part.type === 'text' && 'text' in part) { + text += String(part.text) + } + else if (part.type === 'tool-call' && 'toolName' in part) { + text += `[Tool call: ${String(part.toolName)}]` + } + else if (part.type === 'tool-result' && 'output' in part) { + const output = typeof part.output === 'string' ? part.output : JSON.stringify(part.output) + text += `[Tool result: ${output.slice(0, 500)}]` + } + } + } + return text +} + +export const chatCompaction = { + estimateTokenCount, + shouldCompact, + compactMessages, + buildCompactedPayload, +} diff --git a/packages/server/api/src/app/chat/chat-conversation-entity.ts b/packages/server/api/src/app/chat/chat-conversation-entity.ts index fae00c1b188..448f340b9f8 100644 --- a/packages/server/api/src/app/chat/chat-conversation-entity.ts +++ b/packages/server/api/src/app/chat/chat-conversation-entity.ts @@ -32,6 +32,14 @@ export const ChatConversationEntity = new EntitySchema ({ const newUserMessage: ModelMessage = { role: 'user' as const, content: userContent } const allMessages = [...previousMessages, newUserMessage] + const compactionState = await resolveCompactionState({ + conversation, + allMessages, + systemPromptLength: systemPrompt.length, + provider: providerConfig.provider, + model, + conversationId, + log, + }) + + const messagesForLlm = chatCompaction.buildCompactedPayload({ + messages: allMessages, + summary: compactionState.summary, + summarizedUpToIndex: compactionState.summarizedUpToIndex, + provider: providerConfig.provider, + }) + let pendingTitle = '' const localTools = createChatTools({ onSessionTitle: (title) => { @@ -162,7 +180,7 @@ export const chatService = (log: FastifyBaseLogger) => ({ const result = streamText({ model, system: systemPrompt, - messages: allMessages, + messages: messagesForLlm, tools, stopWhen: stepCountIs(MAX_STEPS), onStepFinish: ({ finishReason, usage }) => { @@ -234,6 +252,45 @@ async function resolveDefaultChatModel({ platformId, provider, log }: { }) } +async function resolveCompactionState({ conversation, allMessages, systemPromptLength, provider, model, conversationId, log }: { + conversation: ChatConversation + allMessages: ModelMessage[] + systemPromptLength: number + provider: AIProviderName + model: LanguageModel + conversationId: string + log: FastifyBaseLogger +}): Promise<{ summary: string | null, summarizedUpToIndex: number | null }> { + const summary = conversation.summary ?? null + const summarizedUpToIndex = conversation.summarizedUpToIndex ?? null + + const estimatedTokens = chatCompaction.estimateTokenCount({ + messages: allMessages, + systemPromptLength, + }) + + if (!chatCompaction.shouldCompact({ estimatedTokens, provider, messageCount: allMessages.length })) { + return { summary, summarizedUpToIndex } + } + + const result = await chatCompaction.compactMessages({ + messages: allMessages, + existingSummary: summary, + summarizedUpToIndex, + provider, + model, + log, + }) + + await conversationRepo().update(conversationId, { + summary: result.summary, + summarizedUpToIndex: result.summarizedUpToIndex, + }) + log.info({ conversationId, summarizedUpToIndex: result.summarizedUpToIndex }, 'Chat compaction completed') + + return result +} + async function connectMcpClient({ mcpCredentials, log }: { mcpCredentials: { mcpServerUrl: string | null, mcpToken: string | null } log: FastifyBaseLogger diff --git a/packages/server/api/src/app/database/migration/postgres/1786000000000-AddChatCompactionColumns.ts b/packages/server/api/src/app/database/migration/postgres/1786000000000-AddChatCompactionColumns.ts new file mode 100644 index 00000000000..cdecf8d4e9a --- /dev/null +++ b/packages/server/api/src/app/database/migration/postgres/1786000000000-AddChatCompactionColumns.ts @@ -0,0 +1,25 @@ +import { QueryRunner } from 'typeorm' +import { Migration } from '../../migration' + +export class AddChatCompactionColumns1786000000000 implements Migration { + name = 'AddChatCompactionColumns1786000000000' + breaking = false + release = '0.85.0' + transaction = true + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "chat_conversation" + ADD COLUMN "summary" text, + ADD COLUMN "summarizedUpToIndex" integer + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "chat_conversation" + DROP COLUMN IF EXISTS "summary", + DROP COLUMN IF EXISTS "summarizedUpToIndex" + `) + } +} diff --git a/packages/server/api/src/app/database/postgres-connection.ts b/packages/server/api/src/app/database/postgres-connection.ts index 5596158ab82..95b89b1a160 100644 --- a/packages/server/api/src/app/database/postgres-connection.ts +++ b/packages/server/api/src/app/database/postgres-connection.ts @@ -366,6 +366,7 @@ import { AddLastLoggedInPlatformIdToUserIdentity1777491000474 } from './migratio import { DropChatTokenColumns1782000000000 } from './migration/postgres/1782000000000-DropChatTokenColumns' import { AddUserSandboxTable1784000000000 } from './migration/postgres/1784000000000-AddUserSandboxTable' import { ReplacesSandboxWithVercelAiSdk1785000000000 } from './migration/postgres/1785000000000-ReplacesSandboxWithVercelAiSdk' +import { AddChatCompactionColumns1786000000000 } from './migration/postgres/1786000000000-AddChatCompactionColumns' const getSslConfig = (): boolean | TlsOptions => { const useSsl = system.get(AppSystemProp.POSTGRES_USE_SSL) @@ -747,6 +748,7 @@ export const getMigrations = (): (new () => Migration)[] => { AddUserSandboxTable1784000000000, AddLastLoggedInPlatformIdToUserIdentity1777491000474, ReplacesSandboxWithVercelAiSdk1785000000000, + AddChatCompactionColumns1786000000000, ] return migrations } diff --git a/packages/server/api/src/app/helper/error-handler.ts b/packages/server/api/src/app/helper/error-handler.ts index c7a3d3cee22..de97f95b17f 100644 --- a/packages/server/api/src/app/helper/error-handler.ts +++ b/packages/server/api/src/app/helper/error-handler.ts @@ -44,6 +44,7 @@ export const errorHandler = async ( [ErrorCode.DOES_NOT_MEET_BUSINESS_REQUIREMENTS]: StatusCodes.UNPROCESSABLE_ENTITY, [ErrorCode.FLOW_RUN_RETRY_OUTSIDE_RETENTION]: StatusCodes.GONE, [ErrorCode.SANDBOX_CAPACITY_EXCEEDED]: StatusCodes.TOO_MANY_REQUESTS, + [ErrorCode.CHAT_CONTEXT_LIMIT_EXCEEDED]: StatusCodes.BAD_REQUEST, } const statusCode = statusCodeMap[error.error.code] ?? StatusCodes.BAD_REQUEST diff --git a/packages/server/api/src/app/workers/job-queue/job-queue.ts b/packages/server/api/src/app/workers/job-queue/job-queue.ts index 41c26737540..dd95bb3e984 100644 --- a/packages/server/api/src/app/workers/job-queue/job-queue.ts +++ b/packages/server/api/src/app/workers/job-queue/job-queue.ts @@ -46,7 +46,7 @@ export const jobQueue = (log: FastifyBaseLogger) => ({ priority: JOB_PRIORITY[getDefaultJobPriority(data)], delay: params.delay, jobId: params.id, - removeOnFail: data.jobType === WorkerJobType.EVENT_DESTINATION, + ...(data.jobType === WorkerJobType.EVENT_DESTINATION ? { removeOnFail: true } : {}), ...isUserInteractionJob(data.jobType) ? { attempts: 1, removeOnComplete: { age: 300 }, diff --git a/packages/server/api/src/assets/prompts/chat-compaction-prompt.md b/packages/server/api/src/assets/prompts/chat-compaction-prompt.md new file mode 100644 index 00000000000..996a25953e9 --- /dev/null +++ b/packages/server/api/src/assets/prompts/chat-compaction-prompt.md @@ -0,0 +1,8 @@ +You are a conversation summarizer. Summarize the conversation below for context continuity. You MUST preserve: +- All user-stated facts, preferences, and decisions +- Names of entities, flows, pieces, and connections referenced +- Results of any tool calls (what was called and what it returned) +- The current task or question being worked on +- Any errors or issues encountered + +Output a concise context block using bullet points. Do NOT include pleasantries, greetings, or filler. Do NOT use narrative form. diff --git a/packages/server/api/test/unit/app/chat/chat-compaction.test.ts b/packages/server/api/test/unit/app/chat/chat-compaction.test.ts new file mode 100644 index 00000000000..f30f9644e5c --- /dev/null +++ b/packages/server/api/test/unit/app/chat/chat-compaction.test.ts @@ -0,0 +1,185 @@ +import { AIProviderName, ErrorCode } from '@activepieces/shared' +import { ModelMessage } from 'ai' +import { describe, expect, it } from 'vitest' +import { chatCompaction } from '../../../../src/app/chat/chat-compaction' + +function makeMessages(count: number, charsPer = 100): ModelMessage[] { + return Array.from({ length: count }, (_, i) => ({ + role: i % 2 === 0 ? 'user' as const : 'assistant' as const, + content: `Message ${i}: ${'x'.repeat(charsPer)}`, + })) +} + +describe('chatCompaction.estimateTokenCount', () => { + it('estimates tokens from message character length', () => { + const messages = makeMessages(2, 100) + const result = chatCompaction.estimateTokenCount({ messages, systemPromptLength: 0 }) + expect(result).toBeGreaterThan(0) + expect(result).toBe(Math.ceil(JSON.stringify(messages).length / 4)) + }) + + it('includes system prompt length in estimate', () => { + const messages = makeMessages(1) + const withoutSystem = chatCompaction.estimateTokenCount({ messages, systemPromptLength: 0 }) + const withSystem = chatCompaction.estimateTokenCount({ messages, systemPromptLength: 400 }) + expect(withSystem - withoutSystem).toBe(100) + }) + + it('returns 1 for empty messages with no system prompt', () => { + const result = chatCompaction.estimateTokenCount({ messages: [], systemPromptLength: 0 }) + expect(result).toBe(Math.ceil('[]'.length / 4)) + }) +}) + +describe('chatCompaction.shouldCompact', () => { + it('returns false when message count is below minimum', () => { + const result = chatCompaction.shouldCompact({ + estimatedTokens: 999_999, + provider: AIProviderName.ANTHROPIC, + messageCount: 5, + }) + expect(result).toBe(false) + }) + + it('returns false when tokens are below 70% of provider limit', () => { + // Anthropic has 200K context. 70% = 140K + const result = chatCompaction.shouldCompact({ + estimatedTokens: 100_000, + provider: AIProviderName.ANTHROPIC, + messageCount: 20, + }) + expect(result).toBe(false) + }) + + it('returns true when tokens exceed 70% of provider limit', () => { + // Anthropic has 200K context. 70% = 140K + const result = chatCompaction.shouldCompact({ + estimatedTokens: 150_000, + provider: AIProviderName.ANTHROPIC, + messageCount: 20, + }) + expect(result).toBe(true) + }) + + it('uses correct limits per provider', () => { + // Google has 1M context. 70% = ~700K. 150K is well below threshold. + const result = chatCompaction.shouldCompact({ + estimatedTokens: 150_000, + provider: AIProviderName.GOOGLE, + messageCount: 20, + }) + expect(result).toBe(false) + }) +}) + +describe('chatCompaction.buildCompactedPayload', () => { + it('returns messages as-is when no summary exists', () => { + const messages = makeMessages(10) + const result = chatCompaction.buildCompactedPayload({ + messages, + summary: null, + summarizedUpToIndex: null, + provider: AIProviderName.ANTHROPIC, + }) + expect(result).toBe(messages) + }) + + it('prepends summary and keeps only recent messages', () => { + const messages = makeMessages(10, 50) + const result = chatCompaction.buildCompactedPayload({ + messages, + summary: 'User discussed flow creation.', + summarizedUpToIndex: 7, + provider: AIProviderName.ANTHROPIC, + }) + + expect(result.length).toBe(4) // 1 summary + 3 recent (index 7,8,9) + expect(result[0].role).toBe('user') + expect(result[0].content).toContain('[Previous conversation summary]') + expect(result[0].content).toContain('User discussed flow creation.') + expect(result[1]).toBe(messages[7]) + expect(result[2]).toBe(messages[8]) + expect(result[3]).toBe(messages[9]) + }) + + it('trims recent messages if compacted payload still exceeds threshold', () => { + // Create messages with very large content so payload exceeds threshold + // Anthropic: 200K * 0.7 = 140K tokens = 560K chars + const largeMessages = Array.from({ length: 10 }, (_, i) => ({ + role: i % 2 === 0 ? 'user' as const : 'assistant' as const, + content: `Message ${i}: ${'x'.repeat(200_000)}`, + })) + + const result = chatCompaction.buildCompactedPayload({ + messages: largeMessages, + summary: 'Short summary.', + summarizedUpToIndex: 5, + provider: AIProviderName.ANTHROPIC, + }) + + // Should have trimmed some recent messages + expect(result.length).toBeLessThan(6) // less than 1 summary + 5 recent + expect(result[0].content).toContain('[Previous conversation summary]') + }) + + it('throws CHAT_CONTEXT_LIMIT_EXCEEDED when even minimal payload is too large', () => { + // Single message larger than the entire context window + const hugeMessages: ModelMessage[] = [{ + role: 'user', + content: 'x'.repeat(2_000_000), + }] + + expect(() => chatCompaction.buildCompactedPayload({ + messages: hugeMessages, + summary: 'Summary', + summarizedUpToIndex: 0, + provider: AIProviderName.ANTHROPIC, + })).toThrow(expect.objectContaining({ + error: expect.objectContaining({ + code: ErrorCode.CHAT_CONTEXT_LIMIT_EXCEEDED, + }), + })) + }) + + it('skips orphaned tool messages when trimming the recent window', () => { + const messages: ModelMessage[] = [ + { role: 'user', content: 'msg 0' }, + { role: 'assistant', content: 'msg 1' }, + { role: 'user', content: 'msg 2' }, + { role: 'assistant', content: [{ type: 'tool-call', toolCallId: 't1', toolName: 'myTool', args: {} }] }, + { role: 'tool', content: [{ type: 'tool-result', toolCallId: 't1', result: 'done' }] }, + { role: 'assistant', content: 'msg 5' }, + { role: 'user', content: 'msg 6' }, + { role: 'assistant', content: 'msg 7' }, + ] + + // summarizedUpToIndex=4 means recent window starts at the tool message + const result = chatCompaction.buildCompactedPayload({ + messages, + summary: 'Summary of earlier messages.', + summarizedUpToIndex: 4, + provider: AIProviderName.ANTHROPIC, + }) + + // First message should be summary, second should NOT be a tool message + expect(result[0].content).toContain('[Previous conversation summary]') + for (let i = 1; i < result.length; i++) { + if (i === 1) { + expect(result[i].role).not.toBe('tool') + } + } + }) + + it('does not trim when compacted payload fits within threshold', () => { + const messages = makeMessages(20, 50) + const result = chatCompaction.buildCompactedPayload({ + messages, + summary: 'Brief summary.', + summarizedUpToIndex: 15, + provider: AIProviderName.ANTHROPIC, + }) + + // 1 summary + 5 recent messages (index 15-19) + expect(result.length).toBe(6) + }) +}) diff --git a/packages/shared/package.json b/packages/shared/package.json index 44b4efae84c..944cd7f8a57 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/shared", - "version": "0.68.5", + "version": "0.68.6", "type": "commonjs", "sideEffects": false, "main": "./dist/src/index.js", diff --git a/packages/shared/src/lib/automation/chat/index.ts b/packages/shared/src/lib/automation/chat/index.ts index 7f24038b3c3..53862f0edb5 100644 --- a/packages/shared/src/lib/automation/chat/index.ts +++ b/packages/shared/src/lib/automation/chat/index.ts @@ -30,6 +30,8 @@ export const ChatConversation = z.object({ title: Nullable(z.string()), modelName: Nullable(z.string()), messages: z.array(z.record(z.string(), z.unknown())).default([]), + summary: Nullable(z.string()), + summarizedUpToIndex: Nullable(z.number().int()), }) export type ChatConversation = z.infer diff --git a/packages/shared/src/lib/core/common/activepieces-error.ts b/packages/shared/src/lib/core/common/activepieces-error.ts index e002a4d7934..3d933283a8b 100755 --- a/packages/shared/src/lib/core/common/activepieces-error.ts +++ b/packages/shared/src/lib/core/common/activepieces-error.ts @@ -68,6 +68,7 @@ export type ApErrorParams = | SessionExpiredParams | InvalidLicenseKeyParams | NoChatResponseParams + | ChatContextLimitExceededParams | InvalidSmtpCredentialsErrorParams | InvalidGitCredentialsParams | InvalidReleaseTypeParams @@ -144,6 +145,8 @@ export type SessionExpiredParams = BaseErrorParams> +export type ChatContextLimitExceededParams = BaseErrorParams> + export type EmailAuthIsDisabledParams = BaseErrorParams> export type AuthorizationErrorParams = BaseErrorParams< @@ -496,6 +499,7 @@ export enum ErrorCode { MACHINE_NOT_AVAILABLE = 'MACHINE_NOT_AVAILABLE', INVALID_CUSTOM_DOMAIN = 'INVALID_CUSTOM_DOMAIN', NO_CHAT_RESPONSE = 'NO_CHAT_RESPONSE', + CHAT_CONTEXT_LIMIT_EXCEEDED = 'CHAT_CONTEXT_LIMIT_EXCEEDED', ERROR_UPDATING_SUBSCRIPTION = 'ERROR_UPDATING_SUBSCRIPTION', AUTHENTICATION = 'AUTHENTICATION', AUTHORIZATION = 'AUTHORIZATION', diff --git a/packages/shared/src/lib/management/ai-providers/index.ts b/packages/shared/src/lib/management/ai-providers/index.ts index 854d3dad0f2..401b771b879 100644 --- a/packages/shared/src/lib/management/ai-providers/index.ts +++ b/packages/shared/src/lib/management/ai-providers/index.ts @@ -354,3 +354,24 @@ export function splitCloudflareGatewayModelId(modelId: string): { publisher: undefined, } } + +const DEFAULT_MAX_CONTEXT_TOKENS = 128_000 + +const PROVIDER_MAX_CONTEXT_TOKENS: Partial> = { + [AIProviderName.OPENAI]: 128_000, + [AIProviderName.ANTHROPIC]: 200_000, + [AIProviderName.GOOGLE]: 1_048_576, + [AIProviderName.BEDROCK]: 200_000, + [AIProviderName.AZURE]: 128_000, + [AIProviderName.OPENROUTER]: 128_000, + [AIProviderName.ACTIVEPIECES]: 200_000, +} + +function getMaxContextTokens({ provider }: { provider: AIProviderName | undefined }): number { + if (!provider) return DEFAULT_MAX_CONTEXT_TOKENS + return PROVIDER_MAX_CONTEXT_TOKENS[provider] ?? DEFAULT_MAX_CONTEXT_TOKENS +} + +export const aiProviderUtils = { + getMaxContextTokens, +} diff --git a/packages/web/src/components/prompt-kit/code-block.tsx b/packages/web/src/components/prompt-kit/code-block.tsx index 55e47372379..6419962b459 100644 --- a/packages/web/src/components/prompt-kit/code-block.tsx +++ b/packages/web/src/components/prompt-kit/code-block.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useState } from 'react'; -import { codeToHtml } from 'shiki'; +import { + bundledLanguages, + codeToTokens, + type BundledLanguage, + type ThemedToken, +} from 'shiki'; import { cn } from '@/lib/utils'; -export type CodeBlockProps = { - children?: React.ReactNode; - className?: string; -} & React.HTMLProps; - function CodeBlock({ children, className, ...props }: CodeBlockProps) { return (
; - function CodeBlockCode({ code, language = 'tsx', @@ -37,15 +30,17 @@ function CodeBlockCode({ className, ...props }: CodeBlockCodeProps) { - const [highlightedHtml, setHighlightedHtml] = useState(null); + const [tokenResult, setTokenResult] = useState(null); useEffect(() => { - async function highlight() { - if (!code) { - setHighlightedHtml('
'); - return; - } + if (!code) { + setTokenResult(null); + return; + } + + let cancelled = false; + async function highlight() { const themeOptions = theme ? { theme } : { @@ -53,21 +48,43 @@ function CodeBlockCode({ defaultColor: false as const, }; + const lang = isBundledLanguage(language) ? language : 'plaintext'; + + let result; try { - const html = await codeToHtml(code, { - lang: language, - ...themeOptions, - }); - setHighlightedHtml(html); + result = await codeToTokens(code, { lang, ...themeOptions }); } catch { - const html = await codeToHtml(code, { - lang: 'plaintext', - ...themeOptions, - }); - setHighlightedHtml(html); + if (lang === 'plaintext') return; + try { + result = await codeToTokens(code, { + lang: 'plaintext', + ...themeOptions, + }); + } catch { + return; + } } + + if (cancelled) return; + + setTokenResult({ + lines: result.tokens.map((line) => + line.map((token) => ({ + content: token.content, + style: getTokenStyle(token), + })), + ), + preStyle: + typeof result.rootStyle === 'string' + ? parseCssProperties(result.rootStyle) + : {}, + }); } + highlight(); + return () => { + cancelled = true; + }; }, [code, language, theme]); const classNames = cn( @@ -75,24 +92,38 @@ function CodeBlockCode({ className, ); - // SSR fallback: render plain code if not hydrated yet - return highlightedHtml ? ( -
- ) : ( + if (!tokenResult) { + return ( +
+
+          {code}
+        
+
+ ); + } + + const lastLineIndex = tokenResult.lines.length - 1; + + return (
-
-        {code}
+      
+        
+          {tokenResult.lines.map((line, lineIndex) => (
+            
+              {line.map((token, tokenIndex) => (
+                
+                  {token.content}
+                
+              ))}
+              {lineIndex < lastLineIndex ? '\n' : ''}
+            
+          ))}
+        
       
); } -export type CodeBlockGroupProps = React.HTMLAttributes; - function CodeBlockGroup({ children, className, @@ -108,4 +139,78 @@ function CodeBlockGroup({ ); } +const EMPTY_STYLE: React.CSSProperties = {}; +const FONT_STYLE_ITALIC = 1; +const FONT_STYLE_BOLD = 2; +const FONT_STYLE_UNDERLINE = 4; + +function getTokenStyle(token: ThemedToken): React.CSSProperties { + if (token.htmlStyle) { + const style: React.CSSProperties = {}; + for (const [key, value] of Object.entries(token.htmlStyle)) { + Object.assign(style, { [toCssPropertyKey(key)]: value }); + } + return style; + } + const style: React.CSSProperties = {}; + if (token.color) { + style.color = token.color; + } + if (token.fontStyle) { + if (token.fontStyle & FONT_STYLE_ITALIC) style.fontStyle = 'italic'; + if (token.fontStyle & FONT_STYLE_BOLD) style.fontWeight = 'bold'; + if (token.fontStyle & FONT_STYLE_UNDERLINE) + style.textDecoration = 'underline'; + } + if (!token.color && !token.fontStyle) return EMPTY_STYLE; + return style; +} + +function parseCssProperties(cssString: string): React.CSSProperties { + const style: React.CSSProperties = {}; + for (const part of cssString.split(';')) { + const colonIndex = part.indexOf(':'); + if (colonIndex === -1) continue; + const key = part.slice(0, colonIndex).trim(); + const value = part.slice(colonIndex + 1).trim(); + if (key && value) { + Object.assign(style, { [toCssPropertyKey(key)]: value }); + } + } + return style; +} + +function toCssPropertyKey(key: string): string { + if (key.startsWith('--')) return key; + return key.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); +} + +function isBundledLanguage(lang: string): lang is BundledLanguage { + return lang in bundledLanguages; +} + +type PrecomputedToken = { + content: string; + style: React.CSSProperties; +}; + +type TokenResult = { + lines: PrecomputedToken[][]; + preStyle: React.CSSProperties; +}; + +export type CodeBlockProps = { + children?: React.ReactNode; + className?: string; +} & React.HTMLProps; + +export type CodeBlockCodeProps = { + code: string; + language?: string; + theme?: string; + className?: string; +} & React.HTMLProps; + +export type CodeBlockGroupProps = React.HTMLAttributes; + export { CodeBlockGroup, CodeBlockCode, CodeBlock }; diff --git a/tsconfig.base.json b/tsconfig.base.json index 5376a6a2bd1..be234d24b35 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -119,6 +119,9 @@ "@activepieces/piece-autocalls": [ "packages/pieces/community/autocalls/src/index.ts" ], + "@activepieces/piece-avian": [ + "packages/pieces/community/avian/src/index.ts" + ], "@activepieces/piece-avoma": [ "packages/pieces/community/avoma/src/index.ts" ],